Skip to content
Open
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
109 changes: 66 additions & 43 deletions src/components/Block.vue
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
<template>
<div class="group flex w-full rounded"
<div
:class="{
// Add top margin for headings
'pt-12 first:pt-0': block.type === BlockType.H1,
'pt-4 first:pt-0': block.type === BlockType.H2,
}">
<div class="h-full pl-4 pr-2 text-center cursor-pointer transition-all duration-150 text-neutral-300 flex"
:class="{
'invisible': props.readonly,
'py-3.5': block.type === BlockType.H1,
'py-3': block.type === BlockType.H2,
'py-2.5': block.type === BlockType.H3,
'py-1.5': ![BlockType.H1, BlockType.H2, BlockType.H3].includes(block.type),
}">
<Tooltip value="<span class='text-neutral-400'><span class='text-white'>Click</span> to delete block</span>">
<v-icon name="hi-trash" @click="emit('deleteBlock')"
class="w-6 h-6 hover:bg-neutral-100 hover:text-neutral-400 p-0.5 rounded group-hover:opacity-100 opacity-0" />
</Tooltip>
<Tooltip value="<span class='text-neutral-400'><span class='text-white'>Click</span> to add block below</span>">
<v-icon name="hi-plus" @click="emit('newBlock')"
class="w-6 h-6 hover:bg-neutral-100 hover:text-neutral-400 p-0.5 rounded group-hover:opacity-100 opacity-0" />
</Tooltip>
<BlockMenu ref="menu"
@setBlockType="setBlockType"
:blockTypes="props.block.details.blockTypes || props.blockTypes"
}"
>
<div class="group relative w-full rounded mr-32">
<div
class="h-full absolute min-h-[2rem] top-1/2 pl-4 pr-2 text-center cursor-pointer transition-opacity duration-150 text-neutral-300 z-10 flex -translate-y-1/2 -left-24"
:class="[
{
'invisible': props.readonly,
'py-3.5': block.type === BlockType.H1,
'py-3': block.type === BlockType.H2,
'py-2.5': block.type === BlockType.H3,
'py-1.5': ![BlockType.H1, BlockType.H2, BlockType.H3].includes(block.type),
}
]"
>
<Tooltip value="<span class='text-neutral-400'><span class='text-white'>Click</span> to delete block</span>">
<v-icon name="hi-trash" @click="emit('deleteBlock')"
class="w-6 h-6 hover:bg-neutral-100 hover:text-neutral-400 p-0.5 rounded group-hover:opacity-100 opacity-0" />
</Tooltip>
<Tooltip value="<span class='text-neutral-400'><span class='text-white'>Click</span> to add block below</span>">
<v-icon name="hi-plus" @click="emit('newBlock')"
class="w-6 h-6 hover:bg-neutral-100 hover:text-neutral-400 p-0.5 rounded group-hover:opacity-100 opacity-0" />
</Tooltip>
<BlockMenu ref="menu"
@setBlockType="setBlockType"
:blockTypes="props.block.details.blockTypes || props.blockTypes"
/>
</div>
<div class="w-full relative" :class="{ 'px-0': block.type !== BlockType.Divider }">
</div>
<div
class="w-full relative list-marker"
:data-index="listIndex"
:class="{
'px-0': block.type !== BlockType.Divider,
'pl-9': [BlockType.UnorderedList, BlockType.OrderedList].includes(block.type),
'ordered-list': block.type === BlockType.OrderedList,
'unordered-list': block.type === BlockType.UnorderedList,
}"
>
<!-- Actual content -->
<component :is="BlockComponents[props.block.type]" ref="content"
:block="block" :readonly="props.readonly"
@keydown="keyDownHandler"
@keyup="parseMarkdown" />
<component :is="BlockComponents[props.block.type]" ref="content"
:block="block" :readonly="props.readonly"
@keydown="keyDownHandler"
@keyup="parseMarkdown"
/>
</div>
</div>
</div>
</template>
Expand Down Expand Up @@ -60,6 +77,10 @@ const props = defineProps({
type: Boolean,
default: false,
},
listIndex: {
type: Number,
default: null
}
})

const emit = defineEmits([
Expand Down Expand Up @@ -155,7 +176,11 @@ function keyDownHandler (event:KeyboardEvent) {
const selection = window.getSelection()
if (!(menu.value && menu.value.open) && atFirstChar() && selection && selection.anchorOffset === 0 && !props.readonly) {
event.preventDefault()
emit('merge')
if (props.block.type === BlockType.UnorderedList || props.block.type === BlockType.OrderedList) {
setBlockType(BlockType.Text, 0)
} else {
emit('merge')
}
}
} else if (event.key === 'Enter') {
event.preventDefault()
Expand All @@ -166,7 +191,7 @@ function keyDownHandler (event:KeyboardEvent) {
}

function isContentBlock () {
return [BlockType.Text, BlockType.Quote, BlockType.H1, BlockType.H2, BlockType.H3].includes(props.block.type)
return [BlockType.Text, BlockType.Quote, BlockType.H1, BlockType.H2, BlockType.H3, BlockType.OrderedList, BlockType.UnorderedList].includes(props.block.type)
}

const content = ref<any>(null)
Expand Down Expand Up @@ -379,7 +404,7 @@ function getCaretPosWithoutTags () {
function setCaretPos (caretPos:number) {
const innerContent = getInnerContent()
if (innerContent) {
if (isTextBlock(props.block.type)) {
if (isTextBlock(props.block.type)) {
let offsetNode, offset = 0
const numNodes = (content.value as any).$el.firstChild.firstChild.childNodes.length
for (const [i, node] of (content.value as any).$el.firstChild.firstChild.childNodes.entries()) {
Expand Down Expand Up @@ -439,12 +464,14 @@ function parseMarkdown (event:KeyboardEvent) {
const textContent = getTextContent()
if(!textContent) return

const markdownRegexpMap = {
const markdownRegexpMap: Record<string, RegExp> = {
[BlockType.OrderedList]: /^1.\s(.*)$/,
[BlockType.UnorderedList]: /^-\s(.*)$/,
[BlockType.H1]: /^#\s(.*)$/,
[BlockType.H2]: /^##\s(.*)$/,
[BlockType.H3]: /^###\s(.*)$/,
[BlockType.Quote]: /^>\s(.*)$/,
[BlockType.Divider]: /^---\s$/
[BlockType.Divider]: /^---\s$/,
}

const handleMarkdownContent = (blockType: keyof typeof markdownRegexpMap) => {
Expand All @@ -457,18 +484,14 @@ function parseMarkdown (event:KeyboardEvent) {
})
}

const blockTypes = [BlockType.OrderedList, BlockType.UnorderedList, BlockType.H1, BlockType.H2, BlockType.H3, BlockType.Quote, BlockType.Divider]
const matchedBlockType = blockTypes.find((type) => textContent.match(markdownRegexpMap[type]))

if (textContent.match(markdownRegexpMap[BlockType.H1]) && event.key === ' ') {
handleMarkdownContent(BlockType.H1)
} else if (textContent.match(markdownRegexpMap[BlockType.H2]) && event.key === ' ') {
handleMarkdownContent(BlockType.H2)
} else if (textContent.match(markdownRegexpMap[BlockType.H3]) && event.key === ' ') {
handleMarkdownContent(BlockType.H3)
} else if (textContent.match(markdownRegexpMap[BlockType.Quote]) && event.key === ' ') {
handleMarkdownContent(BlockType.Quote)
} else if (textContent.match(markdownRegexpMap[BlockType.Divider]) && event.key === ' ') {
handleMarkdownContent(BlockType.Divider)
props.block.details.value = ''
if (matchedBlockType && event.key === ' ') {
handleMarkdownContent(matchedBlockType)
if (matchedBlockType === BlockType.Divider) {
props.block.details.value = ''
}
} else if (event.key === '/') {
if (menu.value && !menu.value.open) {
menu.value.open = true
Expand Down
7 changes: 5 additions & 2 deletions src/components/BlockMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</div>
<div v-show="open" class="block-menu">
<div ref="menu"
class="w-[10rem] lg:w-[12rem] xl:w-[16rem] absolute z-10 shadow-block rounded py-1 text-neutral-700 text-sm right-full bg-white max-h-[24rem] overflow-auto focus-visible:outline-none top-0">
class="w-[10rem] lg:w-[12rem] xl:w-[16rem] absolute z-20 shadow-block rounded py-1 text-neutral-700 text-sm right-full bg-white max-h-[24rem] overflow-auto focus-visible:outline-none top-0">
<div class="text-left divide-y">
<!-- Search term -->
<div v-if="searchTerm" class="block-menu-search px-2 py-2 flex gap-2 w-full">
Expand All @@ -20,7 +20,10 @@
</div>
<!-- Turn into another block like Text, Heading or Divider -->
<div class="px-2 py-2" v-if="options.filter(option => option.type === 'Turn into').length">
<div class="px-2 pb-2 font-semibold uppercase text-xs text-neutral-400">Turn into</div>
<div class="flex justify-between px-2 pb-2 text-xs">
<div class="font-semibold uppercase text-neutral-500">Turn into</div>
<div class="text-neutral-300">Esc to Close</div>
</div>
<div v-for="option, i in options.filter(option => option.type === 'Turn into')"
class="px-2 py-1 rounded flex items-center gap-2"
:class="[active === (i + options.filter(option => option.type !== 'Turn into').length) ? 'bg-neutral-100' : '']"
Expand Down
42 changes: 32 additions & 10 deletions src/components/Lotion.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div class="lotion w-[65ch] mx-auto my-24 font-sans text-base" v-if="props.page" ref="editor">
<div class="lotion w-[75ch] mx-auto my-24 font-sans text-base" v-if="props.page" ref="editor">
<h1 id="title" ref="title" :contenteditable="!props.readonly" spellcheck="false" data-ph="Untitled"
@keydown.enter.prevent="splitTitle"
@keydown.down="blockElements[0]?.moveToFirstLine(); scrollIntoView();"
Expand All @@ -9,12 +9,17 @@
{{ props.page.name || '' }}
</h1>
<draggable id="blocks" tag="div" :list="props.page.blocks" handle=".handle"
v-bind="dragOptions" class="-ml-24 space-y-2 pb-4">
v-bind="dragOptions" class="space-y-2 pb-4">
<transition-group type="transition">
<BlockComponent :block="block" v-for="block, i in props.page.blocks" :key="i" :id="'block-'+block.id"
<BlockComponent
v-for="block, i in props.page.blocks"
:key="block.id"
:id="'block-'+block.id"
:block="block"
:blockTypes="props.blockTypes"
:readonly="props.readonly"
:ref="el => blockElements[i] = (el as unknown as typeof Block)"
:listIndex="listIndex(i)"
:ref="(el: unknown) => blockElements[i] = (el as unknown as typeof Block)"
@deleteBlock="deleteBlock(i)"
@newBlock="insertBlock(i)"
@moveToPrevChar="blockElements[i-1]?.moveToEnd(); scrollIntoView();"
Expand All @@ -23,15 +28,15 @@
@moveToNextLine="blockElements[i+1]?.moveToFirstLine(); scrollIntoView();"
@merge="merge(i)"
@split="split(i)"
@setBlockType="type => setBlockType(i, type)"
/>
@setBlockType="(type: BlockType) => setBlockType(i, type)"
/>
</transition-group>
</draggable>
</div>
</template>

<script setup lang="ts">
import { ref, onBeforeUpdate, PropType } from 'vue'
import { ref, onBeforeUpdate, PropType, computed } from 'vue'
import { VueDraggableNext as draggable } from 'vue-draggable-next'
import { v4 as uuidv4 } from 'uuid'
import { Block, BlockType, isTextBlock, availableBlockTypes } from '@/utils/types'
Expand Down Expand Up @@ -175,10 +180,10 @@ function handleMoveToPrevLine (blockIdx:number) {
scrollIntoView()
}

function insertBlock (blockIdx: number) {
function insertBlock (blockIdx: number, type = BlockType.Text) {
const newBlock = {
id: uuidv4(),
type: BlockType.Text,
type,
details: {
value: '',
},
Expand Down Expand Up @@ -280,7 +285,8 @@ function mergeTitle (blockIdx:number = 0) {

function split (blockIdx: number) {
const caretPos = blockElements.value[blockIdx].getCaretPos()
insertBlock(blockIdx)
const currentBlockType = props.page.blocks[blockIdx].type
insertBlock(blockIdx, currentBlockType)
const blockTypeDetails = availableBlockTypes.find(blockType => blockType.blockType === props.page.blocks[blockIdx].type)
if (!blockTypeDetails) return
if (blockTypeDetails.canSplit) {
Expand All @@ -303,4 +309,20 @@ function splitTitle () {
props.page.name = titleString.slice(0, caretPos)
props.page.blocks[0].details.value = titleString.slice(caretPos)
}

function listIndex(i: number) {
let index = 0
let previousBlockType = null
for (let j = 0; j <= i; j++) {
if (props.page.blocks[j].type !== previousBlockType) {
index = 0
}
previousBlockType = props.page.blocks[j].type
if (props.page.blocks[j].type === BlockType.OrderedList) {
index++
}
}
return index
}

</script>
20 changes: 10 additions & 10 deletions src/components/Main.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,43 +50,43 @@ const page = ref({
id: uuidv4(),
type: BlockType.Text,
details: {
value: '1. Hover on the left of each line for quick actions'
value: 'Hover on the left of each line for quick actions'
},
}, {
id: uuidv4(),
type: BlockType.Text,
details: {
value: '2. Click on the + button to add a new line'
value: 'Click on the + button to add a new line'
},
}, {
id: uuidv4(),
type: BlockType.Text,
details: {
value: '3. Drag the ⋮⋮ button to reorder'
value: 'Drag the ⋮⋮ button to reorder'
},
}, {
id: uuidv4(),
type: BlockType.Text,
details: {
value: '4. Click the trash icon to delete this block'
value: 'Click the trash icon to delete this block'
},
}, {
id: uuidv4(),
type: BlockType.Text,
type: BlockType.OrderedList,
details: {
value: '5. **Bold** and *italicize* using markdown e.g. \\*\\*bold\\*\\* and \\*italics\\*'
value: '**Bold** and *italicize* using markdown e.\\*\\*bold\\*\\* and \\*italics\\*'
},
}, {
id: uuidv4(),
type: BlockType.Text,
type: BlockType.OrderedList,
details: {
value: '6. Add headers and dividers with \'#\', \'##\' or \'---\' followed by a space'
value: 'Add headers and dividers with \'#\', \'##\' or \'---\' followed by a space'
},
}, {
id: uuidv4(),
type: BlockType.Text,
type: BlockType.OrderedList,
details: {
value: '7. Type \'/\' for a menu to quickly switch blocks and search by typing'
value: 'Type \'/\' for a menu to quickly switch blocks and search by typing'
},
},]
})
Expand Down
5 changes: 2 additions & 3 deletions src/components/blocks/DividerBlock.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
<template>
<div class="w-full py-0 h-[1px] bg-neutral-300 mt-[1.2rem]">
</div>
<div class="w-full py-0 h-[1px] bg-neutral-300 mt-5"></div>
</template>

<script setup lang="ts">
import { PropType } from 'vue'
import { Block } from '@/utils/types'

const props = defineProps({
defineProps({
block: {
type: Object as PropType<Block>,
required: true,
Expand Down
2 changes: 1 addition & 1 deletion src/components/blocks/HeadingBlock.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import { ref, PropType } from 'vue'
import { Block, BlockType } from '@/utils/types'

const headingConfig = {
const headingConfig: Record<string, { placeholder: string, class: string } | null> = {
[BlockType.H1]: {
placeholder: 'Heading 1',
class: 'text-4xl font-semibold',
Expand Down
14 changes: 14 additions & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,17 @@ pre.lotion-md {
pointer-events: none;
height: 0;
}

.list-marker.ordered-list::before {
content: attr(data-index) ".";
position: absolute;
left: 0;
@apply py-1.5 text-neutral-600 left-0 absolute w-7 pr-1 text-right tabular-nums;
}

.list-marker.unordered-list::before {
content: "•";
position: absolute;
left: 0;
@apply py-1.5 text-neutral-600 left-0 absolute w-6 text-right tabular-nums;
}
8 changes: 6 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
BiTypeH2,
BiTypeH3,
BiHr,
BiQuote
BiQuote,
BiListOl,
BiListUl,
} from "oh-vue-icons/icons"
import App from './App.vue'
import './index.css'
Expand All @@ -25,7 +27,9 @@ addIcons(
BiTypeH2,
BiTypeH3,
BiHr,
BiQuote
BiQuote,
BiListOl,
BiListUl,
)

const app = createApp(App)
Expand Down
Loading