This commit is contained in:
Oleg Mokhov
2015-06-20 12:26:08 +03:00
committed by mokhov
parent a716969f4e
commit f3546ef3a5
85 changed files with 16682 additions and 1 deletions

26
.enb/make.js Normal file
View File

@@ -0,0 +1,26 @@
module.exports = function(config) {
config.includeConfig('enb-bevis-helper');
var browserSupport = [
'IE >= 9',
'Safari >= 5',
'Chrome >= 33',
'Opera >= 12.16',
'Firefox >= 28'
];
var bevisHelper = config.module('enb-bevis-helper')
.browserSupport(browserSupport)
.useAutopolyfiller();
config.setLanguages(['ru']);
config.node('build/index', function (nodeConfig) {
bevisHelper
.sourceDeps('index')
.sources({profile: 'index'})
.forStaticHtmlPage()
//.forServerPage()
.configureNode(nodeConfig);
});
};

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
node_modules
.enb/tmp
build/index/*
!build/index/i
!build/index/index.btjson.js

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
# On development servers we have to use versioned node and a unix-socket.
# Otherwise (for local development) we use the /usr/bin/node and a web-socket (localhost:<port>).
# Bellow you can see this detection.
SERVER_NODE := /opt/nodejs/0.10/bin/node
SERVER_NPM := /opt/nodejs/0.10/bin/npm
LOCAL_NODE := node
LOCAL_NPM := npm
NODE := $(firstword $(shell which $(SERVER_NODE) $(LOCAL_NODE)))
NPM := $(firstword $(shell which $(SERVER_NPM) $(LOCAL_NPM)))
# if server node isn't found then specify PORT for local development
ifneq ($(NODE),$(SERVER_NODE))
PORT ?= 8080
endif
NODE_MODULES_BIN := node_modules/.bin
ENB := $(NODE_MODULES_BIN)/enb
#MOCHA_FLAGS ?= -R dot
all: npm build
# Install npm modules
npm:
@$(NPM) install
# Build project
build:
$(ENB) make $(ENB_FLAGS)
@cp -r lib build/index/lib
@mv build/index/index.ru.html build/index/index.html
# Clean build results
clean:
$(ENB) make clean
.PHONY: all install build clean

View File

@@ -1 +1,17 @@
# reader
# chitalka
Welcome to chitalka.js repository. It is a JavaScript-library to read fb2 books.
And [demo](http://chitalka.github.io/demo/), just drag-n-drop fb2-file at your computer to window and read! Or you can read Anna Karenina... :)
## Project Structure
```
.enb ENB configuration
build BTJSON template and build css, js files output
client blocks
client/core chitalka.js blocks
client/islets islets blocks
lib library files and xsl
```
## Build
Just type at your command line `make` then you need to add route to path `build/index` at your nginx (or etc) config and finally it works

Binary file not shown.

View File

@@ -0,0 +1,36 @@
module.exports = {
"block": "y-page",
"title": "Я.Читалка",
"styles": [
{"url": "index.css"}
],
"scripts": [
{"url": "index.{lang}.js"}
],
"body": [
{
block: 'y-block'
},
{
block: 'jquery'
},
{
"block": "chitalka-ui",
"book": {
"block": "chitalka-fb2",
"url": "i/Anna-Karenina.fb2.zip",
"progress": true,
"footnotes": "appendix",
"pages": "auto"
},
"progress": "read",
"progress_bar": true,
"controls": {
// Если параметр не передается - выставляем true иначе выставляем то, что передается
"zoom": true,
"arrows": true
},
"annotations": true
}
]
}

View File

@@ -0,0 +1,213 @@
chitalka_design() {
/* Два раза width, потому что деградация */
/*width: 700px;*/
/* 1ch = ширина буквы 0 в данном шрифте, подробнее тут https://developer.mozilla.org/ru/docs/Web/CSS/length */
/* заокомментировано на память, когда решим проблему */
/*width: 60ch;*/
/*width: 650px;*/
/* в Войне и мире начинают глючит вычисление whatPageIsDomElem */
/*width: 70ch;*/
/*max-width: 700px;*/ /* нужно было, когда размер был в ch */
/*height: 800px;*/
/* Для выравнивания по середие */
margin: 0 auto;
font: 15px/1.4 Arial, sans-serif;
overflow: hidden;
color: #333;
& p {
padding: 0;
margin: 0;
text-align: justify;
hyphens: auto;
}
& section {
/* ни в коем случае не использовать before, иначе баг в вебките! */
-webkit-column-break-after: always;
page-break-inside: avoid;
/* у неправильно размеченных книг и такое бывает */
& section {
-webkit-column-break-after: auto;
page-break-inside: unset;
}
/* Но если вдруг вложена глава, то применяем несколько иные правила */
& section.chapter {
-webkit-column-break-after: always;
page-break-inside: avoid;
}
/* Первая глава наверняка захочет приклеиться к названию части или истории */
& section.chapter:first-of-type {
column-break-after: auto;
page-break-inside: unset;
}
}
& .wrapper {
-webkit-column-break-after: always;
page-break-inside: avoid;
}
& .annotation {
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
}
._footnotes_inline & a.footnote {
text-decoration: none;
color: #333;
cursor: text;
}
._footnotes_inline & a.footnote:after {
content: " [" attr(title) "]";
}
._footnotes_inline & a sup {
display: none;
}
/* Первый параграф в section, где есть заголовок уровня h1 можно сделать без отступа */
& h1 ~ p:first-of-type,
/* Для первого параграфа в аннотации книги, где нет тэга h1*/
& section.book__annotation > p:first-of-type,
& h1 ~ section > p:first-of-type {
text-indent: 0;
}
& p {
text-indent: 1.3em;
}
& h1 {
display: block;
font-size: 2.2em;
-webkit-margin-before: 1.34em;
-webkit-margin-before: 0.67em;
font-weight: normal;
}
& section h1:first-of-type {
margin-top: 0;
}
& h2,
& h3,
& h4,
& h5 {
/* Делаем заранее огромный паддинг */
padding-bottom: 2em;
/* и притягиваем текст, достигается эффект, при котором не возникает разрыва страницы между заголовком и первым параграфом */
margin-bottom: -1em;
margin-top: 2em;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
}
& h2 + h2,
& h3 + h3,
& h4 + h4,
& h5 + h5 {
margin-top: -2em;
}
& .book__cover {
position: relative;
height: 100%;
}
& .book__cover img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
& .image-small {
margin-bottom: 20px;
}
& .image + br,
/**
* FIXME: https://st.yandex-team.ru/CHITALKA-85
*/
& .image + p + br {
display: none;
}
& .image {
display: flex;
overflow: hidden;
max-height: 100%;
max-width: 100%;
-webkit-column-break-inside: avoid;
page-break-inside: avoid;
flex-direction: column;
box-sizing: border-box;
padding: 1em 0;
}
& .image__wrapper {
max-width: 100%;
max-height: 100%;
-webkit-flex-shrink: 1;
flex-shrink: 1;
overflow: hidden;
}
& .image__annotation {
-webkit-flex-shrink: 0;
flex-shrink: 0;
padding: .5em 0;
& p {
text-align: center;
text-indent: 0;
}
}
& .image img {
display: block;
max-height: 100%;
max-width: 100%;
margin: 0 auto;
pointer-events: none;
user-select: none;
}
& .image + p {
display: none;
}
& .epigraph {
margin: 0 0 2em auto;
max-width: 20em;
text-align: right;
& .author {
margin: .5em 0 0;
}
& p {
display: inline-block;
max-width: inherit;
text-indent: 0;
text-align: justify;
}
}
}

View File

@@ -0,0 +1,206 @@
/* global escape */
modules.define(
'chitalka-fb2-parser',
[
'chitalka',
'jquery',
'inherit',
'y-extend',
'unzip'
],
function (
provide,
Chitalka,
$,
inherit,
extend,
zip
) {
var TIMEOUT = 2 * 1000;
/**
* Функция выполняет трансформацию строки в XMLDocument
* @param {String} text
*
* @returns {Document} XMLDocument
*/
var _parseXml = (function () {
var parseXml;
if (window.DOMParser) {
parseXml = function (xmlStr) {
return (new window.DOMParser()).parseFromString(xmlStr, 'text/xml');
};
} else if (typeof window.ActiveXObject !== 'undefined' && new window.ActiveXObject('Microsoft.XMLDOM')) {
parseXml = function (xmlStr) {
var xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM');
xmlDoc.async = 'false';
xmlDoc.loadXML(xmlStr);
return xmlDoc;
};
} else {
parseXml = function () {
return null;
};
}
return parseXml;
})();
/**
* По расщирению файла проверяет,
* запакованный файл или нет
* @param {string} url
* @returns {boolean}
* @private
*/
var _isZipArchive = function (url) {
return /(\.zip)$/i.test(url);
};
var parserFb2 = {
unzip: function (url, encoding) {
var d = $.Deferred();
var isBase64 = /^data:/.test(url);
// наша ручка /data/ проксируется на http://partnersdnld.litres.ru/static/trials
//url = url.replace('http://partnersdnld.litres.ru/static/trials', '/data');
zip.workerScriptsPath = window.document.location.pathname + 'lib/';
zip.createReader((isBase64 ? new zip.Data64URIReader(url) : new zip.HttpReader(url)), function (reader) {
// get all entries from the zip
reader.getEntries(function (entries) {
if (!entries.length) {
return;
}
// get first entry content as text
entries[0].getData(new zip.TextWriter(encoding), function (str) {
// close the zip reader
reader.close(function () {
// onclose callback
d.resolve(str);
});
}, function (/*current, total*/) {
// onprogress callback
});
});
}, function (error) {
// onerror callback
d.reject(error);
});
return d.promise();
},
getXml: function (xmlStr) {
var d = $.Deferred();
var xml = _parseXml(xmlStr);
d.resolve(xml);
return d.promise();
},
/**
* Читает файл по урлу или DataURI,
* если файл в архиве - распаковывает.
* @param {string} obj
* @returns {Promise}
*/
readFile: function (obj) {
var url = obj.file ? obj.result : obj;
if (/^data:/.test(url)) {
return this._readAsDataUri(obj);
}
if (_isZipArchive(url)) {
return this.unzip(url);
}
return $.ajax({
url: url,
dataType: 'text',
contentType: 'text/plain',
timeout: TIMEOUT
});
},
/**
* Читает файл по DataURI
* @param {Object} obj
* @param {String} obj.url данные из файла
* @param {Blob} obj.file файл для чтения
*
* @returns {Promise}
*/
_readAsDataUri: function (obj) {
var file = obj.file;
var url = file ? obj.result : obj;
var mediaInfo = url.split(',')[0];
var data = url.substring(url.indexOf(',') + 1);
var encodingRegExp = /encoding=\"UTF\-8\"/;
// zip-архив
if (mediaInfo.indexOf('zip') > 0) {
// Сразу возвращаем промис unzip, но затем перепроверяем результат относительно кодировки
return this.unzip(url).then(function (res) {
// Если кодировка не UTF-8, то нужно перезиповать с учётом прочитанной кодировки
if (!encodingRegExp.test(res)) {
var encoding = /encoding="([^"]+)"/.exec(res)[1];
return this.unzip(url, encoding);
} else {
// Иначе результат
var d = $.Deferred();
d.resolve(res);
return d.promise();
}
}.bind(this));
// Иначе получили просто текст
} else {
var d = $.Deferred();
// Магия чтения DataURI
// INFO: https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/atob
data = window.atob(data);
// Опять же если кодировка не соответствует, то нужно перечитать файл
if (!encodingRegExp.test(data)) {
var encoding = /encoding="([^"]+)"/.exec(data);
if (!encoding || !Array.isArray(encoding) || encoding.length > 1) {
d.reject('файл повреждён или книга неподдерживаемого формата');
} else {
encoding = encoding[1];
var reader = new FileReader();
reader.readAsText(file, encoding);
reader.onloadend = function () {
d.resolve(reader.result);
};
}
} else {
try {
var result = decodeURIComponent(escape(data));
d.resolve(result);
} catch (e) {
d.reject(e);
}
}
return d.promise();
}
}
};
provide(parserFb2);
});

View File

@@ -0,0 +1,41 @@
module.exports = function (bt) {
bt.setDefaultView('chitalka-fb2', 'default');
bt.match('chitalka-fb2*', function (ctx) {
ctx.setInitOption('keyboard', true);
ctx.setInitOption('url', ctx.getParam('url'));
ctx.enableAutoInit();
var footnotes = ctx.getParam('footnotes') || false;
if (footnotes) {
ctx.setState('footnotes', footnotes);
}
var pages = ctx.getParam('pages') || false;
if (pages) {
ctx.setState('pages', pages);
}
var content = [
{
elem: 'title'
},
{
elem: 'bookholder'
}];
if (ctx.getParam('ui')) {
ctx.setInitOption('ui', true);
content.push({
elem: 'ui',
content: ctx.getParam('ui')
});
}
ctx.setContent(content);
});
bt.match('chitalka-fb2*__ui', function (ctx) {
ctx.setContent(ctx.getParam('content'));
});
};

View File

@@ -0,0 +1,7 @@
- chitalka
- unzip
- storage
- block: chitalka-design
required: true
- gsap
- chitalka-ui

View File

@@ -0,0 +1,964 @@
/* global XSLTProcessor, TweenLite, Power2, alert */
modules.define(
'chitalka-fb2',
[
'chitalka',
'jquery',
'inherit',
'y-extend',
'y-debounce',
'unzip',
'chitalka-fb2-parser',
'storage',
'y-next-tick'
],
function (
provide,
Chitalka,
$,
inherit,
extend,
debounce,
zip,
parser,
Storage,
nextTick
) {
var win = $(window);
var FONT_SIZE_STEP = 2;
var TEXT_NODE = 3;
var ChitalkaFb2 = inherit(Chitalka, {
__constructor: function () {
this.__base.apply(this, arguments);
this._bookPlaceholder = this._findElement('bookholder');
this._title = this._findElement('title');
this._prepareBook();
},
_render: function (book) {
this._bookPlaceholder.html(book);
},
_setup: function () {
this._bookPlaceholder.scrollLeft(0);
this._setTitle();
this._afterDomAppending();
},
_setTitle: function () {
// Ищем ноду с заголовком книги
var titleNode = this._find(this._xml, 'title');
if (!titleNode) {
return;
}
// Ищем все параграфы в ноде, кастуем к массиву
var titleParagraphs = [].slice.call(titleNode.querySelectorAll('p'));
if (titleParagraphs.length === 0) {
return;
}
// Вытягиваем тексты параграфов и конкатенируем
var bookTitle = titleParagraphs.map(function (p) {
return p.textContent;
});
var bookTitleHTML = bookTitle.join('&nbsp;&ndash;&nbsp;');
var bookTitleAttr = bookTitle.join(' - ');
this._title
.attr('title', bookTitleAttr)
.html(bookTitleHTML);
},
/**
* Задаёт режим отображения сноски и триггерит событие change
* @public
*
* @param {String} mode тип отображения сносок
* 'inline' внутри текста
* 'appendix' в конце
*/
setFootnotesMode: function (mode) {
this._setFootnotesMode(mode);
this._settings.save('footnotes', mode);
this._onBookChange();
},
/**
* Задаёт режим количества отображаемых страниц
* @public
*
* @param {String} mode тип режима:
* auto автоматический
* one всегда одна страница на листе
* two всегда две страницы на листt
*/
setPageViewMode: function (mode) {
this._setPageViewMode(mode);
this._settings.save('pages', mode);
this._onBookChange();
},
/**
* Задаёт режим отображения сноски
* @private
*
* @param {String} mode тип отображения сносок
* 'inline' внутри текста
* 'appendix' в конце
*/
_setFootnotesMode: function (mode) {
this._subscribeToLinksEvents();
if (mode === 'inline') {
this._footnotesMode = mode;
this._setState('footnotes', 'inline');
} else {
this._footnotesMode = 'appendix';
this._removeState('footnotes');
}
},
/**
* Задаёт режим количества отображаемых страниц
* @private
*
* параметры см public метод
*/
_setPageViewMode: function (mode) {
if (mode === 'one' || mode === 'two') {
this._setState('pages', mode);
} else {
this._removeState('pages');
}
},
/**
* Возвращает значение параметра «режим отображения сносок»
*
* @returns {String}
*/
getFootnotesMode: function () {
return this._footnotesMode;
},
/**
* Возвращает значение параметра «режим отображения страниц»
*
* @returns {String}
*/
getPageViewMode: function () {
return this._getState('pages');
},
/**
* Действия, которые необходимо проивести, когда книга физически
* появится в DOM-дереве
*/
_afterDomAppending: function () {
this._book = this._findElement('book');
/**
* FIXME: https://st.yandex-team.ru/CHITALKA-84
* Не до конца работают флексы, надо поискать более лаконичное решение,
* нежели задавать контейнеру картинки размеры
*/
this._images = this.getDomNode().find('.image');
this._bookDOM = this._book[0];
if (this._settings.get('font-size')) {
this._setFontSize(this._settings.get('font-size'));
} else {
this._fontSize = parseInt(this._bookPlaceholder.css('font-size'), 10);
this._settings.save('font-size', this._fontSize);
}
this._lineHeight = parseInt(this._bookPlaceholder.css('line-height'), 10);
this._annotations = this.getDomNode().find('.annotation');
this._subscribeToWindowEvents();
this._setFootnotesMode(this._settings.get('footnotes') || this._getState('footnotes') || 'appendix');
this._setPageViewMode(this._settings.get('pages') || this._getState('pages') || 'auto');
this._storage = new Storage(this.getBookId());
// В FF есть бага, что нельзя сразу после вставки в DOM начинать работать с ним
// возможны пропуски элементов и их значений, поэтому работу с размерами DOM
// откладываем до следующего tick'а, когда браузер закончит вставлять данные
// связанный баг https://st.yandex-team.ru/CHITALKA-65
nextTick(function () {
this._buildCFIs();
this._calcDimensions();
this._restoreSavedPosition();
this._firstElementOnPage = this._getKeeper();
this.emit('ready');
}.bind(this));
},
/**
* Строит CSS-селектор для выбора ноды по CFI
*
* @param {String} cfi
* @return {String}
*/
_buildSelectorByCfi: function (cfi) {
return '[data-4cfi="' + cfi + '"]';
},
_storePagePosition: debounce(function () {
this._storage.save({
page: this._currentPage,
'4cfi': this._getKeeper().getAttribute('data-4cfi')
});
}, 500),
/**
* Восстановление позиции последнего чтения книги
*/
_restoreSavedPosition: function () {
var storagePage;
// Восстанавливаем страницу из инфы о местоположении (старая нотация)
if (this._storage.get('page')) {
storagePage = this._storage.get('page');
}
// Или из data-4cfi
if (this._storage.get('4cfi')) {
var selector = this._buildSelectorByCfi(this._storage.get('4cfi'));
if ($(selector).size() > 0) {
storagePage = this._whatPageIsDOMElem($(selector));
} else {
this._storage.remove('cfi');
}
}
// Если есть что восстанавилвать, то идем туда
if (storagePage) {
this._currentPage = storagePage;
this._updateScrollPosition(true, true);
} else {
this._currentPage = 0;
}
},
/**
* Математика внутри читалки - считаем отступы, ширины колонок, колоичество страниц
*/
_calcDimensions: function () {
// Ширина разрыва между колонками
this._gapWidth = parseInt(this._book.css('column-gap'), 10);
// Магия, т.к в вебките есть баг columnt-count: 1 контент вытягивается в высоту
// из-за этого приходится создавать вторую фейковую колонку (для применения свойства)
// и компенисировать фейк математикой, что и происходит
// Количество колонок
// Если gapWidth === 0, то значит одна колонка и включается режим удвоения ширины и количества колонок
// возвращаем количество в исходную позицию, если же ширина gapWidth > 0, то ничего не делаем.
this._gaps = parseInt(this._book.css('column-count'), 10);
// По сколько страниц пролистывать
this._listBy = this._gapWidth === 40 ? 1 : 2;
// Ширина книжного холста
// Если колонка одна (gapWidth === 0), то book.width() вернет значение для 200% width, делим пополам
// Приоритетнее для определения ширины использовать getComputedStyle, т.к он не округляет ширину
if (window.getComputedStyle) {
this._bookCanvasWidth = parseFloat(window.getComputedStyle(this._book[0]).width);
} else {
this._bookCanvasWidth = this._book.width();
}
this._bookCanvasWidth /= this._gapWidth ? 1 : 2;
this._bookCanvasHeight = this._book.height();
//this._updateMaxMins();
// Ширина страницы книги
// FIXME: Нужно будет переделать и ограничить ширину по количеству символов
// task https://st.yandex-team.ru/EBOOKS-106
this._pageWidth = (this._bookCanvasWidth - this._gapWidth) / this._gaps;
// Ширина шага для скролла страницы
this._pageStepWidth = this._pageWidth + this._gapWidth;
// Суммарное количество страниц в книге
this._pageCount = this._getBookPages();
// Среднее число символов на странице, speedCoeff - эмпирически вычисленный коэффцицент
var speedCoeff = (this._pageWidth / (11 * this._fontSize / 16));
this._avgSymbolsOnPage = Math.round(Math.floor(this._bookCanvasHeight / this._lineHeight) * speedCoeff);
},
/**
* События, которые надо произвести когда книга изменилась
*/
_onBookChange: function () {
var oldPageCount = this._pageCount;
this._calcDimensions();
this._updateScrollPosition(true, true);
if (oldPageCount !== this._pageCount) {
this._currentPage = this._whatPageIsDOMElem(this._firstElementOnPage);
this._updateScrollPosition(true, true);
}
/**
* FIXME: https://st.yandex-team.ru/CHITALKA-84
*/
this._images.css('max-height', this._bookPlaceholder.height() + 'px');
},
// ------------------------------------------------------------
// Секция событий
/**
* Подписываем на события окна
*/
_subscribeToWindowEvents: function () {
win.resize(this._onBookChange.bind(this));
},
/**
* Подписка на события ссылок внутри страницы
*/
_subscribeToLinksEvents: function () {
this.getDomNode().on('click', 'a', function (e) {
var link = $(e.currentTarget);
var href = link.attr('href');
if (/^#/.test(href)) {
if (this._footnotesMode === 'appendix') {
this._moveToAnnotation(href.replace('#', ''));
}
return false;
} else {
link.attr('target', '_blank');
}
}.bind(this));
},
_unsubscribeFromLinksEvents: function () {
this.getDomNode().off('click', 'a');
},
/**
* Функция выполняет перелистывание книги до аннотации
*
* @param {String} annotationId значение параметра name аннотации
*/
_moveToAnnotation: function (annotationId) {
// Ищем аннотацию среди ей подобных
var annotation = $.grep(this._annotations, function (annotation) {
return $(annotation).find('a[name="' + annotationId + '"]').size() > 0;
});
if (!annotation) {
return;
}
// Сохраняем место вызова аннотации
this._backPage = this._currentPage;
// Ищем на какой странице находится сноска
this._currentPage = this._annotationPage = this._whatPageIsDOMElem(annotation);
// И идём к ней
this._updateScrollPosition();
},
/**
* Keeper - элемент, видимость которого мы будем сохранять при уменьшении масштаба/режима отображение страницы
* Функции находит этот элемент относительно текущей страницы
*
* @param {String} [page] для какой страницы вернуть keeper'а
* @returns {DOMElem} keeper
*/
_getKeeper: function (page) {
var elementsToPages = this._getElementsToPages(
this._listBy,
this._fontSize,
this._bookCanvasHeight,
this.getFootnotesMode()
);
var currentPage = page || this._currentPage;
var lookup = elementsToPages[currentPage];
// Элемент есть в массиве текущих страниц
if (lookup && lookup.length) {
return lookup[0];
}
// Ищем в предыдущих страницах последний элемент, такое может быть, например,
// когда есть длинный абзац в несколько страниц, тогда на текущей странице
// не будет указателя на элемент
do {
lookup = elementsToPages[currentPage--];
if (lookup && lookup.length) {
return lookup[lookup.length - 1];
}
} while (currentPage >= 0);
// Если всё равно не нашли, то берём первую страницу
return elementsToPages[0][0];
},
/**
* Получаем объект с соответствиями элементов DOM страницам книги
*
* @param {Number} gaps количество колонок
* @param {Number} fontSize размер шрифта
* @param {Number} height высота холста
*
* @returns {Object}
*/
_getElementsToPages: function (gaps, fontSize, height, footnotesMode) {
this._elementsToPages = this._elementsToPages || {};
if (!this._elementsToPages[gaps]) {
this._elementsToPages[gaps] = {};
}
if (!this._elementsToPages[gaps][fontSize]) {
this._elementsToPages[gaps][fontSize] = {};
}
if (!this._elementsToPages[gaps][fontSize][height]) {
this._elementsToPages[gaps][fontSize][height] = {};
}
if (!this._elementsToPages[gaps][fontSize][height][footnotesMode]) {
this._buildElementsToPages(gaps, fontSize, height, footnotesMode);
}
return this._elementsToPages[gaps][fontSize][height][footnotesMode];
},
/**
* Строит объект с соответствиями элементов DOM страницам книги,
* для заданных gaps и fontSize.
*
* @param {Number} gaps количество колонок
* @param {Number} fontSize размер шрифта
* @param {Number} height высота холста
*/
_buildElementsToPages: function (gaps, fontSize, height, footnotesMode) {
var result = {};
var allElementsInBook = this._bookPlaceholder.find('*');
allElementsInBook.map(function (i, el) {
var page = this._whatPageIsDOMElem(el);
if (!result[page]) {
result[page] = [];
}
result[page].push(el);
}.bind(this));
this._elementsToPages[gaps][fontSize][height][footnotesMode] = result;
},
/**
* Строить и навешивает на все элементы (в том числе текстовые)
* data-аттрибут data-4cfi, содержащий универсальный идентификатор каждого элемента
*
* @param {DOM} parent нода внутри которой будет происходить строительство cfi
* @param {String} id айдишник текущей ноды, нужен для конструирования следующего id
*/
_buildCFIs: function (parent, id) {
parent = parent || this._bookPlaceholder;
var counter = 1;
id = id || '/';
var totalSymbols = 0;
$(parent).contents().map(function (i, el) {
var genID = id + counter;
var symbols;
if (el.nodeType === TEXT_NODE) {
symbols = $.trim(el.textContent).length;
totalSymbols += symbols;
// оборачиваем только если не пустая нода и не единственная
if ($.trim(el.textContent) !== '' && $(parent).size() > 1) {
var wrap = $('<span></span>');
wrap.attr('data-4cfi', genID);
if (symbols) {
wrap.attr('data-symbols', symbols);
}
$(el).wrap(wrap);
counter++;
}
} else {
$(el).attr('data-4cfi', genID);
symbols = this._buildCFIs(el, genID + '/');
totalSymbols += symbols;
if (symbols) {
$(el).attr('data-symbols', symbols);
}
counter++;
}
}.bind(this));
return totalSymbols;
},
/**
* Измеряет скорость чтения книги
*/
_measureReadingTime: function () {
var currentTime = Number(new Date());
if (this._previousPaging) {
var readBy = (currentTime - this._previousPaging) / 60000;
var speed = Math.floor(this._avgSymbolsOnPage * this._listBy / readBy);
this._storeSpeed(speed);
this._checkSpeed();
}
this._previousPaging = currentTime;
},
/**
* Выполнить перелистывание книги на страницу где аннотация была вызвана
*/
moveBackFromAnnotation: function () {
this._currentPage = this._backPage;
this.resetBackPage();
this._updateScrollPosition();
},
/**
* Сбрасывает счётчик возврата
*/
resetBackPage: function () {
this._backPage = null;
},
/**
* Функция вычисляет страницу, на которой находится переданный элемент
*
* @param {DOMElem} domElem элемент, который ищем
* @returns {Number} номер страницы, на которой находится левый край элемента
*/
_whatPageIsDOMElem: function (domElem) {
if (!domElem) {
return;
}
// Элементы, которые не видимы или имеют position отличный
// от static возвращают неверные координаты, включаем их
// в boolean флаг preconditions
var preconditions = $(domElem).is(':visible') &&
['fixed', 'absolute'].indexOf($(domElem).css('position')) === -1 &&
// Мега костыль, т.к image__wrapper внутри содержить position: absolute элемент,
// то это сносит крышу счетоводу
!$(domElem).is('.image__wrapper');
var pageDelta = Number($(domElem).position().left) / (this._pageWidth + this._gapWidth);
// И если текущий элемент именно такой, то возвращаем 0
return preconditions ?
Math.floor((this._currentPage || 0) + pageDelta)
: 0;
},
// ------------------------------------------------------------
// Секция выполнения действия с читалкой
nextPage: function () {
if (!this.isLastPage()) {
this._measureReadingTime();
this._currentPage += this._listBy;
this._updateScrollPosition();
}
},
previousPage: function () {
if (!this.isFirstPage()) {
// Меняем поведение: при переходе к сноскам нет смысла листать назад,
// поэтому клик влево переход обратно
if (this._currentPage === this._annotationPage && this._backPage) {
this.moveBackFromAnnotation();
} else {
this._currentPage -= this._listBy;
this._updateScrollPosition();
}
}
},
firstPage: function () {
this._currentPage = 0;
this._updateScrollPosition();
},
lastPage: function () {
this._currentPage = this._pageCount - this._listBy;
this._updateScrollPosition();
},
zoomIn: function () {
this._updateFontSize(this._fontSize + FONT_SIZE_STEP);
},
zoomOut: function () {
this._updateFontSize(this._fontSize - FONT_SIZE_STEP);
},
zoomReset: function () {
this._resetFontSize();
},
/**
* Хак для картинок, т.к max-height, max-width для них не работает
* Хак для элементов section, у которых та же история
*/
_updateMaxMins: function () {
var h = this._bookCanvasHeight;
var w = this._bookCanvasWidth;
if (this._oldHeight !== h) {
this.elem('image').map(function (i, elem) {
var $elem = $(elem);
$elem.find('img').css({
'max-width': w + 'px',
'max-height': h + 'px'
});
$elem.toggleClass('image-small', $elem.height() < h);
});
this.elem('section').css({
'min-height': h + 'px'
});
this._oldHeight = h;
}
},
/**
* Возвращает предыдущую страницу
*
* @returns {Number}
*/
getBackPage: function () {
return this._backPage && (this._backPage + 1) || null;
},
/**
* Возвращает текущую страницу
*
* @returns {Number}
*/
getCurrentPage: function () {
return this._currentPage + 1;
},
/**
* Возвращает общее количество страниц в книге
*
* @returns {Number}
*/
getTotalPages: function () {
return this._pageCount;
},
/**
* Возвращает уникальный идентификатор книги
* @return {String} id
*/
getBookId: function () {
this._isbn = this._isbn ||
this._find(this._xml, 'isbn') ||
this._find(this._xml, 'id') ||
this._find(this._xml, 'title') ||
'';
return this._isbn.textContent;
},
getEstimatedTime: function () {
// пока закомменчено но может понадобиться
//var symbolsInBook = this._book.attr('data-symbols');
var estimated = this.getTotalPages() * this._avgSymbolsOnPage -
this.getCurrentPage() * this._avgSymbolsOnPage;
var estimatedMins = Math.floor(estimated / this.getSpeed());
// hours minutes
return [Math.floor(estimatedMins / 60), estimatedMins % 60];
},
/**
* Изменение страницы
* Функция в том числе включет пересчет важных параметров и физическое изменение скролла до нужной страницы
* @param {Boolean} [noAnimation] изменить страницу без анимации (по-умолчанию анимация будет)
* @param {Boolean} [dontChangeFirstElement] - не пересчитывать первый элемент на странице
*/
_updateScrollPosition: function (noAnimation, dontChangeFirstElement) {
if (this.isLastPage()) {
this._currentPage = this._pageCount - this._listBy;
}
if (this.isFirstPage()) {
this._currentPage = 0;
}
var newLeftPosition = this._pageStepWidth * this._currentPage;
if (noAnimation || typeof TweenLite === 'undefined') {
this._bookPlaceholder.scrollLeft(newLeftPosition);
// Сбрасываем первый элемент
if (!dontChangeFirstElement) {
this._firstElementOnPage = this._getKeeper();
}
} else {
TweenLite.to(this._bookPlaceholder, 0.25, {
scrollTo: {
x: newLeftPosition
},
ease: Power2.easeOut,
onComplete: function () {
// Сбрасываем первый элемент
if (!dontChangeFirstElement) {
this._firstElementOnPage = this._getKeeper();
}
}.bind(this)
});
}
this.emit('page-changed');
this._storePagePosition();
},
/**
* Изменение размера шрифта
*/
_onChangeFontSize: function () {
// Пересчитываем параметры страницы
this._calcDimensions();
// Подстраиваем левые границы для текущей страницы --
// размеры листа могут поменяться, если в данном браузере работает единица ch
this._updateScrollPosition(true, true);
// После изменения размеров ищем где теперь находится элемент, который был первым ранее
this._currentPage = this._whatPageIsDOMElem(this._firstElementOnPage, true);
// Меняем страницу без анимации на ту, где виден элемент
this._updateScrollPosition(true, true);
},
/**
* Установка значения fontSize
*
* @param {Number} fontSize новое значение fontSize
*/
_setFontSize: function (fontSize) {
this._fontSize = fontSize;
// Меняем физически размеры шрифта
this._bookPlaceholder.css('font-size', this._fontSize + 'px');
},
/**
* Обновить значение размера шрифта
* @param {Number} newFontSize разница между текущим шрифтом и новым
*/
_updateFontSize: function (newFontSize) {
if (this._fontSizeLimits[0] <= newFontSize && this._fontSizeLimits[1] >= newFontSize) {
this.emit('reset-zoom-buttons');
this._settings.save('font-size', newFontSize);
this._setFontSize(newFontSize);
this._onChangeFontSize();
}
if (this._fontSizeLimits[1] <= newFontSize) {
this.emit('disabled-zoom-in');
}
if (this._fontSizeLimits[0] >= newFontSize) {
this.emit('disabled-zoom-out');
}
},
/**
* Сбросить значение размера шрифта до первоначального
*/
_resetFontSize: function () {
this._fontSize = this._defaultFontSize;
this._onChangeFontSize();
},
/**
* Функция возращает true, если мы на первой странице или меньше (возможно при ресайзе)
*
* @returns {Boolean}
*/
isFirstPage: function () {
return this._currentPage <= 0;
},
/**
* Функция возращает true, если мы на последней странице или больше (возможно при ресайзе)
*
* @returns {Boolean}
*/
isLastPage: function () {
return this._currentPage >= this._pageCount - this._listBy;
},
/**
* Функция подсчета количества страниц в книге
* формула: ширина книги + ширина распорки между страницами (т.к на n страниц n-1 распорка)
* поделённая на ширину страницы книги + ширину распорки.
*
* @return {Number} количество страниц в книге
*/
_getBookPages: function () {
var bookDOMWidth = this._bookDOM.scrollWidth;
return Math.floor((bookDOMWidth + this._gapWidth) /
(Math.floor(this._pageWidth) + this._gapWidth));
},
/////////////////////////////////////////////////////////////////////
_flush: function () {
this._currentPage = null;
this._isbn = null;
this._elementsToPages = {};
// Важно отписаться от прошлых событий, иначе возможны двойные срабатывания
this._unsubscribeFromLinksEvents();
},
_prepareBook: function (file) {
this._flush();
var pathToBook = file || this._getOptions().url;
return parser.readFile(pathToBook, file ? true : false)
.then(parser.getXml)
.then(this._convertToHtml.bind(this))
.then(this._render.bind(this))
.then(this._setup.bind(this))
.done(this._onBookChange.bind(this))
.fail(this._fail.bind(this));
},
_fail: function (e) {
alert('Ошибка: ' + e);
this.emit('load-fail');
},
/**
* Находит ноду selector в переданном xml (для ускорения написания)
*
* @param {XMLTree} xml
* @param {String} selector
*
* @returns {Node} возвращает найденный в xml узел, соответствующий selector
*/
_find: function (xml, selector) {
return xml.querySelector(selector);
},
_convertToHtml: function (xml) {
this._xml = xml;
if (this._xsl) {
var d = $.Deferred();
d.resolve(this._xsltTransform(xml, this._xsl));
return d.promise();
}
return $.ajax({
dataType: 'xml',
url: window.document.location.href + 'lib/reader.xsl'
}).then(function (xsl) {
return this._xsltTransform(xml, xsl);
}.bind(this));
},
_xsltTransform: function (xml, xsl) {
this._xsl = xsl;
var html;
// code for IE
if (window.ActiveXObject) {
html = xml.transformNode(xsl);
// code for Chrome, Firefox, Opera, etc.
} else if (document.implementation && document.implementation.createDocument) {
var xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsl);
html = xsltProcessor.transformToFragment(xml, document);
}
return html;
},
/**
* @private
* Функция проверки на то доступен ли данный формат книг для чтения в данном окружении
*
* @param {String} format строчное название формата
* @returns {Boolean}
*/
_isAvailable: function () {
// Нет технологий
if (!this._hasTechnologies(
'Blob',
'FileReader',
'ArrayBuffer',
'Uint8Array',
'XSLTProcessor',
'DataView')) {
return false;
}
// Opera 12 падает по RangeError
if (window.opera && parseInt(window.opera.version(), 10) <= 12) {
return false;
}
return true;
},
/**
* @private
* Проверка на доступность технологии в данном окружении
* каждый аргумент это технология, наличие которой проверяется в окружении
* @returns {Boolean}
*/
_hasTechnologies: function () {
return [].map.call(arguments, function (tech) {
// Без window не сработает в IE
return typeof window[tech] !== 'undefined';
}).indexOf(false) === -1;
}
}, {
getBlockName: function () {
return 'chitalka-fb2';
}
});
provide(ChitalkaFb2);
});

View File

@@ -0,0 +1,88 @@
.chitalka-fb2_default {
font-family: Arial, serif;
min-height: 50ch;
&__book {
height: 100%;
width: 100%;
-webkit-column-count: 2;
-moz-column-count: 2;
column-count: 2;
-webkit-column-gap: 50px;
-moz-column-gap: 50px;
column-gap: 50px;
-webkit-column-axis: horizont;
-moz-column-axis: horizont;
column-axis: horizont;
@media (max-width: 1400px) {
& {
/* Здесь так, потому что вебкит не понимает одну колонку и вытягивает её в высоту */
width: 200%;
-webkit-column-gap: 40px;
-moz-column-gap: 40px;
column-gap: 40px;
}
}
& img {
max-width: 100%;
max-height: 100%;
}
}
&__title {
position: relative;
width: 650px;
height 52px;
margin: 15px auto 0;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
color: #999;
}
&__bookholder {
chitalka_design();
width: 1300px;
position: absolute;
top: 77px;
bottom: 60px;
left: 81px;
right: 81px;
transform: translateZ(0);
@media (max-width: 1400px) {
width: 650px;
}
}
._pages_one &__book {
width: 200%;
-webkit-column-gap: 40px;
-moz-column-gap: 40px;
column-gap: 40px;
}
._pages_two &__book {
width: 100%;
-webkit-column-gap: 50px;
-moz-column-gap: 50px;
column-gap: 50px;
}
._pages_one &__bookholder {
width: 650px;
}
._pages_two &__bookholder {
width: 1300px;
}
}

View File

@@ -0,0 +1,90 @@
module.exports = function (bt) {
bt.match('chitalka-ui', function (ctx) {
ctx.enableAutoInit();
var content = [];
if (ctx.getParam('controls')) {
var controls = ctx.getParam('controls');
ctx.setInitOption('controls', controls);
if (ctx.getParam('book')) {
if (ctx.getParam('book').footnotes) {
controls.footnotes = ctx.getParam('book').footnotes;
}
if (ctx.getParam('book').pages) {
controls.pages = ctx.getParam('book').pages;
}
}
controls.block = 'controls';
content.push({
elem: 'controls',
/*
Передается объект вида(по умолчанию)
{
block: controls,
zoom: true,
arrows: true
}
*/
content: controls
});
}
content.push({
elem: 'book',
content: ctx.getParam('book')
});
if (ctx.getParam('progress')) {
ctx.setInitOption('progress', true);
content.push({
elem: 'progress'
});
}
if (ctx.getParam('progress_bar')) {
ctx.setInitOption('progress-bar', true);
content.push({
elem: 'progress-bar'
});
}
if (ctx.getParam('annotations')) {
ctx.setInitOption('annotations', true);
content.push({
elem: 'back-to-page'
});
}
content.push({
elem: 'estimated'
});
ctx.setState('loading');
content.push({
elem: 'loader'
});
ctx.setContent(content);
});
bt.match('chitalka-ui*__loader', function (ctx) {
ctx.setContent({
block: 'spin',
view: 'default-large'
});
});
bt.match([
'chitalka-ui*__controls',
'chitalka-ui*__book'
], function (ctx) {
ctx.setContent(ctx.getParam('content'));
});
};

View File

@@ -0,0 +1,21 @@
- block: y-page
required: true
- block: y-block
required: true
- block: jquery
required: true
- block: inherit
required: true
- block: chitalka-fb2
required: true
- controls
- spin
- block: 'file-drag'
required: true
- block: spin
view: 'default-large'
- block: y-block
elem: auto-init

View File

@@ -0,0 +1,356 @@
modules.define(
'chitalka-ui',
[
'controls',
'y-block',
'jquery',
'y-extend',
'spin',
'file-drag',
'inherit'
],
function (
provide,
Controls,
YBlock,
$,
extend,
Spin,
FileDrag,
inherit
) {
var ChitalkaUI = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
//var params = extend({
//menu: false,
//progress: false
//}, this._getOptions());
},
init: function (chitalka) {
this._chitalka = chitalka;
this._bindTo(this._chitalka, 'ready', this._onBookLoaded.bind(this));
if (this._getOptions().controls) {
this._initControls();
}
if (this._getOptions().progress) {
this._initProgress();
}
if (this._getOptions()['progress-bar']) {
this._initProgressBar();
}
if (this._getOptions().annotations) {
this._initAnnotationsControl();
}
this._initEstimated();
this._initDragListeners();
return this;
},
_initControls: function () {
this._controls = Controls.find(this.getDomNode());
if (this._getOptions().controls.arrows) {
this._initArrows();
}
this._bindTo(this._chitalka, 'ready', function () {
if (this._getOptions().controls.zoom) {
this._controls.setFootnotesMode(this._chitalka.getFootnotesMode());
this._controls.setPageViewMode(this._chitalka.getPageViewMode());
this._initMenu();
}
}.bind(this));
},
_initArrows: function () {
this._bindTo(this._controls, 'next-page', function () {
this._chitalka.nextPage();
});
this._bindTo(this._controls, 'previous-page', function () {
this._chitalka.previousPage();
});
this._bindTo(this._chitalka, 'page-changed', function () {
this._updateArrows();
});
},
_initMenu: function () {
this._bindTo(this._controls, 'zoom-in', function () {
this._chitalka.zoomIn();
});
this._bindTo(this._controls, 'zoom-out', function () {
this._chitalka.zoomOut();
});
this._bindTo(this._controls, 'footnotes-appendix', function () {
this._chitalka.setFootnotesMode('appendix');
});
this._bindTo(this._controls, 'footnotes-inline', function () {
this._chitalka.setFootnotesMode('inline');
});
this._bindTo(this._controls, 'pages-one', function () {
this._chitalka.setPageViewMode('one');
this._setState('mode', 'one-page');
});
this._bindTo(this._controls, 'pages-two', function () {
this._chitalka.setPageViewMode('two');
this._setState('mode', 'two-page');
});
this._bindTo(this._controls, 'pages-auto', function () {
this._chitalka.setPageViewMode();
this._removeState('mode');
});
this._bindTo(this._chitalka, 'disabled-zoom-in', function () {
this._controls.disableZoomIn();
});
this._bindTo(this._chitalka, 'disabled-zoom-out', function () {
this._controls.disableZoomOut();
});
this._bindTo(this._chitalka, 'reset-zoom-buttons', function () {
this._controls.resetZoomButtons();
});
this._bindTo(this._chitalka, 'load-fail', function () {
this._noBook();
this._fileLoaded = false;
}.bind(this));
},
/**
* Переводит ui в состояние «нет книги
*/
_noBook: function () {
this._setState('no-book');
Spin.find(this._findElement('loader')).stop();
},
/**
* Активировать слушатели drag-событий
*/
_initDragListeners: function () {
this._drag = new FileDrag(this.getDomNode());
this._bindTo(this._drag, 'show-drag', this._showDrag.bind(this));
this._bindTo(this._drag, 'hide-drag', this._hideDrag.bind(this));
this._bindTo(this._drag, 'file-dropped', this._onFileDropped.bind(this));
this._bindTo(this._drag, 'file-loaded', this._onFileLoaded.bind(this));
},
/**
* Активировать состояние drag
*/
_showDrag: function () {
this._setState('drag');
this._removeState('no-book');
this._controls.hide();
},
/**
* Убрать состояние drag
*/
_hideDrag: function () {
if (this._fileLoaded) {
this._removeState('drag');
this._controls.show();
}
},
/**
* Действия по киданию файла внутрь интерфейса
*/
_onFileDropped: function () {
this._fileLoaded = true;
this.loading();
},
/**
* Действия по загрузке файла
* @param {Event} e
*/
_onFileLoaded: function (e) {
this._chitalka._prepareBook(e.data).then(function () {
this._removeState('drag');
this._controls.show();
}.bind(this));
},
/**
* Вернуть UI в состояние «загрузка»
*/
loading: function () {
// Остановить кручения спиннера
Spin.find(this._findElement('loader')).start();
// Убирает стейт загрузки с текущего элемента
this._setState('loading');
// Показываем блок с контролами
this._controls.hide();
},
/**
* Действия после загрузки книги
* @private
*/
_onBookLoaded: function () {
this._fileLoaded = true;
// Остановить кручения спиннера
Spin.find(this._findElement('loader')).stop();
// Убирает стейт загрузки с текущего элемента
this._removeState('loading');
// Показываем блок с контролами
this._controls.show();
},
/**
* Если текущая страница первая/последняя,
* то левая/правая(соответственно) стралка дизейблится.
* @private
*/
_updateArrows: function () {
this._controls.resetArrows();
if (this._chitalka.isFirstPage()) {
this._controls.disableArrowPrev();
}
if (this._chitalka.isLastPage()) {
this._controls.disableArrowNext();
}
},
/**
* Инициализация элемента, который отображает
* номер текущей страницы из общего количества страниц
* @private
*/
_initProgress: function () {
this._progress = this._findElement('progress');
this._bindTo(this._chitalka, 'page-changed', this._updateProgress.bind(this));
this._bindTo(this._chitalka, 'ready', this._updateProgress.bind(this));
},
/**
* Инициализация элемента, который отображает
* номер текущей страницы из общего количества страниц
* @private
*/
_initEstimated: function () {
this._estimated = this._findElement('estimated');
this._bindTo(this._chitalka, 'page-changed', this._updateEstimated.bind(this));
this._bindTo(this._chitalka, 'ready', this._updateEstimated.bind(this));
},
_updateEstimated: function () {
var estimatedTime = this._chitalka.getEstimatedTime();
var estimatedPhrase = 'До конца книги ' +
(estimatedTime[0] ? estimatedTime[0] + ' ч ' : '') +
estimatedTime[1] + ' м';
this._estimated.html(estimatedPhrase);
},
/**
* Обновляет состояние элемента прогресса
*/
_updateProgress: function () {
this._progress.html(this._chitalka.getCurrentPage() + ' из ' + this._chitalka.getTotalPages());
},
/**
* Инициализация прогресс-бара
* @private
*/
_initProgressBar: function () {
this._progressBar = this._findElement('progress-bar');
this._bindTo(this._chitalka, 'page-changed', function () {
var progress = this._getCurrentProgress() + '%';
this._progressBar.width(progress);
this._progressBar.attr('title', progress);
});
},
/**
* Инициализация элемента работы с аннотациями
* @private
*/
_initAnnotationsControl: function () {
this._backTo = this._findElement('back-to-page');
var counter = 0;
this._bindTo(this._chitalka, 'page-changed', function () {
var prevPage = this._chitalka.getBackPage();
// Когда нет prevPage значит его сбросили и надо убрать «возвращатор»
if (!prevPage || counter === 1) {
this._setBackTo();
this._chitalka.resetBackPage();
counter = 0;
} else if (counter) {
counter--;
} else if (prevPage) {
this._setBackTo('Вернуться на страницу ' + prevPage);
// Сколько страниц даём пролистнуть
counter = 3;
} else {
this._setBackTo();
}
});
this._bindTo(this._backTo, 'click', function () {
this._setBackTo();
this._chitalka.moveBackFromAnnotation();
counter = 0;
});
},
_setBackTo: function (text) {
if (text) {
this._setElementState(this._backTo, 'visible');
this._backTo.html(text);
} else {
this._removeElementState(this._backTo, 'visible');
}
},
/**
* Возвращает процент прочтения
* @returns {number}
* @private
*/
_getCurrentProgress: function () {
return ((this._chitalka.getCurrentPage() / this._chitalka.getTotalPages()) * 100).toFixed(2);
}
}, {
getBlockName: function () {
return 'chitalka-ui';
}
});
provide(ChitalkaUI);
});

View File

@@ -0,0 +1,177 @@
.chitalka-ui {
$text-size = 12px;
$margin = 15px;
font: 15px/1.4 Arial, sans-serif;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
min-width: 790px;
&._drag {
background: #fff;
&::after {
position: absolute;
content: '';
left: 30px;
bottom: 30px;
right: 30px;
top: 30px;
text-align: center;
border: 4px dashed #0f0;
}
&::before {
position: absolute;
top: 50%;
width: 100%;
transform: translateY(-50%);
text-align: center;
content: 'drag file here';
}
}
&._mode_one-page {
min-width: 790px;
}
&._mode_two-page {
min-width: 1490px;
}
&__book {
padding: 0 30px;
visibility: visible;
opacity: 1;
transition: visibility 0s ease, opacity 1s ease;
}
&__controls {
position: absolute;
top: $margin;
right: 0;
bottom: 0;
left: 0;
}
&__progress-bar {
position: absolute;
bottom: 0;
left: 0;
height: 4px;
background: #fc0;
transition: width .5s ease;
}
&__progress {
font-size: $text-size;
position: absolute;
bottom: $margin;
left: 50%;
margin-left: -150px;
width: 300px;
text-align: center;
color: #999;
}
&__estimated {
font-size: $text-size;
position: absolute;
bottom: $margin;
right: $margin;
text-align: right;
width: 200px;
color: #999;
}
&._loading &__loader {
display: block;
}
&__loader,
&._loading &__progress,
&._loading &__estimated,
&._drag &__progress,
&._drag &__loader,
&._drag &__estimated {
display: none;
}
&._loading &__book,
&._drag &__book {
visibility: hidden;
opacity: 0;
}
&._no-book {
&::before {
position: absolute;
top: 50%;
width: 100%;
transform: translateY(-50%);
text-align: center;
content: 'no book, drag book here';
}
}
&__loader {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
text-align: center;
/* Псевдо-элмемент нужен для вертикального выравнивания inline-block'а */
&:after {
display: inline-block;
height: 100%;
content: '';
vertical-align: middle
}
}
&__back-to-page {
font-size: $text-size;
position: absolute;
bottom: $margin;
left: $margin;
width: 200px;
display: none;
color: #999;
cursor: pointer;
text-decoration: underline;
&:hover {
color: #333;
}
&._visible {
display: block;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="13" height="48" viewBox="0 0 13 48"><svg width="13" height="24" id="arrow_left" y="0"><path d="M13 .667L12.316 0 0 12.01l.684.667L13 .667m0 22.666l-.684.667L0 11.99l.684-.667L13 23.333" opacity="1" fill="#000"/></svg><svg width="13" height="24" id="arrow_right" y="24"><path d="M0 .667L.684 0 13 12.01l-.684.667L0 .667m0 22.666L.684 24 13 11.99l-.684-.667L0 23.333" opacity="1" fill="#000"/></svg></svg>

After

Width:  |  Height:  |  Size: 494 B

View File

@@ -0,0 +1,9 @@
PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRw
Oi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTMiIGhlaWdodD0iNDgiIHZpZXdCb3g9
IjAgMCAxMyA0OCI+PHN2ZyB3aWR0aD0iMTMiIGhlaWdodD0iMjQiIGlkPSJhcnJvd19sZWZ0IiB5
PSIwIj48cGF0aCBkPSJNMTMgLjY2N0wxMi4zMTYgMCAwIDEyLjAxbC42ODQuNjY3TDEzIC42Njdt
MCAyMi42NjZsLS42ODQuNjY3TDAgMTEuOTlsLjY4NC0uNjY3TDEzIDIzLjMzMyIgb3BhY2l0eT0i
MSIgZmlsbD0iIzAwMCIvPjwvc3ZnPjxzdmcgd2lkdGg9IjEzIiBoZWlnaHQ9IjI0IiBpZD0iYXJy
b3dfcmlnaHQiIHk9IjI0Ij48cGF0aCBkPSJNMCAuNjY3TC42ODQgMCAxMyAxMi4wMWwtLjY4NC42
NjdMMCAuNjY3bTAgMjIuNjY2TC42ODQgMjQgMTMgMTEuOTlsLS42ODQtLjY2N0wwIDIzLjMzMyIg
b3BhY2l0eT0iMSIgZmlsbD0iIzAwMCIvPjwvc3ZnPjwvc3ZnPgo=

View File

@@ -0,0 +1,320 @@
modules.define(
'chitalka',
[
'y-block',
'jquery',
'inherit',
'y-extend',
'chitalka-ui',
'storage'
],
function (
provide,
YBlock,
$,
inherit,
extend,
ChitalkaUI,
Storage
) {
var doc = $(document);
var reportUnimplemented = function (method) {
throw new Error('UNIMPLEMENTED METHOD: ' + method);
};
/**
* Расширение объекта Math для вычисления медианы массива
*
* @param {Array} array
* @returns {Number} медиана
*/
Math.median = function (array) {
if (!array) {
return;
}
var entries = array.length;
var median;
if (entries % 2 === 0) {
median = (array[entries / 2] + array[entries / 2 - 1]) / 2;
} else {
median = array[(entries - 1) / 2];
}
return median;
};
/**
* Выбирает из массива массив медиан в заданном количестве
*
* @param {Array} array
* @param {Number} q количество
*
* @return {Array}
*/
var limitArrayByMedians = function (array, q) {
var result = [];
if (!Array.isArray(array)) {
return result;
}
if (array.length <= q) {
return array;
}
var median = Math.median(array);
var index = array.indexOf(median);
var start = Math.round(index - q / 2);
return array.splice(start, q);
};
/**
* Хэлпер для сортировки массивов чисел
*
* @param {Number} a
* @param {Number} b
* @returns {Number} 1 - a >=b, else -1
*/
var numSort = function (a, b) {
a = parseInt(a, 10);
b = parseInt(b, 10);
return a >= b ? 1 : -1;
};
var Chitalka = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
var params = extend({
keyboard: false,
touch: false,
controls: false,
fontSize: [9, 21],
// Длина свайпа в пикселах
swipeLength: 20
}, this._getOptions());
this._defaultFontSize = 15;
this._settings = new Storage('settings');
// Если читалка не доступна, то кидаем событие и больше
// ничего не делаем
if (!this._isAvailable()) {
this.emit('unavailable');
return;
}
if (params.keyboard) {
this._initKeyboardEvents();
}
if (params.touch) {
this._initTouchEvents();
}
this._fontSizeLimits = params.fontSize;
this._setUpSpeed();
this._initUI();
},
/**
* Выставить скорость чтения книги
*/
_setUpSpeed: function () {
this._speed = Math.median(this._settings.get('speeds')) || 500;
},
_isAvailable: function () {
return false;
},
/**
* Активирует реакцию читалки на события с клавиатуры
*/
_initKeyboardEvents: function () {
this._bindTo(doc, 'keydown', this._onKeyDown);
},
/**
* Активирует реакцию читалки на события блока «Controls»
*/
_initUI: function () {
this._ui = ChitalkaUI.find(doc).init(this);
//var controls = Controls.find(this.getDomNode());
},
/**
* Активация обработки тач-событий (в частности события swipe)
* в функции выполняется навешивание соответствующих событий
*/
_initTouchEvents: function () {
},
_onKeyDown: function (e) {
switch (e.keyCode) {
// Fn + Right
case 35:
this.lastPage();
break;
// Fn + Left
case 36:
this.firstPage();
break;
// Left
case 37:
this.previousPage();
e.preventDefault();
break;
// Right
case 39:
this.nextPage();
e.preventDefault();
break;
// +
case 61:
case 187:
this.zoomIn();
if (e.metaKey) {
e.preventDefault();
}
break;
// -
case 173:
case 189:
this.zoomOut();
if (e.metaKey) {
e.preventDefault();
}
break;
// reset
case 48:
if (e.metaKey) {
this.zoomReset();
}
break;
}
},
/**
* События перемещения по книге
*/
firstPage: function () {
reportUnimplemented('firstPage');
},
previousPage: function () {
reportUnimplemented('previousPage');
},
nextPage: function () {
reportUnimplemented('nextPage');
},
lastPage: function () {
reportUnimplemented('lastPage');
},
/**
* Функция сохранения скорости в аккумулируемый объект
*
* @param {Number} speed
*/
_storeSpeed: function (speed) {
this._speedAccumulator = this._speedAccumulator || [];
this._speedAccumulator.push(speed);
this._speedAccumulator = this._speedAccumulator.sort(numSort);
},
/**
* Функция проверки скорости и её корректировки
* общий принцип работы:
* есть два массива
* this._speedAccumulator аккумулирует чтение текущей книги
* speeds, который хранится в сторадже settings хранит 10 меток скорости для пользователя
* метки это медианы, которые всегда вычисляются из аккумулятора
* как только пользователь прочитывает 10 и более страниц, мы начинаем считать медиану и
* править speeds и класть туда новую скорость, вычисленную из аккумулятора
* При этом глобальная скорость чтения значительно изменится только если пользователь прочитает
* 15 страниц значительно быстрее/медленнее чем раньше.
* Во всех остальных случаях медиана поменяется совсем незначительно
*/
_checkSpeed: function () {
var speedEntries = this._speedAccumulator.length;
if (speedEntries > 10) {
this._speedAccumulator = this._speedAccumulator.sort(numSort);
var median = Math.median(this._speedAccumulator);
// Отсекаем совсем неадекватные скорости
if (median < 100000 && median > 10) {
this._speedAccumulator = this._speedAccumulator.sort(numSort);
if (!this._settings.get('speeds')) {
this._settings.save({
speeds: this._speedAccumulator
});
} else {
var speeds = limitArrayByMedians(this._settings.get('speeds'), 10);
if (speeds.length < 10) {
speeds.push(median);
} else {
if (median <= speeds[5]) {
speeds.pop();
speeds.unshift(median);
} else {
speeds.shift();
speeds.push(median);
}
}
speeds = speeds.sort(numSort);
this._settings.save({
speeds: speeds
});
}
this._speed = Math.median(this._settings.get('speeds'));
}
}
},
/**
* Вернуть текущую скорость чтения
* @returns {Number}
*/
getSpeed: function () {
return this._speed;
},
/**
* События зума книги
*/
zoomIn: function () {
reportUnimplemented('zoomIn');
},
zoomOut: function () {
reportUnimplemented('zoomOut');
},
zoomReset: function () {
reportUnimplemented('zoomReset');
}
});
provide(Chitalka);
});

View File

@@ -0,0 +1,131 @@
modules.define(
'test',
[
'chitalka',
'y-dom',
'jquery',
'inherit'
],
function (
provide,
Chitalka,
dom,
$,
inherit
) {
describe('Chitalka', function () {
var chitalka;
var expect = chai.expect;
// Подменяем для тестов функцию доступности читалки для работы
var ChitalkaStub = inherit(Chitalka, {
_isAvailable: function () {
return true;
},
_initUI: function () {
},
_setUpSpeed: function () {
}
});
var emulateKeyDown = function (keycode) {
if (typeof keycode === 'string' || typeof keycode === 'number') {
keycode = {keyCode: keycode};
}
var e = $.Event('keydown', keycode);
$(document).trigger(e);
};
describe('js', function () {
afterEach(function () {
chitalka.destruct();
});
describe('chitalka methods not implemented', function () {
beforeEach(function () {
chitalka = new ChitalkaStub();
});
it('last page', function () {
expect(chitalka.lastPage).to.throw('UNIMPLEMENTED METHOD: lastPage');
});
it('first page', function () {
expect(chitalka.firstPage).to.throw('UNIMPLEMENTED METHOD: firstPage');
});
it('previous page', function () {
expect(chitalka.previousPage).to.throw('UNIMPLEMENTED METHOD: previousPage');
});
it('next page', function () {
expect(chitalka.nextPage).to.throw('UNIMPLEMENTED METHOD: nextPage');
});
it('zoom in', function () {
expect(chitalka.zoomIn).to.throw('UNIMPLEMENTED METHOD: zoomIn');
});
it('zoom out', function () {
expect(chitalka.zoomOut).to.throw('UNIMPLEMENTED METHOD: zoomOut');
});
it('zoom reset', function () {
expect(chitalka.zoomReset).to.throw('UNIMPLEMENTED METHOD: zoomReset');
});
});
describe('chitalka reacts on keyboard events', function () {
beforeEach(function () {
chitalka = new ChitalkaStub(null, {keyboard: true});
});
it('should call firstPage on home press', function () {
var spy = sinon.stub(chitalka, 'firstPage');
emulateKeyDown(36);
sinon.assert.called(spy);
});
it('should call previousPage on left arrow press', function () {
var spy = sinon.stub(chitalka, 'previousPage');
emulateKeyDown(37);
sinon.assert.called(spy);
});
it('should call nextPage on right arrow press', function () {
var spy = sinon.stub(chitalka, 'nextPage');
emulateKeyDown(39);
sinon.assert.called(spy);
});
it('should call lastPage on End press', function () {
var spy = sinon.stub(chitalka, 'lastPage');
emulateKeyDown(35);
sinon.assert.called(spy);
});
it('should call zoomIn on "+" press', function () {
var spy = sinon.stub(chitalka, 'zoomIn');
emulateKeyDown(61);
emulateKeyDown(187);
sinon.assert.callCount(spy, 2);
});
it('should call zoomOut on "-" press', function () {
var spy = sinon.stub(chitalka, 'zoomOut');
emulateKeyDown(173);
emulateKeyDown(189);
sinon.assert.callCount(spy, 2);
});
it('should call zoomReset on "0" press', function () {
var spy = sinon.stub(chitalka, 'zoomReset');
emulateKeyDown({
keyCode: 48,
metaKey: true
});
sinon.assert.called(spy);
});
});
});
});
provide();
}
);

View File

@@ -0,0 +1,8 @@
module.exports = function (bt) {
bt.match('config', function (ctx) {
ctx.setTag('script');
ctx.setAttr('id', 'config');
ctx.setAttr('type', 'text/json');
ctx.setContent(JSON.stringify(ctx.getParam('config')));
});
};

View File

@@ -0,0 +1,4 @@
modules.define('config', function (provide) {
var domNode = document.getElementById('config');
provide(domNode ? JSON.parse(domNode.innerHTML) : {});
});

View File

@@ -0,0 +1,134 @@
module.exports = function (bt) {
bt.setDefaultView('controls', 'default');
bt.match('controls*', function (ctx) {
var content = [];
ctx.enableAutoInit();
var arrows = ctx.getParam('arrows') || false;
var zoom = ctx.getParam('zoom') || false;
var footnotes = ctx.getParam('footnotes') || false;
var pages = ctx.getParam('pages') || false;
ctx.setInitOption('zoom', zoom);
ctx.setInitOption('footnotes', footnotes);
ctx.setInitOption('pages', pages);
ctx.setState('hidden');
if (arrows) {
ctx.setInitOption('arrows', arrows);
content.push([
{
elem: 'arrow-left',
disabled: true
},
{
elem: 'arrow-right'
}
]);
}
if (zoom || footnotes || pages) {
content.push({
elem: 'menu',
zoom: zoom,
footnotes: footnotes,
pages: pages
});
}
ctx.setContent(content);
});
bt.match('controls*__menu', function (ctx) {
ctx.setState('state', 'closed');
ctx.setContent([
{
elem: 'trigger'
},
{
elem: 'buttons',
zoom: ctx.getParam('zoom'),
footnotes: ctx.getParam('footnotes'),
pages: ctx.getParam('pages')
}
]);
});
bt.match(['controls*__arrow-left', 'controls*__arrow-right'], function (ctx) {
if (ctx.getParam('disabled')) {
ctx.setState('disabled');
}
ctx.setContent({
elem: 'arrow-inner'
});
});
bt.match('controls*__buttons', function (ctx) {
var content = [];
var baseHeight = 42;
var items = 0;
if (ctx.getParam('zoom')) {
items += 2;
content.push({
elem: 'plus'
}, {
elem: 'minus'
});
}
if (ctx.getParam('footnotes')) {
items += 1;
content.push({
elem: 'footnotes',
footnotes: ctx.getParam('footnotes')
});
}
if (ctx.getParam('pages')) {
items += 1;
content.push({
elem: 'pages',
pages: ctx.getParam('pages')
});
}
ctx.setAttr('style', 'height: ' + baseHeight * items + 'px');
ctx.setContent(content);
});
bt.match('controls*__footnotes', function (ctx) {
ctx.setState('mode', ctx.getParam('footnotes') || 'appendix');
ctx.setContent([{
elem: 'footnotes-anchor'
}, {
elem: 'footnotes-footnote'
}
]);
});
bt.match('controls*__footnotes-anchor', function (ctx) {
ctx.setTag('span');
ctx.setContent('x');
});
bt.match('controls*__footnotes-footnote', function (ctx) {
ctx.setTag('span');
ctx.setContent('[x]');
});
bt.match('controls*__pages', function (ctx) {
ctx.setState('mode', ctx.getParam('pages') || 'auto');
ctx.setContent([{
elem: 'pages-one'
}, {
elem: 'pages-two'
}
]);
});
};

187
client/core/controls/controls.js vendored Normal file
View File

@@ -0,0 +1,187 @@
modules.define(
'controls',
[
'y-block',
'jquery',
'y-extend',
'inherit'
],
function (
provide,
YBlock,
$,
extend,
inherit
) {
/*jshint devel:true*/
var Controls = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
var menu = this._findElement('menu');
var params = extend({
zoom: false,
// Длина свайпа в пикселах
swipeLength: 20
}, this._getOptions());
this._trigger = this._findElement('trigger');
this._bindTo(this._trigger, 'click', function () {
this._toggleElementState(menu, 'state', 'opened', 'closed');
});
if (params.zoom) {
this._initZoomControls();
}
if (params.footnotes) {
this._initFootnotes();
}
if (params.pages) {
this._initPageModes();
}
if (params.arrows) {
this._initArrowControls();
}
},
_initArrowControls: function () {
this.arrowLeft = this._findElement('arrow-left');
this.arrowRight = this._findElement('arrow-right');
this._bindTo(this.arrowRight, 'click', function () {
this.emit('next-page');
});
this._bindTo(this.arrowLeft, 'click', function () {
this.emit('previous-page');
});
},
_initZoomControls: function () {
this._bindTo(this._findElement('plus'), 'click', function () {
this.emit('zoom-in');
});
this._bindTo(this._findElement('minus'), 'click', function () {
this.emit('zoom-out');
});
},
/**
* Инициализация блока со сносками
*/
_initFootnotes: function () {
this._bindTo(this._findElement('footnotes'), 'click', function (e) {
this._toggleElementState($(e.currentTarget), 'mode', 'appendix', 'inline');
this.emit('footnotes-' + this._getElementState($(e.currentTarget), 'mode'));
});
},
/**
* Устанавливает режим сносок в нужный
*
* @param {String} mode
*/
setFootnotesMode: function (mode) {
this._setElementState(this._findElement('footnotes'), 'mode', mode);
},
/**
* Инициализация контрола страничного отображения
*/
_initPageModes: function () {
var pages = this._findElement('pages');
var modes = ['auto', 'one', 'two'];
this._pageMode = modes.indexOf(this._getElementState(pages, 'mode'));
this._bindTo(pages, 'click', function () {
this._pageMode = (this._pageMode + 1) % 3;
this._setElementState(pages, 'mode', modes[this._pageMode]);
this.emit('pages-' + this._getElementState(pages, 'mode'));
});
},
/**
* Устанавливает режим отображения в нужный
*
* @param {String} mode
*/
setPageViewMode: function (mode) {
var pages = this._findElement('pages');
var modes = ['auto', 'one', 'two'];
this._setElementState(pages, 'mode', mode);
this._pageMode = modes.indexOf(mode);
},
resetZoomButtons: function () {
this._removeElementState(
this._findElement('plus'),
'disabled'
);
this._removeElementState(
this._findElement('minus'),
'disabled'
);
},
disableZoomIn: function () {
this._setElementState(
this._findElement('plus'),
'disabled'
);
},
disableZoomOut: function () {
this._setElementState(
this._findElement('minus'),
'disabled'
);
},
resetArrows: function () {
this._removeElementState(
this.arrowLeft,
'disabled'
);
this._removeElementState(
this.arrowRight,
'disabled'
);
},
disableArrowNext: function () {
this._setElementState(
this.arrowRight,
'disabled'
);
},
disableArrowPrev: function () {
this._setElementState(
this.arrowLeft,
'disabled'
);
},
/**
* Показывает блок с контролами контролы
*/
show: function () {
this._removeState('hidden');
},
/**
* Показывает блок с контролами контролы
*/
hide: function () {
this._setState('hidden');
}
}, {
getBlockName: function () {
return 'controls';
}
});
provide(Controls);
});

View File

@@ -0,0 +1,319 @@
.controls_default {
$item-height = 42px;
$margin = 15px;
$arrowWidth = 70px;
$arrowImageWidth = 15px;
$arrowImageHeight = 24px;
item() {
position: relative;
width: 42px;
height: $item-height;
cursor: pointer;
background-repeat: no-repeat;
background-position: center, 50% 100%;
&._disabled {
cursor: default;
}
}
&._hidden &__menu {
/*
Добавляем стартовое положение для анимации появления элемента menu.
131px - длина окружности кнопки "menu",
что бы выкатывание(появление) кнопки было реалистичней
*/
transform: translateX(-131px) rotate(-360deg);
}
&__menu {
width: 42px;
margin-left: $margin;
user-select: none;
transition-timing-function: cubic-bezier(.75, 0, .25, 1);
transition-duration: .25s, .25s, .25s, .5s;
transition-property: background-color, box-shadow, opacity, transform;
transform: translateX(0) rotate(0);
border-radius: 21px;
}
&__trigger {
item();
&::before {
position: absolute;
top: 50%;
left: 50%;
content: '';
width: 6px;
height: 28px;
margin: -14px 0 0 -3px;
background: #fff;
border-radius: 5px;
box-shadow: 0 0 2px #999;
transition-timing-function: inherit;
transition-duration: .25s;
transition-property: width, height, margin, background-color;
transition:
.15s width cubic-bezier(.75, 0, .25, 1),
.15s height cubic-bezier(.75, 0, .25, 1),
.15s margin cubic-bezier(.75, 0, .25, 1),
.05s background-color linear;
}
}
&__plus,
&__minus {
item();
&::before {
position: absolute;
top: 50%;
left: 50%;
content: '';
height: 2px;
width: 18px;
margin: -1px 0 0 -9px;
background: #000;
}
&:hover::before,
&:hover::after {
background: #ffce00;
}
&._disabled::before,
&._disabled::after {
background: #ccc !important;
}
}
&__plus {
&::after {
position: absolute;
top: 50%;
left: 50%;
content: '';
width: 2px;
height: 18px;
margin: -9px 0 0 -1px;
background: #000;
}
}
&__footnotes {
item();
line-height: ($item-height - 2);
text-align: center;
&._mode_appendix &-footnote {
vertical-align: super;
font-size: 13px;
}
}
&__pages {
item();
text-align: center;
line-height: $item-height;
&::before {
position: absolute;
z-index: 1;
top: 0;
left: 0;
content: 'auto';
width: 100%;
height: 100%;
/* Выравнивает текст по центру */
line-height: ($item-height - 8);
}
&-one {
width: 8px;
height: 14px;
display: inline-block;
border: 2px solid #eee;
}
&-two {
position: relative;
width: 12px;
height: 14px;
margin-left: 3px;
display: inline-block;
border: 2px solid #eee;
&::before {
position: absolute;
left: 50%;
content: '';
margin-left: -1px;
width: 2px;
height: 100%;
background: #eee;
}
}
&._mode_one::before,
&._mode_two::before {
display: none;
}
&._mode_one &-one,
&._mode_two &-two {
border-color: #000;
margin-left: 0;
}
&._mode_two &-two::before {
background: #000;
}
&._mode_one &-two {
display: none;
}
&._mode_two &-one {
display: none;
}
}
&__menu._state_closed {
background-color: rgba(127, 127, 127, .5);
}
&__menu._state_closed &__buttons {
height: 0 !important;
}
&__buttons {
overflow: hidden;
transition-timing-function: cubic-bezier(.75, 0, .25, 1);
transition-duration: .25s;
transition-property: height opacity;
transform: translateZ(0);
}
&__menu._state_opened {
background-color: #fff;
box-shadow: rgba(0, 0, 0, .3) 0 1px 4px 0;
}
&__menu._state_opened &__trigger::before {
height: 10px;
width: 10px;
margin: -5px 0 0 -5px;
box-shadow: none;
background: #000;
transition:
.15s width cubic-bezier(.75, 0, .25, 1),
.15s height cubic-bezier(.75, 0, .25, 1),
.15s margin cubic-bezier(.75, 0, .25, 1);
}
&__menu._state_opened &__trigger:hover::before {
background: #ffce00;
}
&._hidden &__arrow-right {
opacity: 0;
}
&__arrow {
&-left,
&-right {
position: absolute;
top: 90px;
bottom: 4px;
width: $arrowWidth;
cursor: pointer;
user-select: none;
transition: transform .5s ease;
}
&-left:hover &-inner,
&-right:hover &-inner {
opacity: 1;
}
&-left &-inner,
&-right &-inner {
position: absolute;
top: 50%;
left: 50%;
width: $arrowImageWidth;
height: $arrowImageHeight;
margin-top: -($arrowImageHeight / 2);
margin-left: -($arrowImageWidth / 2);
opacity: .3;
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTMiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCAxMyA0OCI+PHN2ZyB3aWR0aD0iMTMiIGhlaWdodD0iMjQiIGlkPSJhcnJvd19sZWZ0IiB5PSIwIj48cGF0aCBkPSJNMTMgLjY2N0wxMi4zMTYgMCAwIDEyLjAxbC42ODQuNjY3TDEzIC42NjdtMCAyMi42NjZsLS42ODQuNjY3TDAgMTEuOTlsLjY4NC0uNjY3TDEzIDIzLjMzMyIgb3BhY2l0eT0iMSIgZmlsbD0iIzAwMCIvPjwvc3ZnPjxzdmcgd2lkdGg9IjEzIiBoZWlnaHQ9IjI0IiBpZD0iYXJyb3dfcmlnaHQiIHk9IjI0Ij48cGF0aCBkPSJNMCAuNjY3TC42ODQgMCAxMyAxMi4wMWwtLjY4NC42NjdMMCAuNjY3bTAgMjIuNjY2TC42ODQgMjQgMTMgMTEuOTlsLS42ODQtLjY2N0wwIDIzLjMzMyIgb3BhY2l0eT0iMSIgZmlsbD0iIzAwMCIvPjwvc3ZnPjwvc3ZnPgo="); no-repeat;
transition: opacity .3s ease;
/*.y-ua_svg_no & {*/
/*background-image: url(images/arrows.png);*/
/*}*/
}
&-left._disabled {
transform: translateX(-100%);
}
&-left {
left: 0;
}
&-right &-inner {
background-position: 0 -24px;
}
&-right._disabled {
transform: translateX(100%);
}
&-right {
opacity: 1;
right: 0;
}
}
}

View File

@@ -0,0 +1,108 @@
/* global FileReader */
modules.define(
'file-drag',
[
'y-block',
'jquery',
'y-extend',
'inherit'
],
function (
provide,
YBlock,
$,
extend,
inherit
) {
var FileDrag = inherit(YBlock, {
__constructor: function (element) {
this.__base.apply(this, arguments);
if (!element) {
return;
}
this._bindTo(element, 'dragstart', this._onDragOver.bind(this));
this._bindTo(element, 'dragenter', this._onDragOver.bind(this));
this._bindTo(element, 'dragover', this._onDragOver.bind(this));
this._bindTo(element, 'dragleave', this._onDragEnd.bind(this));
this._bindTo(element, 'dragend', this._onDragEnd.bind(this));
this._bindTo(element, 'drop', this._onDrop.bind(this));
},
/**
* Действия по окончанию drag-событий
*
* @param {Event} e
*/
_onDragEnd: function (e) {
this._stopEvent(e);
this._drag = false;
setTimeout(function () {
if (!this._drag) {
this.emit('hide-drag');
}
}.bind(this), 100);
},
/**
* Действия во время drag-событий
*
* @param {Event} e
*/
_onDragOver: function (e) {
this._stopEvent(e);
this._drag = true;
this.emit('show-drag');
},
/**
* Действия по бросания файла (drop-событие)
*
* @param {Event} e
*/
_onDrop: function (e) {
this._stopEvent(e);
this.emit('hide-drag');
var files = e.originalEvent.dataTransfer.files;
if (files.length > 0 && window.FormData !== undefined && files[0]) {
this.emit('file-dropped');
var file = files[0];
var reader = new FileReader();
reader.onload = function (e) {
var res = e.target.result;
this.emit('file-loaded', {
result: res,
file: file
});
}.bind(this);
reader.readAsDataURL(file);
}
},
/**
* Останавливает всплытие события
*
* @param {Event} e событие
*/
_stopEvent: function (e) {
e.stopPropagation();
e.preventDefault();
}
}, {
getBlockName: function () {
return 'file-drag';
}
});
provide(FileDrag);
});

4579
client/core/gsap/gsap.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
spin_skin_common() {
& {
display: inline-block;
box-sizing: border-box;
border: 2px solid transparent;
border-radius: 100px;
/*
* Поддержка CSS анимаций и CSS градиентов у основных браузеров совпадает
* Если браузер не поддерживает градиенты, будет показана gif-анимация
*/
background-image: url(images/spin.gif);
background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0));
}
/* Для правильного позиционирования прелоадера относительно baseline */
&:after {
visibility: hidden;
content: '\00A0'; /* &nbsp; */
}
&._progressed {
display: inline-block;
animation: spin 1s infinite linear;
backface-visibility: hidden; /* Для ускорения анимации */
}
@keyframes spin
{
from
{
border-top-color: #fc0;
border-left-color: #fc0;
transform: rotate(0deg);
}
to
{
border-top-color: #fc0;
border-left-color: #fc0;
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
}

View File

@@ -0,0 +1,9 @@
spin_skin_size-l() {
width: 38px;
height: 38px;
font-size: 18px;
line-height: 34px;
background-position: -2px -106px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,7 @@
module.exports = function (bt) {
bt.setDefaultView('spin', 'default');
bt.match('spin_default*', function (ctx) {
ctx.setState('progressed');
});
};

View File

@@ -0,0 +1 @@
- view: default

38
client/core/spin/spin.js Normal file
View File

@@ -0,0 +1,38 @@
modules.define(
'spin',
[
'y-block',
'inherit'
],
function (
provide,
YBlock,
inherit
) {
var Spin = inherit(YBlock, {
__constructor: function () {
this.__base.apply(this, arguments);
},
/**
* Останаваливает анимацию спиннера
*/
stop: function () {
this._removeState('progressed');
},
/**
* Запускает анимацию спиннера
*/
start: function () {
this._setState('progressed');
}
}, {
getBlockName: function () {
return 'spin';
}
});
provide(Spin);
});

View File

@@ -0,0 +1,4 @@
.spin_default-large {
spin_skin_common();
spin_skin_size-l();
}

View File

@@ -0,0 +1,2 @@
- skin: '*'
required: true

View File

@@ -0,0 +1,97 @@
modules.define(
'storage',
[
'y-block',
'jquery',
'y-extend',
'inherit'
],
function (
provide,
YBlock,
$,
extend,
inherit
) {
var localStorage = window.localStorage;
var Storage = inherit(YBlock, {
__constructor: function (storageId) {
this.__base.apply(this, arguments);
this._id = storageId;
this._restore();
},
/**
* Возвращает значение из хранилища
*
* @param {String} key ключ хранилища
*
* @returns {String} значение
*/
get: function (key) {
return this._data && this._data[key];
},
/**
* Удаляет ключ из хранилища
*
* @param {String} key
*/
remove: function (key) {
delete this._data[key];
this._save();
},
/**
* Сохранить данные в хранилище
*
* @param {String|Object} key ключ сохраняемого или же объект с данными для хранения
* @param {String} [value] значение параметра для хранения
*/
save: function (key, value) {
if (!value && typeof key === 'object') {
extend(this._data, key);
} else {
this._data[key] = value;
}
this._save();
},
/**
* Взять данные из storage и наполнить ими текущий объект
*/
_restore: function () {
this._data = localStorage.getItem(this._id) || {};
if (typeof this._data === 'string') {
try {
this._data = JSON.parse(this._data);
} catch (e) {
this._data = {};
}
}
if (typeof this._data !== 'object') {
this._data = {};
}
},
/**
* Выполнить сохранение всех данных в localStoarage
*/
_save: function () {
localStorage.setItem(this._id, JSON.stringify(this._data));
}
}, {
getBlockName: function () {
return 'storage';
}
});
provide(Storage);
});

993
client/core/unzip/unzip.js Normal file
View File

@@ -0,0 +1,993 @@
// jshint ignore: start
// jscs:disable
modules.define(
'unzip',
[],
function (
provide
) {
var zip;
var ERR_BAD_FORMAT = "File format is not recognized.";
var ERR_ENCRYPTED = "File contains encrypted entry.";
var ERR_ZIP64 = "File is using Zip64 (4gb+ file size).";
var ERR_READ = "Error while reading zip file.";
var ERR_WRITE = "Error while writing zip file.";
var ERR_WRITE_DATA = "Error while writing file data.";
var ERR_READ_DATA = "Error while reading file data.";
var ERR_DUPLICATED_NAME = "File already exists.";
var CHUNK_SIZE = 512 * 1024;
var INFLATE_JS = "inflate.js";
var DEFLATE_JS = "deflate.js";
var TEXT_PLAIN = "text/plain";
var MESSAGE_EVENT = "message";
var appendABViewSupported;
try {
appendABViewSupported = new Blob([new DataView(new ArrayBuffer(0))]).size === 0;
} catch (e) {}
function Crc32() {
var crc = -1,
that = this;
that.append = function (data) {
var offset, table = that.table;
for (offset = 0; offset < data.length; offset++)
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
};
that.get = function () {
return~crc;
};
}
Crc32.prototype.table = (function () {
var i, j, t, table = [];
for (i = 0; i < 256; i++) {
t = i;
for (j = 0; j < 8; j++)
if (t & 1) t = (t >>> 1) ^ 0xEDB88320;
else t = t >>> 1;
table[i] = t;
}
return table;
})();
function blobSlice(blob, index, length) {
if (blob.slice) return blob.slice(index, index + length);
else if (blob.webkitSlice) return blob.webkitSlice(index, index + length);
else if (blob.mozSlice) return blob.mozSlice(index, index + length);
else if (blob.msSlice) return blob.msSlice(index, index + length);
}
function getDataHelper(byteLength, bytes) {
var dataBuffer, dataArray;
dataBuffer = new ArrayBuffer(byteLength);
dataArray = new Uint8Array(dataBuffer);
if (bytes) dataArray.set(bytes, 0);
return {
buffer: dataBuffer,
array: dataArray,
view: new DataView(dataBuffer)
};
}
// Readers
function Reader() {}
function TextReader(text) {
var that = this,
blobReader;
function init(callback, onerror) {
var blob = new Blob([text], {
type: TEXT_PLAIN
});
blobReader = new BlobReader(blob);
blobReader.init(function () {
that.size = blobReader.size;
callback();
}, onerror);
}
function readUint8Array(index, length, callback, onerror) {
blobReader.readUint8Array(index, length, callback, onerror);
}
that.size = 0;
that.init = init;
that.readUint8Array = readUint8Array;
}
TextReader.prototype = new Reader();
TextReader.prototype.constructor = TextReader;
function Data64URIReader(dataURI) {
var that = this,
dataStart;
function init(callback) {
var dataEnd = dataURI.length;
while (dataURI.charAt(dataEnd - 1) == "=")
dataEnd--;
dataStart = dataURI.indexOf(",") + 1;
that.size = Math.floor((dataEnd - dataStart) * 0.75);
callback();
}
function readUint8Array(index, length, callback) {
var i, data = getDataHelper(length);
var start = Math.floor(index / 3) * 4;
var end = Math.ceil((index + length) / 3) * 4;
var bytes = window.atob(dataURI.substring(start + dataStart, end + dataStart));
var delta = index - Math.floor(start / 4) * 3;
for (i = delta; i < delta + length; i++)
data.array[i - delta] = bytes.charCodeAt(i);
callback(data.array);
}
that.size = 0;
that.init = init;
that.readUint8Array = readUint8Array;
}
Data64URIReader.prototype = new Reader();
Data64URIReader.prototype.constructor = Data64URIReader;
function BlobReader(blob) {
var that = this;
function init(callback) {
this.size = blob.size;
callback();
}
function readUint8Array(index, length, callback, onerror) {
var reader = new FileReader();
reader.onload = function (e) {
callback(new Uint8Array(e.target.result));
};
reader.onerror = onerror;
reader.readAsArrayBuffer(blobSlice(blob, index, length));
}
that.size = 0;
that.init = init;
that.readUint8Array = readUint8Array;
}
BlobReader.prototype = new Reader();
BlobReader.prototype.constructor = BlobReader;
// Writers
function Writer() {}
Writer.prototype.getData = function (callback) {
callback(this.data);
};
function TextWriter(encoding) {
var that = this,
blob;
function init(callback) {
blob = new Blob([], {
type: TEXT_PLAIN
});
callback();
}
function writeUint8Array(array, callback) {
blob = new Blob([blob, appendABViewSupported ? array : array.buffer], {
type: TEXT_PLAIN
});
callback();
}
function getData(callback, onerror) {
var reader = new FileReader();
reader.onload = function (e) {
callback(e.target.result);
};
reader.onerror = onerror;
reader.readAsText(blob, encoding);
}
that.init = init;
that.writeUint8Array = writeUint8Array;
that.getData = getData;
}
TextWriter.prototype = new Writer();
TextWriter.prototype.constructor = TextWriter;
function Data64URIWriter(contentType) {
var that = this,
data = "",
pending = "";
function init(callback) {
data += "data:" + (contentType || "") + ";base64,";
callback();
}
function writeUint8Array(array, callback) {
var i, delta = pending.length,
dataString = pending;
pending = "";
for (i = 0; i < (Math.floor((delta + array.length) / 3) * 3) - delta; i++)
dataString += String.fromCharCode(array[i]);
for (; i < array.length; i++)
pending += String.fromCharCode(array[i]);
if (dataString.length > 2) data += window.btoa(dataString);
else pending = dataString;
callback();
}
function getData(callback) {
callback(data + window.btoa(pending));
}
that.init = init;
that.writeUint8Array = writeUint8Array;
that.getData = getData;
}
Data64URIWriter.prototype = new Writer();
Data64URIWriter.prototype.constructor = Data64URIWriter;
function BlobWriter(contentType) {
var blob, that = this;
function init(callback) {
blob = new Blob([], {
type: contentType
});
callback();
}
function writeUint8Array(array, callback) {
blob = new Blob([blob, appendABViewSupported ? array : array.buffer], {
type: contentType
});
callback();
}
function getData(callback) {
callback(blob);
}
that.init = init;
that.writeUint8Array = writeUint8Array;
that.getData = getData;
}
BlobWriter.prototype = new Writer();
BlobWriter.prototype.constructor = BlobWriter;
// inflate/deflate core functions
function launchWorkerProcess(worker, reader, writer, offset, size, onappend, onprogress, onend, onreaderror, onwriteerror) {
var chunkIndex = 0,
index, outputSize;
function onflush() {
worker.removeEventListener(MESSAGE_EVENT, onmessage, false);
onend(outputSize);
}
function onmessage(event) {
var message = event.data,
data = message.data;
if (message.onappend) {
outputSize += data.length;
writer.writeUint8Array(data, function () {
onappend(false, data);
step();
}, onwriteerror);
}
if (message.onflush) if (data) {
outputSize += data.length;
writer.writeUint8Array(data, function () {
onappend(false, data);
onflush();
}, onwriteerror);
} else onflush();
if (message.progress && onprogress) onprogress(index + message.current, size);
}
function step() {
index = chunkIndex * CHUNK_SIZE;
if (index < size) reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function (array) {
worker.postMessage({
append: true,
data: array
});
chunkIndex++;
if (onprogress) onprogress(index, size);
onappend(true, array);
}, onreaderror);
else worker.postMessage({
flush: true
});
}
outputSize = 0;
worker.addEventListener(MESSAGE_EVENT, onmessage, false);
step();
}
function launchProcess(process, reader, writer, offset, size, onappend, onprogress, onend, onreaderror, onwriteerror) {
var chunkIndex = 0,
index, outputSize = 0;
function step() {
var outputData;
index = chunkIndex * CHUNK_SIZE;
if (index < size) reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function (inputData) {
var outputData = process.append(inputData, function () {
if (onprogress) onprogress(offset + index, size);
});
outputSize += outputData.length;
onappend(true, inputData);
writer.writeUint8Array(outputData, function () {
onappend(false, outputData);
chunkIndex++;
setTimeout(step, 1);
}, onwriteerror);
if (onprogress) onprogress(index, size);
}, onreaderror);
else {
outputData = process.flush();
if (outputData) {
outputSize += outputData.length;
writer.writeUint8Array(outputData, function () {
onappend(false, outputData);
onend(outputSize);
}, onwriteerror);
} else onend(outputSize);
}
}
step();
}
function inflate(reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
var worker, crc32 = new Crc32();
function oninflateappend(sending, array) {
if (computeCrc32 && !sending) crc32.append(array);
}
function oninflateend(outputSize) {
onend(outputSize, crc32.get());
}
if (zip.useWebWorkers) {
worker = new Worker(zip.workerScriptsPath + INFLATE_JS);
launchWorkerProcess(worker, reader, writer, offset, size, oninflateappend, onprogress, oninflateend, onreaderror, onwriteerror);
} else launchProcess(new zip.Inflater(), reader, writer, offset, size, oninflateappend, onprogress, oninflateend, onreaderror, onwriteerror);
return worker;
}
function deflate(reader, writer, level, onend, onprogress, onreaderror, onwriteerror) {
var worker, crc32 = new Crc32();
function ondeflateappend(sending, array) {
if (sending) crc32.append(array);
}
function ondeflateend(outputSize) {
onend(outputSize, crc32.get());
}
function onmessage() {
worker.removeEventListener(MESSAGE_EVENT, onmessage, false);
launchWorkerProcess(worker, reader, writer, 0, reader.size, ondeflateappend, onprogress, ondeflateend, onreaderror, onwriteerror);
}
if (zip.useWebWorkers) {
worker = new Worker(zip.workerScriptsPath + DEFLATE_JS);
worker.addEventListener(MESSAGE_EVENT, onmessage, false);
worker.postMessage({
init: true,
level: level
});
} else launchProcess(new zip.Deflater(), reader, writer, 0, reader.size, ondeflateappend, onprogress, ondeflateend, onreaderror, onwriteerror);
return worker;
}
function copy(reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
var chunkIndex = 0,
crc32 = new Crc32();
function step() {
var index = chunkIndex * CHUNK_SIZE;
if (index < size) reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function (array) {
if (computeCrc32) crc32.append(array);
if (onprogress) onprogress(index, size, array);
writer.writeUint8Array(array, function () {
chunkIndex++;
step();
}, onwriteerror);
}, onreaderror);
else onend(size, crc32.get());
}
step();
}
// ZipReader
function decodeASCII(str) {
var i, out = "",
charCode, extendedASCII = ['Ç', 'ü', 'é', 'â', 'ä', 'à', 'å', 'ç', 'ê', 'ë',
'è', 'ï', 'î', 'ì', 'Ä', 'Å', 'É', 'æ', 'Æ', 'ô', 'ö', 'ò', 'û', 'ù',
'ÿ', 'Ö', 'Ü', 'ø', '£', 'Ø', '×', 'ƒ', 'á', 'í', 'ó', 'ú', 'ñ', 'Ñ',
'ª', 'º', '¿', '®', '¬', '½', '¼', '¡', '«', '»', '_', '_', '_', '¦', '¦',
'Á', 'Â', 'À', '©', '¦', '¦', '+', '+', '¢', '¥', '+', '+', '-', '-', '+', '-', '+', 'ã',
'Ã', '+', '+', '-', '-', '¦', '-', '+', '¤', 'ð', 'Ð', 'Ê', 'Ë', 'È', 'i', 'Í', 'Î',
'Ï', '+', '+', '_', '_', '¦', 'Ì', '_', 'Ó', 'ß', 'Ô', 'Ò', 'õ', 'Õ', 'µ', 'þ',
'Þ', 'Ú', 'Û', 'Ù', 'ý', 'Ý', '¯', '´', '­', '±', '_', '¾', '¶', '§',
'÷', '¸', '°', '¨', '·', '¹', '³', '²', '_', ' '];
for (i = 0; i < str.length; i++) {
charCode = str.charCodeAt(i) & 0xFF;
if (charCode > 127) out += extendedASCII[charCode - 128];
else out += String.fromCharCode(charCode);
}
return out;
}
function decodeUTF8(string) {
return decodeURIComponent(escape(string));
}
function getString(bytes) {
var i, str = "";
for (i = 0; i < bytes.length; i++)
str += String.fromCharCode(bytes[i]);
return str;
}
function getDate(timeRaw) {
var date = (timeRaw & 0xffff0000) >> 16,
time = timeRaw & 0x0000ffff;
try {
return new Date(1980 + ((date & 0xFE00) >> 9), ((date & 0x01E0) >> 5) - 1, date & 0x001F, (time & 0xF800) >> 11, (time & 0x07E0) >> 5, (time & 0x001F) * 2, 0);
} catch (e) {}
}
function readCommonHeader(entry, data, index, centralDirectory, onerror) {
entry.version = data.view.getUint16(index, true);
entry.bitFlag = data.view.getUint16(index + 2, true);
entry.compressionMethod = data.view.getUint16(index + 4, true);
entry.lastModDateRaw = data.view.getUint32(index + 6, true);
entry.lastModDate = getDate(entry.lastModDateRaw);
if ((entry.bitFlag & 0x01) === 0x01) {
onerror(ERR_ENCRYPTED);
return;
}
if (centralDirectory || (entry.bitFlag & 0x0008) != 0x0008) {
entry.crc32 = data.view.getUint32(index + 10, true);
entry.compressedSize = data.view.getUint32(index + 14, true);
entry.uncompressedSize = data.view.getUint32(index + 18, true);
}
if (entry.compressedSize === 0xFFFFFFFF || entry.uncompressedSize === 0xFFFFFFFF) {
onerror(ERR_ZIP64);
return;
}
entry.filenameLength = data.view.getUint16(index + 22, true);
entry.extraFieldLength = data.view.getUint16(index + 24, true);
}
function createZipReader(reader, onerror) {
function Entry() {}
Entry.prototype.getData = function (writer, onend, onprogress, checkCrc32) {
var that = this,
worker;
function terminate(callback, param) {
if (worker) worker.terminate();
worker = null;
if (callback) callback(param);
}
function testCrc32(crc32) {
var dataCrc32 = getDataHelper(4);
dataCrc32.view.setUint32(0, crc32);
return that.crc32 == dataCrc32.view.getUint32(0);
}
function getWriterData(uncompressedSize, crc32) {
if (checkCrc32 && !testCrc32(crc32)) onreaderror();
else writer.getData(function (data) {
terminate(onend, data);
});
}
function onreaderror() {
terminate(onerror, ERR_READ_DATA);
}
function onwriteerror() {
terminate(onerror, ERR_WRITE_DATA);
}
reader.readUint8Array(that.offset, 30, function (bytes) {
var data = getDataHelper(bytes.length, bytes),
dataOffset;
if (data.view.getUint32(0) != 0x504b0304) {
onerror(ERR_BAD_FORMAT);
return;
}
readCommonHeader(that, data, 4, false, onerror);
dataOffset = that.offset + 30 + that.filenameLength + that.extraFieldLength;
writer.init(function () {
if (that.compressionMethod === 0) copy(reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
else worker = inflate(reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
}, onwriteerror);
}, onreaderror);
};
function seekEOCDR(offset, entriesCallback) {
reader.readUint8Array(reader.size - offset, offset, function (bytes) {
var dataView = getDataHelper(bytes.length, bytes).view;
if (dataView.getUint32(0) != 0x504b0506) {
seekEOCDR(offset + 1, entriesCallback);
} else {
entriesCallback(dataView);
}
}, function () {
onerror(ERR_READ);
});
}
return {
getEntries: function (callback) {
if (reader.size < 22) {
onerror(ERR_BAD_FORMAT);
return;
}
// look for End of central directory record
seekEOCDR(22, function (dataView) {
var datalength, fileslength;
datalength = dataView.getUint32(16, true);
fileslength = dataView.getUint16(8, true);
reader.readUint8Array(datalength, reader.size - datalength, function (bytes) {
var i, index = 0,
entries = [],
entry, filename, comment, data = getDataHelper(bytes.length, bytes);
for (i = 0; i < fileslength; i++) {
entry = new Entry();
if (data.view.getUint32(index) != 0x504b0102) {
onerror(ERR_BAD_FORMAT);
return;
}
readCommonHeader(entry, data, index + 6, true, onerror);
entry.commentLength = data.view.getUint16(index + 32, true);
entry.directory = ((data.view.getUint8(index + 38) & 0x10) == 0x10);
entry.offset = data.view.getUint32(index + 42, true);
filename = getString(data.array.subarray(index + 46, index + 46 + entry.filenameLength));
entry.filename = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(filename) : decodeASCII(filename);
if (!entry.directory && entry.filename.charAt(entry.filename.length - 1) == "/") entry.directory = true;
comment = getString(data.array.subarray(index + 46 + entry.filenameLength + entry.extraFieldLength, index + 46 + entry.filenameLength + entry.extraFieldLength + entry.commentLength));
entry.comment = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(comment) : decodeASCII(comment);
entries.push(entry);
index += 46 + entry.filenameLength + entry.extraFieldLength + entry.commentLength;
}
callback(entries);
}, function () {
onerror(ERR_READ);
});
});
},
close: function (callback) {
if (callback) callback();
}
};
}
// ZipWriter
function encodeUTF8(string) {
return unescape(encodeURIComponent(string));
}
function getBytes(str) {
var i, array = [];
for (i = 0; i < str.length; i++)
array.push(str.charCodeAt(i));
return array;
}
function createZipWriter(writer, onerror, dontDeflate) {
var worker, files = {}, filenames = [],
datalength = 0;
function terminate(callback, message) {
if (worker) worker.terminate();
worker = null;
if (callback) callback(message);
}
function onwriteerror() {
terminate(onerror, ERR_WRITE);
}
function onreaderror() {
terminate(onerror, ERR_READ_DATA);
}
return {
add: function (name, reader, onend, onprogress, options) {
var header, filename, date;
function writeHeader(callback) {
var data;
date = options.lastModDate || new Date();
header = getDataHelper(26);
files[name] = {
headerArray: header.array,
directory: options.directory,
filename: filename,
offset: datalength,
comment: getBytes(encodeUTF8(options.comment || ""))
};
header.view.setUint32(0, 0x14000808);
if (options.version) header.view.setUint8(0, options.version);
if (!dontDeflate && options.level !== 0 && !options.directory) header.view.setUint16(4, 0x0800);
header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true);
header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true);
header.view.setUint16(22, filename.length, true);
data = getDataHelper(30 + filename.length);
data.view.setUint32(0, 0x504b0304);
data.array.set(header.array, 4);
data.array.set(filename, 30);
datalength += data.array.length;
writer.writeUint8Array(data.array, callback, onwriteerror);
}
function writeFooter(compressedLength, crc32) {
var footer = getDataHelper(16);
datalength += compressedLength || 0;
footer.view.setUint32(0, 0x504b0708);
if (typeof crc32 != "undefined") {
header.view.setUint32(10, crc32, true);
footer.view.setUint32(4, crc32, true);
}
if (reader) {
footer.view.setUint32(8, compressedLength, true);
header.view.setUint32(14, compressedLength, true);
footer.view.setUint32(12, reader.size, true);
header.view.setUint32(18, reader.size, true);
}
writer.writeUint8Array(footer.array, function () {
datalength += 16;
terminate(onend);
}, onwriteerror);
}
function writeFile() {
options = options || {};
name = name.trim();
if (options.directory && name.charAt(name.length - 1) != "/") name += "/";
if (files.hasOwnProperty(name)) {
onerror(ERR_DUPLICATED_NAME);
return;
}
filename = getBytes(encodeUTF8(name));
filenames.push(name);
writeHeader(function () {
if (reader) if (dontDeflate || options.level === 0) copy(reader, writer, 0, reader.size, true, writeFooter, onprogress, onreaderror, onwriteerror);
else worker = deflate(reader, writer, options.level, writeFooter, onprogress, onreaderror, onwriteerror);
else writeFooter();
}, onwriteerror);
}
if (reader) reader.init(writeFile, onreaderror);
else writeFile();
},
close: function (callback) {
var data, length = 0,
index = 0,
indexFilename, file;
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]];
length += 46 + file.filename.length + file.comment.length;
}
data = getDataHelper(length + 22);
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
file = files[filenames[indexFilename]];
data.view.setUint32(index, 0x504b0102);
data.view.setUint16(index + 4, 0x1400);
data.array.set(file.headerArray, index + 6);
data.view.setUint16(index + 32, file.comment.length, true);
if (file.directory) data.view.setUint8(index + 38, 0x10);
data.view.setUint32(index + 42, file.offset, true);
data.array.set(file.filename, index + 46);
data.array.set(file.comment, index + 46 + file.filename.length);
index += 46 + file.filename.length + file.comment.length;
}
data.view.setUint32(index, 0x504b0506);
data.view.setUint16(index + 8, filenames.length, true);
data.view.setUint16(index + 10, filenames.length, true);
data.view.setUint32(index + 12, length, true);
data.view.setUint32(index + 16, datalength, true);
writer.writeUint8Array(data.array, function () {
terminate(function () {
writer.getData(callback);
});
}, onwriteerror);
}
};
}
zip = {
Reader: Reader,
Writer: Writer,
BlobReader: BlobReader,
Data64URIReader: Data64URIReader,
TextReader: TextReader,
BlobWriter: BlobWriter,
Data64URIWriter: Data64URIWriter,
TextWriter: TextWriter,
createReader: function (reader, callback, onerror) {
reader.init(function () {
callback(createZipReader(reader, onerror));
}, onerror);
},
createWriter: function (writer, callback, onerror, dontDeflate) {
writer.init(function () {
callback(createZipWriter(writer, onerror, dontDeflate));
}, onerror);
},
workerScriptsPath: "",
useWebWorkers: true
};
/*
Copyright (c) 2013 Gildas Lormeau. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the distribution.
3. The names of the authors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
(function () {
var ERR_HTTP_RANGE = "HTTP Range not supported.";
var Reader = zip.Reader;
var Writer = zip.Writer;
var ZipDirectoryEntry;
var appendABViewSupported;
try {
appendABViewSupported = new Blob([new DataView(new ArrayBuffer(0))]).size === 0;
} catch (e) {}
function HttpReader(url) {
var that = this;
function getData(callback, onerror) {
var request;
if (!that.data) {
request = new XMLHttpRequest();
request.addEventListener("load", function (data) {
// При 500-ка не срабатывает error - триггерим руками
if (data.target.status >= 500) {
onerror();
return;
}
if (!that.size) that.size = Number(request.getResponseHeader("Content-Length"));
that.data = new Uint8Array(request.response);
callback();
}, false);
request.addEventListener("error", onerror, false);
request.open("GET", url);
request.responseType = "arraybuffer";
request.send();
} else callback();
}
function init(callback, onerror) {
var request = new XMLHttpRequest();
request.addEventListener("load", function (data) {
// При 500-ка не срабатывает error - триггерим руками
if (data.target.status >= 500) {
onerror();
return;
}
that.size = Number(request.getResponseHeader("Content-Length"));
callback();
}, false);
request.addEventListener("error", onerror, false);
request.open("HEAD", url);
request.send();
}
function readUint8Array(index, length, callback, onerror) {
getData(function () {
callback(new Uint8Array(that.data.subarray(index, index + length)));
}, onerror);
}
that.size = 0;
that.init = init;
that.readUint8Array = readUint8Array;
}
HttpReader.prototype = new Reader();
HttpReader.prototype.constructor = HttpReader;
function HttpRangeReader(url) {
var that = this;
function init(callback, onerror) {
var request = new XMLHttpRequest();
request.addEventListener("load", function () {
that.size = Number(request.getResponseHeader("Content-Length"));
if (request.getResponseHeader("Accept-Ranges") == "bytes") callback();
else onerror(ERR_HTTP_RANGE);
}, false);
request.addEventListener("error", onerror, false);
request.open("HEAD", url);
request.send();
}
function readArrayBuffer(index, length, callback, onerror) {
var request = new XMLHttpRequest();
request.open("GET", url);
request.responseType = "arraybuffer";
request.setRequestHeader("Range", "bytes=" + index + "-" + (index + length - 1));
request.addEventListener("load", function () {
callback(request.response);
}, false);
request.addEventListener("error", onerror, false);
request.send();
}
function readUint8Array(index, length, callback, onerror) {
readArrayBuffer(index, length, function (arraybuffer) {
callback(new Uint8Array(arraybuffer));
}, onerror);
}
that.size = 0;
that.init = init;
that.readUint8Array = readUint8Array;
}
HttpRangeReader.prototype = new Reader();
HttpRangeReader.prototype.constructor = HttpRangeReader;
function ArrayBufferReader(arrayBuffer) {
var that = this;
function init(callback, onerror) {
that.size = arrayBuffer.byteLength;
callback();
}
function readUint8Array(index, length, callback, onerror) {
callback(new Uint8Array(arrayBuffer.slice(index, index + length)));
}
that.size = 0;
that.init = init;
that.readUint8Array = readUint8Array;
}
ArrayBufferReader.prototype = new Reader();
ArrayBufferReader.prototype.constructor = ArrayBufferReader;
function ArrayBufferWriter() {
var array, that = this;
function init(callback, onerror) {
array = new Uint8Array();
callback();
}
function writeUint8Array(arr, callback, onerror) {
var tmpArray = new Uint8Array(array.length + arr.length);
tmpArray.set(array);
tmpArray.set(arr, array.length);
array = tmpArray;
callback();
}
function getData(callback) {
callback(array.buffer);
}
that.init = init;
that.writeUint8Array = writeUint8Array;
that.getData = getData;
}
ArrayBufferWriter.prototype = new Writer();
ArrayBufferWriter.prototype.constructor = ArrayBufferWriter;
function FileWriter(fileEntry, contentType) {
var writer, that = this;
function init(callback, onerror) {
fileEntry.createWriter(function (fileWriter) {
writer = fileWriter;
callback();
}, onerror);
}
function writeUint8Array(array, callback, onerror) {
var blob = new Blob([appendABViewSupported ? array : array.buffer], {
type: contentType
});
writer.onwrite = function () {
writer.onwrite = null;
callback();
};
writer.onerror = onerror;
writer.write(blob);
}
function getData(callback) {
fileEntry.file(callback);
}
that.init = init;
that.writeUint8Array = writeUint8Array;
that.getData = getData;
}
FileWriter.prototype = new Writer();
FileWriter.prototype.constructor = FileWriter;
zip.FileWriter = FileWriter;
zip.HttpReader = HttpReader;
zip.HttpRangeReader = HttpRangeReader;
zip.ArrayBufferReader = ArrayBufferReader;
zip.ArrayBufferWriter = ArrayBufferWriter;
if (zip.fs) {
ZipDirectoryEntry = zip.fs.ZipDirectoryEntry;
ZipDirectoryEntry.prototype.addHttpContent = function (name, URL, useRangeHeader) {
function addChild(parent, name, params, directory) {
if (parent.directory) return directory ? new ZipDirectoryEntry(parent.fs, name, params, parent) : new zip.fs.ZipFileEntry(parent.fs, name, params, parent);
else throw "Parent entry is not a directory.";
}
return addChild(this, name, {
data: URL,
Reader: useRangeHeader ? HttpRangeReader : HttpReader
});
};
ZipDirectoryEntry.prototype.importHttpContent = function (URL, useRangeHeader, onend, onerror) {
this.importZip(useRangeHeader ? new HttpRangeReader(URL) : new HttpReader(URL), onend, onerror);
};
zip.fs.FS.prototype.importHttpContent = function (URL, useRangeHeader, onend, onerror) {
this.entries = [];
this.root = new ZipDirectoryEntry(this);
this.root.importHttpContent(URL, useRangeHeader, onend, onerror);
};
}
})();
provide(zip);
});

View 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);
}
};

View 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]-->'];
}
}
});
};

View File

@@ -0,0 +1,4 @@
- y-global
- block: y-design
required: true
- block: y-ua

View File

@@ -0,0 +1,17 @@
# y-page: страница
Используется в качестве контейнера для всех остальных блоков.
Содержимое страницы следует задавать параметром `body` в `bemjson`.
## Варианты представления
| view | Описание
| --------------- | ---------
| `islet` | Дефолтное представление. Содержит глобальные стили для ссылок
## Настройки шаблона
<!--BTJSON_API-->

View 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();
});

View File

@@ -0,0 +1,8 @@
.y-page_islet {
margin: 0;
padding: 0;
background: #F6F5F3;
font-family: $y-design.common['font-family'];
}

View 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);'
]);
});
};

View 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
View 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);
}
});

View 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);
});

View 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();
});

View 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);
});

View File

@@ -0,0 +1,2 @@
- jquery
- y-block

View File

@@ -0,0 +1,5 @@
modules.require(['jquery', 'y-block'], function ($, YBlock) {
$(function () {
YBlock.initDomTree(window.document).done();
});
});

View File

@@ -0,0 +1,5 @@
# y-block__auto-init:
Автоматическая инициализация блоков на странице.
Для того, чтобы воспользоваться этой функциональностью,
необходимо добавить в зависимости элемент `auto-init` блока `y-block`.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
# y-block: базовый блок
Класс `YBlock` — это базовый визуальный блок. Все прочие визуальные блоки должны наследоваться от этого блока с
помощью модуля `inherit`.
<!--JS_API-->

View 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="{&quot;options&quot;:{&quot;answer&quot;: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="{&quot;options&quot;:{}}"></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="{&quot;options&quot;:{&quot;answer&quot;: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="{&quot;options&quot;:{&quot;level&quot;: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="{&quot;options&quot;:{&quot;level&quot;: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();
});

View 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;
};
});
});

View File

@@ -0,0 +1,6 @@
# y-debounce:
Модуль `y-debounce` возвращает функцию `debounce`, которая используется для реализации отложенных действий.
Подробности по клику на функции.
<!--JS_API-->

View 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();
});

View 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)
}
}
}

View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
},
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;
}
}
});
});

View File

@@ -0,0 +1,5 @@
# y-dom: работа с DOM
Модуль `y-dom` возвращает объект `yDom` для работы с DOM со следующими полями и методами:
<!--JS_API-->

View 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);
});

View File

@@ -0,0 +1,7 @@
# y-event-emitter: эмиттер
Предоставляет базовый класс для сущностей, кидающих на себе события.
## API класса
<!--JS_API-->

View 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();
});

View 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);
});

View File

@@ -0,0 +1,5 @@
Адаптер для YEventEmitter, jQuery. Позволяет привязывать обработчики к разным эмиттерам событий
и отвязывать их, используя вызов одной функции. Менеджер всегда привязан к какому-либо объекту, который
является контекстом для всех обработчиков.
<!--JS_API-->

View 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();
});

View 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;
});
});

View 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();
});

View 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);
});

View 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}
*/

View File

@@ -0,0 +1,8 @@
# y-i18n: интернационализация
Модуль `y-i18n` возвращает функцию `i18n`, которая используется для локализации сервисов.
Функция `i18n` в свою очередь имеет ряд методов, для взаимодействия с кейсетами и языками.
Работа данного класса напрямую связана с технологией `y-i18n-lang-js`, которая находится в `islets/.bem/techs`.
<!--JS_API-->

View 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);
});
});

View File

@@ -0,0 +1,5 @@
# y-load-script:
Модуль `y-load-script` возвращает функцию `loadScript`, которая загружает js-файлы добавляя тэг `<script>` в DOM.
<!--JS_API-->

View 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);
});
});

View File

@@ -0,0 +1,5 @@
# y-next-tick:
Вызывает переданную функцию в следующем тике.
<!--JS_API-->

View 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;
};
});
});

View File

@@ -0,0 +1,7 @@
# y-throttle:
Модуль `y-throttle` возвращает функцию `throttle`,
которая ограничивает количество выполненных действий в заданном интервале времени.
Подробности по клику на функции.
<!--JS_API-->

View 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();
});

View 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);
}
});
});

View 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();
});

2168
lib/inflate.js Normal file

File diff suppressed because it is too large Load Diff

438
lib/reader.xsl Normal file
View File

@@ -0,0 +1,438 @@
<?xml version="1.0"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:fb="http://www.gribuser.ru/xml/fictionbook/2.0"
>
<xsl:output
media-type="text/html"
method="html"
encoding="utf-8"
omit-xml-declaration="yes"
doctype-public="HTML5"
/>
<xsl:key name="note-link" match="fb:section" use="@id"/>
<xsl:template match="/*">
<div class="chitalka-fb2_default__book">
<xsl:for-each select="fb:description/fb:title-info/fb:coverpage/fb:image">
<xsl:call-template name="image"/>
</xsl:for-each>
<xsl:for-each select="fb:description/fb:title-info/fb:annotation">
<section class="book__annotation">
<xsl:call-template name="annotation"/>
</section>
</xsl:for-each>
<navMap style="display: none;">
<xsl:apply-templates select="fb:body" mode="toc"/>
</navMap>
<xsl:for-each select="fb:body">
<xsl:if test="position() != 1">
<div class="separator"></div>
</xsl:if>
<!--<xsl:if test="not(fb:title) and @name">-->
<!--<h4 align="center">-->
<!--<xsl:value-of select="@name"/>-->
<!--</h4>-->
<!--</xsl:if>-->
<div class="wrapper">
<xsl:apply-templates/>
</div>
</xsl:for-each>
</div>
</xsl:template>
<!-- author template -->
<xsl:template name="author">
<xsl:value-of select="fb:first-name"/>
<xsl:text disable-output-escaping="no">&#032;</xsl:text>
<xsl:value-of select="fb:middle-name"/>&#032;
<xsl:text disable-output-escaping="no">&#032;</xsl:text>
<xsl:value-of select="fb:last-name"/>
<br/>
</xsl:template>
<!-- secuence template -->
<xsl:template name="sequence">
<xsl:value-of select="@name"/>
<xsl:if test="@number">
<xsl:text disable-output-escaping="no">,&#032;#</xsl:text>
<xsl:value-of select="@number"/>
</xsl:if>
<xsl:if test="fb:sequence">
<ul>
<xsl:for-each select="fb:sequence">
<xsl:call-template name="sequence"/>
</xsl:for-each>
</ul>
</xsl:if>
</xsl:template>
<!-- toc template -->
<xsl:template match="fb:section|fb:body" mode="toc">
<xsl:choose>
<xsl:when test="name()='body' and position()=1 and not(fb:title)">
<xsl:apply-templates select="fb:section" mode="toc"/>
</xsl:when>
<xsl:otherwise>
<navPoint>
<navLabel>
<text>
<xsl:value-of select="normalize-space(fb:title | @name)"/>
</text>
</navLabel>
<content src="#TOC_{generate-id()}"></content>
<!--<xsl:value-of select="normalize-space(fb:title | //fb:body/@name)"/>-->
<!--<xsl:value-of select="normalize-space(fb:body[@name == 'notes']/@name | fb:body/title)"/>-->
<!-- fb:body[@name = 'notes'] -->
<xsl:if test="fb:section">
<navPoint>
<xsl:apply-templates select="fb:section" mode="toc"/>
</navPoint>
</xsl:if>
</navPoint>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- description -->
<xsl:template match="fb:description">
<xsl:apply-templates/>
</xsl:template>
<!-- body -->
<xsl:template match="fb:body">
<div>
<xsl:apply-templates/>
</div>
</xsl:template>
<xsl:template match="fb:section">
<section>
<xsl:attribute name="class">
<xsl:text>chitalka-fb2_default__section</xsl:text>
<xsl:if test="starts-with(translate(fb:title, 'ГЛАВА', 'глава'), 'глава')">
<xsl:text> chapter</xsl:text>
</xsl:if>
</xsl:attribute>
<a name="TOC_{generate-id()}"></a>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</section>
</xsl:template>
<xsl:template match="fb:body[@name='notes']//fb:section">
<div>
<xsl:if test="@id">
<xsl:attribute name="class">
<xsl:value-of select="'annotation'"/>
</xsl:attribute>
</xsl:if>
<a name="TOC_{generate-id()}"></a>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</div>
</xsl:template>
<!-- section/title -->
<xsl:template match="fb:section/fb:title|fb:poem/fb:title">
<xsl:choose>
<xsl:when test="count(ancestor::node()) &lt; 9">
<xsl:element name="{concat('h',count(ancestor::node())-3)}">
<a name="TOC_{generate-id()}"></a>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="h6">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- section/title -->
<xsl:template match="fb:body/fb:title">
<!--<h1 style="display: none;">-->
<!--<xsl:apply-templates mode="title"/>-->
<!--</h1>-->
</xsl:template>
<xsl:template match="fb:body[@name='notes']/fb:title">
<h1>
<xsl:apply-templates mode="title"/>
</h1>
</xsl:template>
<xsl:template match="fb:title/fb:p">
<xsl:apply-templates/>
<xsl:text disable-output-escaping="no">&#032;</xsl:text>
<br/>
</xsl:template>
<!-- subtitle -->
<xsl:template match="fb:subtitle">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<h5>
<xsl:apply-templates/>
</h5>
</xsl:template>
<!-- p -->
<xsl:template match="fb:p">
<!-- https://st.yandex-team.ru/CHITALKA-85 -->
<!--<xsl:if test"preceding-sibling::*[1][name()] != 'fb:image'">-->
<p>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</p>
<!--</xsl:if>-->
</xsl:template>
<!-- strong -->
<xsl:template match="fb:strong">
<b>
<xsl:apply-templates/>
</b>
</xsl:template>
<!-- emphasis -->
<xsl:template match="fb:emphasis">
<i>
<xsl:apply-templates/>
</i>
</xsl:template>
<!-- style -->
<xsl:template match="fb:style">
<span class="{@name}">
<xsl:apply-templates/>
</span>
</xsl:template>
<!-- empty-line -->
<xsl:template match="fb:empty-line">
<xsl:if test="following-sibling::*[1] != fb:image">
<br/>
</xsl:if>
</xsl:template>
<!-- link -->
<xsl:template match="fb:a">
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')">
<xsl:element name="a">
<xsl:attribute name="class">
<xsl:value-of select="'footnote'"/>
</xsl:attribute>
<xsl:attribute name="href">
<xsl:value-of select="@xlink:href"/>
</xsl:attribute>
<xsl:attribute name="title">
<xsl:value-of select="key('note-link',substring-after(@xlink:href,'#'))/fb:p"/>
</xsl:attribute>
<xsl:choose>
<xsl:when test="(@type) = 'note'">
<sup>
<xsl:apply-templates/>
</sup>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="span">
<xsl:choose>
<xsl:when test="(@type) = 'note'">
<sup>
<xsl:apply-templates/>
</sup>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- annotation -->
<xsl:template name="annotation">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:template>
<!-- epigraph -->
<xsl:template match="fb:epigraph">
<blockquote class="epigraph">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- epigraph/text-author -->
<xsl:template match="fb:epigraph/fb:text-author">
<blockquote class="author">
<i>
<xsl:apply-templates/>
</i>
</blockquote>
</xsl:template>
<!-- cite -->
<xsl:template match="fb:cite">
<blockquote>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- cite/text-author -->
<xsl:template match="fb:text-author">
<blockquote>
<i>
<xsl:apply-templates/>
</i>
</blockquote>
</xsl:template>
<!-- date -->
<xsl:template match="fb:date">
<xsl:choose>
<xsl:when test="not(@value)">
&#160;&#160;&#160;
<xsl:apply-templates/>
<br/>
</xsl:when>
<xsl:otherwise>
&#160;&#160;&#160;<xsl:value-of select="@value"/>
<br/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- poem -->
<xsl:template match="fb:poem">
<blockquote>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- stanza -->
<xsl:template match="fb:stanza">
<xsl:apply-templates/>
<br/>
</xsl:template>
<!-- v -->
<xsl:template match="fb:v">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name">
<xsl:value-of select="@id"/>
</xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
<br/>
</xsl:template>
<!-- image -->
<xsl:template match="fb:image" name="image">
<div>
<xsl:choose>
<xsl:when test="ancestor::fb:coverpage">
<xsl:attribute name="class">
<xsl:value-of select="'book__cover'"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="class">
<xsl:text>image chitalka-fb2_default__image</xsl:text>
</xsl:attribute>
</xsl:otherwise>
</xsl:choose>
<div class="image__wrapper">
<img>
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')">
<xsl:attribute name="src">
<xsl:text>data:</xsl:text>
<xsl:variable name="href" select="substring-after(@xlink:href,'#')"/>
<set variable="href" expression="substring-after(@xlink:href,'#')"/>
<xsl:value-of select="//fb:binary[@id=$href]/@content-type" disable-output-escaping="yes"/><xsl:text>;base64,</xsl:text>
<!--<xsl:value-of select="substring-after(@xlink:href,'#')"/>-->
<xsl:value-of select="//fb:binary[@id=$href]" disable-output-escaping="yes"/>
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="src">
<xsl:value-of select="@xlink:href"/>
</xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</img>
</div>
<xsl:if test="following-sibling::node()[1]/fb:emphasis">
<div class="image__annotation">
<xsl:apply-templates select="following-sibling::fb:p[1]"/>
</div>
</xsl:if>
</div>
</xsl:template>
</xsl:stylesheet>

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "chitalka",
"description": "",
"version": "1.0.0",
"author": "Yandex Maps <mapsui-dev-team.ru>",
"private": true,
"devDependencies": {
"inherit": "2.2.2",
"vow-fs": "0.3.2",
"vow": "0.4.4",
"enb-bevis-helper": "1.1.0",
"stylus": "0.47.1"
},
"enb": {
"dependencies": [
"vow",
"inherit"
],
"sources": [
"client/islets/common",
"client/islets/core",
"client/core"
],
"profiles": {
"index": {
"sources": [
"build/index"
]
}
}
}
}