From ea3d853addc6cb5ba89670b5f192a94ca090d01c Mon Sep 17 00:00:00 2001 From: onukura Date: Thu, 27 Aug 2020 22:48:04 +0900 Subject: [PATCH 1/2] add markdown toolbar --- .../gitbucket/core/helper/preview.scala.html | 68 ++++ .../webapp/assets/common/css/gitbucket.css | 16 + src/main/webapp/assets/common/js/gitbucket.js | 341 ++++++++++++++++++ 3 files changed, 425 insertions(+) diff --git a/src/main/twirl/gitbucket/core/helper/preview.scala.html b/src/main/twirl/gitbucket/core/helper/preview.scala.html index aa18dea56c..6db2f5aed2 100644 --- a/src/main/twirl/gitbucket/core/helper/preview.scala.html +++ b/src/main/twirl/gitbucket/core/helper/preview.scala.html @@ -19,6 +19,15 @@
@@ -48,12 +57,14 @@ $('#content@uid').elastic(); $('#content@uid').trigger('blur'); } + @if(focus){ $('#content@uid').trigger('focus'); } $('#write@uid').on('shown.bs.tab', function(){ $('#content@uid').trigger('focus'); + $('.mde-toolbar').show(); }); $('#preview@uid').click(function(){ @@ -69,6 +80,63 @@ $('#preview-area@uid input').prop('disabled', true); prettyPrint(); }); + $('.mde-toolbar').hide(); + }); + + $('#mde-heading@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeDecorateWord( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '###', 1); + mdePostProcess(content, newTextInfo); + }); + $('#mde-bold@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeWrapWord( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '**'); + mdePostProcess(content, newTextInfo); + }); + $('#mde-italic@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeWrapWord( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '_'); + mdePostProcess(content, newTextInfo); + }); + $('#mde-code@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeWrapWord( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '`'); + mdePostProcess(content, newTextInfo); }); + $('#mde-link@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeWrapWordLink( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd')); + mdePostProcess(content, newTextInfo); + }); + $('#mde-quote@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeDecorateWordWithNewLine( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '>', '>'); + mdePostProcess(content, newTextInfo); + }); + $('#mde-list-ul@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeDecorateWordWithNewLine( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '-', '-'); + mdePostProcess(content, newTextInfo); + }); + $('#mde-list-ol@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeDecorateWordWithNewLine( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '1.', "1\\."); + mdePostProcess(content, newTextInfo); + }); + $('#mde-tasklist@uid').click(function(){ + var content = $('#content@uid'); + var newTextInfo = mdeDecorateWordWithNewLine( + content.val(), content.prop('selectionStart'), content.prop('selectionEnd'), '- [ ]', "-\\s\\[\\s\\]"); + mdePostProcess(content, newTextInfo); + }); + }); diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index 79986a9635..29283c3737 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -1665,6 +1665,22 @@ a.markdown-anchor-link span.octicon { vertical-align: middle; } +/****************************************************************************/ +/* Markdown Toolbar */ +/****************************************************************************/ +li.mde-toolbar { + float: right; +} + +li.mde-toolbar a { + border: 0; + margin-right: 0; +} + +li.mde-toolbar a i { + color: gray; +} + /****************************************************************************/ /* File finder */ /****************************************************************************/ diff --git a/src/main/webapp/assets/common/js/gitbucket.js b/src/main/webapp/assets/common/js/gitbucket.js index 22779c5289..e7cb209f7a 100644 --- a/src/main/webapp/assets/common/js/gitbucket.js +++ b/src/main/webapp/assets/common/js/gitbucket.js @@ -765,3 +765,344 @@ var applyTaskListCheckedStatus = function(commentArea, checkboxes) { ss.pop(); return ss.join(''); }; + +/** + * helper function for markdown toolbar operation + * check if index position is the middle of a word. + * @param {String} txt + * @param {Number} pos position in 'txt' + * @returns {Boolean} + */ +function isInWord(txt, pos){ + if(pos <= 0){ + return false; + }else if(pos === txt.length){ + return false; + }else{ + return (txt[pos - 1].match(/\s/g) === null) && (txt[pos].match(/\s/g) === null); + } +} + +/** + * helper function for markdown toolbar operation + * get index of start position and end position according to pattern. + * if pattern is '\s' means word, if '\n' means line. + * @param {String} txt + * @param {Number} pos position in 'txt' + * @param {String} pattern e.g. ' ', '\n' + * @returns Array[Number, Number] + */ +function findStartEnd(txt, pos, pattern){ + var start; + var end; + var indexOfSpace; + for (var i = pos; i >= 0; i--) { + indexOfSpace = txt.indexOf(pattern, i) + if (indexOfSpace !== -1 && indexOfSpace <= i) { + start = indexOfSpace + 1; + break; + } else { + start = i; + } + } + end = txt.indexOf(pattern, pos); + if(end === -1){ + end = txt.length; + } + return [start, end]; +} + +/** + * helper function for markdown toolbar operation + * check if target range is already wrapped by pattern + * @param {String} txt + * @param {Number} posStart where cursor position start + * @param {Number} posEnd where cursor position end + * @param {String} patternHead + * @param {String} patternTail + * @returns {Boolean} + */ +function isAlreadyWrapped(txt, posStart, posEnd, patternHead, patternTail){ + if(posStart < patternHead.length || (txt.length - posEnd) < patternTail.length ){ + return false; + }else{ + return txt.slice(posStart - patternHead.length, posStart) === patternHead + && txt.slice(posEnd, posEnd + patternTail.length) === patternTail; + } +} + +/** + * helper function for markdown toolbar operation + * post process, set new txt, focus, set cursor. + * @param {Element} element Dom + * @param {{focus: {start: number, end: number}, text: string}} newTextInfo + */ +function mdePostProcess(element, newTextInfo){ + element.val(newTextInfo["text"]); + element.focus(); + element.prop('selectionStart', newTextInfo["focus"]["start"]); + element.prop('selectionEnd', newTextInfo["focus"]["end"]); +} + +/** + * functions for insert markdown pattern into text. + * for heading and mention, etc. e.g. a|bs => {pattern} a|bs ('|' means cursor) + * @param {String} txt + * @param {Number} posStart where cursor position start + * @param {Number} posEnd where cursor position end + * @param {String} pattern e.g. '###' + * @param {Number} posOffset the number of space after pattern. + * @returns {{focus: {start: *, end: *}, text: (string|*)}} + */ +function mdeDecorateWord(txt, posStart, posEnd, pattern, posOffset){ + var newTxt; + var focusPosStart; + var focusPosEnd; + if(posStart !== posEnd){ + newTxt = txt.slice(0, posStart) + pattern + " ".repeat(posOffset) + txt.slice(posStart); + focusPosStart = posStart + pattern.length + posOffset; + focusPosEnd = posEnd + pattern.length + posOffset; + }else{ + if(isInWord(txt, posStart)){ + var wordPos = findStartEnd(txt, posStart, " "); + newTxt = txt.slice(0, wordPos[0]) + pattern + " ".repeat(posOffset) + txt.slice(wordPos[0]); + }else{ + newTxt = txt.slice(0, posStart) + pattern + " ".repeat(posOffset) + txt.slice(posStart); + } + focusPosStart = posStart + pattern.length + posOffset; + focusPosEnd = focusPosStart; + } + return {"text": newTxt, "focus": {"start": focusPosStart, "end": focusPosEnd}}; +} + +/** + * functions for insert markdown pattern into text. + * insert line before 'pattern' and after 'txtMiddle' if necessary. + * txtHead + (line){1or2} + pattern + txtMiddle + (line){0or1} + txtTail + * @param {String} txtHead + * @param {String} txtMiddle + * @param {String} txtTail + * @param {String} pattern pattern to insert before txtMiddle + * @param {Number} numSpaces the number of spaces to add after pattern. + * @return {{numBreaksTail: number, text: string, numBreaksHead: number}} + */ +function insertBreaks(txtHead, txtMiddle, txtTail, pattern, numSpaces){ + var txtAll; + var numBreaksHead; + var numBreaksTail; + if(txtHead === "" || txtHead.endsWith("\n\n")){ + numBreaksHead = 0; + }else if(txtHead.endsWith("\n")){ + numBreaksHead = 1; + }else{ + numBreaksHead = 2; + } + if(txtTail.match(/^\n{1}.+$/g) !== null){ + numBreaksTail = 1; + }else{ + numBreaksTail = 0; + } + txtAll = txtHead + "\n".repeat(numBreaksHead) + pattern + " ".repeat(numSpaces) + txtMiddle + "\n".repeat(numBreaksTail) + txtTail; + return {"text": txtAll, "numBreaksHead": numBreaksHead, "numBreaksTail": numBreaksTail}; +} + +/** + * functions for insert markdown pattern into text. + * for quote, list, task list, etc. + * @param {String} txt + * @param {Number} posStart where cursor position start + * @param {Number} posEnd where cursor position end + * @param {String} pattern e.g. '-', '1.', '- [ ]' + * @param {String} patternRegex regex of pattern e.g. '-', '1\\.', '-\\[\\s\\]' + * @return {{focus: {start: number, end: number}, text: string}} + */ +function mdeDecorateWordWithNewLine(txt, posStart, posEnd, pattern, patternRegex){ + var newTxt; + var patternWithSpace = pattern + " "; + var focusPosStart; + var focusPosEnd; + if(txt.length === 0){ // if text area is empty + newTxt = patternWithSpace + txt; + focusPosStart = posStart + pattern.length + 1; + focusPosEnd = posEnd + pattern.length + 1; + return {"text": newTxt, "focus": {"start": focusPosStart, "end": focusPosEnd}}; + } + // If decorated + if(posStart !== posEnd){ + // Undo multi list + if(txt.slice(posStart, posEnd).match(new RegExp("(\\n?" + patternRegex + "\\s)+")) !== null){ + var txtLines = txt.slice(posStart, posEnd).split("\n"); + for(var i=0;i linePos[1]){ + wordPos[1] = linePos[1]; + } + if(wordPos[0] === 0){ + newTxt = patternWithSpace + txt.slice(wordPos[0]); + focusPosStart = pattern.length + 3; + }else{ + var txtWithLine = insertBreaks( + txt.slice(0, wordPos[0]), txt.slice(wordPos[0], wordPos[1]), txt.slice(wordPos[1]), pattern, 1); + newTxt = txtWithLine["text"]; + focusPosStart = posStart + txtWithLine["numBreaksHead"] + pattern.length + 1; + } + }else{ // If cursor is not in word + if(posStart === 0){ + newTxt = patternWithSpace + txt.slice(posStart); + focusPosStart = pattern.length + 1; + }else{ + var txtWithLine = insertBreaks( + txt.slice(0, posStart), txt.slice(posStart, linePos[1]), txt.slice(linePos[1]), pattern, 1); + newTxt = txtWithLine["text"]; + focusPosStart = posStart + txtWithLine["numBreaksHead"] + pattern.length + 1; + } + } + focusPosEnd = focusPosStart; + return {"text": newTxt, "focus": {"start": focusPosStart, "end": focusPosEnd}}; + } +} + +/** + * functions for insert markdown pattern into text. + * for italic ,bold, code, etc. e.g. a|bs => {pattern}a|bs{pattern} + * @param {String} txt + * @param {Number} posStart where cursor position start + * @param {Number} posEnd where cursor position end + * @param {String} pattern e.g. **, _, ` + * @returns {{focus: {start: number, end: number}, text: (string|*)}} + */ +function mdeWrapWord(txt, posStart, posEnd, pattern){ + var newTxt; + var focusPosStart; + var focusPosEnd; + if(posStart !== posEnd){ + if(isAlreadyWrapped(txt, posStart, posEnd, pattern, pattern)){ + newTxt = txt.slice(0, posStart - pattern.length) + + txt.slice(posStart, posEnd) + + txt.slice(posEnd + pattern.length); + focusPosStart = posStart - pattern.length; + focusPosEnd = posEnd - pattern.length; + }else{ + newTxt = txt.slice(0, posStart) + pattern + txt.slice(posStart, posEnd) + pattern + txt.slice(posEnd); + focusPosStart = posStart + pattern.length; + focusPosEnd = posEnd + pattern.length; + } + }else{ + var linePos = findStartEnd(txt, posStart, "\n"); + if(isInWord(txt, posStart)){ + var wordPos = findStartEnd(txt, posStart, " "); + if(wordPos[0] < linePos[0]){ + wordPos[0] = linePos[0]; + } + if(wordPos[1] > linePos[1]){ + wordPos[1] = linePos[1]; + } + if(isAlreadyWrapped(txt, wordPos[0] + pattern.length, wordPos[1] - pattern.length, pattern, pattern)){ + newTxt = txt.slice(0, wordPos[0]) + + txt.slice(wordPos[0] + pattern.length, wordPos[1] - pattern.length) + + txt.slice(wordPos[1]); + focusPosStart = posStart - pattern.length; + focusPosEnd = focusPosStart; + }else{ + newTxt = txt.slice(0, wordPos[0]) + + pattern + txt.slice(wordPos[0], wordPos[1]) + + pattern + txt.slice(wordPos[1]); + focusPosStart = wordPos[0] + pattern.length + (posStart - wordPos[0]); + focusPosEnd = focusPosStart; + } + }else{ + newTxt = txt.slice(0, posStart) + pattern + pattern + txt.slice(posStart); + focusPosStart = posStart + pattern.length; + focusPosEnd = focusPosStart; + } + } + return {"text": newTxt, "focus": {"start": focusPosStart, "end": focusPosEnd}}; +} + +/** + * functions for insert markdown pattern into text. + * for link. e.g. a|bs => [abs](|url) + * @param {String} txt + * @param {Number} posStart where cursor position start + * @param {Number} posEnd where cursor position end + * @returns {{focus: {start: number, end: number}, text: string}} + */ +function mdeWrapWordLink(txt, posStart, posEnd){ + var newTxt; + var focusPosStart; + var focusPosEnd; + var offset; // cursor offset + if(posStart !== posEnd){ + offset = 1; // for "[" + newTxt = txt.slice(0, posStart) + "[" + txt.slice(posStart, posEnd) + "](url)" + txt.slice(posEnd); + focusPosStart = posStart + offset; + focusPosEnd = posEnd + offset; + }else{ + offset = 3; // for "[" + "](" + var linePos = findStartEnd(txt, posStart, "\n"); + if(isInWord(txt, posStart)){ + var wordPos = findStartEnd(txt, posStart, " "); + if(wordPos[0] < linePos[0]){ + wordPos[0] = linePos[0]; + } + if(wordPos[1] > linePos[1]){ + wordPos[1] = linePos[1]; + } + newTxt = txt.slice(0, wordPos[0]) + "[" + txt.slice(wordPos[0], wordPos[1]) + "](url)" + txt.slice(wordPos[1]); + focusPosStart = wordPos[1] + offset; + focusPosEnd = focusPosStart; + }else{ + newTxt = txt.slice(0, posStart) + "[](url)" + txt.slice(posStart); + focusPosStart = posStart + offset; + focusPosEnd = focusPosStart; + } + } + return {"text": newTxt, "focus": {"start": focusPosStart, "end": focusPosEnd}}; +} From a451fda45fcc2a83d17116bbb89fce29a638db89 Mon Sep 17 00:00:00 2001 From: onukura Date: Sat, 29 Aug 2020 22:11:28 +0900 Subject: [PATCH 2/2] fix JSDoc --- src/main/webapp/assets/common/js/gitbucket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/assets/common/js/gitbucket.js b/src/main/webapp/assets/common/js/gitbucket.js index e7cb209f7a..9530068f10 100644 --- a/src/main/webapp/assets/common/js/gitbucket.js +++ b/src/main/webapp/assets/common/js/gitbucket.js @@ -790,7 +790,7 @@ function isInWord(txt, pos){ * @param {String} txt * @param {Number} pos position in 'txt' * @param {String} pattern e.g. ' ', '\n' - * @returns Array[Number, Number] + * @returns {Array} */ function findStartEnd(txt, pos, pattern){ var start;