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

/**
* @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;
});