[Backport] fix several wysiwyg issues

stable
Juanfran 2017-03-14 15:28:07 +01:00 committed by Alejandro Alonso
parent 1740d3e182
commit 1ebd8d0271
13 changed files with 288 additions and 8 deletions

View File

@ -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", $routeProvider.when("/project/:pslug/search",
{ {
templateUrl: "search/search.html", templateUrl: "search/search.html",

View File

@ -74,6 +74,7 @@ urls = {
"blocked-project": "/blocked-project/:project" "blocked-project": "/blocked-project/:project"
"project": "/project/:project" "project": "/project/:project"
"project-detail-ref": "/project/:project/t/:ref"
"project-backlog": "/project/:project/backlog" "project-backlog": "/project/:project/backlog"
"project-taskboard": "/project/:project/taskboard/:sprint" "project-taskboard": "/project/:project/taskboard/:sprint"
"project-kanban": "/project/:project/kanban" "project-kanban": "/project/:project/kanban"

View File

@ -230,6 +230,7 @@ class RepositoryService extends taiga.Service
params.issue = options.issueref if options.issueref? params.issue = options.issueref if options.issueref?
params.milestone = options.sslug if options.sslug? params.milestone = options.sslug if options.sslug?
params.wikipage = options.wikipage if options.wikipage? params.wikipage = options.wikipage if options.wikipage?
params.ref = options.ref if options.ref?
cache = not (options.wikipage or options.sslug) cache = not (options.wikipage or options.sslug)
return @.queryOneRaw("resolver", null, params, {cache: cache}) return @.queryOneRaw("resolver", null, params, {cache: cache})

View File

@ -0,0 +1,79 @@
###
# Copyright (C) 2014-2017 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2017 Jesús Espino Garcia <jespinog@gmail.com>
# Copyright (C) 2014-2017 David Barragán Merino <bameda@dbarragan.com>
# Copyright (C) 2014-2017 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# Copyright (C) 2014-2017 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
# Copyright (C) 2014-2017 Xavi Julian <xavier.julian@kaleidos.net>
#
# 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 <http://www.gnu.org/licenses/>.
#
# 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)

View File

@ -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' ]
});
};
}());

View File

@ -196,7 +196,7 @@ var MentionExtension = MediumEditor.Extension.extend({
e.stopPropagation(); e.stopPropagation();
var event = document.createEvent('HTMLEvents'); var event = document.createEvent('HTMLEvents');
event.initEvent('click', true, false); event.initEvent('mousedown', true, false);
active.dispatchEvent(event); active.dispatchEvent(event);

View File

@ -266,6 +266,9 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad
if $scope.mode == 'html' if $scope.mode == 'html'
updateMarkdownWithCurrentHtml() updateMarkdownWithCurrentHtml()
html = wysiwygService.getHTML($scope.markdown)
editorMedium.html(html)
return if $scope.required && !$scope.markdown.length return if $scope.required && !$scope.markdown.length
$scope.saving = true $scope.saving = true

View File

@ -176,6 +176,13 @@ tg-wysiwyg {
} }
.tools { .tools {
padding-left: 1rem; padding-left: 1rem;
&:not(.visible) {
opacity: 0;
pointer-events: none;
a {
cursor: default;
}
}
a { a {
display: block; display: block;
margin-bottom: .5rem; margin-bottom: .5rem;
@ -213,6 +220,9 @@ tg-wysiwyg {
.read-mode { .read-mode {
cursor: pointer; cursor: pointer;
} }
.medium {
border: 1px solid transparent;
}
.edit-mode { .edit-mode {
.markdown, .markdown,
.medium { .medium {

View File

@ -23,7 +23,12 @@
### ###
class WysiwygService class WysiwygService
constructor: (@wysiwygCodeHightlighterService) -> @.$inject = [
"tgWysiwygCodeHightlighterService",
"tgProjectService",
"$tgNavUrls"
]
constructor: (@wysiwygCodeHightlighterService, @projectService, @navurls) ->
searchEmojiByName: (name) -> searchEmojiByName: (name) ->
return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1 return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1
@ -65,6 +70,20 @@ class WysiwygService
return text 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) -> removeTrailingListBr: (text) ->
return text.replace(/<li>(.*?)<br><\/li>/g, '<li>$1</li>') return text.replace(/<li>(.*?)<br><\/li>/g, '<li>$1</li>')
@ -90,6 +109,7 @@ class WysiwygService
html = html.replace(/&nbsp;(<\/.*>)/g, "$1") html = html.replace(/&nbsp;(<\/.*>)/g, "$1")
html = @.replaceImgsByEmojiName(html) html = @.replaceImgsByEmojiName(html)
html = @.replaceUrls(html)
html = @.removeTrailingListBr(html) html = @.removeTrailingListBr(html)
markdown = toMarkdown(html, { markdown = toMarkdown(html, {
@ -97,25 +117,47 @@ class WysiwygService
converters: [cleanIssueConverter, codeLanguageConverter] converters: [cleanIssueConverter, codeLanguageConverter]
}) })
return markdown 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 '<a class="autolink" href="' + profileUrl + '">@' + match.getMention() + '</a>'
else if match.getType() == 'hashtag'
url = @navurls.resolve('project-detail-ref', {
project: @projectService.project.get('slug'),
ref: match.getHashtag()
})
return '<a class="autolink" href="' + url + '">#' + match.getHashtag() + '</a>'
})
getHTML: (text) -> getHTML: (text) ->
return "" if !text || !text.length return "" if !text || !text.length
options = { options = {
breaks: true breaks: true
} }
text = @.replaceEmojiNameByImgs(text) text = @.replaceEmojiNameByImgs(text)
md = window.markdownit({ md = window.markdownit({
breaks: true breaks: true
}) })
md.use(window.markdownitLazyHeaders)
result = md.render(text) result = md.render(text)
result = @.autoLinkHTML(result)
return result return result
angular.module("taigaComponents") angular.module("taigaComponents")
.service("tgWysiwygService", ["tgWysiwygCodeHightlighterService", WysiwygService]) .service("tgWysiwygService", WysiwygService)

View File

@ -326,7 +326,7 @@ describe "ImportProjectMembersCtrl", ->
expect(ctrl.displayEmailSelector).to.be.true 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 = $controller("ImportProjectMembersCtrl")
ctrl.getDistinctSelectedTaigaUsers = sinon.stub().returns(Immutable.fromJS([ ctrl.getDistinctSelectedTaigaUsers = sinon.stub().returns(Immutable.fromJS([

View File

@ -34,7 +34,7 @@
tg-svg(svg-icon="icon-question") tg-svg(svg-icon="icon-question")
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP") span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")
.tools(ng-if="editMode") .tools(ng-class="{\"visible\": editMode}")
a.e2e-save-editor( a.e2e-save-editor(
ng-class="{disabled: required && !markdown.length}" ng-class="{disabled: required && !markdown.length}"
tg-loading="saving" tg-loading="saving"

View File

@ -187,12 +187,14 @@ paths.libs = [
paths.modules + "highlight.js/lib/highlight.js", paths.modules + "highlight.js/lib/highlight.js",
paths.modules + "prismjs/prism.js", paths.modules + "prismjs/prism.js",
paths.modules + "medium-editor-autolist/dist/autolist.js", paths.modules + "medium-editor-autolist/dist/autolist.js",
paths.modules + "autolinker/dist/Autolinker.js",
paths.app + "js/dom-autoscroller.js", paths.app + "js/dom-autoscroller.js",
paths.app + "js/dragula-drag-multiple.js", paths.app + "js/dragula-drag-multiple.js",
paths.app + "js/tg-repeat.js", paths.app + "js/tg-repeat.js",
paths.app + "js/sha1-custom.js", paths.app + "js/sha1-custom.js",
paths.app + "js/murmurhash3_gc.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) { paths.libs.forEach(function(file) {

View File

@ -109,6 +109,7 @@
"angular-translate-interpolation-messageformat": "2.10.0", "angular-translate-interpolation-messageformat": "2.10.0",
"angular-translate-loader-partial": "2.10.0", "angular-translate-loader-partial": "2.10.0",
"angular-translate-loader-static-files": "2.10.0", "angular-translate-loader-static-files": "2.10.0",
"autolinker": "^1.4.2",
"awesomplete": "^1.0.0", "awesomplete": "^1.0.0",
"bluebird": "^3.4.6", "bluebird": "^3.4.6",
"bourbon": "git+https://github.com/thoughtbot/bourbon.git", "bourbon": "git+https://github.com/thoughtbot/bourbon.git",