mirror of https://github.com/RubaXa/Ply.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2322 lines
48 KiB
2322 lines
48 KiB
/** |
|
* @author RubaXa <trash@rubaxa.org> |
|
* @licence MIT |
|
* Обязательно нужен JSON и Promise |
|
*/ |
|
|
|
/*global require, define, window*/ |
|
(function(factory ) { |
|
'use strict'; |
|
|
|
/* istanbul ignore next */ |
|
if (typeof define === 'function' && define.amd) { |
|
define(factory); |
|
} |
|
else { |
|
window['Ply'] = factory(); |
|
} |
|
})(function() { |
|
'use strict'; |
|
|
|
|
|
var gid = 1, |
|
noop = function() {}, |
|
|
|
env = window, |
|
document = env.document, |
|
setTimeout = env.setTimeout, |
|
|
|
lang = /** |
|
* @desc Языковые константы |
|
*/ |
|
|
|
{ |
|
ok: 'OK', |
|
cancel: 'Cancel', |
|
cross: '✖' |
|
}, |
|
support = /** |
|
* @desc Что поддерживает браузер |
|
*/ |
|
|
|
|
|
/*global _buildDOM*/ |
|
(function() { |
|
var props = {}, |
|
style = document.createElement('div').style, |
|
names = 'opacity transition transform perspective transformStyle transformOrigin backfaceVisibility'.split(' '), |
|
prefix = ['Webkit', 'Moz', 'O', 'MS'] |
|
; |
|
|
|
_each(names, function(name, i) { |
|
props[name] = (name in style) && /* istanbul ignore next */ name; |
|
|
|
/* istanbul ignore else */ |
|
if (!props[name]) { |
|
for (i = 0; i < 4; i++) { |
|
var pname = prefix[i] + name.charAt(0).toUpperCase() + name.substr(1); |
|
/* istanbul ignore else */ |
|
if (props[name] = (pname in style) && pname) { |
|
break; |
|
} |
|
} |
|
} |
|
}); |
|
|
|
return props; |
|
})(), |
|
|
|
/** |
|
* Коды кнопок |
|
* @type {Object} |
|
*/ |
|
keys = /** |
|
* Коды кнопок |
|
* @type {Object} |
|
*/ |
|
{ |
|
esc: 27 |
|
}, |
|
|
|
|
|
_plyAttr = 'data-ply' |
|
; |
|
|
|
|
|
// |
|
// $-like object |
|
// |
|
var $ = /** |
|
* @desc $-like adapter |
|
*/ |
|
window.jQuery |
|
|| /* istanbul ignore next */ window.Zepto |
|
|| /* istanbul ignore next */ window.ender |
|
|| /* istanbul ignore next */ window.$ |
|
|
|
|
|
|
|
// |
|
// Настройки по умолчанию |
|
// |
|
var _defaults = /** |
|
* @desc Настройки по умолчанию |
|
*/ |
|
|
|
{ |
|
clone: true, |
|
zIndex: 10000, |
|
rootTag: 'form', |
|
baseHtml: true, |
|
|
|
layer: {}, // css |
|
wrapper: {}, // css |
|
|
|
overlay: { |
|
opacity: .6, |
|
backgroundColor: 'rgb(0, 0, 0)' |
|
}, |
|
|
|
flags: { |
|
closeBtn: true, |
|
bodyScroll: false, |
|
closeByEsc: true, |
|
closeByOverlay: true, |
|
hideLayerInStack: true, |
|
visibleOverlayInStack: false |
|
}, |
|
|
|
// Callbacks |
|
oninit: noop, |
|
onopen: noop, |
|
onclose: noop, |
|
ondestroy: noop, |
|
onaction: noop |
|
} |
|
|
|
|
|
// |
|
// Обещания, Утилиты, CSS, DOM |
|
// |
|
/** |
|
* @desc «Обещания» |
|
*/ |
|
|
|
var NativePromise = window.Promise, |
|
Promise = window.Deferred || NativePromise |
|
; |
|
|
|
|
|
/** |
|
* Создать «Обещание» |
|
* @param {Function} executor |
|
* @returns {Promise} |
|
* @private |
|
*/ |
|
function _promise(executor) { |
|
/* istanbul ignore if */ |
|
if (Promise) { |
|
return new Promise(executor); |
|
} else { |
|
var dfd = $.Deferred(); |
|
executor(dfd.resolve, dfd.reject); |
|
return dfd; |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Дождаться разрешения всех «Обещаний» |
|
* @param {Promise[]} iterable |
|
* @returns {Promise} |
|
* @private |
|
*/ |
|
function _promiseAll(iterable) { |
|
return Promise |
|
? /* istanbul ignore next */ Promise.all(iterable) |
|
: $.when.apply($, iterable); |
|
} |
|
|
|
|
|
/** |
|
* Вернуть разрешенное «Обещание» |
|
* @param {*} [value] |
|
* @returns {Promise} |
|
* @private |
|
*/ |
|
function _resolvePromise(value) { |
|
return _promise(function(resolve ) {return resolve(value)}); |
|
} |
|
|
|
|
|
/** |
|
* Привести значение к «Обещанию» |
|
* @param {*} value |
|
* @returns {Promise} |
|
* @private |
|
*/ |
|
function _cast(value) { |
|
/* istanbul ignore next */ |
|
return value && value.then ? value : _resolvePromise(value); |
|
} |
|
|
|
|
|
|
|
// |
|
// Проверяем поддержку методы: done, fail, always |
|
// |
|
var __promise__ = _resolvePromise(); |
|
|
|
/* istanbul ignore next */ |
|
if (NativePromise && !__promise__.always) { |
|
Promise = function (executor) { |
|
var promise = new NativePromise(executor); |
|
promise.__proto__ = this.__proto__; |
|
return promise; |
|
}; |
|
|
|
Promise.prototype = Object.create(NativePromise.prototype); |
|
Promise.prototype.constructor = Promise; |
|
|
|
Promise.prototype.then = function (doneFn, failFn) { |
|
var promise = NativePromise.prototype.then.call(this, doneFn, failFn); |
|
promise.__proto__ = this.__proto__; // for FireFox |
|
return promise; |
|
}; |
|
|
|
Promise.prototype.done = function (callback) { |
|
this.then(callback); |
|
return this; |
|
}; |
|
|
|
Promise.prototype.fail = function (callback) { |
|
this['catch'](callback); |
|
return this; |
|
}; |
|
|
|
Promise.prototype.always = function (callback) { |
|
this.then(callback, callback); |
|
return this; |
|
}; |
|
|
|
['all', 'cast', 'reject', 'resolve'].forEach(function(name ) { |
|
Promise[name] = NativePromise[name]; |
|
}); |
|
} |
|
/** |
|
* @desc Вспомогательные методы |
|
*/ |
|
|
|
|
|
/** |
|
* Функция? |
|
* @param {*} fn |
|
* @returns {Boolean} |
|
*/ |
|
function isFn(fn) { |
|
return typeof fn === 'function'; |
|
} |
|
|
|
|
|
/** |
|
* Object iterator |
|
* @param {Object|Array} obj |
|
* @param {Function} iterator |
|
* @private |
|
*/ |
|
function _each(obj, iterator) { |
|
if (obj) { |
|
for (var key in obj) { |
|
/* istanbul ignore else */ |
|
if (obj.hasOwnProperty(key)) { |
|
iterator(obj[key], key, obj); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Глубокое клонирование |
|
* @param {*} obj |
|
* @returns {*} |
|
* @private |
|
*/ |
|
function _deepClone(obj) { |
|
var result = {}; |
|
|
|
_each(obj, function(val, key) { |
|
if (isFn(val)) { |
|
result[key] = val; |
|
} |
|
else if (val instanceof Object) { |
|
result[key] = _deepClone(val); |
|
} |
|
else { |
|
result[key] = val; |
|
} |
|
}); |
|
|
|
return result; |
|
} |
|
|
|
|
|
/** |
|
* Перенос свойств одного объект к другому |
|
* @param {Object} dst |
|
* @param {...Object} src |
|
* @returns {Object} |
|
* @private |
|
*/ |
|
function _extend(dst) {var SLICE$0 = Array.prototype.slice;var src = SLICE$0.call(arguments, 1); |
|
var i = 0, n = src.length; |
|
for (; i < n; i++) { |
|
_each(src[i], function(val, key) { |
|
dst[key] = val; |
|
}); |
|
} |
|
|
|
return dst; |
|
} |
|
/** |
|
* @desc Работа с DOM |
|
*/ |
|
|
|
|
|
/** |
|
* Разбор строки "tag#id.foo.bar" |
|
* @const {RegExp} |
|
*/ |
|
var R_SELECTOR = /^(\w+)?(#\w+)?((?:\.[\w_-]+)*)/i; |
|
|
|
|
|
|
|
/** |
|
* Выбрать элементы по заданному селектору |
|
* @param {String} selector |
|
* @param {HTMLElement} [ctx] |
|
* @returns {HTMLElement} |
|
*/ |
|
function _querySelector(selector, ctx) { |
|
try { |
|
return (ctx || document).querySelector(selector); |
|
} catch (err) { |
|
/* istanbul ignore next */ |
|
return $(selector, ctx)[0]; |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Найти элементы по имени |
|
* @param {HTMLElement} el |
|
* @param {String} name |
|
* @returns {NodeList} |
|
*/ |
|
function _getElementsByTagName(el, name) { |
|
return el.getElementsByTagName(name); |
|
} |
|
|
|
|
|
/** |
|
* Присоединить элемент |
|
* @param {HTMLElement} parent |
|
* @param {HTMLElement} el |
|
* @private |
|
*/ |
|
function _appendChild(parent, el) { |
|
try { |
|
parent && el && parent.appendChild(el); |
|
} catch (e) {} |
|
} |
|
|
|
|
|
/** |
|
* Удалить элемент |
|
* @param {HTMLElement} el |
|
* @private |
|
*/ |
|
function _removeElement(el) { |
|
/* istanbul ignore else */ |
|
if (el && el.parentNode) { |
|
el.parentNode.removeChild(el); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Добавить слуашетеля |
|
* @param {HTMLElement} el |
|
* @param {String} name |
|
* @param {Function} fn |
|
* @private |
|
*/ |
|
function _addEvent(el, name, fn) { |
|
var handle = fn.handle = fn.handle || (function(evt) { |
|
/* istanbul ignore if */ |
|
if (!evt.target) { |
|
evt.target = evt.srcElement || document; |
|
} |
|
|
|
/* istanbul ignore if */ |
|
if (evt.target.nodeType === 3) { |
|
evt.target = evt.target.parentNode; |
|
} |
|
|
|
/* istanbul ignore if */ |
|
if (!evt.preventDefault) { |
|
evt.preventDefault = function() { |
|
evt.returnValue = false; |
|
}; |
|
} |
|
|
|
/* istanbul ignore if */ |
|
if (!evt.stopPropagation) { |
|
evt.stopPropagation = function() { |
|
evt.cancelBubble = true; |
|
}; |
|
} |
|
|
|
fn.call(el, evt); |
|
}); |
|
|
|
/* istanbul ignore else */ |
|
if (el.addEventListener) { |
|
el.addEventListener(name, handle, false); |
|
} else { |
|
el.attachEvent('on' + name, handle); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Удалить слуашетеля |
|
* @param {HTMLElement} el |
|
* @param {String} name |
|
* @param {Function} fn |
|
* @private |
|
*/ |
|
function _removeEvent(el, name, fn) { |
|
var handle = fn.handle; |
|
if (handle) { |
|
/* istanbul ignore else */ |
|
if (el.removeEventListener) { |
|
el.removeEventListener(name, handle, false); |
|
} else { |
|
el.detachEvent('on' + name, handle); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
/** |
|
* Создание DOM структуры по спецификации |
|
* @param {String|Object|HTMLElement} [spec] |
|
* @returns {HTMLElement} |
|
* @private |
|
*/ |
|
function _buildDOM(spec) { |
|
if (spec == null) { |
|
spec = 'div'; |
|
} |
|
|
|
if (spec.appendChild) { |
|
return spec; |
|
} |
|
else if (spec.skip) { |
|
return document.createDocumentFragment(); |
|
} |
|
|
|
if (typeof spec === 'string') { // selector |
|
spec = { tag: spec }; |
|
} |
|
|
|
var el, |
|
children = spec.children, |
|
selector = R_SELECTOR.exec(spec.tag || '') |
|
; |
|
|
|
// Это нам больше не нужно |
|
delete spec.children; |
|
|
|
// Разбираем селектор |
|
spec.tag = selector[1] || 'div'; |
|
spec.id = spec.id || (selector[2] || '').substr(1); |
|
|
|
// Собираем className |
|
selector = (selector[3] || '').split('.'); |
|
selector[0] = (spec.className || ''); |
|
spec.className = selector.join(' '); |
|
|
|
// Создаем элемент, теперь можно |
|
el = document.createElement(spec.tag); |
|
delete spec.tag; |
|
|
|
// Определяем свойсва |
|
_each(spec, function(value, name) { |
|
if (value) { |
|
if (name === 'css') { |
|
// Определяем CSS свойства |
|
_css(el, spec.css); |
|
} |
|
else if (name === 'text') { |
|
(value != null) && _appendChild(el, document.createTextNode(value)); |
|
} |
|
else if (name === 'html') { |
|
(value != null) && (el.innerHTML = value); |
|
} |
|
else if (name === 'ply') { |
|
// Ply-аттрибут |
|
el.setAttribute(_plyAttr, value); |
|
} |
|
else if (name in el) { |
|
try { |
|
el[name] = value; |
|
} catch (e) { |
|
el.setAttribute(name, value); |
|
} |
|
} |
|
else if (/^data-/.test(name)) { |
|
el.setAttribute(name, value); |
|
} |
|
} |
|
}); |
|
|
|
// Детишки |
|
if (children && children.appendChild) { |
|
_appendChild(el, children); |
|
} |
|
else { |
|
_each(children, function(spec, selector) { |
|
if (spec) { |
|
if (typeof spec === 'string') { |
|
spec = { text: spec }; |
|
} |
|
else if (typeof spec !== 'object') { |
|
spec = {}; |
|
} |
|
|
|
/* istanbul ignore else */ |
|
if (typeof selector === 'string') { |
|
spec.tag = spec.tag || selector; |
|
} |
|
|
|
_appendChild(el, _buildDOM(spec)); |
|
} |
|
}); |
|
} |
|
|
|
return el; |
|
} |
|
|
|
|
|
/** |
|
* Выбрать первый не заполненый элемент |
|
* @param {HTMLElement} parentNode |
|
* @private |
|
*/ |
|
function _autoFocus(parentNode) { |
|
var items = _getElementsByTagName(parentNode, 'input'), |
|
i = 0, |
|
n = items.length, |
|
el, |
|
element |
|
; |
|
|
|
for (; i < n; i++) { |
|
el = items[i]; |
|
|
|
/* istanbul ignore else */ |
|
if (el.type === 'submit') { |
|
!element && (element = el); |
|
} |
|
else if (!/hidden|check|radio/.test(el.type) && el.value == '') { |
|
element = el; |
|
break; |
|
} |
|
} |
|
|
|
if (!element) { |
|
element = _getElementsByTagName(parentNode, 'button')[0]; |
|
} |
|
|
|
try { element.focus(); } catch (err) { } |
|
} |
|
|
|
|
|
/** |
|
* Предзагрузить все изображения |
|
* @param {HTMLElement} parentNode |
|
* @returns {Promise} |
|
* @private |
|
*/ |
|
function _preloadImage(parentNode) { |
|
_loading(true); |
|
|
|
return _promise(function(resolve) { |
|
var items = _getElementsByTagName(parentNode, 'img'), |
|
i = items.length, |
|
queue = i, |
|
img, |
|
complete = function() { |
|
/* istanbul ignore else */ |
|
if (--queue <= 0) { |
|
i = items.length; |
|
while (i--) { |
|
img = items[i]; |
|
_removeEvent(img, 'load', complete); |
|
_removeEvent(img, 'error', complete); |
|
} |
|
_loading(false); |
|
resolve(); |
|
} |
|
} |
|
; |
|
|
|
while (i--) { |
|
img = items[i]; |
|
if (img.complete) { |
|
queue--; |
|
} else { |
|
_addEvent(img, 'load', complete); |
|
_addEvent(img, 'error', complete); |
|
} |
|
} |
|
|
|
!queue && complete(); |
|
}); |
|
} |
|
/** |
|
* @desc Функция для работы с style |
|
*/ |
|
|
|
/*global support, document*/ |
|
|
|
|
|
/** |
|
* Хуки для css |
|
* @type {Object} |
|
*/ |
|
var _cssHooks = { |
|
opacity: !support.opacity && function (style, value) { |
|
style.zoom = 1; |
|
style.filter = 'alpha(opacity=' + (value * 100) + ')'; |
|
} |
|
}; |
|
|
|
|
|
|
|
/** |
|
* Установка или получение css свойства |
|
* @param {HTMLElement} el |
|
* @param {Object|String} prop |
|
* @param {String} [val] |
|
* @returns {*} |
|
* @private |
|
*/ |
|
function _css(el, prop, val) { |
|
if (el && el.style && prop) { |
|
if (prop instanceof Object) { |
|
for (var name in prop) { |
|
_css(el, name, prop[name]); |
|
} |
|
} |
|
else if (val === void 0) { |
|
/* istanbul ignore else */ |
|
if (document.defaultView && document.defaultView.getComputedStyle) { |
|
val = document.defaultView.getComputedStyle(el, ''); |
|
} |
|
else if (el.currentStyle) { |
|
val = el.currentStyle; |
|
} |
|
|
|
return prop === void 0 ? val : val[prop]; |
|
} else if (_cssHooks[prop]) { |
|
_cssHooks[prop](el.style, val); |
|
} else { |
|
el.style[support[prop] || prop] = val; |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
/** |
|
* «Загрузка» |
|
* @param {Boolean} state |
|
* @private |
|
*/ |
|
function _loading(state) { |
|
var el = _loading.get(); |
|
|
|
clearTimeout(_loading.pid); |
|
if (state) { |
|
_loading.pid = setTimeout(function() { |
|
_appendChild(document.body, el); |
|
}, 100); |
|
} else { |
|
_loading.pid = setTimeout(function() { |
|
_removeElement(el); |
|
}, 100); |
|
} |
|
} |
|
|
|
|
|
/** |
|
* Получить ссылку на элемент loading |
|
* @returns {HTMLElement} |
|
*/ |
|
_loading.get = function() { |
|
return _loading.el || (_loading.el = _buildDOM({ tag: '.ply-global-loading', children: { '.ply-loading-spinner': true } })); |
|
}; |
|
|
|
|
|
|
|
/** |
|
* Создать слой с контентом |
|
* @param {HTMLElement} contentEl |
|
* @param {Object} options |
|
* @returns {HTMLElement} |
|
* @private |
|
*/ |
|
function _createLayer(contentEl, options) { |
|
return _buildDOM({ |
|
css: _extend({ |
|
padding: '20px 20px 40px', // Сницу в два раза больше, так лучше |
|
display: 'inline-block', |
|
position: 'relative', |
|
textAlign: 'left', |
|
whiteSpace: 'normal', |
|
verticalAlign: 'middle', |
|
transform: 'translate3d(0, 0, 0)' |
|
}, options.wrapper), |
|
children: options.baseHtml ? [{ |
|
ply: ':layer', |
|
tag: '.ply-layer', |
|
className: options.mod, |
|
css: _extend({ |
|
overflow: 'hidden', |
|
position: 'relative', |
|
backfaceVisibility: 'hidden' |
|
}, options.layer), |
|
children: [options.flags.closeBtn && { |
|
ply: ':close', |
|
tag: '.ply-x', |
|
text: lang.cross |
|
}, { |
|
tag: '.ply-inside', |
|
children: contentEl |
|
}] |
|
}] : contentEl |
|
}); |
|
} |
|
|
|
|
|
/** |
|
* Создать затемнение |
|
* @param {Object} style |
|
* @returns {HTMLElement} |
|
* @private |
|
*/ |
|
function _createOverlay(style) { |
|
return _buildDOM({ |
|
ply: ':overlay', |
|
tag: '.ply-overlay', |
|
css: { |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
bottom: 0, |
|
position: 'fixed' |
|
}, |
|
children: [{ tag: 'div', css: _extend({ width: '100%', height: '100%' }, style) }] |
|
}); |
|
} |
|
|
|
|
|
/** |
|
* Создать ply—объвязку |
|
* @param {Object} target |
|
* @param {Object} options |
|
* @param {Boolean} [onlyLayer] |
|
* @returns {Object} |
|
* @private |
|
*/ |
|
function _createPly(target, options, onlyLayer) { |
|
// Корневой слой |
|
target.wrapEl = _buildDOM({ |
|
tag: options.rootTag, |
|
css: { whiteSpace: 'nowrap', zIndex: options.zIndex }, |
|
method: 'post', |
|
action: '/' |
|
}); |
|
|
|
|
|
// Затемнение |
|
if (!onlyLayer) { |
|
target.overlayEl = _createOverlay(options.overlay); |
|
target.overlayBoxEl = target.overlayEl.firstChild; |
|
_appendChild(target.wrapEl, target.overlayEl); |
|
} |
|
|
|
|
|
// Пустышка для центрирования по вертикали |
|
var dummyEl = _buildDOM(); |
|
_css(dummyEl, { |
|
height: '100%', |
|
display: 'inline-block', |
|
verticalAlign: 'middle' |
|
}); |
|
_appendChild(target.wrapEl, dummyEl); |
|
|
|
|
|
// Контент |
|
var el = options.el; |
|
target.el = (el && el.cloneNode) ? (options.clone ? el.cloneNode(true) : el) : _buildDOM({ html: el || '' }); |
|
|
|
|
|
// Содержит контент |
|
target.layerEl = _createLayer(target.el, options); |
|
target.contentEl = _getContentEl(target.layerEl); |
|
target.context = new Context(target.layerEl); |
|
|
|
_appendChild(target.wrapEl, target.layerEl); |
|
|
|
|
|
// Родительский элемент |
|
target.bodyEl = options.body && _querySelector(options.body) || document.body; |
|
|
|
|
|
target.wrapEl.tabIndex = -1; // для фокуса |
|
_css(target.wrapEl, { |
|
top: 0, |
|
left: 0, |
|
right: 0, |
|
bottom: 0, |
|
position: 'fixed', |
|
textAlign: 'center', |
|
overflow: 'auto', |
|
outline: 0 |
|
}); |
|
|
|
return target; |
|
} |
|
|
|
|
|
/** |
|
* Получить ссылку на контент |
|
* @param {HTMLElement} layerEl |
|
* @returns {HTMLElement} |
|
* @private |
|
*/ |
|
function _getContentEl(layerEl) { |
|
return layerEl.firstChild.lastChild.firstChild; |
|
} |
|
|
|
|
|
|
|
// |
|
// Основной код |
|
// |
|
|
|
|
|
|
|
|
|
/** |
|
* @class Ply |
|
* @param {HTMLElement|Object} el слой или опции |
|
* @param {Object} [options] опции слоя |
|
*/ |
|
function Ply(el, options) { |
|
options = (el instanceof Object) ? el : (options || {}); |
|
options.el = options.el || el; |
|
|
|
|
|
var _this = this; |
|
|
|
// Локальный идентификатор |
|
_this.cid = 'c' + gid++; |
|
|
|
|
|
// Увеличиваем глобальный zIndex |
|
_defaults.zIndex++; |
|
|
|
|
|
// Опции |
|
_this.options = options = _extend({}, _defaults, options); |
|
|
|
|
|
// Флаги |
|
options.flags = _extend({}, _defaults.flags, options.flags); |
|
|
|
|
|
// Создаем Ply-элементы |
|
_createPly(_this, options); |
|
|
|
|
|
// Установим эффекты |
|
_this.setEffect(options.effect); |
|
|
|
|
|
// Очередь эффектов |
|
_this.fx = function(executor) { |
|
/* jshint boss:true */ |
|
return !(_this.fx.queue = _this.fx.queue.then(executor, executor).then(function() { |
|
return _this; |
|
})); |
|
}; |
|
_this.fx.queue = _resolvePromise(); |
|
|
|
|
|
// Клик по затемнению |
|
_this.on('click', ':overlay', function() { |
|
_this.hasFlag('closeByOverlay') && _this.closeBy('overlay'); |
|
}); |
|
|
|
|
|
// Подписываемся кнопку «отмена» и «крестик» |
|
_this.on('click', ':close', function(evt, el) { |
|
evt.preventDefault(); |
|
_this.closeBy(el.nodeName === 'BUTTON' ? 'cancel' : 'x'); |
|
}); |
|
|
|
|
|
// Событие инициализации |
|
_this.options.oninit(this); |
|
} |
|
|
|
|
|
// Методы |
|
Ply.fn = Ply.prototype = /** @lends Ply.prototype */ { |
|
constructor: Ply, |
|
|
|
|
|
/** @private */ |
|
_activate: function () { |
|
if (!this.hasFlag('bodyScroll')) { |
|
var bodyEl = this.bodyEl, |
|
dummyEl = _buildDOM({ |
|
css: { overflow: 'auto', visibility: 'hidden', height: '5px' }, |
|
children: [{ tag: 'div', css: { height: '100px' } }] |
|
}) |
|
; |
|
|
|
// @todo: Покрыть тестами |
|
// Сохраняем оригинальные значения |
|
this.__overflow = _css(bodyEl, 'overflow'); |
|
this.__paddingRight = _css(bodyEl, 'paddingRight'); |
|
|
|
_appendChild(bodyEl, dummyEl); |
|
_css(bodyEl, { |
|
overflow: 'hidden', |
|
paddingRight: (dummyEl.offsetWidth - dummyEl.firstChild.offsetWidth) + 'px' |
|
}); |
|
_removeElement(dummyEl); |
|
} |
|
|
|
_addEvent(this.wrapEl, 'submit', this._getHandleEvent('submit')); |
|
}, |
|
|
|
|
|
/** @private */ |
|
_deactivate: function () { |
|
if (!this.hasFlag('bodyScroll')) { |
|
_css(this.bodyEl, { |
|
overflow: this.__overflow, |
|
paddingRight: this.__paddingRight |
|
}); |
|
} |
|
|
|
_removeEvent(this.layerEl, 'submit', this._getHandleEvent('submit')); |
|
}, |
|
|
|
|
|
/** |
|
* Получить обработчик события |
|
* @param {String} name событие |
|
* @returns {*} |
|
* @private |
|
*/ |
|
_getHandleEvent: function (name) { |
|
var _this = this, |
|
handleEvent = _this.__handleEvent || (_this.__handleEvent = {}) |
|
; |
|
|
|
if (!handleEvent[name]) { |
|
handleEvent[name] = function(evt) { |
|
_this._handleEvent(name, evt); |
|
}; |
|
} |
|
|
|
return handleEvent[name]; |
|
}, |
|
|
|
|
|
/** |
|
* Центральный обработчик события |
|
* @param {String} name |
|
* @param {Event} evt |
|
* @private |
|
*/ |
|
_handleEvent: function (name, evt) { |
|
evt.preventDefault(); |
|
this.closeBy(name); |
|
}, |
|
|
|
|
|
/** |
|
* jQuery выборка из слоя |
|
* @param {String} selector |
|
* @returns {jQuery} |
|
*/ |
|
$: function (selector) { |
|
return $(selector, this.layerEl); |
|
}, |
|
|
|
|
|
/** |
|
* Найти элемент внутри слоя |
|
* @param {String} selector |
|
* @returns {HTMLElement} |
|
*/ |
|
find: function (selector) { |
|
return _querySelector(selector, this.layerEl); |
|
}, |
|
|
|
|
|
/** |
|
* Применить эффект к элементу |
|
* @param {HTMLElement} el |
|
* @param {String} name |
|
* @param {String|Object} [effects] |
|
* @returns {Promise} |
|
*/ |
|
applyEffect: function (el, name, effects) { |
|
el = this[el] || el; |
|
|
|
if (!el.nodeType) { |
|
effects = name; |
|
name = el; |
|
el = this.layerEl; |
|
} |
|
|
|
effects = Ply.effects.get(effects || this.effects); |
|
return Ply.effects.apply.call(effects, el, name); |
|
}, |
|
|
|
|
|
/** |
|
* Закрыть «по» |
|
* @param {String} name прчина закрытия |
|
*/ |
|
closeBy: function (name) {var this$0 = this; |
|
var ui = { |
|
by: name, |
|
state: name === 'submit', |
|
data: this.context.toJSON(), |
|
widget: this, |
|
context: this.context |
|
}, |
|
el = this.el, |
|
result = this.options.onaction(ui) |
|
; |
|
|
|
if (!this.__lock) { |
|
this.__lock = true; |
|
this.el.className += ' ply-loading'; |
|
|
|
_cast(result) |
|
.done(function(state ) { |
|
if (state !== false) { |
|
this$0.result = ui; |
|
this$0.close(); |
|
} |
|
}) |
|
.always(function() { |
|
this$0.__lock = false; |
|
this$0.el.className = this$0.el.className.replace(/\s?ply-loading/, ''); |
|
}) |
|
; |
|
} |
|
}, |
|
|
|
|
|
/** |
|
* Подписаться на ply-событие |
|
* @param {String} event событие |
|
* @param {String} target ply-selector |
|
* @param {Function} handle |
|
* @returns {Ply} |
|
*/ |
|
on: function (event, target, handle) { |
|
var _this = this; |
|
|
|
if (!handle) { |
|
handle = target; |
|
target = ':layer'; |
|
} |
|
|
|
handle['_' + target] = function(evt) { |
|
var el = evt.target; |
|
|
|
do { |
|
if (el.nodeType === 1) { |
|
if (el.getAttribute(_plyAttr) === target) { |
|
return handle.call(_this, evt, el); |
|
} |
|
} |
|
} |
|
while ((el !== _this.wrapEl) && (el = el.parentNode)); |
|
}; |
|
|
|
_addEvent(_this.wrapEl, event, handle['_' + target]); |
|
return _this; |
|
}, |
|
|
|
|
|
/** |
|
* Отписаться от ply-событие |
|
* @param {String} event событие |
|
* @param {String} target ply-selector |
|
* @param {Function} handle |
|
* @returns {Ply} |
|
*/ |
|
off: function (event, target, handle) { |
|
if (!handle) { |
|
handle = target; |
|
target = 'layer'; |
|
} |
|
|
|
_removeEvent(this.wrapEl, event, handle['_' + target] || noop); |
|
return this; |
|
}, |
|
|
|
|
|
/** |
|
* Проверить наличие флага |
|
* @param {String} name имя флага |
|
* @returns {Boolean} |
|
*/ |
|
hasFlag: function (name) { |
|
return !!this.options.flags[name]; |
|
}, |
|
|
|
|
|
/** |
|
* Установить effect |
|
* @param {String|Object} name |
|
* @returns {Ply} |
|
*/ |
|
setEffect: function (name) { |
|
this.effects = Ply.effects.get(name); |
|
return this; |
|
}, |
|
|
|
|
|
/** |
|
* Открыть/закрыть слой |
|
* @param {Boolean} state |
|
* @param {*} effect |
|
* @returns {Promise} |
|
* @private |
|
*/ |
|
_toggleState: function (state, effect) { |
|
var _this = this, |
|
mode = state ? 'open' : 'close', |
|
prevLayer = Ply.stack.last |
|
; |
|
|
|
/* istanbul ignore else */ |
|
if (_this.visible != state) { |
|
_this.visible = state; |
|
_this[state ? '_activate' : '_deactivate'](); |
|
|
|
// Добавить или удалить слой из стека |
|
Ply.stack[state ? 'add' : 'remove'](_this); |
|
|
|
// Очередь эффектов |
|
_this.fx(function() { |
|
return _preloadImage(_this.wrapEl).then(function() { |
|
var isFirst = Ply.stack.length === (state ? 1 : 0), |
|
hideLayer = prevLayer && prevLayer.hasFlag('hideLayerInStack'), |
|
hasOverlay = isFirst || _this.hasFlag('visibleOverlayInStack'); |
|
|
|
if (state) { |
|
// Убрать «затемнение» если мы не первые в стеке |
|
!hasOverlay && _removeElement(_this.overlayBoxEl); |
|
|
|
_appendChild(_this.bodyEl, _this.wrapEl); |
|
_this.wrapEl.focus(); |
|
_autoFocus(_this.layerEl); |
|
|
|
if (hideLayer) { |
|
// Скрыть слой «под» |
|
prevLayer.applyEffect('close.layer', effect).then(function() { |
|
_removeElement(prevLayer.layerEl); |
|
}); |
|
} |
|
} else if (prevLayer = Ply.stack.last) { |
|
// Слой мог быть скрыт, нужно вернуть его |
|
_appendChild(prevLayer.wrapEl, prevLayer.layerEl); |
|
prevLayer.hasFlag('hideLayerInStack') && prevLayer.applyEffect('open.layer', effect).then(function() { |
|
_autoFocus(prevLayer.el); // todo: нужен метод layer.autoFocus(); |
|
}); |
|
} |
|
|
|
// Применяем основные эффекты |
|
return _promiseAll([ |
|
_this.applyEffect(mode + '.layer', effect), |
|
hasOverlay && _this.applyEffect('overlayEl', mode + '.overlay', effect) |
|
]).then(function() { |
|
if (!state) { |
|
_removeElement(_this.wrapEl); |
|
_appendChild(_this.overlayEl, _this.overlayBoxEl); |
|
} |
|
// «Событие» open или close |
|
_this.options['on' + mode](_this); |
|
}); |
|
}); |
|
}); |
|
} |
|
|
|
return _this.fx.queue; |
|
}, |
|
|
|
|
|
/** |
|
* Открыть слой |
|
* @param {*} [effect] |
|
* @returns {Promise} |
|
*/ |
|
open: function (effect) { |
|
this.result = null; |
|
return this._toggleState(true, effect); |
|
}, |
|
|
|
|
|
/** |
|
* Закрыть слой |
|
* @param {*} [effect] |
|
* @returns {Promise} |
|
*/ |
|
close: function (effect) { |
|
return this._toggleState(false, effect); |
|
}, |
|
|
|
|
|
/** |
|
* @param {HTMLElement} closeEl |
|
* @param {HTMLElement} openEl |
|
* @param {Object} effects |
|
* @param {Function} prepare |
|
* @param {Function} [complete] |
|
* @returns {*} |
|
* @private |
|
*/ |
|
_swap: function (closeEl, openEl, effects, prepare, complete) { |
|
var _this = this; |
|
|
|
if (_this.visible) { |
|
_this.fx(function() { |
|
return _preloadImage(openEl).then(function() { |
|
prepare(); |
|
|
|
return _promiseAll([ |
|
_this.applyEffect(closeEl, 'close.layer', effects), |
|
_this.applyEffect(openEl, 'open.layer', effects) |
|
]).then(function() { |
|
_removeElement(closeEl); |
|
complete(); |
|
_this.wrapEl.focus(); |
|
_autoFocus(openEl); |
|
}); |
|
}); |
|
}); |
|
} else { |
|
complete(); |
|
} |
|
|
|
return _this.fx.queue; |
|
}, |
|
|
|
|
|
/** |
|
* Заменить слой |
|
* @param {Object} options |
|
* @param {Object} [effect] эффект замены |
|
* @returns {Promise} |
|
*/ |
|
swap: function (options, effect) { |
|
options = _extend({}, this.options, options); |
|
|
|
var _this = this, |
|
ply = _createPly({}, options, true), |
|
effects = (effect || options.effect) ? Ply.effects.get(effect || options.effect) : _this.effects, |
|
closeEl = _this.layerEl, |
|
openEl = ply.layerEl |
|
; |
|
|
|
return _this._swap(closeEl, openEl, effects, |
|
function() { |
|
_appendChild(_this.bodyEl, _this.wrapEl); |
|
_appendChild(_this.bodyEl, ply.wrapEl); |
|
}, |
|
function() { |
|
_removeElement(ply.wrapEl); |
|
_appendChild(_this.wrapEl, openEl); |
|
|
|
_this.el = ply.el; |
|
_this.layerEl = openEl; |
|
_this.contentEl = _getContentEl(openEl); |
|
_this.context.el = openEl; |
|
}) |
|
; |
|
}, |
|
|
|
|
|
/** |
|
* Заменить внутренности слоя |
|
* @param {Object} options |
|
* @param {Object} [effect] эффект замены |
|
* @returns {Promise} |
|
*/ |
|
innerSwap: function (options, effect) { |
|
options = _extend({}, this.options, options); |
|
|
|
var _this = this, |
|
ply = _createPly({}, options, true), |
|
effects = (effect || options.effect) ? Ply.effects.get(effect || options.effect) : _this.effects, |
|
|
|
inEl = _querySelector('.ply-inside', ply.layerEl), |
|
outEl = _querySelector('.ply-inside', _this.layerEl) |
|
; |
|
|
|
return _this._swap(outEl, inEl, effects, function() { |
|
_css(outEl, { width: outEl.offsetWidth + 'px', position: 'absolute' }); |
|
_appendChild(outEl.parentNode, inEl); |
|
}, noop); |
|
}, |
|
|
|
|
|
/** |
|
* Уничтожить слой |
|
*/ |
|
destroy: function () { |
|
_removeElement(this.wrapEl); |
|
|
|
this._deactivate(); |
|
Ply.stack.remove(this); |
|
|
|
this.visible = false; |
|
this.options.ondestroy(this); |
|
} |
|
}; |
|
|
|
|
|
// |
|
// Ply-стек |
|
// |
|
/** |
|
* @desc Работы со стеком |
|
*/ |
|
|
|
var array_core = [], |
|
array_push = array_core.push, |
|
array_splice = array_core.splice |
|
; |
|
|
|
|
|
Ply.stack = { |
|
_idx: {}, |
|
|
|
|
|
/** |
|
* Последний Ply в стеке |
|
* @type {Ply} |
|
*/ |
|
last: null, |
|
|
|
|
|
/** |
|
* Длинна стека |
|
* @type {Number} |
|
*/ |
|
length: 0, |
|
|
|
|
|
/** |
|
* Удаить последний ply-слой из стека |
|
* @param {Event} evt |
|
* @private |
|
*/ |
|
_pop: function (evt) { |
|
var layer = Ply.stack.last; |
|
|
|
if (evt.keyCode === keys.esc && layer.hasFlag('closeByEsc')) { |
|
layer.closeBy('esc'); |
|
} |
|
}, |
|
|
|
|
|
/** |
|
* Добавить ply в стек |
|
* @param {Ply} layer |
|
*/ |
|
add: function (layer) { |
|
var idx = array_push.call(this, layer); |
|
|
|
this.last = layer; |
|
this._idx[layer.cid] = idx - 1; |
|
|
|
if (idx === 1) { |
|
_addEvent(document, 'keyup', this._pop); |
|
} |
|
}, |
|
|
|
|
|
/** |
|
* Удалить ply из стека |
|
* @param {Ply} layer |
|
*/ |
|
remove: function (layer) { |
|
var idx = this._idx[layer.cid]; |
|
|
|
if (idx >= 0) { |
|
array_splice.call(this, idx, 1); |
|
|
|
delete this._idx[layer.cid]; |
|
this.last = this[this.length-1]; |
|
|
|
if (!this.last) { |
|
_removeEvent(document, 'keyup', this._pop); |
|
} |
|
} |
|
} |
|
}; |
|
|
|
|
|
|
|
// |
|
// Эффекты |
|
// |
|
Ply.effects = /** |
|
* @desc Объект для работы с эффектами |
|
*/ |
|
|
|
{ |
|
// Установки по умолчанию |
|
defaults: { |
|
duration: 300, |
|
|
|
open: { |
|
layer: null, |
|
overlay: null |
|
}, |
|
|
|
close: { |
|
layer: null, |
|
overlay: null |
|
} |
|
}, |
|
|
|
|
|
/** |
|
* Настройти эффекты по умолчанию |
|
* @static |
|
* @param {Object} options |
|
*/ |
|
setup: function (options) { |
|
this.defaults = this.get(options); |
|
}, |
|
|
|
|
|
set: function (desc) { |
|
_extend(this, desc); |
|
}, |
|
|
|
|
|
/** |
|
* Получить опции на основе переданных и по умолчанию |
|
* @static |
|
* @param {Object} options опции |
|
* @returns {Object} |
|
*/ |
|
get: function (options) { |
|
var defaults = _deepClone(this.defaults); |
|
|
|
// Функция разбора выражения `name:duration[args]` |
|
function parseKey(key) { |
|
var match = /^([\w_-]+)(?::(\d+%?))?(\[[^\]]+\])?/.exec(key) || []; |
|
return { |
|
name: match[1] || key, |
|
duration: match[2] || null, |
|
args: JSON.parse(match[3] || 'null') || {} |
|
}; |
|
} |
|
|
|
|
|
function toObj(obj, key, def) { |
|
var fx = parseKey(key), |
|
val = (obj[fx.name] || def || {}), |
|
duration = (fx.duration || val.duration || obj.duration || options.duration) |
|
; |
|
|
|
if (typeof val === 'string') { |
|
val = parseKey(val); |
|
delete val.args; |
|
} |
|
|
|
if (/%/.test(val.duration)) { |
|
val.duration = parseInt(val.duration, 10) / 100 * duration; |
|
} |
|
|
|
val.duration = (val.duration || duration) | 0; |
|
|
|
return val; |
|
} |
|
|
|
|
|
if (typeof options === 'string') { |
|
var fx = parseKey(options); |
|
options = _deepClone(this[fx.name] || { open: {}, close: {} }); |
|
options.duration = fx.duration || options.duration; |
|
options.open.args = fx.args[0]; |
|
options.close.args = fx.args[1]; |
|
} |
|
else if (options instanceof Array) { |
|
var openFx = parseKey(options[0]), |
|
closeFx = parseKey(options[1]), |
|
open = this[openFx.name], |
|
close = this[closeFx.name] |
|
; |
|
|
|
options = { |
|
open: _deepClone(open && open.open || { layer: options[0], overlay: options[0] }), |
|
close: _deepClone(close && close.close || { layer: options[1], overlay: options[1] }) |
|
}; |
|
|
|
options.open.args = openFx.args[0]; |
|
options.close.args = closeFx.args[0]; |
|
} |
|
else if (!(options instanceof Object)) { |
|
options = {}; |
|
} |
|
|
|
options.duration = (options.duration || defaults.duration) | 0; |
|
|
|
for (var key in {open: 0, close: 0}) { |
|
var val = options[key] || defaults[key] || {}; |
|
if (typeof val === 'string') { |
|
// если это строка, то только layer |
|
val = { layer: val }; |
|
} |
|
val.layer = toObj(val, 'layer', defaults[key].layer); |
|
val.overlay = toObj(val, 'overlay', defaults[key].overlay); |
|
|
|
if(val.args === void 0){ |
|
// clean |
|
delete val.args; |
|
} |
|
|
|
options[key] = val; |
|
} |
|
|
|
return options; |
|
}, |
|
|
|
|
|
/** |
|
* Применить эффекты |
|
* @static |
|
* @param {HTMLElement} el элемент, к которому нужно применить эффект |
|
* @param {String} name название эффекта |
|
* @returns {Promise|undefined} |
|
*/ |
|
apply: function (el, name) { |
|
name = name.split('.'); |
|
|
|
var effects = this[name[0]], // эффекты open/close |
|
firstEl = el.firstChild, |
|
oldStyle = [el.getAttribute('style'), firstEl && firstEl.getAttribute('style')], |
|
fx, |
|
effect |
|
; |
|
|
|
|
|
if (support.transition && effects && (effect = effects[name[1]]) && (fx = Ply.effects[effect.name])) { |
|
if (fx['to'] || fx['from']) { |
|
// Клонируем |
|
fx = _deepClone(fx); |
|
|
|
// Выключаем анимацию |
|
_css(el, 'transition', 'none'); |
|
_css(firstEl, 'transition', 'none'); |
|
|
|
// Определяем текущее css-значения |
|
_each(fx['to'], function(val, key, target) { |
|
if (val === '&') { |
|
target[key] = _css(el, key); |
|
} |
|
}); |
|
|
|
// Выставляем initied значения |
|
if (isFn(fx['from'])) { |
|
fx['from'](el, effects.args); |
|
} else if (fx['from']) { |
|
_css(el, fx['from']); |
|
} |
|
|
|
return _promise(function(resolve) { |
|
// Принудительный repaint/reflow |
|
fx.width = el.offsetWidth; |
|
|
|
// Включаем анимацию |
|
_css(el, 'transition', 'all ' + effect.duration + 'ms'); |
|
_css(firstEl, 'transition', 'all ' + effect.duration + 'ms'); |
|
|
|
// Изменяем css |
|
if (isFn(fx['to'])) { |
|
fx['to'](el, effects.args); |
|
} |
|
else { |
|
_css(el, fx['to']); |
|
} |
|
|
|
// Ждем завершения анимации |
|
setTimeout(resolve, effect.duration); |
|
}).then(function() { |
|
el.setAttribute('style', oldStyle[0]); |
|
firstEl && firstEl.setAttribute('style', oldStyle[1]); |
|
}); |
|
} |
|
} |
|
|
|
return _resolvePromise(); |
|
} |
|
} |
|
Ply.effects.set(/** |
|
* @desc Предустановленные эффекты |
|
*/ |
|
|
|
{ |
|
// |
|
// Комбинированный эффекты |
|
// |
|
|
|
'fade': { |
|
open: { layer: 'fade-in:80%', overlay: 'fade-in:100%' }, |
|
close: { layer: 'fade-out:60%', overlay: 'fade-out:60%' } |
|
}, |
|
|
|
'scale': { |
|
open: { layer: 'scale-in', overlay: 'fade-in' }, |
|
close: { layer: 'scale-out', overlay: 'fade-out' } |
|
}, |
|
|
|
'fall': { |
|
open: { layer: 'fall-in', overlay: 'fade-in' }, |
|
close: { layer: 'fall-out', overlay: 'fade-out' } |
|
}, |
|
|
|
'slide': { |
|
open: { layer: 'slide-in', overlay: 'fade-in' }, |
|
close: { layer: 'slide-out', overlay: 'fade-out' } |
|
}, |
|
|
|
'3d-flip': { |
|
open: { layer: '3d-flip-in', overlay: 'fade-in' }, |
|
close: { layer: '3d-flip-out', overlay: 'fade-out' } |
|
}, |
|
|
|
'3d-sign': { |
|
open: { layer: '3d-sign-in', overlay: 'fade-in' }, |
|
close: { layer: '3d-sign-out', overlay: 'fade-out' } |
|
}, |
|
|
|
'inner': { |
|
open: { layer: 'inner-in' }, |
|
close: { layer: 'inner-out' } |
|
}, |
|
|
|
|
|
// |
|
// Описание эффекта |
|
// |
|
|
|
'fade-in': { |
|
'from': { opacity: 0 }, |
|
'to': { opacity: '&' } |
|
}, |
|
|
|
'fade-out': { |
|
'to': { opacity: 0 } |
|
}, |
|
|
|
'slide-in': { |
|
'from': { opacity: 0, transform: 'translateY(20%)' }, |
|
'to': { opacity: '&', transform: 'translateY(0)' } |
|
}, |
|
|
|
'slide-out': { |
|
'to': { opacity: 0, transform: 'translateY(20%)' } |
|
}, |
|
|
|
'fall-in': { |
|
'from': { opacity: 0, transform: 'scale(1.3)' }, |
|
'to': { opacity: '&', transform: 'scale(1)' } |
|
}, |
|
|
|
'fall-out': { |
|
'to': { opacity: 0, transform: 'scale(1.3)' } |
|
}, |
|
|
|
'scale-in': { |
|
'from': { opacity: 0, transform: 'scale(0.7)' }, |
|
'to': { opacity: '&', transform: 'scale(1)' } |
|
}, |
|
|
|
'scale-out': { |
|
'to': { opacity: 0, 'transform': 'scale(0.7)' } |
|
}, |
|
|
|
'rotate3d': function(el, opacity, axis, deg, origin) { |
|
_css(el, { perspective: '1300px' }); |
|
_css(el.firstChild, { |
|
opacity: opacity, |
|
transform: 'rotate' + axis + '(' + deg + 'deg)', |
|
transformStyle: 'preserve-3d', |
|
transformOrigin: origin ? '50% 0' : '50%' |
|
}); |
|
}, |
|
|
|
'3d-sign-in': { |
|
'from': function(el) { |
|
Ply.effects.rotate3d(el, 0, 'X', -60, '50% 0'); |
|
}, |
|
'to': function(el) { |
|
_css(el.firstChild, { opacity: 1, transform: 'rotateX(0)' }); |
|
} |
|
}, |
|
|
|
'3d-sign-out': { |
|
'from': function(el) { |
|
Ply.effects.rotate3d(el, 1, 'X', 0, '50% 0'); |
|
}, |
|
'to': function(el) { |
|
_css(el.firstChild, { opacity: 0, transform: 'rotateX(-60deg)' }); |
|
} |
|
}, |
|
|
|
'3d-flip-in': { |
|
'from': function(el, deg) { |
|
Ply.effects.rotate3d(el, 0, 'Y', deg || -70); |
|
}, |
|
'to': function(el) { |
|
_css(el.firstChild, { opacity: 1, transform: 'rotateY(0)' }); |
|
} |
|
}, |
|
|
|
'3d-flip-out': { |
|
'from': function(el) { |
|
Ply.effects.rotate3d(el, 1, 'Y', 0); |
|
}, |
|
'to': function(el, deg) { |
|
_css(el.firstChild, { opacity: 0, transform: 'rotateY(' + (deg || 70) + 'deg)' }); |
|
} |
|
}, |
|
|
|
'inner-in': { |
|
'from': function(el) { _css(el, 'transform', 'translateX(100%)'); }, |
|
'to': function(el) { _css(el, 'transform', 'translateX(0%)'); } |
|
}, |
|
|
|
'inner-out': { |
|
'from': function(el) { _css(el, 'transform', 'translateX(0%)'); }, |
|
'to': function(el) { _css(el, 'transform', 'translateX(-100%)'); } |
|
} |
|
}); |
|
|
|
|
|
|
|
// |
|
// Ply.Context |
|
// |
|
/** |
|
* @desc Ply-контекст |
|
*/ |
|
|
|
|
|
/** |
|
* @class Ply.Context |
|
* @param {HTMLElement} el |
|
*/ |
|
function Context(el) { |
|
/** |
|
* Корневой элемент |
|
* @type {HTMLElement} |
|
*/ |
|
this.el = el; |
|
} |
|
|
|
Context.fn = Context.prototype = /** @lends Ply.Context */{ |
|
constructor: Context, |
|
|
|
|
|
/** |
|
* Получить элемент по имени |
|
* @param {String} name |
|
* @returns {HTMLElement|undefined} |
|
*/ |
|
getEl: function (name) { |
|
if (this.el) { |
|
return _querySelector('[' + _plyAttr + '-name="' + name + '"]', this.el); |
|
} |
|
}, |
|
|
|
|
|
/** |
|
* Получить или установить значение по имени |
|
* @param {String} name |
|
* @param {String} [value] |
|
* @returns {String} |
|
*/ |
|
val: function (name, value) { |
|
var el = typeof name === 'string' ? this.getEl(name) : name; |
|
|
|
if (el && (el.value == null)) { |
|
el = _getElementsByTagName(el, 'input')[0] |
|
|| _getElementsByTagName(el, 'textarea')[0] |
|
|| _getElementsByTagName(el, 'select')[0] |
|
; |
|
} |
|
|
|
if (el && value != null) { |
|
el.value = value; |
|
} |
|
|
|
return el && el.value || ""; |
|
}, |
|
|
|
|
|
/** |
|
* Получить JSON |
|
* @returns {Object} |
|
*/ |
|
toJSON: function () { |
|
var items = this.el.querySelectorAll('[' + _plyAttr + '-name]'), |
|
json = {}, |
|
el, |
|
i = items.length |
|
; |
|
while (i--) { |
|
el = items[i]; |
|
json[el.getAttribute(_plyAttr + '-name')] = this.val(el); |
|
} |
|
return json; |
|
} |
|
}; |
|
|
|
|
|
// |
|
// Ply.ui |
|
// |
|
/** |
|
* @desc Диалоги |
|
*/ |
|
|
|
/*global Ply */ |
|
|
|
'use strict'; |
|
|
|
function _isNode(el) { |
|
return el && el.appendChild; |
|
} |
|
|
|
function _toBlock(block, name) { |
|
if (block == null) { |
|
return { skip: true }; |
|
} |
|
|
|
if (typeof block === 'string') { |
|
block = { text: block }; |
|
} |
|
|
|
if (typeof block === 'object') { |
|
block.name = block.name || name; |
|
} |
|
|
|
return block; |
|
} |
|
|
|
|
|
|
|
/** |
|
* Управление рендером UI |
|
* @param {String} name |
|
* @param {Object} [data] |
|
* @param {String} [path] |
|
* @returns {HTMLElement} |
|
*/ |
|
function ui(name, data, path) { |
|
var fn = ui[name], el; |
|
|
|
if (!fn) { |
|
name = name.split(/\s+/).slice(0, -1).join(' '); |
|
fn = data && ( |
|
ui[name + ' [name=' + data.name + ']'] |
|
|| ui[name + ' [type=' + data.type + ']'] |
|
) |
|
|| ui[name + ' *'] |
|
|| ui[':default']; |
|
} |
|
|
|
el = _buildDOM(fn(data, path)); |
|
if (data && data.name) { |
|
el.setAttribute(_plyAttr + '-name', data.name); |
|
} |
|
el.className += ' ply-ui'; |
|
|
|
return el; |
|
} |
|
|
|
|
|
/** |
|
* Назначение визуализатор |
|
* @param {String} name имя фабрики |
|
* @param {Function} renderer |
|
* @param {Boolean} [simpleMode] |
|
*/ |
|
ui.factory = function (name, renderer, simpleMode) { |
|
ui[name.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ')] = function (data, path) { |
|
var fragment = document.createDocumentFragment(), result; |
|
|
|
if ((data != null) || name === ':root') { |
|
data = simpleMode ? data : _toBlock(data); |
|
|
|
_each(simpleMode ? data : data.children, function (block, key) { |
|
var abs = ((path || name) + ' ' + key).replace(/^:\w+\s+/, ''), |
|
el = _isNode(block) ? block : ui(abs, _toBlock(block, key), abs) |
|
; |
|
|
|
_appendChild(fragment, el); |
|
}); |
|
|
|
if (!simpleMode) { |
|
delete data.children; |
|
} |
|
|
|
result = renderer(data, fragment); |
|
|
|
/* istanbul ignore else */ |
|
if (!_isNode(result)) { |
|
_extend(result, data); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
return fragment; |
|
}; |
|
}; |
|
|
|
|
|
// Элемент по умолчанию |
|
ui.factory(':default', function(data, children) { |
|
data.children = children; |
|
return data; |
|
}); |
|
|
|
|
|
// Ply-слой - корневой элемент |
|
ui.factory(':root', function (data) { |
|
return { |
|
tag: '.ply-form', |
|
className: data.mod, |
|
children: [ |
|
ui(':header', data.header), |
|
ui(':content', data.content), |
|
data.ctrls && ui(':default', { |
|
tag: 'div.ply-footer', |
|
children: data.ctrls |
|
}) |
|
] |
|
}; |
|
}); |
|
|
|
|
|
// «Заголовк» слоя |
|
ui.factory(':header', function (data, children) { |
|
return { tag: '.ply-header', text: data.text, children: children }; |
|
}); |
|
|
|
|
|
// «Содержимое» слоя |
|
ui.factory(':content', function (data, children) { |
|
return { tag: '.ply-content', children: children }; |
|
}, true); |
|
|
|
|
|
// Кнопка «ОК» |
|
ui.factory('ok', function (data) { |
|
return { |
|
ply: ':ok', |
|
tag: 'button.ply-ctrl.ply-ok', |
|
text: data === true ? lang.ok : data |
|
}; |
|
}); |
|
|
|
|
|
// Кнопка «Отмена» |
|
ui.factory('cancel', function (data) { |
|
return { |
|
ply: ':close', |
|
tag: 'button.ply-ctrl.ply-cancel', |
|
type: 'reset', |
|
text: data === true ? lang.cancel : data |
|
}; |
|
}); |
|
|
|
|
|
/** |
|
* Фабрика слоев |
|
* @param {String} name |
|
* @param {Function} renderer |
|
*/ |
|
function factory(name, renderer) { |
|
factory['_' + name] = renderer; |
|
|
|
factory[name] = function(options, data) { |
|
return _promise(function(resolve, reject) { |
|
renderer(options, data, resolve, reject); |
|
}).then(function(el) { |
|
/* istanbul ignore else */ |
|
if (!_isNode(el)) { |
|
el = ui(':root', el); |
|
} |
|
|
|
return el; |
|
}); |
|
}; |
|
} |
|
|
|
|
|
/** |
|
* Использовать фабрику |
|
* @param {String} name |
|
* @param {Object} options |
|
* @param {Object} data |
|
* @param {Function} resolve |
|
* @param {Function} [reject] |
|
*/ |
|
factory.use = function(name, options, data, resolve, reject) { |
|
factory['_' + name](options, data, resolve, reject); |
|
}; |
|
|
|
|
|
/** |
|
* Абстрактный диалог |
|
* @param {String} mod модификатор |
|
* @param {Object} options |
|
* @param {Object} data |
|
* @param {Object} defaults |
|
* @returns {Object} |
|
* @private |
|
*/ |
|
function _dialogFactory(mod, options, data, defaults) { |
|
options.mod = mod; |
|
options.effect = options.effect || 'slide'; |
|
options.flags = _extend({ closeBtn: false }, options.flags); |
|
|
|
return { |
|
header: data.title, |
|
content: data.form |
|
? { 'dialog-form': { children: data.form } } |
|
: { el: data.text || data }, |
|
ctrls: { |
|
ok: data.ok || defaults.ok, |
|
cancel: data.cancel === false ? null : (data.cancel || defaults.cancel) |
|
} |
|
}; |
|
} |
|
|
|
|
|
// Фабрика по умолчанию |
|
factory('default', function(options, data, resolve) { |
|
resolve(data || /* istanbul ignore next */ {}); |
|
}); |
|
|
|
|
|
// Базовый жиалог |
|
factory('base', function(options, data, resolve) { |
|
resolve(_dialogFactory('base', options, data)); |
|
}); |
|
|
|
|
|
// Диалог: «Предупреждение» |
|
factory('alert', function(options, data, resolve) { |
|
resolve(_dialogFactory('alert', options, data, { ok: true })); |
|
}); |
|
|
|
|
|
// Диалог: «Подтверждение» |
|
factory('confirm', function(options, data, resolve) { |
|
resolve(_dialogFactory('confirm', options, data, { |
|
ok: true, |
|
cancel: true |
|
})); |
|
}); |
|
|
|
|
|
// Диалог: «Запросить данные» |
|
factory('prompt', function(options, data, resolve) { |
|
resolve(_dialogFactory('prompt', options, data, { |
|
ok: true, |
|
cancel: true |
|
})); |
|
}); |
|
|
|
|
|
// Элемент формы |
|
ui.factory('dialog-form *', function(data) { |
|
return { |
|
tag: 'input.ply-input', |
|
type: data.type || 'text', |
|
name: data.name, |
|
value: data.value, |
|
required: true, |
|
placeholder: data.hint || data.text |
|
}; |
|
}); |
|
|
|
|
|
/** |
|
* Создать Ply-слой на основе фабрики |
|
* @param {String} name название фабрики |
|
* @param {Object} [options] опции |
|
* @param {Object} [data] данные для фабрики |
|
* @returns {Promise} |
|
*/ |
|
Ply.create = function(name, options, data) { |
|
if (_isNode(name)) { |
|
return _resolvePromise(new Ply(_extend(options || {}, { el: name }))); |
|
} |
|
|
|
if (!data) { |
|
data = options; |
|
options = {}; |
|
} |
|
|
|
var renderer = (factory[name] || factory['default']); |
|
return renderer(options, data).then(function(el) { |
|
return new Ply(_extend(options, { el: el })); |
|
}); |
|
}; |
|
|
|
|
|
/** |
|
* Открыть Ply-слой |
|
* @param {String} name |
|
* @param {Object} [options] |
|
* @param {Object} [data] |
|
* @returns {Promise} |
|
*/ |
|
Ply.open = function(name, options, data) { |
|
return Ply.create(name, options, data).then(function(layer) { |
|
return layer.open(); |
|
}); |
|
}; |
|
|
|
|
|
/** |
|
* Создать диалог или систему диалогов |
|
* @param {String|Object} name |
|
* @param {Object} [options] |
|
* @param {Object} [data] |
|
* @returns {Promise} |
|
*/ |
|
Ply.dialog = function(name, options, data) { |
|
if ((name instanceof Object) && !_isNode(name)) { |
|
options = options || /* istanbul ignore next */ {}; |
|
|
|
return _promise(function(resolve, reject) { |
|
var first = options.initState, |
|
current, |
|
rootLayer, |
|
interactive, |
|
stack = name, |
|
dialogs = {}, |
|
|
|
_progress = function(ui, layer) { |
|
(options.progress || /* istanbul ignore next */ noop)(_extend({ |
|
name: current.$name, |
|
index: current.$index, |
|
length: length, |
|
stack: stack, |
|
current: current, |
|
widget: layer |
|
}, ui), dialogs); |
|
}, |
|
|
|
changeLayer = function(spec, effect, callback) { |
|
// Клонирование данных |
|
var data = JSON.parse(JSON.stringify(spec.data)); |
|
|
|
current = spec; // текущий диалог |
|
interactive = true; // идет анимация |
|
(spec.prepare || noop)(data, dialogs); |
|
|
|
Ply.create(spec.ui || 'alert', spec.options || {}, data).then(function(layer) { |
|
var promise; |
|
|
|
if (rootLayer) { |
|
promise = rootLayer[/^inner/.test(effect) ? 'innerSwap' : 'swap'](layer, effect); |
|
} else { |
|
rootLayer = layer; |
|
promise = rootLayer.open(); |
|
} |
|
|
|
promise.then(function() { |
|
dialogs[spec.$name].el = rootLayer.layerEl; |
|
interactive = false; |
|
}); |
|
|
|
callback(rootLayer); |
|
}); |
|
} |
|
; |
|
|
|
|
|
var length = 0; |
|
_each(stack, function(spec, key) { |
|
first = first || key; |
|
spec.effects = spec.effects || {}; |
|
spec.$name = key; |
|
spec.$index = length++; |
|
dialogs[key] = new Ply.Context(); |
|
}); |
|
stack.$length = length; |
|
|
|
|
|
changeLayer(stack[first], null, function(layer) { |
|
_progress({}, layer); |
|
|
|
//noinspection FunctionWithInconsistentReturnsJS |
|
rootLayer.options.onaction = function(ui) { |
|
if (interactive) { |
|
return false; |
|
} |
|
|
|
var isNext = ui.state || (current.back === 'next'), |
|
swap = stack[current[isNext ? 'next' : 'back']] |
|
; |
|
|
|
// console.log(current.$name, stack[current[isNext ? 'next' : 'back']]); |
|
|
|
if (swap) { |
|
changeLayer(swap, current[isNext ? 'nextEffect' : 'backEffect'], function(layer) { |
|
_progress(ui, layer); |
|
}); |
|
|
|
return false; |
|
} else { |
|
(ui.state ? resolve : /* istanbul ignore next */ reject)(ui, dialogs); |
|
} |
|
}; |
|
}); |
|
}); |
|
} |
|
else { |
|
if (!data && !_isNode(name)) { |
|
data = options || {}; |
|
options = {}; |
|
} |
|
|
|
return Ply.open(name, options, data).then(function(layer) { |
|
return _promise(function(resolve, reject) { |
|
layer.options.onclose = function() { |
|
(layer.result.state ? resolve : reject)(layer.result); |
|
}; |
|
}); |
|
}); |
|
} |
|
}; |
|
|
|
|
|
// Export |
|
Ply.ui = ui; |
|
Ply.factory = factory; |
|
|
|
|
|
|
|
// |
|
// Export |
|
// |
|
Ply.lang = lang; |
|
Ply.css = _css; |
|
Ply.cssHooks = _cssHooks; |
|
|
|
Ply.keys = keys; |
|
Ply.noop = noop; |
|
Ply.each = _each; |
|
Ply.extend = _extend; |
|
Ply.promise = _promise; |
|
Ply.Promise = Promise; |
|
Ply.support = support; |
|
Ply.defaults = _defaults; |
|
Ply.attrName = _plyAttr; |
|
Ply.Context = Context; |
|
|
|
Ply.dom = { |
|
build: _buildDOM, |
|
append: _appendChild, |
|
remove: _removeElement, |
|
addEvent: _addEvent, |
|
removeEvent: _removeEvent |
|
}; |
|
|
|
|
|
Ply.version = '0.6.1'; |
|
|
|
return Ply; |
|
}); |