diff options
Diffstat (limited to 'app/assets/javascripts/static_site_editor')
7 files changed, 208 insertions, 51 deletions
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue index 53fbb2a330d..e602f26acdf 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_area.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue @@ -2,6 +2,7 @@ import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue'; import PublishToolbar from './publish_toolbar.vue'; import EditHeader from './edit_header.vue'; +import EditDrawer from './edit_drawer.vue'; import UnsavedChangesConfirmDialog from './unsaved_changes_confirm_dialog.vue'; import parseSourceFile from '~/static_site_editor/services/parse_source_file'; import { EDITOR_TYPES } from '~/vue_shared/components/rich_content_editor/constants'; @@ -15,6 +16,7 @@ export default { RichContentEditor, PublishToolbar, EditHeader, + EditDrawer, UnsavedChangesConfirmDialog, }, props: { @@ -48,6 +50,8 @@ export default { parsedSource: parseSourceFile(this.preProcess(true, this.content)), editorMode: EDITOR_TYPES.wysiwyg, isModified: false, + hasMatter: false, + isDrawerOpen: false, }; }, imageRepository: imageRepository(), @@ -55,10 +59,19 @@ export default { editableContent() { return this.parsedSource.content(this.isWysiwygMode); }, + editableMatter() { + return this.isDrawerOpen ? this.parsedSource.matter() : {}; + }, + hasSettings() { + return this.hasMatter && this.isWysiwygMode; + }, isWysiwygMode() { return this.editorMode === EDITOR_TYPES.wysiwyg; }, }, + created() { + this.refreshEditHelpers(); + }, methods: { preProcess(isWrap, value) { const formattedContent = formatter(value); @@ -67,9 +80,21 @@ export default { : templater.unwrap(formattedContent); return templatedContent; }, - onInputChange(newVal) { - this.parsedSource.sync(newVal, this.isWysiwygMode); + refreshEditHelpers() { this.isModified = this.parsedSource.isModified(); + this.hasMatter = this.parsedSource.hasMatter(); + }, + onDrawerOpen() { + this.isDrawerOpen = true; + this.refreshEditHelpers(); + }, + onDrawerClose() { + this.isDrawerOpen = false; + this.refreshEditHelpers(); + }, + onInputChange(newVal) { + this.parsedSource.syncContent(newVal, this.isWysiwygMode); + this.refreshEditHelpers(); }, onModeChange(mode) { this.editorMode = mode; @@ -77,6 +102,9 @@ export default { const preProcessedContent = this.preProcess(this.isWysiwygMode, this.editableContent); this.$refs.editor.resetInitialValue(preProcessedContent); }, + onUpdateSettings(settings) { + this.parsedSource.syncMatter(settings); + }, onUploadImage({ file, imageUrl }) { this.$options.imageRepository.add(file, imageUrl); }, @@ -93,12 +121,19 @@ export default { <template> <div class="d-flex flex-grow-1 flex-column h-100"> <edit-header class="py-2" :title="title" /> + <edit-drawer + v-if="hasMatter" + :is-open="isDrawerOpen" + :settings="editableMatter" + @close="onDrawerClose" + @updateSettings="onUpdateSettings" + /> <rich-content-editor ref="editor" :content="editableContent" :initial-edit-type="editorMode" :image-root="imageRoot" - class="mb-9 h-100" + class="mb-9 pb-6 h-100" @modeChange="onModeChange" @input="onInputChange" @uploadImage="onUploadImage" @@ -106,9 +141,11 @@ export default { <unsaved-changes-confirm-dialog :modified="isModified" /> <publish-toolbar class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full" + :has-settings="hasSettings" :return-url="returnUrl" :saveable="isModified" :saving-changes="savingChanges" + @editSettings="onDrawerOpen" @submit="onSubmit" /> </div> diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue new file mode 100644 index 00000000000..0484d38dde0 --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue @@ -0,0 +1,32 @@ +<script> +import { GlDrawer } from '@gitlab/ui'; +import FrontMatterControls from './front_matter_controls.vue'; + +export default { + components: { + GlDrawer, + FrontMatterControls, + }, + props: { + isOpen: { + type: Boolean, + required: true, + }, + settings: { + type: Object, + required: true, + }, + }, +}; +</script> +<template> + <gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')"> + <template #header>{{ __('Page settings') }}</template> + <template> + <front-matter-controls + :settings="settings" + @updateSettings="$emit('updateSettings', $event)" + /> + </template> + </gl-drawer> +</template> diff --git a/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue new file mode 100644 index 00000000000..dad3907c3ff --- /dev/null +++ b/app/assets/javascripts/static_site_editor/components/front_matter_controls.vue @@ -0,0 +1,57 @@ +<script> +import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui'; +import { humanize } from '~/lib/utils/text_utility'; + +export default { + components: { + GlForm, + GlFormInput, + GlFormGroup, + }, + props: { + settings: { + type: Object, + required: true, + }, + }, + data() { + return { + editableSettings: { ...this.settings }, + }; + }, + methods: { + getId(type, key) { + return `sse-front-matter-${type}-${key}`; + }, + getIsSupported(val) { + return ['string', 'number'].includes(typeof val); + }, + getLabel(str) { + return humanize(str); + }, + onUpdate() { + this.$emit('updateSettings', { ...this.editableSettings }); + }, + }, +}; +</script> +<template> + <gl-form> + <template v-for="(value, key) of editableSettings"> + <gl-form-group + v-if="getIsSupported(value)" + :id="getId('form-group', key)" + :key="key" + :label="getLabel(key)" + :label-for="getId('control', key)" + > + <gl-form-input + :id="getId('control', key)" + v-model.lazy="editableSettings[key]" + type="text" + @input="onUpdate" + /> + </gl-form-group> + </template> + </gl-form> +</template> diff --git a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue index 6cd2a4dd700..2d62964cb3b 100644 --- a/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue +++ b/app/assets/javascripts/static_site_editor/components/publish_toolbar.vue @@ -6,6 +6,11 @@ export default { GlButton, }, props: { + hasSettings: { + type: Boolean, + required: false, + default: false, + }, returnUrl: { type: String, required: false, @@ -31,12 +36,21 @@ export default { s__('StaticSiteEditor|Return to site') }}</gl-button> <gl-button + v-if="hasSettings" + ref="settings" + :disabled="savingChanges" + @click="$emit('editSettings')" + > + {{ __('Settings') }} + </gl-button> + <gl-button + ref="submit" variant="success" :disabled="!saveable" :loading="savingChanges" @click="$emit('submit')" > - <span>{{ __('Submit Changes') }}</span> + {{ __('Submit changes') }} </gl-button> </div> </div> diff --git a/app/assets/javascripts/static_site_editor/services/formatter.js b/app/assets/javascripts/static_site_editor/services/formatter.js index 92d5e8a5df8..9a5dcd307eb 100644 --- a/app/assets/javascripts/static_site_editor/services/formatter.js +++ b/app/assets/javascripts/static_site_editor/services/formatter.js @@ -1,3 +1,45 @@ +import { repeat } from 'lodash'; + +const topLevelOrderedRegexp = /^\d{1,3}/; +const nestedLineRegexp = /^\s+/; + +/** + * DISCLAIMER: This is a temporary fix that corrects the indentation + * spaces of list items. This workaround originates in the usage of + * the Static Site Editor to edit the Handbook. The Handbook uses a + * Markdown parser called Kramdown interprets lines indented + * with two spaces as content within a list. For example: + * + * 1. ordered list + * - nested unordered list + * + * The Static Site Editor uses a different Markdown parser based on the + * CommonMark specification (official Markdown spec) called ToastMark. + * When the SSE encounters a nested list with only two spaces, it flattens + * the list: + * + * 1. ordered list + * - nested unordered list + * + * This function attempts to correct this problem before the content is loaded + * by Toast UI. + */ +const correctNestedContentIndenation = source => { + const lines = source.split('\n'); + let topLevelOrderedListDetected = false; + + return lines + .reduce((result, line) => { + if (topLevelOrderedListDetected && nestedLineRegexp.test(line)) { + return [...result, line.replace(nestedLineRegexp, repeat(' ', 4))]; + } + + topLevelOrderedListDetected = topLevelOrderedRegexp.test(line); + return [...result, line]; + }, []) + .join('\n'); +}; + const removeOrphanedBrTags = source => { /* Until the underlying Squire editor of Toast UI Editor resolves duplicate `<br>` tags, this `replace` solution will clear out orphaned `<br>` tags that it generates. Additionally, @@ -8,7 +50,7 @@ const removeOrphanedBrTags = source => { }; const format = source => { - return removeOrphanedBrTags(source); + return correctNestedContentIndenation(removeOrphanedBrTags(source)); }; export default format; diff --git a/app/assets/javascripts/static_site_editor/services/image_service.js b/app/assets/javascripts/static_site_editor/services/image_service.js index edc69d0579a..25ab1084572 100644 --- a/app/assets/javascripts/static_site_editor/services/image_service.js +++ b/app/assets/javascripts/static_site_editor/services/image_service.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/prefer-default-export export const getBinary = file => { return new Promise((resolve, reject) => { const reader = new FileReader(); diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js index 126dfe81b90..640186ee1d0 100644 --- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js +++ b/app/assets/javascripts/static_site_editor/services/parse_source_file.js @@ -1,64 +1,40 @@ -const parseSourceFile = raw => { - const frontMatterRegex = /(^---$[\s\S]*?^---$)/m; - const preGroupedRegex = /([\s\S]*?)(^---$[\s\S]*?^---$)(\s*)([\s\S]*)/m; // preFrontMatter, frontMatter, spacing, and content - let initial; - let editable; - - const hasFrontMatter = source => frontMatterRegex.test(source); +import grayMatter from 'gray-matter'; - const buildPayload = (source, header, spacing, body) => { - return { raw: source, header, spacing, body }; - }; +const parseSourceFile = raw => { + const remake = source => grayMatter(source, {}); - const parse = source => { - if (hasFrontMatter(source)) { - const match = source.match(preGroupedRegex); - const [, preFrontMatter, frontMatter, spacing, content] = match; - const header = preFrontMatter + frontMatter; + let editable = remake(raw); - return buildPayload(source, header, spacing, content); + const syncContent = (newVal, isBody) => { + if (isBody) { + editable.content = newVal; + } else { + editable = remake(newVal); } - - return buildPayload(source, '', '', source); }; - const syncEditable = () => { - /* - We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing). - Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw - */ - editable = parse(editable.raw); - }; + const trimmedEditable = () => grayMatter.stringify(editable).trim(); - const syncBodyToRaw = () => { - editable.raw = `${editable.header}${editable.spacing}${editable.body}`; - }; - - const sync = (newVal, isBodyToRaw) => { - const editableKey = isBodyToRaw ? 'body' : 'raw'; - editable[editableKey] = newVal; + const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96 - if (isBodyToRaw) { - syncBodyToRaw(); - } - - syncEditable(); - }; + const matter = () => editable.data; - const content = (isBody = false) => { - const editableKey = isBody ? 'body' : 'raw'; - return editable[editableKey]; + const syncMatter = settings => { + const source = grayMatter.stringify(editable.content, settings); + syncContent(source); }; - const isModified = () => initial.raw !== editable.raw; + const isModified = () => trimmedEditable() !== raw; - initial = parse(raw); - editable = parse(raw); + const hasMatter = () => editable.matter.length > 0; return { + matter, + syncMatter, content, + syncContent, isModified, - sync, + hasMatter, }; }; |