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