From 1ebd8d0271deb658c53ace8054c148783a47d241 Mon Sep 17 00:00:00 2001 From: Juanfran Date: Tue, 14 Mar 2017 15:28:07 +0100 Subject: [PATCH] [Backport] fix several wysiwyg issues --- app/coffee/app.coffee | 9 ++ app/coffee/modules/base.coffee | 1 + app/coffee/modules/base/repository.coffee | 1 + app/coffee/modules/detail.coffee | 79 +++++++++++ app/js/markdown-it-lazy-headers.js | 132 ++++++++++++++++++ app/js/medium-mention.js | 2 +- .../wysiwyg/wysiwyg.directive.coffee | 3 + app/modules/components/wysiwyg/wysiwyg.scss | 10 ++ .../components/wysiwyg/wysiwyg.service.coffee | 50 ++++++- ...ort-project-members.controller.spec.coffee | 2 +- .../common/components/wysiwyg-toolbar.jade | 2 +- gulpfile.js | 4 +- package.json | 1 + 13 files changed, 288 insertions(+), 8 deletions(-) create mode 100644 app/coffee/modules/detail.coffee create mode 100644 app/js/markdown-it-lazy-headers.js diff --git a/app/coffee/app.coffee b/app/coffee/app.coffee index 3ee64100..4b3af7ed 100644 --- a/app/coffee/app.coffee +++ b/app/coffee/app.coffee @@ -185,6 +185,15 @@ configure = ($routeProvider, $locationProvider, $httpProvider, $provide, $tgEven } ) + # Project ref detail + $routeProvider.when("/project/:pslug/t/:ref", + { + loader: true, + controller: "DetailController", + template: "" + } + ) + $routeProvider.when("/project/:pslug/search", { templateUrl: "search/search.html", diff --git a/app/coffee/modules/base.coffee b/app/coffee/modules/base.coffee index ba8a3d40..ce0ecdca 100644 --- a/app/coffee/modules/base.coffee +++ b/app/coffee/modules/base.coffee @@ -74,6 +74,7 @@ urls = { "blocked-project": "/blocked-project/:project" "project": "/project/:project" + "project-detail-ref": "/project/:project/t/:ref" "project-backlog": "/project/:project/backlog" "project-taskboard": "/project/:project/taskboard/:sprint" "project-kanban": "/project/:project/kanban" diff --git a/app/coffee/modules/base/repository.coffee b/app/coffee/modules/base/repository.coffee index 31c81152..3fbba37f 100644 --- a/app/coffee/modules/base/repository.coffee +++ b/app/coffee/modules/base/repository.coffee @@ -230,6 +230,7 @@ class RepositoryService extends taiga.Service params.issue = options.issueref if options.issueref? params.milestone = options.sslug if options.sslug? params.wikipage = options.wikipage if options.wikipage? + params.ref = options.ref if options.ref? cache = not (options.wikipage or options.sslug) return @.queryOneRaw("resolver", null, params, {cache: cache}) diff --git a/app/coffee/modules/detail.coffee b/app/coffee/modules/detail.coffee new file mode 100644 index 00000000..0385b2d7 --- /dev/null +++ b/app/coffee/modules/detail.coffee @@ -0,0 +1,79 @@ +### +# Copyright (C) 2014-2017 Andrey Antukh +# Copyright (C) 2014-2017 Jesús Espino Garcia +# Copyright (C) 2014-2017 David Barragán Merino +# Copyright (C) 2014-2017 Alejandro Alonso +# Copyright (C) 2014-2017 Juan Francisco Alcántara +# Copyright (C) 2014-2017 Xavi Julian +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/detail.coffee +### + +taiga = @.taiga + +mixOf = @.taiga.mixOf +toString = @.taiga.toString +joinStr = @.taiga.joinStr +groupBy = @.taiga.groupBy +bindOnce = @.taiga.bindOnce +bindMethods = @.taiga.bindMethods + +module = angular.module("taigaCommon") + +class DetailController + @.$inject = [ + '$routeParams', + '$tgRepo', + "tgProjectService", + "$tgNavUrls", + "$location" + ] + + constructor: (@params, @repo, @projectService, @navurls, @location) -> + @repo.resolve({ + pslug: @params.pslug, + ref: @params.ref + }) + .then (result) => + if result.issue + url = @navurls.resolve('project-issues-detail', { + project: @projectService.project.get('slug'), + ref: @params.ref + }) + else if result.task + url = @navurls.resolve('project-tasks-detail', { + project: @projectService.project.get('slug'), + ref: @params.ref + }) + else if result.us + url = @navurls.resolve('project-userstories-detail', { + project: @projectService.project.get('slug'), + ref: @params.ref + }) + else if result.epic + url = @navurls.resolve('project-epics-detail', { + project: @projectService.project.get('slug'), + ref: @params.ref + }) + else if result.wikipage + url = @navurls.resolve('project-wiki-page', { + project: @projectService.project.get('slug'), + slug: @params.ref + }) + + @location.path(url) + +module.controller("DetailController", DetailController) diff --git a/app/js/markdown-it-lazy-headers.js b/app/js/markdown-it-lazy-headers.js new file mode 100644 index 00000000..d45ade6c --- /dev/null +++ b/app/js/markdown-it-lazy-headers.js @@ -0,0 +1,132 @@ +// https://github.com/Galadirith/markdown-it-lazy-headers +// Copyright (c) 2015 Edward Fauchon-Jones + +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: + +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// ------------------------------------------------------------------------ + +// This repository incorporates code from +// [markdown-it](https://github.com/markdown-it/markdown-it) covered by the +// following terms: + +// > Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. +// > +// > Permission is hereby granted, free of charge, to any person +// > obtaining a copy of this software and associated documentation +// > files (the "Software"), to deal in the Software without +// > restriction, including without limitation the rights to use, +// > copy, modify, merge, publish, distribute, sublicense, and/or sell +// > copies of the Software, and to permit persons to whom the +// > Software is furnished to do so, subject to the following +// > conditions: +// > +// > The above copyright notice and this permission notice shall be +// > included in all copies or substantial portions of the Software. +// > +// > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// > OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// > NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// > HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// > WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// > FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// > OTHER DEALINGS IN THE SOFTWARE. + +// ------------------------------------------------------------------------ + +// This repository incorporates code from +// [markdown-it-math](https://github.com/runarberg/markdown-it-math) covered by the +// following terms: + +// > Copyright (c) 2015 Rúnar Berg Baugsson Sigríðarson +// > +// > Permission is hereby granted, free of charge, to any person obtaining a copy +// > of this software and associated documentation files (the "Software"), to deal +// > in the Software without restriction, including without limitation the rights +// > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// > copies of the Software, and to permit persons to whom the Software is +// > furnished to do so, subject to the following conditions: +// > +// > The above copyright notice and this permission notice shall be included in +// > all copies or substantial portions of the Software. +// > +// > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// > THE SOFTWARE. + +(function() { +'use strict'; + +window.markdownitLazyHeaders = function lazy_headers_plugin(md) { + function heading(state, startLine, endLine, silent) { + var ch, level, tmp, token, + pos = state.bMarks[startLine] + state.tShift[startLine], + max = state.eMarks[startLine]; + + ch = state.src.charCodeAt(pos); + + if (ch !== 0x23/* # */ || pos >= max) { return false; } + + // count heading level + level = 1; + ch = state.src.charCodeAt(++pos); + while (ch === 0x23/* # */ && pos < max && level <= 6) { + level++; + ch = state.src.charCodeAt(++pos); + } + + if (level > 6) { return false; } + + if (silent) { return true; } + + // Let's cut tails like ' ### ' from the end of string + + max = state.skipCharsBack(max, 0x20, pos); // space + tmp = state.skipCharsBack(max, 0x23, pos); // # + if (tmp > pos && state.src.charCodeAt(tmp - 1) === 0x20/* space */) { + max = tmp; + } + + state.line = startLine + 1; + + token = state.push('heading_open', 'h' + String(level), 1); + token.markup = '########'.slice(0, level); + token.map = [ startLine, state.line ]; + + token = state.push('inline', '', 0); + token.content = state.src.slice(pos, max).trim(); + token.map = [ startLine, state.line ]; + token.children = []; + + token = state.push('heading_close', 'h' + String(level), -1); + token.markup = '########'.slice(0, level); + + return true; + } + + md.block.ruler.at('heading', heading, { + alt: [ 'paragraph', 'reference', 'blockquote' ] + }); +}; +}()); diff --git a/app/js/medium-mention.js b/app/js/medium-mention.js index 936874be..5b0fe821 100644 --- a/app/js/medium-mention.js +++ b/app/js/medium-mention.js @@ -196,7 +196,7 @@ var MentionExtension = MediumEditor.Extension.extend({ e.stopPropagation(); var event = document.createEvent('HTMLEvents'); - event.initEvent('click', true, false); + event.initEvent('mousedown', true, false); active.dispatchEvent(event); diff --git a/app/modules/components/wysiwyg/wysiwyg.directive.coffee b/app/modules/components/wysiwyg/wysiwyg.directive.coffee index 3b39a05b..394e7914 100644 --- a/app/modules/components/wysiwyg/wysiwyg.directive.coffee +++ b/app/modules/components/wysiwyg/wysiwyg.directive.coffee @@ -266,6 +266,9 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad if $scope.mode == 'html' updateMarkdownWithCurrentHtml() + html = wysiwygService.getHTML($scope.markdown) + editorMedium.html(html) + return if $scope.required && !$scope.markdown.length $scope.saving = true diff --git a/app/modules/components/wysiwyg/wysiwyg.scss b/app/modules/components/wysiwyg/wysiwyg.scss index ef9d90ea..465311f0 100644 --- a/app/modules/components/wysiwyg/wysiwyg.scss +++ b/app/modules/components/wysiwyg/wysiwyg.scss @@ -176,6 +176,13 @@ tg-wysiwyg { } .tools { padding-left: 1rem; + &:not(.visible) { + opacity: 0; + pointer-events: none; + a { + cursor: default; + } + } a { display: block; margin-bottom: .5rem; @@ -213,6 +220,9 @@ tg-wysiwyg { .read-mode { cursor: pointer; } + .medium { + border: 1px solid transparent; + } .edit-mode { .markdown, .medium { diff --git a/app/modules/components/wysiwyg/wysiwyg.service.coffee b/app/modules/components/wysiwyg/wysiwyg.service.coffee index 67236ffa..271dc349 100644 --- a/app/modules/components/wysiwyg/wysiwyg.service.coffee +++ b/app/modules/components/wysiwyg/wysiwyg.service.coffee @@ -23,7 +23,12 @@ ### class WysiwygService - constructor: (@wysiwygCodeHightlighterService) -> + @.$inject = [ + "tgWysiwygCodeHightlighterService", + "tgProjectService", + "$tgNavUrls" + ] + constructor: (@wysiwygCodeHightlighterService, @projectService, @navurls) -> searchEmojiByName: (name) -> return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1 @@ -65,6 +70,20 @@ class WysiwygService return text + replaceUrls: (html) -> + el = document.createElement( 'html' ) + el.innerHTML = html + + links = el.querySelectorAll('a') + + for link in links + if link.getAttribute('href').indexOf('/profile/') != -1 + link.parentNode.replaceChild(document.createTextNode(link.innerText), link) + else if link.getAttribute('href').indexOf('/t/') != -1 + link.parentNode.replaceChild(document.createTextNode(link.innerText), link) + + return el.innerHTML + removeTrailingListBr: (text) -> return text.replace(/
  • (.*?)
    <\/li>/g, '
  • $1
  • ') @@ -90,6 +109,7 @@ class WysiwygService html = html.replace(/ (<\/.*>)/g, "$1") html = @.replaceImgsByEmojiName(html) + html = @.replaceUrls(html) html = @.removeTrailingListBr(html) markdown = toMarkdown(html, { @@ -97,25 +117,47 @@ class WysiwygService converters: [cleanIssueConverter, codeLanguageConverter] }) - return markdown + autoLinkHTML: (html) -> + return Autolinker.link(html, { + mention: 'twitter', + hashtag: 'twitter', + replaceFn: (match) => + if match.getType() == 'mention' + profileUrl = @navurls.resolve('user-profile', { + project: @projectService.project.get('slug'), + username: match.getMention() + }) + + return '@' + match.getMention() + '' + else if match.getType() == 'hashtag' + url = @navurls.resolve('project-detail-ref', { + project: @projectService.project.get('slug'), + ref: match.getHashtag() + }) + + return '#' + match.getHashtag() + '' + }) + getHTML: (text) -> return "" if !text || !text.length options = { breaks: true } - text = @.replaceEmojiNameByImgs(text) md = window.markdownit({ breaks: true }) + md.use(window.markdownitLazyHeaders) + result = md.render(text) + result = @.autoLinkHTML(result) return result angular.module("taigaComponents") - .service("tgWysiwygService", ["tgWysiwygCodeHightlighterService", WysiwygService]) + .service("tgWysiwygService", WysiwygService) diff --git a/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee b/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee index 47be1b10..3bfb133a 100644 --- a/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee +++ b/app/modules/projects/create/import-project-members/import-project-members.controller.spec.coffee @@ -326,7 +326,7 @@ describe "ImportProjectMembersCtrl", -> expect(ctrl.displayEmailSelector).to.be.true - it.only "refresh selectable users array with the selected ones", () -> + it "refresh selectable users array with the selected ones", () -> ctrl = $controller("ImportProjectMembersCtrl") ctrl.getDistinctSelectedTaigaUsers = sinon.stub().returns(Immutable.fromJS([ diff --git a/app/partials/common/components/wysiwyg-toolbar.jade b/app/partials/common/components/wysiwyg-toolbar.jade index 24dbe439..035267a8 100644 --- a/app/partials/common/components/wysiwyg-toolbar.jade +++ b/app/partials/common/components/wysiwyg-toolbar.jade @@ -34,7 +34,7 @@ tg-svg(svg-icon="icon-question") span(translate="COMMON.WYSIWYG.MARKDOWN_HELP") -.tools(ng-if="editMode") +.tools(ng-class="{\"visible\": editMode}") a.e2e-save-editor( ng-class="{disabled: required && !markdown.length}" tg-loading="saving" diff --git a/gulpfile.js b/gulpfile.js index f141184e..244f01c5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -187,12 +187,14 @@ paths.libs = [ paths.modules + "highlight.js/lib/highlight.js", paths.modules + "prismjs/prism.js", paths.modules + "medium-editor-autolist/dist/autolist.js", + paths.modules + "autolinker/dist/Autolinker.js", paths.app + "js/dom-autoscroller.js", paths.app + "js/dragula-drag-multiple.js", paths.app + "js/tg-repeat.js", paths.app + "js/sha1-custom.js", paths.app + "js/murmurhash3_gc.js", - paths.app + "js/medium-mention.js" + paths.app + "js/medium-mention.js", + paths.app + "js/markdown-it-lazy-headers.js" ]; paths.libs.forEach(function(file) { diff --git a/package.json b/package.json index a0ccf7d2..736491b1 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "angular-translate-interpolation-messageformat": "2.10.0", "angular-translate-loader-partial": "2.10.0", "angular-translate-loader-static-files": "2.10.0", + "autolinker": "^1.4.2", "awesomplete": "^1.0.0", "bluebird": "^3.4.6", "bourbon": "git+https://github.com/thoughtbot/bourbon.git",