You've already forked AstralRinth
forked from didirus/AstralRinth
Requested Changes for Editor Knossos Implementation (#129)
* placeholder * max length & placeholder * Accept editor comment conflict * integrate requested features * null check for ref * Change prompt for image upload * change filter for proper input blocking * Add spoiler button * change url of helper link * shallow resource link style * resource link inherit site style * detach preview styling from markdown-body style * remove sizing dependance on global styles * Bump 0.6.5
This commit is contained in:
@@ -21,3 +21,25 @@
|
|||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
</FileInput>
|
</FileInput>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Long Style
|
||||||
|
|
||||||
|
<DemoContainer>
|
||||||
|
<FileInput
|
||||||
|
:max-size="262144"
|
||||||
|
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||||
|
long-style
|
||||||
|
class="btn"
|
||||||
|
prompt="Upload icon"
|
||||||
|
/>
|
||||||
|
</DemoContainer>
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<FileInput
|
||||||
|
:max-size="262144"
|
||||||
|
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||||
|
long-style
|
||||||
|
class="btn"
|
||||||
|
prompt="Upload icon"
|
||||||
|
/>
|
||||||
|
```
|
||||||
@@ -30,7 +30,7 @@ const description = ref(null)
|
|||||||
|
|
||||||
## With options
|
## With options
|
||||||
<DemoContainer>
|
<DemoContainer>
|
||||||
<MarkdownEditor v-model="description1" placeholder="Enter a description" max-length="30" />
|
<MarkdownEditor v-model="description1" placeholder="Enter a description" max-length="800" max-height="400" />
|
||||||
</DemoContainer>
|
</DemoContainer>
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
@@ -39,7 +39,7 @@ import { ref } from "vue";
|
|||||||
const description = ref(null)
|
const description = ref(null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MarkdownEditor v-model="description" placeholder="Enter a description" max-length="30" />
|
<MarkdownEditor v-model="description" placeholder="Enter a description" max-length="800" max-height="400" />
|
||||||
```
|
```
|
||||||
|
|
||||||
## With image upload
|
## With image upload
|
||||||
|
|||||||
1
lib/assets/icons/scan-eye.svg
Normal file
1
lib/assets/icons/scan-eye.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scan-eye"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="1"/><path d="M5 12s2.5-5 7-5 7 5 7 5-2.5 5-7 5-7-5-7-5"/></svg>
|
||||||
|
After Width: | Height: | Size: 444 B |
@@ -883,7 +883,7 @@ a,
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-blue);
|
color: var(--color-link);
|
||||||
|
|
||||||
&:focus-visible,
|
&:focus-visible,
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ const isChildOfDropdown = (element) => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.animated-dropdown {
|
.animated-dropdown {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
|
min-height: 40px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
@@ -199,6 +200,9 @@ const isChildOfDropdown = (element) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.selected {
|
.selected {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ label {
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
border: dashed 0.3rem var(--color-contrast);
|
border: dashed 0.3rem var(--color-contrast);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--color-accent-contrast);
|
color: var(--color-contrast);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,11 +37,13 @@
|
|||||||
<span class="label__title">Preview</span>
|
<span class="label__title">Preview</span>
|
||||||
<span class="label__description"></span>
|
<span class="label__description"></span>
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div class="markdown-body-wrapper">
|
||||||
style="width: 100%"
|
<div
|
||||||
class="markdown-body"
|
style="width: 100%"
|
||||||
v-html="renderHighlightedString(linkMarkdown)"
|
class="markdown-body"
|
||||||
/>
|
v-html="renderHighlightedString(linkMarkdown)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button :action="() => linkModal?.hide()"><XIcon /> Cancel</Button>
|
<Button :action="() => linkModal?.hide()"><XIcon /> Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -86,13 +88,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="props.onImageUpload && imageUploadOption === 'upload'"
|
v-if="props.onImageUpload && imageUploadOption === 'upload'"
|
||||||
class="iconified-input btn-input-alternative"
|
class="btn-input-alternative"
|
||||||
>
|
>
|
||||||
<FileInput
|
<FileInput
|
||||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||||
prompt="Upload an image"
|
prompt="Drag and drop to upload or click to select file"
|
||||||
class="btn"
|
long-style
|
||||||
should-always-reset
|
should-always-reset
|
||||||
|
class="file-input"
|
||||||
@change="handleImageUpload"
|
@change="handleImageUpload"
|
||||||
>
|
>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
@@ -121,11 +124,13 @@
|
|||||||
<span class="label__title">Preview</span>
|
<span class="label__title">Preview</span>
|
||||||
<span class="label__description"></span>
|
<span class="label__description"></span>
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div class="markdown-body-wrapper">
|
||||||
style="width: 100%"
|
<div
|
||||||
class="markdown-body"
|
style="width: 100%"
|
||||||
v-html="renderHighlightedString(imageMarkdown)"
|
class="markdown-body"
|
||||||
/>
|
v-html="renderHighlightedString(imageMarkdown)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button :action="() => imageModal?.hide()"><XIcon /> Cancel</Button>
|
<Button :action="() => imageModal?.hide()"><XIcon /> Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -172,11 +177,14 @@
|
|||||||
<span class="label__title">Preview</span>
|
<span class="label__title">Preview</span>
|
||||||
<span class="label__description"></span>
|
<span class="label__description"></span>
|
||||||
</span>
|
</span>
|
||||||
<div
|
|
||||||
style="width: 100%"
|
<div class="markdown-body-wrapper">
|
||||||
class="markdown-body"
|
<div
|
||||||
v-html="renderHighlightedString(videoMarkdown)"
|
style="width: 100%"
|
||||||
/>
|
class="markdown-body"
|
||||||
|
v-html="renderHighlightedString(videoMarkdown)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="input-group push-right">
|
<div class="input-group push-right">
|
||||||
<Button :action="() => videoModal?.hide()"><XIcon /> Cancel</Button>
|
<Button :action="() => videoModal?.hide()"><XIcon /> Cancel</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -227,22 +235,30 @@
|
|||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
<span
|
<span
|
||||||
>This editor supports
|
>This editor supports
|
||||||
<a class="link" href="https://docs.modrinth.com/docs/markdown" target="_blank"
|
<a
|
||||||
|
class="markdown-resource-link"
|
||||||
|
href="https://docs.modrinth.com/markdown"
|
||||||
|
target="_blank"
|
||||||
>Markdown formatting</a
|
>Markdown formatting</a
|
||||||
>.</span
|
>.</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div :class="{ hide: !props.maxLength }" class="max-length-label">
|
<div :class="{ hide: !props.maxLength }" class="max-length-label">
|
||||||
<span>Max length: </span>
|
<span>Max length: </span>
|
||||||
<span>{{ props.maxLength ?? 'Unlimited' }}</span>
|
<span>
|
||||||
|
{{ props.maxLength ? `${currentValue?.length || 0}/${props.maxLength}` : 'Unlimited' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="previewMode">
|
||||||
|
<div class="markdown-body-wrapper">
|
||||||
|
<div
|
||||||
|
style="width: 100%"
|
||||||
|
class="markdown-body"
|
||||||
|
v-html="renderHighlightedString(currentValue ?? '')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-if="previewMode"
|
|
||||||
style="width: 100%"
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="renderHighlightedString(currentValue ?? '')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -260,6 +276,7 @@ import {
|
|||||||
Heading3Icon,
|
Heading3Icon,
|
||||||
BoldIcon,
|
BoldIcon,
|
||||||
ItalicIcon,
|
ItalicIcon,
|
||||||
|
ScanEyeIcon,
|
||||||
StrikethroughIcon,
|
StrikethroughIcon,
|
||||||
CodeIcon,
|
CodeIcon,
|
||||||
ListBulletedIcon,
|
ListBulletedIcon,
|
||||||
@@ -294,14 +311,16 @@ const props = withDefaults(
|
|||||||
onImageUpload?: (file: File) => Promise<string>
|
onImageUpload?: (file: File) => Promise<string>
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
maxLength?: number
|
maxLength?: number
|
||||||
|
maxHeight?: number
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
headingButtons: true,
|
headingButtons: true,
|
||||||
onImageUpload: undefined,
|
onImageUpload: undefined,
|
||||||
placeholder: undefined,
|
placeholder: 'Write something...',
|
||||||
maxLength: undefined,
|
maxLength: undefined,
|
||||||
|
maxHeight: undefined,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -320,13 +339,15 @@ onMounted(() => {
|
|||||||
|
|
||||||
const theme = EditorView.theme({
|
const theme = EditorView.theme({
|
||||||
// in defualts.scss there's references to .cm-content and such to inherit global styles
|
// in defualts.scss there's references to .cm-content and such to inherit global styles
|
||||||
'.cm-content, .cm-gutter': {
|
'.cm-content': {
|
||||||
marginBlockEnd: '0.5rem',
|
marginBlockEnd: '0.5rem',
|
||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
minHeight: '200px',
|
minHeight: '200px',
|
||||||
caretColor: 'var(--color-contrast)',
|
caretColor: 'var(--color-contrast)',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
overflowX: 'scroll',
|
overflowX: 'scroll',
|
||||||
|
maxHeight: props.maxHeight ? `${props.maxHeight}px` : 'unset',
|
||||||
|
overflowY: 'scroll',
|
||||||
},
|
},
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@@ -359,19 +380,6 @@ onMounted(() => {
|
|||||||
return false
|
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) => {
|
blur: (_, view) => {
|
||||||
if (props.maxLength && view.state.doc.length > props.maxLength) {
|
if (props.maxLength && view.state.doc.length > props.maxLength) {
|
||||||
// Calculate how many characters to remove from the end
|
// Calculate how many characters to remove from the end
|
||||||
@@ -385,6 +393,13 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const inputFilter = EditorState.changeFilter.of((transaction) => {
|
||||||
|
if (props.maxLength && transaction.newDoc.length > props.maxLength) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
const editorState = EditorState.create({
|
const editorState = EditorState.create({
|
||||||
doc: props.modelValue,
|
doc: props.modelValue,
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -399,6 +414,7 @@ onMounted(() => {
|
|||||||
}),
|
}),
|
||||||
keymap.of(historyKeymap),
|
keymap.of(historyKeymap),
|
||||||
cm_placeholder(props.placeholder || ''),
|
cm_placeholder(props.placeholder || ''),
|
||||||
|
inputFilter,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -468,6 +484,7 @@ const BUTTONS: ButtonGroupMap = {
|
|||||||
markdownCommands.toggleStrikethrough
|
markdownCommands.toggleStrikethrough
|
||||||
),
|
),
|
||||||
composeCommandButton('Code', CodeIcon, markdownCommands.toggleCodeBlock),
|
composeCommandButton('Code', CodeIcon, markdownCommands.toggleCodeBlock),
|
||||||
|
composeCommandButton('Spoiler', ScanEyeIcon, markdownCommands.toggleSpoiler),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
lists: {
|
lists: {
|
||||||
@@ -627,6 +644,38 @@ function openVideoModal() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.file-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1.5rem;
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
background: var(--color-button-bg);
|
||||||
|
border: 2px dashed var(--color-gray);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, scale 0.05s ease-in-out,
|
||||||
|
outline 0.2s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.85);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-resource-link {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-link);
|
||||||
|
|
||||||
|
&:focus-visible,
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
filter: brightness(1.1);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.display-options {
|
.display-options {
|
||||||
margin-bottom: var(--gap-sm);
|
margin-bottom: var(--gap-sm);
|
||||||
}
|
}
|
||||||
@@ -698,9 +747,10 @@ function openVideoModal() {
|
|||||||
gap: var(--gap-xs);
|
gap: var(--gap-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body-wrapper {
|
||||||
border: 1px solid var(--color-button-bg);
|
border: 1px solid var(--color-button-bg);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
width: 100%;
|
||||||
padding: var(--radius-md);
|
padding: var(--radius-md);
|
||||||
min-height: 6rem;
|
min-height: 6rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ export { default as ReportIcon } from '@/assets/icons/report.svg?component'
|
|||||||
export { default as RightArrowIcon } from '@/assets/icons/right-arrow.svg?component'
|
export { default as RightArrowIcon } from '@/assets/icons/right-arrow.svg?component'
|
||||||
export { default as SaveIcon } from '@/assets/icons/save.svg?component'
|
export { default as SaveIcon } from '@/assets/icons/save.svg?component'
|
||||||
export { default as ScaleIcon } from '@/assets/icons/scale.svg?component'
|
export { default as ScaleIcon } from '@/assets/icons/scale.svg?component'
|
||||||
|
export { default as ScanEyeIcon } from '@/assets/icons/scan-eye.svg?component'
|
||||||
export { default as SearchIcon } from '@/assets/icons/search.svg?component'
|
export { default as SearchIcon } from '@/assets/icons/search.svg?component'
|
||||||
export { default as SendIcon } from '@/assets/icons/send.svg?component'
|
export { default as SendIcon } from '@/assets/icons/send.svg?component'
|
||||||
export { default as ServerIcon } from '@/assets/icons/server.svg?component'
|
export { default as ServerIcon } from '@/assets/icons/server.svg?component'
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ const toggleCodeBlock: Command = ({ state, dispatch }) => {
|
|||||||
return toggleAround(state, dispatch, codeBlockMark, codeBlockMark)
|
return toggleAround(state, dispatch, codeBlockMark, codeBlockMark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleSpoiler: Command = ({ state, dispatch }) => {
|
||||||
|
return toggleAround(state, dispatch, '||', '||')
|
||||||
|
}
|
||||||
|
|
||||||
const toggleHeader: Command = ({ state, dispatch }) => {
|
const toggleHeader: Command = ({ state, dispatch }) => {
|
||||||
return toggleLineStart(state, dispatch, '# ')
|
return toggleLineStart(state, dispatch, '# ')
|
||||||
}
|
}
|
||||||
@@ -342,6 +346,7 @@ const commands = {
|
|||||||
toggleItalic,
|
toggleItalic,
|
||||||
toggleStrikethrough,
|
toggleStrikethrough,
|
||||||
toggleCodeBlock,
|
toggleCodeBlock,
|
||||||
|
toggleSpoiler,
|
||||||
toggleHeader,
|
toggleHeader,
|
||||||
toggleHeader2,
|
toggleHeader2,
|
||||||
toggleHeader3,
|
toggleHeader3,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "omorphia",
|
"name": "omorphia",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.6.4",
|
"version": "0.6.5",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user