Skip to content

Commit

Permalink
feat: toolbar updates (#3)
Browse files Browse the repository at this point in the history
* feat: ordered list

* chore: new line character

* chore: tokens

* chore: tokens

* refactor: header link

* feat: copy code block (needs init)

* chore: cleanup

* fix: copy code

* fix: rename component
  • Loading branch information
adamdehaven authored Dec 22, 2023
1 parent 63a1708 commit 14a2976
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 137 deletions.
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

0 comments on commit 14a2976

Please sign in to comment.