Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: toolbar updates #3

Merged
merged 9 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"vue": "^3.3.8"
},
"dependencies": {
"@kong/icons": "^1.8.7",
"@kong/icons": "^1.8.8",
"@sindresorhus/slugify": "^2.2.1",
"@vueuse/core": "^10.7.0",
"html-format": "^1.1.2",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion sandbox/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<MarkdownUi
v-model="content"
:editable="editable"
mode="read"
theme="light"
@cancel="cancelEdit"
@mode="modeChanged"
@save="contentSaved"
Expand Down Expand Up @@ -68,6 +70,6 @@ onBeforeMount(async () => {

<style lang="scss" scoped>
.sandbox-container {
padding: $kui-space-0 $kui-space-70;
padding: var(--kui-space-0, $kui-space-0) var(--kui-space-70, $kui-space-70);
}
</style>
2 changes: 1 addition & 1 deletion sandbox/mock-document-response.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default {
id: 'da315607-4d04-4a54-810d-45156b575932',
content: "# Markdown UI\n\nA markdown renderer and edit UI.\n\n## Formatting types\n\n### Code block and inline code\n\n```typescript {2}\n// TODO: Fenced code blocks need default styles\nconst name = 'Adam'\n```\n\nThis sentence has \\`inlineCode\\` in the middle.\n\n### Emoji\n\n:smile: :rocket: :tada:\n\n### Link\n\n[link](https://github.com)\n\n### Tables\n\n| Column1 | Column2 | Column3 |\n| :--- | :--- | :--- |\n| Content | Content | Content |\n\n### Todo items\n\n- [x] Todo item checked\n- [ ] Another item not checked\n\n---\n\n### Diagrams\n\n#### Mermaid\n\n```mermaid\ngitGraph\n commit\n commit\n branch develop\n checkout develop\n commit\n commit\n checkout main\n merge develop\n commit\n commit\n```\n\n#### Plantuml\n\n```plantuml\nBob -> Alice : hello\n```\n\n#### DOT\n\n```dot\ndigraph example1 {\n 1 -> 2 -> { 4, 5 };\n 1 -> 3 -> { 6, 7 };\n}\n```\n",
content: "# Markdown UI\n\nA markdown renderer and edit UI.\n\n## Formatting types\n\n### Code block and inline code\n\n```typescript\n// TODO: Fenced code blocks need default styles\nconst name = 'Adam'\n```\n\nThis sentence has \\`inlineCode\\` in the middle.\n\n### Emoji\n\n:smile: :rocket: :tada:\n\n### Link\n\n[link](https://github.com)\n\n### Tables\n\n| Column1 | Column2 | Column3 |\n| :--- | :--- | :--- |\n| Content | Content | Content |\n\n### Todo items\n\n- [x] Todo item checked\n- [ ] Another item not checked\n\n---\n\n### Diagrams\n\n#### Mermaid\n\n```mermaid\ngitGraph\n commit\n commit\n branch develop\n checkout develop\n commit\n commit\n checkout main\n merge develop\n commit\n commit\n```\n\n#### Plantuml\n\n```plantuml\nBob -> Alice : hello\n```\n\n#### DOT\n\n```dot\ndigraph example1 {\n 1 -> 2 -> { 4, 5 };\n 1 -> 3 -> { 6, 7 };\n}\n```\n",
parent_document_id: null,
slug: 'markdown',
title: 'markdown',
Expand Down
57 changes: 57 additions & 0 deletions src/assets/_mixins.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@mixin icon-button {
align-items: center;
background-color: var(--kui-color-background-transparent, $kui-color-background-transparent);
border: var(--kui-border-width-20, $kui-border-width-20) solid
var(--kui-color-border-transparent, $kui-color-border-transparent);
border-radius: var(--kui-border-radius-30, $kui-border-radius-30);
color: var(--kui-color-text-neutral, $kui-color-text-neutral);
cursor: pointer;
display: inline-flex;
font-family: var(--kui-font-family-text, $kui-font-family-text);
font-size: var(--kui-font-size-30, $kui-font-size-30);
font-weight: var(--kui-font-weight-semibold, $kui-font-weight-semibold);
gap: var(--kui-space-30, $kui-space-30);
justify-content: center;
padding: var(--kui-space-10, $kui-space-10);
// Remove tap color highlight on mobile Safari
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
text-decoration: none;
transition:
background-color 0.2s ease-in-out,
color 0.2s ease-in-out,
border-color 0.2s ease-in-out;
user-select: none;
vertical-align: middle;
white-space: nowrap;

&:hover:not(:disabled):not(:focus):not(:active) {
background-color: var(--kui-color-background-neutral-weaker, $kui-color-background-neutral-weaker);
}

&:focus {
background-color: var(--kui-color-background-neutral-weaker, $kui-color-background-neutral-weaker);
}

&:active {
background-color: var(--kui-color-background-neutral-weak, $kui-color-background-neutral-weak);
}

&:disabled,
&[disabled] {
background-color: var(--kui-color-background-disabled, $kui-color-background-disabled);
box-shadow: none;
color: var(--kui-color-text-disabled, $kui-color-text-disabled);
cursor: not-allowed;
}

&:focus,
&:active,
&:focus-visible {
outline: none;
}

&:focus-visible {
// Same as $kui-shadow-focus with 2px instead of 4px
box-shadow: 0px 0px 0px 2px rgba(0, 68, 244, 0.2);
}
}
51 changes: 37 additions & 14 deletions src/components/MarkdownContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ watch(() => props.content, (content: string): void => {
</script>

<style lang="scss" scoped>
@import "../assets/mixins";

// Computed component variables
$header-anchor-offset-top: calc(var(--kui-space-50, $kui-space-50) + 2px);

Expand All @@ -37,28 +39,29 @@ $header-anchor-offset-top: calc(var(--kui-space-50, $kui-space-50) + 2px);
font-weight: var(--kui-font-weight-regular, $kui-font-weight-regular);
line-height: var(--kui-line-height-40, $kui-line-height-40);
margin: 0;
// max-width: 900px;
padding: 0 var(--kui-space-70, $kui-space-70);
width: calc(100% - (#{$kui-space-70} * 2)); // 100% width minus 2x padding
word-wrap: break-word;

:deep() {
font-size: $kui-font-size-40;
line-height: $kui-line-height-40;
font-size: var(--kui-font-size-40, $kui-font-size-40);
line-height: var(--kui-line-height-40, $kui-line-height-40);

// Adjust h2-66 tags for scroll-to margin & padding
// Exclude the h1 header
h2, h3, h4, h5, h6 {
margin-top: -$kui-space-20;
padding-top: $kui-space-50;
margin-top: calc(var(--kui-space-20, $kui-space-20) * -1);
padding-top: var(--kui-space-50, $kui-space-50);
position: relative;

a.header-anchor {
font-size: $kui-font-size-30;
font-size: var(--kui-font-size-30, $kui-font-size-30);
left: 0;
line-height: 1;
margin-left: -$kui-space-60;
margin-left: calc(var(--kui-space-60, $kui-space-60) * -1);
opacity: 0;
padding-right: 4px;
padding-right: var(--kui-space-20, $kui-space-20);
position: absolute;
text-decoration: none;
top: $header-anchor-offset-top;
Expand All @@ -80,22 +83,42 @@ $header-anchor-offset-top: calc(var(--kui-space-50, $kui-space-50) + 2px);
// task list
.contains-task-list {
list-style-type: none;
padding-left: $kui-space-0;
padding-left: var(--kui-space-0, $kui-space-0);
}

// inline code
code {
font-family: $kui-font-family-code;
font-family: var(--kui-font-family-code, $kui-font-family-code);
}

// code blocks
pre,
code {
white-space: pre;
// width: 100%;
word-break: normal;
word-spacing: normal;
word-wrap: normal;
}

pre {
border-radius: $kui-border-radius-40;
font-family: $kui-font-family-code;
font-size: $kui-font-size-30;
line-height: $kui-line-height-30;
border: var(--kui-border-width-10, $kui-border-width-10) solid var(--kui-color-border, $kui-color-border);
border-radius: var(--kui-border-radius-40, $kui-border-radius-40);
font-family: var(--kui-font-family-code, $kui-font-family-code);
font-size: var(--kui-font-size-30, $kui-font-size-30);
line-height: var(--kui-line-height-50, $kui-line-height-50);
margin: var(--kui-space-0, $kui-space-0);
overflow-wrap: break-word;
white-space: pre-wrap;
overflow-x: auto;
padding: var(--kui-space-70, $kui-space-70);
}

// Styles for fenced code block copy button in `src/composables/useMarkdownIt.ts`
.kong-markdown-code-block-copy {
@include icon-button;
position: absolute;
right: var(--kui-space-40, $kui-space-40);
top: var(--kui-space-40, $kui-space-40);
}

.line.highlighted {
Expand Down
64 changes: 55 additions & 9 deletions src/components/MarkdownUi.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import MarkdownToolbar from '@/components/toolbar/MarkdownToolbar.vue'
import MarkdownContent from '@/components/MarkdownContent.vue'
import composables from '@/composables'
import { TEXTAREA_ID_INJECTION_KEY, MODE_INJECTION_KEY, EDITABLE_INJECTION_KEY, FULLSCREEN_INJECTION_KEY, HTML_PREVIEW_INJECTION_KEY } from '@/injection-keys'
import { EDITOR_DEBOUNCE_TIMEOUT, TOOLBAR_HEIGHT } from '@/constants'
import { EDITOR_DEBOUNCE_TIMEOUT, TOOLBAR_HEIGHT, NEW_LINE_CHARACTER } from '@/constants'
import { v4 as uuidv4 } from 'uuid'
import type { MarkdownMode, InlineFormat, MarkdownTemplate, TextAreaInputEvent } from '@/types'
import formatHtml from 'html-format'
Expand Down Expand Up @@ -108,11 +108,16 @@ const props = defineProps({
default: 300,
validator: (height: number): boolean => height >= 100,
},
/** When the editor is in fullscreen mode, the top offset, in pixels */
/** When the editor is in fullscreen, the top offset, in pixels */
fullscreenOffsetTop: {
type: Number,
default: 0,
},
/** The z-index of the position:fixed container when in fullscreen. */
fullscreenZIndex: {
type: Number,
default: 1001,
},
})

const emit = defineEmits<{
Expand Down Expand Up @@ -220,7 +225,7 @@ const htmlPreview = ref<boolean>(false)
// If the htmlPreview is enabled, pass the generated HTML through the markdown renderer and output the syntax-highlighted result
watchEffect(() => {
if (htmlPreview.value) {
markdownPreviewHtml.value = md.value?.render('```html\n' + formatHtml(markdownHtml.value, ' '.repeat(props.tabSize)) + '\n```')
markdownPreviewHtml.value = md.value?.render('```html' + NEW_LINE_CHARACTER + formatHtml(markdownHtml.value, ' '.repeat(props.tabSize)) + NEW_LINE_CHARACTER + '```')
}
})

Expand All @@ -237,6 +242,22 @@ const updateMermaid = async () => {
}
}

/** Copy the contents of the code block to the clipboard */
const copyCodeBlock = async (e: any): Promise<void> => {
try {
e.preventDefault()
if (navigator?.clipboard?.writeText) {
const copyText = e.target?.dataset?.copytext || ''
if (copyText) {
await navigator.clipboard.writeText(copyText)
e?.target?.blur()
}
}
} catch (err) {
console.warn('Could not copy text to clipboard', err)
}
}

// When the textarea `input` event is triggered, or "faked" by other editor methods, update the Vue refs and rendered markdown
const onContentEdit = (event: TextAreaInputEvent, emitEvent = true): void => {
// Update the ref immediately
Expand All @@ -250,16 +271,33 @@ const debouncedUpdateContent = debounce(async (emitEvent = true): Promise<void>
// Update the output
markdownHtml.value = getHtmlFromMarkdown(rawMarkdown.value)

await nextTick() // **MUST** await nextTick for the virtual DOM to refresh again

updateCodeCopyClickEvents(true)

// Emit the updated content if `emitEvent` is not false
if (emitEvent) {
emit('update:modelValue', rawMarkdown.value)
}

// Re-render any `.mermaid` containers
await nextTick() // **MUST** await nextTick for the virtual DOM to refresh

await updateMermaid()
}, EDITOR_DEBOUNCE_TIMEOUT)

const updateCodeCopyClickEvents = (enable = true): void => {
// Bind click events to code copy blocks
Array.from([...document.querySelectorAll(`#${componentContainerId.value} .kong-markdown-code-block-copy[data-copytext]`)]).forEach((el: Element) => {
if (enable) {
el.removeEventListener('click', copyCodeBlock)
el.addEventListener('click', copyCodeBlock)
} else {
el.removeEventListener('click', copyCodeBlock)
}
})
}

/**
* Emulate an `input` event when injecting content into the textarea
* @param {boolean} emitEvent Should the `onContentEdit` function emit the `update:modelValue` event. Should be false when this event is triggered by `props.modelValue` changes.
Expand All @@ -273,6 +311,8 @@ const emulateInputEvent = (emitEvent = true): void => {

// Trigger the update
onContentEdit(event, emitEvent)

updateCodeCopyClickEvents(true)
}

// Initialize rawMarkdown.value with the props.modelValue content
Expand Down Expand Up @@ -338,13 +378,19 @@ onMounted(async () => {
})
}

// Must await to let virtual DOM cycle
await nextTick()
if (currentMode.value === 'split') {
await nextTick()
// Synchronize the scroll containers
initializeSyncScroll()
}
})

onUnmounted(() => {
// Remove scrolling event listeners
destroySyncScroll()

// Unbind click events
updateCodeCopyClickEvents(false)
})

// Calculate the max height of the `.markdown-panes` when fullscreen is true. 100vh, minus the toolbar height, minus 10px padding.
Expand All @@ -360,14 +406,14 @@ const markdownEditorMaxHeight = computed((): string => `${props.editorMaxHeight}
width: 100%;

@media (min-width: $kui-breakpoint-phablet) {
gap: $kui-space-0;
gap: var(--kui-space-0, $kui-space-0);
}

.markdown-panes {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: $kui-space-40;
gap: var(--kui-space-40, $kui-space-40);
width: 100%;

@media (min-width: $kui-breakpoint-phablet) {
Expand All @@ -393,12 +439,12 @@ const markdownEditorMaxHeight = computed((): string => `${props.editorMaxHeight}
height: 100%;
left: 0;
margin-top: v-bind('fullscreenOffsetTop');
padding: $kui-space-0 $kui-space-40 $kui-space-40;
padding: var(--kui-space-0, $kui-space-0) var(--kui-space-40, $kui-space-40) var(--kui-space-40, $kui-space-40);
position: fixed;
right: 0;
top: 0;
width: 100%;
z-index: 1001;
z-index: v-bind('$props.fullscreenZIndex');

.markdown-panes {
height: v-bind('fullscreenMarkdownPanesHeight');
Expand Down
Loading