402 lines
13 KiB
CoffeeScript
402 lines
13 KiB
CoffeeScript
###
|
|
# 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
|
|
])
|