Release
This commit is contained in:
46
client/islets/common/y-global/y-global.bt.js
Normal file
46
client/islets/common/y-global/y-global.bt.js
Normal file
@@ -0,0 +1,46 @@
|
||||
module.exports = function (bt) {
|
||||
|
||||
bt.lib.global = bt.lib.global || {};
|
||||
bt.lib.global.lang = bt.lib.global.lang || 'ru';
|
||||
bt.lib.global.tld = bt.lib.global.tld || 'ru';
|
||||
bt.lib.global['content-region'] = bt.lib.global['content-region'] || 'ru';
|
||||
bt.lib.global['click-host'] = bt.lib.global['click-host'] || '//clck.yandex.ru';
|
||||
bt.lib.global['passport-host'] = bt.lib.global['passport-host'] || 'https://passport.yandex.ru';
|
||||
bt.lib.global['pass-host'] = bt.lib.global['pass-host'] || '//pass.yandex.ru';
|
||||
bt.lib.global['social-host'] = bt.lib.global['social-host'] || '//social.yandex.ru';
|
||||
bt.lib.global['export-host'] = bt.lib.global['export-host'] || '//export.yandex.ru';
|
||||
|
||||
/**
|
||||
* Changes top level domain.
|
||||
*
|
||||
* @param {String} tld Top level domain.
|
||||
*/
|
||||
bt.lib.global.setTld = function (tld) {
|
||||
var xYaDomain = tld === 'tr' ? 'yandex.com.tr' : 'yandex.' + tld;
|
||||
var yaDomain = ['ua', 'by', 'kz'].indexOf(tld) !== -1 ? 'yandex.ru' : xYaDomain;
|
||||
var globalObj = bt.lib.global;
|
||||
globalObj['content-region'] = tld;
|
||||
globalObj['click-host'] = '//clck.' + yaDomain;
|
||||
globalObj['passport-host'] = 'https://passport.' + yaDomain;
|
||||
globalObj['pass-host'] = '//pass.' + xYaDomain;
|
||||
globalObj['social-host'] = '//social.' + xYaDomain;
|
||||
globalObj['export-host'] = '//export.' + xYaDomain;
|
||||
globalObj.tld = tld;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {String}
|
||||
*/
|
||||
bt.lib.global.getTld = function () {
|
||||
return bt.lib.global.tld;
|
||||
};
|
||||
|
||||
if (bt.lib.i18n && bt.lib.i18n.getLanguage) {
|
||||
var tld = bt.lib.i18n.getLanguage();
|
||||
if (tld === 'uk') {
|
||||
tld = 'ua';
|
||||
}
|
||||
bt.lib.global.setTld(tld);
|
||||
}
|
||||
|
||||
};
|
||||
162
client/islets/common/y-page/y-page.bt.js
Normal file
162
client/islets/common/y-page/y-page.bt.js
Normal file
@@ -0,0 +1,162 @@
|
||||
module.exports = function (bt) {
|
||||
|
||||
/**
|
||||
* @param {Bemjson} body Содержимое страницы. Следует использовать вместо `content`.
|
||||
* @param {String} doctype Доктайп. По умолчанию используется HTML5 doctype.
|
||||
* @param {Object[]} styles Набор CSS-файлов для подключения.
|
||||
* Каждый элемент массива должен содержать ключ `url`, содержащий путь к файлу.
|
||||
* @param {Object[]} scripts Набор JS-файлов для подключения.
|
||||
* Каждый элемент массива должен содержать ключ `url`, содержащий путь к файлу.
|
||||
* @param {Bemjson} head Дополнительные элементы для заголовочной части страницы.
|
||||
* @param {String} favicon Путь к фавиконке.
|
||||
*/
|
||||
|
||||
bt.setDefaultView('y-page', 'islet');
|
||||
|
||||
bt.match('y-page_islet*', function (ctx) {
|
||||
var styleElements;
|
||||
var styles = ctx.getParam('styles');
|
||||
if (styles) {
|
||||
styleElements = styles.map(function (style) {
|
||||
return {
|
||||
elem: 'css',
|
||||
url: style.url,
|
||||
ie: style.ie
|
||||
};
|
||||
});
|
||||
}
|
||||
return [
|
||||
ctx.getParam('doctype') || '<!DOCTYPE html>',
|
||||
{
|
||||
elem: 'html',
|
||||
content: [
|
||||
{
|
||||
elem: 'head',
|
||||
content: [
|
||||
[
|
||||
{
|
||||
elem: 'meta',
|
||||
charset: 'utf-8'
|
||||
},
|
||||
ctx.getParam('x-ua-compatible') === false ?
|
||||
false :
|
||||
{
|
||||
elem: 'meta',
|
||||
'http-equiv': 'X-UA-Compatible',
|
||||
content: ctx.getParam('x-ua-compatible') || 'IE=edge'
|
||||
},
|
||||
{
|
||||
elem: 'title',
|
||||
content: ctx.getParam('title')
|
||||
},
|
||||
ctx.getParam('favicon') ?
|
||||
{
|
||||
elem: 'favicon',
|
||||
url: ctx.getParam('favicon')
|
||||
} :
|
||||
'',
|
||||
{
|
||||
block: 'y-ua'
|
||||
}
|
||||
],
|
||||
styleElements,
|
||||
ctx.getParam('head')
|
||||
]
|
||||
},
|
||||
ctx.getJson()
|
||||
]
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*', function (ctx) {
|
||||
ctx.setTag('body');
|
||||
ctx.enableAutoInit();
|
||||
var scriptElements;
|
||||
var scripts = ctx.getParam('scripts');
|
||||
if (scripts) {
|
||||
var global = bt.lib.global;
|
||||
scriptElements = scripts.map(function (script) {
|
||||
return {
|
||||
elem: 'js',
|
||||
url: script.url ? script.url.replace('{lang}', global.lang) : undefined,
|
||||
source: script.source
|
||||
};
|
||||
});
|
||||
}
|
||||
ctx.setContent([ctx.getParam('body'), scriptElements]);
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__title', function (ctx) {
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.setTag('title');
|
||||
ctx.setContent(ctx.getParam('content'));
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__html', function (ctx) {
|
||||
ctx.setTag('html');
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.setAttr('class', 'y-ua_js_no y-ua_css_standard');
|
||||
ctx.setContent(ctx.getParam('content'));
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__head', function (ctx) {
|
||||
ctx.setTag('head');
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.setContent(ctx.getParam('content'));
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__meta', function (ctx) {
|
||||
ctx.setTag('meta');
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.setAttr('content', ctx.getParam('content'));
|
||||
ctx.setAttr('http-equiv', ctx.getParam('http-equiv'));
|
||||
ctx.setAttr('charset', ctx.getParam('charset'));
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__favicon', function (ctx) {
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.setTag('link');
|
||||
ctx.setAttr('rel', 'shortcut icon');
|
||||
ctx.setAttr('href', ctx.getParam('url'));
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__js', function (ctx) {
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.setTag('script');
|
||||
var url = ctx.getParam('url');
|
||||
if (url) {
|
||||
ctx.setAttr('src', url);
|
||||
}
|
||||
var source = ctx.getParam('source');
|
||||
if (source) {
|
||||
ctx.setContent(source);
|
||||
}
|
||||
ctx.setAttr('type', 'text/javascript');
|
||||
});
|
||||
|
||||
bt.match('y-page_islet*__css', function (ctx) {
|
||||
ctx.disableCssClassGeneration();
|
||||
var url = ctx.getParam('url');
|
||||
|
||||
if (url) {
|
||||
ctx.setTag('link');
|
||||
ctx.setAttr('rel', 'stylesheet');
|
||||
ctx.setAttr('href', url);
|
||||
} else {
|
||||
ctx.setTag('style');
|
||||
}
|
||||
|
||||
var ie = ctx.getParam('ie');
|
||||
if (ie !== undefined) {
|
||||
if (ie === true) {
|
||||
return ['<!--[if IE]>', ctx.getJson(), '<![endif]-->'];
|
||||
} else if (ie === false) {
|
||||
return ['<!--[if !IE]> -->', ctx.getJson(), '<!-- <![endif]-->'];
|
||||
} else {
|
||||
return ['<!--[if ' + ie + ']>', ctx.getJson(), '<![endif]-->'];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
4
client/islets/common/y-page/y-page.deps.yaml
Normal file
4
client/islets/common/y-page/y-page.deps.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
- y-global
|
||||
- block: y-design
|
||||
required: true
|
||||
- block: y-ua
|
||||
17
client/islets/common/y-page/y-page.md
Normal file
17
client/islets/common/y-page/y-page.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# y-page: страница
|
||||
|
||||
Используется в качестве контейнера для всех остальных блоков.
|
||||
|
||||
Содержимое страницы следует задавать параметром `body` в `bemjson`.
|
||||
|
||||
## Варианты представления
|
||||
|
||||
| view | Описание
|
||||
| --------------- | ---------
|
||||
| `islet` | Дефолтное представление. Содержит глобальные стили для ссылок
|
||||
|
||||
|
||||
## Настройки шаблона
|
||||
|
||||
<!--BTJSON_API-->
|
||||
|
||||
40
client/islets/common/y-page/y-page.test.js
Normal file
40
client/islets/common/y-page/y-page.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
modules.define(
|
||||
'test',
|
||||
['bt'],
|
||||
function (provide, bt) {
|
||||
|
||||
describe('y-page', function () {
|
||||
describe('bt', function () {
|
||||
describe('doctype', function () {
|
||||
it('should should render HTML5 doctype by default', function () {
|
||||
bt.processBtJson({block: 'y-page'})[0].should.equal('<!DOCTYPE html>');
|
||||
});
|
||||
it('should should render given doctype', function () {
|
||||
bt.processBtJson({block: 'y-page', doctype: '<!DOCTYPE>'})[0].should.equal('<!DOCTYPE>');
|
||||
});
|
||||
});
|
||||
describe('layout', function () {
|
||||
it('should render html tag', function () {
|
||||
bt.processBtJson({block: 'y-page'})[1]._tag.should.equal('html');
|
||||
});
|
||||
it('should render head tag', function () {
|
||||
bt.processBtJson({block: 'y-page'})[1].content[0]._tag.should.equal('head');
|
||||
});
|
||||
it('should render body tag', function () {
|
||||
bt.processBtJson({block: 'y-page'})[1].content[1]._tag.should.equal('body');
|
||||
});
|
||||
});
|
||||
describe('js', function () {
|
||||
bt.apply({
|
||||
block: 'y-page',
|
||||
scripts: [{url: '1.js'}, {source: 'alert("Hello World!");'}]
|
||||
}).should.contain(
|
||||
'<script src="1.js" type="text/javascript"></script>' +
|
||||
'<script type="text/javascript">alert("Hello World!");</script>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
8
client/islets/common/y-page/y-page_islet.styl
Normal file
8
client/islets/common/y-page/y-page_islet.styl
Normal file
@@ -0,0 +1,8 @@
|
||||
.y-page_islet {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
background: #F6F5F3;
|
||||
|
||||
font-family: $y-design.common['font-family'];
|
||||
}
|
||||
31
client/islets/common/y-ua/y-ua.bt.js
Normal file
31
client/islets/common/y-ua/y-ua.bt.js
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = function (bt) {
|
||||
|
||||
bt.match('y-ua', function (ctx) {
|
||||
ctx.setTag('script');
|
||||
ctx.disableCssClassGeneration();
|
||||
ctx.disableDataAttrGeneration();
|
||||
ctx.setContent([
|
||||
';(function (d,e,c,r){' +
|
||||
'e=d.documentElement;' +
|
||||
'c="className";' +
|
||||
'r="replace";' +
|
||||
'e[c]=e[c][r]("y-ua_js_no","y-ua_js_yes");' +
|
||||
'if(d.compatMode!="CSS1Compat")' +
|
||||
'e[c]=e[c][r]("y-ua_css_standart","y-ua_css_quirks")' +
|
||||
'})(document);' +
|
||||
';(function (d,e,c,r,n,w,v,f){' +
|
||||
'e=d.documentElement;' +
|
||||
'c="className";' +
|
||||
'r="replace";' +
|
||||
'n="createElementNS";' +
|
||||
'f="firstChild";' +
|
||||
'w="http://www.w3.org/2000/svg";' +
|
||||
'e[c]+=!!d[n]&&!!d[n](w,"svg").createSVGRect?" y-ua_svg_yes":" y-ua_svg_no";' +
|
||||
'v=d.createElement("div");' +
|
||||
'v.innerHTML="<svg/>";' +
|
||||
'e[c]+=(v[f]&&v[f].namespaceURI)==w?" y-ua_inlinesvg_yes":" y-ua_inlinesvg_no";' +
|
||||
'})(document);'
|
||||
]);
|
||||
});
|
||||
|
||||
};
|
||||
5
client/islets/core/jquery/jquery-config.js
vendored
Normal file
5
client/islets/core/jquery/jquery-config.js
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
modules.define('jquery-config', function (provide) {
|
||||
provide({
|
||||
url: '//yastatic.net/jquery/1.10.1/jquery.min.js'
|
||||
});
|
||||
});
|
||||
27
client/islets/core/jquery/jquery.js
vendored
Normal file
27
client/islets/core/jquery/jquery.js
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Загружает (если нет на странице) и предоставляет jQuery.
|
||||
*/
|
||||
|
||||
/* global jQuery */
|
||||
modules.define(
|
||||
'jquery',
|
||||
[
|
||||
'y-load-script',
|
||||
'jquery-config'
|
||||
],
|
||||
function (
|
||||
provide,
|
||||
loadScript,
|
||||
config
|
||||
) {
|
||||
|
||||
function doProvide() {
|
||||
provide(jQuery.noConflict(true));
|
||||
}
|
||||
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
doProvide();
|
||||
} else {
|
||||
loadScript(config.url, doProvide);
|
||||
}
|
||||
});
|
||||
60
client/islets/core/y-block-event/y-block-event.js
Normal file
60
client/islets/core/y-block-event/y-block-event.js
Normal file
@@ -0,0 +1,60 @@
|
||||
modules.define(
|
||||
'y-block-event',
|
||||
[
|
||||
'inherit'
|
||||
],
|
||||
function (
|
||||
provide,
|
||||
inherit
|
||||
) {
|
||||
|
||||
/**
|
||||
* Класс, представляющий событие блока.
|
||||
*/
|
||||
var YBlockEvent = inherit({
|
||||
/**
|
||||
* @param {String} type Тип события.
|
||||
* @param {Boolean} [isPropagationStopped=false] Запрещает распространение события.
|
||||
* @param {Boolean} [isDefaultPrevented=false] Запрещает действие по умолчанию.
|
||||
*/
|
||||
__constructor: function (type, isPropagationStopped, isDefaultPrevented) {
|
||||
this.type = type;
|
||||
this._isPropagationStopped = Boolean(isPropagationStopped);
|
||||
this._isDefaultPrevented = Boolean(isDefaultPrevented);
|
||||
},
|
||||
|
||||
/**
|
||||
* Определяет, прекращено ли распространение события.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isPropagationStopped: function () {
|
||||
return this._isPropagationStopped;
|
||||
},
|
||||
|
||||
/**
|
||||
* Проверяет, отменена ли реакция по умолчанию на событие.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isDefaultPrevented: function () {
|
||||
return this._isDefaultPrevented;
|
||||
},
|
||||
|
||||
/**
|
||||
* Прекращает распространение события.
|
||||
*/
|
||||
stopPropagation: function () {
|
||||
this._isPropagationStopped = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Отменяет реакцию по умолчанию на событие.
|
||||
*/
|
||||
preventDefault: function () {
|
||||
this._isDefaultPrevented = true;
|
||||
}
|
||||
});
|
||||
|
||||
provide(YBlockEvent);
|
||||
});
|
||||
63
client/islets/core/y-block-event/y-block-event.test.js
Normal file
63
client/islets/core/y-block-event/y-block-event.test.js
Normal file
@@ -0,0 +1,63 @@
|
||||
modules.define(
|
||||
'test',
|
||||
[
|
||||
'y-block-event'
|
||||
],
|
||||
function (
|
||||
provide,
|
||||
YBlockEvent
|
||||
) {
|
||||
|
||||
describe('YBlockEvent', function () {
|
||||
describe('new YBlockEvent("type")', function () {
|
||||
var event;
|
||||
|
||||
beforeEach(function () {
|
||||
event = new YBlockEvent('foo');
|
||||
});
|
||||
|
||||
it('should not stop propagation and not stop default action', function () {
|
||||
event.isPropagationStopped().should.be.false;
|
||||
event.isDefaultPrevented().should.be.false;
|
||||
});
|
||||
|
||||
it('should have property `type`', function () {
|
||||
event.type.should.eq('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('new YBlockEvent("type", true, false)', function () {
|
||||
it('should stop propagation', function () {
|
||||
var event = new YBlockEvent('type', true, false);
|
||||
event.isPropagationStopped().should.be.true;
|
||||
event.isDefaultPrevented().should.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('new YBlockEvent("type", false, true)', function () {
|
||||
it('should prevent default action', function () {
|
||||
var event = new YBlockEvent('type', false, true);
|
||||
event.isPropagationStopped().should.be.false;
|
||||
event.isDefaultPrevented().should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('preventDefault()', function () {
|
||||
it('should prevent default action of event', function () {
|
||||
var event = new YBlockEvent('type');
|
||||
event.preventDefault();
|
||||
event.isDefaultPrevented().should.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopPropagation()', function () {
|
||||
it('should stop propagation of event', function () {
|
||||
var event = new YBlockEvent('type');
|
||||
event.stopPropagation();
|
||||
event.isPropagationStopped().should.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
70
client/islets/core/y-block-mixin/y-block-mixin.js
Normal file
70
client/islets/core/y-block-mixin/y-block-mixin.js
Normal file
@@ -0,0 +1,70 @@
|
||||
modules.define(
|
||||
'y-block-mixin',
|
||||
['inherit', 'y-event-emitter', 'y-event-manager'],
|
||||
function (provide, inherit, YEventEmitter, YEventManager) {
|
||||
|
||||
var YBlockMixin = inherit(YEventEmitter, {
|
||||
__constructor: function (blockInstance, options) {
|
||||
this._block = blockInstance;
|
||||
this._options = options;
|
||||
this._eventManager = new YEventManager(this);
|
||||
},
|
||||
|
||||
_getBlock: function () {
|
||||
return this._block;
|
||||
},
|
||||
|
||||
_bindTo: function (emitter, event, callback) {
|
||||
this._eventManager.bindTo(emitter, event, callback);
|
||||
return this;
|
||||
}
|
||||
}, {
|
||||
|
||||
/**
|
||||
* Возвращает имя миксина.
|
||||
* Этот метод следует перекрывать при создании новых миксинов.
|
||||
*
|
||||
* @static
|
||||
* @returns {String|null}
|
||||
*
|
||||
* @example
|
||||
* provide(inherit(YBlockMixin, {}, {
|
||||
* getMixinName: function() {
|
||||
* return 'auto-focus';
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
getMixinName: function () {
|
||||
return 'y-block-mixin';
|
||||
},
|
||||
|
||||
fromBlock: function (blockInstance, options) {
|
||||
var mixinName = this.getMixinName();
|
||||
var mixins = this._getMixinsFromDomNode(blockInstance.getDomNode());
|
||||
if (!mixins[mixinName]) {
|
||||
var Mixin = this;
|
||||
mixins[mixinName] = new Mixin(blockInstance, options);
|
||||
}
|
||||
return mixins[mixinName];
|
||||
},
|
||||
|
||||
/**
|
||||
* Возвращает инстанции миксинов для данного DOM-элемента.
|
||||
*
|
||||
* @param {jQuery} domNode
|
||||
* @param {Boolean} [skipCreating]
|
||||
*/
|
||||
_getMixinsFromDomNode: function (domNode, skipCreating) {
|
||||
var data = domNode.data(this._mixinsStorageKey);
|
||||
if (!data && !skipCreating) {
|
||||
data = {};
|
||||
domNode.data(this._mixinsStorageKey, data);
|
||||
}
|
||||
return data;
|
||||
|
||||
},
|
||||
|
||||
_mixinsStorageKey: 'y-block-mixin'
|
||||
});
|
||||
provide(YBlockMixin);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
- jquery
|
||||
- y-block
|
||||
@@ -0,0 +1,5 @@
|
||||
modules.require(['jquery', 'y-block'], function ($, YBlock) {
|
||||
$(function () {
|
||||
YBlock.initDomTree(window.document).done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
# y-block__auto-init:
|
||||
|
||||
Автоматическая инициализация блоков на странице.
|
||||
Для того, чтобы воспользоваться этой функциональностью,
|
||||
необходимо добавить в зависимости элемент `auto-init` блока `y-block`.
|
||||
1200
client/islets/core/y-block/y-block.js
Normal file
1200
client/islets/core/y-block/y-block.js
Normal file
File diff suppressed because it is too large
Load Diff
6
client/islets/core/y-block/y-block.md
Normal file
6
client/islets/core/y-block/y-block.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# y-block: базовый блок
|
||||
|
||||
Класс `YBlock` — это базовый визуальный блок. Все прочие визуальные блоки должны наследоваться от этого блока с
|
||||
помощью модуля `inherit`.
|
||||
|
||||
<!--JS_API-->
|
||||
856
client/islets/core/y-block/y-block.test.js
Normal file
856
client/islets/core/y-block/y-block.test.js
Normal file
@@ -0,0 +1,856 @@
|
||||
modules.define(
|
||||
'test',
|
||||
[
|
||||
'y-block',
|
||||
'y-block-event',
|
||||
'jquery',
|
||||
'inherit'
|
||||
],
|
||||
function (
|
||||
provide,
|
||||
YBlock,
|
||||
YBlockEvent,
|
||||
$,
|
||||
inherit
|
||||
) {
|
||||
|
||||
describe('YBlock', function () {
|
||||
var modulesStorage;
|
||||
|
||||
beforeEach(function () {
|
||||
modulesStorage = {};
|
||||
|
||||
sinon.stub(modules, 'require', function (blocks, callback) {
|
||||
var result = blocks.map(function (blockName) {
|
||||
return modulesStorage[blockName];
|
||||
});
|
||||
setTimeout(function () {
|
||||
callback.apply(null, result);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
sinon.stub(modules, 'isDefined', function (moduleName) {
|
||||
return modulesStorage[moduleName];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
modules.require.restore();
|
||||
modules.isDefined.restore();
|
||||
});
|
||||
|
||||
describe('__constructor', function () {
|
||||
it('should add _init class', function () {
|
||||
var block = new YBlock($('<div class="y-block"></div>'));
|
||||
block.getDomNode().hasClass('_init').should.be.true;
|
||||
});
|
||||
|
||||
it('should accept domNode as the first argument', function () {
|
||||
var domNode = $('<div class="y-block"></div>');
|
||||
var block = new YBlock(domNode);
|
||||
block.getDomNode().should.equal(domNode);
|
||||
});
|
||||
|
||||
it('should accept domNode as the first argument and options as the second', function () {
|
||||
var domNode = $('<div class="y-block"></div>');
|
||||
var block = new YBlock(domNode, {opt: 'val'});
|
||||
block.getDomNode().should.equal(domNode);
|
||||
block._getOptions().opt.should.equal('val');
|
||||
});
|
||||
|
||||
it('should accept options as the first argument', function () {
|
||||
var block = new YBlock({opt: 'val'});
|
||||
block.getDomNode().should.not.equal(null);
|
||||
block.getDomNode().should.not.equal(undefined);
|
||||
block._getOptions().opt.should.equal('val');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_findElement', function () {
|
||||
it('should return element by name', function () {
|
||||
var block = new YBlock(
|
||||
$('<div class="y-block"><a class="y-block__elem" data-attr="42"></a></div>')
|
||||
);
|
||||
block._findElement('elem').attr('data-attr').should.equal('42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_findAllElements', function () {
|
||||
it('should return elements by name', function () {
|
||||
var block = new YBlock($(
|
||||
'<div class="y-block">' +
|
||||
'<a class="y-block__elem" data-attr="41"></a>' +
|
||||
'<a class="y-block__elem" data-attr="42"></a>' +
|
||||
'<a class="y-block__elem" data-attr="43"></a>' +
|
||||
'</div>'
|
||||
));
|
||||
block._findAllElements('elem').map(function (elem) {
|
||||
return elem.attr('data-attr');
|
||||
}).should.have.members(['41', '42', '43']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_findAllParentElements', function () {
|
||||
it('should return parent elements by name', function () {
|
||||
var block = new YBlock($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__parent" data-attr="43">' +
|
||||
'<div class="y-block__parent" data-attr="42">' +
|
||||
'<div class="y-block__parent" data-attr="41">' +
|
||||
'<a class="y-block__elem"></a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._findAllParentElements('parent', block._findElement('elem')).map(function (parent) {
|
||||
return parent.attr('data-attr');
|
||||
}).should.have.members(['41', '42', '43']);
|
||||
});
|
||||
it('should return parent elements by name in block bounds', function () {
|
||||
var block = new YBlock($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__parent" data-attr="43">' +
|
||||
'<div class="y-block__parent" data-attr="42">' +
|
||||
'<div class="y-block__parent" data-attr="41">' +
|
||||
'<a class="y-block__elem"></a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
));
|
||||
var outer = $('<div class="y-block2__parent" data-attr="44"></div>');
|
||||
block.getDomNode().appendTo(outer);
|
||||
block._findAllParentElements('parent', block._findElement('elem')).map(function (parent) {
|
||||
return parent.attr('data-attr');
|
||||
}).should.have.members(['41', '42', '43']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_findParentElement', function () {
|
||||
it('should return first parent element by name', function () {
|
||||
var block = new YBlock($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__parent" data-attr="41">' +
|
||||
'<div class="y-block__parent" data-attr="42">' +
|
||||
'<a class="y-block__elem"></a>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._findParentElement('parent', block._findElement('elem')).attr('data-attr').should.equal('42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('find', function () {
|
||||
var SubBlock;
|
||||
var block;
|
||||
|
||||
beforeEach(function () {
|
||||
SubBlock = inherit(YBlock, {
|
||||
getTheAnswer: function () {
|
||||
return this.getDomNode().attr('data-attr');
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
block = new YBlock($(
|
||||
'<div class="y-block">' +
|
||||
'<a class="sub-block" data-block="sub-block" data-attr="42"></a>' +
|
||||
'<a class="sub-block" data-block="sub-block" data-attr="24"></a>' +
|
||||
'<a class="sub-block" data-block="sub-block" data-attr="12"></a>' +
|
||||
'</div>'
|
||||
));
|
||||
});
|
||||
|
||||
it('should find first block', function () {
|
||||
SubBlock.find(block).getTheAnswer().should.equal('42');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', function () {
|
||||
var SubBlock;
|
||||
var block;
|
||||
|
||||
beforeEach(function () {
|
||||
SubBlock = inherit(YBlock, {
|
||||
getTheAnswer: function () {
|
||||
return this.getDomNode().attr('data-attr');
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
block = new YBlock($(
|
||||
'<div class="y-block">' +
|
||||
'<a class="sub-block" data-block="sub-block" data-attr="42"></a>' +
|
||||
'<a class="sub-block" data-block="sub-block" data-attr="24"></a>' +
|
||||
'<a class="sub-block" data-block="sub-block" data-attr="12"></a>' +
|
||||
'</div>'
|
||||
));
|
||||
});
|
||||
|
||||
it('should find all blocks', function () {
|
||||
SubBlock.findAll(block).map(function (subBlock) {
|
||||
return subBlock.getTheAnswer();
|
||||
}).should.have.members(['42', '24', '12']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initDomTree', function () {
|
||||
it('should initialize block without params', function (done) {
|
||||
modulesStorage['sub-block'] = inherit(YBlock, {
|
||||
__constructor: function () {
|
||||
this.__base.apply(this, arguments);
|
||||
done();
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
|
||||
YBlock.initDomTree($(
|
||||
'<div class="y-block">' +
|
||||
'<a class="sub-block _init" data-block="sub-block"></a>' +
|
||||
'</div>'
|
||||
)).fail(done);
|
||||
});
|
||||
|
||||
it('should initialize block inside DOM Tree', function (done) {
|
||||
modulesStorage['sub-block'] = inherit(YBlock, {
|
||||
__constructor: function (domNode, params) {
|
||||
this.__base.apply(this, arguments);
|
||||
params.answer.should.equal(42);
|
||||
done();
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
YBlock.initDomTree($(
|
||||
'<div class="y-block">' +
|
||||
'<a' +
|
||||
' class="sub-block _init"' +
|
||||
' data-block="sub-block" ' +
|
||||
' data-options="{"options":{"answer":42}}"></a>' +
|
||||
'</div>'
|
||||
)).fail(done);
|
||||
});
|
||||
|
||||
it('should not initialize block twice', function (done) {
|
||||
var counter = 0;
|
||||
modulesStorage['sub-block'] = inherit(YBlock, {
|
||||
__constructor: function () {
|
||||
this.__base.apply(this, arguments);
|
||||
counter++;
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
var dom = $(
|
||||
'<div class="y-block">' +
|
||||
'<a class="sub-block _init"' +
|
||||
' data-block="sub-block" data-options="{"options":{}}"></a>' +
|
||||
'</div>'
|
||||
);
|
||||
YBlock
|
||||
.initDomTree(dom)
|
||||
.then(function () {
|
||||
return YBlock.initDomTree(dom);
|
||||
})
|
||||
.then(function () {
|
||||
counter.should.equal(1);
|
||||
done();
|
||||
})
|
||||
.fail(done);
|
||||
});
|
||||
|
||||
it('should not initialize block without `_init`', function (done) {
|
||||
modulesStorage['sub-block'] = inherit(YBlock, {
|
||||
__constructor: function () {
|
||||
this.__base.apply(this, arguments);
|
||||
throw new Error('Initialized');
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
YBlock.initDomTree($(
|
||||
'<div class="y-block">' +
|
||||
'<a class="sub-block"' +
|
||||
' data-block="sub-block"' +
|
||||
' data-options="{"options":{"answer":42}}"></a>' +
|
||||
'</div>'
|
||||
)).then(done, done);
|
||||
});
|
||||
|
||||
it('should not initialize block without `data-block`', function (done) {
|
||||
modulesStorage['sub-block'] = inherit(YBlock, {
|
||||
__constructor: function () {
|
||||
this.__base.apply(this, arguments);
|
||||
throw new Error('Initialized');
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
YBlock.initDomTree($(
|
||||
'<div class="y-block">' +
|
||||
'<a class="sub-block _init"></a>' +
|
||||
'</div>'
|
||||
)).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destructDomTree()', function () {
|
||||
it('should destruct once all blocks inside given DOM tree', function (done) {
|
||||
var spies = {};
|
||||
|
||||
['block1', 'block2', 'block3'].forEach(function (blockName) {
|
||||
var Block = inherit(YBlock, null, {
|
||||
getBlockName: function () {
|
||||
return blockName;
|
||||
}
|
||||
});
|
||||
spies[blockName] = sinon.spy(Block.prototype, 'destruct');
|
||||
modulesStorage[blockName] = Block;
|
||||
});
|
||||
|
||||
var elem = $(
|
||||
'<div>' +
|
||||
'<div data-block="block1" class="_init">' +
|
||||
'<div>' +
|
||||
'<div data-block="block2" class="_init"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div data-block="block3" class="_init"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
YBlock.initDomTree(elem).done(function () {
|
||||
YBlock.destructDomTree(elem);
|
||||
spies.block1.calledOnce.should.be.true;
|
||||
spies.block2.calledOnce.should.be.true;
|
||||
spies.block3.calledOnce.should.be.true;
|
||||
|
||||
YBlock.destructDomTree(elem);
|
||||
spies.block1.calledOnce.should.be.true;
|
||||
spies.block2.calledOnce.should.be.true;
|
||||
spies.block3.calledOnce.should.be.true;
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should destruct emitters', function () {
|
||||
var Block = inherit(YBlock, null, {
|
||||
getBlockName: function () {
|
||||
return 'block';
|
||||
}
|
||||
});
|
||||
modulesStorage.block = Block;
|
||||
|
||||
var subElem = $(
|
||||
'<div>' +
|
||||
'<div data-block="block"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
var elem = $('<div>').append(subElem);
|
||||
|
||||
var emitter = Block.getEmitter(elem);
|
||||
var subEmitter = Block.getEmitter(subElem);
|
||||
var spy = sinon.spy();
|
||||
emitter.on('event', spy);
|
||||
subEmitter.on('event', spy);
|
||||
|
||||
Block._getDomNodeDataStorage(elem).blockEvents.block.should.equal(emitter);
|
||||
Block._getDomNodeDataStorage(subElem).blockEvents.block.should.equal(subEmitter);
|
||||
|
||||
Block.destructDomTree(elem);
|
||||
|
||||
Block._getDomNodeDataStorage(elem).blockEvents.should.be.empty;
|
||||
Block._getDomNodeDataStorage(subElem).blockEvents.should.be.empty;
|
||||
|
||||
var eventName = Block._getPropagationEventName('event');
|
||||
elem.trigger(eventName);
|
||||
subElem.trigger(eventName);
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe('emit()', function () {
|
||||
var block;
|
||||
var spy1;
|
||||
var spy2;
|
||||
|
||||
beforeEach(function () {
|
||||
block = new YBlock();
|
||||
spy1 = sinon.spy();
|
||||
spy2 = sinon.spy();
|
||||
|
||||
block.on('event1', spy1);
|
||||
block.on('event2', spy2);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
block.destruct();
|
||||
});
|
||||
|
||||
it('should emit event on block', function () {
|
||||
block.emit('event1');
|
||||
|
||||
var event2 = new YBlockEvent('event2');
|
||||
block.emit(event2);
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
var e = spy1.firstCall.args[0];
|
||||
e.should.be.instanceof(YBlockEvent);
|
||||
e.type.should.eq('event1');
|
||||
e.target.should.eq(block);
|
||||
|
||||
spy2.calledOnce.should.be.true;
|
||||
e = spy2.firstCall.args[0];
|
||||
e.should.be.eq(event2);
|
||||
e.type.should.eq('event2');
|
||||
e.target.should.eq(block);
|
||||
});
|
||||
|
||||
it('should emit event width additional data', function () {
|
||||
var data = {foo: 'bar'};
|
||||
block.emit('event1', data);
|
||||
var event2 = new YBlockEvent('event2');
|
||||
block.emit(event2, data);
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
var e = spy1.firstCall.args[0];
|
||||
e.should.be.instanceof(YBlockEvent);
|
||||
e.type.should.eq('event1');
|
||||
e.target.should.eq(block);
|
||||
e.data.should.eq(data);
|
||||
|
||||
spy2.calledOnce.should.be.true;
|
||||
e = spy2.firstCall.args[0];
|
||||
e.should.be.eq(event2);
|
||||
e.type.should.eq('event2');
|
||||
e.target.should.eq(block);
|
||||
e.data.should.eq(data);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmitter()', function () {
|
||||
it('should return the same instance for same DOM node', function () {
|
||||
var dom = $('<div></div>');
|
||||
YBlock.getEmitter(dom).should.equal(YBlock.getEmitter(dom));
|
||||
});
|
||||
|
||||
it('should listen handle bubbling events', function (done) {
|
||||
var SubBlock = inherit(YBlock, {
|
||||
__constructor: function () {
|
||||
this.__base.apply(this, arguments);
|
||||
this._bindTo(this._findElement('button'), 'click', function () {
|
||||
this.emit('button-click');
|
||||
});
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
var dom = $(
|
||||
'<div><div><div>' +
|
||||
'<div class="sub-block" data-block="sub-block">' +
|
||||
'<div class="sub-block__button"></div>' +
|
||||
'</div>' +
|
||||
'</div></div></div>'
|
||||
);
|
||||
var block = SubBlock.find(dom);
|
||||
SubBlock.getEmitter(dom).on('button-click', function (event) {
|
||||
event.target.should.equal(block);
|
||||
done();
|
||||
});
|
||||
dom.find('.sub-block__button').click();
|
||||
});
|
||||
|
||||
it('should stop propagation', function (done) {
|
||||
var SubBlock = inherit(YBlock, {
|
||||
__constructor: function () {
|
||||
this.__base.apply(this, arguments);
|
||||
this._bindTo(this._findElement('button'), 'click', function () {
|
||||
this.emit('button-click');
|
||||
});
|
||||
}
|
||||
}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
var subDom = $(
|
||||
'<div>' +
|
||||
'<div class="sub-block" data-block="sub-block">' +
|
||||
'<div class="sub-block__button"></div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
var clickTriggered = false;
|
||||
var dom = $('<div></div>').append(subDom);
|
||||
SubBlock.find(dom); // init sub-block
|
||||
SubBlock.getEmitter(subDom).on('button-click', function (event) {
|
||||
clickTriggered = true;
|
||||
event.stopPropagation();
|
||||
});
|
||||
SubBlock.getEmitter(dom).on('button-click', function () {
|
||||
done(new Error('Stop propagation should work'));
|
||||
});
|
||||
dom.find('.sub-block__button').click();
|
||||
clickTriggered.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getDomNodeDataStorage', function () {
|
||||
it('should return the same instance for the same DOM node', function () {
|
||||
var dom = $('<div></div>');
|
||||
YBlock._getDomNodeDataStorage(dom).should.equal(YBlock._getDomNodeDataStorage(dom));
|
||||
});
|
||||
});
|
||||
|
||||
describe('_liveBindToElement', function () {
|
||||
it('should catch event on element', function (done) {
|
||||
var Block = inherit(YBlock, {}, {
|
||||
getBlockName: function () {
|
||||
return 'block1';
|
||||
},
|
||||
_liveInit: function () {
|
||||
this._liveBindToElement('button', 'click', function () {
|
||||
this.emit('button-click');
|
||||
});
|
||||
}
|
||||
});
|
||||
var block = new Block($(
|
||||
'<div class="block1" data-block="block1">' +
|
||||
'<div class="block1__button"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
block.getDomNode().appendTo(document.body);
|
||||
block.on('button-click', function () {
|
||||
block.getDomNode().remove();
|
||||
done();
|
||||
});
|
||||
block._findElement('button').trigger('click');
|
||||
});
|
||||
it('should catch event on element with exact name', function () {
|
||||
var Block = inherit(YBlock, {}, {
|
||||
getBlockName: function () {
|
||||
return 'block2';
|
||||
},
|
||||
_liveInit: function () {
|
||||
this._liveBindToElement('button', 'click', function () {
|
||||
this.emit('button-click');
|
||||
});
|
||||
}
|
||||
});
|
||||
var block = new Block($(
|
||||
'<div class="block2" data-block="block2">' +
|
||||
'<div class="block2__button"></div>' +
|
||||
'<div class="block2__buttons"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
var triggerCount = 0;
|
||||
block.getDomNode().appendTo(document.body);
|
||||
block.on('button-click', function () {
|
||||
triggerCount++;
|
||||
});
|
||||
block._findElement('button').trigger('click');
|
||||
block._findElement('buttons').trigger('click');
|
||||
block.getDomNode().remove();
|
||||
triggerCount.should.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', function () {
|
||||
describe('_getState', function () {
|
||||
it('should return mod value', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block _color_red"></div>')
|
||||
);
|
||||
block._getState('color').should.equal('red');
|
||||
block._getState('type').should.equal(false);
|
||||
});
|
||||
|
||||
it('should return mod value after set', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block _color_red"></div>')
|
||||
);
|
||||
block._getState('color').should.equal('red');
|
||||
block._setState('color', 'blue');
|
||||
block._getState('color').should.equal('blue');
|
||||
});
|
||||
|
||||
it('should not return mod value after del', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block _color_red"></div>')
|
||||
);
|
||||
block._getState('color').should.equal('red');
|
||||
block._removeState('color');
|
||||
block._getState('color').should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_setState', function () {
|
||||
it('should set mod value', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block"></div>')
|
||||
);
|
||||
block._setState('color', 'red');
|
||||
block.getDomNode().attr('class').should.equal('y-block _init _color_red');
|
||||
block._setState('color', 'blue');
|
||||
block.getDomNode().attr('class').should.equal('y-block _init _color_blue');
|
||||
block._setState('color', null);
|
||||
block._setState('size', 'm');
|
||||
block.getDomNode().attr('class').should.equal('y-block _init _size_m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_removeState', function () {
|
||||
it('should remove mod value', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block _color_red"></div>')
|
||||
);
|
||||
block._removeState('color');
|
||||
block.getDomNode().attr('class').should.equal('y-block _init');
|
||||
block._setState('color', 'blue');
|
||||
block._removeState('color');
|
||||
block.getDomNode().attr('class').should.equal('y-block _init');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getState', function () {
|
||||
it('should return mod value', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block _color_red"></div>')
|
||||
);
|
||||
block._getState('color').should.equal('red');
|
||||
block._setState('color', 'blue');
|
||||
block._getState('color').should.equal('blue');
|
||||
block._setState('color', null);
|
||||
block._getState('color').should.equal(false);
|
||||
block._setState('color', undefined);
|
||||
block._getState('color').should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_toggleState', function () {
|
||||
it('should toggle mod value', function () {
|
||||
var block = YBlock.fromDomNode(
|
||||
$('<div class="y-block _color_red"></div>')
|
||||
);
|
||||
block._toggleState('color', 'red', false);
|
||||
block._getState('color').should.equal(false);
|
||||
block._toggleState('color', false, 'red');
|
||||
block._getState('color').should.equal('red');
|
||||
block._toggleState('color', 'red', 'blue');
|
||||
block._getState('color').should.equal('blue');
|
||||
block._toggleState('color', null, 'blue');
|
||||
block._toggleState('color', null, 'blue');
|
||||
block._getState('color').should.equal('blue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_setElementState', function () {
|
||||
it('should set mod value', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__button"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._setElementState(block._findElement('button'), 'color', 'red');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button _color_red');
|
||||
block._setElementState(block._findElement('button'), 'color', 'blue');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button _color_blue');
|
||||
block._setElementState(block._findElement('button'), 'color', '');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button');
|
||||
});
|
||||
it('should set true mod value', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__button"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._setElementState(block._findElement('button'), 'active');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button _active');
|
||||
block._setElementState(block._findElement('button'), 'active', false);
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button');
|
||||
});
|
||||
it('should set mod value with another view', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block_red" data-block="y-block">' +
|
||||
'<div class="y-block_red__button"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._setElementState(block._findElement('button'), 'color', 'red');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block_red__button _color_red');
|
||||
block._setElementState(block._findElement('button'), 'color', 'blue');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block_red__button _color_blue');
|
||||
block._setElementState(block._findElement('button'), 'color', '');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block_red__button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_removeElementState', function () {
|
||||
it('should remove mod value', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__button _color_red"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._removeElementState(block._findElement('button'), 'color');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button');
|
||||
block._setElementState(block._findElement('button'), 'color', 'blue');
|
||||
block._removeElementState(block._findElement('button'), 'color');
|
||||
block._findElement('button')
|
||||
.attr('class').should.equal('y-block__button');
|
||||
});
|
||||
});
|
||||
|
||||
describe('_getElementState', function () {
|
||||
it('should return mod value', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__button _color_red"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
|
||||
var button = block._findElement('button');
|
||||
block._getElementState(button, 'color').should.equal('red');
|
||||
block._setElementState(button, 'color', 'blue');
|
||||
block._getElementState(button, 'color').should.equal('blue');
|
||||
block._setElementState(button, 'color', null);
|
||||
block._getElementState(button, 'color').should.equal(false);
|
||||
block._setElementState(button, 'color', undefined);
|
||||
block._getElementState(button, 'color').should.equal(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_toggleElementState', function () {
|
||||
it('should toggle mod value', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__button _color_red"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
|
||||
var button = block._findElement('button');
|
||||
block._toggleElementState(button, 'color', 'red', false);
|
||||
block._getElementState(button, 'color').should.equal(false);
|
||||
block._toggleElementState(button, 'color', false, 'red');
|
||||
block._getElementState(button, 'color').should.equal('red');
|
||||
block._toggleElementState(button, 'color', 'red', 'blue');
|
||||
block._getElementState(button, 'color').should.equal('blue');
|
||||
block._toggleElementState(button, 'color', null, 'blue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('options', function () {
|
||||
it('should return block options', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block" data-options="{"options":{"level":5}}"></div>'
|
||||
));
|
||||
block._getOptions().level.should.equal(5);
|
||||
});
|
||||
it('should return element options', function () {
|
||||
var block = YBlock.fromDomNode($(
|
||||
'<div class="y-block">' +
|
||||
'<div class="y-block__test" data-options="{"options":{"level":5}}"></div>' +
|
||||
'</div>'
|
||||
));
|
||||
block._getElementOptions(block._findElement('test')).level.should.equal(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destruct()', function () {
|
||||
it('should remove DOM Node on destruct', function () {
|
||||
var SubBlock = inherit(YBlock, {}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
var block = new SubBlock();
|
||||
var blockDomNode = block.getDomNode();
|
||||
blockDomNode.appendTo(document.body);
|
||||
$.contains(document.body, blockDomNode[0]).should.be.true;
|
||||
block.destruct();
|
||||
$.contains(document.body, blockDomNode[0]).should.be.false;
|
||||
});
|
||||
|
||||
it('should destruct child blocks in descending order', function () {
|
||||
var SubBlock = inherit(YBlock, {}, {
|
||||
getBlockName: function () {
|
||||
return 'sub-block';
|
||||
}
|
||||
});
|
||||
var InnerBlock = inherit(YBlock, {}, {
|
||||
getBlockName: function () {
|
||||
return 'inner-block';
|
||||
}
|
||||
});
|
||||
var block = new SubBlock();
|
||||
sinon.spy(block, 'destruct');
|
||||
|
||||
var innerBlock = new InnerBlock();
|
||||
sinon.spy(innerBlock, 'destruct');
|
||||
block.getDomNode().append(innerBlock.getDomNode());
|
||||
|
||||
var innerSubBlock = new SubBlock();
|
||||
sinon.spy(innerSubBlock, 'destruct');
|
||||
innerBlock.getDomNode().append(innerSubBlock.getDomNode());
|
||||
|
||||
block.destruct();
|
||||
sinon.assert.callOrder(block.destruct, innerBlock.destruct, innerSubBlock.destruct);
|
||||
});
|
||||
|
||||
it('should throw error on double destruct', function () {
|
||||
var block = new YBlock($('<div class="y-block"></div>'));
|
||||
block.destruct();
|
||||
function destructBlockAgain() {
|
||||
block.destruct();
|
||||
}
|
||||
destructBlockAgain.should.throw(Error, 'Block `y-block` was already destroyed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('YBlock.fromDomNode()', function () {
|
||||
it('should return instance of block for given DOM node', function () {
|
||||
var elem = $('div');
|
||||
var block = YBlock.fromDomNode(elem);
|
||||
block.should.be.instanceof(YBlock);
|
||||
});
|
||||
|
||||
it('should return same instance for same DOM node', function () {
|
||||
var elem = document.createElement('div');
|
||||
var block = YBlock.fromDomNode($(elem));
|
||||
YBlock.fromDomNode($(elem)).should.eq(block);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
45
client/islets/core/y-debounce/y-debounce.js
Normal file
45
client/islets/core/y-debounce/y-debounce.js
Normal file
@@ -0,0 +1,45 @@
|
||||
modules.define('y-debounce', function (provide) {
|
||||
/**
|
||||
* Вернет версию функции, исполнение которой начнется не ранее,
|
||||
* чем истечет промежуток wait, после ее последнего вызова.
|
||||
*
|
||||
* Полезно для реализации логики, которая зависит от завершения
|
||||
* действий пользователя. Например, проверить орфографию комментария
|
||||
* пользователя лучше будет после того, как он его окончательно введет,
|
||||
* а динамечески перерассчитать разметку после того, как пользователь
|
||||
* закончит изменять размер окна.
|
||||
*
|
||||
* @name debounce
|
||||
* @param {Function} func
|
||||
* @param {Number} wait
|
||||
* @param {Boolean} [immediate=false] Если true, выполнит функцию в начале
|
||||
* интервала wait, иначе - в конце.
|
||||
* @returns {Function}
|
||||
*
|
||||
* @example
|
||||
* var calculateLayout = function() {};
|
||||
* var lazyLayout = debounce(calculateLayout, 300);
|
||||
* $(window).resize(lazyLayout);
|
||||
*/
|
||||
provide(function (func, wait, immediate) {
|
||||
var result;
|
||||
var timeout = null;
|
||||
return function () {
|
||||
var context = this;
|
||||
var args = arguments;
|
||||
var later = function () {
|
||||
timeout = null;
|
||||
if (!immediate) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) {
|
||||
result = func.apply(context, args);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
});
|
||||
});
|
||||
6
client/islets/core/y-debounce/y-debounce.md
Normal file
6
client/islets/core/y-debounce/y-debounce.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# y-debounce:
|
||||
|
||||
Модуль `y-debounce` возвращает функцию `debounce`, которая используется для реализации отложенных действий.
|
||||
Подробности по клику на функции.
|
||||
|
||||
<!--JS_API-->
|
||||
59
client/islets/core/y-debounce/y-debounce.test.js
Normal file
59
client/islets/core/y-debounce/y-debounce.test.js
Normal file
@@ -0,0 +1,59 @@
|
||||
modules.define('test', ['y-debounce'], function (provide, debounce) {
|
||||
|
||||
describe('debounce', function () {
|
||||
it('should debounce given function', function (done) {
|
||||
var counter = 0;
|
||||
var incr = function () {
|
||||
counter++;
|
||||
};
|
||||
var debouncedIncr = debounce(incr, 32);
|
||||
debouncedIncr();
|
||||
debouncedIncr();
|
||||
setTimeout(debouncedIncr, 16);
|
||||
setTimeout(function () {
|
||||
counter.should.eq(1, 'incr was debounced');
|
||||
done();
|
||||
}, 96);
|
||||
});
|
||||
|
||||
it('should call given function immediately if "immediate" param is true', function (done) {
|
||||
var a;
|
||||
var b;
|
||||
var counter = 0;
|
||||
var incr = function () {
|
||||
return ++counter;
|
||||
};
|
||||
var debouncedIncr = debounce(incr, 64, true);
|
||||
a = debouncedIncr();
|
||||
b = debouncedIncr();
|
||||
a.should.eq(1);
|
||||
b.should.eq(1);
|
||||
counter.should.eq(1, 'incr was called immediately');
|
||||
setTimeout(debouncedIncr, 16);
|
||||
setTimeout(debouncedIncr, 32);
|
||||
setTimeout(debouncedIncr, 48);
|
||||
setTimeout(function () {
|
||||
counter.should.eq(1, 'incr was debounced');
|
||||
done();
|
||||
}, 128);
|
||||
});
|
||||
|
||||
it('should work properly when debounced function called recursively', function (done) {
|
||||
var counter = 0;
|
||||
var debouncedIncr = debounce(function () {
|
||||
counter++;
|
||||
if (counter < 10) {
|
||||
debouncedIncr();
|
||||
}
|
||||
}, 32, true);
|
||||
debouncedIncr();
|
||||
counter.should.eq(1, 'incr was called immediately');
|
||||
setTimeout(function () {
|
||||
counter.should.eq(1, 'incr was debounced');
|
||||
done();
|
||||
}, 96);
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
129
client/islets/core/y-design/y-design.styl
Normal file
129
client/islets/core/y-design/y-design.styl
Normal file
@@ -0,0 +1,129 @@
|
||||
$y-design = {
|
||||
common: {
|
||||
font-family: Arial\, Helvetica\, sans-serif,
|
||||
transition-duration: .15s,
|
||||
transition-timing-function: ease-out,
|
||||
transition: .15s ease-out
|
||||
},
|
||||
|
||||
link: {
|
||||
color: #44b,
|
||||
text-decoration: none,
|
||||
|
||||
// Второстепенная ссылка с фиолетовым оттенком. Используется, например, в футере.
|
||||
_minor: {
|
||||
color: #669
|
||||
},
|
||||
|
||||
// Ссылка на внешние ресурсы, зеленого цвета.
|
||||
_outer: {
|
||||
color: #070
|
||||
}
|
||||
|
||||
_hover: {
|
||||
color: #e00
|
||||
}
|
||||
},
|
||||
|
||||
island: {
|
||||
background-color: #fff,
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1),
|
||||
|
||||
_flying: {
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, .1)\, 0 10px 20px -5px rgba(0, 0, 0, .4)
|
||||
}
|
||||
},
|
||||
|
||||
box: {
|
||||
cursor: pointer,
|
||||
|
||||
border-radius: 3px,
|
||||
border-width: 1px,
|
||||
border-style: solid,
|
||||
|
||||
border-color: rgba(0, 0, 0, 0.2),
|
||||
border-color-ie8: #CCC,
|
||||
|
||||
background-color: #FFF
|
||||
|
||||
_hover: {
|
||||
border-color: rgba(0, 0, 0, 0.3)
|
||||
border-color-ie8: #B3B3B3,
|
||||
},
|
||||
_focus: {
|
||||
box-shadow: 0 0 10px #FC0
|
||||
border-color: rgba(178, 142, 0, 0.6),
|
||||
border-color-ie8: #D1BB66,
|
||||
},
|
||||
_pressed: {
|
||||
background-color: #F6F5F3
|
||||
},
|
||||
_checked: {
|
||||
background-color: #FFEBA0
|
||||
border-color: rgba(153, 122, 0, 0.5),
|
||||
border-color-ie8: #CCB350,
|
||||
},
|
||||
_checked_hover: {
|
||||
border-color: rgba(129, 103, 0, 0.6),
|
||||
border-color-ie8: #B39C40
|
||||
},
|
||||
_disabled: {
|
||||
border-color: transparent, // $y-design.box._disabled['border-color'] не нужен, но удалить не получается чтоб не ломалось
|
||||
background-color: rgba(0, 0, 0, 0.08),
|
||||
background-color-ie8: #EBEBEB,
|
||||
box-shadow: none,
|
||||
cursor: default
|
||||
}
|
||||
},
|
||||
|
||||
airbox: {
|
||||
border-color: rgba(0, 0, 0, 0.08),
|
||||
border-color-ie8: #ccc,
|
||||
box-shadow: 0 3px 0 0 rgba(0, 0, 0, 0.06),
|
||||
background-color: rgba(255, 255, 255, 0.95),
|
||||
background-color-ie8: white,
|
||||
|
||||
_focus: {
|
||||
box-shadow: 0 3px 0 0 rgba(0, 0, 0, 0.06),
|
||||
border-color: #f5ba4c
|
||||
},
|
||||
|
||||
_hover: {
|
||||
border-color: rgba(0, 0, 0, 0.2),
|
||||
border-color-ie8: #b3b3b3
|
||||
},
|
||||
|
||||
_pressed: {
|
||||
border-color: rgba(0, 0, 0, 0.2),
|
||||
border-color-ie8: #b3b3b3,
|
||||
opacity: 0.8
|
||||
},
|
||||
|
||||
_disabled: {
|
||||
background-color: rgba(242, 242, 242, 0.95),
|
||||
background-color-ie8: #dfdfdf
|
||||
}
|
||||
},
|
||||
|
||||
pseudobox: {
|
||||
background-color: transparent
|
||||
|
||||
_pressed: {
|
||||
background-color: rgba(0, 0, 0, 0.05)
|
||||
}
|
||||
},
|
||||
|
||||
popup: {
|
||||
background-color: #FFF
|
||||
|
||||
ok: {
|
||||
background-color: rgba(108, 186, 104, .9)
|
||||
},
|
||||
help: {
|
||||
background-color: rgba(50, 50, 50, .8)
|
||||
},
|
||||
error: {
|
||||
background-color: rgba(255, 100, 100,.9)
|
||||
}
|
||||
}
|
||||
}
|
||||
211
client/islets/core/y-dom/y-dom.js
Normal file
211
client/islets/core/y-dom/y-dom.js
Normal file
@@ -0,0 +1,211 @@
|
||||
modules.define(
|
||||
'y-dom',
|
||||
['jquery', 'y-block'],
|
||||
function (provide, $, YBlock) {
|
||||
|
||||
/**
|
||||
* @name yDom
|
||||
*/
|
||||
provide({
|
||||
/**
|
||||
* Отсоединяет фрагмент DOM-дерева от документа.
|
||||
* Сохраняет слушатели событий и данные (jQuery data).
|
||||
*
|
||||
* @name yDom.detach
|
||||
* @param {jQuery|HTMLElement|YBlock} domNode
|
||||
*/
|
||||
detach: function (domNode) {
|
||||
domNode = this._getDomElement(domNode);
|
||||
var l = domNode.length;
|
||||
for (var i = 0; i < l; i++) {
|
||||
var node = domNode[i];
|
||||
if (node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Заменяет один DOM-фрагмент другим.
|
||||
*
|
||||
* @name yDom.replace
|
||||
* @param {jQuery|HTMLElement|YBlock} replaceWhat
|
||||
* @param {jQuery|HTMLElement|YBlock} replaceWith
|
||||
*/
|
||||
replace: function (replaceWhat, replaceWith) {
|
||||
replaceWhat = this._getDomElement(replaceWhat);
|
||||
replaceWith = this._getDomElement(replaceWith);
|
||||
replaceWith.insertBefore(replaceWhat);
|
||||
this.detach(replaceWhat);
|
||||
},
|
||||
|
||||
/**
|
||||
* Вставляет `domNode` перед `sourceDomNode`.
|
||||
*
|
||||
* @name yDom.insertBefore
|
||||
* @param {jQuery|HTMLElement|YBlock} domNode
|
||||
* @param {jQuery|HTMLElement|YBlock} sourceDomNode
|
||||
*/
|
||||
insertBefore: function (domNode, sourceDomNode) {
|
||||
domNode = this._getDomElement(domNode);
|
||||
sourceDomNode = this._getDomElement(sourceDomNode);
|
||||
sourceDomNode.insertBefore(domNode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Вставляет `domNode` после `sourceDomNode`.
|
||||
*
|
||||
* @name yDom.insertAfter
|
||||
* @param {jQuery|HTMLElement|YBlock} domNode
|
||||
* @param {jQuery|HTMLElement|YBlock} sourceDomNode
|
||||
*/
|
||||
insertAfter: function (domNode, sourceDomNode) {
|
||||
domNode = this._getDomElement(domNode);
|
||||
sourceDomNode = this._getDomElement(sourceDomNode);
|
||||
sourceDomNode.insertAfter(domNode);
|
||||
},
|
||||
|
||||
/**
|
||||
* Добавляет `domNode` в конец `parentDomNode`.
|
||||
*
|
||||
* @name yDom.append
|
||||
* @param {jQuery|HTMLElement} parentDomNode
|
||||
* @param {jQuery|HTMLElement|YBlock} domNode
|
||||
*/
|
||||
append: function (parentDomNode, domNode) {
|
||||
parentDomNode = $(parentDomNode);
|
||||
parentDomNode.append(this._getDomElement(domNode));
|
||||
},
|
||||
|
||||
/**
|
||||
* Добавляет `domNode` в начало `parentDomNode`.
|
||||
*
|
||||
* @name yDom.prepend
|
||||
* @param {jQuery|HTMLElement} parentDomNode
|
||||
* @param {jQuery|HTMLElement|YBlock} domNode
|
||||
*/
|
||||
prepend: function (parentDomNode, domNode) {
|
||||
parentDomNode = $(parentDomNode);
|
||||
parentDomNode.prepend(this._getDomElement(domNode));
|
||||
},
|
||||
|
||||
/**
|
||||
* Заменяет содержимое `parentDomNode` фрагментом `domNode`.
|
||||
*
|
||||
* @name yDom.replaceContents
|
||||
* @param {jQuery|HTMLElement} parentDomNode
|
||||
* @param {jQuery|HTMLElement|YBlock} domNode
|
||||
*/
|
||||
replaceContents: function (parentDomNode, domNode) {
|
||||
parentDomNode = $(parentDomNode);
|
||||
domNode = this._getDomElement(domNode);
|
||||
var contents = parentDomNode.contents();
|
||||
if (contents.length) {
|
||||
this.replace(contents, domNode);
|
||||
} else {
|
||||
parentDomNode.append(domNode);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Возвращает jQuery-элемент для переданного `HTML`/`jQuery`/`YBlock`/`String`-представления элемента.
|
||||
*
|
||||
* @param {jQuery|HTMLElement|YBlock|String} domNode
|
||||
* @returns {jQuery}
|
||||
*/
|
||||
_getDomElement: function (domNode) {
|
||||
if (domNode instanceof YBlock) {
|
||||
domNode = domNode.getDomNode();
|
||||
}
|
||||
if (typeof domNode === 'string') {
|
||||
var div = $('<div></div>');
|
||||
div.html(domNode);
|
||||
return div.contents();
|
||||
} else {
|
||||
return $(domNode);
|
||||
}
|
||||
},
|
||||
html: {
|
||||
/**
|
||||
* Преобразует сущности HTML-синтаксиса в безопасные эквиваленты.
|
||||
*
|
||||
* @name yDom.html.escape
|
||||
* @param {String} str
|
||||
* @returns {String}
|
||||
*/
|
||||
escape: function (str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
},
|
||||
focus: {
|
||||
/**
|
||||
* Возвращает `true` если на элемент возможно поставить фокус.
|
||||
*
|
||||
* @name yDom.focus.isFocusable
|
||||
* @param {jQuery|HTMLElement} domNode
|
||||
*/
|
||||
isFocusable: function (domNode) {
|
||||
domNode = $(domNode)[0];
|
||||
switch (domNode.nodeName.toLowerCase()) {
|
||||
case 'iframe':
|
||||
return true;
|
||||
case 'input':
|
||||
case 'button':
|
||||
case 'textarea':
|
||||
case 'select':
|
||||
return !domNode.hasAttribute('disabled');
|
||||
case 'a':
|
||||
return domNode.hasAttribute('href');
|
||||
default:
|
||||
return domNode.hasAttribute('tabindex');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Возвращает `true` если элемент сфокусирован.
|
||||
*
|
||||
* @name yDom.focus.hasFocus
|
||||
* @param {jQuery|HTMLElement} domNode
|
||||
*/
|
||||
hasFocus: function (domNode) {
|
||||
domNode = $(domNode)[0];
|
||||
var activeNode = document.activeElement;
|
||||
if (activeNode) {
|
||||
var currentNode = activeNode;
|
||||
while (currentNode) {
|
||||
if (currentNode === domNode) {
|
||||
return true;
|
||||
}
|
||||
currentNode = currentNode.parentNode;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
selection: {
|
||||
/**
|
||||
* Возвращает позицию курсора в поле ввода.
|
||||
*
|
||||
* @param {jQuery|HTMLElement} input
|
||||
* @returns {number}
|
||||
*/
|
||||
getInputCaretPosition: function (input) {
|
||||
input = $(input)[0];
|
||||
var pos = 0;
|
||||
if (document.selection) { // ie
|
||||
input.focus();
|
||||
var selection = document.selection.createRange();
|
||||
selection.moveStart('character', -input.value.length);
|
||||
pos = selection.text.length;
|
||||
} else if (input.selectionStart || input.selectionStart === 0) { // firefox
|
||||
pos = input.selectionStart;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
5
client/islets/core/y-dom/y-dom.md
Normal file
5
client/islets/core/y-dom/y-dom.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# y-dom: работа с DOM
|
||||
|
||||
Модуль `y-dom` возвращает объект `yDom` для работы с DOM со следующими полями и методами:
|
||||
|
||||
<!--JS_API-->
|
||||
211
client/islets/core/y-event-emitter/y-event-emitter.js
Normal file
211
client/islets/core/y-event-emitter/y-event-emitter.js
Normal file
@@ -0,0 +1,211 @@
|
||||
modules.define(
|
||||
'y-event-emitter',
|
||||
['inherit'],
|
||||
function (provide, inherit) {
|
||||
|
||||
var slice = Array.prototype.slice;
|
||||
|
||||
/**
|
||||
* @name YEventEmitter
|
||||
*/
|
||||
var YEventEmitter = inherit({
|
||||
/**
|
||||
* Добавляет обработчик события.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @param {Object} [context]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
on: function (event, callback, context) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
if (!this._events) {
|
||||
this._events = {};
|
||||
}
|
||||
|
||||
var listener = {
|
||||
callback: callback,
|
||||
context: context
|
||||
};
|
||||
|
||||
var listeners = this._events[event];
|
||||
if (listeners) {
|
||||
listeners.push(listener);
|
||||
} else {
|
||||
this._events[event] = [listener];
|
||||
this._onAddEvent(event);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Добавляет обработчик события, который исполнится только 1 раз, затем удалится.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @param {Object} [context]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
once: function (event, callback, context) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
var _this = this;
|
||||
|
||||
function once() {
|
||||
_this.off(event, once, context);
|
||||
callback.apply(context, arguments);
|
||||
}
|
||||
|
||||
// Сохраняем ссылку на оригинальный колбэк. Благодаря этому можно удалить колбэк `once`,
|
||||
// используя оригинальный колбэк в методе `off()`.
|
||||
once._callback = callback;
|
||||
|
||||
this.on(event, once, context);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удаляет обработчик события.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @param {Object} [context]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
off: function (event, callback, context) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new TypeError('callback must be a function');
|
||||
}
|
||||
|
||||
if (!this._events) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var listeners = this._events[event];
|
||||
if (!listeners) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var len = listeners.length;
|
||||
for (var i = 0; i < len; i++) {
|
||||
var listener = listeners[i];
|
||||
var cb = listener.callback;
|
||||
if ((cb === callback || cb._callback === callback) && listener.context === context) {
|
||||
if (len === 1) {
|
||||
delete this._events[event];
|
||||
this._onRemoveEvent(event);
|
||||
} else {
|
||||
listeners.splice(i, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удаляет все обработчики всех событий или все обработчики переданного события `event`.
|
||||
*
|
||||
* @param {String} [event]
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
offAll: function (event) {
|
||||
if (this._events) {
|
||||
if (event) {
|
||||
if (this._events[event]) {
|
||||
delete this._events[event];
|
||||
this._onRemoveEvent(event);
|
||||
}
|
||||
} else {
|
||||
for (event in this._events) {
|
||||
if (this._events.hasOwnProperty(event)) {
|
||||
this._onRemoveEvent(event);
|
||||
}
|
||||
}
|
||||
delete this._events;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Исполняет все обработчики события `event`.
|
||||
*
|
||||
* @param {String} event
|
||||
* @param {...*} [args] Аргументы, которые будут переданы в обработчики события.
|
||||
* @returns {YEventEmitter}
|
||||
*/
|
||||
emit: function (event) {
|
||||
if (!this._events) {
|
||||
return this;
|
||||
}
|
||||
|
||||
var listeners = this._events[event];
|
||||
if (!listeners) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Копируем массив обработчиков, чтобы добавление/удаление обработчиков внутри колбэков не оказывало
|
||||
// влияния в цикле.
|
||||
var listenersCopy = listeners.slice(0);
|
||||
var len = listenersCopy.length;
|
||||
var listener;
|
||||
var i = -1;
|
||||
|
||||
switch (arguments.length) {
|
||||
// Оптимизируем наиболее частые случаи.
|
||||
case 1:
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.call(listener.context);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.call(listener.context, arguments[1]);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.call(listener.context, arguments[1], arguments[2]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
var args = slice.call(arguments, 1);
|
||||
while (++i < len) {
|
||||
listener = listenersCopy[i];
|
||||
listener.callback.apply(listener.context, args);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Вызывается когда было добавлено новое событие.
|
||||
*
|
||||
* @protected
|
||||
* @param {String} event
|
||||
*/
|
||||
_onAddEvent: function () {},
|
||||
|
||||
/**
|
||||
* Вызывается когда все обработчики события были удалены.
|
||||
*
|
||||
* @protected
|
||||
* @param {String} event
|
||||
*/
|
||||
_onRemoveEvent: function () {}
|
||||
});
|
||||
|
||||
provide(YEventEmitter);
|
||||
});
|
||||
7
client/islets/core/y-event-emitter/y-event-emitter.md
Normal file
7
client/islets/core/y-event-emitter/y-event-emitter.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# y-event-emitter: эмиттер
|
||||
|
||||
Предоставляет базовый класс для сущностей, кидающих на себе события.
|
||||
|
||||
## API класса
|
||||
|
||||
<!--JS_API-->
|
||||
403
client/islets/core/y-event-emitter/y-event-emitter.test.js
Normal file
403
client/islets/core/y-event-emitter/y-event-emitter.test.js
Normal file
@@ -0,0 +1,403 @@
|
||||
modules.define(
|
||||
'test',
|
||||
['y-event-emitter'],
|
||||
function (provide, YEventEmitter) {
|
||||
|
||||
describe('YEventEmitter', function () {
|
||||
var emitter;
|
||||
|
||||
beforeEach(function () {
|
||||
emitter = new YEventEmitter();
|
||||
});
|
||||
|
||||
function testWrongCallbacks(action) {
|
||||
var wrongCallbacks = [
|
||||
undefined,
|
||||
null,
|
||||
0,
|
||||
'',
|
||||
[],
|
||||
{},
|
||||
/\w/
|
||||
];
|
||||
|
||||
wrongCallbacks.forEach(function (wrongCallback) {
|
||||
var fn = function () {
|
||||
action(wrongCallback);
|
||||
};
|
||||
fn.should.throw(TypeError, 'callback must be a function');
|
||||
});
|
||||
}
|
||||
|
||||
describe('on()', function () {
|
||||
it('should add event listeners', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy1_1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.on('event1', spy1)
|
||||
.on('event1', spy1_1)
|
||||
.on('event2', spy2)
|
||||
.emit('event1');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy1.firstCall.calledWithExactly().should.be.true;
|
||||
|
||||
spy1_1.calledOnce.should.be.true;
|
||||
spy2.called.should.be.false;
|
||||
|
||||
emitter.emit('event2', 2, 3, 'foo');
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy2.firstCall.calledWithExactly(2, 3, 'foo').should.be.true;
|
||||
|
||||
var obj = {a: 'b'};
|
||||
emitter.emit('event1', obj);
|
||||
spy1.calledTwice.should.be.true;
|
||||
spy1.secondCall.calledWithExactly(obj).should.be.true;
|
||||
spy1_1.calledTwice.should.be.true;
|
||||
spy1_1.secondCall.calledWithExactly(obj).should.be.true;
|
||||
});
|
||||
|
||||
it('should add event listener with context', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var context1 = {foo: 1};
|
||||
var spy2 = sinon.spy();
|
||||
var context2 = {bar: 2};
|
||||
|
||||
emitter.on('event', spy1, context1);
|
||||
emitter.on('event', spy2, context2);
|
||||
|
||||
emitter.emit('event');
|
||||
|
||||
spy1.firstCall.calledOn(context1).should.be.true;
|
||||
spy2.firstCall.calledOn(context2).should.be.true;
|
||||
});
|
||||
|
||||
it('should can add the same listener many times', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var ctx = {};
|
||||
|
||||
emitter
|
||||
.on('event', spy1)
|
||||
.on('event', spy1)
|
||||
.on('event', spy2, ctx)
|
||||
.on('event', spy2, ctx)
|
||||
.emit('event');
|
||||
|
||||
spy1.callCount.should.eq(2);
|
||||
spy2.callCount.should.eq(2);
|
||||
spy2.alwaysCalledOn(ctx).should.be.true;
|
||||
});
|
||||
|
||||
it('should throw error if callback is not a function', function () {
|
||||
testWrongCallbacks(function (callback) {
|
||||
emitter.on('event', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('once()', function () {
|
||||
it('should add a single-shot listener', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy)
|
||||
.emit('event')
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy.calledOnce.should.be.true;
|
||||
});
|
||||
|
||||
it('should add a single-shot listener with context', function () {
|
||||
var ctx1 = {};
|
||||
var spy1 = sinon.spy();
|
||||
var ctx2 = {};
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy1, ctx1)
|
||||
.once('event', spy2, ctx2)
|
||||
.emit('event')
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy1.firstCall.calledOn(ctx1).should.be.true;
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy2.firstCall.calledOn(ctx2).should.be.true;
|
||||
});
|
||||
|
||||
it('should throw error if callback is not a function', function () {
|
||||
testWrongCallbacks(function (callback) {
|
||||
emitter.once('event', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('emit()', function () {
|
||||
it('should work before add any event', function () {
|
||||
emitter.emit('event', 1, 2).should.eq(emitter);
|
||||
});
|
||||
|
||||
describe('while emiting event', function () {
|
||||
it('should not call listener that was added in another listener', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter.on('event', function () {
|
||||
emitter.on('event', spy);
|
||||
});
|
||||
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.false;
|
||||
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should call listener that was removed in another listener', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter.on('event', spy);
|
||||
emitter.on('event', function () {
|
||||
emitter.off('event', spy);
|
||||
});
|
||||
|
||||
emitter.emit('event');
|
||||
spy.calledOnce.should.be.true;
|
||||
spy.reset();
|
||||
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('off()', function () {
|
||||
it('should remove listener according to event', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.on('event', spy1)
|
||||
.on('event', spy2)
|
||||
.on('event2', spy1)
|
||||
.off('event', spy1)
|
||||
.off('event2', spy2)
|
||||
.emit('event');
|
||||
|
||||
spy1.called.should.be.false;
|
||||
spy2.called.should.be.true;
|
||||
|
||||
emitter.emit('event2');
|
||||
spy1.called.should.be.true;
|
||||
});
|
||||
|
||||
it('should remove listener according to event and context', function () {
|
||||
var spy = sinon.spy();
|
||||
var ctx1 = {};
|
||||
var ctx2 = {};
|
||||
|
||||
emitter
|
||||
.on('event', spy, ctx1)
|
||||
.on('event', spy, ctx2)
|
||||
.on('event', spy)
|
||||
.off('event', spy, ctx1)
|
||||
.emit('event');
|
||||
|
||||
spy.callCount.should.eq(2);
|
||||
});
|
||||
|
||||
it('should remove once listener according to event', function () {
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy)
|
||||
.off('event', spy)
|
||||
.emit('event')
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
|
||||
it('should remove once listener according to event and context', function () {
|
||||
var ctx1 = {};
|
||||
var ctx2 = {};
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter
|
||||
.once('event', spy, ctx1)
|
||||
.once('event', spy, ctx2)
|
||||
.off('event', spy, ctx1)
|
||||
.off('event', spy)
|
||||
.emit('event')
|
||||
.emit('event');
|
||||
|
||||
spy.calledOnce.should.be.true;
|
||||
spy.firstCall.calledOn(ctx2).should.be.true;
|
||||
});
|
||||
|
||||
it('should work before add any event', function () {
|
||||
emitter.off('event', function () {}).should.eq(emitter);
|
||||
});
|
||||
|
||||
it('should remove first listener from the list of same listeners', function () {
|
||||
var spy = sinon.spy();
|
||||
emitter
|
||||
.on('event', spy)
|
||||
.on('event', spy)
|
||||
.on('event', spy);
|
||||
|
||||
emitter.off('event', spy);
|
||||
emitter.emit('event');
|
||||
spy.callCount.should.eq(2);
|
||||
spy.reset();
|
||||
|
||||
emitter.off('event', spy);
|
||||
emitter.emit('event');
|
||||
spy.callCount.should.eq(1);
|
||||
spy.reset();
|
||||
|
||||
emitter.off('event', spy);
|
||||
emitter.emit('event');
|
||||
spy.called.should.be.false;
|
||||
});
|
||||
|
||||
it('should throw error if callback is not a function', function () {
|
||||
testWrongCallbacks(function (callback) {
|
||||
emitter.off('event', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('offAll()', function () {
|
||||
it('should remove all listeners of all events', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
|
||||
emitter.on('event1', spy1);
|
||||
emitter.on('event2', spy2);
|
||||
|
||||
emitter.emit('event1');
|
||||
emitter.emit('event2');
|
||||
|
||||
emitter
|
||||
.offAll()
|
||||
.emit('event1')
|
||||
.emit('event2');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy2.calledOnce.should.be.true;
|
||||
});
|
||||
|
||||
it('should work before add any event', function () {
|
||||
emitter.offAll().should.eq(emitter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('offAll(event)', function () {
|
||||
it('should remove all listeners for the specified event', function () {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var spy3 = sinon.spy();
|
||||
|
||||
emitter
|
||||
.on('event1', spy1)
|
||||
.on('event1', spy2)
|
||||
.on('event2', spy1)
|
||||
.on('event2', spy3)
|
||||
.offAll('event2')
|
||||
.emit('event1')
|
||||
.emit('event2');
|
||||
|
||||
spy1.calledOnce.should.be.true;
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy3.called.should.be.false;
|
||||
});
|
||||
|
||||
it('should work before add any event', function () {
|
||||
emitter.offAll('event').should.eq(emitter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onAddEvent()', function () {
|
||||
it('should be called when new event was added', function () {
|
||||
var _onAddEvent = sinon.spy(emitter, '_onAddEvent');
|
||||
var fn1 = function () {};
|
||||
var fn2 = function () {};
|
||||
var fn3 = function () {};
|
||||
|
||||
emitter.on('event1', fn1);
|
||||
_onAddEvent.callCount.should.eq(1);
|
||||
_onAddEvent.getCall(0).calledWithExactly('event1').should.be.true;
|
||||
|
||||
emitter.on('event1', fn2);
|
||||
_onAddEvent.callCount.should.eq(1);
|
||||
|
||||
emitter.on('event2', fn1);
|
||||
_onAddEvent.callCount.should.eq(2);
|
||||
_onAddEvent.getCall(1).calledWithExactly('event2').should.be.true;
|
||||
|
||||
emitter.on('event1', fn3);
|
||||
emitter.on('event2', fn2);
|
||||
_onAddEvent.callCount.should.eq(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('_onRemoveEvent()', function () {
|
||||
var _onRemoveEvent;
|
||||
|
||||
beforeEach(function () {
|
||||
_onRemoveEvent = sinon.spy(emitter, '_onRemoveEvent');
|
||||
});
|
||||
|
||||
it('should be called when event was removed', function () {
|
||||
var fn1 = function () {};
|
||||
var fn2 = function () {};
|
||||
var fn3 = function () {};
|
||||
var fn4 = function () {};
|
||||
|
||||
emitter.on('event1', fn1);
|
||||
emitter.on('event1', fn2);
|
||||
|
||||
emitter.on('event2', fn3);
|
||||
emitter.on('event2', fn4);
|
||||
|
||||
emitter.off('event1', fn2);
|
||||
_onRemoveEvent.called.should.be.false;
|
||||
|
||||
emitter.off('event1', fn1);
|
||||
_onRemoveEvent.callCount.should.eq(1);
|
||||
_onRemoveEvent.getCall(0).calledWithExactly('event1').should.be.true;
|
||||
|
||||
emitter.offAll('event1');
|
||||
_onRemoveEvent.callCount.should.eq(1, 'should not be called for already removed event');
|
||||
|
||||
emitter.offAll('event2');
|
||||
_onRemoveEvent.callCount.should.eq(2);
|
||||
_onRemoveEvent.getCall(1).calledWithExactly('event2').should.be.true;
|
||||
});
|
||||
|
||||
describe('when remove all events using offAll()', function () {
|
||||
it('should be called for each removed event', function () {
|
||||
emitter
|
||||
.on('event1', function () {})
|
||||
.on('event2', function () {})
|
||||
.on('event2', function () {})
|
||||
.on('event3', function () {})
|
||||
.offAll();
|
||||
|
||||
_onRemoveEvent.callCount.should.eq(3);
|
||||
_onRemoveEvent.getCall(0).calledWithExactly('event1').should.be.true;
|
||||
_onRemoveEvent.getCall(1).calledWithExactly('event2').should.be.true;
|
||||
_onRemoveEvent.getCall(2).calledWithExactly('event3').should.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
138
client/islets/core/y-event-manager/y-event-manager.js
Normal file
138
client/islets/core/y-event-manager/y-event-manager.js
Normal file
@@ -0,0 +1,138 @@
|
||||
modules.define(
|
||||
'y-event-manager',
|
||||
[
|
||||
'inherit',
|
||||
'y-event-emitter',
|
||||
'jquery'
|
||||
],
|
||||
function (
|
||||
provide,
|
||||
inherit,
|
||||
YEventEmitter,
|
||||
$
|
||||
) {
|
||||
|
||||
/**
|
||||
* Адаптер для YEventEmitter, jQuery. Позволяет привязывать обработчики к разным эмиттерам событий
|
||||
* и отвязывать их, используя вызов одной функции. Менеджер всегда привязан к какому-либо объекту, который
|
||||
* является контекстом для всех обработчиков.
|
||||
*
|
||||
* Полезен, когда нужно отвязать все обработчики сразу. Например, при уничтожении объекта.
|
||||
*
|
||||
* @example
|
||||
* function UserView(model, el) {
|
||||
* this._eventManager = new YEventManager(this);
|
||||
*
|
||||
* // Привязываем обработчик к YEventEmitter
|
||||
* this._eventManager.bindTo(model, 'change-name', this._changeName);
|
||||
*
|
||||
* // Привязываем обработчик к jQuery объекту
|
||||
* var hideEl = el.find('.hide');
|
||||
* this._eventManager.bindTo(hideEl, 'click', this._hide);
|
||||
* }
|
||||
*
|
||||
* UserView.prototype.destruct = function () {
|
||||
* // Удаляем все обработчики
|
||||
* this._eventManager.unbindAll();
|
||||
* };
|
||||
*
|
||||
* UserView.prototype._changeName = function () {};
|
||||
*
|
||||
* UserView.prototype._hide = function () {};
|
||||
*/
|
||||
var YEventManager = inherit({
|
||||
/**
|
||||
* Создает менджер событий для переданного объекта.
|
||||
*
|
||||
* @param {Object} owner Контекст для всех обработчиков событий.
|
||||
*/
|
||||
__constructor: function (owner) {
|
||||
this._owner = owner;
|
||||
this._listeners = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Привязывает обработчик к переданному эмиттеру событий.
|
||||
*
|
||||
* @param {YEventEmitter|jQuery} emitter
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @returns {YEventManager}
|
||||
*/
|
||||
bindTo: function (emitter, event, callback) {
|
||||
if (emitter instanceof YEventEmitter) {
|
||||
this._listeners.push({
|
||||
type: 'islets',
|
||||
emitter: emitter.on(event, callback, this._owner),
|
||||
event: event,
|
||||
callback: callback
|
||||
});
|
||||
} else if (emitter instanceof $) {
|
||||
var proxy = callback.bind(this._owner);
|
||||
this._listeners.push({
|
||||
type: 'jquery',
|
||||
emitter: emitter.on(event, proxy),
|
||||
event: event,
|
||||
callback: callback,
|
||||
proxy: proxy
|
||||
});
|
||||
} else {
|
||||
throw new Error('Unsupported emitter type');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Отвязывает обработчик от переданного эмиттера событий.
|
||||
*
|
||||
* @param {YEventEmitter|jQuery} emitter
|
||||
* @param {String} event
|
||||
* @param {Function} callback
|
||||
* @returns {YEventManager}
|
||||
*/
|
||||
unbindFrom: function (emitter, event, callback) {
|
||||
for (var i = 0; i < this._listeners.length; i++) {
|
||||
var listener = this._listeners[i];
|
||||
if (listener.emitter === emitter &&
|
||||
listener.event === event &&
|
||||
listener.callback === callback
|
||||
) {
|
||||
this._unbind(listener);
|
||||
this._listeners.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Отвязывает все обработчики от всех эмиттеров событий.
|
||||
*
|
||||
* @returns {YEventManager}
|
||||
*/
|
||||
unbindAll: function () {
|
||||
while (this._listeners.length) {
|
||||
var listener = this._listeners.pop();
|
||||
this._unbind(listener);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Отвязывает обработчик события.
|
||||
*
|
||||
* @param {Object} listener
|
||||
*/
|
||||
_unbind: function (listener) {
|
||||
switch (listener.type) {
|
||||
case 'islets':
|
||||
listener.emitter.off(listener.event, listener.callback, this._owner);
|
||||
break;
|
||||
case 'jquery':
|
||||
listener.emitter.off(listener.event, listener.proxy);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
provide(YEventManager);
|
||||
});
|
||||
5
client/islets/core/y-event-manager/y-event-manager.md
Normal file
5
client/islets/core/y-event-manager/y-event-manager.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Адаптер для YEventEmitter, jQuery. Позволяет привязывать обработчики к разным эмиттерам событий
|
||||
и отвязывать их, используя вызов одной функции. Менеджер всегда привязан к какому-либо объекту, который
|
||||
является контекстом для всех обработчиков.
|
||||
|
||||
<!--JS_API-->
|
||||
267
client/islets/core/y-event-manager/y-event-manager.test.js
Normal file
267
client/islets/core/y-event-manager/y-event-manager.test.js
Normal file
@@ -0,0 +1,267 @@
|
||||
modules.define(
|
||||
'test',
|
||||
[
|
||||
'y-event-manager',
|
||||
'y-event-emitter',
|
||||
'jquery'
|
||||
],
|
||||
function (
|
||||
provide,
|
||||
YEventManager,
|
||||
YEventEmitter,
|
||||
$
|
||||
) {
|
||||
|
||||
describe('YEventManager', function () {
|
||||
var manager;
|
||||
var owner;
|
||||
|
||||
beforeEach(function () {
|
||||
owner = {};
|
||||
manager = new YEventManager(owner);
|
||||
});
|
||||
|
||||
describe('bindTo()', function () {
|
||||
it('should bind event listeners to YEventEmitter', function () {
|
||||
var emitter = new YEventEmitter();
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var spy3 = sinon.spy();
|
||||
|
||||
manager.bindTo(emitter, 'event1', spy1).should.eq(manager);
|
||||
manager.bindTo(emitter, 'event2', spy2);
|
||||
manager.bindTo(emitter, 'event2', spy3);
|
||||
|
||||
emitter.emit('event1', 1, 2);
|
||||
spy1.callCount.should.eq(1);
|
||||
spy1.calledWithExactly(1, 2).should.be.true;
|
||||
spy1.calledOn(owner).should.be.true;
|
||||
spy2.callCount.should.eq(0);
|
||||
spy3.callCount.should.eq(0);
|
||||
|
||||
emitter.emit('event2', 3, 4);
|
||||
spy1.callCount.should.eq(1);
|
||||
spy2.callCount.should.eq(1);
|
||||
spy2.calledWithExactly(3, 4).should.be.true;
|
||||
spy2.calledOn(owner).should.be.true;
|
||||
spy3.callCount.should.eq(1);
|
||||
spy3.calledWithExactly(3, 4).should.be.true;
|
||||
spy3.calledOn(owner).should.be.true;
|
||||
});
|
||||
|
||||
it('should bind event listeners to jQuery', function () {
|
||||
var jqObj = $({});
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var spy3 = sinon.spy();
|
||||
|
||||
manager.bindTo(jqObj, 'event1', spy1).should.eq(manager);
|
||||
manager.bindTo(jqObj, 'event2', spy2);
|
||||
manager.bindTo(jqObj, 'event2', spy3);
|
||||
|
||||
var data = {};
|
||||
jqObj.trigger('event1', data);
|
||||
spy1.callCount.should.eq(1);
|
||||
var args = spy1.getCall(0).args;
|
||||
args[1].should.eq(data);
|
||||
spy2.called.should.be.false;
|
||||
spy3.called.should.be.false;
|
||||
|
||||
jqObj.trigger('event2', [3, 4]);
|
||||
spy1.callCount.should.eq(1);
|
||||
spy2.callCount.should.eq(1);
|
||||
args = spy2.getCall(0).args;
|
||||
args[1].should.eq(3);
|
||||
args[2].should.eq(4);
|
||||
spy3.callCount.should.eq(1);
|
||||
args = spy3.getCall(0).args;
|
||||
args[1].should.eq(3);
|
||||
args[2].should.eq(4);
|
||||
});
|
||||
|
||||
it('should throw error for unsupported emitter type', function () {
|
||||
/* jshint -W068 */
|
||||
(function () {
|
||||
var FakeEmitter = {
|
||||
events: [],
|
||||
on: function () {}
|
||||
};
|
||||
manager.bindTo(FakeEmitter, 'event', function () {});
|
||||
}).should.throw(Error, 'Unsupported emitter type');
|
||||
});
|
||||
|
||||
it('should work with different emitters together', function () {
|
||||
var emitter = new YEventEmitter();
|
||||
var jqObj = $({});
|
||||
var emitterSpy1 = sinon.spy();
|
||||
var emitterSpy2 = sinon.spy();
|
||||
var jqSpy1 = sinon.spy();
|
||||
var jqSpy2 = sinon.spy();
|
||||
|
||||
manager.bindTo(emitter, 'event', emitterSpy2);
|
||||
manager.bindTo(jqObj, 'event', jqSpy1);
|
||||
manager.bindTo(jqObj, 'event', jqSpy2);
|
||||
manager.bindTo(emitter, 'event', emitterSpy1);
|
||||
|
||||
jqObj.trigger('event');
|
||||
jqSpy1.callCount.should.eq(1);
|
||||
jqSpy2.callCount.should.eq(1);
|
||||
emitterSpy1.callCount.should.eq(0);
|
||||
emitterSpy2.callCount.should.eq(0);
|
||||
|
||||
emitter.emit('event');
|
||||
jqSpy1.callCount.should.eq(1);
|
||||
jqSpy2.callCount.should.eq(1);
|
||||
emitterSpy1.callCount.should.eq(1);
|
||||
emitterSpy2.callCount.should.eq(1);
|
||||
|
||||
jqSpy1.alwaysCalledOn(owner);
|
||||
jqSpy2.alwaysCalledOn(owner);
|
||||
emitterSpy1.alwaysCalledOn(owner);
|
||||
emitterSpy2.alwaysCalledOn(owner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unbindFrom()', function () {
|
||||
function testUnbind(emitter, anotherEmitter, emitFn) {
|
||||
var spy1 = sinon.spy();
|
||||
var spy2 = sinon.spy();
|
||||
var spy3 = sinon.spy();
|
||||
|
||||
manager.bindTo(anotherEmitter, 'event1', spy1);
|
||||
manager.bindTo(emitter, 'event1', spy1);
|
||||
manager.bindTo(emitter, 'event1', spy2);
|
||||
manager.bindTo(emitter, 'event2', spy3);
|
||||
|
||||
manager.unbindFrom(emitter, 'event1', spy1).should.eq(manager);
|
||||
|
||||
emitter[emitFn]('event1');
|
||||
spy1.called.should.be.false;
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy2.calledOn(owner);
|
||||
|
||||
emitter[emitFn]('event2');
|
||||
spy3.calledOnce.should.be.true;
|
||||
spy3.calledOn(owner);
|
||||
|
||||
manager.unbindFrom(emitter, 'event1', spy2);
|
||||
manager.unbindFrom(emitter, 'event2', spy3);
|
||||
|
||||
emitter[emitFn]('event1');
|
||||
emitter[emitFn]('event2');
|
||||
|
||||
spy1.called.should.be.false;
|
||||
spy2.calledOnce.should.be.true;
|
||||
spy3.calledOnce.should.be.true;
|
||||
|
||||
anotherEmitter[emitFn]('event1');
|
||||
spy1.calledOnce.should.be.true;
|
||||
}
|
||||
|
||||
function testUnbindFirst(emitter, emitFn) {
|
||||
var spy = sinon.spy();
|
||||
|
||||
manager.bindTo(emitter, 'test', spy);
|
||||
manager.bindTo(emitter, 'test', spy);
|
||||
manager.bindTo(emitter, 'test', spy);
|
||||
manager.unbindFrom(emitter, 'test', spy);
|
||||
|
||||
emitter[emitFn]('test');
|
||||
spy.callCount.should.eq(2);
|
||||
|
||||
manager.unbindFrom(emitter, 'test', spy);
|
||||
emitter[emitFn]('test');
|
||||
spy.callCount.should.eq(3);
|
||||
|
||||
manager.unbindFrom(emitter, 'test', spy);
|
||||
emitter[emitFn]('test');
|
||||
spy.callCount.should.eq(3);
|
||||
}
|
||||
|
||||
it('should unbind event listeners from YEventEmitter', function () {
|
||||
var emitter1 = new YEventEmitter();
|
||||
var emitter2 = new YEventEmitter();
|
||||
testUnbind(emitter1, emitter2, 'emit');
|
||||
});
|
||||
|
||||
it('should unbind event listeners from jQuery', function () {
|
||||
var jqObj1 = $({});
|
||||
var jqObj2 = $({});
|
||||
testUnbind(jqObj1, jqObj2, 'trigger');
|
||||
});
|
||||
|
||||
it('should unbind first listener from list of same listeners', function () {
|
||||
var emitter = new YEventEmitter();
|
||||
testUnbindFirst(emitter, 'emit');
|
||||
var jqObj = $({});
|
||||
testUnbindFirst(jqObj, 'trigger');
|
||||
});
|
||||
|
||||
it('should work with different emitters together', function () {
|
||||
var emitter = new YEventEmitter();
|
||||
var jqObj = $({});
|
||||
|
||||
var emitterSpy1 = sinon.spy();
|
||||
var emitterSpy2 = sinon.spy();
|
||||
var jqSpy1 = sinon.spy();
|
||||
var jqSpy2 = sinon.spy();
|
||||
|
||||
manager.bindTo(emitter, 'event', emitterSpy2);
|
||||
manager.bindTo(jqObj, 'event', jqSpy1);
|
||||
manager.bindTo(jqObj, 'event', jqSpy2);
|
||||
manager.bindTo(emitter, 'event', emitterSpy1);
|
||||
|
||||
manager.unbindFrom(emitter, 'event', emitterSpy1);
|
||||
manager.unbindFrom(jqObj, 'event', jqSpy2);
|
||||
|
||||
jqObj.trigger('event');
|
||||
jqSpy1.callCount.should.eq(1);
|
||||
emitterSpy2.callCount.should.eq(0);
|
||||
|
||||
emitter.emit('event');
|
||||
jqSpy1.callCount.should.eq(1);
|
||||
emitterSpy2.callCount.should.eq(1);
|
||||
|
||||
emitterSpy1.called.should.be.false;
|
||||
jqSpy2.called.should.be.false;
|
||||
jqSpy1.alwaysCalledOn(owner);
|
||||
emitterSpy2.alwaysCalledOn(owner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unbindAll()', function () {
|
||||
it('should unbind all events from different emitters', function () {
|
||||
var emitter = new YEventEmitter();
|
||||
var jqObj = $({});
|
||||
var spy = sinon.spy();
|
||||
|
||||
emitter.on('event5', spy);
|
||||
jqObj.on('event5', spy);
|
||||
|
||||
manager.bindTo(emitter, 'event1', spy);
|
||||
manager.bindTo(emitter, 'event2', spy);
|
||||
manager.bindTo(emitter, 'event2', spy);
|
||||
manager.bindTo(jqObj, 'event3', spy);
|
||||
manager.bindTo(jqObj, 'event4', spy);
|
||||
manager.bindTo(jqObj, 'event4', spy);
|
||||
|
||||
manager.unbindAll().should.eq(manager);
|
||||
|
||||
emitter.emit('event1');
|
||||
emitter.emit('event2');
|
||||
|
||||
jqObj.trigger('event3');
|
||||
jqObj.trigger('event4');
|
||||
|
||||
spy.called.should.be.false;
|
||||
|
||||
emitter.emit('event5');
|
||||
spy.calledOnce.should.be.true;
|
||||
jqObj.trigger('event5');
|
||||
spy.calledTwice.should.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
80
client/islets/core/y-extend/y-extend.js
Normal file
80
client/islets/core/y-extend/y-extend.js
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Предоставляет функцию для расширения объектов.
|
||||
*/
|
||||
modules.define('y-extend', function (provide) {
|
||||
|
||||
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
/**
|
||||
* Проверяет, что переданный объект является "плоским" (т.е. созданным с помощью "{}"
|
||||
* или "new Object").
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
function isPlainObject(obj) {
|
||||
// Не являются плоским объектом:
|
||||
// - Любой объект или значение, чьё внутреннее свойство [[Class]] не равно "[object Object]"
|
||||
// - DOM-нода
|
||||
// - window
|
||||
return !(toString.call(obj) !== '[object Object]' ||
|
||||
obj.nodeType ||
|
||||
obj.window === window);
|
||||
}
|
||||
|
||||
/**
|
||||
* Копирует перечислимые свойства одного или нескольких объектов в целевой объект.
|
||||
*
|
||||
* @param {Boolean} [deep=false] При значении `true` свойства копируются рекурсивно.
|
||||
* @param {Object} target Объект для расширения. Он получит новые свойства.
|
||||
* @param {...Object} objects Объекты со свойствами для копирования. Аргументы со значениями
|
||||
* `null` или `undefined` игнорируются.
|
||||
* @returns {Object}
|
||||
*/
|
||||
provide(function extend() {
|
||||
var target = arguments[0];
|
||||
var deep;
|
||||
var i;
|
||||
|
||||
// Обрабатываем ситуацию глубокого копирования.
|
||||
if (typeof target === 'boolean') {
|
||||
deep = target;
|
||||
target = arguments[1];
|
||||
i = 2;
|
||||
} else {
|
||||
deep = false;
|
||||
i = 1;
|
||||
}
|
||||
|
||||
for (; i < arguments.length; i++) {
|
||||
var obj = arguments[i];
|
||||
if (!obj) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var key in obj) {
|
||||
if (hasOwnProperty.call(obj, key)) {
|
||||
var val = obj[key];
|
||||
var isArray = false;
|
||||
|
||||
// Копируем "плоские" объекты и массивы рекурсивно.
|
||||
if (deep && val && (isPlainObject(val) || (isArray = Array.isArray(val)))) {
|
||||
var src = target[key];
|
||||
var clone;
|
||||
if (isArray) {
|
||||
clone = src && Array.isArray(src) ? src : [];
|
||||
} else {
|
||||
clone = src && isPlainObject(src) ? src : {};
|
||||
}
|
||||
target[key] = extend(deep, clone, val);
|
||||
} else {
|
||||
target[key] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
});
|
||||
});
|
||||
72
client/islets/core/y-extend/y-extend.test.js
Normal file
72
client/islets/core/y-extend/y-extend.test.js
Normal file
@@ -0,0 +1,72 @@
|
||||
modules.define('test', ['y-extend'], function (provide, extend) {
|
||||
|
||||
describe('extend', function () {
|
||||
it('should return target object', function () {
|
||||
var target = {a: true};
|
||||
extend(target).should.eq(target);
|
||||
});
|
||||
|
||||
it('should copy properties of one object to target object', function () {
|
||||
var source = {num: 1, str: 'str', obj: {b: 2}, arr: null, undef: undefined};
|
||||
var sourceCopy = {num: 1, str: 'str', obj: {b: 2}, arr: null, undef: undefined};
|
||||
|
||||
var destination = {num: 2, newstr: 'newstr', obj: {a: 1}, arr: [1, 2]};
|
||||
|
||||
extend(destination, source);
|
||||
destination.should.deep.eq({
|
||||
num: 1, str: 'str', newstr: 'newstr', obj: {b: 2}, arr: null, undef: undefined
|
||||
});
|
||||
source.should.deep.eq(sourceCopy);
|
||||
});
|
||||
|
||||
it('should copy properties of many objects to target object', function () {
|
||||
var source1 = {a: 1, b: 2};
|
||||
var source1Copy = {a: 1, b: 2};
|
||||
|
||||
var source2 = {b: 3, c: {y: 2}};
|
||||
var source2Copy = {b: 3, c: {y: 2}};
|
||||
|
||||
var destination = {d: 'str', c: {x: 1}};
|
||||
|
||||
extend(destination, source1, null, source2);
|
||||
destination.should.deep.eq({d: 'str', c: {y: 2}, a: 1, b: 3});
|
||||
source1.should.deep.eq(source1Copy);
|
||||
source2.should.deep.eq(source2Copy);
|
||||
|
||||
extend(destination, source2, undefined, source2, source1);
|
||||
destination.should.deep.eq({d: 'str', c: {y: 2}, a: 1, b: 2});
|
||||
source1.should.deep.eq(source1Copy);
|
||||
source2.should.deep.eq(source2Copy);
|
||||
});
|
||||
|
||||
it('should properly extend object with "hasOwnProperty" property', function () {
|
||||
/* jshint -W001 */
|
||||
extend({hasOwnProperty: 1}, {hasOwnProperty: 'yes'}).should.deep.eq({hasOwnProperty: 'yes'});
|
||||
});
|
||||
|
||||
describe('deep extend', function () {
|
||||
it('should copy recursively plain objects and arrays', function () {
|
||||
var deep1 = {foo: {bar: true}, arr: [1, 2]};
|
||||
var deep1Copy = {foo: {bar: true}, arr: [1, 2]};
|
||||
|
||||
var deep2 = {foo: {baz: true}, arr: [1, 3, 4]};
|
||||
var deep2Copy = {foo: {baz: true}, arr: [1, 3, 4]};
|
||||
|
||||
extend(true, {}, deep1, deep2).should.deep.eq({foo: {bar: true, baz: true}, arr: [1, 3, 4]});
|
||||
deep1.should.deep.eq(deep1Copy);
|
||||
deep2.should.deep.eq(deep2Copy);
|
||||
});
|
||||
|
||||
it('should not copy recursively not plain objects', function () {
|
||||
var obj = {date: new Date(), div: document.createElement('div'), window: window};
|
||||
var target = {};
|
||||
extend(true, target, obj);
|
||||
target.date.should.eq(obj.date);
|
||||
target.div.should.eq(obj.div);
|
||||
target.window.should.eq(obj.window);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
48
client/islets/core/y-focus-holder/y-focus-holder.js
Normal file
48
client/islets/core/y-focus-holder/y-focus-holder.js
Normal file
@@ -0,0 +1,48 @@
|
||||
modules.define(
|
||||
'y-focus-holder',
|
||||
['inherit', 'jquery', 'y-event-emitter'],
|
||||
function (provide, inherit, $, YEventEmitter) {
|
||||
|
||||
var YFocusHolder = inherit(YEventEmitter, {
|
||||
__constructor: function () {
|
||||
this._domElement = $('<button>focus</button>');
|
||||
this._domElement.css({
|
||||
position: 'absolute',
|
||||
top: '-1000px',
|
||||
left: '-1000px'
|
||||
});
|
||||
this._focused = false;
|
||||
},
|
||||
|
||||
focus: function () {
|
||||
if (this._focused) {
|
||||
return;
|
||||
}
|
||||
this.emit('focus');
|
||||
this._domElement.on('blur', this._onBlur.bind(this));
|
||||
this._domElement.appendTo(document.body);
|
||||
this._domElement.focus();
|
||||
this._focused = true;
|
||||
},
|
||||
|
||||
blur: function () {
|
||||
if (!this._focused) {
|
||||
return;
|
||||
}
|
||||
this._domElement.blur();
|
||||
},
|
||||
|
||||
_onBlur: function () {
|
||||
this.emit('blur');
|
||||
this._domElement.remove();
|
||||
this._focused = false;
|
||||
},
|
||||
|
||||
destruct: function () {
|
||||
this._domElement.remove();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
provide(YFocusHolder);
|
||||
});
|
||||
30
client/islets/core/y-i18n/y-i18n.js
Normal file
30
client/islets/core/y-i18n/y-i18n.js
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Возвращает значение ключа для переданного кейсета.
|
||||
*
|
||||
* @name i18n
|
||||
* @param {String} keyset
|
||||
* @param {String} key
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Добавляет кейсет в хранилище.
|
||||
*
|
||||
* @name i18n.add
|
||||
* @param {String} keyset
|
||||
* @param {Object} keysetData
|
||||
*/
|
||||
|
||||
/**
|
||||
* Устанавливает текущий язык.
|
||||
*
|
||||
* @name i18n.setLanguage
|
||||
* @param {String} language
|
||||
*/
|
||||
|
||||
/**
|
||||
* Возвращает текущий язык.
|
||||
*
|
||||
* @name i18n.getLanguage
|
||||
* @returns {String}
|
||||
*/
|
||||
8
client/islets/core/y-i18n/y-i18n.md
Normal file
8
client/islets/core/y-i18n/y-i18n.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# y-i18n: интернационализация
|
||||
|
||||
Модуль `y-i18n` возвращает функцию `i18n`, которая используется для локализации сервисов.
|
||||
Функция `i18n` в свою очередь имеет ряд методов, для взаимодействия с кейсетами и языками.
|
||||
|
||||
Работа данного класса напрямую связана с технологией `y-i18n-lang-js`, которая находится в `islets/.bem/techs`.
|
||||
|
||||
<!--JS_API-->
|
||||
66
client/islets/core/y-load-script/y-load-script.js
Normal file
66
client/islets/core/y-load-script/y-load-script.js
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Загружает js-файлы добавляя тэг <script> в DOM.
|
||||
*/
|
||||
modules.define('y-load-script', function (provide) {
|
||||
var loading = {};
|
||||
var loaded = {};
|
||||
var head = document.getElementsByTagName('head')[0];
|
||||
|
||||
/**
|
||||
* @param {String} path
|
||||
*/
|
||||
function onLoad(path) {
|
||||
loaded[path] = true;
|
||||
var cbs = loading[path];
|
||||
delete loading[path];
|
||||
cbs.forEach(function (cb) {
|
||||
cb();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает js-файл по переданному пути `path` и вызывает
|
||||
* колбэк `cb` по окончании загрузки.
|
||||
*
|
||||
* @name loadScript
|
||||
* @param {String} path
|
||||
* @param {Function} cb
|
||||
*/
|
||||
provide(function (path, cb) {
|
||||
if (loaded[path]) {
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
if (loading[path]) {
|
||||
loading[path].push(cb);
|
||||
return;
|
||||
}
|
||||
|
||||
loading[path] = [cb];
|
||||
|
||||
var script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.charset = 'utf-8';
|
||||
// Добавляем `http:` к `//` если страница была открыта, используя `file://`-протокол.
|
||||
// Полезно для тестирования через PhantomJS, локальной отладки с внешними скриптами.
|
||||
script.src = (location.protocol === 'file:' && path.indexOf('//') === 0 ? 'http:' : '') + path;
|
||||
|
||||
if (script.onreadystatechange === null) {
|
||||
script.onreadystatechange = function () {
|
||||
var readyState = this.readyState;
|
||||
if (readyState === 'loaded' || readyState === 'complete') {
|
||||
script.onreadystatechange = null;
|
||||
onLoad(path);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
script.onload = script.onerror = function () {
|
||||
script.onload = script.onerror = null;
|
||||
onLoad(path);
|
||||
};
|
||||
}
|
||||
|
||||
head.insertBefore(script, head.lastChild);
|
||||
});
|
||||
});
|
||||
5
client/islets/core/y-load-script/y-load-script.md
Normal file
5
client/islets/core/y-load-script/y-load-script.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# y-load-script:
|
||||
|
||||
Модуль `y-load-script` возвращает функцию `loadScript`, которая загружает js-файлы добавляя тэг `<script>` в DOM.
|
||||
|
||||
<!--JS_API-->
|
||||
96
client/islets/core/y-next-tick/y-next-tick.js
Normal file
96
client/islets/core/y-next-tick/y-next-tick.js
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* next-tick module
|
||||
*
|
||||
* Copyright (c) 2013 Filatov Dmitry (dfilatov@yandex-team.ru)
|
||||
* Dual licensed under the MIT and GPL licenses:
|
||||
* http://www.opensource.org/licenses/mit-license.php
|
||||
* http://www.gnu.org/licenses/gpl.html
|
||||
*
|
||||
* @version 1.0.1
|
||||
*/
|
||||
|
||||
modules.define('y-next-tick', function(provide) {
|
||||
|
||||
/**
|
||||
* Вызывает переданную функцию в следующем тике.
|
||||
*
|
||||
* @name nextTick
|
||||
* @param {Function} callback
|
||||
*/
|
||||
|
||||
var global = this.global,
|
||||
fns = [],
|
||||
enqueueFn = function(fn) {
|
||||
return fns.push(fn) === 1;
|
||||
},
|
||||
callFns = function() {
|
||||
var fnsToCall = fns, i = 0, len = fns.length;
|
||||
fns = [];
|
||||
while(i < len) {
|
||||
fnsToCall[i++]();
|
||||
}
|
||||
};
|
||||
|
||||
if(typeof process === 'object' && process.nextTick) { // nodejs
|
||||
return provide(function(fn) {
|
||||
enqueueFn(fn) && process.nextTick(callFns);
|
||||
});
|
||||
}
|
||||
|
||||
if(global.setImmediate) { // ie10
|
||||
return provide(function(fn) {
|
||||
enqueueFn(fn) && global.setImmediate(callFns);
|
||||
});
|
||||
}
|
||||
|
||||
if(global.postMessage) { // modern browsers
|
||||
var isPostMessageAsync = true;
|
||||
if(global.attachEvent) {
|
||||
var checkAsync = function() {
|
||||
isPostMessageAsync = false;
|
||||
};
|
||||
global.attachEvent('onmessage', checkAsync);
|
||||
global.postMessage('__checkAsync', '*');
|
||||
global.detachEvent('onmessage', checkAsync);
|
||||
}
|
||||
|
||||
if(isPostMessageAsync) {
|
||||
var msg = '__nextTick' + +new Date,
|
||||
onMessage = function(e) {
|
||||
if(e.data === msg) {
|
||||
e.stopPropagation && e.stopPropagation();
|
||||
callFns();
|
||||
}
|
||||
};
|
||||
|
||||
global.addEventListener?
|
||||
global.addEventListener('message', onMessage, true) :
|
||||
global.attachEvent('onmessage', onMessage);
|
||||
|
||||
return provide(function(fn) {
|
||||
enqueueFn(fn) && global.postMessage(msg, '*');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var doc = global.document;
|
||||
if('onreadystatechange' in doc.createElement('script')) { // ie6-ie8
|
||||
var createScript = function() {
|
||||
var script = doc.createElement('script');
|
||||
script.onreadystatechange = function() {
|
||||
script.parentNode.removeChild(script);
|
||||
script = script.onreadystatechange = null;
|
||||
callFns();
|
||||
};
|
||||
(doc.documentElement || doc.body).appendChild(script);
|
||||
};
|
||||
|
||||
return provide(function(fn) {
|
||||
enqueueFn(fn) && createScript();
|
||||
});
|
||||
}
|
||||
|
||||
provide(function(fn) { // old browsers
|
||||
enqueueFn(fn) && setTimeout(callFns, 0);
|
||||
});
|
||||
});
|
||||
5
client/islets/core/y-next-tick/y-next-tick.md
Normal file
5
client/islets/core/y-next-tick/y-next-tick.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# y-next-tick:
|
||||
|
||||
Вызывает переданную функцию в следующем тике.
|
||||
|
||||
<!--JS_API-->
|
||||
57
client/islets/core/y-throttle/y-throttle.js
Normal file
57
client/islets/core/y-throttle/y-throttle.js
Normal file
@@ -0,0 +1,57 @@
|
||||
modules.define('y-throttle', function (provide) {
|
||||
/**
|
||||
* Возвращает новую функцию, которая при повторных вызовах,
|
||||
* вызывает функцию func не чаще одного раза в заданный
|
||||
* промежуток wait.
|
||||
*
|
||||
* Полезна для использования при обработке событий, которые
|
||||
* происходят слишком часто.
|
||||
*
|
||||
* @name throttle
|
||||
* @param {Function} func
|
||||
* @param {Number} wait Минимальный промежуток времени в миллисекундах,
|
||||
* который должен пройти между вызовами func.
|
||||
* @param {Object} [options]
|
||||
* @param {Boolean} [options.leading=true] Включает исполнение функции вначале.
|
||||
* @param {Boolean} [options.trailing=true] Включает исполнение функции вконце.
|
||||
* @returns {Function}
|
||||
*
|
||||
* @example
|
||||
* var updatePosition = function () {};
|
||||
* var throttled = throttle(updatePosition, 100);
|
||||
* $(window).scroll(throttled);
|
||||
*/
|
||||
provide(function (func, wait, options) {
|
||||
var context;
|
||||
var args;
|
||||
var result;
|
||||
var timeout = null;
|
||||
var previous = 0;
|
||||
options = options || {};
|
||||
|
||||
var later = function () {
|
||||
previous = options.leading === false ? 0 : Date.now();
|
||||
timeout = null;
|
||||
result = func.apply(context, args);
|
||||
};
|
||||
|
||||
return function () {
|
||||
var now = Date.now();
|
||||
if (!previous && options.leading === false) {
|
||||
previous = now;
|
||||
}
|
||||
var remaining = wait - (now - previous);
|
||||
context = this;
|
||||
args = arguments;
|
||||
if (remaining <= 0) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
previous = now;
|
||||
result = func.apply(context, args);
|
||||
} else if (!timeout && options.trailing !== false) {
|
||||
timeout = setTimeout(later, remaining);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
});
|
||||
});
|
||||
7
client/islets/core/y-throttle/y-throttle.md
Normal file
7
client/islets/core/y-throttle/y-throttle.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# y-throttle:
|
||||
|
||||
Модуль `y-throttle` возвращает функцию `throttle`,
|
||||
которая ограничивает количество выполненных действий в заданном интервале времени.
|
||||
Подробности по клику на функции.
|
||||
|
||||
<!--JS_API-->
|
||||
88
client/islets/core/y-throttle/y-throttle.test.js
Normal file
88
client/islets/core/y-throttle/y-throttle.test.js
Normal file
@@ -0,0 +1,88 @@
|
||||
modules.define('test', ['y-throttle'], function (provide, throttle) {
|
||||
|
||||
describe('throttle', function () {
|
||||
it('should throttle given function', function (done) {
|
||||
var res = [];
|
||||
var throttledFn = throttle(function (arg) {
|
||||
res.push(arg);
|
||||
}, 20);
|
||||
|
||||
throttledFn(1);
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
|
||||
setTimeout(function () {
|
||||
throttledFn(4);
|
||||
}, 10);
|
||||
|
||||
setTimeout(function () {
|
||||
throttledFn(5);
|
||||
res.should.deep.eq([1, 4]);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
it('should not trigger leading call when option "leading" is set to false', function (done) {
|
||||
var res = [];
|
||||
var throttledFn = throttle(function (arg) {
|
||||
res.push(arg);
|
||||
}, 20, {leading: false});
|
||||
|
||||
throttledFn(1);
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
|
||||
setTimeout(function () {
|
||||
throttledFn(4);
|
||||
}, 10);
|
||||
|
||||
setTimeout(function () {
|
||||
throttledFn(5);
|
||||
res.should.deep.eq([4]);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
it('should not trigger trailing call when option "trailing" is set to false', function (done) {
|
||||
var res = [];
|
||||
var throttledFn = throttle(function (arg) {
|
||||
res.push(arg);
|
||||
}, 20, {trailing: false});
|
||||
|
||||
throttledFn(1);
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
|
||||
setTimeout(function () {
|
||||
throttledFn(4);
|
||||
}, 10);
|
||||
|
||||
setTimeout(function () {
|
||||
res.should.deep.eq([1]);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
|
||||
it('should not trigger leading and trailing calls when both options are set to false', function (done) {
|
||||
var res = [];
|
||||
var throttledFn = throttle(function (arg) {
|
||||
res.push(arg);
|
||||
}, 20, {leading: false, trailing: false});
|
||||
|
||||
throttledFn(1);
|
||||
throttledFn(2);
|
||||
throttledFn(3);
|
||||
|
||||
setTimeout(function () {
|
||||
throttledFn(4);
|
||||
}, 10);
|
||||
|
||||
setTimeout(function () {
|
||||
res.should.deep.eq([]);
|
||||
done();
|
||||
}, 30);
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
44
client/islets/core/y-unique-id/y-unique-id.js
Normal file
44
client/islets/core/y-unique-id/y-unique-id.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Модуль для генерации уникальных идентификаторов.
|
||||
*/
|
||||
modules.define('y-unique-id', function (provide) {
|
||||
|
||||
// Префикс имеет 3 применения:
|
||||
// - гарантирует уникальность идентификаторов для каждой загрузки страницы
|
||||
// - имя свойства, в котором хранятся id, выданные объектам
|
||||
// - уникальный id для window
|
||||
var prefix = 'id_' + Date.now() + Math.round(Math.random() * 10000);
|
||||
var counterId = 0;
|
||||
|
||||
provide({
|
||||
/**
|
||||
* Генерирует уникальный идентификатор.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
generate: function () {
|
||||
return prefix + (++counterId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Генерирует уникальный идентификатор и присваивает его переданному объекту.
|
||||
* Если объект уже имеет идентификатор, просто возвращает его.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @returns {String}
|
||||
*/
|
||||
identify: function (obj) {
|
||||
return obj === window ? prefix : obj[prefix] || (obj[prefix] = this.generate());
|
||||
},
|
||||
|
||||
/**
|
||||
* Возвращает `true`, если объект имеет уникальный идентификатор.
|
||||
*
|
||||
* @param {Object} obj
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isIdentified: function (obj) {
|
||||
return obj.hasOwnProperty(prefix);
|
||||
}
|
||||
});
|
||||
});
|
||||
60
client/islets/core/y-unique-id/y-unique-id.test.js
Normal file
60
client/islets/core/y-unique-id/y-unique-id.test.js
Normal file
@@ -0,0 +1,60 @@
|
||||
modules.define(
|
||||
'test',
|
||||
['y-unique-id'],
|
||||
function (provide, uniqueId) {
|
||||
|
||||
var should = chai.should();
|
||||
|
||||
describe('uniqueId', function () {
|
||||
describe('generate()', function () {
|
||||
it('should generate unique id on each call', function () {
|
||||
var id1 = uniqueId.generate();
|
||||
var id2 = uniqueId.generate();
|
||||
var id3 = uniqueId.generate();
|
||||
|
||||
should.exist(id1);
|
||||
should.exist(id2);
|
||||
should.exist(id3);
|
||||
|
||||
id1.should.not.eq(id2);
|
||||
id1.should.not.eq(id3);
|
||||
id2.should.not.eq(id3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('identify()', function () {
|
||||
it('should generate different ids for different objects', function () {
|
||||
var obj1 = {};
|
||||
var obj2 = {};
|
||||
var id1 = uniqueId.identify(obj1);
|
||||
var id2 = uniqueId.identify(obj2);
|
||||
id1.should.not.eq(id2);
|
||||
});
|
||||
|
||||
it('should generate same id for same objects', function () {
|
||||
var obj = {};
|
||||
var id1 = uniqueId.identify(obj);
|
||||
var id2 = uniqueId.identify(obj);
|
||||
id1.should.eq(id2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isIdentified()', function () {
|
||||
it('should return true if object has unique id ', function () {
|
||||
var obj = {};
|
||||
uniqueId.isIdentified(obj).should.be.false;
|
||||
uniqueId.identify(obj);
|
||||
uniqueId.isIdentified(obj).should.be.true;
|
||||
});
|
||||
|
||||
it('should check own object\'s property', function () {
|
||||
function Custom() {}
|
||||
uniqueId.identify(Custom.prototype);
|
||||
var custom = new Custom();
|
||||
uniqueId.isIdentified(custom).should.be.false;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
provide();
|
||||
});
|
||||
Reference in New Issue
Block a user