Release
This commit is contained in:
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);
|
||||
});
|
||||
Reference in New Issue
Block a user