JSFiddle - React, Tailwind, and code Playground
HTML
<script src="http://underscorejs.org/underscore.js"></script>
<script src="http://momentjs.com/downloads/moment-with-locales.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/marked/0.3.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ractive"></script>
<main>
<div style="width:600px" id="comments"></div>
</main>
<script id="comments_template" type="ractive/text">
{{#top_reply}}
<!--Мы показываем форму добавления комментария сверху и снизу. Но когда комментариев нет, только снизу-->
{{#if replys.length > 0}}
<!--Если есть черновик - покажем форму-->
{{#if this.reply_draft}}
<!--это развернется в темплейт который находится между блоками comment_form - темплейт формы введения комментария.-->
{{>comment_form}}
{{else}}
<!--Если нет, то покажем кнопку 'Добавить комментарий' которая раскроет форму -->
<!--По нажатию выстрелит событие reply и e.context = this; this = top_reply -->
<button class="add_comment" on-click="reply">Добавить комментарий</button>
{{/if}}
{{/if}}
{{/top_reply}}
<!--Для каждого комментария верхнего уровня рендерим partial комментария смотри ниже-->
{{#replys}}
{{>comment}}
{{/replys}}
<!--Нижняя форма ответа-->
{{#bottom_reply}}
{{>comment_form}}
{{/bottom_reply}}
<!-- {{>comment}} -->
<!--Темплейт комментария -->
<!--Важно понять что поскольку этот partial показываем из блока replys - this будет равняться текущему комментарию-->
<article id="{{this.id}}" class="comment" intro="scroll_to:{go:{{this.is_new}}}">
<header>
<!--Тут мы выставляем пользовательский аватар либо дефолтную картинку если у пользователя нет аватара-->
<img class="avatar" src="{{this.user.image || default_avatar_url}}"/>
<span class="author">{{this.user.username}}</span>
<!-- Можно использовать функции. Здесь мы используем moment что бы отформатировать время. Ссылку на функцию мы...
CSS
.fotorama__arr:focus:after,.fotorama__fullscreen-icon:focus:after,.fotorama__html,.fotorama__img,.fotorama__nav__frame:focus .fotorama__dot:after,.fotorama__nav__frame:focus .fotorama__thumb:after,.fotorama__stage__frame,.fotorama__stage__shaft,.fotorama__video iframe{position:absolute;width:100%;height:100%;top:0;right:0;left:0;bottom:0}.fotorama--fullscreen,.fotorama__img{max-width:99999px!important;max-height:99999px!important;min-width:0!important;min-height:0!important;border-radius:0!important;box-shadow:none!important;padding:0!important}.fotorama__wrap .fotorama__grab{cursor:-webkit-grab;cursor:-o-grab;cursor:-ms-grab;cursor:grab}.fotorama__grabbing *{cursor:-webkit-grabbing;cursor:-o-grabbing;cursor:-ms-grabbing;cursor:grabbing}.fotorama__spinner{position:absolute!important;top:50%!important;left:50%!important}.fotorama__wrap--css3 .fotorama__arr,.fotorama__wrap--css3 .fotorama__fullscreen-icon,.fotorama__wrap--css3 .fotorama__nav__shaft,.fotorama__wrap--css3 .fotorama__stage__shaft,.fotorama__wrap--css3 .fotorama__thumb-border,.fotorama__wrap--css3 .fotorama__video-play{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.fotorama__caption,.fotorama__nav:after,.fotorama__nav:before,.fotorama__stage:after,.fotorama__stage:before,.fotorama__wrap--css3 .fotorama__html,.fotorama__wrap--css3 .fotorama__nav,.fotorama__wrap--css3 .fotorama__spinner,.fotorama__wrap--css3 .fotorama__stage,.fotorama__wrap--css3 .fotorama__stage .fotorama__img,.fotorama__wrap--css3 .fotorama__stage__frame{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.fotorama__arr:focus,.fotorama__fullscreen-icon:focus,.fotorama__nav__frame{outline:0}.fotorama__arr:focus:after,.fotorama__fullscreen-icon:focus:after,.fotorama__nav__frame:focus .fotorama__dot:after,.fotorama__nav__frame:focus .fotorama__thumb:after{content:'';border-radius:inherit;background-color:rgba(0,175,234,.5)}.fotorama__wrap--video .fotorama__stage,.fotorama__wrap--video...
JavaScript
function Comment(id, md_text, user, domain, date, reply_to) {
this.id = id;
this.md_text = md_text;
this.user = user;
this.date = date;
this.reply_to = reply_to ? reply_to.id || reply_to : null;
this.domain = domain;
this.replys = [];
};
Comment.prototype.destroy = function () {
return $('<div></div>')
.promise()
.then(function () {
return {
md_text: '*Комментарий был удален*'
}
});
};
Comment.prototype.save = function () {
var self = this;
return $('<div></div>')
.promise()
.then(function (rep) {
self.id = Math.random();
self.is_new = true;
return self;
});
};
Comment.fetch = function (domain, options) {
return $('<div></div>')
.promise()
.then(function (res) {
return [];
});
};
var Comments = Ractive.extend({
//$(selector) Элемента куда Ractive будет рендарить этот компонент
el: '#comments',
//id tag <script> в котором содержится темлейт для этого компонента ( подробней в разделе темплейтов)
template: '#comments_template',
init : function (options) {
var self = this;
//Домен куда сохранять на сервере коменты.
var domain = options.comments_domain;
//Текущий пользователь.
var current_user = options.current_user;
if (!current_user) {
throw new Error('options.domain and options.current_user is required');
}
//И так для начала качнем комменты с сервера.
Comment
.fetch(domain)
.then(function (comments) {
//Разложим коменты по айдишникам что бы потом меньше бегать по списку.
var comments_by_id = _.indexBy(comments, 'id');
//Сортируем по дате
comments = _.sortBy(comments, 'date');
//Так как коменты имеет древовидную форму
//Раскладываем ответы в массив replys отцовского комента, а так же оставляем в массиве только комменты верхненго уровня.
comments = _.filter(comments, function (comment) {
if (comment.reply_to && comments_by_id[comment.reply_to]) {
var parent =...