summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_shared/components/markdown
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/markdown')
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue47
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue216
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue6
7 files changed, 272 insertions, 12 deletions
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index 926034efd10..caec49c557a 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -51,6 +51,7 @@ export default {
<gl-dropdown
:text="dropdownText"
:disabled="disabled"
+ size="small"
boundary="window"
right
lazy
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 32b3a0e22c2..657e4498b53 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -3,7 +3,7 @@ import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { debounce, unescape } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
@@ -272,7 +272,7 @@ export default {
this.fetchMarkdown()
.then((data) => this.renderMarkdown(data))
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error loading markdown preview'),
}),
);
@@ -315,7 +315,7 @@ export default {
this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() =>
- createFlash({
+ createAlert({
message: __('Error rendering Markdown preview'),
}),
);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 458dfe0ed23..89fffdedbfd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -7,6 +7,8 @@ import {
ITALIC_TEXT,
STRIKETHROUGH_TEXT,
LINK_TEXT,
+ INDENT_LINE,
+ OUTDENT_LINE,
} from '~/behaviors/shortcuts/keybindings';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
@@ -68,12 +70,15 @@ export default {
},
computed: {
mdTable() {
+ const header = s__('MarkdownEditor|header');
+ const divider = '-'.repeat(header.length);
+ const cell = ' '.repeat(header.length);
+
return [
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- '| header | header |', // eslint-disable-line @gitlab/require-i18n-strings
- '| ------ | ------ |',
- '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings
- '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings
+ `| ${header} | ${header} |`,
+ `| ${divider} | ${divider} |`,
+ `| ${cell} | ${cell} |`,
+ `| ${cell} | ${cell} |`,
].join('\n');
},
mdSuggestion() {
@@ -82,7 +87,8 @@ export default {
);
},
mdCollapsibleSection() {
- return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n');
+ const expandText = s__('MarkdownEditor|Click to expand');
+ return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n');
},
isMac() {
// Accessing properties using ?. to allow tests to use
@@ -170,6 +176,8 @@ export default {
italic: keysFor(ITALIC_TEXT),
strikethrough: keysFor(STRIKETHROUGH_TEXT),
link: keysFor(LINK_TEXT),
+ indent: keysFor(INDENT_LINE),
+ outdent: keysFor(OUTDENT_LINE),
},
i18n: {
writeTabTitle: __('Write'),
@@ -235,6 +243,7 @@ export default {
variant="confirm"
category="primary"
size="small"
+ data-qa-selector="dismiss_suggestion_popover_button"
@click="handleSuggestDismissed"
>
{{ __('Got it') }}
@@ -318,6 +327,32 @@ export default {
icon="list-task"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('indent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.indent"
+ command="indentLines"
+ icon="list-indent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('outdent')"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.outdent"
+ command="outdentLines"
+ icon="list-outdent"
+ />
+ <toolbar-button
v-if="!restrictedToolBarItems.includes('collapsible-section')"
:tag="mdCollapsibleSection"
:prepend="true"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
new file mode 100644
index 00000000000..b38772d5aa5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -0,0 +1,216 @@
+<script>
+import { GlSegmentedControl } from '@gitlab/ui';
+import { __ } from '~/locale';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import axios from '~/lib/utils/axios_utils';
+import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants';
+import MarkdownField from './field.vue';
+
+export default {
+ components: {
+ MarkdownField,
+ LocalStorageSync,
+ GlSegmentedControl,
+ ContentEditor: () =>
+ import(
+ /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
+ ),
+ },
+ props: {
+ value: {
+ type: String,
+ required: true,
+ },
+ renderMarkdownPath: {
+ type: String,
+ required: true,
+ },
+ markdownDocsPath: {
+ type: String,
+ required: true,
+ },
+ quickActionsDocsPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ uploadsPath: {
+ type: String,
+ required: false,
+ default: () => window.uploads_path,
+ },
+ enableContentEditor: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ formFieldId: {
+ type: String,
+ required: true,
+ },
+ formFieldName: {
+ type: String,
+ required: true,
+ },
+ enablePreview: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ enableAutocomplete: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ formFieldPlaceholder: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ formFieldAriaLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ initOnAutofocus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ supportsQuickActions: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ editingMode: EDITING_MODE_MARKDOWN_FIELD,
+ switchEditingControlEnabled: true,
+ autofocus: this.initOnAutofocus,
+ };
+ },
+ computed: {
+ isContentEditorActive() {
+ return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR;
+ },
+ contentEditorAutofocus() {
+ // Match textarea focus behavior
+ return this.autofocus ? 'end' : false;
+ },
+ },
+ mounted() {
+ this.autofocusTextarea(this.editingMode);
+ },
+ methods: {
+ updateMarkdownFromContentEditor({ markdown }) {
+ this.$emit('input', markdown);
+ },
+ updateMarkdownFromMarkdownField({ target }) {
+ this.$emit('input', target.value);
+ },
+ enableSwitchEditingControl() {
+ this.switchEditingControlEnabled = true;
+ },
+ disableSwitchEditingControl() {
+ this.switchEditingControlEnabled = false;
+ },
+ renderMarkdown(markdown) {
+ return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body);
+ },
+ onEditingModeChange(editingMode) {
+ this.notifyEditingModeChange(editingMode);
+ this.enableAutofocus(editingMode);
+ },
+ onEditingModeRestored(editingMode) {
+ this.notifyEditingModeChange(editingMode);
+ },
+ notifyEditingModeChange(editingMode) {
+ this.$emit(editingMode);
+ },
+ enableAutofocus(editingMode) {
+ this.autofocus = true;
+ this.autofocusTextarea(editingMode);
+ },
+ autofocusTextarea(editingMode) {
+ if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) {
+ this.$refs.textarea.focus();
+ }
+ },
+ },
+ switchEditingControlOptions: [
+ { text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD },
+ { text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR },
+ ],
+};
+</script>
+<template>
+ <div>
+ <div class="gl-display-flex gl-justify-content-start gl-mb-3">
+ <gl-segmented-control
+ v-model="editingMode"
+ data-testid="toggle-editing-mode-button"
+ data-qa-selector="editing_mode_button"
+ class="gl-display-flex"
+ :options="$options.switchEditingControlOptions"
+ :disabled="!enableContentEditor || !switchEditingControlEnabled"
+ @change="onEditingModeChange"
+ />
+ </div>
+ <local-storage-sync
+ v-model="editingMode"
+ storage-key="gl-wiki-content-editor-enabled"
+ @input="onEditingModeRestored"
+ />
+ <markdown-field
+ v-if="!isContentEditorActive"
+ :markdown-preview-path="renderMarkdownPath"
+ can-attach-file
+ :enable-autocomplete="enableAutocomplete"
+ :textarea-value="value"
+ :markdown-docs-path="markdownDocsPath"
+ :quick-actions-docs-path="quickActionsDocsPath"
+ :uploads-path="uploadsPath"
+ :enable-preview="enablePreview"
+ class="bordered-box"
+ >
+ <template #textarea>
+ <textarea
+ :id="formFieldId"
+ ref="textarea"
+ :value="value"
+ :name="formFieldName"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ :data-supports-quick-actions="supportsQuickActions"
+ data-qa-selector="markdown_editor_form_field"
+ :aria-label="formFieldAriaLabel"
+ :placeholder="formFieldPlaceholder"
+ @input="updateMarkdownFromMarkdownField"
+ @keydown="$emit('keydown', $event)"
+ >
+ </textarea>
+ </template>
+ </markdown-field>
+ <div v-else>
+ <content-editor
+ :render-markdown="renderMarkdown"
+ :uploads-path="uploadsPath"
+ :markdown="value"
+ :autofocus="contentEditorAutofocus"
+ @change="updateMarkdownFromContentEditor"
+ @loading="disableSwitchEditingControl"
+ @loadingSuccess="enableSwitchEditingControl"
+ @loadingError="enableSwitchEditingControl"
+ @keydown="$emit('keydown', $event)"
+ />
+ <input
+ :id="formFieldId"
+ :value="value"
+ :name="formFieldName"
+ data-qa-selector="markdown_editor_form_field"
+ type="hidden"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 7646a8718d6..855c7a449c4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -139,7 +139,7 @@ export default {
</script>
<template>
- <div class="md-suggestion-header border-bottom-0 gl-mt-3">
+ <div class="md-suggestion-header border-bottom-0 gl-px-4 gl-py-3">
<div class="js-suggestion-diff-header gl-font-weight-bold">
{{ __('Suggested change') }}
<a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn">
@@ -162,6 +162,7 @@ export default {
<gl-button
class="btn-inverted js-remove-from-batch-btn btn-grouped"
:disabled="isApplying"
+ size="small"
@click="removeSuggestionFromBatch"
>
{{ __('Remove from batch') }}
@@ -172,6 +173,7 @@ export default {
class="btn-inverted js-add-to-batch-btn btn-grouped"
data-qa-selector="add_suggestion_batch_button"
:disabled="isDisableButton"
+ size="small"
@click="addSuggestionToBatch"
>
{{ __('Add suggestion to batch') }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 9b81444fc04..30d72332c90 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
@@ -91,7 +91,7 @@ export default {
const suggestionElements = container.querySelectorAll('.js-render-suggestion');
if (this.lineType === 'old') {
- createFlash({
+ createAlert({
message: __('Unable to apply suggestions to a deleted line.'),
parent: this.$el,
});
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index 49217e38a1b..5ca21522d33 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -47,6 +47,11 @@ export default {
required: false,
default: 0,
},
+ command: {
+ type: String,
+ required: false,
+ default: '',
+ },
/**
* A string (or an array of strings) of
@@ -81,6 +86,7 @@ export default {
:data-md-tag-content="tagContent"
:data-md-prepend="prepend"
:data-md-shortcuts="shortcutsString"
+ :data-md-command="command"
:title="buttonTitle"
:aria-label="buttonTitle"
:icon="icon"