From 2aeafb2bce63d08cfdd4d02cb1261309b1659968 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 14 Jan 2021 19:34:16 -0500 Subject: [PATCH] Finish markdown interface v3 --- app/src/components/register.ts | 2 + .../v-button-group/v-button-group.vue | 8 +- app/src/components/v-button/v-button.vue | 11 +- .../markdown/composables/use-edit.ts | 246 +++++++++++++-- app/src/interfaces/markdown/markdown.vue | 290 +++++++++++++++++- .../interfaces/wysiwyg/tinymce-overrides.css | 13 +- package-lock.json | 65 +++- package.json | 6 +- 8 files changed, 578 insertions(+), 63 deletions(-) diff --git a/app/src/components/register.ts b/app/src/components/register.ts index 53c643919b..0540da310e 100644 --- a/app/src/components/register.ts +++ b/app/src/components/register.ts @@ -4,6 +4,7 @@ import VAvatar from './v-avatar/'; import VBadge from './v-badge/'; import VBreadcrumb from './v-breadcrumb'; import VButton from './v-button/'; +import VButtonGroup from './v-button-group/'; import VCard, { VCardActions, VCardTitle, VCardSubtitle, VCardText } from './v-card'; import VCheckbox from './v-checkbox/'; import VChip from './v-chip/'; @@ -43,6 +44,7 @@ Vue.component('v-avatar', VAvatar); Vue.component('v-badge', VBadge); Vue.component('v-breadcrumb', VBreadcrumb); Vue.component('v-button', VButton); +Vue.component('v-button-group', VButtonGroup); Vue.component('v-card-actions', VCardActions); Vue.component('v-card-subtitle', VCardSubtitle); Vue.component('v-card-text', VCardText); diff --git a/app/src/components/v-button-group/v-button-group.vue b/app/src/components/v-button-group/v-button-group.vue index 13d0c60dbe..a479275e4b 100644 --- a/app/src/components/v-button-group/v-button-group.vue +++ b/app/src/components/v-button-group/v-button-group.vue @@ -85,21 +85,21 @@ body { } &.tile .v-item-group ::v-deep .v-button { - &:first-child { + &:first-child .button { --border-radius: 0px; } - &:last-child { + &:last-child .button { --border-radius: 0px; } } &.rounded:not(.tile) .v-item-group ::v-deep .v-button { - &:first-child { + &:first-child .button { --border-radius: var(--v-button-height) 0px 0px var(--v-button-height); } - &:last-child { + &:last-child .button { --border-radius: 0px var(--v-button-height) var(--v-button-height) 0px; } } diff --git a/app/src/components/v-button/v-button.vue b/app/src/components/v-button/v-button.vue index 374d15658c..86c4a0f49f 100644 --- a/app/src/components/v-button/v-button.vue +++ b/app/src/components/v-button/v-button.vue @@ -209,12 +209,6 @@ body { border-color: var(--v-button-background-color-hover); } - &.activated { - color: var(--v-button-color); - background-color: var(--v-button-background-color); - border-color: var(--v-button-background-color); - } - &.align-left { justify-content: flex-start; } @@ -267,9 +261,9 @@ body { --v-button-font-size: 12px; --v-button-font-weight: 600; --v-button-min-width: 60px; + --border-radius: 4px; padding: 0 12px; - border-radius: 4px; } &.small { @@ -336,7 +330,8 @@ body { } } - &.activated { + &.activated, + &.active { --v-button-color: var(--v-button-color-activated) !important; --v-button-background-color: var(--v-button-background-color-activated) !important; --v-button-background-color-hover: var(--v-button-background-color-activated) !important; diff --git a/app/src/interfaces/markdown/composables/use-edit.ts b/app/src/interfaces/markdown/composables/use-edit.ts index a30520e444..095a36ae98 100644 --- a/app/src/interfaces/markdown/composables/use-edit.ts +++ b/app/src/interfaces/markdown/composables/use-edit.ts @@ -1,46 +1,234 @@ -import { Ref } from '@vue/composition-api'; +import { Ref, nextTick } from '@vue/composition-api'; +import { Position } from 'codemirror'; +import { cloneDeep } from 'lodash'; -type Alteration = 'bold' | 'italic' | 'strikethrough'; +type Alteration = + | 'bold' + | 'italic' + | 'strikethrough' + | 'listBulleted' + | 'listNumbered' + | 'heading' + | 'blockquote' + | 'code' + | 'link'; -type AlterationFunctions = Record string[]>; +type AlterationFunctions = Record< + Alteration, + ( + selections: string, + cursors: { cursorHead: Position; cursorFrom: Position; cursorTo: Position } + ) => { newSelection: string; newCursor: Position; highlight?: { from: Position; to: Position } } +>; export function useEdit(codemirror: Ref) { const alterations: AlterationFunctions = { - bold(selections) { - return selections.map((selection) => { - if (selection.startsWith('**') && selection.endsWith('**')) { - return selection.substring(2, selection.length - 2); - } else { - return `**${selection}**`; - } - }); + heading(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + if (selection.startsWith('# ')) { + newSelection = selection.substring(2); + } else { + newSelection = `# ${selection}`; + newCursor.ch = newCursor.ch + 2; + } + + return { newSelection, newCursor }; }, - italic(selections) { - return selections.map((selection) => { - if (selection.startsWith('*') && selection.endsWith('*')) { - return selection.substring(1, selection.length - 1); - } else { - return `*${selection}*`; - } - }); + bold(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + if (selection.startsWith('**') && selection.endsWith('**')) { + newSelection = selection.substring(2, selection.length - 2); + } else { + newSelection = `**${selection}**`; + newCursor.ch = newCursor.ch + 2; + } + + return { newSelection, newCursor }; }, - strikethrough(selections) { - return selections.map((selection) => { - if (selection.startsWith('~~') && selection.endsWith('~~')) { - return selection.substring(2, selection.length - 2); + italic(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + if (selection.startsWith('*') && selection.endsWith('*')) { + newSelection = selection.substring(1, selection.length - 1); + } else { + newSelection = `*${selection}*`; + newCursor.ch = newCursor.ch + 1; + } + + return { newSelection, newCursor }; + }, + strikethrough(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + if (selection.startsWith('~~') && selection.endsWith('~~')) { + newSelection = selection.substring(2, selection.length - 2); + } else { + newSelection = `~~${selection}~~`; + newCursor.ch = newCursor.ch + 2; + } + + return { newSelection, newCursor }; + }, + listBulleted(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + const lines = selection.split('\n'); + + const isList = lines.every((line) => line.startsWith('- ')); + + if (isList) { + newSelection = lines.map((line) => line.substring(2)).join('\n'); + } else { + newSelection = lines.map((line) => `- ${line}`).join('\n'); + } + + if (!selection) { + newCursor.ch = newCursor.ch + 2; + } + + return { newSelection, newCursor }; + }, + listNumbered(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + const lines = selection.split('\n'); + + const isList = lines.every((line, index) => line.startsWith(`${index + 1}.`)); + + if (isList) { + newSelection = lines.map((line) => line.substring(3)).join('\n'); + } else { + newSelection = lines.map((line, index) => `${index + 1}. ${line}`).join('\n'); + } + + if (!selection) { + newCursor.ch = newCursor.ch + 2; + } + + return { newSelection, newCursor }; + }, + blockquote(selection, { cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + + const lines = selection.split('\n'); + + const isList = lines.every((line) => line.startsWith('> ')); + + if (isList) { + newSelection = lines.map((line) => line.substring(2)).join('\n'); + } else { + newSelection = lines.map((line) => `> ${line}`).join('\n'); + } + + if (!selection) { + newCursor.ch = newCursor.ch + 2; + } + + return { newSelection, newCursor }; + }, + code(selection, { cursorTo }) { + if (selection.includes('\n')) { + // Multiline + let newSelection = selection; + let newCursor = cursorTo; + + if (selection.startsWith('```') && selection.endsWith('```')) { + newSelection = selection.substring(3, selection.length - 3); } else { - return `~~${selection}~~`; + newSelection = '```\n' + newSelection + '\n```'; + newCursor.line = newCursor.line + 1; } - }); - } - } + + return { newSelection, newCursor }; + } else { + // Inline + let newSelection = selection; + let newCursor = cursorTo; + + if (selection.startsWith('`') && selection.endsWith('`')) { + newSelection = selection.substring(1, selection.length - 1); + } else { + newSelection = `\`${selection}\``; + newCursor.ch = newCursor.ch + 1; + } + + return { newSelection, newCursor }; + } + }, + link(selection, { cursorFrom, cursorTo }) { + let newSelection = selection; + let newCursor = cursorTo; + let highlight; + + if (selection.endsWith('](url)')) { + newSelection = selection.substring(1, selection.length - 6); + } else if (selection.startsWith('http')) { + newSelection = `[](${selection})`; + newCursor.ch = cursorFrom.ch + 1; + } else { + newSelection = `[${selection}](url)`; + + if (selection) { + highlight = { + from: { + ...cloneDeep(newCursor), + ch: newCursor.ch + 3, + }, + to: { + ...cloneDeep(newCursor), + ch: newCursor.ch + 6, + }, + }; + } else { + newCursor.ch = cursorFrom.ch + 1; + } + } + + return { newSelection, newCursor, highlight }; + }, + }; return { edit }; function edit(type: Alteration) { if (codemirror.value) { - const selections = codemirror.value.getSelections(); - codemirror.value.replaceSelection(alterations[type](selections)); + const cursor = codemirror.value.getCursor('head'); + const cursorFrom = codemirror.value.getCursor('from'); + const cursorTo = codemirror.value.getCursor('to'); + + const wordRange = codemirror.value.findWordAt(cursor); + const word = codemirror.value.getRange(wordRange.anchor, wordRange.head).trim(); + + const selection = codemirror.value.getSelection(); + + const { newSelection, newCursor, highlight } = alterations[type](selection || word, { + cursorFrom: cloneDeep(selection ? cursorFrom : wordRange.anchor), + cursorTo: cloneDeep(selection ? cursorTo : wordRange.head), + cursorHead: cursor, + }); + + if (word && !selection) { + codemirror.value.replaceRange(newSelection, wordRange.anchor, wordRange.head); + } else { + codemirror.value.replaceSelection(newSelection); + } + + codemirror.value.setCursor(newCursor); + + if (highlight) { + codemirror.value.setSelection(highlight.from, highlight.to); + } + + codemirror.value.focus(); } } } diff --git a/app/src/interfaces/markdown/markdown.vue b/app/src/interfaces/markdown/markdown.vue index 42e8ff58d9..76f20a2a17 100644 --- a/app/src/interfaces/markdown/markdown.vue +++ b/app/src/interfaces/markdown/markdown.vue @@ -1,19 +1,34 @@