/* jshint browser: true */ (function () { // The properties that we copy into a mirrored div. // Note that some browsers, such as Firefox, // do not concatenate properties, i.e. padding-top, bottom etc. -> padding, // so we have to do every single property specifically. var properties = [ 'direction', // RTL support 'boxSizing', 'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does 'height', 'overflowX', 'overflowY', // copy the scrollbar for IE 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', 'borderStyle', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', // https://developer.mozilla.org/en-US/docs/Web/CSS/font 'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch', 'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily', 'textAlign', 'textTransform', 'textIndent', 'textDecoration', // might not make a difference, but better be safe 'letterSpacing', 'wordSpacing', 'tabSize', 'MozTabSize' ]; var isBrowser = (typeof window !== 'undefined'); var isFirefox = (isBrowser && window.mozInnerScreenX != null); function getCaretCoordinates(element, position, options) { if(!isBrowser) { throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser'); } var debug = options && options.debug || false; if (debug) { var el = document.querySelector('#input-textarea-caret-position-mirror-div'); if ( el ) { el.parentNode.removeChild(el); } } // mirrored div var div = document.createElement('div'); div.id = 'input-textarea-caret-position-mirror-div'; document.body.appendChild(div); var style = div.style; var computed = window.getComputedStyle? getComputedStyle(element) : element.currentStyle; // currentStyle for IE < 9 // default textarea styles style.whiteSpace = 'pre-wrap'; if (element.nodeName !== 'INPUT') style.wordWrap = 'break-word'; // only for textarea-s // position off-screen style.position = 'absolute'; // required to return coordinates properly if (!debug) style.visibility = 'hidden'; // not 'display: none' because we want rendering // transfer the element's properties to the div properties.forEach(function (prop) { style[prop] = computed[prop]; }); if (isFirefox) { // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 if (element.scrollHeight > parseInt(computed.height)) style.overflowY = 'scroll'; } else { style.overflow = 'hidden'; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' } div.textContent = element.value.substring(0, position); // the second special handling for input type="text" vs textarea: spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 if (element.nodeName === 'INPUT') div.textContent = div.textContent.replace(/\s/g, '\u00a0'); var span = document.createElement('span'); // Wrapping must be replicated *exactly*, including when a long word gets // onto the next line, with whitespace at the end of the line before (#7). // The *only* reliable way to do that is to copy the *entire* rest of the // textarea's content into the created at the caret position. // for inputs, just '.' would be enough, but why bother? span.textContent = element.value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all div.appendChild(span); var coordinates = { top: span.offsetTop + parseInt(computed['borderTopWidth']), left: span.offsetLeft + parseInt(computed['borderLeftWidth']) }; if (debug) { span.style.backgroundColor = '#aaa'; } else { document.body.removeChild(div); } return coordinates; } if (typeof module != 'undefined' && typeof module.exports != 'undefined') { module.exports = getCaretCoordinates; } else if(isBrowser){ window.getCaretCoordinates = getCaretCoordinates; } }()); ; 'use strict'; System.register('flarum/mentions/addComposerAutocomplete', ['flarum/extend', 'flarum/components/ComposerBody', 'flarum/helpers/avatar', 'flarum/helpers/username', 'flarum/helpers/highlight', 'flarum/utils/KeyboardNavigatable', 'flarum/utils/string', 'flarum/mentions/components/AutocompleteDropdown'], function (_export, _context) { "use strict"; var extend, ComposerBody, avatar, usernameHelper, highlight, KeyboardNavigatable, truncate, AutocompleteDropdown; function addComposerAutocomplete() { extend(ComposerBody.prototype, 'config', function (original, isInitialized) { if (isInitialized) return; var composer = this; var $container = $('
'); var dropdown = new AutocompleteDropdown({ items: [] }); var $textarea = this.$('textarea').wrap('
'); var searched = []; var mentionStart = void 0; var typed = void 0; var searchTimeout = void 0; var applySuggestion = function applySuggestion(replacement) { var insert = replacement + ' '; var content = composer.content(); composer.editor.setValue(content.substring(0, mentionStart - 1) + insert + content.substr($textarea[0].selectionStart)); var index = mentionStart - 1 + insert.length; composer.editor.setSelectionRange(index, index); dropdown.hide(); }; this.navigator = new KeyboardNavigatable(); this.navigator.when(function () { return dropdown.active; }).onUp(function () { return dropdown.navigate(-1); }).onDown(function () { return dropdown.navigate(1); }).onSelect(dropdown.complete.bind(dropdown)).onCancel(dropdown.hide.bind(dropdown)).bindTo($textarea); $textarea.after($container).on('click keyup', function (e) { var _this = this; // Up, down, enter, tab, escape, left, right. if ([9, 13, 27, 40, 38, 37, 39].indexOf(e.which) !== -1) return; var cursor = this.selectionStart; if (this.selectionEnd - cursor > 0) return; // Search backwards from the cursor for an '@' symbol. If we find one, // we will want to show the autocomplete dropdown! var value = this.value; mentionStart = 0; for (var i = cursor - 1; i >= cursor - 30; i--) { var character = value.substr(i, 1); if (character === '@') { mentionStart = i + 1; break; } } dropdown.hide(); dropdown.active = false; if (mentionStart) { typed = value.substring(mentionStart, cursor).toLowerCase(); var makeSuggestion = function makeSuggestion(user, replacement, content) { var className = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : ''; var username = usernameHelper(user); if (typed) { username.children[0] = highlight(username.children[0], typed); } return m( 'button', { className: 'PostPreview ' + className, onclick: function onclick() { return applySuggestion(replacement); }, onmouseenter: function onmouseenter() { dropdown.setIndex($(this).parent().index()); } }, m( 'span', { className: 'PostPreview-content' }, avatar(user), username, ' ', ' ', content ) ); }; var userMatches = function userMatches(user) { var names = [user.username(), user.displayName()]; return names.some(function (value) { return value.toLowerCase().substr(0, typed.length) === typed; }); }; var buildSuggestions = function buildSuggestions() { var suggestions = []; // If the user has started to type a username, then suggest users // matching that username. if (typed) { app.store.all('users').forEach(function (user) { if (!userMatches(user)) return; suggestions.push(makeSuggestion(user, '@' + user.username(), '', 'MentionsDropdown-user')); }); } // If the user is replying to a discussion, or if they are editing a // post, then we can suggest other posts in the discussion to mention. // We will add the 5 most recent comments in the discussion which // match any username characters that have been typed. var composerPost = composer.props.post; var discussion = composerPost && composerPost.discussion() || composer.props.discussion; if (discussion) { discussion.posts().filter(function (post) { return post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()); }).sort(function (a, b) { return b.time() - a.time(); }).filter(function (post) { var user = post.user(); return user && userMatches(user); }).splice(0, 5).forEach(function (post) { var user = post.user(); suggestions.push(makeSuggestion(user, '@' + user.username() + '#' + post.id(), [app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: post.number() }), ' — ', truncate(post.contentPlain(), 200)], 'MentionsDropdown-post')); }); } if (suggestions.length) { dropdown.props.items = suggestions; m.render($container[0], dropdown.render()); dropdown.show(); var coordinates = getCaretCoordinates(_this, mentionStart); var width = dropdown.$().outerWidth(); var height = dropdown.$().outerHeight(); var parent = dropdown.$().offsetParent(); var left = coordinates.left; var top = coordinates.top + 15; if (top + height > parent.height()) { top = coordinates.top - height - 15; } if (left + width > parent.width()) { left = parent.width() - width; } dropdown.show(left, top); } else { dropdown.active = false; dropdown.hide(); } }; dropdown.active = true; buildSuggestions(); dropdown.setIndex(0); dropdown.$().scrollTop(0); clearTimeout(searchTimeout); if (typed) { searchTimeout = setTimeout(function () { var typedLower = typed.toLowerCase(); if (searched.indexOf(typedLower) === -1) { app.store.find('users', { q: typed, page: { limit: 5 } }).then(function () { if (dropdown.active) buildSuggestions(); }); searched.push(typedLower); } }, 250); } } }); }); } _export('default', addComposerAutocomplete); return { setters: [function (_flarumExtend) { extend = _flarumExtend.extend; }, function (_flarumComponentsComposerBody) { ComposerBody = _flarumComponentsComposerBody.default; }, function (_flarumHelpersAvatar) { avatar = _flarumHelpersAvatar.default; }, function (_flarumHelpersUsername) { usernameHelper = _flarumHelpersUsername.default; }, function (_flarumHelpersHighlight) { highlight = _flarumHelpersHighlight.default; }, function (_flarumUtilsKeyboardNavigatable) { KeyboardNavigatable = _flarumUtilsKeyboardNavigatable.default; }, function (_flarumUtilsString) { truncate = _flarumUtilsString.truncate; }, function (_flarumMentionsComponentsAutocompleteDropdown) { AutocompleteDropdown = _flarumMentionsComponentsAutocompleteDropdown.default; }], execute: function () {} }; });; 'use strict'; System.register('flarum/mentions/addMentionedByList', ['flarum/extend', 'flarum/Model', 'flarum/models/Post', 'flarum/components/CommentPost', 'flarum/components/PostPreview', 'flarum/helpers/punctuateSeries', 'flarum/helpers/username', 'flarum/helpers/icon'], function (_export, _context) { "use strict"; var extend, Model, Post, CommentPost, PostPreview, punctuateSeries, username, icon; function addMentionedByList() { Post.prototype.mentionedBy = Model.hasMany('mentionedBy'); extend(CommentPost.prototype, 'footerItems', function (items) { var _this = this; var post = this.props.post; var replies = post.mentionedBy(); if (replies && replies.length) { // If there is only one reply, and it's adjacent to this post, we don't // really need to show the list. if (replies.length === 1 && replies[0].number() === post.number() + 1) { return; } var hidePreview = function hidePreview() { _this.$('.Post-mentionedBy-preview').removeClass('in').one('transitionend', function () { $(this).hide(); }); }; var config = function config(element, isInitialized) { if (isInitialized) return; var $this = $(element); var timeout = void 0; var $preview = $('