\" +\n `
})
` +\n `
${Thredded.escapeHtml(name)}` +\n (name !== display_name && display_name ?\n `
${Thredded.escapeHtml(display_name)}` :\n '') +\n '
';\n },\n\n searchFn({url, autocompleteMinLength}) {\n return function search(term, callback, match) {\n if (term.length < autocompleteMinLength) {\n callback([]);\n return;\n }\n const request = new XMLHttpRequest();\n request.open('GET', `${url}?q=${term}`, /* async */ true);\n request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');\n request.onload = () => {\n // Ignore errors\n if (request.status < 200 || request.status >= 400) {\n callback([]);\n return;\n }\n callback(JSON.parse(request.responseText).results.map(({avatar_url, id, display_name, name}) => {\n return {avatar_url, id, name, display_name, match};\n }));\n };\n request.send();\n }\n }\n };\n\n document.addEventListener('turbolinks:before-cache', () => {\n Array.prototype.forEach.call(\n document.getElementsByClassName(Thredded.UserTextcomplete.DROPDOWN_CLASS_NAME), (node) => {\n node.parentNode.removeChild(node);\n });\n });\n})();\n","//= require thredded/components/user_textcomplete\n\nconst ThreddedMentionAutocompletion = {\n MATCH_RE: /(^@|\\s@)\"?([\\w., \\-()]+[\\w.,\\-()])$/,\n // the last letter has to not be a space so it doesn't match after replacement\n DROPDOWN_MAX_COUNT: 6,\n\n init(form, textarea) {\n const editor = new Textcomplete.editors.Textarea(textarea);\n const textcomplete = new Textcomplete(editor, {\n dropdown: {\n className: Thredded.UserTextcomplete.DROPDOWN_CLASS_NAME,\n maxCount: ThreddedMentionAutocompletion.DROPDOWN_MAX_COUNT\n },\n });\n textcomplete.on('rendered', function() {\n if (textcomplete.dropdown.items.length) {\n textcomplete.dropdown.items[0].activate();\n }\n });\n textcomplete.register([{\n match: ThreddedMentionAutocompletion.MATCH_RE,\n search: Thredded.UserTextcomplete.searchFn({\n url: form.getAttribute('data-autocomplete-url'),\n autocompleteMinLength: parseInt(form.getAttribute('data-autocomplete-min-length'), 10)\n }),\n template: Thredded.UserTextcomplete.formatUser,\n replace ({name, match}) {\n let prefix = match[1];\n if (/[., ()]/.test(name)) {\n return `${prefix}\"${name}\" `\n } else {\n return `${prefix}${name} `\n }\n }\n }]);\n }\n};\n\nwindow.ThreddedMentionAutocompletion = ThreddedMentionAutocompletion;\n","/** https://unpkg.com/autosize@4.0.2/dist/autosize.js **/\n// Modified to always define the autosize global.\n// This allows Thredded code to be compatible with both Webpack and Sprockets.\n(function (global, factory) {\n var mod = {\n exports: {}\n };\n factory(mod, mod.exports);\n global.autosize = mod.exports;\n})(window, function (module, exports) {\n 'use strict';\n\n var map = typeof Map === \"function\" ? new Map() : function () {\n var keys = [];\n var values = [];\n\n return {\n has: function has(key) {\n return keys.indexOf(key) > -1;\n },\n get: function get(key) {\n return values[keys.indexOf(key)];\n },\n set: function set(key, value) {\n if (keys.indexOf(key) === -1) {\n keys.push(key);\n values.push(value);\n }\n },\n delete: function _delete(key) {\n var index = keys.indexOf(key);\n if (index > -1) {\n keys.splice(index, 1);\n values.splice(index, 1);\n }\n }\n };\n }();\n\n var createEvent = function createEvent(name) {\n return new Event(name, { bubbles: true });\n };\n try {\n new Event('test');\n } catch (e) {\n // IE does not support `new Event()`\n createEvent = function createEvent(name) {\n var evt = document.createEvent('Event');\n evt.initEvent(name, true, false);\n return evt;\n };\n }\n\n function assign(ta) {\n if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || map.has(ta)) return;\n\n var heightOffset = null;\n var clientWidth = null;\n var cachedHeight = null;\n\n function init() {\n var style = window.getComputedStyle(ta, null);\n\n if (style.resize === 'vertical') {\n ta.style.resize = 'none';\n } else if (style.resize === 'both') {\n ta.style.resize = 'horizontal';\n }\n\n if (style.boxSizing === 'content-box') {\n heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));\n } else {\n heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);\n }\n // Fix when a textarea is not on document body and heightOffset is Not a Number\n if (isNaN(heightOffset)) {\n heightOffset = 0;\n }\n\n update();\n }\n\n function changeOverflow(value) {\n {\n // Chrome/Safari-specific fix:\n // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space\n // made available by removing the scrollbar. The following forces the necessary text reflow.\n var width = ta.style.width;\n ta.style.width = '0px';\n // Force reflow:\n /* jshint ignore:start */\n ta.offsetWidth;\n /* jshint ignore:end */\n ta.style.width = width;\n }\n\n ta.style.overflowY = value;\n }\n\n function getParentOverflows(el) {\n var arr = [];\n\n while (el && el.parentNode && el.parentNode instanceof Element) {\n if (el.parentNode.scrollTop) {\n arr.push({\n node: el.parentNode,\n scrollTop: el.parentNode.scrollTop\n });\n }\n el = el.parentNode;\n }\n\n return arr;\n }\n\n function resize() {\n if (ta.scrollHeight === 0) {\n // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.\n return;\n }\n\n var overflows = getParentOverflows(ta);\n var docTop = document.documentElement && document.documentElement.scrollTop; // Needed for Mobile IE (ticket #240)\n\n ta.style.height = '';\n ta.style.height = ta.scrollHeight + heightOffset + 'px';\n\n // used to check if an update is actually necessary on window.resize\n clientWidth = ta.clientWidth;\n\n // prevents scroll-position jumping\n overflows.forEach(function (el) {\n el.node.scrollTop = el.scrollTop;\n });\n\n if (docTop) {\n document.documentElement.scrollTop = docTop;\n }\n }\n\n function update() {\n resize();\n\n var styleHeight = Math.round(parseFloat(ta.style.height));\n var computed = window.getComputedStyle(ta, null);\n\n // Using offsetHeight as a replacement for computed.height in IE, because IE does not account use of border-box\n var actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(computed.height)) : ta.offsetHeight;\n\n // The actual height not matching the style height (set via the resize method) indicates that\n // the max-height has been exceeded, in which case the overflow should be allowed.\n if (actualHeight < styleHeight) {\n if (computed.overflowY === 'hidden') {\n changeOverflow('scroll');\n resize();\n actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight;\n }\n } else {\n // Normally keep overflow set to hidden, to avoid flash of scrollbar as the textarea expands.\n if (computed.overflowY !== 'hidden') {\n changeOverflow('hidden');\n resize();\n actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight;\n }\n }\n\n if (cachedHeight !== actualHeight) {\n cachedHeight = actualHeight;\n var evt = createEvent('autosize:resized');\n try {\n ta.dispatchEvent(evt);\n } catch (err) {\n // Firefox will throw an error on dispatchEvent for a detached element\n // https://bugzilla.mozilla.org/show_bug.cgi?id=889376\n }\n }\n }\n\n var pageResize = function pageResize() {\n if (ta.clientWidth !== clientWidth) {\n update();\n }\n };\n\n var destroy = function (style) {\n window.removeEventListener('resize', pageResize, false);\n ta.removeEventListener('input', update, false);\n ta.removeEventListener('keyup', update, false);\n ta.removeEventListener('autosize:destroy', destroy, false);\n ta.removeEventListener('autosize:update', update, false);\n\n Object.keys(style).forEach(function (key) {\n ta.style[key] = style[key];\n });\n\n map.delete(ta);\n }.bind(ta, {\n height: ta.style.height,\n resize: ta.style.resize,\n overflowY: ta.style.overflowY,\n overflowX: ta.style.overflowX,\n wordWrap: ta.style.wordWrap\n });\n\n ta.addEventListener('autosize:destroy', destroy, false);\n\n // IE9 does not fire onpropertychange or oninput for deletions,\n // so binding to onkeyup to catch most of those events.\n // There is no way that I know of to detect something like 'cut' in IE9.\n if ('onpropertychange' in ta && 'oninput' in ta) {\n ta.addEventListener('keyup', update, false);\n }\n\n window.addEventListener('resize', pageResize, false);\n ta.addEventListener('input', update, false);\n ta.addEventListener('autosize:update', update, false);\n ta.style.overflowX = 'hidden';\n ta.style.wordWrap = 'break-word';\n\n map.set(ta, {\n destroy: destroy,\n update: update\n });\n\n init();\n }\n\n function destroy(ta) {\n var methods = map.get(ta);\n if (methods) {\n methods.destroy();\n }\n }\n\n function update(ta) {\n var methods = map.get(ta);\n if (methods) {\n methods.update();\n }\n }\n\n var autosize = null;\n\n // Do nothing in Node.js environment and IE8 (or lower)\n if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {\n autosize = function autosize(el) {\n return el;\n };\n autosize.destroy = function (el) {\n return el;\n };\n autosize.update = function (el) {\n return el;\n };\n } else {\n autosize = function autosize(el, options) {\n if (el) {\n Array.prototype.forEach.call(el.length ? el : [el], function (x) {\n return assign(x, options);\n });\n }\n return el;\n };\n autosize.destroy = function (el) {\n if (el) {\n Array.prototype.forEach.call(el.length ? el : [el], destroy);\n }\n return el;\n };\n autosize.update = function (el) {\n if (el) {\n Array.prototype.forEach.call(el.length ? el : [el], update);\n }\n return el;\n };\n }\n\n exports.default = autosize;\n module.exports = exports['default'];\n});\n","//= require thredded/core/thredded\n//= require thredded/dependencies/autosize\n//= require thredded/core/on_page_load\n//= require thredded/components/mention_autocompletion\n//= require thredded/components/preview_area\n\n(() => {\n const Thredded = window.Thredded;\n const ThreddedMentionAutocompletion = window.ThreddedMentionAutocompletion;\n const ThreddedPreviewArea = window.ThreddedPreviewArea;\n const autosize = window.autosize;\n\n const COMPONENT_SELECTOR = '[data-thredded-post-form]';\n const CONTENT_TEXTAREA_SELECTOR = 'textarea[name$=\"[content]\"]';\n\n const initPostForm = (form) => {\n const textarea = form.querySelector(CONTENT_TEXTAREA_SELECTOR);\n autosize(textarea);\n new ThreddedPreviewArea(form, textarea);\n ThreddedMentionAutocompletion.init(form, textarea);\n };\n\n const destroyPostForm = (form) => {\n autosize.destroy(form.querySelector(CONTENT_TEXTAREA_SELECTOR));\n };\n\n Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n initPostForm(node);\n });\n });\n\n document.addEventListener('turbolinks:before-cache', () => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n destroyPostForm(node);\n });\n });\n})();\n","//= require thredded/core/thredded\n//= require thredded/core/on_page_load\n//= require thredded/core/serialize_form\n\n// Makes topics in the list appear read as soon as the topic link is clicked,\n// iff the topic link leads to the last page of the topic.\n(() => {\n const Thredded = window.Thredded;\n\n const COMPONENT_SELECTOR = '[data-thredded-topics]';\n const TOPIC_UNREAD_CLASS = 'thredded--topic-unread';\n const TOPIC_READ_CLASS = 'thredded--topic-read';\n const POSTS_COUNT_SELECTOR = '.thredded--topics--posts-count';\n\n function pageNumber(url) {\n const match = url.match(/\\/page-(\\d)$/);\n return match ? +match[1] : 1;\n }\n\n function totalPages(numPosts, postsPerPage) {\n return Math.ceil(numPosts / postsPerPage);\n }\n\n function getAncestorTag(node, ancestorTagName) {\n do {\n node = node.parentNode;\n } while (node && node.tagName !== ancestorTagName);\n return node;\n }\n\n function getTopicNode(node) {\n return getAncestorTag(node, 'ARTICLE');\n }\n\n function getUnreadNavItem(unreadFollowedCountElement) {\n return getAncestorTag(unreadFollowedCountElement, 'LI');\n }\n\n function initTopicsList(topicsList) {\n const postsPerPage = +topicsList.getAttribute('data-thredded-topic-posts-per-page') || 25;\n const isPrivateTopics = topicsList.getAttribute('data-thredded-topics') === 'private';\n const unreadFollowedCountElement = document.querySelector('[data-unread-followed-count]');\n topicsList.addEventListener('click', (evt) => {\n const link = evt.target;\n if (link.tagName !== 'A' || link.parentNode.tagName !== 'H1') return;\n const topic = getTopicNode(link);\n if (pageNumber(link.href) === totalPages(+topic.querySelector(POSTS_COUNT_SELECTOR).textContent, postsPerPage)) {\n if (!isPrivateTopics && unreadFollowedCountElement &&\n topic.hasAttribute('data-followed') && topic.hasAttribute('data-unread')) {\n const count = (+unreadFollowedCountElement.textContent) - 1;\n if (count === 0) {\n const navItem = getUnreadNavItem(unreadFollowedCountElement);\n navItem.parentElement.removeChild(navItem);\n } else {\n unreadFollowedCountElement.textContent = count.toLocaleString();\n }\n }\n topic.classList.add(TOPIC_READ_CLASS);\n topic.classList.remove(TOPIC_UNREAD_CLASS);\n topic.removeAttribute('data-unread');\n }\n });\n }\n\n Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), initTopicsList);\n });\n})();\n","//= require thredded/core/thredded\n(function() {\n const Thredded = window.Thredded;\n Thredded.isSubmitHotkey = (evt) => {\n // Ctrl+Enter.\n return evt.ctrlKey && (evt.keyCode === 13 || evt.keyCode === 10 /* http://crbug.com/79407 */);\n };\n\n document.addEventListener('keypress', (evt) => {\n if (Thredded.isSubmitHotkey(evt)) {\n const submitButton = document.querySelector('[data-thredded-submit-hotkey] [type=\"submit\"]');\n if (!submitButton) return;\n evt.preventDefault();\n // Focus first for better visual feedback.\n submitButton.focus();\n submitButton.click();\n }\n });\n})();\n","//= require thredded/core/thredded\n//= require thredded/core/on_page_load\n\n(() => {\n const Thredded = window.Thredded;\n\n const COMPONENT_SELECTOR = '[data-thredded-currently-online]';\n const EXPANDED_CLASS = 'thredded--is-expanded';\n\n const handleMouseEnter = (evt) => {\n evt.target.classList.add(EXPANDED_CLASS);\n };\n\n const handleMouseLeave = (evt) => {\n evt.target.classList.remove(EXPANDED_CLASS);\n };\n\n const handleTouchStart = (evt) => {\n evt.target.classList.toggle(EXPANDED_CLASS);\n };\n\n const initCurrentlyOnline = (node) => {\n node.addEventListener('mouseenter', handleMouseEnter);\n node.addEventListener('mouseleave', handleMouseLeave);\n node.addEventListener('touchstart', handleTouchStart);\n };\n\n Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n initCurrentlyOnline(node);\n });\n });\n})();\n","//= require thredded/core/thredded\n//= require thredded/core/on_page_load\n\n(function() {\n const Thredded = window.Thredded;\n\n Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll('[data-thredded-quote-post]'), (el) => {\n el.addEventListener('click', onClick);\n });\n });\n\n function onClick(evt) {\n // Handle only left clicks with no modifier keys\n if (evt.button !== 0 || evt.ctrlKey || evt.altKey || evt.metaKey || evt.shiftKey) return;\n evt.preventDefault();\n const target = document.getElementById('post_content');\n target.scrollIntoView();\n target.value = '...';\n fetchReply(evt.target.getAttribute('data-thredded-quote-post'), (replyText) => {\n if (!target.ownerDocument.body.contains(target)) return;\n target.focus();\n target.value = replyText;\n\n const autosizeUpdateEvent = document.createEvent('Event');\n autosizeUpdateEvent.initEvent('autosize:update', true, false);\n target.dispatchEvent(autosizeUpdateEvent);\n // Scroll into view again as the size might have changed.\n target.scrollIntoView();\n }, (errorMessage) => {\n target.value = errorMessage;\n });\n }\n\n function fetchReply(url, onSuccess, onError) {\n const request = new XMLHttpRequest();\n request.open('GET', url, /* async */ true);\n request.onload = () => {\n if (request.status >= 200 && request.status < 400) {\n onSuccess(request.responseText);\n } else {\n onError(`Error (${request.status}): ${request.statusText} ${request.responseText}`);\n }\n };\n request.onerror = () => {\n onError('Network Error');\n };\n request.send();\n }\n})();\n","//= require thredded/core/thredded\n(() => {\n const COMPONENT_SELECTOR = '#thredded--container [data-time-ago]';\n const Thredded = window.Thredded;\n if ('timeago' in window) {\n const timeago = window.timeago;\n Thredded.onPageLoad(() => {\n const threddedContainer = document.querySelector('#thredded--container');\n if (!threddedContainer) return;\n timeago().render(\n document.querySelectorAll(COMPONENT_SELECTOR),\n threddedContainer.getAttribute('data-thredded-locale').replace('-', '_'));\n });\n document.addEventListener('turbolinks:before-cache', () => {\n timeago.cancel();\n });\n } else if ('jQuery' in window && 'timeago' in jQuery.fn) {\n const $ = window.jQuery;\n Thredded.onPageLoad(() => {\n const allowFutureWas = $.timeago.settings.allowFuture;\n $.timeago.settings.allowFuture = true;\n $(COMPONENT_SELECTOR).timeago();\n $.timeago.settings.allowFuture = allowFutureWas;\n });\n }\n})();\n","(() => {\n const COMPONENT_SELECTOR = '[data-thredded-flash-message]';\n\n document.addEventListener('turbolinks:before-cache', () => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n node.parentNode.removeChild(node);\n });\n });\n})();\n","//= require thredded/core/thredded\n//= require thredded/core/on_page_load\n\n// Reflects the logic of user preference settings by enabling/disabling certain inputs.\n(() => {\n const Thredded = window.Thredded;\n\n const COMPONENT_SELECTOR = '[data-thredded-user-preferences-form]';\n const BOUND_MESSAGEBOARD_NAME = 'data-thredded-bound-messageboard-pref';\n const UPDATE_ON_CHANGE_NAME = 'data-thredded-update-checkbox-on-change';\n\n class MessageboardPreferenceBinding {\n constructor(form, genericCheckboxName, messageboardCheckboxName) {\n this.messageboardCheckbox = form.querySelector(`[type=\"checkbox\"][name=\"${messageboardCheckboxName}\"]`);\n if (!this.messageboardCheckbox) {\n return;\n }\n this.messageboardCheckbox.addEventListener('change', () => {\n this.rememberMessageboardChecked();\n });\n this.rememberMessageboardChecked();\n\n this.genericCheckbox = form.querySelector(`[type=\"checkbox\"][name=\"${genericCheckboxName}\"]`);\n this.genericCheckbox.addEventListener('change', () => {\n this.updateMessageboardCheckbox();\n });\n this.updateMessageboardCheckbox();\n }\n\n rememberMessageboardChecked() {\n this.messageboardCheckedWas = this.messageboardCheckbox.checked;\n }\n\n updateMessageboardCheckbox() {\n const enabled = this.genericCheckbox.checked;\n this.messageboardCheckbox.disabled = !enabled;\n this.messageboardCheckbox.checked = enabled ? this.messageboardCheckedWas : false;\n }\n }\n\n class UpdateOnChange {\n constructor(form, sourceElement, targetName) {\n const target = form.querySelector(`[type=\"checkbox\"][name=\"${targetName}\"]`);\n if (!target) return;\n sourceElement.addEventListener('change', () => {\n target.checked = sourceElement.checked;\n });\n }\n }\n\n class UserPreferencesForm {\n constructor(form) {\n Array.prototype.forEach.call(form.querySelectorAll(`input[${BOUND_MESSAGEBOARD_NAME}]`), (element) => {\n new MessageboardPreferenceBinding(form, element.name, element.getAttribute(BOUND_MESSAGEBOARD_NAME));\n });\n Array.prototype.forEach.call(form.querySelectorAll(`input[${UPDATE_ON_CHANGE_NAME}]`), (element) => {\n new UpdateOnChange(form, element, element.getAttribute(UPDATE_ON_CHANGE_NAME));\n });\n }\n }\n\n Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (form) => {\n new UserPreferencesForm(form);\n });\n });\n})();\n","//= require thredded/core/thredded\n//= require thredded/core/on_page_load\n//= require thredded/core/serialize_form\n\n// Submit GET forms with turbolinks\n(() => {\n const Thredded = window.Thredded;\n const Turbolinks = window.Turbolinks;\n\n const handleSubmit = (evt) => {\n evt.preventDefault();\n const form = evt.currentTarget;\n Turbolinks.visit(form.action + (form.action.indexOf('?') === -1 ? '?' : '&') + Thredded.serializeForm(form));\n\n // On mobile the soft keyboard doesn't won't go away after the submit since we're submitting with\n // Turbolinks. Hide it:\n Thredded.hideSoftKeyboard();\n };\n\n Thredded.onPageLoad(() => {\n if (!Turbolinks || !Turbolinks.supported) return;\n Array.prototype.forEach.call(document.querySelectorAll('[data-thredded-turboform]'), (form) => {\n form.addEventListener('submit', handleSubmit);\n });\n });\n})();\n","//= require thredded/core/thredded\n//= require thredded/core/on_page_load\n//= require thredded/components/user_textcomplete\n//= require thredded/dependencies/autosize\n\n(() => {\n const Thredded = window.Thredded;\n const autosize = window.autosize;\n\n const COMPONENT_SELECTOR = '[data-thredded-users-select]';\n\n Thredded.UsersSelect = {\n DROPDOWN_MAX_COUNT: 6,\n };\n\n function parseNames(text) {\n const result = [];\n let current = [];\n let currentIndex = 0;\n let inQuoted = false;\n let inName = false;\n for (let i = 0; i < text.length; ++i) {\n const char = text.charAt(i);\n switch (char) {\n case '\"':\n inQuoted = !inQuoted;\n break;\n case ' ':\n if (inName) current.push(char);\n break;\n case ',':\n if (inQuoted) {\n current.push(char);\n } else {\n inName = false;\n if (current.length) {\n result.push({name: current.join(''), index: currentIndex});\n current.length = 0;\n }\n }\n break;\n default:\n if (!inName) currentIndex = i;\n inName = true;\n current.push(char);\n }\n }\n if (current.length) result.current = {name: current.join(''), index: currentIndex};\n return result;\n }\n\n const initUsersSelect = (textarea) => {\n autosize(textarea);\n // Prevent multiple lines\n textarea.addEventListener('keypress', (evt) => {\n if (evt.keyCode === 13 || evt.keyCode === 10) {\n evt.preventDefault()\n }\n });\n const editor = new Textcomplete.editors.Textarea(textarea);\n const textcomplete = new Textcomplete(editor, {\n dropdown: {\n className: Thredded.UserTextcomplete.DROPDOWN_CLASS_NAME,\n maxCount: Thredded.UsersSelect.DROPDOWN_MAX_COUNT,\n },\n });\n textarea.addEventListener('blur', (evt) => {\n textcomplete.hide();\n });\n\n const searchFn = Thredded.UserTextcomplete.searchFn({\n url: textarea.getAttribute('data-autocomplete-url'),\n autocompleteMinLength: parseInt(textarea.getAttribute('data-autocomplete-min-length'), 10)\n });\n textcomplete.on('rendered', function() {\n if (textcomplete.dropdown.items.length) {\n textcomplete.dropdown.items[0].activate();\n }\n });\n textcomplete.register([{\n index: 0,\n match: (text) => {\n const names = parseNames(text);\n if (names.current) {\n const {name, index} = names.current;\n const matchData = [name];\n matchData.index = index;\n return matchData;\n } else {\n return null;\n }\n },\n search (term, callback, match) {\n searchFn(term, function(results) {\n const names = parseNames(textarea.value).map(({name}) => name);\n callback(results.filter((result) => names.indexOf(result.name) === -1));\n }, match);\n },\n template: Thredded.UserTextcomplete.formatUser,\n replace ({name}) {\n if (/,/.test(name)) {\n return `\"${name}\", `\n } else {\n return `${name}, `\n }\n }\n }]);\n };\n\n function destroyUsersSelect(textarea) {\n autosize.destroy(textarea);\n }\n\n window.Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n initUsersSelect(node);\n });\n });\n\n document.addEventListener('turbolinks:before-cache', () => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n destroyUsersSelect(node);\n });\n });\n\n})();\n","//= require thredded/core/thredded\n//= require thredded/dependencies/autosize\n//= require thredded/core/on_page_load\n//= require thredded/components/mention_autocompletion\n//= require thredded/components/preview_area\n\n(() => {\n const Thredded = window.Thredded;\n const ThreddedMentionAutocompletion = window.ThreddedMentionAutocompletion;\n const ThreddedPreviewArea = window.ThreddedPreviewArea;\n const autosize = window.autosize;\n\n const COMPONENT_SELECTOR = '[data-thredded-topic-form]';\n const TITLE_SELECTOR = '[name$=\"topic[title]\"]';\n const CONTENT_TEXTAREA_SELECTOR = 'textarea[name$=\"[content]\"]';\n const COMPACT_CLASS = 'thredded--is-compact';\n const EXPANDED_CLASS = 'thredded--is-expanded';\n const ESCAPE_KEY_CODE = 27;\n\n const initTopicForm = (form) => {\n const textarea = form.querySelector(CONTENT_TEXTAREA_SELECTOR);\n if (!textarea) {\n return;\n }\n autosize(textarea);\n new ThreddedPreviewArea(form, textarea);\n ThreddedMentionAutocompletion.init(form, textarea);\n\n if (!form.classList.contains(COMPACT_CLASS)) {\n return;\n }\n\n const title = form.querySelector(TITLE_SELECTOR);\n title.addEventListener('focus', () => {\n toggleExpanded(form, true);\n });\n\n [title, textarea].forEach((node) => {\n // Un-expand on Escape key.\n node.addEventListener('keydown', (evt) => {\n if (evt.keyCode === ESCAPE_KEY_CODE) {\n evt.target.blur();\n toggleExpanded(form, false);\n }\n });\n\n // Un-expand on blur if the new focus element is outside of the same form and\n // all the form inputs are empty.\n node.addEventListener('blur', () => {\n // This listener will be fired right after the blur event has finished.\n const listener = (evt) => {\n if (!form.contains(evt.target) && !title.value && !textarea.value) {\n toggleExpanded(form, false);\n }\n document.body.removeEventListener('touchend', listener);\n document.body.removeEventListener('mouseup', listener);\n };\n document.body.addEventListener('mouseup', listener);\n document.body.addEventListener('touchend', listener);\n })\n });\n };\n\n const toggleExpanded = (form, expand) => {\n if (expand) {\n form.classList.remove(COMPACT_CLASS);\n form.classList.add(EXPANDED_CLASS);\n } else {\n form.classList.remove(EXPANDED_CLASS);\n form.classList.add(COMPACT_CLASS);\n }\n };\n\n const destroyTopicForm = (form) => {\n const textarea = form.querySelector(CONTENT_TEXTAREA_SELECTOR);\n if (!textarea) {\n return;\n }\n autosize.destroy(textarea);\n };\n\n Thredded.onPageLoad(() => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n initTopicForm(node);\n });\n });\n\n document.addEventListener('turbolinks:before-cache', () => {\n Array.prototype.forEach.call(document.querySelectorAll(COMPONENT_SELECTOR), (node) => {\n destroyTopicForm(node);\n });\n });\n})();\n\n\n","//= require thredded/core/thredded\n\nwindow.Thredded.hideSoftKeyboard = () => {\n const activeElement = document.activeElement;\n if (!activeElement || !activeElement.blur) return;\n activeElement.blur();\n};\n","//= require thredded/core/on_page_load\n\nwindow.Thredded.onPageLoad(() => {\n if ('Rails' in window) {\n window.Rails.refreshCSRFTokens();\n } else if ('jQuery' in window && 'rails' in window.jQuery) {\n window.jQuery.rails.refreshCSRFTokens();\n }\n});\n","/**\n * Copyright (c) 2016 hustcc\n * License: MIT\n * Version: v3.0.2\n * https://github.com/hustcc/timeago.js\n**/\n/* jshint expr: true */\n!function (root, factory) {\n root.timeago = factory(root);\n}(window,\nfunction () {\n var indexMapEn = 'second_minute_hour_day_week_month_year'.split('_'),\n indexMapZh = '秒_分钟_小时_天_周_月_年'.split('_'),\n // build-in locales: en & zh_CN\n locales = {\n 'en': function(number, index) {\n if (index === 0) return ['just now', 'right now'];\n var unit = indexMapEn[parseInt(index / 2)];\n if (number > 1) unit += 's';\n return [number + ' ' + unit + ' ago', 'in ' + number + ' ' + unit];\n },\n 'zh_CN': function(number, index) {\n if (index === 0) return ['刚刚', '片刻后'];\n var unit = indexMapZh[parseInt(index / 2)];\n return [number + unit + '前', number + unit + '后'];\n }\n },\n // second, minute, hour, day, week, month, year(365 days)\n SEC_ARRAY = [60, 60, 24, 7, 365/7/12, 12],\n SEC_ARRAY_LEN = 6,\n // ATTR_DATETIME = 'datetime',\n ATTR_DATA_TID = 'data-tid',\n timers = {}; // real-time render timers\n\n // format Date / string / timestamp to Date instance.\n function toDate(input) {\n if (input instanceof Date) return input;\n if (!isNaN(input)) return new Date(toInt(input));\n if (/^\\d+$/.test(input)) return new Date(toInt(input));\n input = (input || '').trim().replace(/\\.\\d+/, '') // remove milliseconds\n .replace(/-/, '/').replace(/-/, '/')\n .replace(/(\\d)T(\\d)/, '$1 $2').replace(/Z/, ' UTC') // 2017-2-5T3:57:52Z -> 2017-2-5 3:57:52UTC\n .replace(/([\\+\\-]\\d\\d)\\:?(\\d\\d)/, ' $1$2'); // -04:00 -> -0400\n return new Date(input);\n }\n // change f into int, remove decimal. Just for code compression\n function toInt(f) {\n return parseInt(f);\n }\n // format the diff second to *** time ago, with setting locale\n function formatDiff(diff, locale, defaultLocale) {\n // if locale is not exist, use defaultLocale.\n // if defaultLocale is not exist, use build-in `en`.\n // be sure of no error when locale is not exist.\n locale = locales[locale] ? locale : (locales[defaultLocale] ? defaultLocale : 'en');\n // if (! locales[locale]) locale = defaultLocale;\n var i = 0,\n agoin = diff < 0 ? 1 : 0, // timein or timeago\n total_sec = diff = Math.abs(diff);\n\n for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {\n diff /= SEC_ARRAY[i];\n }\n diff = toInt(diff);\n i *= 2;\n\n if (diff > (i === 0 ? 9 : 1)) i += 1;\n return locales[locale](diff, i, total_sec)[agoin].replace('%s', diff);\n }\n // calculate the diff second between date to be formated an now date.\n function diffSec(date, nowDate) {\n nowDate = nowDate ? toDate(nowDate) : new Date();\n return (nowDate - toDate(date)) / 1000;\n }\n /**\n * nextInterval: calculate the next interval time.\n * - diff: the diff sec between now and date to be formated.\n *\n * What's the meaning?\n * diff = 61 then return 59\n * diff = 3601 (an hour + 1 second), then return 3599\n * make the interval with high performace.\n **/\n function nextInterval(diff) {\n var rst = 1, i = 0, d = Math.abs(diff);\n for (; diff >= SEC_ARRAY[i] && i < SEC_ARRAY_LEN; i++) {\n diff /= SEC_ARRAY[i];\n rst *= SEC_ARRAY[i];\n }\n // return leftSec(d, rst);\n d = d % rst;\n d = d ? rst - d : rst;\n return Math.ceil(d);\n }\n // get the datetime attribute, `data-timeagp` / `datetime` are supported.\n function getDateAttr(node) {\n return getAttr(node, 'data-timeago') || getAttr(node, 'datetime');\n }\n // get the node attribute, native DOM and jquery supported.\n function getAttr(node, name) {\n if(node.getAttribute) return node.getAttribute(name); // native\n if(node.attr) return node.attr(name); // jquery\n }\n // set the node attribute, native DOM and jquery supported.\n function setTidAttr(node, val) {\n if(node.setAttribute) return node.setAttribute(ATTR_DATA_TID, val); // native\n if(node.attr) return node.attr(ATTR_DATA_TID, val); // jquery\n }\n // get the timer id of node.\n // remove the function, can save some bytes.\n // function getTidFromNode(node) {\n // return getAttr(node, ATTR_DATA_TID);\n // }\n /**\n * timeago: the function to get `timeago` instance.\n * - nowDate: the relative date, default is new Date().\n * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.\n *\n * How to use it?\n * var timeagoLib = require('timeago.js');\n * var timeago = timeagoLib(); // all use default.\n * var timeago = timeagoLib('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.\n * var timeago = timeagoLib(null, 'zh_CN'); // set default locale is `zh_CN`.\n * var timeago = timeagoLib('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.\n **/\n function Timeago(nowDate, defaultLocale) {\n this.nowDate = nowDate;\n // if do not set the defaultLocale, set it with `en`\n this.defaultLocale = defaultLocale || 'en'; // use default build-in locale\n // for dev test\n // this.nextInterval = nextInterval;\n }\n // what the timer will do\n Timeago.prototype.doRender = function(node, date, locale) {\n var diff = diffSec(date, this.nowDate),\n self = this,\n tid;\n // delete previously assigned timeout's id to node\n node.innerHTML = formatDiff(diff, locale, this.defaultLocale);\n // waiting %s seconds, do the next render\n timers[tid = setTimeout(function() {\n self.doRender(node, date, locale);\n delete timers[tid];\n }, Math.min(nextInterval(diff) * 1000, 0x7FFFFFFF))] = 0; // there is no need to save node in object.\n // set attribute date-tid\n setTidAttr(node, tid);\n };\n /**\n * format: format the date to *** time ago, with setting or default locale\n * - date: the date / string / timestamp to be formated\n * - locale: the formated string's locale name, e.g. en / zh_CN\n *\n * How to use it?\n * var timeago = require('timeago.js')();\n * timeago.format(new Date(), 'pl'); // Date instance\n * timeago.format('2016-09-10', 'fr'); // formated date string\n * timeago.format(1473473400269); // timestamp with ms\n **/\n Timeago.prototype.format = function(date, locale) {\n return formatDiff(diffSec(date, this.nowDate), locale, this.defaultLocale);\n };\n /**\n * render: render the DOM real-time.\n * - nodes: which nodes will be rendered.\n * - locale: the locale name used to format date.\n *\n * How to use it?\n * var timeago = require('timeago.js')();\n * // 1. javascript selector\n * timeago.render(document.querySelectorAll('.need_to_be_rendered'));\n * // 2. use jQuery selector\n * timeago.render($('.need_to_be_rendered'), 'pl');\n *\n * Notice: please be sure the dom has attribute `datetime`.\n **/\n Timeago.prototype.render = function(nodes, locale) {\n if (nodes.length === undefined) nodes = [nodes];\n for (var i = 0, len = nodes.length; i < len; i++) {\n this.doRender(nodes[i], getDateAttr(nodes[i]), locale); // render item\n }\n };\n /**\n * setLocale: set the default locale name.\n *\n * How to use it?\n * var timeago = require('timeago.js')();\n * timeago.setLocale('fr');\n **/\n Timeago.prototype.setLocale = function(locale) {\n this.defaultLocale = locale;\n };\n /**\n * timeago: the function to get `timeago` instance.\n * - nowDate: the relative date, default is new Date().\n * - defaultLocale: the default locale, default is en. if your set it, then the `locale` parameter of format is not needed of you.\n *\n * How to use it?\n * var timeagoFactory = require('timeago.js');\n * var timeago = timeagoFactory(); // all use default.\n * var timeago = timeagoFactory('2016-09-10'); // the relative date is 2016-09-10, so the 2016-09-11 will be 1 day ago.\n * var timeago = timeagoFactory(null, 'zh_CN'); // set default locale is `zh_CN`.\n * var timeago = timeagoFactory('2016-09-10', 'zh_CN'); // the relative date is 2016-09-10, and locale is zh_CN, so the 2016-09-11 will be 1天前.\n **/\n function timeagoFactory(nowDate, defaultLocale) {\n return new Timeago(nowDate, defaultLocale);\n }\n /**\n * register: register a new language locale\n * - locale: locale name, e.g. en / zh_CN, notice the standard.\n * - localeFunc: the locale process function\n *\n * How to use it?\n * var timeagoFactory = require('timeago.js');\n *\n * timeagoFactory.register('the locale name', the_locale_func);\n * // or\n * timeagoFactory.register('pl', require('timeago.js/locales/pl'));\n **/\n timeagoFactory.register = function(locale, localeFunc) {\n locales[locale] = localeFunc;\n };\n\n /**\n * cancel: cancels one or all the timers which are doing real-time render.\n *\n * How to use it?\n * For canceling all the timers:\n * var timeagoFactory = require('timeago.js');\n * var timeago = timeagoFactory();\n * timeago.render(document.querySelectorAll('.need_to_be_rendered'));\n * timeagoFactory.cancel(); // will stop all the timers, stop render in real time.\n *\n * For canceling single timer on specific node:\n * var timeagoFactory = require('timeago.js');\n * var timeago = timeagoFactory();\n * var nodes = document.querySelectorAll('.need_to_be_rendered');\n * timeago.render(nodes);\n * timeagoFactory.cancel(nodes[0]); // will clear a timer attached to the first node, stop render in real time.\n **/\n timeagoFactory.cancel = function(node) {\n var tid;\n // assigning in if statement to save space\n if (node) {\n tid = getAttr(node, ATTR_DATA_TID); // get the timer of DOM node(native / jq).\n if (tid) {\n clearTimeout(tid);\n delete timers[tid];\n }\n } else {\n for (tid in timers) clearTimeout(tid);\n timers = {};\n }\n };\n\n return timeagoFactory;\n});\n"],"sourceRoot":""}