debian-mirror-gitlab/app/assets/javascripts/vue_shared/components/markdown/field.vue

387 lines
10 KiB
Vue
Raw Normal View History

2017-09-10 17:25:29 +05:30
<script>
2021-03-11 19:13:27 +05:30
import { GlIcon } from '@gitlab/ui';
2018-12-13 13:39:08 +05:30
import $ from 'jquery';
2019-12-04 20:38:33 +05:30
import '~/behaviors/markdown/render_gfm';
2022-03-02 08:16:31 +05:30
import { debounce, unescape } from 'lodash';
2021-09-30 23:02:18 +05:30
import createFlash from '~/flash';
2020-07-28 23:09:34 +05:30
import GLForm from '~/gl_form';
2021-03-11 19:13:27 +05:30
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
2019-02-15 15:39:39 +05:30
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
2020-07-28 23:09:34 +05:30
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
2021-03-11 19:13:27 +05:30
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
2017-09-10 17:25:29 +05:30
2021-11-11 11:23:49 +05:30
function cleanUpLine(content) {
return unescape(stripHtml(content).replace(/\\n/g, '%br').replace(/\n/g, ''));
}
2018-12-13 13:39:08 +05:30
export default {
components: {
2020-07-28 23:09:34 +05:30
MarkdownHeader,
MarkdownToolbar,
2020-11-24 15:15:51 +05:30
GlIcon,
2019-02-15 15:39:39 +05:30
Suggestions,
2018-12-13 13:39:08 +05:30
},
2020-07-28 23:09:34 +05:30
mixins: [glFeatureFlagsMixin()],
2018-12-13 13:39:08 +05:30
props: {
2021-01-03 14:25:43 +05:30
/**
* This prop should be bound to the value of the `<textarea>` element
* that is rendered as a child of this component (in the `textarea` slot)
*/
textareaValue: {
type: String,
required: true,
},
markdownDocsPath: {
type: String,
required: true,
},
2020-03-13 15:44:24 +05:30
isSubmitting: {
type: Boolean,
required: false,
default: false,
},
2018-12-13 13:39:08 +05:30
markdownPreviewPath: {
type: String,
required: false,
default: '',
2018-03-17 18:26:18 +05:30
},
2022-05-07 20:08:51 +05:30
enablePreview: {
type: Boolean,
required: false,
default: true,
},
2018-12-13 13:39:08 +05:30
addSpacingClasses: {
type: Boolean,
required: false,
default: true,
2017-09-10 17:25:29 +05:30
},
2018-12-13 13:39:08 +05:30
quickActionsDocsPath: {
type: String,
required: false,
default: '',
2017-09-10 17:25:29 +05:30
},
2018-12-13 13:39:08 +05:30
canAttachFile: {
type: Boolean,
required: false,
default: true,
},
2021-04-29 21:17:54 +05:30
uploadsPath: {
type: String,
required: false,
default: '',
},
2018-12-13 13:39:08 +05:30
enableAutocomplete: {
type: Boolean,
required: false,
default: true,
2017-09-10 17:25:29 +05:30
},
2019-02-15 15:39:39 +05:30
line: {
type: Object,
required: false,
default: null,
},
2021-04-29 21:17:54 +05:30
lines: {
type: Array,
required: false,
default: () => [],
},
2019-02-15 15:39:39 +05:30
note: {
type: Object,
required: false,
default: () => ({}),
},
canSuggest: {
type: Boolean,
required: false,
default: false,
},
helpPagePath: {
type: String,
required: false,
default: '',
},
2019-09-04 21:01:54 +05:30
showSuggestPopover: {
type: Boolean,
required: false,
default: false,
},
2018-12-13 13:39:08 +05:30
},
data() {
return {
markdownPreview: '',
referencedCommands: '',
2022-03-02 08:16:31 +05:30
referencedUsers: [],
2019-02-15 15:39:39 +05:30
hasSuggestion: false,
2018-12-13 13:39:08 +05:30
markdownPreviewLoading: false,
previewMarkdown: false,
2019-07-07 11:18:12 +05:30
suggestions: this.note.suggestions || [],
2022-05-07 20:08:51 +05:30
debouncedFetchMarkdownLoading: false,
2018-12-13 13:39:08 +05:30
};
},
computed: {
shouldShowReferencedUsers() {
const referencedUsersThreshold = 10;
return this.referencedUsers.length >= referencedUsersThreshold;
},
2019-02-15 15:39:39 +05:30
lineContent() {
2021-04-29 21:17:54 +05:30
if (this.lines.length) {
return this.lines
.map((line) => {
const { rich_text: richText, text } = line;
if (text) {
return text;
}
2021-11-11 11:23:49 +05:30
return cleanUpLine(richText);
2021-04-29 21:17:54 +05:30
})
.join('\\n');
}
2019-02-15 15:39:39 +05:30
if (this.line) {
const { rich_text: richText, text } = this.line;
if (text) {
return text;
}
2021-11-11 11:23:49 +05:30
return cleanUpLine(richText);
2019-02-15 15:39:39 +05:30
}
return '';
},
lineNumber() {
let lineNumber;
if (this.line) {
const { new_line: newLine, old_line: oldLine } = this.line;
lineNumber = newLine || oldLine;
}
return lineNumber;
},
lineType() {
return this.line ? this.line.type : '';
},
2019-09-30 21:07:59 +05:30
addMultipleToDiscussionWarning() {
return sprintf(
__(
2021-01-29 00:20:46 +05:30
'You are about to add %{usersTag} people to the discussion. They will all receive a notification.',
2019-09-30 21:07:59 +05:30
),
{
usersTag: `<strong><span class="js-referenced-users-count">${this.referencedUsers.length}</span></strong>`,
},
false,
);
},
2021-04-29 21:17:54 +05:30
suggestionsStartIndex() {
return Math.max(this.lines.length - 1, 0);
},
2018-12-13 13:39:08 +05:30
},
2020-03-13 15:44:24 +05:30
watch: {
isSubmitting(isSubmitting) {
if (!isSubmitting || !this.$refs['markdown-preview'].querySelectorAll) {
return;
}
const mediaInPreview = this.$refs['markdown-preview'].querySelectorAll('video, audio');
if (mediaInPreview) {
2021-03-08 18:12:59 +05:30
mediaInPreview.forEach((media) => {
2020-03-13 15:44:24 +05:30
media.pause();
});
}
},
2022-03-02 08:16:31 +05:30
textareaValue: {
immediate: true,
handler(textareaValue, oldVal) {
const all = /@all([^\w._-]|$)/;
const hasAll = all.test(textareaValue);
const hadAll = all.test(oldVal);
const justAddedAll = !hadAll && hasAll;
const justRemovedAll = hadAll && !hasAll;
if (justAddedAll) {
2022-05-07 20:08:51 +05:30
this.debouncedFetchMarkdownLoading = false;
2022-03-02 08:16:31 +05:30
this.debouncedFetchMarkdown();
} else if (justRemovedAll) {
2022-05-07 20:08:51 +05:30
this.debouncedFetchMarkdownLoading = true;
2022-03-02 08:16:31 +05:30
this.referencedUsers = [];
}
},
},
2022-05-07 20:08:51 +05:30
enablePreview: {
immediate: true,
handler(newVal) {
if (!newVal) {
this.showWriteTab();
}
},
},
2020-03-13 15:44:24 +05:30
},
2018-12-13 13:39:08 +05:30
mounted() {
2020-07-28 23:09:34 +05:30
// GLForm class handles all the toolbar buttons
2021-01-03 14:25:43 +05:30
return new GLForm(
$(this.$refs['gl-form']),
{
2022-04-04 11:22:00 +05:30
emojis: this.enableAutocomplete,
members: this.enableAutocomplete,
issues: this.enableAutocomplete,
mergeRequests: this.enableAutocomplete,
epics: this.enableAutocomplete,
milestones: this.enableAutocomplete,
labels: this.enableAutocomplete,
snippets: this.enableAutocomplete,
2021-01-29 00:20:46 +05:30
vulnerabilities: this.enableAutocomplete,
2022-04-04 11:22:00 +05:30
contacts: this.enableAutocomplete && this.glFeatures.contactsAutocomplete,
2021-01-03 14:25:43 +05:30
},
true,
);
2018-12-13 13:39:08 +05:30
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('glForm');
if (glForm) {
glForm.destroy();
}
},
methods: {
showPreviewTab() {
if (this.previewMarkdown) return;
2018-03-17 18:26:18 +05:30
2018-12-13 13:39:08 +05:30
this.previewMarkdown = true;
2018-03-17 18:26:18 +05:30
2021-01-03 14:25:43 +05:30
if (this.textareaValue) {
2018-12-13 13:39:08 +05:30
this.markdownPreviewLoading = true;
2019-02-15 15:39:39 +05:30
this.markdownPreview = __('Loading…');
2022-03-02 08:16:31 +05:30
this.fetchMarkdown()
.then((data) => this.renderMarkdown(data))
2021-09-30 23:02:18 +05:30
.catch(() =>
createFlash({
message: __('Error loading markdown preview'),
}),
);
2018-12-13 13:39:08 +05:30
} else {
this.renderMarkdown();
}
},
showWriteTab() {
this.markdownPreview = '';
this.previewMarkdown = false;
},
2018-03-17 18:26:18 +05:30
2022-03-02 08:16:31 +05:30
fetchMarkdown() {
return axios.post(this.markdownPreviewPath, { text: this.textareaValue }).then(({ data }) => {
const { references } = data;
if (references) {
this.referencedCommands = references.commands;
this.referencedUsers = references.users;
this.hasSuggestion = references.suggestions?.length > 0;
this.suggestions = references.suggestions;
}
return data;
});
},
debouncedFetchMarkdown: debounce(function debouncedFetchMarkdown() {
2022-05-07 20:08:51 +05:30
return this.fetchMarkdown().then(() => {
if (this.debouncedFetchMarkdownLoading) {
this.referencedUsers = [];
this.debouncedFetchMarkdownLoading = false;
}
});
2022-03-02 08:16:31 +05:30
}, 400),
2018-12-13 13:39:08 +05:30
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
2019-09-30 21:07:59 +05:30
this.markdownPreview = data.body || __('Nothing to preview.');
2018-03-17 18:26:18 +05:30
2019-03-02 22:35:43 +05:30
this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM())
2021-09-30 23:02:18 +05:30
.catch(() =>
createFlash({
2021-11-18 22:05:49 +05:30
message: __('Error rendering Markdown preview'),
2021-09-30 23:02:18 +05:30
}),
);
2018-03-17 18:26:18 +05:30
},
2018-12-13 13:39:08 +05:30
},
};
2017-09-10 17:25:29 +05:30
</script>
<template>
<div
2018-11-08 19:23:39 +05:30
ref="gl-form"
2020-07-28 23:09:34 +05:30
:class="{ 'gl-mt-3 gl-mb-3': addSpacingClasses }"
2021-01-03 14:25:43 +05:30
class="js-vue-markdown-field md-area position-relative gfm-form"
2021-04-29 21:17:54 +05:30
:data-uploads-path="uploadsPath"
2019-02-15 15:39:39 +05:30
>
2017-09-10 17:25:29 +05:30
<markdown-header
:preview-markdown="previewMarkdown"
2019-02-15 15:39:39 +05:30
:line-content="lineContent"
:can-suggest="canSuggest"
2022-05-07 20:08:51 +05:30
:enable-preview="enablePreview"
2019-09-04 21:01:54 +05:30
:show-suggest-popover="showSuggestPopover"
2021-04-29 21:17:54 +05:30
:suggestion-start-index="suggestionsStartIndex"
2021-11-11 11:23:49 +05:30
data-testid="markdownHeader"
2018-03-17 18:26:18 +05:30
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab"
2019-09-04 21:01:54 +05:30
@handleSuggestDismissed="() => $emit('handleSuggestDismissed')"
2018-03-17 18:26:18 +05:30
/>
2019-02-15 15:39:39 +05:30
<div v-show="!previewMarkdown" class="md-write-holder">
2017-09-10 17:25:29 +05:30
<div class="zen-backdrop">
2022-04-04 11:22:00 +05:30
<slot name="textarea"></slot>
2019-09-30 21:07:59 +05:30
<a
2020-10-24 23:57:45 +05:30
class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
2019-09-30 21:07:59 +05:30
href="#"
2020-06-23 00:09:42 +05:30
:aria-label="__('Leave zen mode')"
2019-09-30 21:07:59 +05:30
>
2020-11-24 15:15:51 +05:30
<gl-icon :size="16" name="minimize" />
2017-09-10 17:25:29 +05:30
</a>
<markdown-toolbar
2018-03-17 18:26:18 +05:30
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
:can-attach-file="canAttachFile"
/>
2017-09-10 17:25:29 +05:30
</div>
</div>
2019-02-15 15:39:39 +05:30
<template v-if="hasSuggestion">
2017-09-10 17:25:29 +05:30
<div
2019-02-15 15:39:39 +05:30
v-show="previewMarkdown"
2017-09-10 17:25:29 +05:30
ref="markdown-preview"
2019-07-07 11:18:12 +05:30
class="js-vue-md-preview md-preview-holder"
2019-01-03 12:48:30 +05:30
>
2019-02-15 15:39:39 +05:30
<suggestions
v-if="hasSuggestion"
:note-html="markdownPreview"
:from-line="lineNumber"
:from-content="lineContent"
:line-type="lineType"
:disabled="true"
:suggestions="suggestions"
:help-page-path="helpPagePath"
/>
2019-01-03 12:48:30 +05:30
</div>
2019-02-15 15:39:39 +05:30
</template>
<template v-else>
2019-01-03 12:48:30 +05:30
<div
2019-02-15 15:39:39 +05:30
v-show="previewMarkdown"
ref="markdown-preview"
2019-07-07 11:18:12 +05:30
class="js-vue-md-preview md md-preview-holder"
2021-11-11 11:23:49 +05:30
v-html="markdownPreview /* eslint-disable-line vue/no-v-html */"
2019-02-15 15:39:39 +05:30
></div>
</template>
2022-03-02 08:16:31 +05:30
<div
v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading"
class="referenced-commands"
v-html="referencedCommands /* eslint-disable-line vue/no-v-html */"
></div>
<div v-if="shouldShowReferencedUsers" class="referenced-users">
<gl-icon name="warning-solid" />
<span v-html="addMultipleToDiscussionWarning /* eslint-disable-line vue/no-v-html */"></span>
</div>
2017-09-10 17:25:29 +05:30
</div>
</template>