PageRenderTime 41ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/public/javascripts/mmblog/index.js

https://github.com/coocon/mmblog
JavaScript | 546 lines | 438 code | 85 blank | 23 comment | 44 complexity | d31d19d3147338c11e0596f9bc7fd43d MD5 | raw file
  1. (function() {
  2. /**
  3. * template
  4. */
  5. var RecentListTemplate = '<h4 class="side-title">最新文章</h4>\
  6. <ul class="unstyled recent-list">\
  7. <% _.each(obj, function(item, i) { %>\
  8. <% if (i == 0) { %>\
  9. <li class="recent-item recent-first">\
  10. <% } else { %>\
  11. <li class="recent-item">\
  12. <% } %>\
  13. <a href="#article/<%= item._id %>" class="recent-title"><%= item.title %></a>\
  14. </li>\
  15. <% }); %>\
  16. </ul>';
  17. var ArticleListTemplate = '<section class="article-list">\
  18. <% _.each(obj, function(item) { %>\
  19. <article class="article">\
  20. <% var d = _.templateHelper.formatDate(item.createTime); %>\
  21. <header class="article-header clearfix">\
  22. <time datatime="<%= item.createTime.year %>-<%= item.createTime.month %>-<%= item.createTime.day %>" class="article-date pull-left">\
  23. <span class="article-month"><%= d.month %></span>\
  24. <span class="article-day"><%= d.day %><br><%= d.year %></span>\
  25. </time>\
  26. <h3 class="article-title">\
  27. <a href="#article/<%= item._id %>" title="<%= item.title %>">\
  28. <%= item.title %>\
  29. </a>\
  30. </h3>\
  31. </header>\
  32. <div class="article-content">\
  33. <%= item.content %>\
  34. </div>\
  35. <aside class="article-footer clearfix">\
  36. <a class="pull-left article-footer-commentCount" href="#article/<%= item._id %>" title="<%= item.commentCount %>条评论">\
  37. <i class="icon-comment"></i>\
  38. <span><%= item.commentCount %></span>\
  39. </a>\
  40. <a href="#article/<%= item._id %>" class="pull-right article-more" title="点击阅读更多">点击阅读更多</a>\
  41. </aside>\
  42. </article>\
  43. <% }); %>\
  44. </section>';
  45. var ArticleTemplate = '<article class="article">\
  46. <h3>\
  47. <%= title %>\
  48. </h3>\
  49. <div class="article-content">\
  50. <%= content %>\
  51. </div>\
  52. <section class="article-comment" data-article="<%= _id %>">\
  53. </section>\
  54. <aside class="article-footer">\
  55. <ul class="pager">\
  56. <% if (previous) { %>\
  57. <li class="previous">\
  58. <a href="#article/<%= previous %>">\
  59. <i class="icon-arrow-left"></i>\
  60. 上一篇\
  61. </a>\
  62. <% } else { %>\
  63. <li class="previous disabled">\
  64. <a href="javascript:;">\
  65. <i class="icon-arrow-left"></i>\
  66. 上一篇\
  67. </a>\
  68. <% } %>\
  69. </li>\
  70. <% if (next) { %>\
  71. <li class="next">\
  72. <a href="#article/<%= next %>">\
  73. 下一篇\
  74. <i class="icon-arrow-right"></i>\
  75. </a>\
  76. <% } else { %>\
  77. <li class="next disabled">\
  78. <a href="javascript:;">\
  79. 下一篇\
  80. <i class="icon-arrow-right"></i>\
  81. </a>\
  82. <% } %>\
  83. </li>\
  84. </ul>\
  85. </aside>\
  86. </article>';
  87. var CommentTemplate = '<h4><span class="article-comment-count"><%= commentsLength %></span> 条评论</h4>\
  88. <div class="article-comment-list">\
  89. </div>\
  90. <div class="article-comment-editor">\
  91. <form class="form-inline article-comment-form" onsubmit="return false">\
  92. <div class="alert alert-error article-comment-error">\
  93. <button type="button" class="close" data-dismiss="alert">×</button>\
  94. <p class="alert-text"></p>\
  95. </div>\
  96. <textarea class="article-comment-textarea" required="required" placeholder="评论..."></textarea>\
  97. <i class="icon-envelope-alt icon-large"></i>\
  98. <input type="email" name="email" class="article-comment-email input-medium" required="required" placeholder="邮箱(必填)" autocomplete="on" />\
  99. <i class="icon-user icon-large"></i>\
  100. <input type="text" name="name" class="article-comment-name input-medium" required="required" placeholder="姓名(必填)" autocomplete="on" />\
  101. <i class="icon-home icon-large"></i>\
  102. <input type="url" name="url" class="article-comment-home input-medium" placeholder="主页" />\
  103. <input type="hidden" class="article-comment-articleId" value="<%= articleId %>"/>\
  104. <button class="article-comment-reply btn disabled pull-right" disabled="disabled">回复</button>\
  105. </form>\
  106. </div>';
  107. var CommentItemTemplate = '<article class="article-comment-item">\
  108. <div class="comment-avatar"><img width="40" height="40" src="<%= avatar %>"/></div>\
  109. <div class="comment-item-head">\
  110. <% if (home) { %>\
  111. <a class="comment-item-name" title="<%= name %>" href="<%= home %>" target="_black"><%= name %></a>\
  112. <% } else { %>\
  113. <span class="comment-item-name" title="<%= name %>"><%= name %></span>\
  114. <% } %>\
  115. <span class="comment-item-bullet" aria-hidden="true"></span>\
  116. <time datatime="<%= createTime.year %>-<%= createTime.month %>-<%= createTime.day %>" class="comment-item-time"><%= _.templateHelper.diffDate(createTime) %></time>\
  117. </div>\
  118. <div class="comment-item-body">\
  119. <p class="comment-item-content"><%= content %></p>\
  120. </div>\
  121. </article>';
  122. _.templateHelper = {
  123. months: ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'],
  124. formatDate: function(date) {
  125. return {
  126. 'year': date.year,
  127. 'month': this.months[date.month],
  128. 'day': date.day
  129. };
  130. },
  131. diffDate: function(date) {
  132. var now = new Date(),
  133. date = new Date(date.year, date.month, date.day, date.hours, date.minutes, date.seconds),
  134. diff = null,
  135. base = Math.abs(date - now) / 1000;
  136. if ((diff = base / 60 / 60 /24) && diff >= 1) {
  137. return parseInt(diff) + '天前';
  138. } else if ((diff = base / 60 / 60) && diff >= 1) {
  139. return parseInt(diff) + '小时前';
  140. } else if ((diff = base / 60) && diff >= 1) {
  141. return parseInt(diff) + '分钟前';
  142. } else if ((diff = base) && diff >= 1){
  143. return parseInt(diff) + '秒前';
  144. } else {
  145. return '刚刚';
  146. }
  147. }
  148. };
  149. //验证
  150. var validation = {
  151. email: function(str) {
  152. return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(str);
  153. },
  154. url: function(str) {
  155. return /^((https|http|ftp|rtsp|mms)?:\/\/)+[A-Za-z0-9]+\.[A-Za-z0-9]+[\/=\?%\-&_~`@[\]\':+!]*([^<>\"\"])*$/.test(str);
  156. }
  157. };
  158. var prettify = function(dom) {
  159. var dom = dom || $(document),
  160. preEls = dom.find('pre');
  161. _.each(preEls, function(el){
  162. $(el).addClass('prettyprint').addClass('linenums');
  163. });
  164. prettyPrint();
  165. };
  166. /**
  167. * model
  168. */
  169. var Article = Backbone.Model.extend({
  170. urlRoot: '/article/'
  171. });
  172. var Recent = Backbone.Model.extend({
  173. });
  174. var Comment = Backbone.Model.extend({
  175. urlRoot: '/comment/'
  176. });
  177. /**
  178. * collection
  179. */
  180. var ArticleList = Backbone.Collection.extend({
  181. model: Article,
  182. url: '/article/'
  183. });
  184. var RecentList = Backbone.Collection.extend({
  185. model: Recent,
  186. url: 'article/recent'
  187. });
  188. var CommentList = Backbone.Collection.extend({
  189. model: Comment,
  190. url: '/comment/'
  191. });
  192. /**
  193. * view
  194. */
  195. var BaseView = Backbone.View.extend({
  196. elements: {
  197. },
  198. refreshElement: function() {
  199. for (var key in this.elements) {
  200. this[this.elements[key]] = this.$(key);
  201. }
  202. },
  203. destroy: function() {
  204. this.remove();
  205. if (!this.model) {
  206. return;
  207. }
  208. //collection
  209. if (this.model.models) {
  210. var model;
  211. while ((model = this.model.models.pop())) {
  212. model.destroy();
  213. }
  214. this.model = null;
  215. //model
  216. } else {
  217. this.model.destroy();
  218. this.model = null;
  219. }
  220. }
  221. });
  222. var ArticleListView = BaseView.extend({
  223. template: _.template(ArticleListTemplate),
  224. el: $('#main'),
  225. events: {
  226. },
  227. initialize: function() {
  228. this.model = new ArticleList;
  229. this.listenTo(this.model, 'reset', this.render);
  230. this.model.fetch();
  231. },
  232. render: function() {
  233. this.$el.empty();
  234. this.$el.append(this.template(this.model.toJSON()));
  235. //语法着色
  236. prettify();
  237. },
  238. remove: function() {
  239. this.$el.empty();
  240. this.stopListening();
  241. return this;
  242. }
  243. });
  244. var RecentListView = BaseView.extend({
  245. template: _.template(RecentListTemplate),
  246. el: $('#recent'),
  247. initialize: function() {
  248. this.model = new RecentList;
  249. this.listenTo(this.model, 'reset', this.render);
  250. this.model.fetch();
  251. },
  252. render: function() {
  253. this.$el.addClass('active');
  254. this.$el.append(this.template(this.model.toJSON()));
  255. },
  256. remove: function() {
  257. this.$el.removeClass('active');
  258. this.$el.empty();
  259. this.stopListening();
  260. return this;
  261. }
  262. });
  263. var ArticleView = BaseView.extend({
  264. template: _.template(ArticleTemplate),
  265. elements: {
  266. '.article-comment': 'commentEl'
  267. },
  268. el: $('#main'),
  269. initialize: function() {
  270. this.listenTo(this.model, 'change', this.render);
  271. this.model.fetch();
  272. },
  273. render: function() {
  274. this.$el.empty();
  275. this.$el.append(this.template(this.model.toJSON()));
  276. this.refreshElement();
  277. this.initComment();
  278. //语法着色
  279. prettify();
  280. },
  281. //初始化评论功能。
  282. initComment: function() {
  283. this.commentView = new CommentView({
  284. el: this.commentEl
  285. });
  286. },
  287. remove: function() {
  288. this.$el.empty();
  289. this.stopListening();
  290. return this;
  291. },
  292. destroy: function() {
  293. this.remove();
  294. this.model.destroy();
  295. this.commentView.destroy();
  296. }
  297. });
  298. var CommentView = BaseView.extend({
  299. template: _.template(CommentTemplate),
  300. itemTemplate: _.template(CommentItemTemplate),
  301. events: {
  302. 'focus .article-comment-textarea': 'extendTextarea',
  303. 'blur .article-comment-textarea': 'retractTextarea',
  304. 'keyup .article-comment-textarea': 'changeReplyBtnStatus',
  305. 'keyup .article-comment-email': 'changeReplyBtnStatus',
  306. 'keyup .article-comment-name': 'changeReplyBtnStatus',
  307. 'click .article-comment-reply': 'save',
  308. 'click .article-comment-error .close': 'hideError'
  309. },
  310. elements: {
  311. '.article-comment-count': 'commentCountEl',
  312. '.article-comment-editor': 'commentEditorEl',
  313. '.article-comment-error': 'commentErrorEl',
  314. '.article-comment-textarea': 'commentTextareaEl',
  315. '.article-comment-email': 'commentEmailEl',
  316. '.article-comment-name': 'commentNameEl',
  317. '.article-comment-home': 'commentHomeEl',
  318. '.article-comment-articleId': 'commentArticleIdEl',
  319. '.article-comment-reply': 'commentBtnReplyEl',
  320. '.article-comment-list': 'commentListEl'
  321. },
  322. initialize: function() {
  323. this.model = new CommentList;
  324. this.listenTo(this.model, 'reset', this.render);
  325. this.listenTo(this.model, 'add', this.commentAdded);
  326. this.model.fetch({
  327. 'url': this.model.url + this.$el.attr('data-article')
  328. });
  329. },
  330. render: function() {
  331. this.$el.append(this.template({
  332. 'articleId': this.$el.attr('data-article'),
  333. 'commentsLength': this.model.length
  334. }));
  335. this.refreshElement();
  336. this.appendComments();
  337. },
  338. appendComments: function() {
  339. _.each(this.model.models, _.bind(function(model, index) {
  340. this.appendOne(model);
  341. }, this));
  342. },
  343. appendOne: function(model) {
  344. this.commentListEl.append(this.itemTemplate(model.toJSON()));
  345. },
  346. extendTextarea: function() {
  347. if (this.commentEditorEl.hasClass('extend')) return;
  348. this.commentEditorEl.addClass('extend');
  349. },
  350. retractTextarea: function() {
  351. if (!_.isEmpty(this.commentTextareaEl.val())) return;
  352. this.commentEditorEl.removeClass('extend');
  353. },
  354. changeReplyBtnStatus: function() {
  355. var commentBtnReply = this.commentBtnReplyEl,
  356. commentTextareaStr = this.commentTextareaEl.val(),
  357. commentEmailStr = this.commentEmailEl.val(),
  358. commentNameStr = this.commentNameEl.val(),
  359. isEmpty = _.isEmpty(commentTextareaStr) || _.isEmpty(commentEmailStr) || _.isEmpty(commentNameStr);
  360. if (isEmpty) {
  361. commentBtnReply.addClass('disabled').removeClass('btn-primary').attr('disabled', 'disabled');
  362. } else {
  363. commentBtnReply.addClass('btn-primary').removeClass('disabled').removeAttr('disabled');
  364. }
  365. },
  366. validForm: function(content, email, name, home) {
  367. if (_.isEmpty(content) || _.isEmpty(email) || _.isEmpty(name)) {
  368. return false;
  369. } else if (!validation.email(email)) {
  370. this.alertError('请输入格式正确的<strong>电子邮件</strong>地址');
  371. return false;
  372. } else if (!_.isEmpty(home) && !validation.url(home)) {
  373. this.alertError('请输入格式正确的<strong>个人主页</strong>地址');
  374. return false;
  375. } else {
  376. return true;
  377. }
  378. },
  379. alertError: function(info) {
  380. this.commentErrorEl.addClass('comment-alert-active');
  381. this.commentErrorEl.find('.alert-text').html(info);
  382. },
  383. hideError: function() {
  384. this.commentErrorEl.removeClass('comment-alert-active');
  385. this.commentErrorEl.find('.alert-text').html('');
  386. },
  387. save: function() {
  388. var content = this.commentTextareaEl.val(),
  389. email = this.commentEmailEl.val(),
  390. name = this.commentNameEl.val(),
  391. home = this.commentHomeEl.val(),
  392. articleId = this.commentArticleIdEl.val(),
  393. comment;
  394. if (!this.validForm(content, email, name, home)) {
  395. return;
  396. }
  397. comment = new Comment({
  398. 'articleId': articleId,
  399. 'content': content,
  400. 'email': email,
  401. 'name': name,
  402. 'home': home
  403. });
  404. this.clearEditor();
  405. this.model.create(comment, {'wait': true});
  406. //阻止表单提交
  407. return false;
  408. },
  409. commentAdded: function(model) {
  410. this.appendOne(model);
  411. this.refreshCommentCount();
  412. },
  413. refreshCommentCount: function() {
  414. this.commentCountEl.html(this.model.length);
  415. },
  416. clearEditor: function() {
  417. this.commentTextareaEl.val('');
  418. this.changeReplyBtnStatus();
  419. this.hideError();
  420. this.commentEditorEl.removeClass('extend');
  421. }
  422. });
  423. /**
  424. * Router
  425. */
  426. var AppRouter = Backbone.Router.extend({
  427. views: [],
  428. 'routes': {
  429. '': 'list',
  430. 'article/:id': 'article'
  431. },
  432. list: function() {
  433. this.destroy();
  434. this.views.push(new ArticleListView);
  435. this.views.push(new RecentListView);
  436. },
  437. article: function(id) {
  438. this.destroy();
  439. this.views.push(new ArticleView({
  440. model: new Article({id:id})
  441. }));
  442. },
  443. loading: function() {
  444. var loadingEl = $('<i class="icon-spinner icon-spin icon-4x icon-loading"></i>').hide();
  445. $('#main').append(loadingEl);
  446. //0.5秒之后再添加loading。
  447. setTimeout(function() {
  448. loadingEl.show();
  449. }, 500);
  450. },
  451. destroy: function() {
  452. var view;
  453. while ((view = this.views.pop())) {
  454. view.destroy();
  455. }
  456. this.loading();
  457. }
  458. });
  459. new AppRouter;
  460. Backbone.history.start();
  461. })();