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