Skip to content

Commit

Permalink
feat: insert new line
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdehaven committed Dec 19, 2023
1 parent 7fa25fb commit 3b88ae6
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 21 deletions.
2 changes: 1 addition & 1 deletion sandbox/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import mockResponse from './mock-document-response'
const editable = ref<boolean>(true)
const contentUpdated = (markdown: string) => {

Check failure on line 26 in sandbox/App.vue

View workflow job for this annotation

GitHub Actions / Run Tests

'markdown' is defined but never used
console.log('content updated: %o', markdown)
console.log('content updated')
}
const modeChanged = (mode: string) => {
Expand Down
16 changes: 9 additions & 7 deletions src/components/MarkdownUi.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
spellcheck="false"
translate="no"
:value="rawMarkdown"
@input="debouncedContentEdit"
@input="$event => onContentEdit($event as any)"
@keydown.shift.tab.exact.prevent="onShiftTab"
@keydown.tab.exact.prevent="onTab"
/>
Expand Down Expand Up @@ -232,10 +232,15 @@ const updateMermaid = async () => {
}
// When the textarea `input` event is triggered, or "faked" by other editor methods, update the Vue refs and rendered markdown
const onContentEdit = async (event: TextAreaInputEvent, emitEvent = true): Promise<void> => {
// Update the ref
const onContentEdit = (event: TextAreaInputEvent, emitEvent = true): void => {
// Update the ref immediately
rawMarkdown.value = event.target.value
// Debounce the update for the UI
debouncedUpdateContent(emitEvent)
}
const debouncedUpdateContent = debounce(async (emitEvent = true): Promise<void> => {
// Update the output
markdownHtml.value = getHtmlFromMarkdown(rawMarkdown.value)
Expand All @@ -247,10 +252,7 @@ const onContentEdit = async (event: TextAreaInputEvent, emitEvent = true): Promi
// Re-render any `.mermaid` containers
await nextTick() // **MUST** await nextTick for the virtual DOM to refresh
await updateMermaid()
}
// Call the `onContentEdit` method, debounced, since this is bound to the textarea element's input event
const debouncedContentEdit = debounce(async (event: TextAreaInputEvent): Promise<void> => onContentEdit(event), EDITOR_DEBOUNCE_TIMEOUT)
}, EDITOR_DEBOUNCE_TIMEOUT)
/**
* Emulate an `input` event when injecting content into the textarea
Expand Down
13 changes: 11 additions & 2 deletions src/composables/useKeyboardShortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function useKeyboardShortcuts(
// The document.activeElement
const activeElement = useActiveElement()
const textareaIsActive = computed((): boolean => activeElement.value?.id === textareaId)
const { toggleInlineFormatting } = useMarkdownActions(textareaId, rawMarkdown)
const { toggleInlineFormatting, insertNewLine } = useMarkdownActions(textareaId, rawMarkdown)

const getFormatForKeyEvent = (evt: any): InlineFormat | undefined => {
let format: InlineFormat | undefined
Expand All @@ -44,7 +44,7 @@ export default function useKeyboardShortcuts(
format = 'strikethrough'
}
break
// Code (also requires shift modifier)
// Inline code (also requires shift modifier)
case 'c':
if (evt.shiftKey) {
format = 'code'
Expand All @@ -55,6 +55,7 @@ export default function useKeyboardShortcuts(
return format
}

// Bind keyboard events
useMagicKeys({
passive: false,
onEventFired(e) {
Expand All @@ -70,9 +71,17 @@ export default function useKeyboardShortcuts(
if (format) {
e.preventDefault()
toggleInlineFormatting(format)
// Always fire the callback
onEditCallback()
}
}

if (e.key && e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
insertNewLine()
// Always fire the callback
onEditCallback()
}
},
})
}
81 changes: 71 additions & 10 deletions src/composables/useMarkdownActions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { reactive, nextTick } from 'vue'
import type { Ref } from 'vue'
import { InlineFormatWrapper, DEFAULT_CODEBLOCK_LANGUAGE } from '../constants'
import { InlineFormatWrapper, DEFAULT_CODEBLOCK_LANGUAGE, MARKDOWN_TEMPLATE_CODEBLOCK, MARKDOWN_TEMPLATE_TASK, MARKDOWN_TEMPLATE_TABLE } from '../constants'
import type { InlineFormat, MarkdownTemplate } from '../types'

/**
Expand Down Expand Up @@ -116,6 +116,7 @@ export default function useMarkdownActions(
// Remove the empty wrapper
rawMarkdown.value = beforeText.substring(0, beforeText.length - wrapperLength) + afterText.substring(wrapperLength)

// Wait for the DOM to cycle
await nextTick()
// Always focus back on the textarea
textarea.focus()
Expand All @@ -127,6 +128,7 @@ export default function useMarkdownActions(
// If the `wrapper` text does not exist to the left and right of the cursor
rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start) + wrapper + wrapper + rawMarkdown.value.substring(selectedText.end)

// Wait for the DOM to cycle
await nextTick()
// Always focus back on the textarea
textarea.focus()
Expand Down Expand Up @@ -167,6 +169,7 @@ export default function useMarkdownActions(
rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start) + newText + rawMarkdown.value.substring(selectedText.end)
}

// Wait for the DOM to cycle
await nextTick()
// Always focus back on the textarea
textarea.focus()
Expand Down Expand Up @@ -227,6 +230,7 @@ export default function useMarkdownActions(
rawMarkdown.value = action === 'add' ? rawMarkdown.value.substring(0, selectedText.start) + spaces + rawMarkdown.value.substring(selectedText.end) : rawMarkdown.value.substring(0, selectedText.start - spaces.length) + rawMarkdown.value.substring(selectedText.end)
}

// Wait for the DOM to cycle
await nextTick()
// Always focus back on the textarea
textarea.focus()
Expand Down Expand Up @@ -267,24 +271,28 @@ export default function useMarkdownActions(
return
}

const prevLineText = rawMarkdown.value.substring(0, selectedText.start)
// If the previous line is not empty, insert a new line at the cursor position in the switch
const needsNewLine: string = prevLineText.length === 0 || prevLineText.endsWith('\n\n') ? '' : '\n'

let markdownTemplate: string = ''

switch (template) {
case 'codeblock':
markdownTemplate =
'```' + DEFAULT_CODEBLOCK_LANGUAGE + '\n' +
'\n' +
'```'
needsNewLine +
MARKDOWN_TEMPLATE_CODEBLOCK
break
case 'table':
markdownTemplate =
'| Column1 | Column2 | Column3 |\n' +
'| :--- | :--- | :--- |\n' +
'| Content | Content | Content |'
needsNewLine +
MARKDOWN_TEMPLATE_TABLE
break
case 'task':
// needsNewLine not needed here
markdownTemplate =
'- [ ] '
needsNewLine +
MARKDOWN_TEMPLATE_TASK
break
}

Expand All @@ -295,16 +303,17 @@ export default function useMarkdownActions(

rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start) + markdownTemplate + rawMarkdown.value.substring(selectedText.end)

// Wait for the DOM to cycle
await nextTick()
// Always focus back on the textarea
textarea.focus()

switch (template) {
case 'codeblock':
// Move the cursor to the language string of the codeblock
textarea.selectionStart = selectedText.start + 3
textarea.selectionStart = selectedText.start + (needsNewLine ? 4 : 3)
// Move the end of the selection to the end of the default language so it is selected
textarea.selectionEnd = selectedText.start + 3 + DEFAULT_CODEBLOCK_LANGUAGE.length
textarea.selectionEnd = selectedText.start + (needsNewLine ? 4 : 3) + DEFAULT_CODEBLOCK_LANGUAGE.length
break
default:
// Move the cursor to the end of the table markdown
Expand All @@ -316,10 +325,62 @@ export default function useMarkdownActions(
}
}

const insertNewLine = async (): Promise<void> => {
try {
const textarea = getTextarea()

if (!textarea) {
return
}

// Update the selected text object
getSelectedText()

// Check the current line to see if we're within another format block (e.g. list, code, etc.)
const prevLineText = rawMarkdown.value.substring(0, selectedText.start)
// Grab the last line before the cursor
const lastLine = prevLineText?.split('\n')?.pop() || ''

const newLineCharacter = '\n'
let newLineContent = newLineCharacter

// Should we remove the new line template on second Enter keypress
let removeNewLineTemplate = false
let templateLength = 0

// If the last line starts with any formatting blocks, also inject them into the next line
if (lastLine.startsWith(MARKDOWN_TEMPLATE_TASK)) {
templateLength = MARKDOWN_TEMPLATE_TASK.length
// If the last task item is empty, remove the template
if (lastLine === MARKDOWN_TEMPLATE_TASK) {
removeNewLineTemplate = true
} else {
newLineContent += MARKDOWN_TEMPLATE_TASK
}
}

if (removeNewLineTemplate) {
rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start - templateLength) + rawMarkdown.value.substring(selectedText.end)
} else {
rawMarkdown.value = rawMarkdown.value.substring(0, selectedText.start) + newLineContent + rawMarkdown.value.substring(selectedText.end)
}

// Wait for the DOM to cycle
await nextTick()
// Always focus back on the textarea
textarea.focus()

textarea.selectionEnd = selectedText.start + newLineContent.length
} catch (err) {
console.warn('insertNewLine', err)
}
}

return {
selectedText,
toggleInlineFormatting,
toggleTab,
insertMarkdownTemplate,
insertNewLine,
}
}
17 changes: 16 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** The time, in milliseconds, to debounce the textarea input event */
export const EDITOR_DEBOUNCE_TIMEOUT: number = 500
export const EDITOR_DEBOUNCE_TIMEOUT: number = 400

export const DEFAULT_CODEBLOCK_LANGUAGE: string = 'markdown'

Expand All @@ -16,3 +16,18 @@ export enum InlineFormatWrapper {

/** The height of the .markdown-ui-toolbar */
export const TOOLBAR_HEIGHT: string = '40px'

/** The markdown template for a codeblock */
export const MARKDOWN_TEMPLATE_CODEBLOCK =
'```' + DEFAULT_CODEBLOCK_LANGUAGE + '\n' +
'\n' +
'```'

/** The markdown template for a task */
export const MARKDOWN_TEMPLATE_TASK = '- [ ] ' // Ensure trailing space remains

/** The markdown template for a table */
export const MARKDOWN_TEMPLATE_TABLE =
'| Column1 | Column2 | Column3 |\n' +
'| :--- | :--- | :--- |\n' +
'| Content | Content | Content |\n'

0 comments on commit 3b88ae6

Please sign in to comment.