diff --git a/extensions/mentions/.deploy.enc b/extensions/mentions/.deploy.enc new file mode 100644 index 000000000..addfced11 Binary files /dev/null and b/extensions/mentions/.deploy.enc differ diff --git a/extensions/mentions/.gitattributes b/extensions/mentions/.gitattributes index 053481a3e..16ac2892f 100644 --- a/extensions/mentions/.gitattributes +++ b/extensions/mentions/.gitattributes @@ -2,4 +2,4 @@ .gitignore export-ignore .travis.yml export-ignore -js/*/dist/*.js -diff +js/dist/* -diff diff --git a/extensions/mentions/.gitignore b/extensions/mentions/.gitignore index 43eeee7fe..7f43257e7 100644 --- a/extensions/mentions/.gitignore +++ b/extensions/mentions/.gitignore @@ -2,5 +2,5 @@ composer.phar .DS_Store Thumbs.db -bower_components -node_modules \ No newline at end of file +node_modules +js/dist/* diff --git a/extensions/mentions/.travis.yml b/extensions/mentions/.travis.yml new file mode 100644 index 000000000..79eacd253 --- /dev/null +++ b/extensions/mentions/.travis.yml @@ -0,0 +1,15 @@ +language: minimal + +sudo: false + +cache: + directories: + - $HOME/.npm + +jobs: + include: + - stage: build + if: branch = master AND type = push + script: curl -s https://raw.githubusercontent.com/flarum/core/master/.travis/build.sh | bash -s - + -k $encrypted_e5bd9d54e015_key + -i $encrypted_e5bd9d54e015_iv diff --git a/extensions/mentions/bootstrap.php b/extensions/mentions/bootstrap.php index 67b38fc97..b20093c4e 100644 --- a/extensions/mentions/bootstrap.php +++ b/extensions/mentions/bootstrap.php @@ -16,9 +16,9 @@ use Illuminate\Contracts\View\Factory; return [ (new Extend\Assets('forum')) - ->asset(__DIR__.'/js/forum/dist/extension.js') - ->asset(__DIR__.'/less/forum/extension.less') - ->bootstrapper('flarum/mentions/main'), + ->js(__DIR__.'/js/dist/forum.js') + ->asset(__DIR__.'/less/forum.less'), + function (Dispatcher $events, Factory $views) { $events->subscribe(Listener\AddPostMentionedByRelationship::class); $events->subscribe(Listener\FormatPostMentions::class); diff --git a/extensions/mentions/js/forum.js b/extensions/mentions/js/forum.js new file mode 100644 index 000000000..cc78f6edc --- /dev/null +++ b/extensions/mentions/js/forum.js @@ -0,0 +1,10 @@ +/* + * This file is part of Flarum. + * + * (c) Toby Zerner + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export * from './src/forum'; diff --git a/extensions/mentions/js/forum/Gulpfile.js b/extensions/mentions/js/forum/Gulpfile.js deleted file mode 100644 index 0dc548119..000000000 --- a/extensions/mentions/js/forum/Gulpfile.js +++ /dev/null @@ -1,10 +0,0 @@ -var gulp = require('flarum-gulp'); - -gulp({ - modules: { - 'flarum/mentions': 'src/**/*.js' - }, - files: [ - 'node_modules/textarea-caret/index.js' - ] -}); diff --git a/extensions/mentions/js/forum/dist/extension.js b/extensions/mentions/js/forum/dist/extension.js deleted file mode 100644 index 6fad09bc2..000000000 --- a/extensions/mentions/js/forum/dist/extension.js +++ /dev/null @@ -1,1322 +0,0 @@ -/* 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', { filter: { 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 = $('