1<script> 2import { GlPopover, GlButton, GlTooltipDirective } from '@gitlab/ui'; 3import $ from 'jquery'; 4import { keysFor, BOLD_TEXT, ITALIC_TEXT, LINK_TEXT } from '~/behaviors/shortcuts/keybindings'; 5import { getSelectedFragment } from '~/lib/utils/common_utils'; 6import { s__ } from '~/locale'; 7import { CopyAsGFM } from '../../../behaviors/markdown/copy_as_gfm'; 8import ToolbarButton from './toolbar_button.vue'; 9 10export default { 11 components: { 12 ToolbarButton, 13 GlPopover, 14 GlButton, 15 }, 16 directives: { 17 GlTooltip: GlTooltipDirective, 18 }, 19 props: { 20 previewMarkdown: { 21 type: Boolean, 22 required: true, 23 }, 24 lineContent: { 25 type: String, 26 required: false, 27 default: '', 28 }, 29 canSuggest: { 30 type: Boolean, 31 required: false, 32 default: true, 33 }, 34 showSuggestPopover: { 35 type: Boolean, 36 required: false, 37 default: false, 38 }, 39 suggestionStartIndex: { 40 type: Number, 41 required: false, 42 default: 0, 43 }, 44 }, 45 data() { 46 return { 47 tag: '> ', 48 suggestPopoverVisible: false, 49 }; 50 }, 51 computed: { 52 mdTable() { 53 return [ 54 // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 55 '| header | header |', // eslint-disable-line @gitlab/require-i18n-strings 56 '| ------ | ------ |', 57 '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings 58 '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings 59 ].join('\n'); 60 }, 61 mdSuggestion() { 62 return [['```', `suggestion:-${this.suggestionStartIndex}+0`].join(''), `{text}`, '```'].join( 63 '\n', 64 ); 65 }, 66 mdCollapsibleSection() { 67 return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n'); 68 }, 69 isMac() { 70 // Accessing properties using ?. to allow tests to use 71 // this component without setting up window.gl.client. 72 // In production, window.gl.client should always be present. 73 return Boolean(window.gl?.client?.isMac); 74 }, 75 modifierKey() { 76 return this.isMac ? '⌘' : s__('KeyboardKey|Ctrl+'); 77 }, 78 }, 79 watch: { 80 showSuggestPopover() { 81 this.updateSuggestPopoverVisibility(); 82 }, 83 }, 84 mounted() { 85 $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); 86 $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); 87 88 this.updateSuggestPopoverVisibility(); 89 }, 90 beforeDestroy() { 91 $(document).off('markdown-preview:show.vue', this.previewMarkdownTab); 92 $(document).off('markdown-preview:hide.vue', this.writeMarkdownTab); 93 }, 94 methods: { 95 async updateSuggestPopoverVisibility() { 96 await this.$nextTick(); 97 98 this.suggestPopoverVisible = this.showSuggestPopover && this.canSuggest; 99 }, 100 isValid(form) { 101 return ( 102 !form || 103 (form.find('.js-vue-markdown-field').length && $(this.$el).closest('form')[0] === form[0]) 104 ); 105 }, 106 107 previewMarkdownTab(event, form) { 108 if (event.target.blur) event.target.blur(); 109 if (!this.isValid(form)) return; 110 111 this.$emit('preview-markdown'); 112 }, 113 114 writeMarkdownTab(event, form) { 115 if (event.target.blur) event.target.blur(); 116 if (!this.isValid(form)) return; 117 118 this.$emit('write-markdown'); 119 }, 120 handleSuggestDismissed() { 121 this.$emit('handleSuggestDismissed'); 122 }, 123 handleQuote() { 124 const documentFragment = getSelectedFragment(); 125 126 if (!documentFragment || !documentFragment.textContent) { 127 this.tag = '> '; 128 return; 129 } 130 this.tag = ''; 131 132 const transformed = CopyAsGFM.transformGFMSelection(documentFragment); 133 const area = this.$el.parentNode.querySelector('textarea'); 134 135 CopyAsGFM.nodeToGFM(transformed) 136 .then((gfm) => { 137 CopyAsGFM.insertPastedText(area, documentFragment.textContent, CopyAsGFM.quoted(gfm)); 138 }) 139 .catch(() => {}); 140 }, 141 }, 142 shortcuts: { 143 bold: keysFor(BOLD_TEXT), 144 italic: keysFor(ITALIC_TEXT), 145 link: keysFor(LINK_TEXT), 146 }, 147}; 148</script> 149 150<template> 151 <div class="md-header"> 152 <ul class="nav-links clearfix"> 153 <li :class="{ active: !previewMarkdown }" class="md-header-tab"> 154 <button class="js-write-link" type="button" @click="writeMarkdownTab($event)"> 155 {{ __('Write') }} 156 </button> 157 </li> 158 <li :class="{ active: previewMarkdown }" class="md-header-tab"> 159 <button 160 class="js-preview-link js-md-preview-button" 161 type="button" 162 @click="previewMarkdownTab($event)" 163 > 164 {{ __('Preview') }} 165 </button> 166 </li> 167 <li :class="{ active: !previewMarkdown }" class="md-header-toolbar"> 168 <toolbar-button 169 tag="**" 170 :button-title=" 171 sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), { modifierKey }) 172 " 173 :shortcuts="$options.shortcuts.bold" 174 icon="bold" 175 /> 176 <toolbar-button 177 tag="_" 178 :button-title=" 179 sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), { modifierKey }) 180 " 181 :shortcuts="$options.shortcuts.italic" 182 icon="italic" 183 /> 184 <toolbar-button 185 :prepend="true" 186 :tag="tag" 187 :button-title="__('Insert a quote')" 188 icon="quote" 189 @click="handleQuote" 190 /> 191 <template v-if="canSuggest"> 192 <toolbar-button 193 ref="suggestButton" 194 :tag="mdSuggestion" 195 :prepend="true" 196 :button-title="__('Insert suggestion')" 197 :cursor-offset="4" 198 :tag-content="lineContent" 199 icon="doc-code" 200 data-qa-selector="suggestion_button" 201 class="js-suggestion-btn" 202 @click="handleSuggestDismissed" 203 /> 204 <gl-popover 205 v-if="suggestPopoverVisible" 206 :target="$refs.suggestButton.$el" 207 :css-classes="['diff-suggest-popover']" 208 placement="bottom" 209 :show="suggestPopoverVisible" 210 > 211 <strong>{{ __('New! Suggest changes directly') }}</strong> 212 <p class="mb-2"> 213 {{ 214 __( 215 'Suggest code changes which can be immediately applied in one click. Try it out!', 216 ) 217 }} 218 </p> 219 <gl-button 220 variant="info" 221 category="primary" 222 size="small" 223 @click="handleSuggestDismissed" 224 > 225 {{ __('Got it') }} 226 </gl-button> 227 </gl-popover> 228 </template> 229 <toolbar-button tag="`" tag-block="```" :button-title="__('Insert code')" icon="code" /> 230 <toolbar-button 231 tag="[{text}](url)" 232 tag-select="url" 233 :button-title=" 234 sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), { modifierKey }) 235 " 236 :shortcuts="$options.shortcuts.link" 237 icon="link" 238 /> 239 <toolbar-button 240 :prepend="true" 241 tag="- " 242 :button-title="__('Add a bullet list')" 243 icon="list-bulleted" 244 /> 245 <toolbar-button 246 :prepend="true" 247 tag="1. " 248 :button-title="__('Add a numbered list')" 249 icon="list-numbered" 250 /> 251 <toolbar-button 252 :prepend="true" 253 tag="- [ ] " 254 :button-title="__('Add a task list')" 255 icon="list-task" 256 /> 257 <toolbar-button 258 :tag="mdCollapsibleSection" 259 :prepend="true" 260 tag-select="Click to expand" 261 :button-title="__('Add a collapsible section')" 262 icon="details-block" 263 /> 264 <toolbar-button 265 :tag="mdTable" 266 :prepend="true" 267 :button-title="__('Add a table')" 268 icon="table" 269 /> 270 <toolbar-button 271 class="js-zen-enter" 272 :prepend="true" 273 :button-title="__('Go full screen')" 274 icon="maximize" 275 /> 276 </li> 277 </ul> 278 </div> 279</template> 280