X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/4e71a5a47b97bee59b8afff98b510c4d0d7ead15..refs/pull/261/head:/resources/assets/js/directives.js diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 10458e753..16d1ad2a4 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,8 +1,10 @@ "use strict"; -import DropZone from "dropzone"; -import markdown from "marked"; +const DropZone = require("dropzone"); +const MarkdownIt = require("markdown-it"); +const mdTasksLists = require('markdown-it-task-lists'); +const code = require('./code'); -export default function (ngApp, events) { +module.exports = function (ngApp, events) { /** * Common tab controls using simple jQuery functions. @@ -214,18 +216,8 @@ export default function (ngApp, events) { } }]); - let renderer = new markdown.Renderer(); - // Custom markdown checkbox list item - // Attribution: https://github.com/chjj/marked/issues/107#issuecomment-44542001 - renderer.listitem = function(text) { - if (/^\s*\[[x ]\]\s*/.test(text)) { - text = text - .replace(/^\s*\[ \]\s*/, '') - .replace(/^\s*\[x\]\s*/, ''); - return `
  • ${text}
  • `; - } - return `
  • ${text}
  • `; - }; + const md = new MarkdownIt({html: true}); + md.use(mdTasksLists, {label: true}); /** * Markdown input @@ -242,22 +234,44 @@ export default function (ngApp, events) { // Set initial model content element = element.find('textarea').first(); - let content = element.val(); - scope.mdModel = content; - scope.mdChange(markdown(content, {renderer: renderer})); - element.on('change input', (event) => { - content = element.val(); + // Codemirror Setup + let cm = code.markdownEditor(element[0]); + cm.on('change', (instance, changeObj) => { + update(instance); + }); + + cm.on('scroll', instance => { + // Thanks to http://liuhao.im/english/2015/11/10/the-sync-scroll-of-markdown-editor-in-javascript.html + let scroll = instance.getScrollInfo(); + let atEnd = scroll.top + scroll.clientHeight === scroll.height; + if (atEnd) { + scope.$emit('markdown-scroll', -1); + return; + } + let lineNum = instance.lineAtHeight(scroll.top, 'local'); + let range = instance.getRange({line: 0, ch: null}, {line: lineNum, ch: null}); + let parser = new DOMParser(); + let doc = parser.parseFromString(md.render(range), 'text/html'); + let totalLines = doc.documentElement.querySelectorAll('body > *'); + scope.$emit('markdown-scroll', totalLines.length); + }); + + function update(instance) { + let content = instance.getValue(); + element.val(content); $timeout(() => { scope.mdModel = content; - scope.mdChange(markdown(content, {renderer: renderer})); + scope.mdChange(md.render(content)); }); - }); + } + update(cm); scope.$on('markdown-update', (event, value) => { + cm.setValue(value); element.val(value); scope.mdModel = value; - scope.mdChange(markdown(value)); + scope.mdChange(md.render(value)); }); } @@ -268,7 +282,7 @@ export default function (ngApp, events) { * Markdown Editor * Handles all functionality of the markdown editor. */ - ngApp.directive('markdownEditor', ['$timeout', function ($timeout) { + ngApp.directive('markdownEditor', ['$timeout', '$rootScope', function ($timeout, $rootScope) { return { restrict: 'A', link: function (scope, element, attrs) { @@ -291,34 +305,15 @@ export default function (ngApp, events) { currentCaretPos = $input[0].selectionStart; }); - // Scroll sync - let inputScrollHeight, - inputHeight, - displayScrollHeight, - displayHeight; - - function setScrollHeights() { - inputScrollHeight = $input[0].scrollHeight; - inputHeight = $input.height(); - displayScrollHeight = $display[0].scrollHeight; - displayHeight = $display.height(); - } - - setTimeout(() => { - setScrollHeights(); - }, 200); - window.addEventListener('resize', setScrollHeights); - let scrollDebounceTime = 800; - let lastScroll = 0; - $input.on('scroll', event => { - let now = Date.now(); - if (now - lastScroll > scrollDebounceTime) { - setScrollHeights() + // Handle scroll sync event from editor scroll + $rootScope.$on('markdown-scroll', (event, lineCount) => { + let elems = $display[0].children[0].children; + if (elems.length > lineCount) { + let topElem = (lineCount === -1) ? elems[elems.length-1] : elems[lineCount]; + $display.animate({ + scrollTop: topElem.offsetTop + }, {queue: false, duration: 200, easing: 'linear'}); } - let scrollPercent = ($input.scrollTop() / (inputScrollHeight - inputHeight)); - let displayScrollY = (displayScrollHeight - displayHeight) * scrollPercent; - $display.scrollTop(displayScrollY); - lastScroll = now; }); // Editor key-presses @@ -827,4 +822,128 @@ export default function (ngApp, events) { } }; }]); + + ngApp.directive('commentReply', [function () { + return { + restrict: 'E', + templateUrl: 'comment-reply.html', + scope: { + pageId: '=', + parentId: '=', + parent: '=' + }, + link: function (scope, element) { + scope.isReply = true; + element.find('textarea').focus(); + scope.$on('evt.comment-success', function (event) { + // no need for the event to do anything more. + event.stopPropagation(); + event.preventDefault(); + scope.closeBox(); + }); + + scope.closeBox = function () { + element.remove(); + scope.$destroy(); + }; + } + }; + }]); + + ngApp.directive('commentEdit', [function () { + return { + restrict: 'E', + templateUrl: 'comment-reply.html', + scope: { + comment: '=' + }, + link: function (scope, element) { + scope.isEdit = true; + element.find('textarea').focus(); + scope.$on('evt.comment-success', function (event, commentId) { + // no need for the event to do anything more. + event.stopPropagation(); + event.preventDefault(); + if (commentId === scope.comment.id && !scope.isNew) { + scope.closeBox(); + } + }); + + scope.closeBox = function () { + element.remove(); + scope.$destroy(); + }; + } + }; + }]); + + + ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) { + return { + scope: { + comment: '=' + }, + link: function (scope, element, attr) { + element.on('$destroy', function () { + element.off('click'); + scope.$destroy(); + }); + + element.on('click', function (e) { + e.preventDefault(); + var $container = element.parents('.comment-actions').first(); + if (!$container.length) { + console.error('commentReplyLink directive should be placed inside a container with class comment-box!'); + return; + } + if (attr.noCommentReplyDupe) { + removeDupe(); + } + + compileHtml($container, scope, attr.isReply === 'true'); + }); + } + }; + + function compileHtml($container, scope, isReply) { + let lnkFunc = null; + if (isReply) { + lnkFunc = $compile(''); + } else { + lnkFunc = $compile(''); + } + var compiledHTML = lnkFunc(scope); + $container.append(compiledHTML); + } + + function removeDupe() { + let $existingElement = $document.find('.comments-list comment-reply, .comments-list comment-edit'); + if (!$existingElement.length) { + return; + } + + $existingElement.remove(); + } + }]); + + ngApp.directive('commentDeleteLink', ['$window', function ($window) { + return { + controller: 'CommentDeleteController', + scope: { + comment: '=' + }, + link: function (scope, element, attr, ctrl) { + + element.on('click', function(e) { + e.preventDefault(); + var resp = $window.confirm(trans('entities.comment_delete_confirm')); + if (!resp) { + return; + } + + ctrl.delete(scope.comment); + }); + } + }; + }]); };