|
@ -5,10 +5,8 @@ before_install:
|
||||||
- export CHROME_BIN=chromium-browser
|
- export CHROME_BIN=chromium-browser
|
||||||
- export DISPLAY=:99.0
|
- export DISPLAY=:99.0
|
||||||
- sh -e /etc/init.d/xvfb start
|
- sh -e /etc/init.d/xvfb start
|
||||||
- travis_retry npm install -g bower
|
|
||||||
- travis_retry npm install -g gulp
|
- travis_retry npm install -g gulp
|
||||||
install:
|
install:
|
||||||
- travis_retry npm install
|
- travis_retry npm install
|
||||||
- travis_retry bower install
|
|
||||||
before_script:
|
before_script:
|
||||||
- gulp deploy
|
- gulp deploy
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
## 3.1.0 No name yet (no date yet)
|
## 3.1.0 No name yet (no date yet)
|
||||||
- Velocity forecasting. Create sprints according to team velocity.
|
- Velocity forecasting. Create sprints according to team velocity.
|
||||||
|
- Remove bower
|
||||||
|
- Add new wysiwyg editor (emojis, local storage changes, mentions)
|
||||||
|
|
||||||
## 3.0.0 Stellaria Borealis (2016-10-02)
|
## 3.0.0 Stellaria Borealis (2016-10-02)
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ Every code patch accepted in taiga codebase is licensed under [AGPL v3.0](http:/
|
||||||
|
|
||||||
Please read carefully [our license](https://github.com/taigaio/taiga-front/blob/master/LICENSE) and ask us if you have any questions.
|
Please read carefully [our license](https://github.com/taigaio/taiga-front/blob/master/LICENSE) and ask us if you have any questions.
|
||||||
|
|
||||||
|
Emoji provided free by [Twemoji](https://github.com/twitter/twemoji)
|
||||||
|
|
||||||
#### Bug reports, enhancements and support ####
|
#### Bug reports, enhancements and support ####
|
||||||
|
|
||||||
|
@ -125,14 +126,12 @@ sass -v # should return Sass 3.3.8 (Maptastic Maple)
|
||||||
|
|
||||||
Complete process for all OS at: http://sass-lang.com/install
|
Complete process for all OS at: http://sass-lang.com/install
|
||||||
|
|
||||||
**Node + Bower + Gulp**
|
**Node + Gulp**
|
||||||
|
|
||||||
We recommend using [nvm](https://github.com/creationix/nvm) to manage different node versions
|
We recommend using [nvm](https://github.com/creationix/nvm) to manage different node versions
|
||||||
```
|
```
|
||||||
npm install -g gulp
|
npm install -g gulp
|
||||||
npm install -g bower
|
|
||||||
npm install
|
npm install
|
||||||
bower install
|
|
||||||
gulp
|
gulp
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -178,4 +177,4 @@ To run a local Selenium Server, you will need to have the Java Development Kit (
|
||||||
```
|
```
|
||||||
protractor conf.e2e.js --suite=auth # To tests authentication
|
protractor conf.e2e.js --suite=auth # To tests authentication
|
||||||
protractor conf.e2e.js --suite=full # To test all the platform authenticated
|
protractor conf.e2e.js --suite=full # To test all the platform authenticated
|
||||||
```
|
```
|
|
@ -39,7 +39,7 @@ class Model
|
||||||
instance._modifiedAttrs = _.cloneDeep(@._modifiedAttrs)
|
instance._modifiedAttrs = _.cloneDeep(@._modifiedAttrs)
|
||||||
instance._isModified = _.cloneDeep(@._isModified)
|
instance._isModified = _.cloneDeep(@._isModified)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
clone: ->
|
clone: ->
|
||||||
instance = new Model(@._name, @._attrs, @._dataTypes)
|
instance = new Model(@._name, @._attrs, @._dataTypes)
|
||||||
|
|
|
@ -390,14 +390,22 @@ Svg = () ->
|
||||||
|
|
||||||
module.directive("tgSvg", [Svg])
|
module.directive("tgSvg", [Svg])
|
||||||
|
|
||||||
Autofocus = ($timeout) ->
|
Autofocus = ($timeout, $parse, animationFrame) ->
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
link : ($scope, $element) ->
|
link : ($scope, $element, attrs) ->
|
||||||
$timeout -> $element[0].focus()
|
if attrs.ngShow
|
||||||
|
model = $parse(attrs.ngShow)
|
||||||
|
|
||||||
|
$scope.$watch model, (value) ->
|
||||||
|
if value == true
|
||||||
|
$timeout () -> $element[0].focus()
|
||||||
|
|
||||||
|
else
|
||||||
|
$timeout () -> $element[0].focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
module.directive('tgAutofocus', ['$timeout', Autofocus])
|
module.directive('tgAutofocus', ['$timeout', '$parse', "animationFrame", Autofocus])
|
||||||
|
|
||||||
module.directive 'tgPreloadImage', () ->
|
module.directive 'tgPreloadImage', () ->
|
||||||
spinner = "<img class='loading-spinner' src='/" + window._version + "/svg/spinner-circle.svg' alt='loading...' />"
|
spinner = "<img class='loading-spinner' src='/" + window._version + "/svg/spinner-circle.svg' alt='loading...' />"
|
||||||
|
|
|
@ -496,40 +496,37 @@ DeleteButtonDirective = ($log, $repo, $confirm, $location, $template) ->
|
||||||
module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "$tgTemplate", DeleteButtonDirective])
|
module.directive("tgDeleteButton", ["$log", "$tgRepo", "$tgConfirm", "$tgLocation", "$tgTemplate", DeleteButtonDirective])
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Editable description directive
|
## Editable subject directive
|
||||||
#############################################################################
|
#############################################################################
|
||||||
|
|
||||||
EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading, $selectedText, $modelTransform, $template, $translate) ->
|
EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $modelTransform, $template) ->
|
||||||
template = $template.get("common/components/editable-description.html")
|
template = $template.get("common/components/editable-subject.html")
|
||||||
noDescriptionMegEditMode = $template.get("common/components/editable-description-msg-edit-mode.html")
|
|
||||||
noDescriptionMegReadMode = $template.get("common/components/editable-description-msg-read-mode.html")
|
|
||||||
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
link = ($scope, $el, $attrs, $model) ->
|
||||||
$el.find('.edit-description').hide()
|
|
||||||
$el.find('.view-description .edit').hide()
|
|
||||||
|
|
||||||
$scope.$on "object:updated", () ->
|
$scope.$on "object:updated", () ->
|
||||||
$el.find('.edit-description').hide()
|
$el.find('.edit-subject').hide()
|
||||||
$el.find('.view-description').show()
|
$el.find('.view-subject').show()
|
||||||
|
|
||||||
isEditable = ->
|
isEditable = ->
|
||||||
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
|
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
|
||||||
|
|
||||||
save = (description) ->
|
save = (subject) ->
|
||||||
currentLoading = $loading()
|
currentLoading = $loading()
|
||||||
.target($el.find('.save-container'))
|
.target($el.find('.save-container'))
|
||||||
.start()
|
.start()
|
||||||
|
|
||||||
transform = $modelTransform.save (item) ->
|
transform = $modelTransform.save (item) ->
|
||||||
item.description = description
|
|
||||||
|
item.subject = subject
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
transform.then ->
|
transform.then =>
|
||||||
$confirm.notify("success")
|
$confirm.notify("success")
|
||||||
$rootscope.$broadcast("object:updated")
|
$rootscope.$broadcast("object:updated")
|
||||||
$el.find('.edit-description').hide()
|
$el.find('.edit-subject').hide()
|
||||||
$el.find('.view-description').show()
|
$el.find('.view-subject').show()
|
||||||
|
|
||||||
transform.then null, ->
|
transform.then null, ->
|
||||||
$confirm.notify("error")
|
$confirm.notify("error")
|
||||||
|
@ -537,60 +534,43 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading,
|
||||||
transform.finally ->
|
transform.finally ->
|
||||||
currentLoading.finish()
|
currentLoading.finish()
|
||||||
|
|
||||||
cancelEdition = () ->
|
return transform
|
||||||
$scope.item.revert()
|
|
||||||
$el.find('.edit-description').hide()
|
|
||||||
$el.find('.view-description').show()
|
|
||||||
|
|
||||||
$el.on "mouseup", ".view-description", (event) ->
|
$el.click ->
|
||||||
# We want to dettect the a inside the div so we use the target and
|
|
||||||
# not the currentTarget
|
|
||||||
target = angular.element(event.target)
|
|
||||||
return if not isEditable()
|
return if not isEditable()
|
||||||
return if target.is('a')
|
$el.find('.edit-subject').show()
|
||||||
return if $selectedText.get().length
|
$el.find('.view-subject').hide()
|
||||||
|
$el.find('input').focus()
|
||||||
$el.find('.edit-description').show()
|
|
||||||
$el.find('.view-description').hide()
|
|
||||||
$el.find('textarea').focus()
|
|
||||||
|
|
||||||
$el.on "click", "a", (event) ->
|
|
||||||
target = angular.element(event.target)
|
|
||||||
href = target.attr('href')
|
|
||||||
if href.indexOf("#") == 0
|
|
||||||
event.preventDefault()
|
|
||||||
$('body').scrollTop($(href).offset().top)
|
|
||||||
|
|
||||||
$el.on "click", ".save", (e) ->
|
$el.on "click", ".save", (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
description = $scope.item.description
|
subject = $scope.item.subject
|
||||||
save(description)
|
save(subject)
|
||||||
|
|
||||||
$el.on "keydown", "textarea", (event) ->
|
$el.on "keyup", "input", (event) ->
|
||||||
return if event.keyCode != 27
|
if event.keyCode == 13
|
||||||
|
subject = $scope.item.subject
|
||||||
|
save(subject)
|
||||||
|
else if event.keyCode == 27
|
||||||
|
$scope.$apply () => $model.$modelValue.revert()
|
||||||
|
|
||||||
$scope.$applyAsync () ->
|
$el.find('.edit-subject').hide()
|
||||||
title = $translate.instant("COMMON.CONFIRM_CLOSE_EDIT_MODE_TITLE")
|
$el.find('.view-subject').show()
|
||||||
message = $translate.instant("COMMON.CONFIRM_CLOSE_EDIT_MODE_MESSAGE")
|
|
||||||
$confirm.ask(title, null, message).then (askResponse) ->
|
$el.find('.edit-subject').hide()
|
||||||
cancelEdition()
|
|
||||||
askResponse.finish()
|
|
||||||
|
|
||||||
$scope.$watch $attrs.ngModel, (value) ->
|
$scope.$watch $attrs.ngModel, (value) ->
|
||||||
return if not value
|
return if not value
|
||||||
|
|
||||||
$scope.item = value
|
$scope.item = value
|
||||||
if isEditable()
|
|
||||||
$el.find('.view-description .edit').show()
|
if not isEditable()
|
||||||
$el.find('.view-description .us-content').addClass('editable')
|
$el.find('.view-subject .edit').remove()
|
||||||
$scope.noDescriptionMsg = $compile(noDescriptionMegEditMode)($scope)
|
|
||||||
else
|
|
||||||
$scope.noDescriptionMsg = $compile(noDescriptionMegReadMode)($scope)
|
|
||||||
|
|
||||||
$scope.$on "$destroy", ->
|
$scope.$on "$destroy", ->
|
||||||
$el.off()
|
$el.off()
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
link: link
|
link: link
|
||||||
restrict: "EA"
|
restrict: "EA"
|
||||||
|
@ -598,81 +578,8 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading,
|
||||||
template: template
|
template: template
|
||||||
}
|
}
|
||||||
|
|
||||||
module.directive("tgEditableDescription", [
|
module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQueueModelTransformation",
|
||||||
"$rootScope",
|
"$tgTemplate", EditableSubjectDirective])
|
||||||
"$tgRepo",
|
|
||||||
"$tgConfirm",
|
|
||||||
"$compile",
|
|
||||||
"$tgLoading",
|
|
||||||
"$selectedText",
|
|
||||||
"$tgQueueModelTransformation",
|
|
||||||
"$tgTemplate",
|
|
||||||
"$translate",
|
|
||||||
EditableDescriptionDirective])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
EditableWysiwyg = (attachmentsService, attachmentsFullService) ->
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
|
||||||
|
|
||||||
isInEditMode = ->
|
|
||||||
return $el.find('textarea').is(':visible') and $model.$modelValue.id
|
|
||||||
|
|
||||||
|
|
||||||
uploadFile = (file, type) ->
|
|
||||||
return if !attachmentsService.validate(file)
|
|
||||||
|
|
||||||
return attachmentsFullService.addAttachment($model.$modelValue.project, $model.$modelValue.id, type, file).then (result) ->
|
|
||||||
if taiga.isImage(result.getIn(['file', 'name']))
|
|
||||||
return '![' + result.getIn(['file', 'name']) + '](' + result.getIn(['file', 'url']) + ')'
|
|
||||||
else
|
|
||||||
return '[' + result.getIn(['file', 'name']) + '](' + result.getIn(['file', 'url']) + ')'
|
|
||||||
|
|
||||||
$el.on 'dragover', (e) ->
|
|
||||||
textarea = $el.find('textarea').focus()
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
$el.on 'drop', (e) ->
|
|
||||||
e.stopPropagation()
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
if isInEditMode()
|
|
||||||
dataTransfer = e.dataTransfer || (e.originalEvent && e.originalEvent.dataTransfer)
|
|
||||||
|
|
||||||
textarea = $el.find('textarea')
|
|
||||||
|
|
||||||
textarea.addClass('in-progress')
|
|
||||||
|
|
||||||
type = $model.$modelValue['_name']
|
|
||||||
|
|
||||||
if type == "userstories"
|
|
||||||
type = "us"
|
|
||||||
else if type == "tasks"
|
|
||||||
type = "task"
|
|
||||||
else if type == "issues"
|
|
||||||
type = "issue"
|
|
||||||
else if type == "wiki"
|
|
||||||
type = "wiki_page"
|
|
||||||
|
|
||||||
promises = _.map dataTransfer.files, (file) ->
|
|
||||||
return uploadFile(file, type)
|
|
||||||
|
|
||||||
Promise.all(promises).then (result) ->
|
|
||||||
textarea = $el.find('textarea')
|
|
||||||
|
|
||||||
$.markItUp({ replaceWith: result.join(' ') })
|
|
||||||
|
|
||||||
textarea.removeClass('in-progress')
|
|
||||||
|
|
||||||
return {
|
|
||||||
link: link
|
|
||||||
restrict: "EA"
|
|
||||||
require: "ngModel"
|
|
||||||
}
|
|
||||||
|
|
||||||
module.directive("tgEditableWysiwyg", ["tgAttachmentsService", "tgAttachmentsFullService", EditableWysiwyg])
|
|
||||||
|
|
||||||
|
|
||||||
#############################################################################
|
#############################################################################
|
||||||
## Common list directives
|
## Common list directives
|
||||||
|
|
|
@ -75,7 +75,6 @@ sizeFormat = =>
|
||||||
|
|
||||||
module.filter("sizeFormat", sizeFormat)
|
module.filter("sizeFormat", sizeFormat)
|
||||||
|
|
||||||
|
|
||||||
toMutableFilter = ->
|
toMutableFilter = ->
|
||||||
toMutable = (js) ->
|
toMutable = (js) ->
|
||||||
return js.toJS()
|
return js.toJS()
|
||||||
|
@ -128,6 +127,15 @@ darkerFilter = ->
|
||||||
|
|
||||||
module.filter("darker", darkerFilter)
|
module.filter("darker", darkerFilter)
|
||||||
|
|
||||||
|
markdownToHTML = (wysiwigService) ->
|
||||||
|
return (input) ->
|
||||||
|
if input
|
||||||
|
return wysiwigService.getHTML(input)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
module.filter("markdownToHTML", ["tgWysiwygService", markdownToHTML])
|
||||||
|
|
||||||
inArray = ($filter) ->
|
inArray = ($filter) ->
|
||||||
return (list, arrayFilter, element) ->
|
return (list, arrayFilter, element) ->
|
||||||
if arrayFilter
|
if arrayFilter
|
||||||
|
|
|
@ -92,15 +92,16 @@ Loader = ($rootscope) ->
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageLoaded: pageLoaded
|
pageLoaded: pageLoaded
|
||||||
|
open: () -> open
|
||||||
start: (auto=false) ->
|
start: (auto=false) ->
|
||||||
if !open
|
if !open
|
||||||
start()
|
start()
|
||||||
autoClose() if auto
|
autoClose() if auto
|
||||||
onStart: (fn) ->
|
onStart: (fn) ->
|
||||||
$rootscope.$on("loader:start", fn)
|
return $rootscope.$on("loader:start", fn)
|
||||||
|
|
||||||
onEnd: (fn) ->
|
onEnd: (fn) ->
|
||||||
$rootscope.$on("loader:end", fn)
|
return $rootscope.$on("loader:end", fn)
|
||||||
|
|
||||||
logRequest: () ->
|
logRequest: () ->
|
||||||
requestCount++
|
requestCount++
|
||||||
|
|
|
@ -1,489 +0,0 @@
|
||||||
###
|
|
||||||
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
|
||||||
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
|
||||||
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
|
||||||
# Copyright (C) 2014-2016 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/common/wisiwyg.coffee
|
|
||||||
###
|
|
||||||
|
|
||||||
taiga = @.taiga
|
|
||||||
bindOnce = @.taiga.bindOnce
|
|
||||||
|
|
||||||
module = angular.module("taigaCommon")
|
|
||||||
|
|
||||||
# How to test lists (-, *, 1.)
|
|
||||||
# test it with text after & before the list
|
|
||||||
# + is the cursor position
|
|
||||||
|
|
||||||
# CASE 1
|
|
||||||
# - aa+
|
|
||||||
# --> enter
|
|
||||||
# - aa
|
|
||||||
# - +
|
|
||||||
|
|
||||||
# CASE 1
|
|
||||||
# - +
|
|
||||||
# --> enter
|
|
||||||
|
|
||||||
# +
|
|
||||||
|
|
||||||
# CASE 3
|
|
||||||
# - bb+cc
|
|
||||||
# --> enter
|
|
||||||
# - bb
|
|
||||||
# - cc
|
|
||||||
|
|
||||||
# CASE 3
|
|
||||||
# +- aa
|
|
||||||
# --> enter
|
|
||||||
|
|
||||||
# - aa
|
|
||||||
|
|
||||||
#############################################################################
|
|
||||||
## WYSIWYG markitup editor directive
|
|
||||||
#############################################################################
|
|
||||||
MarkitupDirective = ($rootscope, $rs, $selectedText, $template, $compile, $translate, projectService) ->
|
|
||||||
previewTemplate = $template.get("common/wysiwyg/wysiwyg-markitup-preview.html", true)
|
|
||||||
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
|
||||||
if not $scope.project
|
|
||||||
# for backward compatibility
|
|
||||||
$scope.project = projectService.project.toJS()
|
|
||||||
|
|
||||||
element = angular.element($el)
|
|
||||||
previewDomNode = $("<div/>", {class: "preview"})
|
|
||||||
|
|
||||||
closePreviewMode = ->
|
|
||||||
element.parents(".markdown").find(".preview").remove()
|
|
||||||
element.parents(".markItUp").show()
|
|
||||||
|
|
||||||
$scope.$on "markdown-editor:submit", ->
|
|
||||||
closePreviewMode()
|
|
||||||
|
|
||||||
cancelablePromise = null
|
|
||||||
previewInProgress = false
|
|
||||||
|
|
||||||
preview = ->
|
|
||||||
return if previewInProgress
|
|
||||||
|
|
||||||
previewInProgress = true
|
|
||||||
|
|
||||||
markdownDomNode = element.parents(".markdown")
|
|
||||||
markItUpDomNode = element.parents(".markItUp")
|
|
||||||
|
|
||||||
$rs.mdrender.render($scope.project.id, $model.$modelValue).then (data) ->
|
|
||||||
html = previewTemplate({data: data.data})
|
|
||||||
html = $compile(html)($scope)
|
|
||||||
|
|
||||||
markdownDomNode.append(html)
|
|
||||||
markItUpDomNode.hide()
|
|
||||||
|
|
||||||
previewInProgress = false
|
|
||||||
|
|
||||||
markdown = element.closest(".markdown")
|
|
||||||
|
|
||||||
markdown.on "mouseup.preview", ".preview", (event) ->
|
|
||||||
event.preventDefault()
|
|
||||||
target = angular.element(event.target)
|
|
||||||
|
|
||||||
if !target.is('a') and $selectedText.get().length
|
|
||||||
return
|
|
||||||
|
|
||||||
markdown.off(".preview")
|
|
||||||
closePreviewMode()
|
|
||||||
|
|
||||||
setCaretPosition = (textarea, caretPosition) ->
|
|
||||||
if textarea.createTextRange
|
|
||||||
range = textarea.createTextRange()
|
|
||||||
range.move("character", caretPosition)
|
|
||||||
range.select()
|
|
||||||
|
|
||||||
else if textarea.selectionStart
|
|
||||||
textarea.focus()
|
|
||||||
textarea.setSelectionRange(caretPosition, caretPosition)
|
|
||||||
|
|
||||||
# Calculate the scroll position
|
|
||||||
totalLines = textarea.value.split("\n").length
|
|
||||||
line = textarea.value[0..(caretPosition - 1)].split("\n").length
|
|
||||||
scrollRelation = line / totalLines
|
|
||||||
$el.scrollTop((scrollRelation * $el[0].scrollHeight) - ($el.height() / 2))
|
|
||||||
|
|
||||||
addLine = (textarea, nline, replace) ->
|
|
||||||
lines = textarea.value.split("\n")
|
|
||||||
|
|
||||||
if replace
|
|
||||||
lines[nline] = replace + lines[nline]
|
|
||||||
else
|
|
||||||
lines[nline] = ""
|
|
||||||
|
|
||||||
cursorPosition = 0
|
|
||||||
|
|
||||||
for line, key in lines
|
|
||||||
cursorPosition += line.length + 1 || 1
|
|
||||||
|
|
||||||
break if key == nline
|
|
||||||
|
|
||||||
textarea.value = lines.join("\n")
|
|
||||||
|
|
||||||
#return the new position
|
|
||||||
if replace
|
|
||||||
return cursorPosition - lines[nline].length + replace.length - 1
|
|
||||||
else
|
|
||||||
return cursorPosition
|
|
||||||
|
|
||||||
prepareUrlFormatting = (markItUp) ->
|
|
||||||
regex = /(<<<|>>>)/gi
|
|
||||||
result = 0
|
|
||||||
indices = []
|
|
||||||
(indices.push(result.index)) while ( (result = regex.exec(markItUp.textarea.value)) )
|
|
||||||
markItUp.donotparse = indices
|
|
||||||
|
|
||||||
urlFormatting = (markItUp) ->
|
|
||||||
regex = /<<</gi
|
|
||||||
result = 0
|
|
||||||
startIndex = 0
|
|
||||||
|
|
||||||
loop
|
|
||||||
result = regex.exec(markItUp.textarea.value)
|
|
||||||
break if !result
|
|
||||||
if result.index not in markItUp.donotparse
|
|
||||||
startIndex = result.index
|
|
||||||
break
|
|
||||||
|
|
||||||
return if !result
|
|
||||||
|
|
||||||
regex = />>>/gi
|
|
||||||
endIndex = 0
|
|
||||||
loop
|
|
||||||
result = regex.exec(markItUp.textarea.value)
|
|
||||||
break if !result
|
|
||||||
if result.index not in markItUp.donotparse
|
|
||||||
endIndex = result.index
|
|
||||||
break
|
|
||||||
|
|
||||||
value = markItUp.textarea.value
|
|
||||||
url = value.substring(startIndex, endIndex).replace('<<<', '').replace('>>>', '')
|
|
||||||
url = url.replace('(', '%28').replace(')', '%29')
|
|
||||||
url = url.replace('[', '%5B').replace(']', '%5D')
|
|
||||||
value = value.substring(0, startIndex) + url + value.substring(endIndex+3, value.length)
|
|
||||||
markItUp.textarea.value = value
|
|
||||||
markItUp.donotparse = undefined
|
|
||||||
|
|
||||||
markdownTitle = (markItUp, char) ->
|
|
||||||
heading = ""
|
|
||||||
n = $.trim(markItUp.selection or markItUp.placeHolder).length
|
|
||||||
|
|
||||||
for i in [0..n-1]
|
|
||||||
heading += char
|
|
||||||
|
|
||||||
return "\n"+heading+"\n"
|
|
||||||
|
|
||||||
renderMarkItUp = () ->
|
|
||||||
markdownSettings =
|
|
||||||
nameSpace: "markdown"
|
|
||||||
onShiftEnter: {keepDefault:false, openWith:"\n\n"}
|
|
||||||
onEnter:
|
|
||||||
keepDefault: false,
|
|
||||||
replaceWith: () ->
|
|
||||||
# Allow textcomplete to intercept the enter key if the options list is displayed
|
|
||||||
# @todo There doesn't seem to be a more graceful way to do this with the textcomplete API.
|
|
||||||
if not $('.textcomplete-dropdown').is(':visible')
|
|
||||||
"\n"
|
|
||||||
afterInsert: (data) ->
|
|
||||||
lines = data.textarea.value.split("\n")
|
|
||||||
# Detect if we are in this situation +- aa at the beginning if the textarea
|
|
||||||
if data.caretPosition > 0
|
|
||||||
cursorLine = data.textarea.value[0..(data.caretPosition - 1)].split("\n").length
|
|
||||||
else
|
|
||||||
cursorLine = 1
|
|
||||||
|
|
||||||
newLineContent = data.textarea.value[data.caretPosition..].split("\n")[0]
|
|
||||||
lastLine = lines[cursorLine - 1]
|
|
||||||
|
|
||||||
# unordered list -
|
|
||||||
match = lastLine.match /^(\s*- ).*/
|
|
||||||
|
|
||||||
if match
|
|
||||||
emptyListItem = lastLine.match /^(\s*)\-\s$/
|
|
||||||
|
|
||||||
if emptyListItem
|
|
||||||
nline = cursorLine - 1
|
|
||||||
replace = null
|
|
||||||
else
|
|
||||||
nline = cursorLine
|
|
||||||
replace = "#{match[1]}"
|
|
||||||
|
|
||||||
markdownCaretPositon = addLine(data.textarea, nline, replace)
|
|
||||||
|
|
||||||
# unordered list *
|
|
||||||
match = lastLine.match /^(\s*\* ).*/
|
|
||||||
|
|
||||||
if match
|
|
||||||
emptyListItem = lastLine.match /^(\s*\* )$/
|
|
||||||
|
|
||||||
if emptyListItem
|
|
||||||
nline = cursorLine - 1
|
|
||||||
replace = null
|
|
||||||
else
|
|
||||||
nline = cursorLine
|
|
||||||
replace = "#{match[1]}"
|
|
||||||
|
|
||||||
markdownCaretPositon = addLine(data.textarea, nline, replace)
|
|
||||||
|
|
||||||
# ordered list
|
|
||||||
match = lastLine.match /^(\s*)(\d+)\.\s/
|
|
||||||
|
|
||||||
if match
|
|
||||||
emptyListItem = lastLine.match /^(\s*)(\d+)\.\s$/
|
|
||||||
|
|
||||||
if emptyListItem
|
|
||||||
nline = cursorLine - 1
|
|
||||||
replace = null
|
|
||||||
else
|
|
||||||
nline = cursorLine
|
|
||||||
replace = "#{match[1] + (parseInt(match[2], 10) + 1)}. "
|
|
||||||
|
|
||||||
markdownCaretPositon = addLine(data.textarea, nline, replace)
|
|
||||||
|
|
||||||
setCaretPosition(data.textarea, markdownCaretPositon) if markdownCaretPositon
|
|
||||||
|
|
||||||
markupSet: [
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.H1_BUTTON")
|
|
||||||
key: "1"
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.H1_SAMPLE_TEXT")
|
|
||||||
closeWith: (markItUp) -> markdownTitle(markItUp, "=")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.H2_BUTTON")
|
|
||||||
key: "2"
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.H2_SAMPLE_TEXT")
|
|
||||||
closeWith: (markItUp) -> markdownTitle(markItUp, "-")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.H3_BUTTON")
|
|
||||||
key: "3"
|
|
||||||
openWith: "### "
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.H3_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
separator: "---------------"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.BOLD_BUTTON")
|
|
||||||
key: "B"
|
|
||||||
openWith: "**"
|
|
||||||
closeWith: "**"
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.BOLD_BUTTON_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.ITALIC_SAMPLE_TEXT")
|
|
||||||
key: "I"
|
|
||||||
openWith: "_"
|
|
||||||
closeWith: "_"
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.ITALIC_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.STRIKE_BUTTON")
|
|
||||||
key: "S"
|
|
||||||
openWith: "~~"
|
|
||||||
closeWith: "~~"
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.STRIKE_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
separator: "---------------"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.BULLETED_LIST_BUTTON")
|
|
||||||
openWith: "- "
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.BULLETED_LIST_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.NUMERIC_LIST_BUTTON")
|
|
||||||
openWith: (markItUp) -> markItUp.line+". "
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.NUMERIC_LIST_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
separator: "---------------"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.PICTURE_BUTTON")
|
|
||||||
key: "P"
|
|
||||||
openWith: "'
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.PICTURE_SAMPLE_TEXT")
|
|
||||||
beforeInsert:(markItUp) -> prepareUrlFormatting(markItUp)
|
|
||||||
afterInsert:(markItUp) -> urlFormatting(markItUp)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.LINK_BUTTON")
|
|
||||||
key: "L"
|
|
||||||
openWith: "["
|
|
||||||
closeWith: '](<<<[![Url:!:http://]!]>>> "[![Title]!]")'
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.LINK_SAMPLE_TEXT")
|
|
||||||
beforeInsert:(markItUp) -> prepareUrlFormatting(markItUp)
|
|
||||||
afterInsert:(markItUp) -> urlFormatting(markItUp)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
separator: "---------------"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.QUOTE_BLOCK_BUTTON")
|
|
||||||
openWith: "> "
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.QUOTE_BLOCK_SAMPLE_TEXT")
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.CODE_BLOCK_BUTTON")
|
|
||||||
openWith: "```\n"
|
|
||||||
placeHolder: $translate.instant("COMMON.WYSIWYG.CODE_BLOCK_SAMPLE_TEXT")
|
|
||||||
closeWith: "\n```"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
separator: "---------------"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: $translate.instant("COMMON.WYSIWYG.PREVIEW_BUTTON")
|
|
||||||
call: preview
|
|
||||||
className: "preview-icon"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
afterInsert: (event) ->
|
|
||||||
target = angular.element(event.textarea)
|
|
||||||
$model.$setViewValue(target.val())
|
|
||||||
|
|
||||||
element
|
|
||||||
.markItUpRemove()
|
|
||||||
.markItUp(markdownSettings)
|
|
||||||
.textcomplete([
|
|
||||||
# us, task, and issue autocomplete: #id or #<part of title>
|
|
||||||
{
|
|
||||||
cache: true
|
|
||||||
match: /(^|\s)#([a-z0-9]+)$/i,
|
|
||||||
search: (term, callback) ->
|
|
||||||
term = taiga.slugify(term)
|
|
||||||
|
|
||||||
searchTypes = ['issues', 'tasks', 'userstories', 'epics']
|
|
||||||
searchProps = ['ref', 'subject']
|
|
||||||
|
|
||||||
filter = (item) =>
|
|
||||||
for prop in searchProps
|
|
||||||
if taiga.slugify(item[prop]).indexOf(term) >= 0
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
cancelablePromise.abort() if cancelablePromise
|
|
||||||
cancelablePromise = $rs.search.do($scope.project.id, term)
|
|
||||||
|
|
||||||
cancelablePromise.then (res) =>
|
|
||||||
# ignore wikipages if they're the only results. can't exclude them in search
|
|
||||||
if res.count < 1 or res.count == res.wikipages.length
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
else
|
|
||||||
for type in searchTypes
|
|
||||||
if res[type] and res[type].length > 0
|
|
||||||
callback(res[type].filter(filter), true)
|
|
||||||
|
|
||||||
# must signal end of lists
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
replace: (res) ->
|
|
||||||
return "$1\##{res.ref} "
|
|
||||||
|
|
||||||
template: (res, term) ->
|
|
||||||
return "\##{res.ref} - #{res.subject}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# username autocomplete: @username or @<part of name>
|
|
||||||
{
|
|
||||||
cache: true
|
|
||||||
match: /(^|\s)@([a-z0-9\-\._]{2,})$/i
|
|
||||||
search: (term, callback) ->
|
|
||||||
username = taiga.slugify(term)
|
|
||||||
searchProps = ['username', 'full_name', 'full_name_display']
|
|
||||||
|
|
||||||
if $scope.project.members.length < 1
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
else
|
|
||||||
callback $scope.project.members.filter (user) =>
|
|
||||||
for prop in searchProps
|
|
||||||
if taiga.slugify(user[prop]).indexOf(username) >= 0
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
replace: (user) ->
|
|
||||||
return "$1@#{user.username} "
|
|
||||||
|
|
||||||
template: (user) ->
|
|
||||||
return "#{user.username} - #{user.full_name_display}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# wiki pages autocomplete: [[slug or [[<part of slug>
|
|
||||||
# if the search function was called with the 3rd param the regex
|
|
||||||
# like the docs claim, we could combine this with the #123 search
|
|
||||||
{
|
|
||||||
cache: true
|
|
||||||
match: /(^|\s)\[\[([a-z0-9\-]+)$/i
|
|
||||||
search: (term, callback) ->
|
|
||||||
term = taiga.slugify(term)
|
|
||||||
|
|
||||||
$rs.search.do($scope.project.id, term).then (res) =>
|
|
||||||
if res.count < 1
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
if res.count < 1 or not res.wikipages or res.wikipages.length <= 0
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
else
|
|
||||||
callback res.wikipages.filter((page) =>
|
|
||||||
return taiga.slugify(page['slug']).indexOf(term) >= 0
|
|
||||||
), true
|
|
||||||
|
|
||||||
# must signal end of lists
|
|
||||||
callback([])
|
|
||||||
|
|
||||||
|
|
||||||
replace: (res) ->
|
|
||||||
return "$1[[#{res.slug}]]"
|
|
||||||
|
|
||||||
template: (res, term) ->
|
|
||||||
return res.slug
|
|
||||||
}
|
|
||||||
],
|
|
||||||
{
|
|
||||||
debounce: 200
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
renderMarkItUp()
|
|
||||||
|
|
||||||
unbind = $rootscope.$on "$translateChangeEnd", renderMarkItUp
|
|
||||||
|
|
||||||
element.on "keypress", (event) ->
|
|
||||||
$scope.$apply()
|
|
||||||
|
|
||||||
$scope.$on "$destroy", ->
|
|
||||||
$el.off()
|
|
||||||
unbind()
|
|
||||||
|
|
||||||
return {link:link, require:"ngModel"}
|
|
||||||
|
|
||||||
module.directive("tgMarkitup", ["$rootScope", "$tgResources", "$selectedText", "$tgTemplate", "$compile",
|
|
||||||
"$translate", "tgProjectService", MarkitupDirective])
|
|
|
@ -53,11 +53,13 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
"$tgQueueModelTransformation",
|
"$tgQueueModelTransformation",
|
||||||
"tgErrorHandlingService",
|
"tgErrorHandlingService",
|
||||||
"$tgConfig",
|
"$tgConfig",
|
||||||
"tgProjectService"
|
"tgProjectService",
|
||||||
|
"tgWysiwygService"
|
||||||
]
|
]
|
||||||
|
|
||||||
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
|
constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location,
|
||||||
@log, @appMetaService, @navUrls, @analytics, @translate, @modelTransform, @errorHandlingService, @configService, @projectService) ->
|
@log, @appMetaService, @navUrls, @analytics, @translate, @modelTransform,
|
||||||
|
@errorHandlingService, @configService, @projectService, @wysiwigService) ->
|
||||||
bindMethods(@)
|
bindMethods(@)
|
||||||
|
|
||||||
@scope.usRef = @params.usref
|
@scope.usRef = @params.usref
|
||||||
|
@ -89,7 +91,7 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
|
||||||
description = @translate.instant("US.PAGE_DESCRIPTION", {
|
description = @translate.instant("US.PAGE_DESCRIPTION", {
|
||||||
userStoryStatus: @scope.statusById[@scope.us.status]?.name or "--"
|
userStoryStatus: @scope.statusById[@scope.us.status]?.name or "--"
|
||||||
userStoryPoints: @scope.us.total_points
|
userStoryPoints: @scope.us.total_points
|
||||||
userStoryDescription: angular.element(@scope.us.description_html or "").text()
|
userStoryDescription: angular.element(@wysiwigService.getHTML(@scope.us.description) or "").text()
|
||||||
userStoryClosedTasks: closedTasks
|
userStoryClosedTasks: closedTasks
|
||||||
userStoryTotalTasks: totalTasks
|
userStoryTotalTasks: totalTasks
|
||||||
userStoryProgressPercentage: progressPercentage
|
userStoryProgressPercentage: progressPercentage
|
||||||
|
|
|
@ -218,126 +218,82 @@ WikiSummaryDirective = ($log, $template, $compile, $translate, avatarService) ->
|
||||||
|
|
||||||
module.directive("tgWikiSummary", ["$log", "$tgTemplate", "$compile", "$translate", "tgAvatarService", WikiSummaryDirective])
|
module.directive("tgWikiSummary", ["$log", "$tgTemplate", "$compile", "$translate", "tgAvatarService", WikiSummaryDirective])
|
||||||
|
|
||||||
|
WikiWysiwyg = ($modelTransform, $rootscope, $confirm, attachmentsFullService,
|
||||||
|
$qqueue, $repo, $analytics, wikiHistoryService) ->
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
$scope.editableDescription = false
|
||||||
|
|
||||||
#############################################################################
|
$scope.saveDescription = $qqueue.bindAdd (description, cb) ->
|
||||||
## Editable Wiki Content Directive
|
|
||||||
#############################################################################
|
|
||||||
|
|
||||||
EditableWikiContentDirective = ($window, $document, $repo, $confirm, $loading, $analytics, $qqueue, $translate,
|
|
||||||
$wikiHistoryService) ->
|
|
||||||
link = ($scope, $el, $attrs, $model) ->
|
|
||||||
isEditable = ->
|
|
||||||
return $scope.project.my_permissions.indexOf("modify_wiki_page") != -1
|
|
||||||
|
|
||||||
switchToEditMode = ->
|
|
||||||
$el.find('.edit-wiki-content').show()
|
|
||||||
$el.find('.view-wiki-content').hide()
|
|
||||||
$el.find('textarea').focus()
|
|
||||||
|
|
||||||
switchToReadMode = ->
|
|
||||||
$el.find('.edit-wiki-content').hide()
|
|
||||||
$el.find('.view-wiki-content').show()
|
|
||||||
|
|
||||||
disableEdition = ->
|
|
||||||
$el.find(".view-wiki-content .edit").remove()
|
|
||||||
$el.find(".edit-wiki-content").remove()
|
|
||||||
|
|
||||||
cancelEdition = ->
|
|
||||||
return if not $model.$modelValue.id
|
|
||||||
|
|
||||||
$model.$modelValue.revert()
|
|
||||||
switchToReadMode()
|
|
||||||
|
|
||||||
getSelectedText = ->
|
|
||||||
if $window.getSelection
|
|
||||||
return $window.getSelection().toString()
|
|
||||||
else if $document.selection
|
|
||||||
return $document.selection.createRange().text
|
|
||||||
return null
|
|
||||||
|
|
||||||
save = $qqueue.bindAdd (wiki) ->
|
|
||||||
onSuccess = (wikiPage) ->
|
onSuccess = (wikiPage) ->
|
||||||
if not wiki.id?
|
if not $scope.item.id?
|
||||||
$analytics.trackEvent("wikipage", "create", "create wiki page", 1)
|
$analytics.trackEvent("wikipage", "create", "create wiki page", 1)
|
||||||
|
|
||||||
$model.$setViewValue wikiPage.clone()
|
wikiHistoryService.loadHistoryEntries()
|
||||||
|
|
||||||
$wikiHistoryService.loadHistoryEntries()
|
|
||||||
$confirm.notify("success")
|
$confirm.notify("success")
|
||||||
switchToReadMode()
|
|
||||||
|
|
||||||
onError = ->
|
onError = ->
|
||||||
$confirm.notify("error")
|
$confirm.notify("error")
|
||||||
|
|
||||||
currentLoading = $loading()
|
$scope.item.content = description
|
||||||
.target($el.find('.save'))
|
|
||||||
.start()
|
|
||||||
|
|
||||||
if wiki.id?
|
if $scope.item.id?
|
||||||
promise = $repo.save(wiki).then(onSuccess, onError)
|
promise = $repo.save($scope.item).then(onSuccess, onError)
|
||||||
else
|
else
|
||||||
promise = $repo.create("wiki", wiki).then(onSuccess, onError)
|
promise = $repo.create("wiki", $scope.item).then(onSuccess, onError)
|
||||||
|
|
||||||
promise.finally ->
|
promise.finally(cb)
|
||||||
currentLoading.finish()
|
|
||||||
|
|
||||||
$el.on "click", "a", (event) ->
|
uploadFile = (file, cb) ->
|
||||||
target = angular.element(event.currentTarget)
|
return attachmentsFullService.addAttachment($scope.project.id, $scope.item.id, 'wiki_page', file).then (result) ->
|
||||||
href = target.attr('href')
|
cb(result.getIn(['file', 'name']), result.getIn(['file', 'url']))
|
||||||
|
|
||||||
if href.indexOf("#") == 0
|
$scope.uploadFiles = (files, cb) ->
|
||||||
event.preventDefault()
|
for file in files
|
||||||
$('body').scrollTop($(href).offset().top)
|
uploadFile(file, cb)
|
||||||
|
|
||||||
$el.on "mousedown", ".view-wiki-content", (event) ->
|
$scope.$watch $attrs.model, (value) ->
|
||||||
target = angular.element(event.target)
|
return if not value
|
||||||
return if not isEditable()
|
$scope.item = value
|
||||||
return if event.button == 2
|
$scope.version = value.version
|
||||||
|
$scope.storageKey = $scope.project.id + "-" + value.id + "-" + $attrs.type
|
||||||
|
|
||||||
$el.on "mouseup", ".view-wiki-content", (event) ->
|
$scope.$watch 'project', (project) ->
|
||||||
target = angular.element(event.target)
|
return if !project
|
||||||
return if getSelectedText()
|
|
||||||
return if not isEditable()
|
|
||||||
return if target.is('a')
|
|
||||||
return if target.is('pre')
|
|
||||||
|
|
||||||
switchToEditMode()
|
$scope.editableDescription = project.my_permissions.indexOf("modify_wiki_page") != -1
|
||||||
|
|
||||||
$el.on "click", ".save", debounce 2000, ->
|
|
||||||
save($scope.wiki)
|
|
||||||
|
|
||||||
$el.on "click", ".cancel", ->
|
|
||||||
$scope.$apply(cancelEdition)
|
|
||||||
|
|
||||||
$el.on "keydown", "textarea", (event) ->
|
|
||||||
return if event.keyCode != 27
|
|
||||||
$scope.$applyAsync () ->
|
|
||||||
title = $translate.instant("COMMON.CONFIRM_CLOSE_EDIT_MODE_TITLE")
|
|
||||||
message = $translate.instant("COMMON.CONFIRM_CLOSE_EDIT_MODE_MESSAGE")
|
|
||||||
$confirm.ask(title, null, message).then (askResponse) ->
|
|
||||||
cancelEdition()
|
|
||||||
askResponse.finish()
|
|
||||||
|
|
||||||
$scope.$watch $attrs.ngModel, (wikiPage) ->
|
|
||||||
return if not wikiPage
|
|
||||||
|
|
||||||
if isEditable()
|
|
||||||
$el.addClass('editable')
|
|
||||||
if not wikiPage.id? or $.trim(wikiPage.content).length == 0
|
|
||||||
switchToEditMode()
|
|
||||||
else
|
|
||||||
disableEdition()
|
|
||||||
|
|
||||||
$scope.$on "$destroy", ->
|
|
||||||
$el.off()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
link: link
|
scope: true,
|
||||||
restrict: "EA"
|
link: link,
|
||||||
require: "ngModel"
|
template: """
|
||||||
templateUrl: "wiki/editable-wiki-content.html"
|
<div>
|
||||||
|
<tg-wysiwyg
|
||||||
|
ng-if="editableDescription"
|
||||||
|
version='version'
|
||||||
|
storage-key='storageKey'
|
||||||
|
content='item.content'
|
||||||
|
on-save='saveDescription(text, cb)'
|
||||||
|
on-upload-file='uploadFiles(files, cb)'>
|
||||||
|
</tg-wysiwyg>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wysiwyg"
|
||||||
|
ng-if="!editableDescription && item.content.length"
|
||||||
|
ng-bind-html="item.content | markdownToHTML"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wysiwyg"
|
||||||
|
ng-if="!editableDescription && !item.content.length">
|
||||||
|
{{'COMMON.DESCRIPTION.NO_DESCRIPTION' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
module.directive("tgEditableWikiContent", ["$window", "$document", "$tgRepo", "$tgConfirm", "$tgLoading",
|
module.directive("tgWikiWysiwyg", [
|
||||||
"$tgAnalytics", "$tgQqueue", "$translate", "tgWikiHistoryService",
|
"$tgQueueModelTransformation",
|
||||||
EditableWikiContentDirective])
|
"$rootScope",
|
||||||
|
"$tgConfirm",
|
||||||
|
"tgAttachmentsFullService",
|
||||||
|
"$tgQqueue", "$tgRepo", "$tgAnalytics", "tgWikiHistoryService"
|
||||||
|
WikiWysiwyg])
|
||||||
|
|
|
@ -251,6 +251,16 @@ getRandomDefaultColor = () ->
|
||||||
getDefaulColorList = () ->
|
getDefaulColorList = () ->
|
||||||
return _.clone(DEFAULT_COLOR_LIST)
|
return _.clone(DEFAULT_COLOR_LIST)
|
||||||
|
|
||||||
|
getMatches = (string, regex, index) ->
|
||||||
|
index || (index = 1)
|
||||||
|
matches = []
|
||||||
|
match = null
|
||||||
|
|
||||||
|
while match = regex.exec(string)
|
||||||
|
matches.push(match[index])
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
taiga = @.taiga
|
taiga = @.taiga
|
||||||
taiga.addClass = addClass
|
taiga.addClass = addClass
|
||||||
taiga.nl2br = nl2br
|
taiga.nl2br = nl2br
|
||||||
|
@ -280,3 +290,4 @@ taiga.isPdf = isPdf
|
||||||
taiga.patch = patch
|
taiga.patch = patch
|
||||||
taiga.getRandomDefaultColor = getRandomDefaultColor
|
taiga.getRandomDefaultColor = getRandomDefaultColor
|
||||||
taiga.getDefaulColorList = getDefaulColorList
|
taiga.getDefaulColorList = getDefaulColorList
|
||||||
|
taiga.getMatches = getMatches
|
||||||
|
|
|
@ -0,0 +1,270 @@
|
||||||
|
var MentionExtension = MediumEditor.Extension.extend({
|
||||||
|
name: 'mediumMention',
|
||||||
|
init: function() {
|
||||||
|
this.subscribe('editableKeyup', this.handleKeyup.bind(this));
|
||||||
|
this.subscribe('editableKeydown', this.handleKeydown.bind(this));
|
||||||
|
this.subscribe('blur', this.cancel.bind(this));
|
||||||
|
},
|
||||||
|
isEditMode: function() {
|
||||||
|
return !this.base.origElements.parentNode.classList.contains('read-mode')
|
||||||
|
},
|
||||||
|
cancel: function() {
|
||||||
|
if (this.isEditMode()) {
|
||||||
|
this.hidePanel();
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleKeydown: function(e) {
|
||||||
|
var code = e.keyCode ? e.keyCode : e.which;
|
||||||
|
|
||||||
|
if (this.mentionPanel && code === MediumEditor.util.keyCode.ENTER) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleKeyup: function(e) {
|
||||||
|
var code = e.keyCode ? e.keyCode : e.which;
|
||||||
|
var isSpace = code === MediumEditor.util.keyCode.SPACE;
|
||||||
|
var isBackspace = code === MediumEditor.util.keyCode.BACKSPACE;
|
||||||
|
|
||||||
|
if (this.mentionPanel) {
|
||||||
|
this.keyDownMentionPanel(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var moveKeys = [37, 38, 39, 40];
|
||||||
|
|
||||||
|
if (moveKeys.indexOf(code) !== -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selection = this.document.getSelection();
|
||||||
|
|
||||||
|
if (isBackspace && this.selection.focusNode.nodeName.toLowerCase() === 'p') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSpace && this.selection.rangeCount === 1) {
|
||||||
|
var endChar = this.selection.getRangeAt(0).startOffset;
|
||||||
|
var textContent = this.selection.focusNode.textContent;
|
||||||
|
|
||||||
|
this.word = this.getLastWord(textContent);
|
||||||
|
textContent = textContent.substring(0, endChar);
|
||||||
|
|
||||||
|
if (this.word.length > 1 && ['@', '#', ':'].indexOf(this.word[0]) != -1) {
|
||||||
|
this.wrap();
|
||||||
|
this.showPanel();
|
||||||
|
|
||||||
|
MediumEditor.selection.select(
|
||||||
|
this.document,
|
||||||
|
this.wordNode.firstChild,
|
||||||
|
this.word.length
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (isSpace) {
|
||||||
|
this.cancelMentionSpace();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hidePanel();
|
||||||
|
},
|
||||||
|
reset: function() {
|
||||||
|
this.wordNode = null;
|
||||||
|
this.word = null;
|
||||||
|
this.selection = null;
|
||||||
|
},
|
||||||
|
cancelMentionSpace: function() {
|
||||||
|
if (this.wordNode && this.wordNode.nextSibling) {
|
||||||
|
var textNode = this.document.createTextNode('');
|
||||||
|
textNode.textContent = this.word + '\u00A0';
|
||||||
|
|
||||||
|
this.wordNode.parentNode.replaceChild(textNode, this.wordNode);
|
||||||
|
|
||||||
|
MediumEditor.selection.select(this.document, textNode, this.word.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
wrap: function() {
|
||||||
|
var range = this.selection.getRangeAt(0).cloneRange();
|
||||||
|
|
||||||
|
if (range.startContainer.parentNode.nodeName.toLowerCase() === 'a') {
|
||||||
|
var parentLink = range.startContainer.parentNode.parentNode;
|
||||||
|
var textNode = this.document.createTextNode(range.startContainer.parentNode.innerText);
|
||||||
|
|
||||||
|
parentLink.replaceChild(textNode, range.startContainer.parentNode);
|
||||||
|
|
||||||
|
this.selection.removeAllRanges();
|
||||||
|
|
||||||
|
range = document.createRange();
|
||||||
|
|
||||||
|
range.setStart(textNode, textNode.length);
|
||||||
|
range.setEnd(textNode, textNode.length);
|
||||||
|
|
||||||
|
this.selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!range.startContainer.parentNode.classList.contains('mention')) {
|
||||||
|
this.wordNode = this.document.createElement('span');
|
||||||
|
this.wordNode.classList.add('mention');
|
||||||
|
|
||||||
|
range.setStart(range.startContainer, this.selection.getRangeAt(0).startOffset - this.word.length);
|
||||||
|
range.surroundContents(this.wordNode);
|
||||||
|
|
||||||
|
this.selection.removeAllRanges();
|
||||||
|
this.selection.addRange(range);
|
||||||
|
|
||||||
|
//move cursor to old position
|
||||||
|
range.setStart(range.startContainer, range.endOffset);
|
||||||
|
range.setStart(range.endContainer, range.endOffset);
|
||||||
|
this.selection.removeAllRanges();
|
||||||
|
this.selection.addRange(range);
|
||||||
|
} else {
|
||||||
|
this.wordNode = range.startContainer.parentNode;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
refreshPositionPanel: function() {
|
||||||
|
var bound = this.wordNode.getBoundingClientRect();
|
||||||
|
|
||||||
|
this.mentionPanel.style.top = this.window.pageYOffset + bound.bottom + 'px';
|
||||||
|
this.mentionPanel.style.left = this.window.pageXOffset + bound.left + 'px';
|
||||||
|
},
|
||||||
|
selectMention: function(item) {
|
||||||
|
if (item.image) {
|
||||||
|
var img = document.createElement('img');
|
||||||
|
img.src = item.image;
|
||||||
|
|
||||||
|
this.wordNode.parentNode.replaceChild(img, this.wordNode);
|
||||||
|
this.wordNode = img;
|
||||||
|
} else {
|
||||||
|
var link = document.createElement('a');
|
||||||
|
|
||||||
|
link.setAttribute('href', item.url);
|
||||||
|
|
||||||
|
if (item.ref) {
|
||||||
|
link.innerText = '#' + item.ref + '-' + item.subject;
|
||||||
|
} else {
|
||||||
|
link.innerText = '@' + item.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wordNode.parentNode.replaceChild(link, this.wordNode);
|
||||||
|
this.wordNode = link;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textNode = this.document.createTextNode('');
|
||||||
|
textNode.textContent = '\u00A0';
|
||||||
|
|
||||||
|
this.wordNode.parentNode.insertBefore(textNode, this.wordNode.nextSibling);
|
||||||
|
MediumEditor.selection.select(this.document, textNode, 1);
|
||||||
|
|
||||||
|
var target = this.base.getFocusedElement();
|
||||||
|
|
||||||
|
this.base.events.updateInput(target, {
|
||||||
|
target: target,
|
||||||
|
currentTarget: target
|
||||||
|
});
|
||||||
|
|
||||||
|
this.hidePanel();
|
||||||
|
this.reset();
|
||||||
|
},
|
||||||
|
showPanel: function() {
|
||||||
|
if(document.querySelectorAll('.medium-editor-mention-panel').length) {
|
||||||
|
this.refreshPositionPanel();
|
||||||
|
this.getItems(this.word, this.renderPanel.bind(this));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var el = this.document.createElement('div');
|
||||||
|
el.classList.add('medium-editor-mention-panel');
|
||||||
|
this.mentionPanel = el;
|
||||||
|
this.getEditorOption('elementsContainer').appendChild(el);
|
||||||
|
|
||||||
|
this.refreshPositionPanel();
|
||||||
|
this.getItems(this.word, this.renderPanel.bind(this));
|
||||||
|
},
|
||||||
|
keyDownMentionPanel: function(e) {
|
||||||
|
var code = e.keyCode ? e.keyCode : e.which;
|
||||||
|
var active = this.mentionPanel.querySelector('.active');
|
||||||
|
|
||||||
|
this.wordNode = document.querySelector('span.mention');
|
||||||
|
|
||||||
|
if(!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === MediumEditor.util.keyCode.ENTER) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
var event = document.createEvent('HTMLEvents');
|
||||||
|
event.initEvent('click', true, false);
|
||||||
|
|
||||||
|
active.dispatchEvent(event);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
active.classList.remove('active');
|
||||||
|
|
||||||
|
if (code === 38) {
|
||||||
|
if(active.previousSibling) {
|
||||||
|
active.previousSibling.classList.add('active');
|
||||||
|
} else {
|
||||||
|
active.parentNode.lastChild.classList.add('active');
|
||||||
|
}
|
||||||
|
} else if (code === 40) {
|
||||||
|
if(active.nextSibling) {
|
||||||
|
active.nextSibling.classList.add('active');
|
||||||
|
} else {
|
||||||
|
active.parentNode.firstChild.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderPanel: function(items) {
|
||||||
|
this.mentionPanel.innerHTML = '';
|
||||||
|
|
||||||
|
if (!items.length) return;
|
||||||
|
|
||||||
|
var ul = this.document.createElement('ul');
|
||||||
|
|
||||||
|
ul.classList.add('medium-mention');
|
||||||
|
|
||||||
|
items.forEach(function(it) {
|
||||||
|
var li = this.document.createElement('li');
|
||||||
|
|
||||||
|
if (it.image) {
|
||||||
|
var img = this.document.createElement('img');
|
||||||
|
|
||||||
|
img.src = it.image;
|
||||||
|
li.appendChild(img);
|
||||||
|
|
||||||
|
var textNode = document.createTextNode('');
|
||||||
|
textNode.textContent = ' ' + it.name;
|
||||||
|
|
||||||
|
li.appendChild(textNode);
|
||||||
|
|
||||||
|
} else if (it.ref) {
|
||||||
|
li.innerText = '#' + it.ref + ' - ' + it.subject;
|
||||||
|
} else {
|
||||||
|
li.innerText = '@' + it.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.addEventListener('click', this.selectMention.bind(this, it));
|
||||||
|
|
||||||
|
ul.appendChild(li);
|
||||||
|
}.bind(this));
|
||||||
|
|
||||||
|
ul.firstChild.classList.add('active');
|
||||||
|
|
||||||
|
this.mentionPanel.appendChild(ul);
|
||||||
|
},
|
||||||
|
hidePanel: function() {
|
||||||
|
if (this.mentionPanel) {
|
||||||
|
this.mentionPanel.parentNode.removeChild(this.mentionPanel);
|
||||||
|
this.mentionPanel = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getLastWord: function(text) {
|
||||||
|
var n = text.split(' ');
|
||||||
|
return n[n.length - 1].trim();
|
||||||
|
}
|
||||||
|
});
|
|
@ -228,32 +228,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"WYSIWYG": {
|
"WYSIWYG": {
|
||||||
"H1_BUTTON": "First Level Heading",
|
"OUTDATED": "Another person has made changes while you were editing. Check the new version on the activiy tab before you save your changes.",
|
||||||
"H1_SAMPLE_TEXT": "Your title here...",
|
|
||||||
"H2_BUTTON": "Second Level Heading",
|
|
||||||
"H2_SAMPLE_TEXT": "Your title here...",
|
|
||||||
"H3_BUTTON": "Third Level Heading",
|
|
||||||
"H3_SAMPLE_TEXT": "Your title here...",
|
|
||||||
"BOLD_BUTTON": "Bold",
|
|
||||||
"BOLD_BUTTON_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"ITALIC_BUTTON": "Italic",
|
|
||||||
"ITALIC_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"STRIKE_BUTTON": "Strike",
|
|
||||||
"STRIKE_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"BULLETED_LIST_BUTTON": "Bulleted List",
|
|
||||||
"BULLETED_LIST_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"NUMERIC_LIST_BUTTON": "Numeric List",
|
|
||||||
"NUMERIC_LIST_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"PICTURE_BUTTON": "Picture",
|
|
||||||
"PICTURE_SAMPLE_TEXT": "Your alternative text to picture here...",
|
|
||||||
"LINK_BUTTON": "Link",
|
|
||||||
"LINK_SAMPLE_TEXT": "Your text to link here....",
|
|
||||||
"QUOTE_BLOCK_BUTTON": "Quote Block",
|
|
||||||
"QUOTE_BLOCK_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"CODE_BLOCK_BUTTON": "Code Block",
|
|
||||||
"CODE_BLOCK_SAMPLE_TEXT": "Your text here...",
|
|
||||||
"PREVIEW_BUTTON": "Preview",
|
|
||||||
"EDIT_BUTTON": "Edit",
|
|
||||||
"ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.",
|
"ATTACH_FILE_HELP": "Attach files by dragging & dropping on the textarea above.",
|
||||||
"ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.",
|
"ATTACH_FILE_HELP_SAVE_FIRST": "Save first before if you want to attach files by dragging & dropping on the textarea above.",
|
||||||
"MARKDOWN_HELP": "Markdown syntax help"
|
"MARKDOWN_HELP": "Markdown syntax help"
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/bind-code.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
BindCode = ($sce, $parse, $compile, wysiwygService, wysiwygCodeHightlighterService) ->
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
compile: (tElement, tAttrs) ->
|
||||||
|
tgBindCodeGetter = $parse(tAttrs.tgBindCode)
|
||||||
|
tgBindCodeWatch = $parse tAttrs.tgBindCode, (value) ->
|
||||||
|
return (value || '').toString()
|
||||||
|
|
||||||
|
$compile.$$addBindingClass(tElement)
|
||||||
|
|
||||||
|
return (scope, element, attr) ->
|
||||||
|
$compile.$$addBindingInfo(element, attr.tgBindCode);
|
||||||
|
|
||||||
|
scope.$watch tgBindCodeWatch, () ->
|
||||||
|
html = wysiwygService.getHTML(tgBindCodeGetter(scope))
|
||||||
|
|
||||||
|
element.html($sce.getTrustedHtml(html) || '')
|
||||||
|
|
||||||
|
wysiwygCodeHightlighterService.addHightlighter(element)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaComponents")
|
||||||
|
.directive("tgBindCode", [
|
||||||
|
"$sce",
|
||||||
|
"$parse",
|
||||||
|
"$compile",
|
||||||
|
"tgWysiwygService",
|
||||||
|
"tgWysiwygCodeHightlighterService",
|
||||||
|
BindCode])
|
|
@ -0,0 +1,59 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/comment-edit-wysiwyg.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
CommentEditWysiwyg = (attachmentsFullService) ->
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
types = {
|
||||||
|
userstories: "us",
|
||||||
|
issues: "issue",
|
||||||
|
tasks: "task"
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile = (file, cb) ->
|
||||||
|
return attachmentsFullService.addAttachment($scope.vm.projectId, $scope.vm.comment.comment.id, types[$scope.vm.comment.comment._name], file).then (result) ->
|
||||||
|
cb(result.getIn(['file', 'name']), result.getIn(['file', 'url']))
|
||||||
|
|
||||||
|
$scope.uploadFiles = (files, cb) ->
|
||||||
|
for file in files
|
||||||
|
uploadFile(file, cb)
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: true,
|
||||||
|
link: link,
|
||||||
|
template: """
|
||||||
|
<div>
|
||||||
|
<tg-wysiwyg
|
||||||
|
editonly
|
||||||
|
required
|
||||||
|
content='vm.comment.comment'
|
||||||
|
on-save="vm.saveComment(text, cb)"
|
||||||
|
on-cancel="vm.onEditMode({commentId: vm.comment.id})"
|
||||||
|
on-upload-file='uploadFiles(files, cb)'>
|
||||||
|
</tg-wysiwyg>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaComponents")
|
||||||
|
.directive("tgCommentEditWysiwyg", ["tgAttachmentsFullService", CommentEditWysiwyg])
|
|
@ -0,0 +1,77 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/comment-wysiwyg.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
CommentWysiwyg = (attachmentsFullService) ->
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
$scope.editableDescription = false
|
||||||
|
|
||||||
|
$scope.saveComment = (description, cb) ->
|
||||||
|
$scope.content = ''
|
||||||
|
$scope.vm.type.comment = description
|
||||||
|
$scope.vm.onAddComment({callback: cb})
|
||||||
|
|
||||||
|
types = {
|
||||||
|
userstories: "us",
|
||||||
|
issues: "issue",
|
||||||
|
tasks: "task"
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile = (file, cb) ->
|
||||||
|
return attachmentsFullService.addAttachment($scope.vm.projectId, $scope.vm.type.id, types[$scope.vm.type._name], file).then (result) ->
|
||||||
|
cb(result.getIn(['file', 'name']), result.getIn(['file', 'url']))
|
||||||
|
|
||||||
|
$scope.onChange = (markdown) ->
|
||||||
|
$scope.vm.type.comment = markdown
|
||||||
|
|
||||||
|
$scope.uploadFiles = (files, cb) ->
|
||||||
|
for file in files
|
||||||
|
uploadFile(file, cb)
|
||||||
|
|
||||||
|
$scope.content = ''
|
||||||
|
|
||||||
|
$scope.$watch "vm.type", (value) ->
|
||||||
|
return if not value
|
||||||
|
|
||||||
|
$scope.storageKey = "comment-" + value.project + "-" + value.id + "-" + value._name
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: true,
|
||||||
|
link: link,
|
||||||
|
template: """
|
||||||
|
<div>
|
||||||
|
<tg-wysiwyg
|
||||||
|
required
|
||||||
|
not-persist
|
||||||
|
placeholder='{{"COMMENTS.TYPE_NEW_COMMENT" | translate}}'
|
||||||
|
storage-key='storageKey'
|
||||||
|
content='content'
|
||||||
|
on-save='saveComment(text, cb)'
|
||||||
|
on-upload-file='uploadFiles(files, cb)'>
|
||||||
|
</tg-wysiwyg>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaComponents")
|
||||||
|
.directive("tgCommentWysiwyg", ["tgAttachmentsFullService", CommentWysiwyg])
|
|
@ -0,0 +1,100 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/item-wysiwyg.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
# Used in details descriptions
|
||||||
|
ItemWysiwyg = ($modelTransform, $rootscope, $confirm, attachmentsFullService, $translate) ->
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
$scope.editableDescription = false
|
||||||
|
|
||||||
|
$scope.saveDescription = (description, cb) ->
|
||||||
|
transform = $modelTransform.save (item) ->
|
||||||
|
item.description = description
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
transform.then ->
|
||||||
|
$confirm.notify("success")
|
||||||
|
$rootscope.$broadcast("object:updated")
|
||||||
|
|
||||||
|
transform.then null, ->
|
||||||
|
$confirm.notify("error")
|
||||||
|
|
||||||
|
transform.finally(cb)
|
||||||
|
|
||||||
|
uploadFile = (file, cb) ->
|
||||||
|
return attachmentsFullService.addAttachment($scope.project.id, $scope.item.id, $attrs.type, file).then (result) ->
|
||||||
|
cb(result.getIn(['file', 'name']), result.getIn(['file', 'url']))
|
||||||
|
|
||||||
|
$scope.uploadFiles = (files, cb) ->
|
||||||
|
for file in files
|
||||||
|
uploadFile(file, cb)
|
||||||
|
|
||||||
|
$scope.$watch $attrs.model, (value) ->
|
||||||
|
return if not value
|
||||||
|
$scope.item = value
|
||||||
|
$scope.version = value.version
|
||||||
|
$scope.storageKey = $scope.project.id + "-" + value.id + "-" + $attrs.type
|
||||||
|
|
||||||
|
$scope.$watch 'project', (project) ->
|
||||||
|
return if !project
|
||||||
|
|
||||||
|
$scope.editableDescription = project.my_permissions.indexOf($attrs.requiredPerm) != -1
|
||||||
|
|
||||||
|
return {
|
||||||
|
scope: true,
|
||||||
|
link: link,
|
||||||
|
template: """
|
||||||
|
<div>
|
||||||
|
<tg-wysiwyg
|
||||||
|
ng-if="editableDescription"
|
||||||
|
placeholder='{{"COMMON.DESCRIPTION.EMPTY" | translate}}'
|
||||||
|
version='version'
|
||||||
|
storage-key='storageKey'
|
||||||
|
content='item.description'
|
||||||
|
on-save='saveDescription(text, cb)'
|
||||||
|
on-upload-file='uploadFiles(files, cb)'>
|
||||||
|
</tg-wysiwyg>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wysiwyg"
|
||||||
|
ng-if="!editableDescription && item.description.length"
|
||||||
|
ng-bind-html="item.description | markdownToHTML"></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="wysiwyg"
|
||||||
|
ng-if="!editableDescription && !item.description.length">
|
||||||
|
{{'COMMON.DESCRIPTION.NO_DESCRIPTION' | translate}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaComponents")
|
||||||
|
.directive("tgItemWysiwyg", [
|
||||||
|
"$tgQueueModelTransformation",
|
||||||
|
"$rootScope",
|
||||||
|
"$tgConfirm",
|
||||||
|
"tgAttachmentsFullService",
|
||||||
|
"$translate",
|
||||||
|
ItemWysiwyg])
|
|
@ -0,0 +1,180 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/wysiwyg-code-hightlighter.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
class WysiwygCodeHightlighterService
|
||||||
|
constructor: () ->
|
||||||
|
if !@.languages
|
||||||
|
@.loadLanguages()
|
||||||
|
|
||||||
|
loadLanguages: () ->
|
||||||
|
$.getJSON("/#{window._version}/prism/prism-languages.json").then (_languages_) =>
|
||||||
|
@.languages = _.map _languages_, (it) ->
|
||||||
|
it.url = "/#{window._version}/prism/" + it.file
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
getLanguageInClassList: (classes) ->
|
||||||
|
lan = _.find @.languages, (it) ->
|
||||||
|
return !!_.find classes, (className) ->
|
||||||
|
return 'language-' + it.name == className
|
||||||
|
|
||||||
|
return if lan then lan.name else null
|
||||||
|
|
||||||
|
addCodeLanguageSelectors: (mediumInstance) ->
|
||||||
|
$(mediumInstance.elements[0]).find('code').each (index, code) =>
|
||||||
|
if !code.classList.contains('has-code-lan-selector')
|
||||||
|
code.classList.add('has-code-lan-selector') # prevent multi instanciate
|
||||||
|
|
||||||
|
currentLan = @.getLanguageInClassList(code.classList)
|
||||||
|
|
||||||
|
id = new Date().getTime()
|
||||||
|
|
||||||
|
text = document.createTextNode(currentLan || 'text')
|
||||||
|
|
||||||
|
tab = document.createElement('div')
|
||||||
|
tab.appendChild(text)
|
||||||
|
tab.addEventListener 'click', () =>
|
||||||
|
@.searchLanguage tab, (lan) =>
|
||||||
|
if lan
|
||||||
|
tab.innerText = lan
|
||||||
|
@.updatePositionCodeTab(code.parentElement, tab)
|
||||||
|
code.classList.add('language-' + lan)
|
||||||
|
code.classList.add(lan)
|
||||||
|
|
||||||
|
document.body.appendChild(tab)
|
||||||
|
|
||||||
|
code.classList.add(id)
|
||||||
|
code.dataset.tab = tab
|
||||||
|
|
||||||
|
tab.classList.add('code-language-selector') # styles
|
||||||
|
tab.classList.add('medium-' + mediumInstance.id) # used to delete
|
||||||
|
tab.dataset.tabId = id
|
||||||
|
|
||||||
|
@.updatePositionCodeTab(code.parentElement, tab)
|
||||||
|
|
||||||
|
removeCodeLanguageSelectors: (mediumInstance) ->
|
||||||
|
return if !mediumInstance || !mediumInstance.elements
|
||||||
|
|
||||||
|
$(mediumInstance.elements[0]).find('code').each (index, code) ->
|
||||||
|
$(code).removeClass('has-code-lan-selector')
|
||||||
|
|
||||||
|
$('.medium-' + mediumInstance.id).remove()
|
||||||
|
|
||||||
|
updatePositionCodeTab: (node, tab) ->
|
||||||
|
preRects = node.getBoundingClientRect()
|
||||||
|
|
||||||
|
tab.style.top = (preRects.top + $(window).scrollTop()) + 'px'
|
||||||
|
tab.style.left = (preRects.left + preRects.width - tab.offsetWidth) + 'px'
|
||||||
|
|
||||||
|
getCodeLanHTML: (filter = '') ->
|
||||||
|
template = _.template("""
|
||||||
|
<% _.forEach(lans, function(lan) { %>
|
||||||
|
<li><%- lan %></li><% });
|
||||||
|
%>
|
||||||
|
""");
|
||||||
|
|
||||||
|
filteresLans = _.map @.languages, (it) -> it.name
|
||||||
|
|
||||||
|
if filter.length
|
||||||
|
filteresLans = _.filter filteresLans, (it) ->
|
||||||
|
return it.indexOf(filter) != -1
|
||||||
|
|
||||||
|
return template({ 'lans': filteresLans });
|
||||||
|
|
||||||
|
searchLanguage: (tab, cb) ->
|
||||||
|
search = document.createElement('div')
|
||||||
|
|
||||||
|
search.className = 'code-language-search'
|
||||||
|
|
||||||
|
preRects = tab.getBoundingClientRect()
|
||||||
|
search.style.top = (preRects.top + $(window).scrollTop() + preRects.height) + 'px'
|
||||||
|
search.style.left = preRects.left + 'px'
|
||||||
|
|
||||||
|
input = document.createElement('input')
|
||||||
|
input.setAttribute('type', 'text')
|
||||||
|
|
||||||
|
ul = document.createElement('ul')
|
||||||
|
|
||||||
|
ul.innerHTML = @.getCodeLanHTML()
|
||||||
|
|
||||||
|
search.appendChild(input)
|
||||||
|
search.appendChild(ul)
|
||||||
|
|
||||||
|
document.body.appendChild(search)
|
||||||
|
|
||||||
|
input.focus()
|
||||||
|
|
||||||
|
close = () ->
|
||||||
|
search.remove()
|
||||||
|
$(document.body).off('.leave-search-codelan')
|
||||||
|
|
||||||
|
clickedInSearchBox = (target) ->
|
||||||
|
return $(search).is(target) || !!$(search).has(target).length
|
||||||
|
|
||||||
|
$(document.body).on 'mouseup.leave-search-codelan', (e) ->
|
||||||
|
if !clickedInSearchBox(e.target)
|
||||||
|
cb(null)
|
||||||
|
close()
|
||||||
|
|
||||||
|
$(input).on 'keyup', (e) =>
|
||||||
|
filter = e.currentTarget.value
|
||||||
|
ul.innerHTML = @.getCodeLanHTML(filter)
|
||||||
|
|
||||||
|
$(ul).on 'click', 'li', (e) ->
|
||||||
|
cb(e.currentTarget.innerText)
|
||||||
|
close()
|
||||||
|
|
||||||
|
loadLanguage: (lan) ->
|
||||||
|
return new Promise (resolve) ->
|
||||||
|
if !Prism.languages[lan]
|
||||||
|
ljs.load("/#{window._version}/prism/prism-#{lan}.min.js", resolve)
|
||||||
|
else
|
||||||
|
resolve()
|
||||||
|
|
||||||
|
removeHightlighter: (element) ->
|
||||||
|
codes = $(element).find('code')
|
||||||
|
|
||||||
|
codes.each (index, code) ->
|
||||||
|
code.innerHTML = code.innerText
|
||||||
|
|
||||||
|
addHightlighter: (element) ->
|
||||||
|
codes = $(element).find('code')
|
||||||
|
|
||||||
|
codes.each (index, code) =>
|
||||||
|
lan = @.getLanguageInClassList(code.classList)
|
||||||
|
|
||||||
|
if lan
|
||||||
|
@.loadLanguage(lan).then () -> Prism.highlightElement(code)
|
||||||
|
|
||||||
|
updateCodeLanguageSelector: (mediumInstance) ->
|
||||||
|
$('.medium-' + mediumInstance.id).each (index, tab) =>
|
||||||
|
node = $('.' + tab.dataset.tabId)
|
||||||
|
|
||||||
|
if !node.length
|
||||||
|
tab.remove()
|
||||||
|
else
|
||||||
|
@.updatePositionCodeTab(node.parent()[0], tab)
|
||||||
|
|
||||||
|
angular.module("taigaComponents")
|
||||||
|
.service("tgWysiwygCodeHightlighterService", WysiwygCodeHightlighterService)
|
|
@ -0,0 +1,117 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/wysiwyg-mention.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
class WysiwygMentionService
|
||||||
|
@.$inject = [
|
||||||
|
"tgProjectService",
|
||||||
|
"tgWysiwygService",
|
||||||
|
"$tgNavUrls",
|
||||||
|
"$tgResources"
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: (@projectService, @wysiwygService, @navurls, @rs) ->
|
||||||
|
@.cancelablePromise = null
|
||||||
|
|
||||||
|
searchEmoji: (name, cb) ->
|
||||||
|
filteredEmojis = @wysiwygService.searchEmojiByName(name)
|
||||||
|
filteredEmojis = filteredEmojis.slice(0, 10)
|
||||||
|
|
||||||
|
cb(filteredEmojis)
|
||||||
|
|
||||||
|
searchUser: (term, cb) ->
|
||||||
|
searchProps = ['username', 'full_name', 'full_name_display']
|
||||||
|
|
||||||
|
users = @projectService.project.toJS().members.filter (user) =>
|
||||||
|
for prop in searchProps
|
||||||
|
if taiga.slugify(user[prop]).indexOf(term) >= 0
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
users = users.slice(0, 10).map (it) =>
|
||||||
|
it.url = @navurls.resolve('user-profile', {
|
||||||
|
project: @projectService.project.get('slug'),
|
||||||
|
username: it.username
|
||||||
|
})
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
cb(users)
|
||||||
|
|
||||||
|
searchItem: (term) ->
|
||||||
|
return new Promise (resolve, reject) =>
|
||||||
|
term = taiga.slugify(term)
|
||||||
|
|
||||||
|
searchTypes = ['issues', 'tasks', 'userstories']
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
issues: "project-issues-detail",
|
||||||
|
tasks: "project-tasks-detail",
|
||||||
|
userstories: "project-userstories-detail"
|
||||||
|
}
|
||||||
|
|
||||||
|
searchProps = ['ref', 'subject']
|
||||||
|
|
||||||
|
filter = (item) =>
|
||||||
|
for prop in searchProps
|
||||||
|
if taiga.slugify(item[prop]).indexOf(term) >= 0
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
@.cancelablePromise.abort() if @.cancelablePromise
|
||||||
|
|
||||||
|
@.cancelablePromise = @rs.search.do(@projectService.project.get('id'), term)
|
||||||
|
|
||||||
|
@.cancelablePromise.then (res) =>
|
||||||
|
# ignore wikipages if they're the only results. can't exclude them in search
|
||||||
|
if res.count < 1 or res.count == res.wikipages.length
|
||||||
|
resolve([])
|
||||||
|
else
|
||||||
|
result = []
|
||||||
|
for type in searchTypes
|
||||||
|
if res[type] and res[type].length > 0
|
||||||
|
items = res[type].filter(filter)
|
||||||
|
items = items.map (it) =>
|
||||||
|
it.url = @navurls.resolve(urls[type], {
|
||||||
|
project: @projectService.project.get('slug'),
|
||||||
|
ref: it.ref
|
||||||
|
})
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
result = result.concat(items)
|
||||||
|
|
||||||
|
resolve(result.slice(0, 10))
|
||||||
|
|
||||||
|
|
||||||
|
search: (mention) ->
|
||||||
|
return new Promise (resolve) =>
|
||||||
|
if '#'.indexOf(mention[0]) != -1
|
||||||
|
@.searchItem(mention.replace('#', '')).then(resolve)
|
||||||
|
else if '@'.indexOf(mention[0]) != -1
|
||||||
|
@.searchUser(mention.replace('@', ''), resolve)
|
||||||
|
else if ':'.indexOf(mention[0]) != -1
|
||||||
|
@.searchEmoji(mention.replace(':', ''), resolve)
|
||||||
|
|
||||||
|
|
||||||
|
angular.module("taigaComponents").service("tgWysiwygMentionService", WysiwygMentionService)
|
|
@ -0,0 +1,401 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/wysiwyg.directive.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
taiga = @.taiga
|
||||||
|
bindOnce = @.taiga.bindOnce
|
||||||
|
|
||||||
|
Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoader, wysiwygCodeHightlighterService, wysiwygMentionService, analytics) ->
|
||||||
|
|
||||||
|
isCodeBlockSelected = (range, elm) ->
|
||||||
|
return !!$(range.endContainer).parentsUntil('.editor', 'code').length
|
||||||
|
|
||||||
|
removeCodeBlockAndHightlight = (range, elm) ->
|
||||||
|
code = $(range.endContainer).closest('code')[0]
|
||||||
|
pre = code.parentNode
|
||||||
|
|
||||||
|
p = document.createElement('p')
|
||||||
|
p.innerText = code.innerText
|
||||||
|
|
||||||
|
pre.parentNode.replaceChild(p, pre)
|
||||||
|
|
||||||
|
wysiwygCodeHightlighterService.removeCodeLanguageSelectors(elm)
|
||||||
|
|
||||||
|
addCodeBlockAndHightlight = (range, elm) ->
|
||||||
|
pre = document.createElement('pre')
|
||||||
|
code = document.createElement('code')
|
||||||
|
|
||||||
|
pre.appendChild(code)
|
||||||
|
code.appendChild(range.extractContents())
|
||||||
|
range.insertNode(pre)
|
||||||
|
|
||||||
|
elm.checkContentChanged()
|
||||||
|
|
||||||
|
wysiwygCodeHightlighterService.addCodeLanguageSelectors(elm)
|
||||||
|
|
||||||
|
# MediumEditor extension to add <code>
|
||||||
|
CodeButton = MediumEditor.extensions.button.extend({
|
||||||
|
name: 'code',
|
||||||
|
init: () ->
|
||||||
|
this.button = this.document.createElement('button')
|
||||||
|
this.button.classList.add('medium-editor-action')
|
||||||
|
this.button.innerHTML = '<b>Code</b>'
|
||||||
|
this.button.title = 'Code'
|
||||||
|
this.on(this.button, 'click', this.handleClick.bind(this))
|
||||||
|
|
||||||
|
getButton: () ->
|
||||||
|
return this.button
|
||||||
|
|
||||||
|
tagNames: ['code']
|
||||||
|
|
||||||
|
handleClick: (event) ->
|
||||||
|
range = MediumEditor.selection.getSelectionRange(self.document)
|
||||||
|
|
||||||
|
if isCodeBlockSelected(range, this.base)
|
||||||
|
removeCodeBlockAndHightlight(range, this.base)
|
||||||
|
else
|
||||||
|
addCodeBlockAndHightlight(range, this.base)
|
||||||
|
})
|
||||||
|
|
||||||
|
# bug
|
||||||
|
# <pre><code></code></pre> the enter key press doesn't work
|
||||||
|
oldIsBlockContainer = MediumEditor.util.isBlockContainer
|
||||||
|
|
||||||
|
MediumEditor.util.isBlockContainer = (element) ->
|
||||||
|
if !element
|
||||||
|
return oldIsBlockContainer(element)
|
||||||
|
|
||||||
|
if element.tagName
|
||||||
|
tagName = element.tagName
|
||||||
|
else
|
||||||
|
tagName = element.parentNode.tagName
|
||||||
|
|
||||||
|
if tagName.toLowerCase() == 'code'
|
||||||
|
return true
|
||||||
|
|
||||||
|
return oldIsBlockContainer(element)
|
||||||
|
|
||||||
|
link = ($scope, $el, $attrs) ->
|
||||||
|
mediumInstance = null
|
||||||
|
editorMedium = $el.find('.medium')
|
||||||
|
editorMarkdown = $el.find('.markdown')
|
||||||
|
|
||||||
|
isEditOnly = !!$attrs.$attr.editonly
|
||||||
|
notPersist = !!$attrs.$attr.notPersist
|
||||||
|
|
||||||
|
$scope.required = !!$attrs.$attr.required
|
||||||
|
$scope.editMode = isEditOnly || false
|
||||||
|
$scope.mode = $storage.get('editor-mode', 'html')
|
||||||
|
|
||||||
|
wysiwygService.loadEmojis()
|
||||||
|
|
||||||
|
setHtmlMedium = (markdown) ->
|
||||||
|
html = wysiwygService.getHTML(markdown)
|
||||||
|
editorMedium.html(html)
|
||||||
|
|
||||||
|
$scope.setMode = (mode) ->
|
||||||
|
$storage.set('editor-mode', mode)
|
||||||
|
|
||||||
|
if mode == 'markdown'
|
||||||
|
updateMarkdownWithCurrentHtml()
|
||||||
|
else
|
||||||
|
setHtmlMedium($scope.markdown)
|
||||||
|
|
||||||
|
$scope.mode = mode
|
||||||
|
mediumInstance.trigger('editableBlur', {}, editorMedium[0])
|
||||||
|
|
||||||
|
$scope.save = () ->
|
||||||
|
if $scope.mode == 'html'
|
||||||
|
updateMarkdownWithCurrentHtml()
|
||||||
|
|
||||||
|
return if $scope.required && !$scope.markdown.length
|
||||||
|
|
||||||
|
$scope.saving = true
|
||||||
|
$scope.outdated = false
|
||||||
|
|
||||||
|
$scope.onSave({text: $scope.markdown, cb: saveEnd})
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
$scope.cancel = () ->
|
||||||
|
if !isEditOnly
|
||||||
|
$scope.editMode = false
|
||||||
|
|
||||||
|
if notPersist
|
||||||
|
clean()
|
||||||
|
else if $scope.mode == 'html'
|
||||||
|
setHtmlMedium($scope.content)
|
||||||
|
|
||||||
|
$scope.markdown = $scope.content
|
||||||
|
|
||||||
|
discardLocalStorage()
|
||||||
|
mediumInstance.trigger('blur', {}, editorMedium[0])
|
||||||
|
$scope.outdated = false
|
||||||
|
|
||||||
|
$scope.onCancel()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
clean = () ->
|
||||||
|
$scope.markdown = ''
|
||||||
|
editorMedium.html('')
|
||||||
|
|
||||||
|
refreshExtras = () ->
|
||||||
|
animationFrame.add () ->
|
||||||
|
if $scope.mode == 'html'
|
||||||
|
if $scope.editMode
|
||||||
|
wysiwygCodeHightlighterService.addCodeLanguageSelectors(mediumInstance)
|
||||||
|
wysiwygCodeHightlighterService.removeHightlighter(mediumInstance.elements[0])
|
||||||
|
else
|
||||||
|
wysiwygCodeHightlighterService.addHightlighter(mediumInstance.elements[0])
|
||||||
|
wysiwygCodeHightlighterService.removeCodeLanguageSelectors(mediumInstance)
|
||||||
|
else
|
||||||
|
wysiwygCodeHightlighterService.removeHightlighter(mediumInstance.elements[0])
|
||||||
|
wysiwygCodeHightlighterService.removeCodeLanguageSelectors(mediumInstance)
|
||||||
|
|
||||||
|
saveEnd = () ->
|
||||||
|
$scope.saving = false
|
||||||
|
|
||||||
|
if !isEditOnly
|
||||||
|
$scope.editMode = false
|
||||||
|
|
||||||
|
if notPersist
|
||||||
|
clean()
|
||||||
|
|
||||||
|
discardLocalStorage()
|
||||||
|
mediumInstance.trigger('blur', {}, editorMedium[0])
|
||||||
|
|
||||||
|
analytics.trackEvent('develop', 'save wysiwyg', $scope.mode, 1)
|
||||||
|
|
||||||
|
uploadEnd = (name, url) ->
|
||||||
|
if taiga.isImage(name)
|
||||||
|
mediumInstance.pasteHTML("<img src='" + url + "' /><br/>")
|
||||||
|
else
|
||||||
|
name = $('<div/>').text(name).html()
|
||||||
|
mediumInstance.pasteHTML("<a target='_blank' href='" + url + "'>" + name + "</a><br/>")
|
||||||
|
|
||||||
|
isOutdated = () ->
|
||||||
|
store = $storage.get($scope.storageKey)
|
||||||
|
|
||||||
|
if store && store.version && store.version != $scope.version
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
isDraft = () ->
|
||||||
|
store = $storage.get($scope.storageKey)
|
||||||
|
|
||||||
|
if store
|
||||||
|
return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
getCurrentContent = () ->
|
||||||
|
store = $storage.get($scope.storageKey)
|
||||||
|
|
||||||
|
if store
|
||||||
|
return store.text
|
||||||
|
|
||||||
|
return $scope.content
|
||||||
|
|
||||||
|
discardLocalStorage = () ->
|
||||||
|
$storage.remove($scope.storageKey)
|
||||||
|
|
||||||
|
cancelWithConfirmation = () ->
|
||||||
|
if $scope.content == $scope.markdown
|
||||||
|
$scope.cancel()
|
||||||
|
|
||||||
|
document.activeElement.blur()
|
||||||
|
document.body.click()
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
title = $translate.instant("COMMON.CONFIRM_CLOSE_EDIT_MODE_TITLE")
|
||||||
|
message = $translate.instant("COMMON.CONFIRM_CLOSE_EDIT_MODE_MESSAGE")
|
||||||
|
|
||||||
|
$confirm.ask(title, null, message).then (askResponse) ->
|
||||||
|
$scope.cancel()
|
||||||
|
askResponse.finish()
|
||||||
|
|
||||||
|
updateMarkdownWithCurrentHtml = () ->
|
||||||
|
$scope.markdown = wysiwygService.getMarkdown(editorMedium.html())
|
||||||
|
|
||||||
|
localSave = (markdown) ->
|
||||||
|
if $scope.storageKey
|
||||||
|
store = {}
|
||||||
|
store.version = $scope.version || 0
|
||||||
|
store.text = markdown
|
||||||
|
$storage.set($scope.storageKey, store)
|
||||||
|
|
||||||
|
change = () ->
|
||||||
|
if $scope.mode == 'html'
|
||||||
|
updateMarkdownWithCurrentHtml()
|
||||||
|
wysiwygCodeHightlighterService.updateCodeLanguageSelector(mediumInstance)
|
||||||
|
|
||||||
|
localSave($scope.markdown)
|
||||||
|
|
||||||
|
$scope.onChange({markdown: $scope.markdown})
|
||||||
|
|
||||||
|
throttleChange = _.throttle(change, 200)
|
||||||
|
|
||||||
|
create = (text, editMode=false) ->
|
||||||
|
if text.length
|
||||||
|
html = wysiwygService.getHTML(text)
|
||||||
|
editorMedium.html(html)
|
||||||
|
|
||||||
|
mediumInstance = new MediumEditor(editorMedium[0], {
|
||||||
|
targetBlank: true,
|
||||||
|
imageDragging: false,
|
||||||
|
placeholder: {
|
||||||
|
text: $scope.placeholder
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
buttons: [
|
||||||
|
'bold',
|
||||||
|
'italic',
|
||||||
|
'strikethrough',
|
||||||
|
'anchor',
|
||||||
|
'image',
|
||||||
|
'orderedlist',
|
||||||
|
'unorderedlist',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'quote',
|
||||||
|
'removeFormat',
|
||||||
|
'code'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
extensions: {
|
||||||
|
code: new CodeButton(),
|
||||||
|
autolist: new AutoList(),
|
||||||
|
mediumMention: new MentionExtension({
|
||||||
|
getItems: (mention, mentionCb) ->
|
||||||
|
wysiwygMentionService.search(mention).then(mentionCb)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$scope.changeMarkdown = throttleChange
|
||||||
|
|
||||||
|
mediumInstance.subscribe 'editableInput', (e) ->
|
||||||
|
$scope.$applyAsync(throttleChange)
|
||||||
|
|
||||||
|
mediumInstance.subscribe "editableClick", (e) ->
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
|
if e.target.href
|
||||||
|
window.open(e.target.href)
|
||||||
|
|
||||||
|
mediumInstance.subscribe 'focus', (event) ->
|
||||||
|
$scope.$applyAsync () ->
|
||||||
|
if !$scope.editMode
|
||||||
|
$scope.editMode = true
|
||||||
|
|
||||||
|
mediumInstance.subscribe 'editableDrop', (event) ->
|
||||||
|
$scope.onUploadFile({files: event.dataTransfer.files, cb: uploadEnd})
|
||||||
|
|
||||||
|
mediumInstance.subscribe 'editableKeydown', (e) ->
|
||||||
|
code = if e.keyCode then e.keyCode else e.which
|
||||||
|
|
||||||
|
mention = $('.medium-mention')
|
||||||
|
|
||||||
|
if (code == 40 || code == 38) && mention.length
|
||||||
|
e.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if $scope.editMode && code == 27
|
||||||
|
e.stopPropagation()
|
||||||
|
$scope.$applyAsync(cancelWithConfirmation)
|
||||||
|
else if code == 27
|
||||||
|
editorMedium.blur()
|
||||||
|
|
||||||
|
$scope.editMode = editMode
|
||||||
|
|
||||||
|
$scope.$applyAsync(refreshExtras)
|
||||||
|
|
||||||
|
$scope.$watch () ->
|
||||||
|
return $scope.mode + ":" + $scope.editMode
|
||||||
|
, () ->
|
||||||
|
$scope.$applyAsync(refreshExtras)
|
||||||
|
|
||||||
|
unwatch = $scope.$watch 'content', (content) ->
|
||||||
|
if !_.isUndefined(content)
|
||||||
|
$scope.outdated = isOutdated()
|
||||||
|
|
||||||
|
if !mediumInstance && isDraft()
|
||||||
|
$scope.editMode = true
|
||||||
|
|
||||||
|
if $scope.markdown == content
|
||||||
|
return
|
||||||
|
|
||||||
|
content = getCurrentContent()
|
||||||
|
|
||||||
|
$scope.markdown = content
|
||||||
|
|
||||||
|
if mediumInstance
|
||||||
|
mediumInstance.destroy()
|
||||||
|
|
||||||
|
if tgLoader.open()
|
||||||
|
unwatchLoader = tgLoader.onEnd () ->
|
||||||
|
create(content, $scope.editMode)
|
||||||
|
unwatchLoader()
|
||||||
|
else
|
||||||
|
create(content, $scope.editMode)
|
||||||
|
|
||||||
|
unwatch()
|
||||||
|
|
||||||
|
$scope.$on "$destroy", () ->
|
||||||
|
if mediumInstance
|
||||||
|
wysiwygCodeHightlighterService.removeCodeLanguageSelectors(mediumInstance)
|
||||||
|
mediumInstance.destroy()
|
||||||
|
|
||||||
|
return {
|
||||||
|
templateUrl: "common/components/wysiwyg-toolbar.html",
|
||||||
|
scope: {
|
||||||
|
placeholder: '@',
|
||||||
|
version: '<',
|
||||||
|
storageKey: '<',
|
||||||
|
content: '<',
|
||||||
|
onCancel: '&',
|
||||||
|
onSave: '&',
|
||||||
|
onUploadFile: '&',
|
||||||
|
onChange: '&'
|
||||||
|
},
|
||||||
|
link: link
|
||||||
|
}
|
||||||
|
|
||||||
|
angular.module("taigaComponents").directive("tgWysiwyg", [
|
||||||
|
"$translate",
|
||||||
|
"$tgConfirm",
|
||||||
|
"$tgStorage",
|
||||||
|
"tgWysiwygService",
|
||||||
|
"animationFrame",
|
||||||
|
"tgLoader",
|
||||||
|
"tgWysiwygCodeHightlighterService",
|
||||||
|
"tgWysiwygMentionService",
|
||||||
|
"$tgAnalytics",
|
||||||
|
Medium
|
||||||
|
])
|
|
@ -1,6 +1,5 @@
|
||||||
.wysiwyg {
|
.wysiwyg {
|
||||||
line-height: 1.4rem;
|
line-height: 1.4rem;
|
||||||
margin-bottom: 2rem;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -37,7 +36,7 @@
|
||||||
ol {
|
ol {
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
list-style-position: outside;
|
list-style-position: outside;
|
||||||
margin-bottom: 0;
|
margin-bottom: 1rem;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-left: 2em;
|
padding-left: 2em;
|
||||||
ul,
|
ul,
|
||||||
|
@ -48,6 +47,15 @@
|
||||||
ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
}
|
}
|
||||||
|
.list-stye-none {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
b {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
dl {
|
dl {
|
||||||
dt {
|
dt {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
@ -63,6 +71,7 @@
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: $primary;
|
color: $primary;
|
||||||
|
cursor: pointer;
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primary-light;
|
color: $primary-light;
|
||||||
}
|
}
|
||||||
|
@ -134,3 +143,113 @@
|
||||||
border: 1px solid $whitish;
|
border: 1px solid $whitish;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.medium-editor-mention-panel {
|
||||||
|
background-color: $white;
|
||||||
|
border: 1px solid $gray-light;
|
||||||
|
position: absolute;
|
||||||
|
ul {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
border-top: 1px solid $gray-light;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 5px;
|
||||||
|
&:first-child {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
background-color: $primary-dark;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tg-wysiwyg {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
.outdated {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
.tools {
|
||||||
|
padding-left: 1rem;
|
||||||
|
a {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
fill: $gray-light;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.editor {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.mode-editor {
|
||||||
|
span {
|
||||||
|
color: $gray-light;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.medium-editor-placeholder,
|
||||||
|
.markdown-editor-placeholder {
|
||||||
|
color: $gray-light;
|
||||||
|
padding-left: 1rem;
|
||||||
|
&::after { // overwrite medium css
|
||||||
|
color: $gray-light;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.markdown:not(.empty) {
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.read-mode {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.edit-mode {
|
||||||
|
.markdown,
|
||||||
|
.medium {
|
||||||
|
border: 1px solid $gray-light;
|
||||||
|
}
|
||||||
|
.medium-editor-element {
|
||||||
|
min-height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mention {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-language-selector {
|
||||||
|
@include font-size(xsmall);
|
||||||
|
background-color: $white;
|
||||||
|
border: 1px solid $gray-light;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: .2rem .5rem 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-language-search {
|
||||||
|
@include font-size(xsmall);
|
||||||
|
background-color: $white;
|
||||||
|
border: 1px solid $gray-light;
|
||||||
|
position: absolute;
|
||||||
|
ul {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 0;
|
||||||
|
max-height: 20vh;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
padding: .2rem .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override medium styles
|
||||||
|
.medium-editor-toolbar li .medium-editor-button-active {
|
||||||
|
color: $primary-light;
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
###
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino Garcia <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán Merino <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 Juan Francisco Alcántara <juanfran.alcantara@kaleidos.net>
|
||||||
|
# Copyright (C) 2014-2016 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/components/wysiwyg/wysiwyg.service.coffee
|
||||||
|
###
|
||||||
|
|
||||||
|
class WysiwygService
|
||||||
|
constructor: (@wysiwygCodeHightlighterService) ->
|
||||||
|
|
||||||
|
searchEmojiByName: (name) ->
|
||||||
|
return _.filter @.emojis, (it) -> it.name.indexOf(name) != -1
|
||||||
|
|
||||||
|
setEmojiImagePath: (emojis) ->
|
||||||
|
@.emojis = _.map emojis, (it) ->
|
||||||
|
it.image = "/#{window._version}/emojis/" + it.image
|
||||||
|
|
||||||
|
return it
|
||||||
|
|
||||||
|
loadEmojis: () ->
|
||||||
|
$.getJSON("/#{window._version}/emojis/emojis-data.json").then(@.setEmojiImagePath.bind(this))
|
||||||
|
|
||||||
|
getEmojiById: (id) ->
|
||||||
|
return _.find @.emojis, (it) -> it.id == id
|
||||||
|
|
||||||
|
getEmojiByName: (name) ->
|
||||||
|
return _.find @.emojis, (it) -> it.name == name
|
||||||
|
|
||||||
|
replaceImgsByEmojiName: (html) ->
|
||||||
|
emojiIds = taiga.getMatches(html, /emojis\/([^"]+).png"/gi)
|
||||||
|
|
||||||
|
for emojiId in emojiIds
|
||||||
|
regexImgs = new RegExp('<img(.*)' + emojiId + '[^>]+\>', 'g')
|
||||||
|
emoji = @.getEmojiById(emojiId)
|
||||||
|
html = html.replace(regexImgs, ':' + emoji.name + ':')
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
replaceEmojiNameByImgs: (text) ->
|
||||||
|
emojiIds = taiga.getMatches(text, /:([^: ]*):/g)
|
||||||
|
|
||||||
|
for emojiId in emojiIds
|
||||||
|
regexImgs = new RegExp(':' + emojiId + ':', 'g')
|
||||||
|
emoji = @.getEmojiByName(emojiId)
|
||||||
|
|
||||||
|
if emoji
|
||||||
|
text = text.replace(regexImgs, '')
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
removeTrailingListBr: (text) ->
|
||||||
|
regex = new RegExp(/<li>(.*?)<br><\/li>/, 'g')
|
||||||
|
return text.replace(regex, '<li>$1</li>')
|
||||||
|
|
||||||
|
getMarkdown: (html) ->
|
||||||
|
# https://github.com/yabwe/medium-editor/issues/543
|
||||||
|
cleanIssueConverter = {
|
||||||
|
filter: ['html', 'body', 'span', 'div'],
|
||||||
|
replacement: (innerHTML) ->
|
||||||
|
return innerHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
codeLanguageConverter = {
|
||||||
|
filter: (node) =>
|
||||||
|
return node.nodeName == 'PRE' &&
|
||||||
|
node.firstChild &&
|
||||||
|
node.firstChild.nodeName == 'CODE'
|
||||||
|
replacement: (content, node) =>
|
||||||
|
lan = @wysiwygCodeHightlighterService.getLanguageInClassList(node.firstChild.classList)
|
||||||
|
lan = '' if !lan
|
||||||
|
|
||||||
|
return '\n\n```' + lan + '\n' + _.trim(node.firstChild.textContent) + '\n```\n\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
html = html.replace(/ (<\/.*>)/g, "$1")
|
||||||
|
html = @.replaceImgsByEmojiName(html)
|
||||||
|
html = @.removeTrailingListBr(html)
|
||||||
|
|
||||||
|
markdown = toMarkdown(html, {
|
||||||
|
gfm: true,
|
||||||
|
converters: [cleanIssueConverter, codeLanguageConverter]
|
||||||
|
})
|
||||||
|
|
||||||
|
return markdown
|
||||||
|
|
||||||
|
getHTML: (text) ->
|
||||||
|
return "" if !text || !text.length
|
||||||
|
|
||||||
|
options = {
|
||||||
|
breaks: true
|
||||||
|
}
|
||||||
|
|
||||||
|
text = @.replaceEmojiNameByImgs(text)
|
||||||
|
|
||||||
|
md = window.markdownit({
|
||||||
|
breaks: true
|
||||||
|
})
|
||||||
|
|
||||||
|
result = md.render(text)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
angular.module("taigaComponents")
|
||||||
|
.service("tgWysiwygService", ["tgWysiwygCodeHightlighterService", WysiwygService])
|
|
@ -28,7 +28,6 @@ class CommentController
|
||||||
|
|
||||||
constructor: (@currentUserService, @permissionService, @lightboxFactory) ->
|
constructor: (@currentUserService, @permissionService, @lightboxFactory) ->
|
||||||
@.hiddenDeletedComment = true
|
@.hiddenDeletedComment = true
|
||||||
@.commentContent = angular.copy(@.comment)
|
|
||||||
|
|
||||||
showDeletedComment: () ->
|
showDeletedComment: () ->
|
||||||
@.hiddenDeletedComment = false
|
@.hiddenDeletedComment = false
|
||||||
|
@ -45,6 +44,9 @@ class CommentController
|
||||||
@.user = @currentUserService.getUser()
|
@.user = @currentUserService.getUser()
|
||||||
return @.user.get('id') == @.comment.user.pk || @permissionService.check('modify_project')
|
return @.user.get('id') == @.comment.user.pk || @permissionService.check('modify_project')
|
||||||
|
|
||||||
|
saveComment: (text, cb) ->
|
||||||
|
@.onEditComment({commentId: @.comment.id, commentData: text, callback: cb})
|
||||||
|
|
||||||
displayCommentHistory: () ->
|
displayCommentHistory: () ->
|
||||||
@lightboxFactory.create('tg-lb-display-historic', {
|
@lightboxFactory.create('tg-lb-display-historic', {
|
||||||
"class": "lightbox lightbox-display-historic"
|
"class": "lightbox lightbox-display-historic"
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
include ../../../partials/common/components/wysiwyg.jade
|
|
||||||
|
|
||||||
.comment-wrapper(ng-if="!vm.comment.delete_comment_date")
|
.comment-wrapper(ng-if="!vm.comment.delete_comment_date")
|
||||||
img.comment-avatar(
|
img.comment-avatar(
|
||||||
tg-avatar="vm.comment.user"
|
tg-avatar="vm.comment.user"
|
||||||
|
@ -23,38 +21,21 @@ include ../../../partials/common/components/wysiwyg.jade
|
||||||
.comment-container
|
.comment-container
|
||||||
.comment-text.wysiwyg(
|
.comment-text.wysiwyg(
|
||||||
ng-if="!vm.editMode"
|
ng-if="!vm.editMode"
|
||||||
ng-bind-html="vm.comment.comment_html"
|
tg-bind-code="vm.comment.comment"
|
||||||
)
|
)
|
||||||
.comment-editor(
|
.comment-editor(
|
||||||
ng-if="vm.editMode"
|
ng-if="vm.editMode"
|
||||||
ng-keyup="vm.checkCancelComment($event)"
|
ng-keyup="vm.checkCancelComment($event)"
|
||||||
)
|
)
|
||||||
.edit-comment(ng-model="vm.type")
|
tg-comment-edit-wysiwyg.edit-comment
|
||||||
textarea(
|
|
||||||
ng-model="vm.commentContent.comment"
|
.comment-options(ng-if="vm.canEditDeleteComment() && !vm.editMode")
|
||||||
)
|
|
||||||
.save-comment-wrapper
|
|
||||||
button.button-green.save-comment(
|
|
||||||
type="button"
|
|
||||||
title="{{'COMMENTS.EDIT_COMMENT' | translate}}"
|
|
||||||
translate="COMMENTS.EDIT_COMMENT"
|
|
||||||
ng-disabled="!vm.commentContent.comment.length || vm.editing == vm.comment.id"
|
|
||||||
ng-click="vm.onEditComment({commentId: vm.comment.id, commentData: vm.commentContent.comment})"
|
|
||||||
tg-loading="vm.editing == vm.comment.id"
|
|
||||||
)
|
|
||||||
.comment-options(ng-if="::vm.canEditDeleteComment()")
|
|
||||||
tg-svg.comment-option(
|
tg-svg.comment-option(
|
||||||
svg-icon="icon-edit"
|
svg-icon="icon-edit"
|
||||||
svg-title-translate="COMMON.EDIT"
|
svg-title-translate="COMMON.EDIT"
|
||||||
ng-click="vm.onEditMode({commentId: vm.comment.id})"
|
ng-click="vm.onEditMode({commentId: vm.comment.id})"
|
||||||
ng-if="!vm.editMode"
|
ng-if="!vm.editMode"
|
||||||
)
|
)
|
||||||
tg-svg.comment-option(
|
|
||||||
svg-icon="icon-close"
|
|
||||||
svg-title-translate="COMMON.CANCEL"
|
|
||||||
ng-click="vm.onEditMode({commentId: vm.comment.id})"
|
|
||||||
ng-if="vm.editMode"
|
|
||||||
)
|
|
||||||
tg-svg.comment-option(
|
tg-svg.comment-option(
|
||||||
svg-icon="icon-trash"
|
svg-icon="icon-trash"
|
||||||
svg-title-translate="COMMON.DELETE"
|
svg-title-translate="COMMON.DELETE"
|
||||||
|
@ -107,5 +88,5 @@ include ../../../partials/common/components/wysiwyg.jade
|
||||||
span(translate="COMMENTS.RESTORE")
|
span(translate="COMMENTS.RESTORE")
|
||||||
p.deleted-comment-comment(
|
p.deleted-comment-comment(
|
||||||
ng-if="!vm.hiddenDeletedComment"
|
ng-if="!vm.hiddenDeletedComment"
|
||||||
ng-bind-html="vm.comment.comment_html"
|
tg-bind-code="vm.comment.comment"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
.comments {
|
.comments {
|
||||||
clear: both;
|
clear: both;
|
||||||
|
tg-wysiwyg {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
.read-mode {
|
||||||
|
border: 1px solid $gray-light;
|
||||||
|
height: 55px;
|
||||||
|
.medium-editor-placeholder,
|
||||||
|
.markdown-editor-placeholder {
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
}
|
||||||
.add-comment {
|
.add-comment {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
textarea {
|
textarea {
|
||||||
|
@ -20,7 +31,6 @@
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
padding: .5rem 4rem;
|
padding: .5rem 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.comment {
|
.comment {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -143,12 +153,6 @@
|
||||||
.deleted-comment-comment {
|
.deleted-comment-comment {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
.comment-editor {
|
|
||||||
textarea {
|
|
||||||
height: 5rem;
|
|
||||||
min-height: 5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-text {
|
.comment-text {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
include ../../../partials/common/components/wysiwyg.jade
|
|
||||||
|
|
||||||
section.comments
|
section.comments
|
||||||
.comments-wrapper
|
.comments-wrapper
|
||||||
tg-comment.comment(
|
tg-comment.comment(
|
||||||
|
@ -15,25 +13,11 @@ section.comments
|
||||||
on-edit-mode="vm.onEditMode({commentId: commentId})"
|
on-edit-mode="vm.onEditMode({commentId: commentId})"
|
||||||
on-delete-comment="vm.onDeleteComment({commentId: commentId})"
|
on-delete-comment="vm.onDeleteComment({commentId: commentId})"
|
||||||
on-restore-deleted-comment="vm.onRestoreDeletedComment({commentId: commentId})"
|
on-restore-deleted-comment="vm.onRestoreDeletedComment({commentId: commentId})"
|
||||||
on-edit-comment="vm.onEditComment({commentId: commentId, commentData: commentData})"
|
on-edit-comment="vm.onEditComment({commentId: commentId, commentData: commentData, callback: callback})"
|
||||||
)
|
)
|
||||||
tg-editable-wysiwyg.add-comment(
|
|
||||||
ng-model="vm.type"
|
tg-comment-wysiwyg(
|
||||||
tg-check-permission="{{::vm.canAddCommentPermission}}"
|
tg-check-permission="{{::vm.canAddCommentPermission}}"
|
||||||
tg-toggle-comment
|
on-update="updateComment(text)"
|
||||||
|
type="vm.type"
|
||||||
)
|
)
|
||||||
textarea(
|
|
||||||
ng-attr-placeholder="{{'COMMENTS.TYPE_NEW_COMMENT' | translate}}"
|
|
||||||
tg-markitup="tg-markitup"
|
|
||||||
ng-model="vm.type.comment"
|
|
||||||
)
|
|
||||||
+wysihelp
|
|
||||||
.save-comment-wrapper
|
|
||||||
button.button-green.save-comment(
|
|
||||||
type="button"
|
|
||||||
title="{{'COMMENTS.COMMENT' | translate}}"
|
|
||||||
translate="COMMENTS.COMMENT"
|
|
||||||
ng-disabled="!vm.type.comment.length || vm.loading"
|
|
||||||
ng-click="vm.onAddComment()"
|
|
||||||
tg-loading="vm.loading"
|
|
||||||
)
|
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
)
|
)
|
||||||
.entry-text(
|
.entry-text(
|
||||||
ng-class="{'ellipsed': !displayFullEntry && entry.comment.length >= 75, 'blurry': entry.comment.length >= 75 && !displayFullEntry}"
|
ng-class="{'ellipsed': !displayFullEntry && entry.comment.length >= 75, 'blurry': entry.comment.length >= 75 && !displayFullEntry}"
|
||||||
ng-bind-html="entry.comment_html"
|
ng-bind-html="entry.comment | markdownToHTML"
|
||||||
)
|
)
|
||||||
|
|
|
@ -72,7 +72,7 @@ class HistorySectionController
|
||||||
@.deleting = commentId
|
@.deleting = commentId
|
||||||
return @rs.history.deleteComment(type, objectId, activityId).then =>
|
return @rs.history.deleteComment(type, objectId, activityId).then =>
|
||||||
@._loadHistory()
|
@._loadHistory()
|
||||||
@.deleting = commentId
|
@.deleting = null
|
||||||
|
|
||||||
editComment: (commentId, comment) ->
|
editComment: (commentId, comment) ->
|
||||||
type = @.name
|
type = @.name
|
||||||
|
@ -93,12 +93,10 @@ class HistorySectionController
|
||||||
@._loadHistory()
|
@._loadHistory()
|
||||||
@.editing = null
|
@.editing = null
|
||||||
|
|
||||||
addComment: () ->
|
addComment: (cb) ->
|
||||||
type = @.type
|
|
||||||
@.loading = true
|
|
||||||
@repo.save(@.type).then =>
|
@repo.save(@.type).then =>
|
||||||
@._loadHistory()
|
@._loadHistory()
|
||||||
@.loading = false
|
cb()
|
||||||
|
|
||||||
onOrderComments: () ->
|
onOrderComments: () ->
|
||||||
@.reverse = !@.reverse
|
@.reverse = !@.reverse
|
||||||
|
|
|
@ -149,12 +149,16 @@ describe "HistorySection", ->
|
||||||
objectId = historyCtrl.id
|
objectId = historyCtrl.id
|
||||||
commentId = 7
|
commentId = 7
|
||||||
|
|
||||||
promise = mocks.tgResources.history.deleteComment.withArgs(type, objectId, commentId).promise().resolve()
|
deleteCommentPromise = mocks.tgResources.history.deleteComment.withArgs(type, objectId, commentId).promise()
|
||||||
|
|
||||||
historyCtrl.deleting = true
|
ctrlPromise = historyCtrl.deleteComment(commentId)
|
||||||
historyCtrl.deleteComment(commentId).then () ->
|
expect(historyCtrl.deleting).to.be.equal(7)
|
||||||
|
|
||||||
|
deleteCommentPromise.resolve()
|
||||||
|
|
||||||
|
ctrlPromise.then () ->
|
||||||
expect(historyCtrl._loadHistory).have.been.called
|
expect(historyCtrl._loadHistory).have.been.called
|
||||||
expect(historyCtrl.deleting).to.be.equal(7)
|
expect(historyCtrl.deleting).to.be.null
|
||||||
|
|
||||||
it "edit comment", () ->
|
it "edit comment", () ->
|
||||||
historyCtrl = controller "HistorySection"
|
historyCtrl = controller "HistorySection"
|
||||||
|
@ -201,13 +205,15 @@ describe "HistorySection", ->
|
||||||
|
|
||||||
historyCtrl.type = "type"
|
historyCtrl.type = "type"
|
||||||
type = historyCtrl.type
|
type = historyCtrl.type
|
||||||
historyCtrl.loading = true
|
|
||||||
|
cb = sinon.spy()
|
||||||
|
|
||||||
promise = mocks.tgRepo.save.withArgs(type).promise().resolve()
|
promise = mocks.tgRepo.save.withArgs(type).promise().resolve()
|
||||||
|
|
||||||
historyCtrl.addComment().then () ->
|
historyCtrl.addComment(cb).then () ->
|
||||||
expect(historyCtrl._loadHistory).has.been.called
|
expect(historyCtrl._loadHistory).has.been.called
|
||||||
expect(historyCtrl.loading).to.be.false
|
expect(cb).to.have.been.called
|
||||||
|
|
||||||
|
|
||||||
it "order comments", () ->
|
it "order comments", () ->
|
||||||
historyCtrl = controller "HistorySection"
|
historyCtrl = controller "HistorySection"
|
||||||
|
|
|
@ -18,8 +18,8 @@ section.history(
|
||||||
on-delete-comment="vm.deleteComment(commentId)"
|
on-delete-comment="vm.deleteComment(commentId)"
|
||||||
on-restore-deleted-comment="vm.restoreDeletedComment(commentId)"
|
on-restore-deleted-comment="vm.restoreDeletedComment(commentId)"
|
||||||
on-edit-mode="vm.toggleEditMode(commentId)"
|
on-edit-mode="vm.toggleEditMode(commentId)"
|
||||||
on-add-comment="vm.addComment()"
|
on-add-comment="vm.addComment(callback)"
|
||||||
on-edit-comment="vm.editComment(commentId, commentData)"
|
on-edit-comment="vm.editComment(commentId, commentData, callback)"
|
||||||
edit-mode="vm.editMode"
|
edit-mode="vm.editMode"
|
||||||
|
|
||||||
object="{{vm.id}}"
|
object="{{vm.id}}"
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
include wysiwyg.jade
|
|
||||||
|
|
||||||
.view-description
|
|
||||||
section.us-content.wysiwyg(tg-bind-html="item.description_html || noDescriptionMsg")
|
|
||||||
tg-svg.edit(svg-icon="icon-edit")
|
|
||||||
|
|
||||||
.edit-description
|
|
||||||
textarea(ng-attr-placeholder="{{'COMMON.DESCRIPTION.EMPTY' | translate}}", ng-model="item.description", tg-markitup="tg-markitup")
|
|
||||||
+wysihelp
|
|
||||||
div.save-container
|
|
||||||
span.save
|
|
||||||
tg-svg(
|
|
||||||
svg-icon="icon-save",
|
|
||||||
svg-title-translate="COMMON.SAVE"
|
|
||||||
)
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
.editor(ng-class="{'edit-mode': editMode, 'read-mode': !editMode}")
|
||||||
|
div(ng-if="outdated")
|
||||||
|
p.outdated {{'COMMON.WYSIWYG.OUTDATED' | translate}}
|
||||||
|
|
||||||
|
.medium.wysiwyg(
|
||||||
|
type="text",
|
||||||
|
ng-show="mode == 'html'"
|
||||||
|
)
|
||||||
|
|
||||||
|
textarea.markdown.e2e-markdown-textarea(
|
||||||
|
placeholder="{{placeholder}}"
|
||||||
|
ng-change="changeMarkdown()"
|
||||||
|
ng-model="markdown"
|
||||||
|
ng-show="mode == 'markdown' && editMode"
|
||||||
|
)
|
||||||
|
|
||||||
|
.markdown(
|
||||||
|
ng-class="{empty: !markdown.length}"
|
||||||
|
ng-click="editMode = true"
|
||||||
|
ng-show="mode == 'markdown' && !editMode"
|
||||||
|
)
|
||||||
|
p(ng-if="markdown.length") {{markdown}}
|
||||||
|
p.markdown-editor-placeholder.wysiwyg(ng-if="!markdown.length") {{placeholder}}
|
||||||
|
|
||||||
|
.mode-editor(ng-if="editMode")
|
||||||
|
span.e2e-markdown-mode(
|
||||||
|
ng-if="mode=='html'"
|
||||||
|
ng-click="setMode('markdown')"
|
||||||
|
) Markdown Mode
|
||||||
|
|
||||||
|
span.e2e-html-mode(
|
||||||
|
ng-if="mode=='markdown'"
|
||||||
|
ng-click="setMode('html')"
|
||||||
|
) HTML Mode
|
||||||
|
|
||||||
|
a.help-markdown(
|
||||||
|
ng-if="mode=='markdown'"
|
||||||
|
href="https://tree.taiga.io/support/misc/taiga-markdown-syntax/"
|
||||||
|
target="_blank"
|
||||||
|
title="{{'COMMON.WYSIWYG.MARKDOWN_HELP' | translate}}"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-question")
|
||||||
|
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")
|
||||||
|
|
||||||
|
.tools(ng-if="editMode")
|
||||||
|
a.e2e-save-editor(
|
||||||
|
ng-class="{disabled: required && !markdown.length}"
|
||||||
|
tg-loading="saving"
|
||||||
|
href="#",
|
||||||
|
ng-click="save()"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-save")
|
||||||
|
a.e2e-cancel-editor(
|
||||||
|
href="#",
|
||||||
|
ng-click="cancel()"
|
||||||
|
title="{{ 'COMMON.CANCEL' | translate }}"
|
||||||
|
)
|
||||||
|
tg-svg(svg-icon="icon-close")
|
|
@ -1,11 +0,0 @@
|
||||||
mixin wysihelp
|
|
||||||
.wysiwyg-help
|
|
||||||
span.drag-drop-help(ng-if="wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP")
|
|
||||||
span.drag-drop-help(ng-if="!wiki.id", translate="COMMON.WYSIWYG.ATTACH_FILE_HELP_SAVE_FIRST")
|
|
||||||
a.help-markdown(
|
|
||||||
href="https://tree.taiga.io/support/misc/taiga-markdown-syntax/"
|
|
||||||
target="_blank"
|
|
||||||
title="{{'COMMON.WYSIWYG.MARKDOWN_HELP' | translate}}"
|
|
||||||
)
|
|
||||||
tg-svg(svg-icon="icon-question")
|
|
||||||
span(translate="COMMON.WYSIWYG.MARKDOWN_HELP")
|
|
|
@ -41,12 +41,12 @@ div.wrapper(
|
||||||
)
|
)
|
||||||
tg-created-by-display.ticket-created-by(ng-model="epic")
|
tg-created-by-display.ticket-created-by(ng-model="epic")
|
||||||
|
|
||||||
section.duty-content(
|
section.duty-content
|
||||||
tg-editable-description
|
tg-item-wysiwyg(
|
||||||
tg-editable-wysiwyg
|
type="epic",
|
||||||
ng-model="epic"
|
model="epic",
|
||||||
required-perm="modify_epic"
|
required-perm="modify_epic"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Custom Fields
|
// Custom Fields
|
||||||
tg-custom-attributes-values(
|
tg-custom-attributes-values(
|
||||||
|
|
|
@ -33,13 +33,13 @@ div.wrapper(
|
||||||
permissions="modify_issue"
|
permissions="modify_issue"
|
||||||
)
|
)
|
||||||
tg-created-by-display.ticket-created-by(ng-model="issue")
|
tg-created-by-display.ticket-created-by(ng-model="issue")
|
||||||
|
|
||||||
section.duty-content(
|
section.duty-content
|
||||||
tg-editable-description
|
tg-item-wysiwyg(
|
||||||
tg-editable-wysiwyg
|
type="issue",
|
||||||
ng-model="issue"
|
model="issue",
|
||||||
required-perm="modify_issue"
|
required-perm="modify_issue"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Custom Fields
|
// Custom Fields
|
||||||
tg-custom-attributes-values(
|
tg-custom-attributes-values(
|
||||||
|
|
|
@ -43,7 +43,12 @@ div.wrapper(
|
||||||
)
|
)
|
||||||
tg-created-by-display.ticket-created-by(ng-model="task")
|
tg-created-by-display.ticket-created-by(ng-model="task")
|
||||||
|
|
||||||
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="task", required-perm="modify_task")
|
section.duty-content
|
||||||
|
tg-item-wysiwyg(
|
||||||
|
type="task",
|
||||||
|
model="task",
|
||||||
|
required-perm="modify_task"
|
||||||
|
)
|
||||||
|
|
||||||
// Custom Fields
|
// Custom Fields
|
||||||
tg-custom-attributes-values(
|
tg-custom-attributes-values(
|
||||||
|
|
|
@ -43,7 +43,12 @@ div.wrapper(
|
||||||
)
|
)
|
||||||
tg-created-by-display.ticket-created-by(ng-model="us")
|
tg-created-by-display.ticket-created-by(ng-model="us")
|
||||||
|
|
||||||
section.duty-content(tg-editable-description, tg-editable-wysiwyg, ng-model="us", required-perm="modify_us")
|
section.duty-content
|
||||||
|
tg-item-wysiwyg(
|
||||||
|
type="us",
|
||||||
|
model="us",
|
||||||
|
required-perm="modify_us"
|
||||||
|
)
|
||||||
|
|
||||||
// Custom Fields
|
// Custom Fields
|
||||||
tg-custom-attributes-values(
|
tg-custom-attributes-values(
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
include ../common/components/wysiwyg.jade
|
|
||||||
|
|
||||||
.view-wiki-content
|
|
||||||
section.wysiwyg(tg-bind-html='wiki.html')
|
|
||||||
a.edit(
|
|
||||||
href=""
|
|
||||||
title="{{'COMMON.EDIT' | translate}}"
|
|
||||||
)
|
|
||||||
tg-svg(svg-icon="icon-edit")
|
|
||||||
|
|
||||||
.edit-wiki-content(style='display: none;')
|
|
||||||
textarea(
|
|
||||||
ng-attr-placeholder="{{'WIKI.PLACEHOLDER_PAGE' | translate}}",
|
|
||||||
ng-model='wiki.content'
|
|
||||||
tg-markitup='tg-markitup'
|
|
||||||
)
|
|
||||||
+wysihelp
|
|
||||||
|
|
||||||
span.action-container
|
|
||||||
a.save(
|
|
||||||
title="{{'COMMON.SAVE' | translate}}"
|
|
||||||
href=""
|
|
||||||
)
|
|
||||||
tg-svg(svg-icon="icon-save")
|
|
||||||
a.cancel(
|
|
||||||
title="{{'COMMON.CANCEL' | translate}}"
|
|
||||||
href=""
|
|
||||||
)
|
|
||||||
tg-svg(svg-icon="icon-close")
|
|
|
@ -17,11 +17,8 @@ div.wrapper(
|
||||||
span.green(translate="PROJECT.SECTION.WIKI")
|
span.green(translate="PROJECT.SECTION.WIKI")
|
||||||
|
|
||||||
h2.wiki-title(ng-bind='wikiTitle')
|
h2.wiki-title(ng-bind='wikiTitle')
|
||||||
section.wiki-content(
|
|
||||||
tg-editable-wysiwyg,
|
tg-wiki-wysiwyg(model="wiki")
|
||||||
tg-editable-wiki-content,
|
|
||||||
ng-model="wiki"
|
|
||||||
)
|
|
||||||
|
|
||||||
.summary.wiki-summary(
|
.summary.wiki-summary(
|
||||||
tg-wiki-summary
|
tg-wiki-summary
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
.markItUpHeader {
|
|
||||||
ul {
|
|
||||||
background: $mass-white;
|
|
||||||
padding: .3rem;
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
float: none;
|
|
||||||
a {
|
|
||||||
opacity: .8;
|
|
||||||
&:hover {
|
|
||||||
opacity: .3;
|
|
||||||
transition: opacity .2s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preview-icon {
|
|
||||||
position: absolute;
|
|
||||||
right: 4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.markItUpContainer {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview {
|
|
||||||
.actions {
|
|
||||||
background: $mass-white;
|
|
||||||
margin-top: .5rem;
|
|
||||||
min-height: 2rem;
|
|
||||||
padding: .3rem;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
background: $white;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
// Bourbon
|
// Bourbon
|
||||||
@import '../../../vendor/bourbon/app/assets/stylesheets/bourbon';
|
@import '../../../node_modules/bourbon/core/bourbon';
|
||||||
|
|
||||||
//#################################################
|
//#################################################
|
||||||
// dependencies
|
// dependencies
|
||||||
|
@ -22,3 +22,13 @@
|
||||||
@import '../dependencies/mixins/svg';
|
@import '../dependencies/mixins/svg';
|
||||||
@import '../dependencies/mixins/track-buttons';
|
@import '../dependencies/mixins/track-buttons';
|
||||||
@import '../dependencies/mixins/empty-color';
|
@import '../dependencies/mixins/empty-color';
|
||||||
|
|
||||||
|
//deprecated
|
||||||
|
@mixin placeholder {
|
||||||
|
$placeholders: ":-webkit-input" ":-moz" "-moz" "-ms-input";
|
||||||
|
@each $placeholder in $placeholders {
|
||||||
|
&:#{$placeholder}-placeholder {
|
||||||
|
@content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,73 +10,3 @@
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wiki-content {
|
|
||||||
@include font-size(large);
|
|
||||||
position: relative;
|
|
||||||
&.editable {
|
|
||||||
&:hover {
|
|
||||||
.wysiwyg {
|
|
||||||
background: $mass-white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.view-wiki-content {
|
|
||||||
&:hover {
|
|
||||||
.edit {
|
|
||||||
opacity: 1;
|
|
||||||
top: -1.5rem;
|
|
||||||
transition: all .2s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.edit {
|
|
||||||
@include svg-size(2rem);
|
|
||||||
background: $mass-white;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0;
|
|
||||||
padding: .2rem .5rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
transition: all .2s linear;
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preview {
|
|
||||||
padding-top: 1.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-wiki-content {
|
|
||||||
a {
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: .5rem;
|
|
||||||
&:last-child {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
.icon {
|
|
||||||
fill: $primary-dark;
|
|
||||||
opacity: .3;
|
|
||||||
transition: all .2s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.preview-icon {
|
|
||||||
position: absolute;
|
|
||||||
right: 3.5rem;
|
|
||||||
}
|
|
||||||
.action-container {
|
|
||||||
position: absolute;
|
|
||||||
right: 1rem;
|
|
||||||
top: .3rem;
|
|
||||||
}
|
|
||||||
.edit {
|
|
||||||
position: absolute;
|
|
||||||
right: 3.5rem;
|
|
||||||
top: .4rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,175 +0,0 @@
|
||||||
/* -------------------------------------------------------------------
|
|
||||||
// markItUp!
|
|
||||||
// By Jay Salvat - http://markitup.jaysalvat.com/
|
|
||||||
// ------------------------------------------------------------------*/
|
|
||||||
.markItUp .markItUpButton1 a {
|
|
||||||
background-image:url("../images/markitup/h1.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markItUp .markItUpButton2 a {
|
|
||||||
background-image:url("../images/markitup/h2.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markItUp .markItUpButton3 a {
|
|
||||||
background-image:url("../images/markitup/h3.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markItUp .markItUpButton4 a {
|
|
||||||
background-image:url("../images/markitup/bold.png");
|
|
||||||
}
|
|
||||||
.markItUp .markItUpButton5 a {
|
|
||||||
background-image:url("../images/markitup/italic.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markItUp .markItUpButton6 a {
|
|
||||||
background-image:url("../images/markitup/stroke.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .markItUpButton7 a {
|
|
||||||
background-image:url("../images/markitup/list-bullet.png");
|
|
||||||
}
|
|
||||||
.markdown .markItUpButton8 a {
|
|
||||||
background-image:url("../images/markitup/list-numeric.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .markItUpButton9 a {
|
|
||||||
background-image:url("../images/markitup/picture.png");
|
|
||||||
}
|
|
||||||
.markdown .markItUpButton10 a {
|
|
||||||
background-image:url("../images/markitup/link.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .markItUpButton11 a {
|
|
||||||
background-image:url("../images/markitup/quotes.png");
|
|
||||||
}
|
|
||||||
.markdown .markItUpButton12 a {
|
|
||||||
background-image:url("../images/markitup/code.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .preview-icon a {
|
|
||||||
background-image:url("../images/markitup/preview.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown .help a {
|
|
||||||
background-image:url("../images/markitup/help.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------------
|
|
||||||
// markItUp! Universal MarkUp Engine, JQuery plugin
|
|
||||||
// By Jay Salvat - http://markitup.jaysalvat.com/
|
|
||||||
// ------------------------------------------------------------------*/
|
|
||||||
.markItUp * {
|
|
||||||
margin:0px; padding:0px;
|
|
||||||
outline:none;
|
|
||||||
}
|
|
||||||
.markItUp a:link,
|
|
||||||
.markItUp a:visited {
|
|
||||||
color:#000;
|
|
||||||
text-decoration:none;
|
|
||||||
}
|
|
||||||
.markItUpContainer {
|
|
||||||
padding:5px 5px 2px 5px;
|
|
||||||
font:11px Verdana, Arial, Helvetica, sans-serif;
|
|
||||||
}
|
|
||||||
.markItUpEditor {
|
|
||||||
font:12px 'Courier New', Courier, monospace;
|
|
||||||
padding:5px;
|
|
||||||
height:320px;
|
|
||||||
clear:both;
|
|
||||||
line-height:18px;
|
|
||||||
overflow:auto;
|
|
||||||
}
|
|
||||||
.markItUpPreviewFrame {
|
|
||||||
overflow:auto;
|
|
||||||
background-color:#FFF;
|
|
||||||
width:99.9%;
|
|
||||||
height:300px;
|
|
||||||
margin:5px 0;
|
|
||||||
}
|
|
||||||
.markItUpFooter {
|
|
||||||
width:100%;
|
|
||||||
}
|
|
||||||
.markItUpResizeHandle {
|
|
||||||
overflow:hidden;
|
|
||||||
width:22px; height:5px;
|
|
||||||
margin-left:auto;
|
|
||||||
margin-right:auto;
|
|
||||||
background-image:url(../images/markitup/handle.png);
|
|
||||||
cursor:n-resize;
|
|
||||||
}
|
|
||||||
/***************************************************************************************/
|
|
||||||
/* first row of buttons */
|
|
||||||
.markItUp .markItUpHeader ul {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul li {
|
|
||||||
list-style:none;
|
|
||||||
float:left;
|
|
||||||
position:relative;
|
|
||||||
margin: 3px;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul li:hover > ul{
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul .markItUpDropMenu {
|
|
||||||
background:transparent url(../images/markitup/menu.png) no-repeat 115% 50%;
|
|
||||||
margin-right:5px;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul .markItUpDropMenu li {
|
|
||||||
margin-right:0px;
|
|
||||||
}
|
|
||||||
/* next rows of buttons */
|
|
||||||
.markItUpHeader ul ul {
|
|
||||||
display:none;
|
|
||||||
position:absolute;
|
|
||||||
top:18px; left:0px;
|
|
||||||
background:#FFF;
|
|
||||||
border:1px solid #000;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul ul li {
|
|
||||||
float:none;
|
|
||||||
border-bottom:1px solid #000;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul ul .markItUpDropMenu {
|
|
||||||
background:#FFF url(../images/markitup/submenu.png) no-repeat 100% 50%;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul .markItUpSeparator {
|
|
||||||
margin:2px 10px 0 10px;
|
|
||||||
width:1px;
|
|
||||||
height:16px;
|
|
||||||
overflow:hidden;
|
|
||||||
background-color:#CCC;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul ul .markItUpSeparator {
|
|
||||||
width:auto; height:1px;
|
|
||||||
margin:0px;
|
|
||||||
}
|
|
||||||
/* next rows of buttons */
|
|
||||||
.markItUpHeader ul ul ul {
|
|
||||||
position:absolute;
|
|
||||||
top:-1px; left:150px;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul ul ul li {
|
|
||||||
float:none;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul a {
|
|
||||||
display:block;
|
|
||||||
width:16px; height:16px;
|
|
||||||
text-indent:-10000px;
|
|
||||||
background-repeat:no-repeat;
|
|
||||||
padding:3px;
|
|
||||||
margin:0px;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul ul a {
|
|
||||||
display:block;
|
|
||||||
padding-left:0px;
|
|
||||||
text-indent:0;
|
|
||||||
width:120px;
|
|
||||||
padding:5px 5px 5px 25px;
|
|
||||||
background-position:2px 50%;
|
|
||||||
}
|
|
||||||
.markItUpHeader ul ul a:hover {
|
|
||||||
color:#FFF;
|
|
||||||
background-color:#000;
|
|
||||||
}
|
|
90
bower.json
|
@ -1,90 +0,0 @@
|
||||||
{
|
|
||||||
"name": "taiga-layout",
|
|
||||||
"version": "2.1.0",
|
|
||||||
"homepage": "https://github.com/taiga.io/taiga-layout",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Andrey Antukh",
|
|
||||||
"email": "niwi@niwi.nz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Jesus Espino Garcia",
|
|
||||||
"email": "jespinog@gmail.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "David Barragán Merino",
|
|
||||||
"email": "dbarragan@dbarragan.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Xavi Julian",
|
|
||||||
"email": "xavier.julian@kaleidos.net"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Alejandro Alonso",
|
|
||||||
"email": "alejandro.alonso@kaleidos.net"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Anler Hernández",
|
|
||||||
"email": "hello@anler.me"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Juan Francisco Alcántara",
|
|
||||||
"email": "juanfran.alcantara@kaleidos.net"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Taiga project management system (frontend)",
|
|
||||||
"license": "AGPL-3.0",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git@github.com:taigaio/taiga-front.git"
|
|
||||||
},
|
|
||||||
"ignore": [
|
|
||||||
"**/.*",
|
|
||||||
"node_modules",
|
|
||||||
"bower_components",
|
|
||||||
"app/vendor",
|
|
||||||
"test",
|
|
||||||
"tests"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"emoticons": "~0.1.7",
|
|
||||||
"jquery-flot": "~0.8.2",
|
|
||||||
"angular": "1.5.5",
|
|
||||||
"angular-route": "1.5.5",
|
|
||||||
"angular-animate": "1.5.5",
|
|
||||||
"angular-aria": "1.5.5",
|
|
||||||
"angular-sanitize": "1.5.5",
|
|
||||||
"checksley": "~0.6.0",
|
|
||||||
"jquery": "~2.2.3",
|
|
||||||
"markitup-1x": "~1.1.14",
|
|
||||||
"jquery-textcomplete": "yuku-t/jquery-textcomplete#~0.7",
|
|
||||||
"flot-axislabels": "markrcote/flot-axislabels",
|
|
||||||
"flot-orderBars": "emmerich/flot-orderBars",
|
|
||||||
"flot.tooltip": "~0.8.4",
|
|
||||||
"moment": "~2.13.0",
|
|
||||||
"pikaday": "~1.4.0",
|
|
||||||
"raven-js": "~3.0.0",
|
|
||||||
"l.js": "~0.1.0",
|
|
||||||
"angular-translate": "~2.10.0",
|
|
||||||
"angular-translate-loader-partial": "~2.10.0",
|
|
||||||
"angular-translate-loader-static-files": "~2.10.0",
|
|
||||||
"angular-translate-interpolation-messageformat": "~2.10.0",
|
|
||||||
"ng-infinite-scroll-npm-is-better-than-bower": "^1.3.0",
|
|
||||||
"immutable": "~3.8.1",
|
|
||||||
"bluebird": "~3.3.5",
|
|
||||||
"intro.js": "~2.1.0",
|
|
||||||
"lodash": "~4.11.2",
|
|
||||||
"messageformat": "^0.3.1",
|
|
||||||
"dragula.js": "dragula#^3.6.6",
|
|
||||||
"bourbon": "^4.2.7"
|
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"lodash": "~4.11.2",
|
|
||||||
"moment": "~2.10.6",
|
|
||||||
"jquery": "~2.2.3",
|
|
||||||
"angular": "1.5.5",
|
|
||||||
"messageformat": "0.3.1",
|
|
||||||
"angular-translate": "2.10.0"
|
|
||||||
},
|
|
||||||
"private": true
|
|
||||||
}
|
|
|
@ -146,24 +146,6 @@ helper.assignedTo = function() {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
helper.editComment = function() {
|
|
||||||
let el = $('.comment-editor');
|
|
||||||
let obj = {
|
|
||||||
el:el,
|
|
||||||
|
|
||||||
updateText: function (text) {
|
|
||||||
el.$('textarea').sendKeys(text);
|
|
||||||
},
|
|
||||||
|
|
||||||
saveComment: async function () {
|
|
||||||
el.$('.save-comment').click();
|
|
||||||
await browser.waitForAngular();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return obj;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
helper.history = function() {
|
helper.history = function() {
|
||||||
let el = $('section.history');
|
let el = $('section.history');
|
||||||
let obj = {
|
let obj = {
|
||||||
|
@ -179,16 +161,6 @@ helper.history = function() {
|
||||||
await browser.waitForAngular();
|
await browser.waitForAngular();
|
||||||
},
|
},
|
||||||
|
|
||||||
addComment: async function(comment) {
|
|
||||||
obj.writeComment(comment);
|
|
||||||
el.$('.save-comment').click();
|
|
||||||
await browser.waitForAngular();
|
|
||||||
},
|
|
||||||
|
|
||||||
writeComment: function(comment) {
|
|
||||||
el.$('textarea[tg-markitup]').sendKeys(comment);
|
|
||||||
},
|
|
||||||
|
|
||||||
countComments: async function() {
|
countComments: async function() {
|
||||||
let comments = await el.$$(".comment-wrapper");
|
let comments = await el.$$(".comment-wrapper");
|
||||||
return comments.length;
|
return comments.length;
|
||||||
|
@ -227,6 +199,10 @@ helper.history = function() {
|
||||||
await browser.waitForAngular();
|
await browser.waitForAngular();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getComments: function() {
|
||||||
|
return $$('tg-comment');
|
||||||
|
},
|
||||||
|
|
||||||
showVersionsLastComment: async function() {
|
showVersionsLastComment: async function() {
|
||||||
el.$$(".comment-edited a").last().click();
|
el.$$(".comment-edited a").last().click();
|
||||||
await browser.waitForAngular();
|
await browser.waitForAngular();
|
||||||
|
@ -252,11 +228,11 @@ helper.history = function() {
|
||||||
el.$$(".deleted-comment-wrapper .restore-comment").last().click();
|
el.$$(".deleted-comment-wrapper .restore-comment").last().click();
|
||||||
await browser.waitForAngular();
|
await browser.waitForAngular();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
|
|
||||||
}
|
};
|
||||||
|
|
||||||
helper.block = function() {
|
helper.block = function() {
|
||||||
let el = $('tg-block-button');
|
let el = $('tg-block-button');
|
||||||
|
|
|
@ -6,6 +6,7 @@ var customFieldsHelper = require('../helpers/custom-fields-helper');
|
||||||
var commonUtil = require('../utils/common');
|
var commonUtil = require('../utils/common');
|
||||||
var lightbox = require('../utils/lightbox');
|
var lightbox = require('../utils/lightbox');
|
||||||
var notifications = require('../utils/notifications');
|
var notifications = require('../utils/notifications');
|
||||||
|
var sharedWysiwyg = require('./wysiwyg').wysiwygTesting;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -48,49 +49,6 @@ shared.tagsTesting = async function() {
|
||||||
expect(newtagsText).to.be.not.eql(tagsText);
|
expect(newtagsText).to.be.not.eql(tagsText);
|
||||||
}
|
}
|
||||||
|
|
||||||
shared.descriptionTesting = function() {
|
|
||||||
it('confirm close with ESC', async function() {
|
|
||||||
let descriptionHelper = detailHelper.description();
|
|
||||||
|
|
||||||
descriptionHelper.enabledEditionMode();
|
|
||||||
|
|
||||||
browser.actions().sendKeys(protractor.Key.ESCAPE).perform();
|
|
||||||
|
|
||||||
await lightbox.confirm.cancel();
|
|
||||||
|
|
||||||
let descriptionVisibility = await $('.edit-description').isDisplayed();
|
|
||||||
|
|
||||||
expect(descriptionVisibility).to.be.true;
|
|
||||||
|
|
||||||
descriptionHelper.focus();
|
|
||||||
|
|
||||||
browser.actions().sendKeys(protractor.Key.ESCAPE).perform();
|
|
||||||
|
|
||||||
await lightbox.confirm.ok();
|
|
||||||
|
|
||||||
descriptionVisibility = await $('.edit-description').isDisplayed();
|
|
||||||
|
|
||||||
expect(descriptionVisibility).to.be.false;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('edit', async function() {
|
|
||||||
let descriptionHelper = detailHelper.description();
|
|
||||||
let description = await descriptionHelper.getInnerHtml();
|
|
||||||
let date = Date.now();
|
|
||||||
descriptionHelper.enabledEditionMode();
|
|
||||||
descriptionHelper.setText("New description " + date);
|
|
||||||
descriptionHelper.save();
|
|
||||||
|
|
||||||
let newDescription = await descriptionHelper.getInnerHtml();
|
|
||||||
let notificationOpen = await notifications.success.open();
|
|
||||||
|
|
||||||
expect(notificationOpen).to.be.equal.true;
|
|
||||||
expect(newDescription).to.be.not.equal(description);
|
|
||||||
|
|
||||||
await notifications.success.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
shared.statusTesting = async function(status1 , status2) {
|
shared.statusTesting = async function(status1 , status2) {
|
||||||
let statusHelper = detailHelper.statusSelector();
|
let statusHelper = detailHelper.statusSelector();
|
||||||
|
|
||||||
|
@ -195,68 +153,9 @@ shared.assignedToTesting = function() {
|
||||||
shared.historyTesting = async function(screenshotsFolder) {
|
shared.historyTesting = async function(screenshotsFolder) {
|
||||||
let historyHelper = detailHelper.history();
|
let historyHelper = detailHelper.history();
|
||||||
|
|
||||||
|
|
||||||
//Adding a comment
|
|
||||||
historyHelper.selectCommentsTab();
|
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "show comments tab");
|
|
||||||
|
|
||||||
let commentsCounter = await historyHelper.countComments();
|
|
||||||
let date = Date.now();
|
|
||||||
|
|
||||||
await historyHelper.addComment("New comment " + date);
|
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "new coment");
|
|
||||||
|
|
||||||
let newCommentsCounter = await historyHelper.countComments();
|
|
||||||
expect(newCommentsCounter).to.be.equal(commentsCounter+1);
|
|
||||||
|
|
||||||
//Edit last comment
|
|
||||||
historyHelper.editLastComment();
|
|
||||||
let editComment = detailHelper.editComment();
|
|
||||||
editComment.updateText("This is the new and updated text");
|
|
||||||
editComment.saveComment();
|
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "edit comment");
|
|
||||||
|
|
||||||
//Show versions from last comment edited
|
|
||||||
historyHelper.showVersionsLastComment();
|
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "show comment versions");
|
|
||||||
|
|
||||||
historyHelper.closeVersionsLastComment();
|
|
||||||
|
|
||||||
//Deleting last comment
|
|
||||||
let deletedCommentsCounter = await historyHelper.countDeletedComments();
|
|
||||||
await historyHelper.deleteLastComment();
|
|
||||||
|
|
||||||
let newDeletedCommentsCounter = await historyHelper.countDeletedComments();
|
|
||||||
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1);
|
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "deleted comment");
|
|
||||||
|
|
||||||
//Restore last comment
|
|
||||||
deletedCommentsCounter = await historyHelper.countDeletedComments();
|
|
||||||
await historyHelper.restoreLastComment();
|
|
||||||
newDeletedCommentsCounter = await historyHelper.countDeletedComments();
|
|
||||||
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1);
|
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "restored comment");
|
|
||||||
|
|
||||||
//Store comment with a modification
|
|
||||||
commentsCounter = await historyHelper.countComments();
|
|
||||||
|
|
||||||
historyHelper.writeComment("New comment " + date);
|
|
||||||
let title = detailHelper.title();
|
|
||||||
title.setTitle('changed');
|
|
||||||
await title.save();
|
|
||||||
await utils.notifications.success.close();
|
|
||||||
|
|
||||||
newCommentsCounter = await historyHelper.countComments();
|
|
||||||
|
|
||||||
expect(newCommentsCounter).to.be.equal(commentsCounter+1);
|
|
||||||
|
|
||||||
//Check activity
|
//Check activity
|
||||||
await historyHelper.selectActivityTab();
|
await historyHelper.selectActivityTab();
|
||||||
await utils.common.takeScreenshot(screenshotsFolder, "show activity tab");
|
await utils.common.takeScreenshot(screenshotsFolder, "show activity tab");
|
||||||
|
|
||||||
let activitiesCounter = await historyHelper.countActivities();
|
|
||||||
|
|
||||||
expect(newCommentsCounter).to.be.least(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shared.blockTesting = async function() {
|
shared.blockTesting = async function() {
|
||||||
|
|
|
@ -0,0 +1,500 @@
|
||||||
|
var chai = require('chai');
|
||||||
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
|
||||||
|
var detailHelper = require('../helpers').detail;
|
||||||
|
var historyHelper = detailHelper.history();
|
||||||
|
|
||||||
|
var utils = require('../utils');
|
||||||
|
var EC = protractor.ExpectedConditions;
|
||||||
|
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
var expect = chai.expect;
|
||||||
|
|
||||||
|
var shared = module.exports;
|
||||||
|
|
||||||
|
function selectEditorFirstChild(elm) {
|
||||||
|
browser.executeScript(function () {
|
||||||
|
// select the first paragraph
|
||||||
|
var range = document.createRange();
|
||||||
|
range.selectNode(arguments[0].firstChild);
|
||||||
|
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}, elm.getWebElement());
|
||||||
|
|
||||||
|
browser.actions().mouseUp().perform(); // trigger medium events
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSelection() {
|
||||||
|
browser.executeScript(function () {
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
browser.actions().mouseUp().perform(); // trigger medium events
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownText(elm) {
|
||||||
|
var markdownTextarea = getMarkdownTextarea(elm);
|
||||||
|
|
||||||
|
return markdownTextarea.getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMarkdownTextarea(elm) {
|
||||||
|
return elm.$('.e2e-markdown-textarea');}
|
||||||
|
|
||||||
|
|
||||||
|
function htmlMode() {
|
||||||
|
$('.e2e-html-mode').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownMode() {
|
||||||
|
$('.e2e-markdown-mode').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEdition() {
|
||||||
|
$('.e2e-save-editor').click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdition(elm) {
|
||||||
|
$('.e2e-cancel-editor').click();
|
||||||
|
|
||||||
|
return browser.wait(async () => {
|
||||||
|
return !!await elm.$$('.read-mode').count();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function edit(elm, elmWrapper, text = null) {
|
||||||
|
await browser.wait(EC.elementToBeClickable(elm), 10000);
|
||||||
|
|
||||||
|
elm.click();
|
||||||
|
|
||||||
|
browser.sleep(200);
|
||||||
|
|
||||||
|
browser.executeScript(function () {
|
||||||
|
if(arguments[0].firstChild) {
|
||||||
|
var range = document.createRange();
|
||||||
|
range.setStart(arguments[0].firstChild, 0);
|
||||||
|
range.setEnd(arguments[0].lastChild, 0);
|
||||||
|
|
||||||
|
var sel = window.getSelection();
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
}, elm.getWebElement());
|
||||||
|
|
||||||
|
if (text !== null) {
|
||||||
|
await cleanWysiwyg(elm, elmWrapper);
|
||||||
|
|
||||||
|
return elm.sendKeys(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cleanWysiwyg(elm, elmWrapper) {
|
||||||
|
let isHtmlMode = await elm.isDisplayed();
|
||||||
|
|
||||||
|
if (isHtmlMode) {
|
||||||
|
let isPresent = await $('.e2e-markdown-mode').isPresent();
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
}
|
||||||
|
var markdownTextarea = getMarkdownTextarea(elmWrapper);
|
||||||
|
|
||||||
|
await utils.common.clear(markdownTextarea);
|
||||||
|
|
||||||
|
return htmlMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.wysiwygTestingComments = function(parentSelector, section) {
|
||||||
|
var editor;
|
||||||
|
var editorWrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
let parent = $(parentSelector);
|
||||||
|
editor = parent.$('.medium');
|
||||||
|
editorWrapper = parent.$('tg-wysiwyg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bold, test normal behavior and check markdown', async () => {
|
||||||
|
let commentsCounter = await historyHelper.countComments();
|
||||||
|
|
||||||
|
await edit(editor, editorWrapper, "test");
|
||||||
|
selectEditorFirstChild(editor);
|
||||||
|
|
||||||
|
$('.medium-editor-toolbar-active .medium-editor-action-bold').click();
|
||||||
|
|
||||||
|
resetSelection();
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdown = await getMarkdownText(editorWrapper);
|
||||||
|
|
||||||
|
expect(markdown).to.be.equal('**test**');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
let newCommentsCounter = await historyHelper.countComments();
|
||||||
|
expect(newCommentsCounter).to.be.equal(commentsCounter+1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert to html', async () => {
|
||||||
|
let commentsCounter = await historyHelper.countComments();
|
||||||
|
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdownTextarea = getMarkdownTextarea(editorWrapper);
|
||||||
|
|
||||||
|
await markdownTextarea.sendKeys('_test2_');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('<p><em>test2</em></p>\n');
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
let newCommentsCounter = await historyHelper.countComments();
|
||||||
|
expect(newCommentsCounter).to.be.equal(commentsCounter+1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('code block', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys("var test = 2;");
|
||||||
|
|
||||||
|
selectEditorFirstChild(editor);
|
||||||
|
|
||||||
|
$('.medium-editor-toolbar-active .medium-editor-button-last').click();
|
||||||
|
|
||||||
|
$('.code-language-selector').click();
|
||||||
|
$('.code-language-search input').sendKeys('javascript');
|
||||||
|
$('.code-language-search li').click();
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
let lastComment = historyHelper.getComments().last();
|
||||||
|
|
||||||
|
let hasHightlighter = !!await lastComment.$$('.token').count();
|
||||||
|
|
||||||
|
expect(hasHightlighter).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('confirm exit when there is changes', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys('text text text');
|
||||||
|
editor.sendKeys(protractor.Key.ESCAPE);
|
||||||
|
|
||||||
|
await utils.lightbox.confirm.ok();
|
||||||
|
|
||||||
|
let isReadMode = !!await editorWrapper.$$('.read-mode').count();
|
||||||
|
|
||||||
|
expect(isReadMode).to.be.true;
|
||||||
|
|
||||||
|
let html = await editor.getText();
|
||||||
|
|
||||||
|
expect(html).not.to.be.eql('text text text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keep changes on reload', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys('text text text');
|
||||||
|
editor.sendKeys(protractor.Key.ESCAPE);
|
||||||
|
|
||||||
|
browser.sleep(400);
|
||||||
|
browser.refresh();
|
||||||
|
|
||||||
|
let isReadMode = !!await editorWrapper.$$('.read-mode').count();
|
||||||
|
|
||||||
|
expect(isReadMode).to.be.false;
|
||||||
|
|
||||||
|
let html = await editor.getText();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('text text text');
|
||||||
|
|
||||||
|
await cancelEdition(editorWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mention user', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys('@use');
|
||||||
|
|
||||||
|
$$('.medium-mention li').get(2).click();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('<p><a href="/profile/user8">@user8</a> </p>');
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdown = await getMarkdownText(editorWrapper);
|
||||||
|
|
||||||
|
expect(markdown).to.be.equal('[@user8](/profile/user8)');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
|
||||||
|
await cancelEdition(editorWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emojis', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys(':smil');
|
||||||
|
|
||||||
|
$$('.medium-mention li').get(2).click();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.include('1f604.png');
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdown = await getMarkdownText(editorWrapper);
|
||||||
|
|
||||||
|
expect(markdown).to.be.equal(':smile:');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
|
||||||
|
await cancelEdition(editorWrapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancel', async () => {
|
||||||
|
let prevHtml = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
await edit(editor, editorWrapper, 'xxx yyy zzz');
|
||||||
|
|
||||||
|
await cancelEdition(editorWrapper);
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.equal(prevHtml);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edit comment', async () => {
|
||||||
|
historyHelper.editLastComment();
|
||||||
|
|
||||||
|
let editWrapperLast = historyHelper.getComments().last();
|
||||||
|
let editLast = editWrapperLast.$('.medium');
|
||||||
|
|
||||||
|
await edit(editLast, editWrapperLast, "This is the new and updated text");
|
||||||
|
await utils.common.takeScreenshot(section, "edit comment");
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
//Show versions from last comment edited
|
||||||
|
historyHelper.showVersionsLastComment();
|
||||||
|
await utils.common.takeScreenshot(section, "show comment versions");
|
||||||
|
|
||||||
|
historyHelper.closeVersionsLastComment();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delete last comment', async () => {
|
||||||
|
let deletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||||
|
await historyHelper.deleteLastComment();
|
||||||
|
|
||||||
|
let newDeletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||||
|
|
||||||
|
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter+1);
|
||||||
|
|
||||||
|
await utils.common.takeScreenshot(section, 'deleted comment');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restore last comment', async () => {
|
||||||
|
let deletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||||
|
|
||||||
|
await historyHelper.restoreLastComment();
|
||||||
|
|
||||||
|
let newDeletedCommentsCounter = await historyHelper.countDeletedComments();
|
||||||
|
|
||||||
|
expect(newDeletedCommentsCounter).to.be.equal(deletedCommentsCounter-1);
|
||||||
|
|
||||||
|
await utils.common.takeScreenshot(section, 'restored comment');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
shared.wysiwygTesting = function(parentSelector) {
|
||||||
|
var editor;
|
||||||
|
var editorWrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let isReadMode = !!await editorWrapper.$$('.read-mode').count();
|
||||||
|
|
||||||
|
if (isReadMode) {
|
||||||
|
editor.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
await cleanWysiwyg(editor, editorWrapper);
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
var markdownTextarea = getMarkdownTextarea(editorWrapper);
|
||||||
|
|
||||||
|
browser.wait(EC.elementToBeClickable(markdownTextarea), 10000);
|
||||||
|
|
||||||
|
await markdownTextarea.sendKeys('test');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
await browser.wait(EC.elementToBeClickable(editor), 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
let parent = $(parentSelector);
|
||||||
|
editor = parent.$('.medium');
|
||||||
|
editorWrapper = parent.$('tg-wysiwyg');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bold, test normal behavior and check markdown', async () => {
|
||||||
|
await edit(editor, editorWrapper, "test");
|
||||||
|
selectEditorFirstChild(editor);
|
||||||
|
|
||||||
|
$('.medium-editor-toolbar-active .medium-editor-action-bold').click();
|
||||||
|
|
||||||
|
resetSelection();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('<p><b>test</b></p>');
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
await edit(editor, editorWrapper);
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdown = await getMarkdownText(editorWrapper);
|
||||||
|
|
||||||
|
expect(markdown).to.be.equal('**test**');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('convert to html', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdownTextarea = getMarkdownTextarea(editorWrapper);
|
||||||
|
|
||||||
|
await markdownTextarea.sendKeys('_test2_');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('<p><em>test2</em></p>\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('code block', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys("var test = 2;");
|
||||||
|
|
||||||
|
selectEditorFirstChild(editor);
|
||||||
|
|
||||||
|
$('.medium-editor-toolbar-active .medium-editor-button-last').click();
|
||||||
|
|
||||||
|
$('.code-language-selector').click();
|
||||||
|
$('.code-language-search input').sendKeys('javascript');
|
||||||
|
$('.code-language-search li').click();
|
||||||
|
|
||||||
|
saveEdition();
|
||||||
|
|
||||||
|
let hasHightlighter = !!await editor.$$('.token').count();
|
||||||
|
|
||||||
|
expect(hasHightlighter).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('save with confirmconfirm exit when there is changes', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys('text text text');
|
||||||
|
editor.sendKeys(protractor.Key.ESCAPE);
|
||||||
|
|
||||||
|
await utils.lightbox.confirm.ok();
|
||||||
|
|
||||||
|
let isReadMode = !!await editorWrapper.$$('.read-mode').count();
|
||||||
|
|
||||||
|
expect(isReadMode).to.be.true;
|
||||||
|
|
||||||
|
let html = await editor.getText();
|
||||||
|
|
||||||
|
expect(html).not.to.be.eql('text text text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keep changes on reload', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys('text text text');
|
||||||
|
editor.sendKeys(protractor.Key.ESCAPE);
|
||||||
|
|
||||||
|
browser.sleep(400);
|
||||||
|
browser.refresh();
|
||||||
|
|
||||||
|
let isReadMode = !!await editorWrapper.$$('.read-mode').count();
|
||||||
|
|
||||||
|
expect(isReadMode).to.be.false;
|
||||||
|
|
||||||
|
let html = await editor.getText();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('text text text');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mention user', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys('@use');
|
||||||
|
|
||||||
|
$$('.medium-mention li').get(2).click();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.eql('<p><a href="/profile/user8">@user8</a> </p>');
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdown = await getMarkdownText(editorWrapper);
|
||||||
|
|
||||||
|
expect(markdown).to.be.equal('[@user8](/profile/user8)');
|
||||||
|
|
||||||
|
htmlMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emojis', async () => {
|
||||||
|
await edit(editor, editorWrapper, '');
|
||||||
|
|
||||||
|
editor.sendKeys(':smil');
|
||||||
|
|
||||||
|
$$('.medium-mention li').get(2).click();
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.include('1f604.png');
|
||||||
|
|
||||||
|
markdownMode();
|
||||||
|
|
||||||
|
let markdown = await getMarkdownText(editorWrapper);
|
||||||
|
|
||||||
|
expect(markdown).to.be.equal(':smile:');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancel', async () => {
|
||||||
|
let prevHtml = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
await edit(editor, editorWrapper, 'xxx yyy zzz');
|
||||||
|
|
||||||
|
await cancelEdition(editorWrapper);
|
||||||
|
|
||||||
|
let html = await editor.getInnerHtml();
|
||||||
|
|
||||||
|
expect(html).to.be.equal(prevHtml);
|
||||||
|
});
|
||||||
|
};
|
|
@ -1,6 +1,9 @@
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
var sharedDetail = require('../../shared/detail');
|
var sharedDetail = require('../../shared/detail');
|
||||||
var epicDetailHelper = require('../../helpers').epicDetail;
|
var epicDetailHelper = require('../../helpers').epicDetail;
|
||||||
|
var wysiwyg = require('../../shared/wysiwyg');
|
||||||
|
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||||
|
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -39,7 +42,7 @@ describe('Epic detail', async function(){
|
||||||
|
|
||||||
it('tags edition', sharedDetail.tagsTesting);
|
it('tags edition', sharedDetail.tagsTesting);
|
||||||
|
|
||||||
describe('description', sharedDetail.descriptionTesting);
|
describe('description', sharedWysiwyg.bind(this, '.duty-content'));
|
||||||
|
|
||||||
describe('related userstories', function() {
|
describe('related userstories', function() {
|
||||||
let relatedUserstories = epicDetailHelper.relatedUserstories();
|
let relatedUserstories = epicDetailHelper.relatedUserstories();
|
||||||
|
@ -68,6 +71,8 @@ describe('Epic detail', async function(){
|
||||||
|
|
||||||
it('history', sharedDetail.historyTesting.bind(this, "epics"));
|
it('history', sharedDetail.historyTesting.bind(this, "epics"));
|
||||||
|
|
||||||
|
describe('comments epics', sharedWysiwygComments.bind(this, '.comments', 'epics'));
|
||||||
|
|
||||||
it('block', sharedDetail.blockTesting);
|
it('block', sharedDetail.blockTesting);
|
||||||
|
|
||||||
describe('team requirement edition', sharedDetail.teamRequirementTesting);
|
describe('team requirement edition', sharedDetail.teamRequirementTesting);
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
var sharedDetail = require('../../shared/detail');
|
var sharedDetail = require('../../shared/detail');
|
||||||
|
var wysiwyg = require('../../shared/wysiwyg');
|
||||||
|
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||||
|
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -29,7 +32,7 @@ describe('Issue detail', async function(){
|
||||||
|
|
||||||
it('tags edition', sharedDetail.tagsTesting);
|
it('tags edition', sharedDetail.tagsTesting);
|
||||||
|
|
||||||
describe('description', sharedDetail.descriptionTesting);
|
describe('description', sharedWysiwyg.bind(this, '.duty-content'));
|
||||||
|
|
||||||
it('status edition', sharedDetail.statusTesting.bind(this, 'In progress', 'Ready for test'));
|
it('status edition', sharedDetail.statusTesting.bind(this, 'In progress', 'Ready for test'));
|
||||||
|
|
||||||
|
@ -39,6 +42,8 @@ describe('Issue detail', async function(){
|
||||||
|
|
||||||
it('history', sharedDetail.historyTesting.bind(this, "issues"));
|
it('history', sharedDetail.historyTesting.bind(this, "issues"));
|
||||||
|
|
||||||
|
describe('comments issue', sharedWysiwygComments.bind(this, '.comments', 'issues'));
|
||||||
|
|
||||||
it('block', sharedDetail.blockTesting);
|
it('block', sharedDetail.blockTesting);
|
||||||
|
|
||||||
it('attachments', sharedDetail.attachmentTesting);
|
it('attachments', sharedDetail.attachmentTesting);
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
var sharedDetail = require('../../shared/detail');
|
var sharedDetail = require('../../shared/detail');
|
||||||
var taskDetailHelper = require('../../helpers').taskDetail;
|
var taskDetailHelper = require('../../helpers').taskDetail;
|
||||||
|
var wysiwyg = require('../../shared/wysiwyg');
|
||||||
|
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||||
|
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -31,7 +34,7 @@ describe('Task detail', function(){
|
||||||
|
|
||||||
it('tags edition', sharedDetail.tagsTesting);
|
it('tags edition', sharedDetail.tagsTesting);
|
||||||
|
|
||||||
describe('description', sharedDetail.descriptionTesting);
|
describe('description', sharedWysiwyg.bind(this, '.duty-content'));
|
||||||
|
|
||||||
it('status edition', sharedDetail.statusTesting.bind(this, 'In progress', 'Ready for test'));
|
it('status edition', sharedDetail.statusTesting.bind(this, 'In progress', 'Ready for test'));
|
||||||
|
|
||||||
|
@ -55,6 +58,8 @@ describe('Task detail', function(){
|
||||||
|
|
||||||
it('history', sharedDetail.historyTesting.bind(this, "tasks"));
|
it('history', sharedDetail.historyTesting.bind(this, "tasks"));
|
||||||
|
|
||||||
|
describe('comments task', sharedWysiwygComments.bind(this, '.comments', 'tasks'));
|
||||||
|
|
||||||
it('block', sharedDetail.blockTesting);
|
it('block', sharedDetail.blockTesting);
|
||||||
|
|
||||||
it('attachments', sharedDetail.attachmentTesting);
|
it('attachments', sharedDetail.attachmentTesting);
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
var utils = require('../../utils');
|
var utils = require('../../utils');
|
||||||
var sharedDetail = require('../../shared/detail');
|
var sharedDetail = require('../../shared/detail');
|
||||||
var usDetailHelper = require('../../helpers').usDetail;
|
var usDetailHelper = require('../../helpers').usDetail;
|
||||||
|
var wysiwyg = require('../../shared/wysiwyg');
|
||||||
|
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||||
|
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -30,7 +33,7 @@ describe('User story detail', function(){
|
||||||
|
|
||||||
it('tags edition', sharedDetail.tagsTesting);
|
it('tags edition', sharedDetail.tagsTesting);
|
||||||
|
|
||||||
describe('description', sharedDetail.descriptionTesting);
|
describe('description', sharedWysiwyg.bind(this, '.duty-content'));
|
||||||
|
|
||||||
it('status edition', sharedDetail.statusTesting.bind(this, 'Ready', 'In progress'));
|
it('status edition', sharedDetail.statusTesting.bind(this, 'Ready', 'In progress'));
|
||||||
|
|
||||||
|
@ -44,6 +47,8 @@ describe('User story detail', function(){
|
||||||
|
|
||||||
it('history', sharedDetail.historyTesting.bind(this, "user-stories"));
|
it('history', sharedDetail.historyTesting.bind(this, "user-stories"));
|
||||||
|
|
||||||
|
describe('comments us', sharedWysiwygComments.bind(this, '.comments', 'issues'));
|
||||||
|
|
||||||
it('block', sharedDetail.blockTesting);
|
it('block', sharedDetail.blockTesting);
|
||||||
|
|
||||||
it('attachments', sharedDetail.attachmentTesting);
|
it('attachments', sharedDetail.attachmentTesting);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
var utils = require('../utils');
|
var utils = require('../utils');
|
||||||
var sharedDetail = require('../shared/detail');
|
var sharedDetail = require('../shared/detail');
|
||||||
var wikiHelper = require('../helpers').wiki;
|
var wikiHelper = require('../helpers').wiki;
|
||||||
|
var sharedWysiwyg = require('../shared/wysiwyg').wysiwygTesting;
|
||||||
|
|
||||||
var chai = require('chai');
|
var chai = require('chai');
|
||||||
var chaiAsPromised = require('chai-as-promised');
|
var chaiAsPromised = require('chai-as-promised');
|
||||||
|
@ -64,30 +65,7 @@ describe('wiki', function() {
|
||||||
await utils.common.takeScreenshot("wiki", "deleting-the-created-link");
|
await utils.common.takeScreenshot("wiki", "deleting-the-created-link");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('edition', async function() {
|
describe('wiki editor', sharedWysiwyg.bind(this));
|
||||||
let timesEdited = wikiHelper.editor().getTimesEdited();
|
|
||||||
let lastEditionDatetime = wikiHelper.editor().getLastEditionDateTime();
|
|
||||||
wikiHelper.editor().enabledEditionMode();
|
|
||||||
let settingText = "This is the new text" + new Date().getTime();
|
|
||||||
wikiHelper.editor().setText(settingText);
|
|
||||||
|
|
||||||
//preview
|
|
||||||
wikiHelper.editor().preview();
|
|
||||||
await utils.common.takeScreenshot("wiki", "home-edition-preview");
|
|
||||||
wikiHelper.editor().closePreview();
|
|
||||||
|
|
||||||
//save
|
|
||||||
wikiHelper.editor().save();
|
|
||||||
let newHtml = await wikiHelper.editor().getInnerHtml();
|
|
||||||
let newTimesEdited = wikiHelper.editor().getTimesEdited();
|
|
||||||
let newLastEditionDatetime = wikiHelper.editor().getLastEditionDateTime();
|
|
||||||
|
|
||||||
expect(newHtml).to.be.equal("<p>" + settingText + "</p>");
|
|
||||||
expect(newTimesEdited).to.be.eventually.equal(timesEdited+1);
|
|
||||||
expect(newLastEditionDatetime).to.be.not.equal(lastEditionDatetime);
|
|
||||||
|
|
||||||
await utils.common.takeScreenshot("wiki", "home-edition");
|
|
||||||
});
|
|
||||||
|
|
||||||
it('confirm close with ESC in lightbox', async function() {
|
it('confirm close with ESC in lightbox', async function() {
|
||||||
wikiHelper.editor().enabledEditionMode();
|
wikiHelper.editor().enabledEditionMode();
|
||||||
|
|
After Width: | Height: | Size: 491 B |
After Width: | Height: | Size: 460 B |
After Width: | Height: | Size: 435 B |
After Width: | Height: | Size: 277 B |
After Width: | Height: | Size: 455 B |
After Width: | Height: | Size: 468 B |
After Width: | Height: | Size: 424 B |
After Width: | Height: | Size: 469 B |
After Width: | Height: | Size: 443 B |
After Width: | Height: | Size: 392 B |
After Width: | Height: | Size: 458 B |
After Width: | Height: | Size: 446 B |
After Width: | Height: | Size: 467 B |
After Width: | Height: | Size: 607 B |
After Width: | Height: | Size: 469 B |
After Width: | Height: | Size: 436 B |
After Width: | Height: | Size: 512 B |
After Width: | Height: | Size: 404 B |
After Width: | Height: | Size: 596 B |
After Width: | Height: | Size: 532 B |
After Width: | Height: | Size: 482 B |
After Width: | Height: | Size: 531 B |
After Width: | Height: | Size: 472 B |
After Width: | Height: | Size: 486 B |
After Width: | Height: | Size: 591 B |
After Width: | Height: | Size: 596 B |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 513 B |
After Width: | Height: | Size: 650 B |
After Width: | Height: | Size: 571 B |
After Width: | Height: | Size: 346 B |
After Width: | Height: | Size: 229 B |
After Width: | Height: | Size: 346 B |
After Width: | Height: | Size: 529 B |
After Width: | Height: | Size: 573 B |
After Width: | Height: | Size: 343 B |
After Width: | Height: | Size: 203 B |
After Width: | Height: | Size: 360 B |
After Width: | Height: | Size: 409 B |
After Width: | Height: | Size: 291 B |
After Width: | Height: | Size: 566 B |
After Width: | Height: | Size: 187 B |
After Width: | Height: | Size: 559 B |
After Width: | Height: | Size: 274 B |
After Width: | Height: | Size: 302 B |
After Width: | Height: | Size: 281 B |