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/code/code.vue b/app/src/interfaces/code/code.vue index 39c816d6a3..738d13f54d 100644 --- a/app/src/interfaces/code/code.vue +++ b/app/src/interfaces/code/code.vue @@ -300,6 +300,7 @@ export default defineComponent({ cursor: pointer; transition: color var(--fast) var(--transition-out); user-select: none; + &:hover { color: var(--primary-125); transition: none; diff --git a/app/src/interfaces/markdown/composables/use-edit.ts b/app/src/interfaces/markdown/composables/use-edit.ts new file mode 100644 index 0000000000..b67b263c46 --- /dev/null +++ b/app/src/interfaces/markdown/composables/use-edit.ts @@ -0,0 +1,234 @@ +import { Ref } from '@vue/composition-api'; +import { Position } from 'codemirror'; +import { cloneDeep } from 'lodash'; + +type Alteration = + | 'bold' + | 'italic' + | 'strikethrough' + | 'listBulleted' + | 'listNumbered' + | 'heading' + | 'blockquote' + | 'code' + | 'link'; + +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 = { + 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 }; + }, + 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 }; + }, + 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 { + 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 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/index.ts b/app/src/interfaces/markdown/index.ts index 6ebe816cbc..6b8a69efa3 100644 --- a/app/src/interfaces/markdown/index.ts +++ b/app/src/interfaces/markdown/index.ts @@ -21,20 +21,5 @@ export default defineInterface(({ i18n }) => ({ }, }, }, - { - field: 'tabbed', - name: i18n.t('interfaces.markdown.tabbed'), - type: 'boolean', - meta: { - width: 'half', - interface: 'toggle', - options: { - label: i18n.t('interfaces.markdown.tabbed_label'), - }, - }, - schema: { - default_value: false, - }, - }, ], })); diff --git a/app/src/interfaces/markdown/markdown.story.ts b/app/src/interfaces/markdown/markdown.story.ts deleted file mode 100644 index 889813b0cf..0000000000 --- a/app/src/interfaces/markdown/markdown.story.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { withKnobs, boolean, text, optionsKnob } from '@storybook/addon-knobs'; -import { action } from '@storybook/addon-actions'; -import Vue from 'vue'; -import InterfaceMarkdown from './markdown.vue'; -import markdown from './readme.md'; -import withPadding from '../../../.storybook/decorators/with-padding'; -import { defineComponent, ref } from '@vue/composition-api'; -import RawValue from '../../../.storybook/raw-value.vue'; -import i18n from '@/lang'; - -Vue.component('interface-markdown', InterfaceMarkdown); - -export default { - title: 'Interfaces / Markdown', - decorators: [withKnobs, withPadding], - parameters: { - notes: markdown, - }, -}; - -export const basic = () => - defineComponent({ - components: { RawValue }, - i18n, - props: { - disabled: { - default: boolean('Disabled', false, 'Options'), - }, - placeholder: { - default: text('Placeholder', 'Enter a value...', 'Options'), - }, - tabbed: { - default: boolean('Tabbed', false, 'Options'), - }, - }, - setup() { - const value = ref(''); - const onInput = action('input'); - return { onInput, value }; - }, - template: ` -
- - {{ value }} -
- `, - }); diff --git a/app/src/interfaces/markdown/markdown.vue b/app/src/interfaces/markdown/markdown.vue index 464fea1d63..76f20a2a17 100644 --- a/app/src/interfaces/markdown/markdown.vue +++ b/app/src/interfaces/markdown/markdown.vue @@ -1,33 +1,39 @@