Tweaks for Knossos Integration (#122)

* placeholder

* max length & placeholder

* remove default placeholder

* remove scoped css

* allow for throwing in the upload process

* explicit import of info

* fix aggressive card input selection
This commit is contained in:
Carter 2023-10-27 17:20:13 -07:00 committed by GitHub
parent 79bdea0441
commit 544111846c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 11 deletions

View File

@ -3,6 +3,7 @@
import { ref } from "vue"; import { ref } from "vue";
const description = ref(null); const description = ref(null);
const description1 = ref(null);
const description2 = ref(null); const description2 = ref(null);
const description3 = ref(null); const description3 = ref(null);
@ -27,6 +28,20 @@ const description = ref(null)
<MarkdownEditor v-model="description" /> <MarkdownEditor v-model="description" />
``` ```
## With options
<DemoContainer>
<MarkdownEditor v-model="description1" placeholder="Enter a description" max-length="30" />
</DemoContainer>
```vue
<script setup>
import { ref } from "vue";
const description = ref(null)
</script>
<MarkdownEditor v-model="description" placeholder="Enter a description" max-length="30" />
```
## With image upload ## With image upload
<DemoContainer> <DemoContainer>
<MarkdownEditor v-model="description2" :on-image-upload="onImageUpload" /> <MarkdownEditor v-model="description2" :on-image-upload="onImageUpload" />
@ -41,6 +56,9 @@ const description = ref(null)
const onImageUpload = (file: File): string => { const onImageUpload = (file: File): string => {
// Upload the file to your server and return a URL // Upload the file to your server and return a URL
// This example url will not work bc of proxy // This example url will not work bc of proxy
// If the upload fails, throw an error and it will show as
// a Validation Error to the user
return URL.createObjectURL(file).replace("blob:", ""); return URL.createObjectURL(file).replace("blob:", "");
}; };
</script> </script>

View File

@ -70,8 +70,6 @@
:where(input) { :where(input) {
box-sizing: border-box; box-sizing: border-box;
max-height: 40px; max-height: 40px;
width: 24rem;
flex-basis: 24rem;
&:not(.stylized-toggle) { &:not(.stylized-toggle) {
max-width: 100%; max-width: 100%;

View File

@ -223,11 +223,19 @@
</div> </div>
<div ref="editorRef" :class="{ hide: previewMode }" /> <div ref="editorRef" :class="{ hide: previewMode }" />
<div v-if="!previewMode" class="info-blurb"> <div v-if="!previewMode" class="info-blurb">
<InfoIcon /> <div class="info-blurb">
<span> <InfoIcon />
This editor supports <span
<a href="https://docs.modrinth.com/docs/markdown" target="_blank">Markdown formatting</a>. >This editor supports
</span> <a class="link" href="https://docs.modrinth.com/docs/markdown" target="_blank"
>Markdown formatting</a
>.</span
>
</div>
<div :class="{ hide: !props.maxLength }" class="max-length-label">
<span>Max length: </span>
<span>{{ props.maxLength ?? 'Unlimited' }}</span>
</div>
</div> </div>
<div <div
v-if="previewMode" v-if="previewMode"
@ -242,7 +250,7 @@
import { type Component, computed, ref, onMounted, onBeforeUnmount } from 'vue' import { type Component, computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { EditorState } from '@codemirror/state' import { EditorState } from '@codemirror/state'
import { EditorView, keymap } from '@codemirror/view' import { EditorView, keymap, placeholder as cm_placeholder } from '@codemirror/view'
import { markdown } from '@codemirror/lang-markdown' import { markdown } from '@codemirror/lang-markdown'
import { indentWithTab, historyKeymap, history } from '@codemirror/commands' import { indentWithTab, historyKeymap, history } from '@codemirror/commands'
@ -268,6 +276,7 @@ import {
Toggle, Toggle,
FileInput, FileInput,
UploadIcon, UploadIcon,
InfoIcon,
Chips, Chips,
} from '@/components' } from '@/components'
import { markdownCommands, modrinthMarkdownEditorKeymap } from '@/helpers/codemirror' import { markdownCommands, modrinthMarkdownEditorKeymap } from '@/helpers/codemirror'
@ -278,13 +287,21 @@ const props = withDefaults(
modelValue: string modelValue: string
disabled: boolean disabled: boolean
headingButtons: boolean headingButtons: boolean
/**
* @param file The file to upload
* @throws If the file is invalid or the upload fails
*/
onImageUpload?: (file: File) => Promise<string> onImageUpload?: (file: File) => Promise<string>
placeholder?: string
maxLength?: number
}>(), }>(),
{ {
modelValue: '', modelValue: '',
disabled: false, disabled: false,
headingButtons: true, headingButtons: true,
onImageUpload: undefined, onImageUpload: undefined,
placeholder: undefined,
maxLength: undefined,
} }
) )
@ -321,7 +338,6 @@ onMounted(() => {
paste: (ev, view) => { paste: (ev, view) => {
// If the user's pasting a url, automatically convert it to a link with the selection as the text or the url itself if no selection content. // If the user's pasting a url, automatically convert it to a link with the selection as the text or the url itself if no selection content.
const url = ev.clipboardData?.getData('text/plain') const url = ev.clipboardData?.getData('text/plain')
if (url) { if (url) {
try { try {
cleanUrl(url) cleanUrl(url)
@ -337,6 +353,35 @@ onMounted(() => {
const linkMarkdown = `[${linkText}](${url})` const linkMarkdown = `[${linkText}](${url})`
return markdownCommands.replaceSelection(view, linkMarkdown) return markdownCommands.replaceSelection(view, linkMarkdown)
} }
// Check if the length of the document is greater than the max length. If it is, prevent the paste.
if (props.maxLength && view.state.doc.length > props.maxLength) {
ev.preventDefault()
return false
}
},
beforeinput: (ev, view) => {
if (props.maxLength && view.state.doc.length > props.maxLength) {
ev.preventDefault()
// Calculate how many characters to remove from the end
const excessLength = view.state.doc.length - props.maxLength
// Dispatch transaction to remove excess characters
view.dispatch({
changes: { from: view.state.doc.length - excessLength, to: view.state.doc.length },
selection: { anchor: props.maxLength, head: props.maxLength }, // Place cursor at the end
})
return true
}
},
blur: (_, view) => {
if (props.maxLength && view.state.doc.length > props.maxLength) {
// Calculate how many characters to remove from the end
const excessLength = view.state.doc.length - props.maxLength
// Dispatch transaction to remove excess characters
view.dispatch({
changes: { from: view.state.doc.length - excessLength, to: view.state.doc.length },
selection: { anchor: props.maxLength, head: props.maxLength }, // Place cursor at the end
})
}
}, },
}) })
@ -353,6 +398,7 @@ onMounted(() => {
addKeymap: false, addKeymap: false,
}), }),
keymap.of(historyKeymap), keymap.of(historyKeymap),
cm_placeholder(props.placeholder || ''),
], ],
}) })
@ -532,6 +578,9 @@ const handleImageUpload = async (files: FileList) => {
linkUrl.value = url linkUrl.value = url
validateURL() validateURL()
} catch (error) { } catch (error) {
if (error instanceof Error) {
linkValidationErrorMessage.value = error.message
}
console.error(error) console.error(error)
} }
} }
@ -577,7 +626,7 @@ function openVideoModal() {
} }
</script> </script>
<style scoped> <style lang="scss">
.display-options { .display-options {
margin-bottom: var(--gap-sm); margin-bottom: var(--gap-sm);
} }
@ -586,7 +635,6 @@ function openVideoModal() {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap; flex-wrap: wrap;
overflow: hidden;
justify-content: space-between; justify-content: space-between;
margin-bottom: var(--gap-sm); margin-bottom: var(--gap-sm);
gap: var(--gap-xs); gap: var(--gap-xs);
@ -635,6 +683,7 @@ function openVideoModal() {
.info-blurb { .info-blurb {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: var(--gap-xs); gap: var(--gap-xs);
} }