New wysiwyg editor (like Medium editor)
|
@ -5,10 +5,8 @@ before_install:
|
|||
- export CHROME_BIN=chromium-browser
|
||||
- export DISPLAY=:99.0
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
- travis_retry npm install -g bower
|
||||
- travis_retry npm install -g gulp
|
||||
install:
|
||||
- travis_retry npm install
|
||||
- travis_retry bower install
|
||||
before_script:
|
||||
- gulp deploy
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
## 3.1.0 No name yet (no date yet)
|
||||
- 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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
Emoji provided free by [Twemoji](https://github.com/twitter/twemoji)
|
||||
|
||||
#### 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
|
||||
|
||||
**Node + Bower + Gulp**
|
||||
**Node + Gulp**
|
||||
|
||||
We recommend using [nvm](https://github.com/creationix/nvm) to manage different node versions
|
||||
```
|
||||
npm install -g gulp
|
||||
npm install -g bower
|
||||
npm install
|
||||
bower install
|
||||
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=full # To test all the platform authenticated
|
||||
```
|
||||
```
|
|
@ -39,7 +39,7 @@ class Model
|
|||
instance._modifiedAttrs = _.cloneDeep(@._modifiedAttrs)
|
||||
instance._isModified = _.cloneDeep(@._isModified)
|
||||
|
||||
return instance
|
||||
return instance
|
||||
|
||||
clone: ->
|
||||
instance = new Model(@._name, @._attrs, @._dataTypes)
|
||||
|
|
|
@ -390,14 +390,22 @@ Svg = () ->
|
|||
|
||||
module.directive("tgSvg", [Svg])
|
||||
|
||||
Autofocus = ($timeout) ->
|
||||
Autofocus = ($timeout, $parse, animationFrame) ->
|
||||
return {
|
||||
restrict: 'A',
|
||||
link : ($scope, $element) ->
|
||||
$timeout -> $element[0].focus()
|
||||
link : ($scope, $element, attrs) ->
|
||||
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', () ->
|
||||
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])
|
||||
|
||||
#############################################################################
|
||||
## Editable description directive
|
||||
## Editable subject directive
|
||||
#############################################################################
|
||||
|
||||
EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading, $selectedText, $modelTransform, $template, $translate) ->
|
||||
template = $template.get("common/components/editable-description.html")
|
||||
noDescriptionMegEditMode = $template.get("common/components/editable-description-msg-edit-mode.html")
|
||||
noDescriptionMegReadMode = $template.get("common/components/editable-description-msg-read-mode.html")
|
||||
EditableSubjectDirective = ($rootscope, $repo, $confirm, $loading, $modelTransform, $template) ->
|
||||
template = $template.get("common/components/editable-subject.html")
|
||||
|
||||
link = ($scope, $el, $attrs, $model) ->
|
||||
$el.find('.edit-description').hide()
|
||||
$el.find('.view-description .edit').hide()
|
||||
|
||||
$scope.$on "object:updated", () ->
|
||||
$el.find('.edit-description').hide()
|
||||
$el.find('.view-description').show()
|
||||
$el.find('.edit-subject').hide()
|
||||
$el.find('.view-subject').show()
|
||||
|
||||
isEditable = ->
|
||||
return $scope.project.my_permissions.indexOf($attrs.requiredPerm) != -1
|
||||
|
||||
save = (description) ->
|
||||
save = (subject) ->
|
||||
currentLoading = $loading()
|
||||
.target($el.find('.save-container'))
|
||||
.start()
|
||||
|
||||
transform = $modelTransform.save (item) ->
|
||||
item.description = description
|
||||
|
||||
item.subject = subject
|
||||
|
||||
return item
|
||||
|
||||
transform.then ->
|
||||
transform.then =>
|
||||
$confirm.notify("success")
|
||||
$rootscope.$broadcast("object:updated")
|
||||
$el.find('.edit-description').hide()
|
||||
$el.find('.view-description').show()
|
||||
$el.find('.edit-subject').hide()
|
||||
$el.find('.view-subject').show()
|
||||
|
||||
transform.then null, ->
|
||||
$confirm.notify("error")
|
||||
|
@ -537,60 +534,43 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading,
|
|||
transform.finally ->
|
||||
currentLoading.finish()
|
||||
|
||||
cancelEdition = () ->
|
||||
$scope.item.revert()
|
||||
$el.find('.edit-description').hide()
|
||||
$el.find('.view-description').show()
|
||||
return transform
|
||||
|
||||
$el.on "mouseup", ".view-description", (event) ->
|
||||
# We want to dettect the a inside the div so we use the target and
|
||||
# not the currentTarget
|
||||
target = angular.element(event.target)
|
||||
$el.click ->
|
||||
return if not isEditable()
|
||||
return if target.is('a')
|
||||
return if $selectedText.get().length
|
||||
|
||||
$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.find('.edit-subject').show()
|
||||
$el.find('.view-subject').hide()
|
||||
$el.find('input').focus()
|
||||
|
||||
$el.on "click", ".save", (e) ->
|
||||
e.preventDefault()
|
||||
|
||||
description = $scope.item.description
|
||||
save(description)
|
||||
subject = $scope.item.subject
|
||||
save(subject)
|
||||
|
||||
$el.on "keydown", "textarea", (event) ->
|
||||
return if event.keyCode != 27
|
||||
$el.on "keyup", "input", (event) ->
|
||||
if event.keyCode == 13
|
||||
subject = $scope.item.subject
|
||||
save(subject)
|
||||
else if event.keyCode == 27
|
||||
$scope.$apply () => $model.$modelValue.revert()
|
||||
|
||||
$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()
|
||||
$el.find('.edit-subject').hide()
|
||||
$el.find('.view-subject').show()
|
||||
|
||||
$el.find('.edit-subject').hide()
|
||||
|
||||
$scope.$watch $attrs.ngModel, (value) ->
|
||||
return if not value
|
||||
|
||||
$scope.item = value
|
||||
if isEditable()
|
||||
$el.find('.view-description .edit').show()
|
||||
$el.find('.view-description .us-content').addClass('editable')
|
||||
$scope.noDescriptionMsg = $compile(noDescriptionMegEditMode)($scope)
|
||||
else
|
||||
$scope.noDescriptionMsg = $compile(noDescriptionMegReadMode)($scope)
|
||||
|
||||
if not isEditable()
|
||||
$el.find('.view-subject .edit').remove()
|
||||
|
||||
$scope.$on "$destroy", ->
|
||||
$el.off()
|
||||
|
||||
|
||||
return {
|
||||
link: link
|
||||
restrict: "EA"
|
||||
|
@ -598,81 +578,8 @@ EditableDescriptionDirective = ($rootscope, $repo, $confirm, $compile, $loading,
|
|||
template: template
|
||||
}
|
||||
|
||||
module.directive("tgEditableDescription", [
|
||||
"$rootScope",
|
||||
"$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])
|
||||
|
||||
module.directive("tgEditableSubject", ["$rootScope", "$tgRepo", "$tgConfirm", "$tgLoading", "$tgQueueModelTransformation",
|
||||
"$tgTemplate", EditableSubjectDirective])
|
||||
|
||||
#############################################################################
|
||||
## Common list directives
|
||||
|
|
|
@ -75,7 +75,6 @@ sizeFormat = =>
|
|||
|
||||
module.filter("sizeFormat", sizeFormat)
|
||||
|
||||
|
||||
toMutableFilter = ->
|
||||
toMutable = (js) ->
|
||||
return js.toJS()
|
||||
|
@ -128,6 +127,15 @@ darkerFilter = ->
|
|||
|
||||
module.filter("darker", darkerFilter)
|
||||
|
||||
markdownToHTML = (wysiwigService) ->
|
||||
return (input) ->
|
||||
if input
|
||||
return wysiwigService.getHTML(input)
|
||||
|
||||
return ""
|
||||
|
||||
module.filter("markdownToHTML", ["tgWysiwygService", markdownToHTML])
|
||||
|
||||
inArray = ($filter) ->
|
||||
return (list, arrayFilter, element) ->
|
||||
if arrayFilter
|
||||
|
|
|
@ -92,15 +92,16 @@ Loader = ($rootscope) ->
|
|||
|
||||
return {
|
||||
pageLoaded: pageLoaded
|
||||
open: () -> open
|
||||
start: (auto=false) ->
|
||||
if !open
|
||||
start()
|
||||
autoClose() if auto
|
||||
onStart: (fn) ->
|
||||
$rootscope.$on("loader:start", fn)
|
||||
return $rootscope.$on("loader:start", fn)
|
||||
|
||||
onEnd: (fn) ->
|
||||
$rootscope.$on("loader:end", fn)
|
||||
return $rootscope.$on("loader:end", fn)
|
||||
|
||||
logRequest: () ->
|
||||
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",
|
||||
"tgErrorHandlingService",
|
||||
"$tgConfig",
|
||||
"tgProjectService"
|
||||
"tgProjectService",
|
||||
"tgWysiwygService"
|
||||
]
|
||||
|
||||
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(@)
|
||||
|
||||
@scope.usRef = @params.usref
|
||||
|
@ -89,7 +91,7 @@ class UserStoryDetailController extends mixOf(taiga.Controller, taiga.PageMixin)
|
|||
description = @translate.instant("US.PAGE_DESCRIPTION", {
|
||||
userStoryStatus: @scope.statusById[@scope.us.status]?.name or "--"
|
||||
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
|
||||
userStoryTotalTasks: totalTasks
|
||||
userStoryProgressPercentage: progressPercentage
|
||||
|
|
|
@ -218,126 +218,82 @@ WikiSummaryDirective = ($log, $template, $compile, $translate, avatarService) ->
|
|||
|
||||
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
|
||||
|
||||
#############################################################################
|
||||
## 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) ->
|
||||
$scope.saveDescription = $qqueue.bindAdd (description, cb) ->
|
||||
onSuccess = (wikiPage) ->
|
||||
if not wiki.id?
|
||||
if not $scope.item.id?
|
||||
$analytics.trackEvent("wikipage", "create", "create wiki page", 1)
|
||||
|
||||
$model.$setViewValue wikiPage.clone()
|
||||
|
||||
$wikiHistoryService.loadHistoryEntries()
|
||||
wikiHistoryService.loadHistoryEntries()
|
||||
$confirm.notify("success")
|
||||
switchToReadMode()
|
||||
|
||||
onError = ->
|
||||
$confirm.notify("error")
|
||||
|
||||
currentLoading = $loading()
|
||||
.target($el.find('.save'))
|
||||
.start()
|
||||
$scope.item.content = description
|
||||
|
||||
if wiki.id?
|
||||
promise = $repo.save(wiki).then(onSuccess, onError)
|
||||
if $scope.item.id?
|
||||
promise = $repo.save($scope.item).then(onSuccess, onError)
|
||||
else
|
||||
promise = $repo.create("wiki", wiki).then(onSuccess, onError)
|
||||
promise = $repo.create("wiki", $scope.item).then(onSuccess, onError)
|
||||
|
||||
promise.finally ->
|
||||
currentLoading.finish()
|
||||
promise.finally(cb)
|
||||
|
||||
$el.on "click", "a", (event) ->
|
||||
target = angular.element(event.currentTarget)
|
||||
href = target.attr('href')
|
||||
uploadFile = (file, cb) ->
|
||||
return attachmentsFullService.addAttachment($scope.project.id, $scope.item.id, 'wiki_page', file).then (result) ->
|
||||
cb(result.getIn(['file', 'name']), result.getIn(['file', 'url']))
|
||||
|
||||
if href.indexOf("#") == 0
|
||||
event.preventDefault()
|
||||
$('body').scrollTop($(href).offset().top)
|
||||
$scope.uploadFiles = (files, cb) ->
|
||||
for file in files
|
||||
uploadFile(file, cb)
|
||||
|
||||
$el.on "mousedown", ".view-wiki-content", (event) ->
|
||||
target = angular.element(event.target)
|
||||
return if not isEditable()
|
||||
return if event.button == 2
|
||||
$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
|
||||
|
||||
$el.on "mouseup", ".view-wiki-content", (event) ->
|
||||
target = angular.element(event.target)
|
||||
return if getSelectedText()
|
||||
return if not isEditable()
|
||||
return if target.is('a')
|
||||
return if target.is('pre')
|
||||
$scope.$watch 'project', (project) ->
|
||||
return if !project
|
||||
|
||||
switchToEditMode()
|
||||
|
||||
$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()
|
||||
$scope.editableDescription = project.my_permissions.indexOf("modify_wiki_page") != -1
|
||||
|
||||
return {
|
||||
link: link
|
||||
restrict: "EA"
|
||||
require: "ngModel"
|
||||
templateUrl: "wiki/editable-wiki-content.html"
|
||||
scope: true,
|
||||
link: link,
|
||||
template: """
|
||||
<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",
|
||||
"$tgAnalytics", "$tgQqueue", "$translate", "tgWikiHistoryService",
|
||||
EditableWikiContentDirective])
|
||||
module.directive("tgWikiWysiwyg", [
|
||||
"$tgQueueModelTransformation",
|
||||
"$rootScope",
|
||||
"$tgConfirm",
|
||||
"tgAttachmentsFullService",
|
||||
"$tgQqueue", "$tgRepo", "$tgAnalytics", "tgWikiHistoryService"
|
||||
WikiWysiwyg])
|
||||
|
|
|
@ -251,6 +251,16 @@ getRandomDefaultColor = () ->
|
|||
getDefaulColorList = () ->
|
||||
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.addClass = addClass
|
||||
taiga.nl2br = nl2br
|
||||
|
@ -280,3 +290,4 @@ taiga.isPdf = isPdf
|
|||
taiga.patch = patch
|
||||
taiga.getRandomDefaultColor = getRandomDefaultColor
|
||||
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": {
|
||||
"H1_BUTTON": "First Level Heading",
|
||||
"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",
|
||||
"OUTDATED": "Another person has made changes while you were editing. Check the new version on the activiy tab before you save your changes.",
|
||||
"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.",
|
||||
"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 {
|
||||
line-height: 1.4rem;
|
||||
margin-bottom: 2rem;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
h1 {
|
||||
|
@ -37,7 +36,7 @@
|
|||
ol {
|
||||
line-height: 1.5;
|
||||
list-style-position: outside;
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 1rem;
|
||||
margin-top: 0;
|
||||
padding-left: 2em;
|
||||
ul,
|
||||
|
@ -48,6 +47,15 @@
|
|||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
.list-stye-none {
|
||||
list-style: none;
|
||||
}
|
||||
b {
|
||||
font-weight: bold;
|
||||
}
|
||||
i {
|
||||
font-style: italic;
|
||||
}
|
||||
dl {
|
||||
dt {
|
||||
font-size: 1em;
|
||||
|
@ -63,6 +71,7 @@
|
|||
}
|
||||
a {
|
||||
color: $primary;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: $primary-light;
|
||||
}
|
||||
|
@ -134,3 +143,113 @@
|
|||
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) ->
|
||||
@.hiddenDeletedComment = true
|
||||
@.commentContent = angular.copy(@.comment)
|
||||
|
||||
showDeletedComment: () ->
|
||||
@.hiddenDeletedComment = false
|
||||
|
@ -45,6 +44,9 @@ class CommentController
|
|||
@.user = @currentUserService.getUser()
|
||||
return @.user.get('id') == @.comment.user.pk || @permissionService.check('modify_project')
|
||||
|
||||
saveComment: (text, cb) ->
|
||||
@.onEditComment({commentId: @.comment.id, commentData: text, callback: cb})
|
||||
|
||||
displayCommentHistory: () ->
|
||||
@lightboxFactory.create('tg-lb-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")
|
||||
img.comment-avatar(
|
||||
tg-avatar="vm.comment.user"
|
||||
|
@ -23,38 +21,21 @@ include ../../../partials/common/components/wysiwyg.jade
|
|||
.comment-container
|
||||
.comment-text.wysiwyg(
|
||||
ng-if="!vm.editMode"
|
||||
ng-bind-html="vm.comment.comment_html"
|
||||
tg-bind-code="vm.comment.comment"
|
||||
)
|
||||
.comment-editor(
|
||||
ng-if="vm.editMode"
|
||||
ng-keyup="vm.checkCancelComment($event)"
|
||||
)
|
||||
.edit-comment(ng-model="vm.type")
|
||||
textarea(
|
||||
ng-model="vm.commentContent.comment"
|
||||
)
|
||||
.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-comment-edit-wysiwyg.edit-comment
|
||||
|
||||
.comment-options(ng-if="vm.canEditDeleteComment() && !vm.editMode")
|
||||
tg-svg.comment-option(
|
||||
svg-icon="icon-edit"
|
||||
svg-title-translate="COMMON.EDIT"
|
||||
ng-click="vm.onEditMode({commentId: vm.comment.id})"
|
||||
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(
|
||||
svg-icon="icon-trash"
|
||||
svg-title-translate="COMMON.DELETE"
|
||||
|
@ -107,5 +88,5 @@ include ../../../partials/common/components/wysiwyg.jade
|
|||
span(translate="COMMENTS.RESTORE")
|
||||
p.deleted-comment-comment(
|
||||
ng-if="!vm.hiddenDeletedComment"
|
||||
ng-bind-html="vm.comment.comment_html"
|
||||
tg-bind-code="vm.comment.comment"
|
||||
)
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
.comments {
|
||||
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 {
|
||||
margin-top: 1rem;
|
||||
textarea {
|
||||
|
@ -20,7 +31,6 @@
|
|||
margin-top: 1rem;
|
||||
padding: .5rem 4rem;
|
||||
}
|
||||
|
||||
}
|
||||
.comment {
|
||||
display: block;
|
||||
|
@ -143,12 +153,6 @@
|
|||
.deleted-comment-comment {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.comment-editor {
|
||||
textarea {
|
||||
height: 5rem;
|
||||
min-height: 5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
include ../../../partials/common/components/wysiwyg.jade
|
||||
|
||||
section.comments
|
||||
.comments-wrapper
|
||||
tg-comment.comment(
|
||||
|
@ -15,25 +13,11 @@ section.comments
|
|||
on-edit-mode="vm.onEditMode({commentId: commentId})"
|
||||
on-delete-comment="vm.onDeleteComment({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-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(
|
||||
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
|
||||
return @rs.history.deleteComment(type, objectId, activityId).then =>
|
||||
@._loadHistory()
|
||||
@.deleting = commentId
|
||||
@.deleting = null
|
||||
|
||||
editComment: (commentId, comment) ->
|
||||
type = @.name
|
||||
|
@ -93,12 +93,10 @@ class HistorySectionController
|
|||
@._loadHistory()
|
||||
@.editing = null
|
||||
|
||||
addComment: () ->
|
||||
type = @.type
|
||||
@.loading = true
|
||||
addComment: (cb) ->
|
||||
@repo.save(@.type).then =>
|
||||
@._loadHistory()
|
||||
@.loading = false
|
||||
cb()
|
||||
|
||||
onOrderComments: () ->
|
||||
@.reverse = !@.reverse
|
||||
|
|
|
@ -149,12 +149,16 @@ describe "HistorySection", ->
|
|||
objectId = historyCtrl.id
|
||||
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
|
||||
historyCtrl.deleteComment(commentId).then () ->
|
||||
ctrlPromise = historyCtrl.deleteComment(commentId)
|
||||
expect(historyCtrl.deleting).to.be.equal(7)
|
||||
|
||||
deleteCommentPromise.resolve()
|
||||
|
||||
ctrlPromise.then () ->
|
||||
expect(historyCtrl._loadHistory).have.been.called
|
||||
expect(historyCtrl.deleting).to.be.equal(7)
|
||||
expect(historyCtrl.deleting).to.be.null
|
||||
|
||||
it "edit comment", () ->
|
||||
historyCtrl = controller "HistorySection"
|
||||
|
@ -201,13 +205,15 @@ describe "HistorySection", ->
|
|||
|
||||
historyCtrl.type = "type"
|
||||
type = historyCtrl.type
|
||||
historyCtrl.loading = true
|
||||
|
||||
cb = sinon.spy()
|
||||
|
||||
promise = mocks.tgRepo.save.withArgs(type).promise().resolve()
|
||||
|
||||
historyCtrl.addComment().then () ->
|
||||
historyCtrl.addComment(cb).then () ->
|
||||
expect(historyCtrl._loadHistory).has.been.called
|
||||
expect(historyCtrl.loading).to.be.false
|
||||
expect(cb).to.have.been.called
|
||||
|
||||
|
||||
it "order comments", () ->
|
||||
historyCtrl = controller "HistorySection"
|
||||
|
|
|
@ -18,8 +18,8 @@ section.history(
|
|||
on-delete-comment="vm.deleteComment(commentId)"
|
||||
on-restore-deleted-comment="vm.restoreDeletedComment(commentId)"
|
||||
on-edit-mode="vm.toggleEditMode(commentId)"
|
||||
on-add-comment="vm.addComment()"
|
||||
on-edit-comment="vm.editComment(commentId, commentData)"
|
||||
on-add-comment="vm.addComment(callback)"
|
||||
on-edit-comment="vm.editComment(commentId, commentData, callback)"
|
||||
edit-mode="vm.editMode"
|
||||
|
||||
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")
|
||||
|
||||
section.duty-content(
|
||||
tg-editable-description
|
||||
tg-editable-wysiwyg
|
||||
ng-model="epic"
|
||||
required-perm="modify_epic"
|
||||
)
|
||||
section.duty-content
|
||||
tg-item-wysiwyg(
|
||||
type="epic",
|
||||
model="epic",
|
||||
required-perm="modify_epic"
|
||||
)
|
||||
|
||||
// Custom Fields
|
||||
tg-custom-attributes-values(
|
||||
|
|
|
@ -33,13 +33,13 @@ div.wrapper(
|
|||
permissions="modify_issue"
|
||||
)
|
||||
tg-created-by-display.ticket-created-by(ng-model="issue")
|
||||
|
||||
section.duty-content(
|
||||
tg-editable-description
|
||||
tg-editable-wysiwyg
|
||||
ng-model="issue"
|
||||
required-perm="modify_issue"
|
||||
)
|
||||
|
||||
section.duty-content
|
||||
tg-item-wysiwyg(
|
||||
type="issue",
|
||||
model="issue",
|
||||
required-perm="modify_issue"
|
||||
)
|
||||
|
||||
// Custom Fields
|
||||
tg-custom-attributes-values(
|
||||
|
|
|
@ -43,7 +43,12 @@ div.wrapper(
|
|||
)
|
||||
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
|
||||
tg-custom-attributes-values(
|
||||
|
|
|
@ -43,7 +43,12 @@ div.wrapper(
|
|||
)
|
||||
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
|
||||
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")
|
||||
|
||||
h2.wiki-title(ng-bind='wikiTitle')
|
||||
section.wiki-content(
|
||||
tg-editable-wysiwyg,
|
||||
tg-editable-wiki-content,
|
||||
ng-model="wiki"
|
||||
)
|
||||
|
||||
tg-wiki-wysiwyg(model="wiki")
|
||||
|
||||
.summary.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
|
||||
@import '../../../vendor/bourbon/app/assets/stylesheets/bourbon';
|
||||
@import '../../../node_modules/bourbon/core/bourbon';
|
||||
|
||||
//#################################################
|
||||
// dependencies
|
||||
|
@ -22,3 +22,13 @@
|
|||
@import '../dependencies/mixins/svg';
|
||||
@import '../dependencies/mixins/track-buttons';
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
};
|
||||
|
||||
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() {
|
||||
let el = $('section.history');
|
||||
let obj = {
|
||||
|
@ -179,16 +161,6 @@ helper.history = function() {
|
|||
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() {
|
||||
let comments = await el.$$(".comment-wrapper");
|
||||
return comments.length;
|
||||
|
@ -227,6 +199,10 @@ helper.history = function() {
|
|||
await browser.waitForAngular();
|
||||
},
|
||||
|
||||
getComments: function() {
|
||||
return $$('tg-comment');
|
||||
},
|
||||
|
||||
showVersionsLastComment: async function() {
|
||||
el.$$(".comment-edited a").last().click();
|
||||
await browser.waitForAngular();
|
||||
|
@ -252,11 +228,11 @@ helper.history = function() {
|
|||
el.$$(".deleted-comment-wrapper .restore-comment").last().click();
|
||||
await browser.waitForAngular();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return obj;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
helper.block = function() {
|
||||
let el = $('tg-block-button');
|
||||
|
|
|
@ -6,6 +6,7 @@ var customFieldsHelper = require('../helpers/custom-fields-helper');
|
|||
var commonUtil = require('../utils/common');
|
||||
var lightbox = require('../utils/lightbox');
|
||||
var notifications = require('../utils/notifications');
|
||||
var sharedWysiwyg = require('./wysiwyg').wysiwygTesting;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -48,49 +49,6 @@ shared.tagsTesting = async function() {
|
|||
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) {
|
||||
let statusHelper = detailHelper.statusSelector();
|
||||
|
||||
|
@ -195,68 +153,9 @@ shared.assignedToTesting = function() {
|
|||
shared.historyTesting = async function(screenshotsFolder) {
|
||||
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
|
||||
await historyHelper.selectActivityTab();
|
||||
await utils.common.takeScreenshot(screenshotsFolder, "show activity tab");
|
||||
|
||||
let activitiesCounter = await historyHelper.countActivities();
|
||||
|
||||
expect(newCommentsCounter).to.be.least(1);
|
||||
}
|
||||
|
||||
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 sharedDetail = require('../../shared/detail');
|
||||
var epicDetailHelper = require('../../helpers').epicDetail;
|
||||
var wysiwyg = require('../../shared/wysiwyg');
|
||||
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -39,7 +42,7 @@ describe('Epic detail', async function(){
|
|||
|
||||
it('tags edition', sharedDetail.tagsTesting);
|
||||
|
||||
describe('description', sharedDetail.descriptionTesting);
|
||||
describe('description', sharedWysiwyg.bind(this, '.duty-content'));
|
||||
|
||||
describe('related userstories', function() {
|
||||
let relatedUserstories = epicDetailHelper.relatedUserstories();
|
||||
|
@ -68,6 +71,8 @@ describe('Epic detail', async function(){
|
|||
|
||||
it('history', sharedDetail.historyTesting.bind(this, "epics"));
|
||||
|
||||
describe('comments epics', sharedWysiwygComments.bind(this, '.comments', 'epics'));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
describe('team requirement edition', sharedDetail.teamRequirementTesting);
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
var utils = require('../../utils');
|
||||
var sharedDetail = require('../../shared/detail');
|
||||
var wysiwyg = require('../../shared/wysiwyg');
|
||||
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -29,7 +32,7 @@ describe('Issue detail', async function(){
|
|||
|
||||
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'));
|
||||
|
||||
|
@ -39,6 +42,8 @@ describe('Issue detail', async function(){
|
|||
|
||||
it('history', sharedDetail.historyTesting.bind(this, "issues"));
|
||||
|
||||
describe('comments issue', sharedWysiwygComments.bind(this, '.comments', 'issues'));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
it('attachments', sharedDetail.attachmentTesting);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
var utils = require('../../utils');
|
||||
var sharedDetail = require('../../shared/detail');
|
||||
var taskDetailHelper = require('../../helpers').taskDetail;
|
||||
var wysiwyg = require('../../shared/wysiwyg');
|
||||
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -31,7 +34,7 @@ describe('Task detail', function(){
|
|||
|
||||
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'));
|
||||
|
||||
|
@ -55,6 +58,8 @@ describe('Task detail', function(){
|
|||
|
||||
it('history', sharedDetail.historyTesting.bind(this, "tasks"));
|
||||
|
||||
describe('comments task', sharedWysiwygComments.bind(this, '.comments', 'tasks'));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
it('attachments', sharedDetail.attachmentTesting);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
var utils = require('../../utils');
|
||||
var sharedDetail = require('../../shared/detail');
|
||||
var usDetailHelper = require('../../helpers').usDetail;
|
||||
var wysiwyg = require('../../shared/wysiwyg');
|
||||
var sharedWysiwyg = wysiwyg.wysiwygTesting;
|
||||
var sharedWysiwygComments = wysiwyg.wysiwygTestingComments;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -30,7 +33,7 @@ describe('User story detail', function(){
|
|||
|
||||
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'));
|
||||
|
||||
|
@ -44,6 +47,8 @@ describe('User story detail', function(){
|
|||
|
||||
it('history', sharedDetail.historyTesting.bind(this, "user-stories"));
|
||||
|
||||
describe('comments us', sharedWysiwygComments.bind(this, '.comments', 'issues'));
|
||||
|
||||
it('block', sharedDetail.blockTesting);
|
||||
|
||||
it('attachments', sharedDetail.attachmentTesting);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
var utils = require('../utils');
|
||||
var sharedDetail = require('../shared/detail');
|
||||
var wikiHelper = require('../helpers').wiki;
|
||||
var sharedWysiwyg = require('../shared/wysiwyg').wysiwygTesting;
|
||||
|
||||
var chai = require('chai');
|
||||
var chaiAsPromised = require('chai-as-promised');
|
||||
|
@ -64,30 +65,7 @@ describe('wiki', function() {
|
|||
await utils.common.takeScreenshot("wiki", "deleting-the-created-link");
|
||||
});
|
||||
|
||||
it('edition', async function() {
|
||||
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");
|
||||
});
|
||||
describe('wiki editor', sharedWysiwyg.bind(this));
|
||||
|
||||
it('confirm close with ESC in lightbox', async function() {
|
||||
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 |