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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

View File

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

After

Width:  |  Height:  |  Size: 494 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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