From 9d225a0ac8feef71ccde0040b06c4af4a6319193 Mon Sep 17 00:00:00 2001 From: RubaXa Date: Wed, 18 Jun 2014 08:36:43 +0400 Subject: [PATCH] * init --- .gitignore | 6 + Gruntfile.js | 66 ++ favicon.ico | Bin 0 -> 318 bytes index.html | 268 +++++++ package.json | 30 + ply.css | 138 ++++ src/Ply.es6 | 1538 ++++++++++++++++++++++++++++++++++++ src/Ply.ui.es6 | 405 ++++++++++ st/app.css | 149 ++++ tests/Ply.dom.tests.js | 115 +++ tests/Ply.effects.tests.js | 237 ++++++ tests/Ply.tests.js | 250 ++++++ tests/Ply.ui.tests.js | 131 +++ tests/index.html | 133 ++++ 14 files changed, 3466 insertions(+) create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 package.json create mode 100644 ply.css create mode 100644 src/Ply.es6 create mode 100644 src/Ply.ui.es6 create mode 100644 st/app.css create mode 100644 tests/Ply.dom.tests.js create mode 100644 tests/Ply.effects.tests.js create mode 100644 tests/Ply.tests.js create mode 100644 tests/Ply.ui.tests.js create mode 100644 tests/index.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc86718 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +temp +report +.DS_Store +node_modules +Ply.js +Ply.ui.js diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..4059f79 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,66 @@ +'use strict'; + +module.exports = function (grunt){ + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + es6transpiler: { + core: { + src: 'src/Ply.es6', + dest: 'Ply.js' + }, + ui: { + src: 'src/Ply.ui.es6', + dest: 'Ply.ui.js' + } + }, + + watch: { + scripts: { + files: 'src/*.es6', + tasks: ['es6transpiler'], + options: { interrupt: true } + } + }, + + qunit: { + all: ['tests/*.html'], + options: { + '--web-security': 'no', + coverage: { + src: ['Ply.js', 'Ply.ui.js'], + instrumentedFiles: 'temp/', + htmlReport: 'report/coverage', + coberturaReport: 'report/', + linesThresholdPct: 95, + statementsThresholdPct: 95, + functionsThresholdPct: 95, + branchesThresholdPct: 95 + } + } + }, + + uglify: { + options: { + banner: '/*! <%= pkg.exportName %> <%= pkg.version %> - <%= pkg.license %> | <%= pkg.repository.url %> */\n' + }, + dist: { + files: { + '<%= pkg.exportName %>.min.js': ['<%= pkg.exportName %>.js'] + } + } + } + }); + + + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-qunit-istanbul'); + grunt.loadNpmTasks('grunt-es6-transpiler'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + + + grunt.registerTask('es', ['es6transpiler']); + grunt.registerTask('build', ['es6transpiler', 'qunit']); + grunt.registerTask('min', ['build', 'uglify']); + grunt.registerTask('default', ['build']); +}; diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9b324ab3489c817537034afe423ab586b8ba65b7 GIT binary patch literal 318 zcmZQzU<5(|0RbS%!l1#(z#zuJz@P!d0zj+)#2|4HXaJKC0wf0lm6aD6{`~(1X8-^H zpJDOh#SEEyEgAOj-_LOE+BL8^K0+-G43d(K!N3eon}!A(Fff}X!)Tbip`b93iBM-6adNu9< literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..e22f74e --- /dev/null +++ b/index.html @@ -0,0 +1,268 @@ + + + + Ply — Amazing layer/modal/dialog system. Wow! + + + + + + + + + + + + + +

Ply

+ +
+
+

Alert

+ +
+
+
Hello %username%!
+ +
+
+ +
+ + Ply.dialog("alert", "Hello %username%!"); + +
+
+ +
+

Confirm

+ +
+
+
Continue?
+ +
+
+ +
+ + Ply.dialog( + "confirm", + { effect: "3d-sign" }, + "Continue?" + ); + +
+
+ +
+

Prompt

+ +
+
+
Spam subscribe
+
+ +
+ +
+
+ +
+ + Ply.dialog("prompt", { + title: "Spam subscribe", + form: { email: "E-mail" } + }); + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..86063f0 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "ply", + "exportName": "Ply", + "version": "0.3.0", + "devDependencies": { + "grunt": "*", + "grunt-contrib-watch": "*", + "grunt-qunit-istanbul": "*", + "grunt-es6-transpiler": "*", + "grunt-contrib-uglify": "*" + }, + "description": "Ply — Amazing layer/modal/dialog system. Wow!", + "main": "Ply.js", + "scripts": { + "test": "grunt" + }, + "repository": { + "type": "git", + "url": "git://github.com/rubaxa/Ply.git" + }, + "keywords": [ + "ply", + "layer", + "modal", + "dialog", + "lightbox" + ], + "author": "Konstantin Lebedev ", + "license": "MIT" +} diff --git a/ply.css b/ply.css new file mode 100644 index 0000000..bba86e9 --- /dev/null +++ b/ply.css @@ -0,0 +1,138 @@ +/* Loading */ +.ply-loading { + top: 50%; + left: 50%; + padding: 30px; + width: 60px; + height: 60px; + margin: -100px 0 0 -60px; + z-index: 100000; + position: fixed; + border-radius: 10%; + background-color: rgba(255,255,255,.5); + box-shadow: 0 1px 2px rgba(0,0,0,.2); +} + +.ply-loading-spinner { + width: 100%; + height: 100%; + opacity: .9; + background: #fff; + border-radius: 100%; + overflow: hidden; + position: relative; + box-shadow: 0 1px 3px rgba(0,0,0,.6); +} + +.ply-loading-spinner::before { + content: ""; + display: block; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + background: #333; + max-height: 0; + -webkit-animation: loading 3s normal infinite; + animation: loading 3s normal infinite; +} + +@keyframes loading { + 0% { max-height: 0; } + 50% { max-height: 100%; top: 0; } + 100% { max-height: 0; top: 120%; } +} + +@-webkit-keyframes loading { + 0% { max-height: 0; } + 50% { max-height: 100%; top: 0; } + 100% { max-height: 0; top: 120%; } +} + + +/* Layer */ +.ply-layer { + color: #333; + min-width: 280px; + box-shadow: 0 0 3px rgba(0,0,0,.3); + background-color: #fff; + border-radius: 2px; + font-family: "Arial", Helvetica; + font-size: 16px; +} + .ply-layer.alert .ply-content, + .ply-layer.confirm .ply-content { + padding: 40px 30px; + text-align: center; + } + + .ply-layer.alert .ply-footer, + .ply-layer.confirm .ply-footer, + .ply-layer.prompt .ply-footer { + text-align: center; + padding-bottom: 20px; + } + + +.ply-header { + padding: 10px 20px; + font-size: 18px; + background-color: #f1f1f1; + border-radius: 2px 2px 0 0; +} + +.ply-content { + padding: 20px; +} + +.ply-footer { + padding: 0 20px 15px; +} + .ply-footer button { + margin-left: 20px; + } + .ply-footer button:first-child { + margin-left: 0; + } + + +/* Controls */ +.ply-ok, +.ply-cancel { + color: #fff; + cursor: pointer; + border: 0; + outline: 0; + padding: 5px 20px; + box-shadow: 0 1px 1px rgba(0,0,0,.2); + background-color: #39C082; + border-radius: 3px; + font-size: 18px; +} +.ply-ok { + width: 100px; +} + +.ply-cancel { + background-color: #b2b2b2; +} + .ply-ok:focus, + .ply-cancel:focus { + box-shadow: 0 0 1px 2px rgba(255, 180, 0, .6); + } + + +/* Forms */ +.ply-input { + width: 100%; + border: 2px solid #ccc; + outline: 0; + padding: 5px 10px; + font-size: 16px; + font-family: "Arial", Helvetica; + box-sizing: border-box; +} + .ply-input:focus { + border-color: #39C082; + } diff --git a/src/Ply.es6 b/src/Ply.es6 new file mode 100644 index 0000000..84b3663 --- /dev/null +++ b/src/Ply.es6 @@ -0,0 +1,1538 @@ +/** + * @author RubaXa + * @licence MIT + * Обязательно нужен JSON и Promise + */ + +/*global define, window */ +((factory) => { + window['Ply'] = factory(window); +})((window) => { + 'use strict'; + + + var gid = 1, + noop = (() => {}), + document = window.document, + + $ = window.jQuery + || /* istanbul ignore next */ window.Zepto + || /* istanbul ignore next */ window.ender + || /* istanbul ignore next */ window.$, + + Promise = window.Promise, + + + /** + * Разбор строки "tag#id.foo.bar" + * @const {RegExp} + */ + R_SELECTOR = /^(\w+)?(#\w+)?((?:\.[\w_-]+)*)/i, + + + /** + * Поддерживаемые css-свойства + * @type {Object} + */ + support = (() => { + var props = {}, + style = _buildDOM().style, + names = 'transition transform perspective transformStyle transformOrigin backfaceVisibility'.split(' '), + prefix = ['Webkit', 'Moz', 'O', 'MS'] + ; + + _each(names, (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 = { + esc: 27 + }, + + + array_core = [], + array_push = array_core.push, + array_splice = array_core.splice, + + _plyAttr = 'data-ply' + ; + + + // + // Вспомогательные методы + // + + + /** + * Функция? + * @param {*} fn + * @returns {Boolean} + */ + function isFn(fn) { + return typeof fn === 'function'; + } + + + /** + * Создать «Обещание» + * @param {Function} executor + * @returns {Promise} + * @private + */ + function _promise(executor) { + /* istanbul ignore if */ + if (Promise) { + // @todo: Поткрыть тестами + return new Promise(executor); + } else { + var dfd = $.Deferred(); + executor(dfd.resolve, dfd.reject); + return dfd; + } + } + + + /** + * Дождаться разрешения всех «Обещаний» + * @param {Promise[]} iterable + * @returns {Promise} + * @private + */ + function _promiseAll(iterable) { + // @todo: Поткрыть тестами `Promise.all` + return Promise ? /* istanbul ignore next */ Promise.all(iterable) : $.when.apply($, iterable); + } + + + /** + * Выполнить действие в следующем тике + * @param {Function} fn + * @returns {Function} + * @private + */ + function _nextTick(fn) { + return () => { + setTimeout(fn, 1); + }; + } + + + /** + * 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, (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, ...src) { + var i = 0, n = src.length, key, obj; + for (; i < n; i++) { + _each(src[i], (val, key) => { + dst[key] = val; + }); + } + + return dst; + } + + + /** + * Выбрать элементы по заданному селектору + * @param {String} selector + * @returns {Array} + */ + function _querySelector(selector) { + try { + return document.querySelector(selector); + } catch (err) { + /* istanbul ignore next */ + return $(selector); + } + } + + + /** + * Найти элементы по имени + * @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) { + /* istanbul ignore else */ + if (parent && el) { + parent.appendChild(el); + } + } + + + /** + * Удалить элемент + * @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 || ((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 = () => { + evt.returnValue = false; + }; + } + + /* istanbul ignore if */ + if (!evt.stopPropagation) { + evt.stopPropagation = () => { + 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); + } + } + } + + + /** + * Установка или получение css свойства + * @param {HTMLElement} el + * @param {Object|String} prop + * @param {String} [val] + * @returns {*} + * @private + */ + function _css(el, prop, val) { + if (el && el.style) { + 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 { + el.style[support[prop] || prop] = val; + } + } + } + + + /** + * Создание DOM структуры по спецификации + * @param {Object} [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 || '') + ; + + // Разбираем селектор + 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) { + el[name] = value; + } + else if (/^data-/.test(name)) { + el.setAttribute(name, value); + } + } + }); + + // Детишки + if (children && children.appendChild) { + _appendChild(el, children); + } + else { + _each(children, (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((resolve) => { + var items = _getElementsByTagName(parentNode, 'img'), + i = items.length, + queue = i, + img, + complete = () => { + /* 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(); + }); + } + + + /** + * «Загрузка» + * @param {Boolean} state + * @private + */ + function _loading(state) { + var el = _loading.get(); + + clearTimeout(_loading.pid); + if (state) { + _loading.pid = setTimeout(() => { + _appendChild(document.body, el); + }, 100); + } else { + _loading.pid = setTimeout(() => { + _removeElement(el); + }, 100); + } + } + + + /** + * Получить ссылку на элемент loading + * @returns {HTMLElement} + */ + _loading.get = () => { + return _loading.el || (_loading.el = _buildDOM({ tag: '.ply-loading', children: { '.ply-loading-spinner': true } })); + }; + + + /** + * Создать лаер с контентом + * @param {HTMLElement} contentEl + * @param {Object} style стили лаера + * @returns {HTMLElement} + * @private + */ + function _createLayer(contentEl, style) { + var el = _buildDOM({ + css: { + padding: '20px 20px 40px', // Сницу в два раза больше, так лучше + display: 'inline-block', + position: 'relative', + overflow: 'hidden', + textAlign: 'left', + whiteSpace: 'normal', + verticalAlign: 'middle' + }, + children: contentEl + }); + + // Контент часть + style && _css(contentEl, style); + _css(contentEl, 'backfaceVisibility', 'hidden'); + + el.setAttribute(_plyAttr, 'layer'); + _appendChild(el, contentEl); + + return el; + } + + + /** + * Создать затемнение + * @param {Object} style + * @returns {HTMLElement} + * @private + */ + function _createOverlay(style) { + var el = _buildDOM(); + + if (style) { + _css(el, style); + _css(el, { + top: 0, + left: 0, + right: 0, + bottom: 0, + position: 'fixed' + }); + el.setAttribute(_plyAttr, 'overlay'); + } + + return el; + } + + + /** + * Создать ply—объвязку + * @param {Object} target + * @param {Object} options + * @param {Boolean} [onlyLayer] + * @returns {Object} + * @private + */ + function _createPly(target, options, onlyLayer) { + // Корневой слой + target.wrapEl = _buildDOM({ + css: { + whiteSpace: 'nowrap', + transform: 'translate3d(0, 0, 0)' + } + }); + + + // Затемнение + if (!onlyLayer) { + target.overlayEl = _createOverlay(options.overlay); + _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) ? el.cloneNode(true) : _buildDOM({ html: el || '' }); + + + // Содержит контент + target.layerEl = _createLayer(target.el, options.layer); + target.contentEl = target.layerEl.firstChild; + 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; + } + + + // + // Настройки по умолчанию + // + var defaults = { + layer: {}, + + overlay: { + opacity: .6, + backgroundColor: 'rgb(0, 0, 0)' + }, + + flags: { + bodyScroll: false, + closeByEsc: true, + closeByOverlay: true + }, + + lang: { + ok: 'OK', + cancel: 'Cancel' + } + }; + + + + + // + // Основной код + // + + + + + /** + * @class Ply + * @param {Object} options опции слоя + */ + function Ply(options) { + var _this = this; + + _this.cid = 'c' + gid++; + + + // Опции + _this.options = options = _extend(/** @lends this.options */{ + layer: defaults.layer, + overlay: defaults.overlay, + + // Callback's + init: noop, + open: noop, + close: noop, + destroy: noop, + callback: noop + }, options); + + + // Флаги + options.flags = _extend({}, defaults.flags, options.flags); + + + // Создаем Ply-элементы + _createPly(_this, options); + + + // Установим эффекты + _this.setEffect(options.effect); + + + // Очередь эффектов + _this.fx = { queue: _promise((resolve) => { resolve(); }) }; + _this.fx.add = (executor) => { + /* jshint boss:true*/ + return !(_this.fx.queue = _this.fx.queue.then(executor, executor).then(() => { + return _this; + })); + }; + + + // Подписываемся «контрол» «отмена» и «крестик» + _this.on('click', ':close', (evt, el) => { + evt.preventDefault(); + _this.closeBy(el.nodeName === 'BUTTON' ? 'cancel' : 'x'); + }); + + + // Событие инициализации + _this.options.init(this); + } + + + // Методы + Ply.fn = Ply.prototype = /** @lends Ply.prototype */ { + constructor: Ply, + + + /** + * Привязать события + * @private + */ + _bindEvents: function () { + if (this.hasFlag('closeByOverlay')) { + _addEvent(this.overlayEl, 'click', this._getHandleEvent('overlay')); + } + + _addEvent(this.wrapEl, 'submit', this._getHandleEvent('submit')); + }, + + + /** + * Отвязать события + * @private + */ + _unbindEvents: function () { + _removeEvent(this.layerEl, 'submit', this._getHandleEvent('submit')); + _removeEvent(this.overlayEl, 'click', this._getHandleEvent('overlay')); + }, + + + /** + * Получить обработчик события + * @param {String} name событие + * @returns {*} + * @private + */ + _getHandleEvent: function (name) { + var _this = this, handleEvent = _this.__handleEvent || (_this.__handleEvent = {}); + + if (!handleEvent[name]) { + handleEvent[name] = (evt) => { + _this._handleEvent(name, evt); + }; + } + + return handleEvent[name]; + }, + + + /** + * Центральный обработчик события + * @param {String} name + * @param {Event} evt + * @private + */ + _handleEvent: function (name, evt) { + evt.preventDefault(); + this.closeBy(name); + }, + + + /** + * Применить эффект к элементу + * @param {HTMLElement} el + * @param {String} name + * @param {String|Object} [effects] + * @returns {Promise} + * @private + */ + _applyEffect: function (el, name, effects) { + return Ply.effects.apply.call(effects || this.effects, el, name); + }, + + + /** + * Закрыть «по» + * @param {String} name прчина закрытия + */ + closeBy: function (name) { + var result = this.options.callback({ + by: name, + state: name === 'submit', + layer: this, + context: this.context + }); + + if (result !== false) { + this.close(); + } + }, + + + /** + * Подписаться на 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] = ((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; + }, + + + /** + * Открыть лаер + * @returns {Promise} + */ + open: function () { + var _this = this; + + /* istanbul ignore else */ + if (!_this.visible) { + _this.visible = true; + _this._bindEvents(); + + // Добавить лаер в stack + Ply.stack.add(_this); + + _this.fx.add(() => { + return _preloadImage(_this.wrapEl).then(() => { + _appendChild(_this.bodyEl, _this.wrapEl); + _this.wrapEl.focus(); + + _autoFocus(_this.layerEl); + _this.options.open(_this); + + return _promiseAll([ + _this._applyEffect(_this.overlayEl, 'open.overlay'), + _this._applyEffect(_this.layerEl, 'open.layer') + ]); + }); + }); + } + + return _this.fx.queue; + }, + + + /** + * Закрыть лаер + * @returns {Promise} + */ + close: function () { + var _this = this; + + /* istanbul ignore else */ + if (_this.visible) { + _this.visible = false; + _this._unbindEvents(); + + // Удалить лаер из stack + Ply.stack.remove(_this); + + _this.fx.add(() => { + return _promiseAll([ + _this._applyEffect(_this.overlayEl, 'close.overlay'), + _this._applyEffect(_this.layerEl, 'close.layer') + ]).then(() => { + _removeElement(_this.wrapEl); + _this.options.close(_this); + }); + }); + } + + return _this.fx.queue; + }, + + + /** + * Заменить лаер + * @param {Object} layer + * @param {Object} [effect] эффект замены + * @returns {Promise} + */ + swap: function (layer, effect) { + layer.layer = layer.layer || this.options.layer; + + var _this = this, + ply = _createPly({}, layer, true), + effects = (effect || layer.effect) ? Ply.effects.get(effect || layer.effect) : _this.effects, + doneFn = () => { + _removeElement(_this.layerEl); + _removeElement(ply.wrapEl); + _appendChild(_this.wrapEl, ply.layerEl); + + _this.el = ply.el; + _this.layerEl = ply.layerEl; + _this.contentEl = ply.layerEl.firstChild; + _this.context.el = _this.layerEl; + + _autoFocus(_this.layerEl); + } + ; + + + if (_this.visible) { + _this.fx.add(() => { + return _preloadImage(ply.layerEl).then(() => { + _appendChild(_this.bodyEl, _this.wrapEl); + _appendChild(_this.bodyEl, ply.wrapEl); + + return _promiseAll([ + _this._applyEffect(_this.layerEl, 'close.layer', effects), + _this._applyEffect(ply.layerEl, 'open.layer', effects) + ]).then(doneFn); + }); + }); + } else { + doneFn(); + } + + return _this.fx.queue; + }, + + + /** + * Уничтожить лаер + */ + destroy: function () { + _removeElement(this.wrapEl); + + this.visible = false; + this._unbindEvents(); + this.options.destroy(this); + } + }; + + + // Ply-стек + 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); + + delete this._idx[layer.cid]; + this.last = this[this.length-1]; + + if (!this.last) { + _removeEvent(document, 'keyup', this._pop); + } + } + } + }; + + + + // + // Эффекты + // + Ply.effects = { + // Установки по умолчанию + 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); + }, + + + + /** + * Получить опции на основе переданных и по умолчанию + * @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]; + } + + options = 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 (effects && (effect = effects[name[1]]) && (fx = Ply.effects[effect.name])) { // layer/overlay + if (fx['to'] || fx['from']) { + // Клонируем + fx = _deepClone(fx); + + // Выключаем анимацию + _css(el, 'transition', 'none'); + _css(firstEl, 'transition', 'none'); + + // Определяем текущее css-значения + _each(fx['to'], (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((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(_nextTick(() => { + // Возвращаем стили, именно на "then" с разрывом, т.к. «Обещания» могу быть ассинхронными + el.setAttribute('style', oldStyle[0]); + firstEl && firstEl.setAttribute('style', oldStyle[1]); + })); + } + } + }, + + + // + // Комбинированный эффекты + // + + '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': (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': (el) => { + Ply.effects.rotate3d(el, 0, 'X', -60, '50% 0'); + }, + 'to': (el) => { + _css(el.firstChild, { opacity: 1, transform: 'rotateX(0)' }); + } + }, + + '3d-sign-out': { + 'from': (el) => { + Ply.effects.rotate3d(el, 1, 'X', 0, '50% 0'); + }, + 'to': (el) => { + _css(el.firstChild, { opacity: 0, transform: 'rotateX(-60deg)' }); + } + }, + + '3d-flip-in': { + 'from': (el, args) => { + Ply.effects.rotate3d(el, 0, 'Y', args || -70); + }, + 'to': (el) => { + _css(el.firstChild, { opacity: 1, transform: 'rotateY(0)' }); + } + }, + + '3d-flip-out': { + 'from': (el) => { + Ply.effects.rotate3d(el, 1, 'Y', 0); + }, + 'to': (el, args) => { + _css(el.firstChild, { opacity: 0, transform: 'rotateY(' + (args || 70) + 'deg)' }); + } + }, + + 'inner-in': { + 'from': (el) => { _css(el, 'transform', 'translateX(100%)'); }, + 'to': (el) => { _css(el, 'transform', 'translateX(0%)'); } + }, + 'inner-out': { + 'from': (el) => { _css(el, 'transform', 'translateX(0%)'); }, + 'to': (el) => { _css(el, 'transform', 'translateX(-100%)'); } + } + }; + + + + // + // Ply-контекст + // + + + /** + * @class Ply.Context + * @param el + * @constructor + */ + function Context(el) { + this.el = el; + } + + Context.prototype = /** @lends Ply.Context */{ + constructor: Context, + + /** + * Получить элемент по имени + * @param {String} name + * @returns {HTMLElement|undefined} + */ + getEl: function (name) { + if (this.el) { + var items = _getElementsByTagName(this.el, '*'), i = items.length; + while (i--) { + if (items[i].getAttribute(_plyAttr + '-name') === name) { + return items[i]; + } + } + } + }, + + + /** + * Получить или установить значение по имени + * @param {String} name + * @param {String} [value] + * @returns {String} + */ + val: function (name, value) { + var el = this.getEl(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 || ""; + } + }; + + + + // Export + Ply.lang = { + ok: 'OK', + cancel: 'Cancel' + }; + + Ply.css = _css; + Ply.keys = keys; + Ply.noop = noop; + Ply.each = _each; + Ply.extend = _extend; + Ply.promise = _promise; + Ply.defaults = defaults; + Ply.attrName = _plyAttr; + Ply.Context = Context; + + Ply.dom = { + build: _buildDOM, + appendChild: _appendChild, + removeElement: _removeElement, + addEvent: _addEvent, + removeEvent: _removeEvent + }; + + return Ply; +}); diff --git a/src/Ply.ui.es6 b/src/Ply.ui.es6 new file mode 100644 index 0000000..42a2367 --- /dev/null +++ b/src/Ply.ui.es6 @@ -0,0 +1,405 @@ +/*global define, Ply */ +((factory) => { + factory(Ply); +})((Ply) => { + 'use strict'; + + + var _plyAttr = Ply.attrName, + noop = Ply.noop, + _each = Ply.each, + _extend = Ply.extend, + _promise = Ply.promise, + _buildDOM = Ply.dom.build, + _appendChild = Ply.dom.appendChild, + _lang = Ply.lang, + + _toBlock = (block, name) => { + if (block == null) { + return { skip: true }; + } + + if (typeof block === 'string') { + block = { text: block }; + } + + 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.trim().replace(/\s+/g, ' ')] = function (data, path) { + var fragment = document.createDocumentFragment(); + + 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+/, ''); + var el = ui(abs, _toBlock(block, key), abs); + + _appendChild(fragment, el); + }); + + if (!simpleMode) { + delete data.children; + } + + var result = renderer(data, fragment); + + /* istanbul ignore else */ + if (!result.appendChild) { + _extend(result, data); + } + + return result; + } + + return fragment; + }; + }; + + + // Элемент по умолчанию + ui.factory(':default', (data, children) => { + data.children = children; + return data; + }); + + + // Ply-слой - корневой элемент + ui.factory(':root', function (data) { + return { + tag: 'form.ply-layer', + 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-ok', + text: data === true ? _lang.ok : data + }; + }); + + + // Кнопка «Отмена» + ui.factory('cancel', function (data) { + return { + ply: ':close', + tag: 'button.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] = (options, data) => { + return _promise((resolve, reject) => { + renderer(options, data, resolve, reject); + }).then((el) => { + /* istanbul ignore else */ + if (!el.appendChild) { + el = ui(':root', el); + } + + return el; + }); + }; + } + + + /** + * Использовать фабрику + * @param {String} name + * @param {Object} options + * @param {Object} data + * @param {Function} resolve + * @param {Function} [reject] + */ + factory.use = (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.effect = options.effect || 'slide'; + + return { + mod: mod, + header: data.title, + content: data.form + ? { 'dialog-form': { children: data.form } } + : { el: data.text || data }, + ctrls: { + ok: data.ok || defaults.ok, + cancel: data.cancel || defaults.cancel + } + }; + } + + + // Фабрика по умолчанию + factory('default', (options, data, resolve) => { + resolve(data || /* istanbul ignore next */ {}); + }); + + + // Диалог: «Предупреждение» + factory('alert', (options, data, resolve) => { + resolve(_dialogFactory('alert', options, data, { ok: true })); + }); + + + // Диалог: «Подтверждение» + factory('confirm', (options, data, resolve) => { + resolve(_dialogFactory('confirm', options, data, { + ok: true, + cancel: true + })); + }); + + + // Диалог: «Запросить данные» + factory('prompt', (options, data, resolve) => { + resolve(_dialogFactory('prompt', options, data, { + ok: true, + cancel: true + })); + }); + + + // Элемент формы + ui.factory('dialog-form *', (data) => { + return { + tag: 'input.ply-input', + 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 = (name, options, data) => { + if (!data) { + data = options; + options = {}; + } + + var renderer = (factory[name] || factory['default']); + return renderer(options, data).then((el) => { + return new Ply(_extend(options, { el: el })); + }); + }; + + + /** + * Открыть Ply-слой + * @param {String} name + * @param {Object} [options] + * @param {Object} [data] + * @returns {Promise} + */ + Ply.open = (name, options, data) => { + return Ply.create(name, options, data).then((layer) => { + return layer.open(); + }); + }; + + + /** + * Создать диалог или систему диалогов + * @param {String|Object} name + * @param {Object} [options] + * @param {Object} [data] + * @returns {Promise} + */ + Ply.dialog = (name, options, data) => { + if (name instanceof Object) { + options = options || /* istanbul ignore next */ {}; + + return _promise((resolve, reject) => { + var first = options.initState, + current, + rootLayer, + stack = name, + dialogs = {}, + + _progress = (ui, layer) => { + (options.progress || /* istanbul ignore next */ noop)(_extend({ + name: current.$name, + index: current.$index, + length: length, + stack: stack, + current: current, + layer: layer + }, ui), dialogs); + }, + + changeLayer = (spec, effect, callback) => { + // Клонирование данных + var data = JSON.parse(JSON.stringify(spec.data)); + + current = spec; + (spec.prepare || noop)(data, dialogs); + + Ply.create(spec.ui || 'alert', spec.options || {}, data).then((layer) => { + var promise; + + if (rootLayer) { + promise = rootLayer.swap(layer, effect); + } else { + promise = layer.open(); + rootLayer = layer; + } + + promise.then(() => { + dialogs[spec.$name].el = rootLayer.layerEl; + }); + + callback(layer); + }); + } + ; + + + var length = 0; + _each(stack, (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, (layer) => { + _progress({}, layer); + + //noinspection FunctionWithInconsistentReturnsJS + rootLayer.options.callback = (ui) => { + var isNext = ui.state || (current.back === 'next'), + swap = isNext ? stack[current.next] : stack[current.back] + ; + + if (swap) { + changeLayer(swap, current[isNext ? 'nextEffect' : 'backEffect'], (layer) => { + _progress(ui, layer); + }); + + return false; + } else { + (ui.state ? resolve : /* istanbul ignore next */ reject)(ui, dialogs); + } + }; + }); + }); + } + else { + if (!data) { + data = options || {}; + options = {}; + } + + return Ply.open(name, options, data).then((layer) => { + return _promise((resolve) => { + layer.options.callback = resolve; + }); + }); + } + }; + + + // Export + Ply.ui = ui; + Ply.factory = factory; +}); diff --git a/st/app.css b/st/app.css new file mode 100644 index 0000000..e7a285b --- /dev/null +++ b/st/app.css @@ -0,0 +1,149 @@ +html { + background-color: #ac0; +} + +html, body { + height: 100%; +} + +.container { + margin: 0 auto; + width: 80%; + min-width: 600px; + max-width: 1200px; +} + +body, h1, h2 { + margin: 0; + padding: 0; +} + +h1, h2 { + color: #fff; + font-family: 'Lato', sans-serif; + font-weight: 300; + text-shadow: 0 1px 1px rgba(0,0,0,.2); +} + + +h1 { + text-shadow: 0 1px 3px rgba(0,0,0,.2); + font-size: 200px; + text-align: center; + margin-top: 50px; + margin-bottom: 50px; +} + h1 u { + cursor: pointer; + border-bottom: 8px dotted #fff; + text-decoration: none; + } + +h2 { + font-size: 30px; + margin-bottom: 10px; +} + + +.row { + margin-bottom: 30px; +} + .row:after { + clear: both; + content: ''; + display: block; + } + + .col-left { + float: left; + width: 35%; + padding: 0 20px; + box-sizing: content-box; + } + + .col-right { + float: left; + width: 55%; + margin-left: 5%; + } + + .example { + cursor: pointer; + opacity: 0.95; + position: relative; + } + .example:hover:after { + top: 50%; + left: 50%; + line-height: 0; + margin: -5px 0 0 -50px; + color: #333; + content: '►'; + font-size: 100px; + position: absolute; + display: block; + opacity: .7; + } + + .example:hover { + opacity: 1; + } + +pre code { + background-color: #fff; + box-shadow: 0 1px 1px rgba(0,0,0,.3); +} + + + + +/* Tomorrow Theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +.tomorrow-comment, pre .comment, pre .title { + color: #8e908c; +} + +.tomorrow-red, pre .variable, pre .attribute, pre .tag, pre .regexp, pre .ruby .constant, pre .xml .tag .title, pre .xml .pi, pre .xml .doctype, pre .html .doctype, pre .css .id, pre .css .class, pre .css .pseudo { + color: #c82829; +} + +.tomorrow-orange, pre .number, pre .preprocessor, pre .built_in, pre .literal, pre .params, pre .constant { + color: #f5871f; +} + +.tomorrow-yellow, pre .class, pre .ruby .class .title, pre .css .rules .attribute { + color: #eab700; +} + +.tomorrow-green, pre .string, pre .value, pre .inheritance, pre .header, pre .ruby .symbol, pre .xml .cdata { + color: #718c00; +} + +.tomorrow-aqua, pre .css .hexcolor { + color: #3e999f; +} + +.tomorrow-blue, pre .function, pre .python .decorator, pre .python .title, pre .ruby .function .title, pre .ruby .title .keyword, pre .perl .sub, pre .javascript .title, pre .coffeescript .title { + color: #4271ae; +} + +.tomorrow-purple, pre .keyword, pre .javascript .function { + color: #8959a8; +} + +pre { + border: 0; + margin: 0; + padding: 0; +} + +pre code { + display: block; + color: #4d4d4c; + font-size: 15px; + font-family: Menlo, Monaco, Consolas, monospace; + line-height: 1.5; + padding: 30px; +} diff --git a/tests/Ply.dom.tests.js b/tests/Ply.dom.tests.js new file mode 100644 index 0000000..e4139ce --- /dev/null +++ b/tests/Ply.dom.tests.js @@ -0,0 +1,115 @@ +(function (Ply) { + module('Ply.dom'); + + + function elementEqual(actual, expected, msg) { + msg = msg || 'el'; + expected.tagName = (expected.tagName || 'div').toUpperCase(); + + Ply.each(expected, function (value, attr) { + equal(actual[attr] || actual.getAttribute(attr), value, msg + '.' + attr); + }); + } + + + test('build()', function () { + elementEqual(Ply.dom.build(), { }); + }); + + + test('build("string")', function () { + Ply.each({ + 'div': { }, + 'div#xxx': { id: 'xxx' }, + 'div.foo': { className: ' foo' }, + 'div#xxx.foo': { id: 'xxx', className: ' foo' }, + 'b.foo.bar': { tagName: 'B', className: ' foo bar' }, + 'span#xxx.foo.bar': { tagName: 'SPAN', id: 'xxx', className: ' foo bar' }, + '#xxx': { id: 'xxx' }, + '#xxx.foo': { id: 'xxx', className: ' foo' }, + '#xxx.foo.bar': { id: 'xxx', className: ' foo bar' } + }, function (data, selector) { + elementEqual(Ply.dom.build(selector), data, selector); + }); + }); + + + test('build({ })', function () { + elementEqual(Ply.dom.build({ + id: 'baz', + tag: 'input.foo', + className: 'bar', + ply: 'baz', + 'data-prop': 'qux' + }), { + id: 'baz', + tagName: 'INPUT', + className: 'bar foo', + 'data-ply': 'baz', + 'data-prop': 'qux' + }, '{}'); + + elementEqual(Ply.dom.build({ text: 'foo' }), { innerHTML: '<b>foo</b>' }, 'text'); + elementEqual(Ply.dom.build({ html: 'bar' }), { innerHTML: 'bar' }, 'html'); + }); + + + test('build(element)', function () { + var el = Ply.dom.build(Ply.dom.build('b.foo')); + elementEqual(el, { tagName: 'b', className: ' foo' }); + }); + + + test('build({ children: {} })', function () { + var el = Ply.dom.build({ + tag: 'form', + children: { + 'input': { + type: 'password' + }, + 'hr': true, + 'br': false, + 'button': 'Enter' + } + }); + + equal(el.childNodes.length, 3); + + elementEqual(el, { tagName: 'form' }); + elementEqual(el.childNodes[0], { tagName: 'input', type: 'password' }); + elementEqual(el.childNodes[1], { tagName: 'hr' }); + elementEqual(el.childNodes[2], { tagName: 'button', innerHTML: 'Enter' }); + }); + + + test('build({ children: [] })', function () { + var el = Ply.dom.build({ + tag: 'form', + children: [ + { + tag: 'input', + type: 'checkbox', + disabled: true + }, + { tag: 'hr', skip: true }, + false && { tag: bar }, + { tag: 'button', text: "Enter" } + ] + }); + + equal(el.childNodes.length, 2); + + elementEqual(el, { tagName: 'form' }); + elementEqual(el.childNodes[0], { tagName: 'input', type: 'checkbox', disabled: true }); + elementEqual(el.childNodes[1], { tagName: 'button', innerHTML: 'Enter' }); + }); + + + test('build({ children: el })', function () { + var el = Ply.dom.build({ + children: Ply.dom.build({ tag: 'b', text: '!' }) + }); + + elementEqual(el, { innerHTML: '!' }); + }); +})(Ply); diff --git a/tests/Ply.effects.tests.js b/tests/Ply.effects.tests.js new file mode 100644 index 0000000..cc2f79b --- /dev/null +++ b/tests/Ply.effects.tests.js @@ -0,0 +1,237 @@ +(function (Ply) { + module('Ply.effects'); + + + test('core', function () { + Ply.effects.defaults = { duration: 300, open: {}, close: {} }; + + + deepEqual(Ply.effects.get(), { + open: { + layer: { duration: 300 }, + overlay: { duration: 300 } + }, + close: { + layer: { duration: 300 }, + overlay: { duration: 300 } + }, + duration: 300 + }, 'def'); + + + deepEqual(Ply.effects.get(['fade-in', 'fade-out']), { + open: { + layer: { name: 'fade-in', duration: 300 }, + overlay: { name: 'fade-in', duration: 300 } + }, + close: { + layer: { name: 'fade-out', duration: 300 }, + overlay: { name: 'fade-out', duration: 300 } + }, + duration: 300 + }, "['fade-in', 'fade-out']"); + + + deepEqual(Ply.effects.get(['fade-in', 'fade-out:100']), { + open: { + layer: { name: 'fade-in', duration: 300 }, + overlay: { name: 'fade-in', duration: 300 } + }, + close: { + layer: { name: 'fade-out', duration: 100 }, + overlay: { name: 'fade-out', duration: 100 } + }, + duration: 300 + }, "['fade-in', 'fade-out:100']"); + + + deepEqual(Ply.effects.get('fade:100'), { + open: { + layer: { name: 'fade-in', duration: 100 * 0.8 }, + overlay: { name: 'fade-in', duration: 100 } + }, + close: { + layer: { name: 'fade-out', duration: 100 * 0.6 }, + overlay: { name: 'fade-out', duration: 100 * 0.6 } + }, + duration: 100 + }, 'fade:100'); + + + deepEqual(Ply.effects.get({ + open: 'slide-in', + close: 'slide-out' + }), { + open: { + layer: { name: 'slide-in', duration: 300 }, + overlay: { duration: 300 } + }, + close: { + layer: { name: 'slide-out', duration: 300 }, + overlay: { duration: 300 } + }, + duration: 300 + }, 'slide-in-out'); + + + + Ply.effects.setup({ open: 'fade-in' }); + + + deepEqual(Ply.effects.get(), { + open: { + layer: { name: 'fade-in', duration: 300 }, + overlay: { duration: 300 } + }, + close: { + layer: { duration: 300 }, + overlay: { duration: 300 } + }, + duration: 300 + }, 'open.fade-in'); + + + Ply.effects.setup({ open: { overlay: 'fade-in' } }); + + + deepEqual(Ply.effects.get({ + open: 'slide-in', + close: 'slide-out' + }), { + open: { + layer: { name: 'slide-in', duration: 300 }, + overlay: { name: 'fade-in', duration: 300 } + }, + close: { + layer: { name: 'slide-out', duration: 300 }, + overlay: { duration: 300 } + }, + duration: 300 + }); + + + Ply.effects.setup('fade:400'); + + + deepEqual(Ply.effects.get({ + open: 'slide-in', + close: 'slide-out' + }), { + open: { + layer: { name: 'slide-in', duration: 400 }, + overlay: { name: 'fade-in', duration: 400 } + }, + close: { + layer: { name: 'slide-out', duration: 400 }, + overlay: { name: 'fade-out', duration: 400 * 0.6 } + }, + duration: 400 + }, 'fade:400'); + }); + + + test('effects:args', function () { + Ply.effects.defaults = { duration: 300, open: {}, close: {} }; + + deepEqual(Ply.effects.get('scale[0.5,0.3]'), { + open: { + args: 0.5, + layer: { name: 'scale-in', duration: 300 }, + overlay: { name: 'fade-in', duration: 300 } + }, + close: { + args: 0.3, + layer: { name: 'scale-out', duration: 300 }, + overlay: { name: 'fade-out', duration: 300 } + }, + duration: 300 + }); + + + deepEqual(Ply.effects.get(['scale["foo"]', 'fall[{"bar":"baz"}]']), { + open: { + args: 'foo', + layer: { name: 'scale-in', duration: 300 }, + overlay: { name: 'fade-in', duration: 300 } + }, + close: { + args: {bar:'baz'}, + layer: { name: 'fall-out', duration: 300 }, + overlay: { name: 'fade-out', duration: 300 } + }, + duration: 300 + }); + }); + + + test('stress', function () { + try { + Ply.effects.get(null); + Ply.effects.get(void 0); + Ply.effects.get(Math.random()); + Ply.effects.get('---'); + Ply.effects.get(123); + Ply.effects.get('\n'); + Ply.effects.get([null, null]); + Ply.effects.get([void 0, void 0]); + Ply.effects.get(['\n', '\t']); + ok(true); + } catch (err) { + equal([err, err.stack], null); + } + }); + + + promiseTest('fade', function () { + var log = { open: [], close: [] }, + type = 'open', + pid, i, + layer = new Ply({ effect: 'fade' }) + ; + + pid = setInterval(function () { + log[type].push( parseFloat(Ply.css(layer.overlayEl, 'opacity')) ); + }, 50); + + return layer.open().then(function () { + type = 'close'; + return layer.close(); + }).then(function () { + ok(log.open.length > 2, 'open'); + ok(log.close.length > 2, 'close'); + + for (i = 1; i < log.open.length; i++) { + if (log.open[i] < log.open[i-1]) { + equal(log.open, null, 'open: ' + i); + break; + } + } + + for (i = 1; i < log.close.length; i++) { + if (log.close[i] > log.close[i-1]) { + equal(log.close, null, 'close: ' + i); + break; + } + } + + clearInterval(pid); + }); + }); + + + promiseTest('other', function () { + expect(0); + + var queue = Ply.promise(function (resolve) { resolve() }); + + Ply.each('scale fall slide 3d-flip 3d-sign'.split(' '), function (name) { + queue = queue.then(function () { + return new Ply({ effect: name + ':50' }).open().then(function (layer) { + return layer.close(); + }); + }); + }); + + return queue; + }); +})(Ply); diff --git a/tests/Ply.tests.js b/tests/Ply.tests.js new file mode 100644 index 0000000..93886ad --- /dev/null +++ b/tests/Ply.tests.js @@ -0,0 +1,250 @@ +(function () { + module('Ply'); + + var layer; + + test('core', function () { + equal(typeof Ply, 'function'); + ok(new Ply instanceof Ply); + }); + + + function sleep(fn, ms) { + return Ply.promise(function (resolve) { + setTimeout(function () { + fn(); + resolve(); + }, ms); + }); + } + + + function checkVisiblity(layer, state, msg) { + equal(!!layer.visible, state, '[' + msg + '] visible: ' + state); + equal(!!layer.wrapEl.parentNode, state, msg + ' -> parentNode is ' + (state ? '' : 'not') + 'exists'); + } + + + test('options', function () { + layer = new Ply(); + + for (var key in Ply.defaults.overlay) { + equal(layer.overlayEl.style[key], Ply.defaults.overlay[key], key); + } + equal(layer.bodyEl, document.body, 'body'); + + // Body + layer = new Ply({ body: '#playground' }); + equal(layer.bodyEl, playground, '#playground'); + + // Overlay 1 + layer = new Ply({ overlay: { opacity: 1 } }); + equal(layer.overlayEl.style.opacity, 1, 'opacity: 1'); + equal(layer.overlayEl.style.backgroundColor, "", 'backgroundColor: ""'); + + // Overlay 2 + layer = new Ply({ overlay: { opacity: 1, backgroundColor: 'rgb(255, 0, 0)' } }); + equal(layer.overlayEl.style.opacity, 1, 'opacity: 1'); + equal(layer.overlayEl.style.backgroundColor, 'rgb(255, 0, 0)', 'backgroundColor: red'); + + // Overlay 3 + layer = new Ply({ overlay: null }); + notEqual(layer.overlayEl.style.position, 'fixed', 'overlay: null'); + + // Layer + layer = new Ply({ layer: { textAlign: 'center' } }); + equal(layer.contentEl.style.textAlign, 'center', 'textAlign: center'); + }); + + + test('flags', function () { + layer = new Ply(); + for (var key in Ply.defaults.flags) { + equal(layer.options.flags[key], Ply.defaults.flags[key], key); + } + + + layer = new Ply({ flags: { bodyScroll: true } }); + for (var key in Ply.defaults.flags) { + equal(layer.options.flags[key], key == 'bodyScroll' ? true : Ply.defaults.flags[key], key); + } + }); + + + test('content', function () { + var content = document.createElement('b'); + content.innerHTML = '!'; + + equal(new Ply().contentEl.innerHTML, ''); + equal(new Ply({ el: 'Wow!' }).contentEl.innerHTML, 'Wow!'); + equal(new Ply({ el: content }).layerEl.innerHTML, '!'); + }); + + + promiseTest('open/close', function () { + layer = new Ply({ el: 'Wow!' }); + + ok(!layer.wrapEl.parentNode, '!parent - open'); + ok(!layer.visible, 'visible: false'); + + return layer.open().then(function () { + var ratio = layer.wrapEl.offsetHeight / (layer.layerEl.offsetTop + layer.layerEl.offsetHeight/2); + + ok(layer.visible, 'visible: true'); + ok(layer.wrapEl.offsetWidth > 0, 'offsetWidth > 0'); + ok(Math.abs(2 - ratio) < 0.1, ratio, 'delat(' + ratio + ') < 0.1'); + + layer.close().then(function () { + ok(!layer.visible, 'visible: false - close'); + ok(!layer.wrapEl.parentNode, '!parent - close'); + }); + }); + }); + + + promiseTest('closeByEsc', function () { + function open(msg, esc) { + var layer = new Ply({ flags: { closeByEsc: esc }, effect: { duration: 1 } }); + + checkVisiblity(layer, false, msg); + + return layer.open().then(function () { + checkVisiblity(layer, true, msg); + return layer; + }); + } + + + return open('1. esc: true', true).then(function (escTrue) { + return open('2. esc: false', false).then(function (escFalse) { + simulateEvent(document, 'keyup', { keyCode: Ply.keys.esc }); + + checkVisiblity(escTrue, true, '3. esc: true'); + checkVisiblity(escFalse, true, '4. esc: false'); + + return escFalse.close().then(function () { + checkVisiblity(escTrue, true, '5. esc: true'); + checkVisiblity(escFalse, false, '6. esc: false'); + + simulateEvent(document, 'keyup', { keyCode: Ply.keys.esc }); + + return sleep(function () { + checkVisiblity(escTrue, false, '7. esc: true'); + checkVisiblity(escFalse, false, '8. esc: false'); + }, 50); + }); + }); + }); + }); + + + promiseTest('closeByOverlay', function () { + function test(msg, state, callback) { + var layer = new Ply({ flags: { closeByOverlay: state }, effect: { duration: 1 } }); + return layer.open().then(function () { + checkVisiblity(layer, true, msg); + simulateEvent(layer.overlayEl, 'click'); + + return sleep(function () { + callback(layer, msg); + }, 50); + }); + } + + + return test('closeByOverlay: true', true, function (layer, msg) { + checkVisiblity(layer, false, msg); + + return test('closeByOverlay: false', false, function (layer, msg) { + checkVisiblity(layer, true, msg); + return layer.close(); + }); + }) + }); + + + promiseTest('swap', function () { + return new Ply({ el: 1, effect: 'none:1' }).open().then(function (layer) { + return layer.swap({ el: 2 }).then(function () { + equal(layer.contentEl.innerHTML, 2); + + return layer.close().then(function () { + return layer.swap({ el: 3 }, 'none:2').then(function () { + equal(layer.contentEl.innerHTML, 3); + }); + }); + }); + }); + }); + + + promiseTest('destory', function () { + return new Ply({ effect: 'none:1' }).open().then(function (layer) { + checkVisiblity(layer, true, '#1'); + + layer.destroy(); + + checkVisiblity(layer, false, '#2'); + }); + }); + + + promiseTest('on/off', function () { + var log = [], + logMe = function (prefix, evt, el) { + log.push(prefix + ':' + el.tagName + '.' + evt.type + '->' + el.getAttribute('data-ply')); + }, + barHandle = function (evt, el) { + log.pop(); + logMe('bar', evt, el); + } + ; + + return new Ply({ el: 'foobar' }).open().then(function (layer) { + layer.on('click', function (evt, el) { + logMe('layer', evt, el); + }); + + layer.on('click', 'foo', function (evt, el) { + logMe('foo', evt, el); + }); + + layer.on('click', 'bar', barHandle); + + simulateEvent(layer.contentEl.getElementsByTagName('em')[0], 'click'); + simulateEvent(layer.contentEl.getElementsByTagName('em')[1], 'click'); + + layer.off('click', barHandle); + + simulateEvent(layer.contentEl.getElementsByTagName('em')[1], 'click'); + + layer.off('click', 'bar', barHandle); + + simulateEvent(layer.contentEl.getElementsByTagName('em')[1], 'click'); + + equal(log.join('\n'), [ + 'layer:DIV.click->layer', + 'foo:I.click->foo', + 'bar:B.click->bar', + 'bar:B.click->bar', + 'layer:DIV.click->layer' + ].join('\n')); + + return layer.close(); + }); + }); + + +// if (/state=2/.test(location)) { +// promiseTest('Promise', function () { +// expect(0); +// +// return Ply.promise(function (resolve) { +// window.resolveTest = function () { +// resolve(); +// }; +// document.write(''); +// }); +// }); +// } +})(); diff --git a/tests/Ply.ui.tests.js b/tests/Ply.ui.tests.js new file mode 100644 index 0000000..73048ff --- /dev/null +++ b/tests/Ply.ui.tests.js @@ -0,0 +1,131 @@ +(function (Ply) { + module('Ply.ui'); + + + promiseTest('dialog("unknown")', function () { + setTimeout(function () { + simulateEvent(Ply.stack.last.overlayEl, 'click'); + }, 50); + + return Ply.dialog('unknown').then(function (ui) { + equal(ui.by, 'overlay', 'ui.by'); + equal(ui.state, false, 'ui.state'); + }); + }); + + + promiseTest('dialog("alert")', function () { + setTimeout(function () { + var el = Ply.stack.last.wrapEl; + simulateEvent(el.getElementsByTagName('button')[0], 'click'); + }, 50); + + return Ply.dialog('alert', { effect: 'none:1' }, 'msg').then(function (ui) { + equal(ui.by, 'submit', 'ui.by'); + equal(ui.state, true, 'ui.state'); + }); + }); + + + promiseTest('dialog("confirm")', function () { + setTimeout(function () { + var el = Ply.stack.last.wrapEl; + simulateEvent(el.getElementsByTagName('button')[1], 'click'); + }, 50); + + return Ply.dialog('confirm', { effect: 'none:1' }, { + title: "???", + text: "!!!" + }).then(function (ui) { + equal(ui.by, 'cancel', 'ui.by'); + equal(ui.state, false, 'ui.state'); + }); + }); + + + promiseTest('dialog("prompt")', function () { + setTimeout(function () { + var el = Ply.stack.last.wrapEl; + Ply.stack.last.context.val('email', 'xx@yy.zz'); + simulateEvent(el.getElementsByTagName('button')[0], 'click'); + }, 50); + + return Ply.dialog('prompt', { effect: 'none:1' }, { + title: "???", + form: { email: "E-mail" } + }).then(function (ui) { + equal(ui.by, 'submit', 'ui.by'); + equal(ui.state, true, 'ui.state'); + equal(ui.context.val('email'), 'xx@yy.zz'); + }); + }); + + + promiseTest('dialog("confirm") with YES/NO', function () { + setTimeout(function () { + var el = Ply.stack.last.wrapEl; + simulateEvent(el.getElementsByTagName('button')[0], 'click'); + }, 50); + + return Ply.dialog("confirm", { effect: 'none:1' }, { ok: 'YES', cancel: 'NO' }).then(function (ui) { + equal(ui.layer.layerEl.getElementsByTagName('button')[0].innerHTML, 'YES', 'ok'); + equal(ui.layer.layerEl.getElementsByTagName('button')[1].innerHTML, 'NO', 'cancel'); + }); + }); + + + promiseTest('dialog({ steps })', function () { + var log = []; + + return Ply.dialog({ + 'foo': { + data: { text: 'foo' }, + options: { effect: 'none:1' }, + prepare: function (data) { + data.text += '!'; + }, + next: 'baz' + }, + 'bar': { + data: { text: 'bar' } + }, + 'baz': { + ui: 'confirm', + data: { text: 'baz' }, + back: 'bar' + } + }, { + progress: function (ui) { + log.push(ui.name + ':' + ui.state); + + setTimeout(function () { + simulateEvent( + ui.layer.layerEl.getElementsByTagName('button')[1] + || ui.layer.layerEl.getElementsByTagName('button')[0], + 'click'); + }, 10); + } + }).then(function () { + equal(log.join('\n'), [ + 'foo:undefined', + 'baz:true', + 'bar:false' + ].join('\n')); + }); + }); + + + promiseTest('factory.use()', function () { + Ply.factory('test', function (options, data, resolve) { + Ply.factory.use('alert', options, { + text: '!?', + ok: 'Y' + }, resolve); + }); + + return Ply.create("test").then(function (layer) { + equal(layer.context.getEl('el').innerHTML, '!?'); + equal(layer.layerEl.getElementsByTagName('button')[0].innerHTML, 'Y'); + }); + }); +})(Ply); diff --git a/tests/index.html b/tests/index.html new file mode 100644 index 0000000..83c4b94 --- /dev/null +++ b/tests/index.html @@ -0,0 +1,133 @@ + + + + + + + Ply :: Tests + + + + + + + + + + +

Ply :: tests

+

+
+

+
    +
    + + + + + + + + + + + + + + + + + +