/* --------------------------------------------------------------------------------- file-start: js/jquery/jquery.comments.js */ /** * @author dmitry.petrov@sup.com (Dmitry Petrov) * @fileoverview Utility functions to navigate through comments tree in s1 */ (function($) { /** * @name $.comments * @namespace Utility functions to get certain comments on the page. * Comment nodes are the ones that have b-leaf class. Twigs are their * direct parents. */ $.comments = $.comments || { /** * @type Boolean */ options: { selectors: { leaf: '.b-leaf', levelTwig: '.b-tree-twig-{level}', twig: '.b-tree-twig' }, classNames: { levelTwig: 'b-tree-twig-{level}' } }, /** * If this flag is false, the comments animation is diabled. * @type Boolean */ skipAnimation: (jQuery.browser.msie && (+jQuery.browser.version) <= 8) || LJ.Support.isMobile() || false, /** * This flag is used to change some hotkeys according to the mac os system specific reality. * @type Boolean */ isMac: !!(navigator.appVersion.match(/mac/i)), _selector: function(name) { return this.options.selectors[name]; }, _className: function(name) { return this.options.classNames[name]; } }; var __sel = $.comments._selector.bind($.comments), __class = $.comments._className.bind($.comments); /** @lends $.comments */ $.extend($.comments, { /** * Get commentLevel * * @param {jQuery} node Leaf or twig node * @return {Number} Comment's level. */ level: function(node) { var twig = (node.is(__sel('twig'))) ? node : node.closest(__sel('twig')), match = RegExp(__class('levelTwig').supplant({ level: '(\\d+)'})) .exec(twig.prop('className')); return match && parseInt(match[1], 10) || 1; }, /** * Finds the specific parent of the comment * @param {jQuery} node Comment node. * @param {String=} ditemid HTML id of comment to find. If not defined, function * will search for direct ancestor. * * @return {jQuery|false} Returns comment node or false if such parent was not found. */ parent: function(node, ditemid) { var twig = node.closest(__sel('twig')), level = $.comments.level(twig), prev = twig; while (level > 1) { level--; prev = prev.prevAll(__sel('levelTwig').supplant({ level: level}) + ':first'); if (prev.length === 0) { return false; } if (!ditemid || prev.data('tid') === ditemid) { return prev.find(__sel('leaf')); } } return false; }, /** * Check if comment has children. * * @param {jQuery} node Leaf or twig node * @return {Boolean} True if coment has child comments. */ hasChildren: function(node) { var twig = (node.is(__sel('twig'))) ? node : node.closest(__sel('twig')), level = $.comments.level(twig); return $.comments.level(twig.next()) > level; }, /** * Check if on comment is child of another * * @param {jQuery} child Child comment or twig. * @param {jQuery} parent Parent comment or twig. * * @return {Boolean} */ isChild: function(child, parent) { var childTwig = (child.is(__sel('twig'))) ? child : child.closest(__sel('twig')), parentTwig = (parent.is(__sel('twig'))) ? parent : parent.closest(__sel('twig')), parentLevel = $.comments.level(parent), childLevel = $.comments.level(child); if (childLevel === 1 || childLevel <= parentLevel) { return false; } //check obvious cases var realParent = childTwig.prevAll(__sel('levelTwig').supplant({ level: parentLevel }) + ':first'); return realParent.get(0) === parentTwig.get(0); }, /** * Get all comments in the thread including the param comment/ * * @param {jQuery} node Root node. * @return {jQuery} jQuery collection containing all leaves of the thread. */ getThread: function(node, raw) { var twig = node.closest(__sel('twig')), level = $.comments.level(twig), children = raw? [] : jQuery(), next = twig, nextLevel; if (raw) { children.push(twig.find(__sel('leaf'))); } else { children = children.add(twig.find(__sel('leaf'))); } while(true) { next = next.next(); if (next.length === 0) { break; } nextLevel = $.comments.level(next); if (nextLevel <= level) { break; } if (raw) { children.push(next.find(__sel('leaf'))); } else { children = children.add(next.find(__sel('leaf'))); } } return children; } }); })(jQuery); ; /* file-end: js/jquery/jquery.comments.js ----------------------------------------------------------------------------------*/ /* --------------------------------------------------------------------------------- file-start: js/jquery/jquery.lj.commentsPager.js */ /* --------------------------------------------------------------------------------- file-start: js/jquery/jquery.hotkeys.js */ /* * jQuery Hotkeys Plugin * Copyright 2010, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * * Based upon the plugin by Tzury Bar Yochay: * http://github.com/tzuryby/hotkeys * * Original idea by: * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ */ (function(jQuery){ jQuery.hotkeys = { version: "0.8", specialKeys: { 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 187: "+", 189: "-", 191: "/", 224: "meta" }, shiftNums: { "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", ".": ">", "/": "?", "\\": "|" } }; function keyHandler( handleObj ) { // Only care when a possible input has been specified if ( typeof handleObj.data !== "string" ) { return; } var origHandler = handleObj.handler, keys = handleObj.data.toLowerCase().split(" "); handleObj.handler = function( event ) { // Don't fire in text-accepting inputs that we didn't directly bind to if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || /text|password|search|tel|url|email|number/.test( event.target.type ) ) ) { return; } // Keypress represents characters, not special keys var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], character = String.fromCharCode( event.which ).toLowerCase(), key, modif = "", possible = {}; // check combinations (alt|ctrl|shift+anything) if ( event.altKey && special !== "alt" ) { modif += "alt+"; } if ( event.ctrlKey && special !== "ctrl" ) { modif += "ctrl+"; } // TODO: Need to make sure this works consistently across platforms if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { modif += "meta+"; } if ( event.shiftKey && special !== "shift" ) { modif += "shift+"; } if ( special ) { possible[ modif + special ] = true; } else { possible[ modif + character ] = true; possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" if ( modif === "shift+" ) { possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; } } for ( var i = 0, l = keys.length; i < l; i++ ) { if ( possible[ keys[i] ] ) { return origHandler.apply( this, arguments ); } } }; } jQuery.each([ "keydown", "keyup", "keypress" ], function() { jQuery.event.special[ this ] = { add: keyHandler }; }); })( jQuery ); ; /* file-end: js/jquery/jquery.hotkeys.js ----------------------------------------------------------------------------------*/ /** * @author dmitry.petrov@sup.com (Dmitry Petrov) * @fileoverview LiveJournal pager for comments page. */ /** * @name $.lj.commentsPager * @requires $.ui.core, $.ui.widget, $.lj.basicWidget * @class Widget pager on the comments page. It handles ctrl + arrows * or alt + arrow (for mac) hotkeys to navigate between comments pages. */ (function ($) { "use strict"; // external variable that allows us detect master widget (that perform actions) var bound = false; /** @lends $.lj.commentsPager.prototype */ $.widget('lj.commentsPager', jQuery.lj.basicWidget, { options: { classNames: { first: 'b-pager-first', last: 'b-pager-last', mac: 'b-pager-mac', active: 'b-pager-page-active' }, selectors: { page: '.b-pager-page', prev: '.b-pager-prev a', next: '.b-pager-next a', active: '.b-pager-page-active' } }, _create: function() { $.lj.basicWidget.prototype._create.apply(this); var selectors = this.options.selectors, el = this.element; // scroll top coordinate of the window (for down scroll detection) this._scrollTop = null; // jQuery element of bottom paginator this._bottomPager = null; // cache object, contains cached pages, to prevent double sending of // ajax request when scroll takes place this._cachePages = {}; // timer for staying on the page // Used for prevent double firing of cache event on scroll (that is fired often) this._cachePagesTime = 100; this._links = { prev: el.find(selectors.prev), next: el.find(selectors.next) }; if ($.comments.isMac) { this.element.addClass( this._cl('mac') ); } this._ajaxLoader = !!(LJ.get('ajaxPagination') && LJ.Support.history); this._currentPage = parseInt(el.find(selectors.active).text(), 10) || 1; this._totalPages = this._el('page').length; this._master = false; // this is a workaround for Chrome, as it fires popstate on first load, // if this flag is 0, then we should not execute popstate callback this._pushStateCount = /webkit/i.test(navigator.userAgent)? 0 : 1; this._bindControls(); }, _bindControls: function() { $.lj.basicWidget.prototype._bindControls.apply(this); var self = this, modifier = ($.comments.isMac) ? 'alt' : 'ctrl', lt = LJ.Function.threshold(this._switch, 500); // detect master and bind actions for it if (!bound) { jQuery(document) .bind('keydown', modifier + '+left', lt.bind(this, 'prev')) .bind('keydown', modifier + '+right', lt.bind(this, 'next')); if (this._ajaxLoader) { window.onpopstate = function () { var page = 1; if (location.href.match(/page=(\d+)/)) { page = Number(RegExp.$1); } if (self._currentPage !== page && self._pushStateCount > 0 ) { self._currentPage = page; self._fire('commentsPage', [page], true); } }; $(window).on('scroll', LJ.Function.throttle(this._scroll.bind(this), 100)); } bound = true; this._master = true; } if (this._ajaxLoader) { this.element .on('click', this._s('page'), this.loadPage.bind(this)) .on('click', this._s('next'), this.next.bind(this)) .on('click', this._s('prev'), this.prev.bind(this)); this._on('commentsPage', function (page) { this._updatePager(page); }.bind(this)); } }, /** * Correct page number: if less than 1: 1 * if great than max: max * @param {Number} num Page number to correct * @return {Number} Corrected page number */ _correctPageNumber: function (num) { if (num < 1) { return 1; } else if (num > this._totalPages) { return this._totalPages; } else { return num; } }, _scroll: function () { var prevScrollTop, that = this; // works only for master pager if (!this._master) { return; } prevScrollTop = this._scrollTop; this._scrollTop = $(window).scrollTop(); // we should react only scrolling from top to bottom if (this._scrollTop <= prevScrollTop) { return; } // cache bottom pager this._bottomPager = this._bottomPager || $('.b-pager').last(); // if bottom pager is on the screen: preload previous and next pages if (this._bottomPager.is(':screenable') && !this._cachePages[ this._currentPage ]) { this._cachePages[ this._currentPage ] = true; // fire cache event once per <_cachePagesTime> seconds setTimeout(function () { that._cachePages[ that._currentPage ] = false; }, that._cachePagesTime * 1000); this._fire('cachePage', [ this._correctPageNumber(this._currentPage + 1) ]); this._fire('cachePage', [ this._correctPageNumber(this._currentPage - 1) ]); } }, /** * Switch page * @param {Number/String} dir Direction: possible values: 'prev', 'next', * @param {Object} ev jQuery event */ _switch: function (dir, ev) { var page, cachePage, that = this; if (this._ajaxLoader) { if (dir !== 'next' && dir !== 'prev') { page = dir; } else { page = this._currentPage + (dir === 'next' ? 1 : -1); } page = this._correctPageNumber(page); if (page !== this._currentPage) { this._fire('commentsPage', [ page ], true); } } else { var link = this._links[dir].prop('href'); document.location = link; } if (ev && ev.preventDefault) { ev.preventDefault(); } }, _updatePager: function (page) { var newlink; this._el('page') .removeClass(this._cl('active')) .eq(page - 1) .addClass(this._cl('active')); this.element .toggleClass(this._cl('first'), page === 1) .toggleClass(this._cl('last'), page === this._totalPages); if (this._ajaxLoader && this._master) { newlink = location.href.split('#')[0] .replace(/\&?(page|view)=\d+/g, ''); newlink = LiveJournal.constructUrl(newlink, page > 1 ? { page: page } : null) .replace('?&', '?'); if (page !== this._currentPage) { this._pushStateCount++; window.history.pushState(null, '', newlink); } } /* Rewrite prev and next links */ if (this._ajaxLoader) { newlink = location.href.split('#')[0].replace(/\&?(page|view)=\d+/g, ''); this._el('prev').attr('href', LiveJournal.constructUrl(newlink, page < 3? null : { page: page - 1 })); this._el('next').attr('href', LiveJournal.constructUrl(newlink, { page: page + 1 })); } this._currentPage = page; }, loadPage: function (ev) { var page = Number(jQuery(ev.currentTarget).text()); this._switch(page, ev); }, /** * Returns the number of the current comment page. * * @return {number} */ page: function () { return this._currentPage; }, /** * Redirect to the next page of comments. */ next: function (ev) { this._switch('next', ev); }, /** * Redirect to the previous. */ prev: function(ev) { this._switch('prev', ev); } }); }(jQuery)); ; /* file-end: js/jquery/jquery.lj.commentsPager.js ----------------------------------------------------------------------------------*/ /* --------------------------------------------------------------------------------- file-start: js/jquery/jquery.lj.comments.js */ LJ.UI.registerTemplate('templates-Comments-Twig', "
{{if $data.html}} {{html $data.html}} {{else}} {{if $data.more}}
{{each ($value.actions || $data.actions)}} {{html ($value.title || $data.title)}} {{if ($value.ljusers || $data.ljusers)}} {{html LJ.mltext(\'talk.from\')}} {{each ($value.ljusers || $data.ljusers)}}{{if !(($value.anonymous || $data.anonymous))}}{{if ($value.legacy || $data.legacy)}}{{html ($value.ljuser || $data.ljuser)}}{{else}}{{if ($value.inline_css || $data.inline_css)}}{{if ($value.bold || $data.bold)}}{{/if}}{{html ($value.journal || $data.journal)}}{{if ($value.alias || $data.alias)}}*{{/if}}{{if ($value.bold || $data.bold)}}{{/if}}{{else}}{{if ($value.bold || $data.bold)}}{{/if}}{{html ($value.journal || $data.journal)}}{{if ($value.bold || $data.bold)}}{{/if}}{{/if}}{{if ($value.alias || $data.alias) && ($value.side_alias || $data.side_alias)}}{{html ($value.user_alias || $data.user_alias)}}{{/if}}{{/if}}{{else}}{{html LJ.mltext(\'talk.anonuser\')}}{{/if}}{{if !(($index === ljusers.length - 1))}}, {{/if}}{{/each}}{{if ($value.moreusers || $data.moreusers)}}…{{/if}} {{/if}} {{html LJ.mltext(\'talk.expandlink\')}} {{/each}}
{{else}} {{if $data.deleted || !$data.shown}}

{{if $data.leafclass == \'deleted\'}} {{html LJ.mltext(\'talk.deletedpost\')}} {{else $data.leafclass == \'screened\'}} {{html LJ.mltext(\'talk.screenedpost\')}} {{else $data.leafclass == \'spammed\'}} {{html LJ.mltext(\'talk.spammedpost\')}} {{else $data.leafclass == \'suspended\'}} {{html LJ.mltext(\'talk.suspendedpost\')}} {{/if}}

{{if $data.controls}} {{/if}} {{if $data.actions}}
    {{each ($value.actions || $data.actions)}} {{if !(($value.footer || $data.footer))}} {{if ($value.allowed || $data.allowed)}} {{if !($value.checkbox || $data.checkbox) || ($value.massactions || $data.massactions)}}
  • {{if ($value.disabled || $data.disabled)}} {{html ($value.title || $data.title)}} {{else}} {{if ($value.checkbox || $data.checkbox)}} {{else}} {{html ($value.title || $data.title)}} {{if ($value.ljusers || $data.ljusers)}} {{html ($value.ljusers || $data.ljusers)}}{{if ($value.moreusers || $data.moreusers)}}, ...{{/if}}{{/if}} {{/if}} {{/if}}
  • {{/if}} {{/if}} {{/if}} {{/each}}
{{/if}}
{{if $data.actions}}
    {{each ($value.actions || $data.actions)}} {{if ($value.footer || $data.footer)}} {{if ($value.allowed || $data.allowed)}} {{if !($value.checkbox || $data.checkbox) || ($value.massactions || $data.massactions)}}
  • {{if ($value.disabled || $data.disabled)}} {{html ($value.title || $data.title)}} {{else}} {{if ($value.checkbox || $data.checkbox)}} {{else}} {{html ($value.title || $data.title)}} {{if ($value.ljusers || $data.ljusers)}} {{html ($value.ljusers || $data.ljusers)}}{{if ($value.moreusers || $data.moreusers)}}, ...{{/if}}{{/if}} {{/if}} {{/if}}
  • {{/if}} {{/if}} {{/if}} {{/each}}
{{/if}}
{{else}}
{{if $data.userpic}} \"\" {{else}} \"\" {{/if}}
{{if $data.shown}}

{{if $data.username}}{{if $data.deleted_poster}}{{html $data.deleted_poster}}{{else}}{{each ($value.username || $data.username)}}{{if ($value.legacy || $data.legacy)}}{{html ($value.ljuser || $data.ljuser)}}{{else}}{{if ($value.inline_css || $data.inline_css)}}{{if ($value.bold || $data.bold)}}{{/if}}{{html ($value.journal || $data.journal)}}{{if ($value.alias || $data.alias)}}*{{/if}}{{if ($value.bold || $data.bold)}}{{/if}}{{else}}{{if ($value.bold || $data.bold)}}{{/if}}{{html ($value.journal || $data.journal)}}{{if ($value.bold || $data.bold)}}{{/if}}{{/if}}{{if ($value.alias || $data.alias) && ($value.side_alias || $data.side_alias)}}{{html ($value.user_alias || $data.user_alias)}}{{/if}}{{/if}}{{/each}}{{/if}}{{else}}{{html LJ.mltext(\'talk.anonuser\')}}{{/if}} {{if $data.ipaddr}}{{html $data.ipaddr}}{{/if}}

{{if $data.ctime}} {{html $data.ctime}} {{/if}} {{if $data.stime}} {{html $data.stime}} {{/if}} {{if $data.etime}} {{html LJ.mltext(\'talk.edited\')}} {{html $data.etime}} {{/if}}

{{if $data.actions}}
    {{each ($value.actions || $data.actions)}} {{if !(($value.footer || $data.footer))}} {{if ($value.allowed || $data.allowed)}} {{if !($value.checkbox || $data.checkbox) || ($value.massactions || $data.massactions)}}
  • {{if ($value.disabled || $data.disabled)}} {{html ($value.title || $data.title)}} {{else}} {{if ($value.checkbox || $data.checkbox)}} {{else}} {{html ($value.title || $data.title)}} {{if ($value.ljusers || $data.ljusers)}} {{html ($value.ljusers || $data.ljusers)}}{{if ($value.moreusers || $data.moreusers)}}, ...{{/if}}{{/if}} {{/if}} {{/if}}
  • {{/if}} {{/if}} {{/if}} {{/each}}
  • {{html LJ.mltext(\'talk.new\')}}
{{/if}} {{/if}} {{if $data.loaded}} {{if $data.controls}} {{/if}} {{/if}}
{{if $data.article}}
{{if $data.subject}}

{{html $data.subject}}

{{/if}} {{html $data.article}}
{{/if}}
{{if $data.actions}}
    {{each ($value.actions || $data.actions)}} {{if ($value.footer || $data.footer)}} {{if ($value.allowed || $data.allowed)}} {{if !($value.checkbox || $data.checkbox) || ($value.massactions || $data.massactions)}}
  • {{if ($value.disabled || $data.disabled)}} {{html ($value.title || $data.title)}} {{else}} {{if ($value.checkbox || $data.checkbox)}} {{else}} {{html ($value.title || $data.title)}} {{if ($value.ljusers || $data.ljusers)}} {{html ($value.ljusers || $data.ljusers)}}{{if ($value.moreusers || $data.moreusers)}}, ...{{/if}}{{/if}} {{/if}} {{/if}}
  • {{/if}} {{/if}} {{/if}} {{/each}}
  • {{html LJ.mltext(\'talk.new\')}}
{{/if}}
{{/if}} {{/if}} {{/if}}
", 'JQuery.stat'); /** * @author dmitry.petrov@sup.com (Dmitry Petrov) * @fileoverview LiveJournal comments widget. Responsible for updating and expanding/collapsing comments. */ /** * @name $.lj.comments * @requires $.ui.core, $.ui.widget, $.lj.basicWidget * @class Widget represents all the comments on the page. * @extends $.lj.basicWidget */ (function($,window) { 'use strict'; // special cache object var Cacher = (function () { var _stack = [], // cached pages object _pages = {}, // object contains time of add page to cache _times = {}, // time (in seconds) while results of caching are valid // after that, they will be removed from cache _cacheTime = 180, // cache stack size (amount of pages that will be cached at the moment) _cacheSize = 10, // pager widget instance _pagerWidget = null, // comments widget instance _commentsWidget = null; /** * Check if page is cached. Also remove frome cache, if data is not up to date * @param {Number} page Page to check * @return {Boolean} Result of checking */ function isCached(page) { var cache = _pages[page], time = new Date(); if (cache) { if ( _isPageTimeValid(page) ) { return true; } else { remove(page); } } return false; } /** * Check if provided page timestamp is valid (not greater than <_cacheTime> seconds ago) * @private * @param {Number} page Number of page to test caching time * @return {Boolean} Result of checking */ function _isPageTimeValid(page) { var now = new Date(), pageCacheTime = _times[page]; return now - pageCacheTime < _cacheTime * 1000; } /** * Check stack length and remove extra cached pages (if length is greater than <_cacheSize>) * @private */ function _checkStack() { var page; while (_stack.length > _cacheSize) { page = _stack[0]; remove(page); } } /** * Cache page * @param {Number} num Page number * @param {Object} json JSON with comments. If provided - we will not do server request */ function add(page, json) { if ( isCached(page) ) { return; } if (json) { _pages[page] = json; _times[page] = new Date(); _stack.push(page); _checkStack(); } else { // fetch page json from server _commentsWidget._fetchThread(page, function (json) { _pages[page] = json; _times[page] = new Date(); _stack.push(page); _checkStack(); }); } } /** * Get page json from cache * @param {Number} page Page number * @return {Object/null} Comments json for the page, or null, if page hasn't been cached */ function get(page) { return isCached(page) ? _pages[page] : null; } /** * Remove page from cache and from cache stack * @private * @param {Number} page Page number */ function remove(page) { // if page hasn't been cached - return if (!_pages[page]) { return; } delete _pages[page]; delete _times[page]; _stack = _stack.filter(function (pageNumber) { return pageNumber !== page; }); } /** * Initialize cacher * @param {Object} commentsWidget Comments widget instance * @param {Object} pagerWidget Pager widget instance */ function init(commentsWidget, pagerWidget) { _commentsWidget = commentsWidget; _pagerWidget = pagerWidget; } return { init: init, add: add, remove: remove, get: get, isCached: isCached }; }()); /** @lends $.lj.comments.prototype */ $.widget('lj.comments', jQuery.lj.basicWidget, { options: { selectors: { linkCollapse: '.b-leaf-actions-collapse a', linkExpand: '.b-leaf-actions-expand a', linkExpandChildren: '.b-leaf-actions-expandchilds a', leafMore: '.b-leaf-seemore', seeMore: '.b-leaf-seemore-more a, .b-leaf-seemore-expand a', commentDeleted: '.b-leaf-deleted', leaf: '.b-leaf', leafCollapsed: '.b-leaf-collapsed', tree: '.b-tree-root', twig: '.b-tree-twig', pager: '.b-pager', replyCounter: '.b-xylem-cell-amount .js-amount', spamCounter: '.b-xylem-cell-spam .js-amount', commentsJSON: '#comments_json' }, classNames: { groveLoading: 'b-grove-loading', newComment: 'b-leaf-new', leafCollapsed: 'b-leaf-collapsed', leafCollapsing: 'b-leaf-collapsing', leafExpanding: 'b-leaf-expanding', leafActive: 'b-leaf-commenting', leafShow: 'b-leaf-hover', leafClipped: 'b-leaf-clipped', leafLevelUp: 'b-leaf-seemore-parent', leafMore: 'b-leaf-seemore', shortTime: 'b-leaf-shorttime', seeMoreVertical: 'b-leaf-seemore-depth', groveShowOnHover: 'b-grove-hover', showSpam: 'b-grove-showspam', twigLevel: 'b-tree-twig-' }, templates: { twig: '
{html}
', emptyLeaf: '
', leaf: 'templates-Comments-Twig' } }, _create: function() { $.lj.basicWidget.prototype._create.apply(this); var match = location.href.match(/(\d+)\.html/), // inline comments json json = null, that = this, currentPage; this._itemid = match && match[1]; this._root = this._el('tree'); this._pager = this._el('pager'); this._constructionCache = {}; this._showSpamPage = this.element.hasClass(this._cl('showSpam')); this.element.removeClass(this._cl('groveShowOnHover')); this._bindControls(); this._pager.commentsPager(); this._pagerWidget = this._pager.first().data('commentsPager'); this._replycount = LJ.get('replycount') || 0; this._spamcount = LJ.get('spamcount') || 0; // initialize cache object Cacher.init(this, this._pagerWidget); // cache pages handler this._on('cachePage', Cacher.add); if (typeof LJ.get('comments') !== 'undefined') { this.element.addClass(this._cl('groveLoading')); this._root.css({ 'min-height': $(window).height() }); this._hideRoot(); this._one('commentsConstructed', function () { this._restoreRoot(); this.element.removeClass(this._cl('groveLoading')); }.bind(this)); this._on('commentsPage', function () { this._root.css({ 'min-height': $(window).height() }); this._loadPage.apply(this, [].slice.call(arguments)); $(window).scrollTop(this.element.offset().top + this.element.outerHeight() - this.element.height()); }.bind(this)); this._on('commentsConstructed', function () { this._root.removeAttr('style'); if (this.hasOwnProperty('_realRoot')) { this._realRoot.removeAttr('style'); } }.bind(this)); // parse inline json json = LJ.get('comments'); // if we have no comments on the page if (this._pagerWidget) { // cache page currentPage = this._pagerWidget.page(); Cacher.add( currentPage, json ); } // create comments tree this._constructComments(json); // initial hash jump (e.g. /79375.html?thread=1568271#t1568271) // seems that only Firefox actually needs it setTimeout(function() { if (document.location.hash.length > 0) { document.location.hash = document.location.hash; } }, 0); } }, _loadPage: function(num) { var self = this, stop = false, cancel = function () { stop = true; }, processResults = function(json) { if (stop) { return; } self._one('commentsConstructed', function () { self._restoreRoot(); self.element.removeClass(self._cl('groveLoading')); }); self._hideRoot(); self._off('commentsPage', cancel); self._constructComments(json); self._fire('commentsPageLoaded', [num]); }; Function.defer(this._one.bind(this, 'commentsPage', cancel)); this._constructionCache = {}; this._currentPage = num; this._root.empty(); // get page from cache if it has been cached if ( Cacher.isCached(num) ) { return processResults( Cacher.get(num) ); } //we don't need very fast loader var d = +new Date(), mindelay = 1500; this.element.addClass(this._cl('groveLoading')); this._fetchThread(num, function(json) { var d2 = +new Date(); if (d2 - d < mindelay) { setTimeout(processResults.bind(null, json), mindelay - (d2 - d)); } else { processResults(json); } Cacher.add(num, json); }); }, /** * Bind common events for the widget */ _bindControls: function() { var self = this, selectors = this.options.selectors, classNames = this.options.classNames; $.lj.basicWidget.prototype._bindControls.apply(this); ['Expand', 'Collapse', 'ExpandChildren'].forEach(function(el) { var selector = selectors['link' + el], method = el.toLowerCase(); if (method === 'expandchildren') { method = 'expand'; } self.element.delegate(selector, 'click', function(ev) { var leaf = jQuery(this).closest(selectors.leaf); //we pass leaf variable just not to construct jquery object again self[method](leaf.prop('id'), leaf); ev.preventDefault(); }); }); //we show hovered buttons after little delay var hoverFunc = new LJ.DelayedCall(function(leaf) { leaf.addClass(classNames.leafShow); }, 150); var loadTimeStamp = new LJ.DelayedCall(this._loadTimestamp.bind(this), 150); this.element.delegate(selectors.leaf, 'click', function(ev) { $(this).removeClass(classNames.newComment); }) .delegate(selectors.seeMore, 'click', function(ev) { var leaf = jQuery(this).closest(selectors.leaf); if (leaf.hasClass(classNames.leafLevelUp)) { return; } self._loadMore(leaf); ev.preventDefault(); }) .delegate(selectors.leaf, 'mouseover', function(ev) { var $this = jQuery(this); if (ev.target.className.indexOf(classNames.shortTime) !== -1 && !ev.target.getAttribute('title')) { loadTimeStamp.run($this, jQuery(ev.target)); } if (!$this.hasClass(classNames.leafShow)) { hoverFunc.run($this); } }) .delegate(selectors.leaf, 'mouseout', function(ev) { var leaf = jQuery(this), toEl = jQuery(ev.relatedTarget); loadTimeStamp.stop(); if (toEl.closest(leaf).length === 0) { hoverFunc.stop(); leaf.removeClass(classNames.leafShow); } }); this.element .bind("expand", function(ev, ditemid, skipAnimation) { self.expand(ditemid, null, skipAnimation); }) .bind("collapse", function(ev, ditemid, skipAnimation) { self.collapse(ditemid, null, skipAnimation); }) .bind("refreshThread", this._refreshThread.bind(this)) .bind('newComment', this._addNewComment.bind(this)) .bind("deleteComment", this._deleteComment.bind(this)) .bind("removeFromDOM", this._removeFromDOM.bind(this)); //proxy to internal events system this.element.bind('commentsUpdated', function(ev, ditemid, action, count) { var delta = 0; if (action === 'addnew') { delta = 1; } else if (action === 'delone' || action === 'delmany') { delta = -count; } if (delta !== 0) { //On the spam comments page we can delete only spam if (self._showSpamPage) { self._spamNumberChanged(delta); } else { self._commentsNumberChanged(delta); } } }); if (this._el('commentsJSON').length === 0) { setTimeout(this._highlightNewComments.bind(this), 100); //do not block the ui with comments scanning } // remove page from cache after add comment LiveJournal.register_hook('commentator/submit', function () { if ( self._pagerWidget ) { Cacher.remove( self._pagerWidget.page() ); } }); }, /** * Load date timestamp for the corresponding collapsed element. * * @param {jQuery} leaf Comment element. * @param {jQuery} dateSpan Date span element. */ _loadTimestamp: function(leaf, dateSpan) { var params = { journal: Site.currentJournal, thread: leaf.attr('id').substr(1) }, handleAnswer = function(ans) { if (ans.status === 'ok') { dateSpan.attr('title', ans.stamp); } }; jQuery.get(LiveJournal.getAjaxUrl('get_thread_timestamp'), params, handleAnswer, 'json'); }, /** * Highlight comments considered as new on the page. * On every page load scripts finds the comment with he biggest id, * and considers all with the bigger ids as new on the next load. */ _highlightNewComments: function() { this._highlightNewLeaves(this.element.find(this.options.selectors.leaf)); }, _highlightNewLeaves: function(leaves, ts) { ts = ts || this._getPageTimestamp(); var self = this, newClass = this.options.classNames.newComment, maxts = ts || 0; $.each(leaves, function() { var stamp = parseInt( this.getAttribute('data-updated-ts'), 10), //we do not generate jquery objects for speed commentUsername = this.getAttribute('data-username'), leafClasses = this.className, isLoadMore = leafClasses.indexOf(self._cl('leafMore')) !== -1; if (maxts < stamp && !isLoadMore) { maxts = stamp; } if (ts && stamp > ts && (!Site.remoteUser || (Site.remoteUser !== commentUsername))) { this.className = leafClasses + ' ' + newClass; if (isLoadMore) { this.setAttribute('data-max-ts', ts); } } }); var truets = this._getPageTimestamp(); if (!ts || truets < maxts) { this._setPageTimestamp(maxts); } }, _getPageTimestamp: function() { var hash = this._getPageHash(), store = LJ.Storage.getItem(hash) || {}; return store && parseInt(store.stamp, 10); }, _setPageTimestamp: function(stamp) { if (!stamp) { return; } var hash = this._getPageHash(); LJ.Storage.setItem(hash, { stamp: stamp, updated: +new Date() }); }, /** * Calculate hash of the page. Different pages of comments and different threads in the same * post are considered as distinct pages. * * @return {String} Page hash. */ _getPageHash: function() { var hash = ['url=' + Site.currentEntry]; hash.push('thread=' + (LiveJournal.parseGetArgs(location.href).thread || 0)); hash.push('page=' + this._getCurrentPage()); return hash.join(';'); }, /** * Get current page of of comments * * @return {Number} */ _getCurrentPage: function() { return this._pager.length === 0 ? 1 : this._pager.commentsPager('page'); }, _addPoster: function(data) { data.poster = LJ.get('entry.poster'); }, /** * Construct twig to add markup for the new comment on the page and place it on the page. * * @param {Object} data Object contains the data necessary to build markup. * Fields: dtalkid and parent exist in any output from __rpc_get_thread endpoint. * If templates are disabled endpoints returns html field and all the data for * the template otherwise. * * @return {jQuery} jQuery object representing twig element of the new comment. */ _createComment: function(data) { var selectors = this.options.selectors, classNames = this.options.classNames, tmpl = this.options.templates, level, margin, twig, parent, leaves; if (data.parent) { if (this._constructionCache.hasOwnProperty(data.parent)) { parent = this._constructionCache[data.parent]; } else { parent = this._root.find('#t' + data.parent); } leaves = $.comments.getThread(parent, true); if (!data.level) { level = $.comments.level(parent) + 1; } else { level = data.level; } twig = leaves[leaves.length - 1].closest(selectors.twig); } else { level = 1; twig = this._root; } margin = data.margin || (level - 1) * 30; var newTwig, twigParams = { dtalkid: data.dtalkid, level: level, margin: margin }; if (data.hasOwnProperty('html')) { newTwig = jQuery(tmpl.twig .supplant({ html: data.html || tmpl.emptyLeaf }) .supplant(twigParams)); // twigParams.html = data.html || tmpl.emptyLeaf.supplant({ dtalkid: data.dtalkid }); } else { data = $.extend({}, data, { level: level, margin: margin }); this._addPoster(data); newTwig = this._tmpl('leaf', data); } if (twig === this._root) { twig.append(newTwig); } else { newTwig.insertAfter(twig); } this._constructionCache[data.dtalkid] = newTwig; return newTwig; }, /** * Event handler responsible for creating new comments on the page. * * @param {jQuery.Event} ev jQuery event object. * @param {Object} data Data necessary to build comment. */ _addNewComment: function(ev, data) { //we redirect if the new comment is on the other page if (this._showSpamPage || //we have only one page in the thread so do not look on the server answer (!LiveJournal.parseGetArgs(location.href).thread && (this._getCurrentPage() - data.page !== 0)) || //we can't post a root comment in a thread view (!data.parenttalkid && LiveJournal.parseGetArgs(location.href).thread)) { window.location.href = decodeURIComponent(data.result); return; } if (jQuery('#t' + data.dtalkid).length === 0) { var parenttalkid = data.parenttalkid && data.parenttalkid.substr(1) || null; this._createComment({ dtalkid: data.dtalkid, parent: parenttalkid, html: ''}); jQuery('#t' + data.dtalkid).empty(); } this._refreshThread(null, data.dtalkid, true, 'addnew'); setTimeout(function() { window.location.hash = 't' + data.dtalkid; //we need this because opera will not jump to the new location otherwise. }, 0); }, /** * Construct comments from the json. * * @param {Array} json array of objects with data necessary to build comments. * @param {number} ts Timestamp to determine whether the comment is new or not. */ _constructComments: function(json, ts) { var self = this, leaves = [], jsonlen = json.length, count = 0, createLeaf = LJ.Function.threshold(function() { if (count === jsonlen) { self._off('commentsPage', cancel); self._highlightNewLeaves(leaves, ts); self._fire('commentsConstructed', null, true); count++; return; } if (count > jsonlen) { return; } var tid = 't' + json[count].dtalkid, leaf = self._createComment(json[count]).find(self._s('leaf')); leaves.push(leaf.get(0)); count++; createLeaf(); }, 25, true), cancel = function () { self._restoreRoot(); createLeaf.resetQueue(); }; ts = ts || this._getPageTimestamp(); /* Process 25 comments at once then let browser breeze */ createLeaf.batch(20); Function.defer(self._one.bind(self, 'commentsPage', cancel)); // Init construction createLeaf(); }, /** * Process load more link. The link should contain id of the parent comment. * Max-ts is set programmatically to the value of the timestamp that was on * the moment when page was opened. * * @param {jQuery} leaf The leaf node of load more link. */ _loadMore: function(leaf) { var self = this, parent = leaf.data('parent'); leaf.addClass(this.options.classNames.leafExpanding); this._fetchThread('t' + parent, function(results) { results.shift(); // Skip thread parent self._constructComments(results, leaf.data('max-ts')); leaf.remove(); }, null, leaf.hasClass(this._cl('seeMoreVertical')) ? 0 : 2); //load more starts always from the third comment in the thread }, /** * Event handler responsible for deleting comments. * * @param {jQuery.Event} ev jQuery event object. * @param {number|string} ditemid Comment ditemid. * @param {boolean} delThread Delete full thread starting from this comment. * @param {boolean} delAuthor Delete all comments of this author in the post. */ _deleteComment: function(ev, ditemid, delThread, delAuthor) { var selectors = this.options.selectors, self = this, outerHTML = function(el) { return el.clone().wrap('
').parent().html(); }, fillNodes = function() { nodesToRemove.replaceWith(deletedNodeHTML); self.element.trigger('commentsUpdated', [ 't' + ditemid, ( delThread || delAuthor ) ? 'delmany' : 'delone', nodesToRemove.length ]); self._checkSpamPageStatus(); }, //we get outerHTML for deleted comment from the post deletedNode = this.element.find(selectors.commentDeleted + ':first'), deletedNodeHTML = deletedNode.length && outerHTML(deletedNode), nodesToRemove = this.element.find('#t' + ditemid); if (deletedNode.length === 0) { this._fetchThread('t' + ditemid, function(results) { //we do not simply assign string, because we should check the answer for errors var nodeData = results.pop(), node = nodeData.hasOwnProperty('html') ? jQuery(nodeData.html).filter(selectors.commentDeleted) : self._tmpl('leaf', nodeData).find(selectors.commentDeleted); if (node.length === 0) { throw new Error('Comment t' + ditemid + ' has not been deleted'); } deletedNodeHTML = outerHTML(node); fillNodes(); }, true); } if (delThread) { nodesToRemove = nodesToRemove.add($.comments.getThread(nodesToRemove)); } if (delAuthor) { nodesToRemove = nodesToRemove.add(this.element.find(selectors.leaf + '[data-username=' + delAuthor + ']')); } if (deletedNode.length !== 0) { fillNodes(); } }, /** * Event handler responsible for removing comments from the DOM * without any further actions. * * @param {jQuery.Event} ev jQuery event object. * @param {number|string|Array} ditemids Comment ditemid or an array of such items */ _removeFromDOM: function(ev, ditemids, action) { var self = this; if (!jQuery.isArray(ditemids)) { ditemids = [ditemids]; } ditemids.forEach(function(ditemid) { var selector = '#t' + ditemid, node = jQuery(selector); if (node.length > 0) { node.remove(); } }); this._checkSpamPageStatus(); if (this._showSpamPage && (action === 'spamcomment' || action === 'unspam')) { this._spamNumberChanged(-ditemids.length); this._commentsNumberChanged(ditemids.length); } }, /** * For spam pages we check the number of nondeleted comments on each * comment remove and redirect to the normal comments page if there * are none of them. */ _checkSpamPageStatus: function() { var args, base, selectors = this.options.selectors, selector = selectors.leaf + ':not(' + selectors.commentDeleted + ')' + ':first'; if (this._showSpamPage && (this.element.find(selector).length === 0)) { args = LiveJournal.parseGetArgs(location.href); base = location.href.split('?')[0]; delete args.mode; delete args.page; location.href = LiveJournal.constructUrl(base, args).replace(/\?$/, '') + '#comments'; } }, /** * Event handler responsible for initiating refresh of commments. * * @param {jQuery.Event} ev jQuery event object. * @param {number|string} ditemid Comment ditemid. * @param {boolean=} fetchSingle If true the method will trigger the update * of only this comment and of the full thread otherwise. * @param {string} Action that caused thread refresh. Used to figure out if * any comments should not be updated. */ _refreshThread: function(ev, ditemid, fetchSingle, action) { var self = this; //here we should collapse nodes, that were not returned by ajax this._fetchThread('t' + ditemid, function(result) { self._applyFetchResults(result, true, function() { if (!fetchSingle) { var ditemids = []; for (var i = 0, l = result.length; i < l; ++i) { ditemids.push('t' + result[i].dtalkid); } var idsStr = ' ' + ditemids.join(' ') + ' '; //these nodes where not updated with request, so, close them var badNodes = $.comments.getThread(jQuery('#t' + ditemid)) .filter(function() { return -1 === idsStr.indexOf(' ' + this.id + ' '); }); badNodes .addClass(self._cl('leafCollapsed')) .removeAttr('data-full'); } self.element.trigger('commentsUpdated', [ 't' + ditemid, action ]); }, action); }, fetchSingle); }, /** * Load page or thread. * * @param {string|number} tid String starting with "t.." will be treated like thread, or * it will be treated as page number. * @param {Function(json)} resultsCallback A callback with one argument for resulting JSON. * @param {boolean=false} fetchSingle Whether to fetch only one comment. * @param {number=} skip Need to figure out the meaning of that parameter on the server. */ _fetchThread: function(tid, resultsCallback, fetchSingle, skip) { fetchSingle = fetchSingle || false; resultsCallback = resultsCallback || $.noop; var self = this; var match = tid.toString().match(/t(\d+)/), id = match && match[1], fetchType = 'thread'; if (!id) { fetchType = 'page'; id = tid; } var fetchParams = { journal: Site.currentJournal, itemid: this._itemid, flat: fetchSingle ? '1' : '', skip: skip ? skip : '' }, getArgs = LiveJournal.parseGetArgs( location.href ); fetchParams[fetchType] = id; if (fetchType === 'thread') { fetchParams.expand_all = fetchSingle ? '' : '1'; } if( getArgs && !!getArgs.style && getArgs.style === "mine" ) { fetchParams.style = "mine"; } var xhr = jQuery.ajax({ url: LiveJournal.getAjaxUrl('get_thread'), data: fetchParams, dataType: 'json', timeout: 7500 }).success(function(result) { self._off('commentsPage', cancel); if ('comments' in result) { self._updateCounters(result); result = result.comments; } resultsCallback(result); }).error(function(result, type) { if (type === 'abort') { return; } else { self._off('commentsPage', cancel); } setTimeout(self._fetchThread.bind(self, tid, resultsCallback, fetchSingle), 500); }), cancel = function () { self._restoreRoot(); xhr.abort(); }; Function.defer(self._one.bind(self, 'commentsPage', cancel)); }, _updateCounters: function (data) { if ('replycount' in data) { this._el('replyCounter').html(LJ.ml('talk.replycount', { count: data.replycount })); } if ('spamcount' in data) { this._el('spamCounter').html(LJ.ml('talk.spamcount', { count: data.spamcount })); } }, _applyFetchResults: function(results, leaveCollapsed, succ, action) { var self = this, count = results.length, updateCount = function() { count--; if (!count) { succ(); } }; succ = succ || $.noop; results.forEach(function(el) { var div; if (el.html) { div = jQuery(el.html).filter('div'); } else { self._addPoster(el); div = self._tmpl('leaf', el).find(self._s('leaf')); } var id = div.prop('id'), target = jQuery('#' + id); if (target.hasClass(self._cl('leafCollapsed')) && !div.hasClass(self._cl('leafCollapsed'))) { // do not expand comment here, do it in expand function div.addClass(self._cl('leafCollapsed')).data('full', 1); } //do not expand comment that is being edited or commented if (action !== 'expand' || !target.hasClass(self.options.classNames.leafActive)) { target.replaceWith(div); } else { //it comment has open form we remove the class but do not update the comment itself if (action === 'expand') { target.removeClass(self._cl('leafExpanding')); } } updateCount(); }); }, _commentsNumberChanged: function(delta) { /** * Read form state from commentform widget to know what * previous action was. It could be 'add' / 'edit' / null * If it is edit - don't change counter and reset previousState value */ // comment form widget instance (for counter manipulations) var _commentform = $('.b-watering').data('commentform'); if (_commentform.previousState() === 'edit') { _commentform.syncPreviousState(); } else { this._replycount += delta; this._updateCounters({ replycount: this._replycount }); } }, _spamNumberChanged: function(delta) { this._spamcount += delta; this._el('spamCounter').html(LJ.ml('talk.spamcount', { count: this._spamcount })); }, /** * Expand thread. * * @param {String} id HTML Id of the comment (t(\d+)) * @param {jQuery?} node If passed this object will be used as a comment node. * If not the comment node will be found by id. */ expand: function(id, node, skipAnimation) { node = node || jQuery('#' + id); if (node.hasClass(this.options.classNames.leafExpanding) || node.hasClass(this.options.classNames.leafCollapsing)) { return; } node.addClass(this.options.classNames.leafExpanding); var self = this, collapsedClass = self.options.classNames.leafCollapsed, selectors = this.options.selectors, leaves = $.comments.getThread(node), collapsedLeaves = leaves.filter(selectors.leafCollapsed), dataFilter = function(dataExist) { return function() { var node = jQuery(this); return ((!!node.data('full')) === dataExist); }; }, checkLoadMore = function(leaves) { leaves.filter(self._s('leafMore')).each(function() { var leaf = jQuery(this), count = +leaf.data('count'); if (count < 25) { self._loadMore(leaf); } }); }; if (skipAnimation === undefined) { skipAnimation = jQuery.comments.skipAnimation; } if (((!node.data('full') && node.hasClass(collapsedClass)) || collapsedLeaves.filter(dataFilter(false)).length > 0)) { this._fetchThread(id, jQuery.delayedCallback(function(results) { self._applyFetchResults(results, false, function() { var newLeaves = jQuery(); collapsedLeaves.each(function() { newLeaves = newLeaves.add(jQuery('#' + this.id)); }); self._expandNodes(node, node.prop('id'), newLeaves.filter(dataFilter(true)), skipAnimation); checkLoadMore(leaves); }, 'expand'); }, 3000)); } else if (node.hasClass(collapsedClass) || collapsedLeaves.length > 0) { this._expandNodes(node, node.prop('id'), collapsedLeaves.filter(dataFilter(true)), skipAnimation); checkLoadMore(leaves); } else { node.removeClass(this.options.classNames.leafExpanding); } }, _expandNodes: function(node, id, collapsedLeaves, skipAnimation) { var self = this, collapsedClass = self.options.classNames.leafCollapsed; if (skipAnimation === undefined) { skipAnimation = jQuery.comments.skipAnimation; } node.removeClass(this.options.classNames.leafExpanding); if (skipAnimation) { collapsedLeaves.removeClass(collapsedClass); } else { collapsedLeaves.each(function() { var leaf = jQuery(this), height = leaf.height(); if (leaf.hasClass(self.options.classNames.leafActive)) { return; } leaf.removeClass(collapsedClass) .attr('style',''); var newHeight = leaf.height(); leaf .css({ opacity: 0, height: height, overflow: 'hidden' }) .animate({ opacity: 1, height: newHeight }, { duration: 300, complete: function() { leaf.attr('style',''); }, queue: false }); }); } this.element.trigger('commentsUpdated', [id, 'expand']); }, /** * Collapse thread. * * @param {String} id HTML Id of the comment (t(\d+)) * @param {jQuery?} node If passed this object will be used as a comment node. * If not the comment node will be found by id. */ collapse: function(id, node, skipAnimation) { var self = this, collapsedClass = self.options.classNames.leafCollapsed, clippedClass = self.options.classNames.leafClipped, collapsingClass = self.options.classNames.leafCollapsing; node = node || jQuery('#' + id); if (skipAnimation === undefined) { skipAnimation = jQuery.comments.skipAnimation; } if (node.hasClass(this.options.classNames.leafExpanding)) { return; } if (node.hasClass(collapsingClass)) { return; } var selectors = this.options.selectors, twig = node.closest(selectors.twig), leaves = $.comments.getThread(node).filter(':not(.' + collapsedClass + ')' + ':not(.' + collapsingClass + ')' + ':not(.' + clippedClass + ')'); if (node.hasClass(collapsedClass) && leaves.length === 0) { return; } //do nothing if thread was already collapsed //we toggle class for non collapsed comments to figure out the height of the collapsed block; // var height = twig.outerHeight(true); leaves.addClass(collapsedClass); var newHeight = twig.height(); //.outerHeight(true); leaves.removeClass(collapsedClass) .data('full', true) .attr('style','') .addClass(collapsingClass); if (skipAnimation) { leaves.addClass(collapsedClass).removeClass(collapsingClass); } else { leaves.each(function() { var leaf = jQuery(this), height = leaf.height(); leaf.css({ height: height, overflow: 'hidden' }); leaf.animate({ opacity: 0, height: newHeight }, { duration: 300, complete: function() { leaves .addClass(collapsedClass) .attr('style','') .removeClass(collapsingClass); }, queue: false }); }); } this.element.trigger('commentsUpdated', [ id, 'collapse' ]); }, _hideRoot: function () { if (!this.hasOwnProperty('_realRoot')) { this._realRoot = this._root; this._root = jQuery('
'); } }, _restoreRoot: function () { if (this.hasOwnProperty('_realRoot')) { this._realRoot.append(this._root.children()); this._root = this._realRoot; delete this._realRoot; } } }); }(jQuery, window)); /** * Fired when comments are update after any operation. * @name $.lj.comments#commentsUpdated * * @param {string} id html id of the topmost updated comment. * @event */ /** * Fired when comments should be updated anywhere. This event * is listened by this widget and any widgets from outside can * use it to refresh some comments on the page. * * @name $.lj.comments#refreshThread * * @param {number|string} ditemid Comment ditemid. * @param {boolean=} fetchSingle If true the method will trigger the update * of only this comment and of the full thread otherwise. * @param {string} Action that caused thread refresh. Used to figure out if * any comments should not be updated. * @event */ /** * Widget listens this event to remove some comment on the page. * * @name $.lj.comments#deleteComment * * @param {number|string} ditemid Comment ditemid. * @param {boolean} delThread Delete full thread starting from this comment. * @param {boolean} delAuthor Delete all comments of this author in the post. * @event */ /** * Widget listens this event to remove some comment nodes from the DOM immediately. * Note: for now this method will not trigger any events. * * @name $.lj.comments#removeFromDOM * * @param {number|string|Array} ditemids Comment ditemid or an array of such items * @event */ ; /* file-end: js/jquery/jquery.lj.comments.js ----------------------------------------------------------------------------------*/ /* --------------------------------------------------------------------------------- file-start: js/jquery/jquery.lj.spellchecker.js */ LJ.injectStyle('.b-spelling {\n position: relative;\n width: 100%;\n height: 13em;\n border: 1px solid #999;\n -moz-box-sizing: border-box;\n -webkit-box-sizing:border-box;\n -ms-box-sizing: border-box;\n box-sizing: border-box;\n -webkit-transition: height 100ms ease-out;\n -moz-transition: height 100ms ease-out;\n -o-transition: height 100ms ease-out;\n transition: height 100ms ease-out;\n background: #FFF;\n }\n.b-spelling:focus {\n outline: thin solid #85C1FF;\n }\n.b-updatepage .b-spelling {\n height: 403px;\n }\n.b-inboxpage .b-spelling {\n height: 400px;\n }\n .b-spelling-faketextarea,\n .b-spelling-textarea {\n position: relative;\n width: 100%;\n margin: 0;\n padding: 5px;\n border: 0;\n -moz-box-sizing: border-box;\n -webkit-box-sizing:border-box;\n -ms-box-sizing: border-box;\n box-sizing: border-box;\n font: 1em/1.2em Arial,sans-serif;\n *padding: 0;\n *line-height: 1.4em;\n }\n /* font for update.bml */\n .b-updatepage .b-spelling-faketextarea,\n .b-updatepage .b-spelling-textarea {\n padding: 10px 8px;\n font: 1.0em/1.4 Consolas,\"Liberation Mono\",Courier,monospace,sans-serif;\n *padding: 0;\n }\n .b-spelling-faketextarea {\n z-index: 1;\n background: #FFF;\n color: #FFF;\n white-space: pre;\n white-space: pre-wrap;\n word-wrap: break-word;\n }\n .b-spelling-popuping .b-spelling-faketextarea { \n z-index: 2;\n color: #000;\n }\n .b-spelling-textarea {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 2;\n /*overflow-x: auto;*/\n overflow: hidden;\n height: 100%;\n opacity: 1;\n resize: none;\n background: transparent;\n color: #000;\n }\n .b-spelling-popuping .b-spelling-textarea {\n z-index: 1;\n opacity: 0;\n }\n .b-spelling-word {\n }\n .b-spelling-inner {\n }\n .b-spelling-error {\n margin: 0;\n padding: 0;\n cursor: pointer;\n }\n .b-spelling-error .b-spelling-inner {\n border-bottom: 1px solid #F00;\n }\n .b-spelling-error.active {\n }\n .b-spelling-error.active .b-spelling-inner {\n border-bottom: 2px solid #F00;\n }\n .b-spelling-editing {\n cursor: text;\n }\n .b-spelling-editing .b-spelling-inner {\n color: #000;\n border-bottom-color: #FFF;\n }\n .b-spelling-bubble {\n position: absolute;\n margin: 0;\n padding: 0;\n border: 0;\n -moz-border-radius: 5px;\n -moz-border-radius-topleft: 0;\n border-radius: 5px;\n border-top-left-radius: 0;\n -webkit-box-shadow: 0 1px 3px rgba(53,99,161,0.8);\n -moz-box-shadow: 0 1px 3px rgba(53,99,161,0.8);\n box-shadow: 0 1px 3px rgba(53,99,161,0.8);\n -webkit-transition: opacity .3s ease;\n -moz-transition: opacity .3s ease;\n -o-transition: opacity .3s ease;\n transition: opacity .3s ease;\n font: 14px/1.2 Arial,sans-serif;\n text-align: left;\n *border: 1px solid rgb(53,99,161);\n }\n .b-spelling-bubble .b-popup-outer {\n margin: 0;\n padding: 10px 15px 5px;\n border: 0;\n -moz-border-radius: 4px;\n -moz-border-radius-topleft: 0;\n border-radius: 4px;\n border-top-left-radius: 0;\n background: #FAFAFA;\n background: #FAFAFA url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgdmlld0JveD0iMCAwIDEgMSIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+CiAgPGxpbmVhckdyYWRpZW50IGlkPSJncmFkLXVjZ2ctZ2VuZXJhdGVkIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgeDE9IjAlIiB5MT0iMCUiIHgyPSIwJSIgeTI9IjEwMCUiPgogICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZhZmNmZSIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjI1JSIgc3RvcC1jb2xvcj0iI2UyZTNlNyIgc3RvcC1vcGFjaXR5PSIxIi8+CiAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmYWZhZmEiIHN0b3Atb3BhY2l0eT0iMSIvPgogIDwvbGluZWFyR3JhZGllbnQ+CiAgPHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLXVjZ2ctZ2VuZXJhdGVkKSIgLz4KPC9zdmc+);\n background: #E2E3E7 -moz-linear-gradient(top, #FAFCFE 0%, #E2E3E7 25%, #FAFAFA 100%);\n background: #E2E3E7 -webkit-gradient(linear, left top, left bottom, color-stop(0%,#FAFCFE), color-stop(25%,#E2E3E7), color-stop(100%,#FAFAFA));\n background: #E2E3E7 -webkit-linear-gradient(top, #FAFCFE 0%,#E2E3E7 25%,#FAFAFA 100%);\n background: #E2E3E7 -o-linear-gradient(top, #FAFCFE 0%,#E2E3E7 25%,#FAFAFA 100%);\n background: #E2E3E7 linear-gradient(top, #FAFCFE 0%,#E2E3E7 25%,#FAFAFA 100%);\n *zoom: 1;\n }\n .b-spelling-bubble .b-popup-inner {\n margin: 0;\n padding: 0;\n border: 0;\n -webkit-border-radius: 0;\n -moz-border-radius: 0;\n border-radius: 0;\n }\n .b-spelling-bubble .i-popup-arr {\n visibility: hidden;\n }\n .b-spelling-bubble .i-popup-arrtl {\n left: 0;\n }\n .b-spelling-bubble .i-popup-close {\n position: absolute;\n top: 5px;\n right: 5px;\n width: 6px;\n height: 5px;\n margin: 0;\n padding: 0;\n background: url(/img/icons/popup-close-s.png?v=20956) -5px -5px no-repeat;\n font: 0/0 serif;\n }\n .b-spelling-bubble .i-popup-close:hover {\n background-position: -5px -21px;\n }\n .b-spelling-bubble-container {\n margin: 0;\n }\n .b-spelling-items {\n margin: 0;\n padding: 0;\n list-style: none;\n }\n .b-spelling-item {\n margin: 0 -15px;\n padding: 3px 15px 5px;\n cursor: pointer;\n }\n .b-spelling-item:hover {\n background: #C8E6FF;\n }\n .b-spelling-item-word {\n margin: 0;\n padding: 0;\n border-bottom: 1px dotted #000;\n }\n .b-spelling-item-skip {\n position: relative;\n margin-top: 10px;\n border-top: 1px solid #C9CACC;\n font-style: italic;\n font-size: 13px;\n color: #232425;\n }\n .b-spelling-item-skip:before {\n content: \" \";\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n display: block;\n height: 2px;\n background: #FFF;\n }\n .b-spelling-item-no {\n font-size: 13px;\n cursor: default;\n color: #232425;\n }\n .b-spelling-item-no:hover {\n background: none;\n }\n .b-spelling-item-no .b-spelling-item-word {\n border-bottom: 0;\n }\n\n.b-updateform-disabled .b-spelling {\n border-color: #CCC;\n }\n .b-updateform-disabled .b-spelling-textarea {\n color: #7F7F7F;\n }\n'); /** * @author dmitry.petrov@sup.com (Dmitry Petrov) * @fileoverview LiveJournal spellchecker widget */ /** * @name $.lj.spellchecker * @requires $.ui.core, $.ui.widget, $.lj.basicWidget * @class Widget adds spellchecking functionality to any * textarea. Widget consumes two text variables: widget.form.skip_word and widget.form.no_suggestions * They should be passed from server. * */ // TODO: refactor autogrow enable/disable (function($,window) { /** @lends $.lj.spellchecker.prototype */ $.widget('lj.spellchecker.js', $.lj.basicWidget, { options: { classNames: { wordActive: 'active', area: 'b-spelling-textarea', skipError: 'b-spelling-item-skip', bubbleContainer: 'b-spelling-bubble', bubbleNoSuggestions: 'b-spelling-item-no' }, selectors: { bubbleItem: '.b-spelling-item', fake: '.b-spelling-faketextarea', error: '.b-spelling-error' }, tmpl: { spellError: '{word}', wrapper: '
' + '
' + '
', bubble: '
', bubbleList: '
    {items}' + '
  • ' + LiveJournal.getLocalizedStr('widget.form.skip_word') + '
  • ' + '
', bubbleItem: '
  • {word}
  • ', bubbleItemNoSuggestions : '
  • ' + LiveJournal.getLocalizedStr('widget.form.no_suggestions') + '
  • ' }, enabled: false, autogrow: true, minHeight: 400 }, // private methods _create: function() { $.lj.basicWidget.prototype._create.apply(this); this._time = null; this._minHeight = this.options.minHeight; this._enabled = false; this._suggestions = {}; //each time text is parsed all tokens reside here this._tokens = null; this._prevText = ''; this._spellingBubble = null; this._currentError = null; this._errorNodes = []; //this flag is used to prevent unnecessary highlighting that may occur if suggestions will //come after user began typing again. this._doHighlight = false; this._build(); this._bindControls(); }, _build: function() { var options = this.options; this._container = jQuery(options.tmpl.wrapper) .insertBefore(this.element); this._fakeArea = this._container.find(options.selectors.fake); this.element .remove() .addClass(options.classNames.area) .appendTo(this._container); this._spellingBubble = jQuery(options.tmpl.bubble); }, _bindControls: function() { var self = this, selectors = this.options.selectors, classNames = this.options.classNames, nodes = this._errorNodes; //jquery.fn.input caches input value inside of itself, so we use events directly this.element.bind('input keyup paste', this._onInput.bind(this)); $.lj.basicWidget.prototype._bindControls.apply(this); LiveJournal.register_hook('skipWord', this._skipWord.bind(this)); this._spellingBubble.bubble({ showOn: 'click', alwaysShowUnderTarget: true, classNames: { containerAddClass: classNames.bubbleContainer } }) .bind('bubblehide', function(ev) { if (self._currentError) { self._currentError.removeClass(classNames.wordActive); self._currentError = null; } }) .delegate(selectors.bubbleItem, 'click', function(ev) { if (this.className.indexOf(classNames.bubbleNoSuggestions) > -1) { return; } self._onCorrectWord(ev, this); }); this.element .bind('mousemove', function(ev) { if (!self._enabled) { return; } var x = ev.pageX, y = ev.pageY, hovered = self._getHoveredNode(x, y) if (hovered) { self.element.css('cursor', 'pointer'); } else { self.element.css('cursor', ''); } }) .bind('click', this._onClickWord.bind(this)); }, _onInput: function(ev) { var text = this.element.val(); if (this._prevText === text) { return; } this._spellingBubble.bubble('hide'); this._prevText = text; this._fakeArea.html(this._prevText.encodeHTML().replace(/(\r\n|\r|\n)/gm, '
    ')); clearTimeout(this._timer); if (this._enabled && this._prevText.length > 0) { this._timer = setTimeout(this._checkSpelling.bind(this), 300); } else { this._errorNodes.length = 0; } this._doHighlight = false; if (this.options.autogrow) { this._checkAreaHeight(); } }, _onCorrectWord: function(ev, sugNode) { var suggestion = sugNode.textContent || sugNode.innerText, word = this._currentError.text().toLowerCase(); if (sugNode.className.indexOf(this.options.classNames.skipError) > -1 && this._suggestions.hasOwnProperty(word)) { // run hook for sync between RTE / HTML spellcheckers LiveJournal.run_hook('skipWord', word); Function.defer(this._checkSpelling.bind(this)); } else { var suggestTextNode = document.createTextNode(suggestion); this._currentError .after(suggestTextNode) .remove(); for (var i=0; i < this._errorNodes.length; ++i) { if (this._errorNodes[i].node === this._currentError) { this._errorNodes.splice(i, 1); break; } } var token_id = this._currentError.data('id'), tokenData = this._tokens[token_id], pos = tokenData.textPos + suggestion.length; this._prevText = this._prevText.substr(0,tokenData.textPos) + suggestion + this._prevText.substr(tokenData.textPos + tokenData.token.length); this.element.val(this._prevText); DOM.setSelectedRange(this.element.get(0), pos, pos); this.element.focus(); } this._spellingBubble.bubble('hide'); }, _onClickWord: function(ev) { var x = ev.pageX, y = ev.pageY, self = this, classNames = this.options.classNames, hovered = this._getHoveredNode(x, y); if (hovered && (!this._currentError || this._currentError.get(0) !== hovered.node.get(0))) { if (this._currentError) { this._currentError.removeClass(classNames.wordActive); this._currentError = null; } //skip all click event chain Function.defer(function() { self._currentError = hovered.node; self._currentError.addClass(classNames.wordActive); self._spellingBubble.html(self._getBubbleContent(hovered.word)); self._spellingBubble.bubble('show', self._currentError); }); } else { this._spellingBubble.bubble('hide'); if (this._currentError) { this._currentError.removeClass(classNames.wordActive); this._currentError = null; } } }, /** * Construct html with suggestions for current word. * * @param {string} word The word to show suggestions for. * * @return {string} resulting popup html. */ _getBubbleContent: function(word) { var tmpl = this.options.tmpl, sugs = this._suggestions[word.toLowerCase()]; if (!sugs.html) { var items = []; if (sugs.words.length > 0) { sugs.words.forEach(function(w) { items.push(tmpl.bubbleItem.supplant({ word: w })); }); } else { items.push(tmpl.bubbleItemNoSuggestions); } sugs.html = tmpl.bubbleList.supplant({ items: items.join('') }); } return sugs.html; }, /** * Expand textarea height as soon as user types to prevend appearing of scroll. */ _checkAreaHeight: function() { var fakeHeight = this._fakeArea.height(), containerHeight = this._container.height(), height; if (!this._minHeight) { this._minHeight = containerHeight; } // hard coded height detection // we assume that line height is equal 25px if (containerHeight - fakeHeight < 25) { height = fakeHeight + 30; } else if (containerHeight - fakeHeight > 50) { height = fakeHeight + 25; } if (height) { height = height < this._minHeight ? this._minHeight : height; this._container.height(height); } }, checkHeight: function () { this._checkAreaHeight(); }, /** * Check user input on spelling errors. */ _checkSpelling: function() { if (!this._enabled) { return; } var self = this, tokens = this._tokenize(this._prevText), filterHelper = {}, words = tokens .filter(function(token) { var t = token.token.toLowerCase(); if (!token.isWord || filterHelper.hasOwnProperty(t)) { return false; } filterHelper[t] = true; return !self._suggestions.hasOwnProperty(t); }) .map(function(token) { return token.token; }); this._doHighlight = true; this._tokens = tokens; this._requestSuggestions(words); }, /** * Public method for checking spell */ check: function () { this._checkSpelling(); }, /** * Fetch suggestions about words in text and highlight errors in the text. * * @param {Array.} words Array with the words to check. */ _requestSuggestions: function(words) { words = words || []; if (words.length > 0) { jQuery.post(LiveJournal.getAjaxUrl('spellcheck'), { html: words.join(' ') }, function(result) { if (result.status === 'ok') { this._addWords(result.words, words); if (this._doHighlight) { this._highlightErrors(); } } }.bind(this), 'json'); } else { this._highlightErrors(); } }, /** * Add words to the suggestions cache. All suggestions are treated as correct words. * * @param {Object} words Hash object with suggestions arrays. * @param {Array} allWords Array contains the list of all words the were checked on correctness. * All the words that were not marked as misspelled are treated as correct. */ _addWords: function(words, allWords) { var sugs = this._suggestions, pushWord = function(word) { word = word.toLowerCase(); if (!sugs.hasOwnProperty(word)) { sugs[word] = { correct: true }; } }, wordLowered; for (var word in words) { wordLowered = word.toLowerCase(); if (words.hasOwnProperty(word) && !sugs.hasOwnProperty(wordLowered)) { sugs[wordLowered] = { words: words[word] }; words[word].forEach(pushWord); } } allWords.forEach(function(word) { if (!sugs.hasOwnProperty(word.toLowerCase())) { pushWord(word); } }); }, /** * Format text according existing suggestions. Method * should be called after all word in text were checker. */ _highlightErrors: function() { var sugs = this._suggestions, error = this.options.tmpl.spellError, token, html, outHTML = '', htmlOffset = 0, word; if(!this._tokens) { return; } for(var idx = 0; idx < this._tokens.length; ++idx) { token = this._tokens[idx]; word = token.token.toLowerCase(); token.htmlPos += htmlOffset; if (!token.isWord) { token.html = token.token.encodeHTML().replace(/(\r\n|\r|\n)/g, '
    '); } else if (sugs[word] && !sugs[word].skip && !sugs[word].correct) { token.html = error.supplant({ word: token.token, id: idx }); } else { token.html = token.token; } htmlOffset += token.html.length - token.token.length; outHTML += token.html; } this._fakeArea.html(outHTML); this._updateErrorNodes(); this._doHighlight = false; }, /** * Check if there is an error node under such position * * @param {number} x * @param {number} y * * @return {Object|null} Returns node information object or null if not found. */ _getHoveredNode: function(x,y) { var nodes = this._errorNodes; for (var i = 0; i < nodes.length; ++i) { if (x >= nodes[i].x && x <= nodes[i].x + nodes[i].w && y >= nodes[i].y && y <= nodes[i].y + nodes[i].h) { return nodes[i]; } } return null; }, /** * Reconstruct errorNodes object and place information about all * error nodes and their position on the page. */ _updateErrorNodes: function() { var nodes = this._errorNodes; nodes.length = 0; this._fakeArea.find(this.options.selectors.error) .each(function() { var node = jQuery(this), pos = node.offset(); nodes.push({ x: pos.left, y: pos.top, w: this.offsetWidth, h: this.offsetHeight, word: this.textContent || this.innerText, node: node }); }); }, /** * Split text on tokens. Only two types are supported now - words and not. * * @param {string} text Text to parse. * @return {Array.<{token: string, textPos: number, isWord: boolean}>} Array contains * descriptions of all tokens in the text. */ _tokenize: function(text) { var regex = /[a-zа-яё]+/ig, tokens = [], token, pos, lastPos = 0, textPos = 0, htmlPos = 0; var pushToken = function(token, isWord) { var tokenObj = function(textPos, htmlPos) { return { token: token, textPos: textPos, isWord: isWord }; }(textPos, htmlPos); tokens.push(tokenObj); textPos += token.length; htmlPos += token.length; }; while (token = regex.exec(text)) { pos = token.index; if (pos > lastPos ) { pushToken(text.substr(lastPos, pos - lastPos), false); } pushToken(token[0], true); lastPos = pos + token[0].length; } return tokens; }, /** * Enable livejournal spellchecker and disable native one. */ enable: function() { if (!this._enabled) { this._fakeArea.show(); this.autogrow(true); this.element .attr('spellcheck', false) .val('').val(this._prevText); this._enabled = true; this.options.enabled = true; if (this._prevText.length > 0) { this._checkSpelling(); } } $.lj.basicWidget.prototype.enable.apply(this); }, /** * Disable livejournal spellchecker and enable internal one. */ disable: function() { if (this._enabled) { this._fakeArea.hide(); this.element .attr('spellcheck', true) .val('').val(this._prevText); this._fakeArea.html(this._prevText.encodeHTML()); this._tokens = null; this._errorNodes = []; this._currentError = null; this._enabled = false; this.options.enabled = false; } $.lj.basicWidget.prototype.disable.apply(this); }, /** * Method should be called if the value of textarea was modified by script. */ update: function() { this._onInput(); }, /** * Enable or disable autogrow */ autogrow: function(enabled) { this.options.autogrow = enabled; this.element.css('overflow', enabled ? 'hidden' : 'auto'); if (enabled) { this._checkAreaHeight(); } }, /** * Method added for possibility turn on/off spellchecker * from other widgets * TODO: refactoring of subscription and event at all * from lj.editor.js, lj.commentsFromToolbar.js, etc */ subscribe: function () { var that = this; this._on('toggle', function(type, enabled) { if (type === 'spell') { that[enabled ? 'enable' : 'disable'](); } }); }, /** * Method provided for sync with forms to accept * correct initial state of spellchecker */ start: function () { var that = this; setTimeout(function () { that._fire('toggle', ['spell', that.options.enabled], true); }, 0); }, _skipWord: function (word) { if (!this._suggestions[word]) { this._suggestions[word] = {}; } this._suggestions[word].skip = true; }, _setOption: function (key, value) { if (value !== undefined) { $.lj.basicWidget.prototype._setOption.call(this, key, value); } switch (key) { case 'minHeight': this._minHeight = value; this._checkAreaHeight(); break; } } }); })(jQuery, window); ; /* file-end: js/jquery/jquery.lj.spellchecker.js ----------------------------------------------------------------------------------*/ /* --------------------------------------------------------------------------------- file-start: js/jquery/jquery.lj.commentator.js */ /** * @author dmitry.petrov@sup.com (Dmitry Petrov) * @fileoverview LiveJournal comments widget, responsible for submitting the form to the server. */ /** * @name $.lj.commentator * @requires $.ui.core, $.ui.widget * @class Widget, responsible for submitting the form to the server. * */ (function($, window) { /** @lends $.lj.commentator.prototype */ $.widget('lj.commentator', { options: { publicKey: '', ajax: true, needCaptcha: false, captchaContainerId: '', selectors: { comments: '#comments', errorWrapper: '.b-msgsystem-errorbox', errorBlock: '.b-postform-alert-ajax', blockingErrorBlocks: '.b-bubble-warning', preloaderElem: '.b-postform-preload', controls: ':button, :submit', ajaxField: 'input[name="json"]', previewControl: 'input[name="submitpreview"]', inputParentTalkid: '#parenttalkid', submitControl: 'button[name=submitpost]', form: 'form', captchaBox: '.b-postform-captchabox', //if user is anonymous and chooses auth as livejournal we should refresh page on comment //only for s1 anonLoginSubmit: '.b-watering-authtype-user.b-watering-user-notreg' }, classNames: { idle: 'b-postform-preload-active', captchaActive: 'b-postform-captchabox-active', errorWrapperShow: 'b-msgsystem-errorbox-show', replyPage: 'b-postform' //'b-watering-replypage' //old form can bee seen only on reply page }, templates: { frame: '