From 1a15ab955e7f025074bc559e34cceb35207b493c Mon Sep 17 00:00:00 2001 From: Juanfran Date: Thu, 23 Feb 2017 15:21:13 +0100 Subject: [PATCH] code snippets --- app/js/medium-mention.js | 4 +- app/locales/taiga/locale-en.json | 3 + .../detail/header/detail-header.jade | 5 +- .../wysiwyg-code-hightlighter.service.coffee | 169 ++--------- .../wysiwyg-code-lightbox.directive.coffee | 55 ++++ .../wysiwyg-code-lightbox.jade | 25 ++ .../wysiwyg-code-lightbox.scss | 3 + .../wysiwyg/wysiwyg.directive.coffee | 164 +++++++---- app/modules/components/wysiwyg/wysiwyg.scss | 11 +- .../common/components/wysiwyg-toolbar.jade | 9 + app/styles/layout/ticket-detail.scss | 2 +- app/themes/high-contrast/variables.scss | 1 + app/themes/material-design/variables.scss | 1 + app/themes/taiga/variables.scss | 1 + e2e/helpers/detail-helper.js | 7 +- e2e/shared/wysiwyg.js | 262 +++++++++++------- e2e/suites/backlog.e2e.js | 2 +- e2e/suites/wiki.e2e.js | 48 +--- e2e/utils/common.js | 14 + 19 files changed, 423 insertions(+), 363 deletions(-) create mode 100644 app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.directive.coffee create mode 100644 app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.jade create mode 100644 app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.scss diff --git a/app/js/medium-mention.js b/app/js/medium-mention.js index 240f6722..936874be 100644 --- a/app/js/medium-mention.js +++ b/app/js/medium-mention.js @@ -6,7 +6,7 @@ var MentionExtension = MediumEditor.Extension.extend({ this.subscribe('blur', this.cancel.bind(this)); }, isEditMode: function() { - return !this.base.origElements.parentNode.classList.contains('read-mode') + return !this.base.origElements.parentNode.classList.contains('read-mode'); }, cancel: function() { if (this.isEditMode()) { @@ -248,7 +248,7 @@ var MentionExtension = MediumEditor.Extension.extend({ li.innerText = '@' + it.username; } - li.addEventListener('click', this.selectMention.bind(this, it)); + li.addEventListener('mousedown', this.selectMention.bind(this, it)); ul.appendChild(li); }.bind(this)); diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index 7fd43392..1cd43791 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -214,6 +214,9 @@ } }, "WYSIWYG": { + "CODE_SNIPPET": "Code Snippet", + "SELECT_LANGUAGE_PLACEHOLDER": "Select Language", + "SELECT_LANGUAGE_REMOVE_FORMATING": "Remove formatting", "OUTDATED": "Another person has made changes while you were editing. Check the new version on the activiy tab before you save your changes.", "MARKDOWN_HELP": "Markdown syntax help" }, diff --git a/app/modules/components/detail/header/detail-header.jade b/app/modules/components/detail/header/detail-header.jade index 842a09d7..a678babd 100644 --- a/app/modules/components/detail/header/detail-header.jade +++ b/app/modules/components/detail/header/detail-header.jade @@ -11,11 +11,12 @@ span.detail-subject.e2e-title-subject( ng-if="!vm.permissions.canEdit" ) {{vm.item.subject}} - tg-svg.detail-edit.e2e-detail-edit( + a( + href="" ng-if="vm.permissions.canEdit" - svg-icon="icon-edit" ng-click="vm.editSubject(true)" ) + tg-svg.detail-edit.e2e-detail-edit(svg-icon="icon-edit") .edit-title-wrapper(ng-if="vm.editMode") input.edit-title-input.e2e-title-input( diff --git a/app/modules/components/wysiwyg/wysiwyg-code-hightlighter.service.coffee b/app/modules/components/wysiwyg/wysiwyg-code-hightlighter.service.coffee index 2eeee29e..7a07e17f 100644 --- a/app/modules/components/wysiwyg/wysiwyg-code-hightlighter.service.coffee +++ b/app/modules/components/wysiwyg/wysiwyg-code-hightlighter.service.coffee @@ -23,16 +23,21 @@ ### class WysiwygCodeHightlighterService - constructor: () -> - if !@.languages - @.loadLanguages() + getLanguages: () -> + return new Promise (resolve, reject) => + if @.languages + resolve(@.languages) + else if @.loadPromise + @.loadPromise.then () => resolve(@.languages) + else + @.loadPromise = $.getJSON("/#{window._version}/prism/prism-languages.json").then (_languages_) => + @.loadPromise = null + @.languages = _.map _languages_, (it) -> + it.url = "/#{window._version}/prism/" + it.file - loadLanguages: () -> - $.getJSON("/#{window._version}/prism/prism-languages.json").then (_languages_) => - @.languages = _.map _languages_, (it) -> - it.url = "/#{window._version}/prism/" + it.file + return it - return it + resolve(@.languages) getLanguageInClassList: (classes) -> lan = _.find @.languages, (it) -> @@ -41,123 +46,6 @@ class WysiwygCodeHightlighterService 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) - code.parentNode.classList.add('language-' + currentLan) - - 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) - - languageClass = _.find code.classList, (className) -> - return className && className.indexOf('language-') != -1 - - if languageClass - code.classList.remove(languageClass.replace('language-', '')) - code.classList.remove(languageClass) - - code.classList.add('language-' + lan) - code.classList.add(lan) - - document.body.appendChild(tab) - - code.dataset.tab = tab - - if !code.dataset.tabId - code.dataset.tabId = id - code.classList.add(id) - - tab.dataset.tabId = code.dataset.tabId - - tab.classList.add('code-language-selector') # styles - tab.classList.add('medium-' + mediumInstance.id) # used to delete - - @.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) { %> -
  • <%- lan %>
  • <% }); - %> - """); - - 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] @@ -165,35 +53,22 @@ class WysiwygCodeHightlighterService else resolve() - removeHightlighter: (element) -> - codes = $(element).find('code') - - codes.each (index, code) -> - code.innerHTML = code.innerText - # firefox adds br instead of new lines inside replaceCodeBrToNl: (code) -> $(code).find('br').replaceWith('\n') + hightlightCode: (code) -> + @.replaceCodeBrToNl(code) + + lan = @.getLanguageInClassList(code.classList) + + if lan + @.loadLanguage(lan).then () -> Prism.highlightElement(code) + addHightlighter: (element) -> codes = $(element).find('code') - codes.each (index, code) => - @.replaceCodeBrToNl(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) + codes.each (index, code) => @.hightlightCode(code) angular.module("taigaComponents") .service("tgWysiwygCodeHightlighterService", WysiwygCodeHightlighterService) diff --git a/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.directive.coffee b/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.directive.coffee new file mode 100644 index 00000000..60ab593b --- /dev/null +++ b/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.directive.coffee @@ -0,0 +1,55 @@ +### +# Copyright (C) 2014-2017 Andrey Antukh +# Copyright (C) 2014-2017 Jesús Espino Garcia +# Copyright (C) 2014-2017 David Barragán Merino +# Copyright (C) 2014-2017 Alejandro Alonso +# Copyright (C) 2014-2017 Juan Francisco Alcántara +# Copyright (C) 2014-2017 Xavi Julian +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# File: modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.directive.coffee +### + +WysiwygCodeLightbox = (lightboxService) -> + link = (scope, el, attrs, ctrl) -> + scope.$watch 'visible', (visible) -> + if visible && !el.hasClass('open') + scope.open = true + lightboxService.open(el, null, scope.onClose) + + scope.$applyAsync () -> + textarea = el[0].querySelector('textarea') + if textarea + textarea.select() + + else if !visible && el.hasClass('open') + scope.open = false + lightboxService.close(el) + + return { + scope: { + languages: '<', + codeLanguage: '<', + code: '<', + visible: '<', + onClose: '&', + onSave: '&' + }, + link: link, + templateUrl: "components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.html" + } + +angular.module("taigaComponents") + .directive("tgWysiwygCodeLightbox", ["lightboxService", WysiwygCodeLightbox]) diff --git a/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.jade b/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.jade new file mode 100644 index 00000000..7126b3bb --- /dev/null +++ b/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.jade @@ -0,0 +1,25 @@ +tg-lightbox-close(on-close="onClose()") + +form( + ng-if="open" + ng-submit="onSave({lan: codeLanguage, code: code})" +) + h2.title(translate="COMMON.WYSIWYG.CODE_SNIPPET") + + fieldset + select(ng-model="codeLanguage") + option(value="") {{'COMMON.WYSIWYG.SELECT_LANGUAGE_PLACEHOLDER' | translate}} + option(value="remove-formating") {{'COMMON.WYSIWYG.SELECT_LANGUAGE_REMOVE_FORMATING' | translate}} + option( + ng-repeat="option in languages" + ng-value="option.name" + ) {{option.name}} + + fieldset + textarea(ng-model="code") + + fieldset + button.button-green.submit-button( + type="submit" + translate="COMMON.SAVE" + ) diff --git a/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.scss b/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.scss new file mode 100644 index 00000000..5ff6ea2f --- /dev/null +++ b/app/modules/components/wysiwyg/wysiwyg-code-lightbox/wysiwyg-code-lightbox.scss @@ -0,0 +1,3 @@ +tg-wysiwyg-code-lightbox textarea { + height: 350px; +} diff --git a/app/modules/components/wysiwyg/wysiwyg.directive.coffee b/app/modules/components/wysiwyg/wysiwyg.directive.coffee index 927a767f..43008899 100644 --- a/app/modules/components/wysiwyg/wysiwyg.directive.coffee +++ b/app/modules/components/wysiwyg/wysiwyg.directive.coffee @@ -26,36 +26,78 @@ taiga = @.taiga bindOnce = @.taiga.bindOnce Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoader, wysiwygCodeHightlighterService, wysiwygMentionService, analytics) -> + removeSelections = () -> + if window.getSelection + if window.getSelection().empty + window.getSelection().empty(); + else if window.getSelection().removeAllRanges + window.getSelection().removeAllRanges() - isCodeBlockSelected = (range, elm) -> - return !!$(range.endContainer).parentsUntil('.editor', 'code').length + else if document.selection + document.selection.empty() - refreshCodeBlockHightlight = (elm) -> - wysiwygCodeHightlighterService.refreshCodeLanguageSelectors(elm) + getRangeCodeBlock = (range) -> + return $(range.endContainer).parentsUntil('.editor', 'code') + + isCodeBlockSelected = (range) -> + return !!getRangeCodeBlock(range).length + + removeCodeBlockAndHightlight = (selection, mediumInstance) -> + if $(selection).is('code') + code = selection + else + code = $(selection).closest('code')[0] - 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) + mediumInstance.checkContentChanged(mediumInstance.elements[0]) addCodeBlockAndHightlight = (range, elm) -> pre = document.createElement('pre') code = document.createElement('code') - pre.appendChild(code) + console.log range.startContainer.parentNode.nextSibling + + if !range.startContainer.parentNode.nextSibling + $('
    ').insertAfter(range.startContainer.parentNode) + + start = range.startContainer.parentNode.nextSibling + code.appendChild(range.extractContents()) - range.insertNode(pre) - elm.checkContentChanged() + pre.appendChild(code) - wysiwygCodeHightlighterService.addCodeLanguageSelectors(elm) + start.parentNode.insertBefore(pre, start) + refreshCodeBlocks(elm) + + refreshCodeBlocks = (mediumInstance) -> + # clean empty

    content editable adds it when range.extractContents has been execute it + for mainChildren in mediumInstance.elements[0].children + if mainChildren && mainChildren.tagName.toLowerCase() == 'p' && !mainChildren.innerText.length + mainChildren.parentNode.removeChild(mainChildren) + + preList = mediumInstance.elements[0].querySelectorAll('pre') + + for pre in preList + # prevent edit a pre + pre.setAttribute('contenteditable', false) + + if pre.nextElementSibling && pre.nextElementSibling.nodeName.toLowerCase() == 'p' && !pre.nextElementSibling.children.length + pre.nextElementSibling.appendChild(document.createElement('br')) + + # add p after every pre + else if !pre.nextElementSibling || pre.nextElementSibling.nodeName.toLowerCase() != 'p' + p = document.createElement('p') + p.appendChild(document.createElement('br')) + + pre.parentNode.insertBefore(p, pre.nextSibling) + + mediumInstance.checkContentChanged(mediumInstance.elements[0]) AlignRightButton = MediumEditor.extensions.button.extend({ name: 'rtl', @@ -107,9 +149,16 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad range = MediumEditor.selection.getSelectionRange(self.document) if isCodeBlockSelected(range, this.base) - removeCodeBlockAndHightlight(range, this.base) + removeCodeBlockAndHightlight(range.endContainer, this.base) else addCodeBlockAndHightlight(range, this.base) + removeSelections() + + toolbar = this.base.getExtensionByName('toolbar') + + if toolbar + toolbar.hideToolbar() + }) CustomPasteHandler = MediumEditor.extensions.paste.extend({ @@ -141,6 +190,7 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad mediumInstance = null editorMedium = $el.find('.medium') editorMarkdown = $el.find('.markdown') + codeBlockSelected = null isEditOnly = !!$attrs.$attr.editonly notPersist = !!$attrs.$attr.notPersist @@ -149,12 +199,47 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad $scope.editMode = isEditOnly || false $scope.mode = $storage.get('editor-mode', 'html') $scope.markdown = '' + $scope.codeEditorVisible = false + $scope.codeLans = [] wysiwygService.loadEmojis() + wysiwygCodeHightlighterService.getLanguages().then (codeLans) -> + $scope.codeLans = codeLans + setHtmlMedium = (markdown) -> html = wysiwygService.getHTML(markdown) editorMedium.html(html) + wysiwygCodeHightlighterService.addHightlighter(mediumInstance.elements[0]) + refreshCodeBlocks(mediumInstance) + + $scope.saveSnippet = (lan, code) -> + $scope.codeEditorVisible = false + codeBlockSelected.innerText = code + codePre = codeBlockSelected.parentNode + + if lan == 'remove-formating' + codeBlockSelected.className = '' + codePre.className = '' + + removeCodeBlockAndHightlight(codeBlockSelected, mediumInstance) + else if _.trim(code).length + if lan + codeBlockSelected.className = 'language-' + lan + codePre.className = 'language-' + lan + else + codeBlockSelected.className = '' + codePre.className = '' + + wysiwygCodeHightlighterService.hightlightCode(codeBlockSelected) + mediumInstance.checkContentChanged(mediumInstance.elements[0]) + else + codeBlockSelected.parentNode.parentNode.removeChild(codeBlockSelected.parentNode) + mediumInstance.checkContentChanged(mediumInstance.elements[0]) + + throttleChange() + + return null $scope.setMode = (mode) -> $storage.set('editor-mode', mode) @@ -191,7 +276,7 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad if notPersist clean() else if $scope.mode == 'html' - setHtmlMedium($scope.content) + setHtmlMedium($scope.content || null) $scope.markdown = $scope.content @@ -207,19 +292,6 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad $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 @@ -305,7 +377,6 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad change = () -> if $scope.mode == 'html' updateMarkdownWithCurrentHtml() - wysiwygCodeHightlighterService.updateCodeLanguageSelector(mediumInstance) localSave($scope.markdown) @@ -415,24 +486,6 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad mediumInstance.subscribe 'editableDrop', (event) -> $scope.onUploadFile({files: event.dataTransfer.files, cb: uploadEnd}) - editorMedium.on 'keydown', (e) -> - code = if e.keyCode then e.keyCode else e.which - range = MediumEditor.selection.getSelectionRange(document) - codeBlock = isCodeBlockSelected(range, document) - selection = window.getSelection() - - if code == 13 && !e.shiftKey && selection.focusOffset == _.trimEnd(selection.focusNode.textContent).length - e.preventDefault() - document.execCommand('insertHTML', false, '


    ') - - lastP = $('#last-p').attr('id', '') - - range = document.createRange() - range.selectNodeContents(lastP[0]) - range.collapse(true); - - MediumEditor.selection.selectRange(document, range) - mediumInstance.subscribe 'editableKeydown', (e) -> code = if e.keyCode then e.keyCode else e.which @@ -452,12 +505,18 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad $scope.editMode = editMode - $scope.$applyAsync(refreshExtras) + $scope.$applyAsync () -> + wysiwygCodeHightlighterService.addHightlighter(mediumInstance.elements[0]) + refreshCodeBlocks(mediumInstance) - $scope.$watch () -> - return $scope.mode + ":" + $scope.editMode - , () -> - $scope.$applyAsync(refreshExtras) + $(editorMedium[0]).on 'dblclick', 'pre', (e) -> + $scope.$applyAsync () -> + $scope.codeEditorVisible = true + + codeBlockSelected = e.currentTarget.querySelector('code') + + $scope.currentCodeLanguage = wysiwygCodeHightlighterService.getLanguageInClassList(codeBlockSelected.classList) + $scope.code = codeBlockSelected.innerText unwatch = $scope.$watch 'content', (content) -> if !_.isUndefined(content) @@ -466,7 +525,7 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad if !mediumInstance && isDraft() $scope.editMode = true - if $scope.markdown == content + if ($scope.markdown.length || content.length) && $scope.markdown == content return content = getCurrentContent() @@ -487,7 +546,6 @@ Medium = ($translate, $confirm, $storage, wysiwygService, animationFrame, tgLoad $scope.$on "$destroy", () -> if mediumInstance - wysiwygCodeHightlighterService.removeCodeLanguageSelectors(mediumInstance) mediumInstance.destroy() return { diff --git a/app/modules/components/wysiwyg/wysiwyg.scss b/app/modules/components/wysiwyg/wysiwyg.scss index 4bfce409..ef9d90ea 100644 --- a/app/modules/components/wysiwyg/wysiwyg.scss +++ b/app/modules/components/wysiwyg/wysiwyg.scss @@ -90,12 +90,12 @@ } pre:not([class*="language-"]) { @include font-size(small); - background: lighten($grayer, 10%); + background: $code-bg; color: $whitish; direction: ltr; font-family: 'courier new', 'monospace'; line-height: 1.4rem; - margin-bottom: 1rem; + margin-bottom: .5rem; overflow: auto; padding: 1rem; unicode-bidi: embed; @@ -165,6 +165,12 @@ tg-wysiwyg { display: flex; margin-bottom: 2rem; + div[contenteditable="true"] *:last-child { + margin-bottom: 0; + } + pre { + cursor: pointer; + } .outdated { color: $red; } @@ -191,6 +197,7 @@ tg-wysiwyg { .medium-editor-placeholder, .markdown-editor-placeholder { color: $gray-light; + overflow: visible; padding-left: 1rem; &::after { // overwrite medium css color: $gray-light; diff --git a/app/partials/common/components/wysiwyg-toolbar.jade b/app/partials/common/components/wysiwyg-toolbar.jade index 67ca9673..502839d5 100644 --- a/app/partials/common/components/wysiwyg-toolbar.jade +++ b/app/partials/common/components/wysiwyg-toolbar.jade @@ -56,3 +56,12 @@ title="{{ 'COMMON.CANCEL' | translate }}" ) tg-svg(svg-icon="icon-close") + +tg-wysiwyg-code-lightbox.lightbox.lightbox-generic-form( + languages="codeLans" + code-language="currentCodeLanguage" + code="code" + visible="codeEditorVisible" + on-close="codeEditorVisible = false" + on-save="saveSnippet(lan, code)" +) \ No newline at end of file diff --git a/app/styles/layout/ticket-detail.scss b/app/styles/layout/ticket-detail.scss index 30af3d0a..8e0efc4f 100644 --- a/app/styles/layout/ticket-detail.scss +++ b/app/styles/layout/ticket-detail.scss @@ -59,7 +59,7 @@ .no-description { color: $gray-light; } - textarea { + .markdown { background: $white; height: 10rem; } diff --git a/app/themes/high-contrast/variables.scss b/app/themes/high-contrast/variables.scss index 1ecd40e2..e3a7a7e3 100755 --- a/app/themes/high-contrast/variables.scss +++ b/app/themes/high-contrast/variables.scss @@ -39,6 +39,7 @@ $tribe-secondary: darken($tribe-primary, 10%); $top-icon-color: $white; $dropdown-color: rgba(darken($primary-dark, 20%), 1); +$code-bg: #272822; /* Overwrite mixins */ diff --git a/app/themes/material-design/variables.scss b/app/themes/material-design/variables.scss index f975e2c3..b59deb2b 100755 --- a/app/themes/material-design/variables.scss +++ b/app/themes/material-design/variables.scss @@ -39,6 +39,7 @@ $tribe-secondary: darken($tribe-primary, 10%); $top-icon-color: $white; $dropdown-color: rgba(darken($primary-dark, 20%), 1); +$code-bg: #272822; /* Overwrite mixins */ diff --git a/app/themes/taiga/variables.scss b/app/themes/taiga/variables.scss index ebc23064..9f692c77 100755 --- a/app/themes/taiga/variables.scss +++ b/app/themes/taiga/variables.scss @@ -39,3 +39,4 @@ $tribe-secondary: darken($tribe-primary, 10%); $top-icon-color: #11241f; $dropdown-color: rgba(darken($grayer, 20%), 1); +$code-bg: #272822; \ No newline at end of file diff --git a/e2e/helpers/detail-helper.js b/e2e/helpers/detail-helper.js index 74834f39..1c180724 100644 --- a/e2e/helpers/detail-helper.js +++ b/e2e/helpers/detail-helper.js @@ -12,7 +12,12 @@ helper.title = function() { }, setTitle: function(title) { - el.$('.e2e-detail-edit').click(); + browser + .actions() + .mouseMove(el.$('.e2e-detail-edit')) + .click() + .perform(); + el.$('.e2e-title-input').clear().sendKeys(title); }, diff --git a/e2e/shared/wysiwyg.js b/e2e/shared/wysiwyg.js index 4dc3097a..1d1b8e2d 100644 --- a/e2e/shared/wysiwyg.js +++ b/e2e/shared/wysiwyg.js @@ -14,16 +14,17 @@ var shared = module.exports; function selectEditorFirstChild(elm) { browser.executeScript(function () { - // select the first paragraph var range = document.createRange(); - range.selectNode(arguments[0].firstChild); + + range.setStart(arguments[0].firstChild.firstChild, 0); + range.setEnd(arguments[0].firstChild.firstChild, arguments[0].firstChild.innerText.length); var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); }, elm.getWebElement()); - browser.actions().mouseUp().perform(); // trigger medium events + browser.actions().mouseUp().perform(); //trigger medium events } function resetSelection() { @@ -32,7 +33,7 @@ function resetSelection() { sel.removeAllRanges(); }); - browser.actions().mouseUp().perform(); // trigger medium events + browser.actions().mouseUp().perform(); //trigger medium events } function getMarkdownText(elm) { @@ -45,34 +46,76 @@ function getMarkdownTextarea(elm) { return elm.$('.e2e-markdown-textarea');} -function htmlMode() { - $('.e2e-html-mode').click(); +function htmlMode(elm) { + elm.$('.e2e-html-mode').click(); + + return utils.common.waitElementPresent($('.e2e-markdown-mode')); } -function markdownMode() { - $('.e2e-markdown-mode').click(); +function markdownMode(elm) { + elm.$('.e2e-markdown-mode').click(); + + return utils.common.waitElementPresent($('.e2e-html-mode')); } -function saveEdition() { - $('.e2e-save-editor').click(); +function saveEdition(elm) { + return elm.$('.e2e-save-editor').click(); } function cancelEdition(elm) { - $('.e2e-cancel-editor').click(); + elm.$('.e2e-cancel-editor').click(); return browser.wait(async () => { return !!await elm.$$('.read-mode').count(); }, 3000); } +function closeMention() { + return utils.common.waitElementNotPresent($('.medium-mention')); +} + +function preventThrottle() { + return browser.sleep(250); +} + +function getSnippeLightbox(parent) { + let el = parent.$('tg-wysiwyg-code-lightbox'); + + let obj = { + el: el, + waitOpen: function() { + return utils.lightbox.open(el); + }, + waitClose: function() { + return utils.lightbox.close(el); + }, + select: function(lan) { + return el.$('select').sendKeys('javascript'); + }, + save: function() { + return el.$('button').click(); + } + }; + + return obj; +}; + async function edit(elm, elmWrapper, text = null) { await browser.wait(EC.elementToBeClickable(elm), 10000); elm.click(); - browser.sleep(200); + await browser.sleep(2000); - browser.executeScript(function () { + if (text !== null) { + await cleanWysiwyg(elm, elmWrapper); + + return elm.sendKeys(text); + } +}; + +async function cleanWysiwyg(elm, elmWrapper) { + await browser.executeScript(function () { if(arguments[0].firstChild) { var range = document.createRange(); range.setStart(arguments[0].firstChild, 0); @@ -84,26 +127,7 @@ async function edit(elm, elmWrapper, text = null) { } }, 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(); + return elm.sendKeys(protractor.Key.BACK_SPACE); } shared.wysiwygTestingComments = function(parentSelector, section) { @@ -126,15 +150,15 @@ shared.wysiwygTestingComments = function(parentSelector, section) { resetSelection(); - markdownMode(); + markdownMode(editorWrapper); let markdown = await getMarkdownText(editorWrapper); expect(markdown).to.be.equal('**test**'); - htmlMode(); + await htmlMode(editorWrapper); - saveEdition(); + await saveEdition(editorWrapper); let newCommentsCounter = await historyHelper.countComments(); expect(newCommentsCounter).to.be.equal(commentsCounter+1); @@ -145,50 +169,29 @@ shared.wysiwygTestingComments = function(parentSelector, section) { await edit(editor, editorWrapper, ''); - markdownMode(); + markdownMode(editorWrapper); let markdownTextarea = getMarkdownTextarea(editorWrapper); await markdownTextarea.sendKeys('_test2_'); - htmlMode(); + await htmlMode(editorWrapper); let html = await editor.getAttribute("innerHTML"); expect(html).to.be.eql('

    test2

    \n'); - saveEdition(); + await saveEdition(editorWrapper); 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'); + await preventThrottle(); editor.sendKeys(protractor.Key.ESCAPE); await utils.lightbox.confirm.ok(); @@ -206,6 +209,7 @@ shared.wysiwygTestingComments = function(parentSelector, section) { await edit(editor, editorWrapper, ''); editor.sendKeys('text text text'); + await preventThrottle(); editor.sendKeys(protractor.Key.ESCAPE); browser.sleep(400); @@ -225,21 +229,21 @@ shared.wysiwygTestingComments = function(parentSelector, section) { it('mention user', async () => { await edit(editor, editorWrapper, ''); - editor.sendKeys('@use'); + editor.sendKeys('@user8'); - $$('.medium-mention li').get(2).click(); + $$('.medium-mention li').get(0).click(); let html = await editor.getAttribute("innerHTML"); expect(html).to.be.eql('

    @user8 

    '); - markdownMode(); + markdownMode(editorWrapper); let markdown = await getMarkdownText(editorWrapper); expect(markdown).to.be.equal('[@user8](/profile/user8)'); - htmlMode(); + await htmlMode(editorWrapper); await cancelEdition(editorWrapper); }); @@ -255,13 +259,13 @@ shared.wysiwygTestingComments = function(parentSelector, section) { expect(html).to.include('1f604.png'); - markdownMode(); + markdownMode(editorWrapper); let markdown = await getMarkdownText(editorWrapper); expect(markdown).to.be.equal(':smile:'); - htmlMode(); + await htmlMode(editorWrapper); await cancelEdition(editorWrapper); }); @@ -287,7 +291,7 @@ shared.wysiwygTestingComments = function(parentSelector, section) { await edit(editLast, editWrapperLast, "This is the new and updated text"); await utils.common.takeScreenshot(section, "edit comment"); - saveEdition(); + await saveEdition(editWrapperLast); //Show versions from last comment edited historyHelper.showVersionsLastComment(); @@ -318,6 +322,32 @@ shared.wysiwygTestingComments = function(parentSelector, section) { await utils.common.takeScreenshot(section, 'restored comment'); }); + + it('code block', async () => { + await edit(editor, editorWrapper, ''); + + editor.sendKeys("var test = 2;"); + + selectEditorFirstChild(editor); + + $('.medium-editor-toolbar-active .medium-editor-button-last').click(); + + browser.actions().doubleClick(editor.$('code')).perform(); + + let lb = getSnippeLightbox(editorWrapper); + + await lb.waitOpen(); + + await lb.select('javascript'); + await lb.save(); + await lb.waitClose(); + + let hasHightlighter = !!await editor.$$('.token').count(); + + expect(hasHightlighter).to.be.true; + + await saveEdition(editorWrapper); + }); }; shared.wysiwygTesting = function(parentSelector) { @@ -331,9 +361,14 @@ shared.wysiwygTesting = function(parentSelector) { editor.click(); } + let isHtmlMode = await editor.isDisplayed(); + if (!isHtmlMode) { + await htmlMode(editorWrapper); + } + await cleanWysiwyg(editor, editorWrapper); - markdownMode(); + markdownMode(editorWrapper); var markdownTextarea = getMarkdownTextarea(editorWrapper); @@ -341,9 +376,9 @@ shared.wysiwygTesting = function(parentSelector) { await markdownTextarea.sendKeys('test'); - htmlMode(); + await htmlMode(editorWrapper); - saveEdition(); + await saveEdition(editorWrapper); await browser.wait(EC.elementToBeClickable(editor), 10000); }); @@ -364,13 +399,13 @@ shared.wysiwygTesting = function(parentSelector) { let html = await editor.getAttribute("innerHTML"); - expect(html).to.be.eql('

    test

    '); + expect(html).to.be.eql('

    test

    \n'); - saveEdition(); + await saveEdition(editorWrapper); await edit(editor, editorWrapper); - markdownMode(); + markdownMode(editorWrapper); let markdown = await getMarkdownText(editorWrapper); @@ -380,43 +415,24 @@ shared.wysiwygTesting = function(parentSelector) { it('convert to html', async () => { await edit(editor, editorWrapper, ''); - markdownMode(); + markdownMode(editorWrapper); let markdownTextarea = getMarkdownTextarea(editorWrapper); await markdownTextarea.sendKeys('_test2_'); - htmlMode(); + htmlMode(editorWrapper); - let html = await editor.getAttribute("innerHTML"); + let html = await editor.getAttribute("innerHTML"); expect(html).to.be.eql('

    test2

    \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'); + await preventThrottle(); editor.sendKeys(protractor.Key.ESCAPE); await utils.lightbox.confirm.ok(); @@ -434,6 +450,7 @@ shared.wysiwygTesting = function(parentSelector) { await edit(editor, editorWrapper, ''); editor.sendKeys('text text text'); + await preventThrottle(); editor.sendKeys(protractor.Key.ESCAPE); browser.sleep(400); @@ -451,21 +468,24 @@ shared.wysiwygTesting = function(parentSelector) { it('mention user', async () => { await edit(editor, editorWrapper, ''); - editor.sendKeys('@use'); + await editor.sendKeys('@user5'); - $$('.medium-mention li').get(2).click(); + $$('.medium-mention li').get(0).click(); + + await closeMention(); let html = await editor.getAttribute("innerHTML"); - expect(html).to.be.eql('

    @user8 

    '); - markdownMode(); + expect(html).to.be.eql('

    @user5 

    \n'); + + markdownMode(editorWrapper); let markdown = await getMarkdownText(editorWrapper); - expect(markdown).to.be.equal('[@user8](/profile/user8)'); + expect(markdown).to.be.equal('[@user5](/profile/user5)'); - htmlMode(); + htmlMode(editorWrapper); }); it('emojis', async () => { @@ -473,13 +493,15 @@ shared.wysiwygTesting = function(parentSelector) { editor.sendKeys(':smil'); - $$('.medium-mention li').get(2).click(); + await $$('.medium-mention li').get(2).click(); + + await closeMention(); let html = await editor.getAttribute("innerHTML"); expect(html).to.include('1f604.png'); - markdownMode(); + markdownMode(editorWrapper); let markdown = await getMarkdownText(editorWrapper); @@ -487,14 +509,40 @@ shared.wysiwygTesting = function(parentSelector) { }); it('cancel', async () => { - let prevHtml = await editor.getAttribute("innerHTML"); + let prevHtml = await editor.getAttribute("innerHTML"); await edit(editor, editorWrapper, 'xxx yyy zzz'); await cancelEdition(editorWrapper); - let html = await editor.getAttribute("innerHTML"); + let html = await editor.getAttribute("innerHTML"); expect(html).to.be.equal(prevHtml); }); + + it('code block', async () => { + await edit(editor, editorWrapper, ''); + + editor.sendKeys("var test = 2;"); + + selectEditorFirstChild(editor); + + $('.medium-editor-toolbar-active .medium-editor-button-last').click(); + + browser.actions().doubleClick(editor.$('code')).perform(); + + let lb = getSnippeLightbox(editorWrapper); + + await lb.waitOpen(); + + await lb.select('javascript'); + await lb.save(); + await lb.waitClose(); + + await saveEdition(editorWrapper); + + let hasHightlighter = !!await editor.$$('.token').count(); + + expect(hasHightlighter).to.be.true; + }); }; diff --git a/e2e/suites/backlog.e2e.js b/e2e/suites/backlog.e2e.js index 958ea18c..515911e0 100644 --- a/e2e/suites/backlog.e2e.js +++ b/e2e/suites/backlog.e2e.js @@ -476,7 +476,7 @@ describe('backlog', function() { expect(sprintCount).is.below(newSprintCount); }); - it.only('hide forecasting if no velocity', async function() { + it('hide forecasting if no velocity', async function() { browser.get(browser.params.glob.host + 'project/project-5/backlog'); await utils.common.waitLoader(); diff --git a/e2e/suites/wiki.e2e.js b/e2e/suites/wiki.e2e.js index 3206047c..255d8356 100644 --- a/e2e/suites/wiki.e2e.js +++ b/e2e/suites/wiki.e2e.js @@ -65,29 +65,7 @@ describe('wiki', function() { await utils.common.takeScreenshot("wiki", "deleting-the-created-link"); }); - describe('wiki editor', sharedWysiwyg.bind(this)); - - it('confirm close with ESC in lightbox', async function() { - wikiHelper.editor().enabledEditionMode(); - - browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); - - await utils.lightbox.confirm.cancel(); - - let descriptionVisibility = await $('.view-wiki-content').isDisplayed(); - - expect(descriptionVisibility).to.be.false; - - wikiHelper.editor().focus(); - - browser.actions().sendKeys(protractor.Key.ESCAPE).perform(); - - await utils.lightbox.confirm.ok(); - - descriptionVisibility = await $('.view-wiki-content').isDisplayed(); - - expect(descriptionVisibility).to.be.true; - }); + describe('wiki editor', sharedWysiwyg.bind(this, '.wiki')); it('attachments', sharedDetail.attachmentTesting); @@ -96,28 +74,4 @@ describe('wiki', function() { expect(browser.getCurrentUrl()).to.be.eventually.equal(browser.params.glob.host + 'project/project-0/wiki/home'); }); - - it('Custom keyboard actions', async function(){ - wikiHelper.editor().enabledEditionMode(); - - wikiHelper.editor().setText("- aa"); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - let text = await wikiHelper.editor().getText(); - expect(text).to.be.equal("- aa\n- "); - - wikiHelper.editor().setText("- "); - browser.actions().sendKeys(protractor.Key.ENTER).perform(); - text = await wikiHelper.editor().getText(); - expect(text).to.be.equal("\n"); - - wikiHelper.editor().setText("- bbcc"); - browser.actions().sendKeys(protractor.Key.ARROW_LEFT).sendKeys(protractor.Key.ARROW_LEFT).sendKeys(protractor.Key.ENTER).perform(); - text = await wikiHelper.editor().getText(); - expect(text).to.be.equal("- bb\n- cc"); - - wikiHelper.editor().setText("- aa"); - browser.actions().sendKeys(protractor.Key.HOME).sendKeys(protractor.Key.ENTER).perform(); - text = await wikiHelper.editor().getText(); - expect(text).to.be.equal("\n- aa"); - }); }); diff --git a/e2e/utils/common.js b/e2e/utils/common.js index d78feafe..6d59c6ec 100644 --- a/e2e/utils/common.js +++ b/e2e/utils/common.js @@ -20,6 +20,20 @@ common.getElm = function(el) { return deferred.promise; }; +common.waitElementNotPresent = function(el) { + return browser.wait(function() { + return el.isPresent().then(function(present) { + return !present; + }); + }); +}; + +common.waitElementPresent = function(el) { + return browser.wait(function() { + return el.isPresent(); + }); +}; + common.hasClass = async function (element, cls) { let classes = await element.getAttribute('class');