Template.editor.onRendered(() => { const textareaSelector = 'textarea'; const disableRicherEditor = Meteor.settings.public.NO_RICHER_EDITOR; const mentions = [ // User mentions { match: /\B@([\w.]*)$/, search(term, callback) { const currentBoard = Boards.findOne(Session.get('currentBoard')); callback( currentBoard .activeMembers() .map(member => { const username = Users.findOne(member.userId).username; return username.includes(term) ? username : null; }) .filter(Boolean), ); }, template(value) { return value; }, replace(username) { return `@${username} `; }, index: 1, }, ]; if (!disableRicherEditor) { const isSmall = Utils.isMiniScreen(); const toolbar = isSmall ? [ ['font', ['bold', 'underline']], ['fontsize', ['fontsize']], ['color', ['color']], ['table', ['table']], ['view', ['fullscreen']], ] : [ ['style', ['style']], ['font', ['bold', 'underline', 'clear']], ['fontsize', ['fontsize']], ['fontname', ['fontname']], ['color', ['color']], ['para', ['ul', 'ol', 'paragraph']], ['table', ['table']], //['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled ['insert', ['link', 'picture']], ['view', ['fullscreen', 'help']], ]; const cleanPastedHTML = function(input) { const badTags = [ 'style', 'script', 'applet', 'embed', 'noframes', 'noscript', 'meta', 'link', 'button', 'form', ].join('|'); const badPatterns = new RegExp( `(?:${[ `<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`, `<(${badTags})[^>]*?\\/>`, ].join('|')})`, 'gi', ); let output = input; // remove bad Tags output = output.replace(badPatterns, ''); // remove attributes ' style="..."' const badAttributes = new RegExp( `(?:${[ 'on\\S+=([\'"]?).*?\\1', 'href=([\'"]?)javascript:.*?\\2', 'style=([\'"]?).*?\\3', 'target=\\S+', ].join('|')})`, 'gi', ); output = output.replace(badAttributes, ''); output = output.replace(/( { const u = Users.findOne(member.userId); if (u) { member.username = u.username; } return member; }); const mentionRegex = /\B@([\w.]*)/gi; let currentMention; while ((currentMention = mentionRegex.exec(content)) !== null) { const [fullMention, username] = currentMention; const knowedUser = _.findWhere(knowedUsers, { username }); if (!knowedUser) { continue; } const linkValue = [' ', at, knowedUser.username]; let linkClass = 'atMention js-open-member'; if (knowedUser.userId === Meteor.userId()) { linkClass += ' me'; } const link = HTML.A( { class: linkClass, // XXX Hack. Since we stringify this render function result below with // `Blaze.toHTML` we can't rely on blaze data contexts to pass the // `userId` to the popup as usual, and we need to store it in the DOM // using a data attribute. 'data-userId': knowedUser.userId, }, linkValue, ); content = content.replace(fullMention, Blaze.toHTML(link)); } return HTML.Raw(sanitizeXss(content)); }), ); Template.viewer.events({ // Viewer sometimes have click-able wrapper around them (for instance to edit // the corresponding text). Clicking a link shouldn't fire these actions, stop // we stop these event at the viewer component level. 'click a'(event, templateInstance) { event.stopPropagation(); // XXX We hijack the build-in browser action because we currently don't have // `_blank` attributes in viewer links, and the transformer function is // handled by a third party package that we can't configure easily. Fix that // by using directly `_blank` attribute in the rendered HTML. event.preventDefault(); const userId = event.currentTarget.dataset.userid; if (userId) { Popup.open('member').call({ userId }, event, templateInstance); } else { const href = event.currentTarget.href; if (href) { window.open(href, '_blank'); } } }, });