This repository has been archived by the owner on May 2, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c4f61cc
commit f05fd5d
Showing
14 changed files
with
1,126 additions
and
593 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,4 +8,5 @@ node_modules | |
!.env.example | ||
vite.config.js.timestamp-* | ||
vite.config.ts.timestamp-* | ||
build | ||
build | ||
test-results |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# Glass Cursor | ||
|
||
Glass Cursor is a free and open source simple web-based markdown note-taking app. It can be used for | ||
writing text for any purpose with markdown. It's designed to be simple but yet useful. With its | ||
Glass Cursor is a free and open source simple web-based note-taking app. It can be used for | ||
writing text for any purpose. It's designed to be simple but yet useful. With its | ||
elegant user interface, you can focus on your notes without any friction. |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,215 +1,100 @@ | ||
<script lang="ts"> | ||
import { | ||
currentNote, | ||
notes, | ||
previewExtended, | ||
sourceExtended, | ||
} from '$lib/stores'; | ||
import { faMarkdown } from '@fortawesome/free-brands-svg-icons'; | ||
import { | ||
faAlignLeft, | ||
faArrowLeft, | ||
faArrowRight, | ||
faQuestion, | ||
} from '@fortawesome/free-solid-svg-icons'; | ||
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome'; | ||
import hljs from 'highlight.js'; | ||
import 'highlight.js/styles/monokai.css'; | ||
import markdownit from 'markdown-it'; | ||
import { afterUpdate, beforeUpdate } from 'svelte'; | ||
import { currentNote, notes } from '$lib/stores'; | ||
import { debounce } from '$lib/utils'; | ||
import { Editor } from '@tiptap/core'; | ||
import BubbleMenu from '@tiptap/extension-bubble-menu'; | ||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'; | ||
import Typography from '@tiptap/extension-typography'; | ||
import StarterKit from '@tiptap/starter-kit'; | ||
import 'highlight.js/styles/github-dark.css'; | ||
import { common, createLowlight } from 'lowlight'; | ||
import { beforeUpdate, onDestroy, onMount } from 'svelte'; | ||
import EditorMenu from './EditorMenu.svelte'; | ||
const md = markdownit({ | ||
linkify: true, | ||
typographer: true, | ||
breaks: true, | ||
highlight: function (str: string, lang: string) { | ||
if (lang && hljs.getLanguage(lang)) { | ||
try { | ||
return hljs.highlight(str, { language: lang }).value; | ||
} catch (__) { | ||
/* empty */ | ||
} | ||
} | ||
let editorElement: HTMLElement; | ||
let menuElement: HTMLElement; | ||
let editor: Editor; | ||
try { | ||
return hljs.highlightAuto(str).value; | ||
} catch (err) { | ||
/* empty */ | ||
} | ||
const defaultText = | ||
'<p>Choose a note from the <b>side panel</b> to start typing.<br/> You can toggle the visibility of the side panel by clicking the vertical line at the left.</p>'; | ||
return ''; | ||
}, | ||
onMount(() => { | ||
const index = $notes.findIndex( | ||
(o: { id: number }) => o.id === $currentNote, | ||
); | ||
editor = new Editor({ | ||
element: editorElement, | ||
editorProps: { | ||
attributes: { | ||
class: 'prose dark:prose-invert focus:outline-none w-10/12 md:w-3/4 max-w-none', | ||
}, | ||
}, | ||
extensions: [ | ||
StarterKit.configure({ | ||
codeBlock: false, | ||
}), | ||
Typography, | ||
CodeBlockLowlight.configure({ | ||
lowlight: createLowlight(common), | ||
}), | ||
BubbleMenu.configure({ | ||
element: menuElement, | ||
}), | ||
], | ||
content: $notes[index]?.content || defaultText, | ||
editable: !!$notes[index]?.content, | ||
onTransaction: () => { | ||
editor = editor; | ||
}, | ||
}); | ||
}); | ||
let selectionStart: number; | ||
let selectionEnd: number; | ||
let source: HTMLTextAreaElement; | ||
let preview: HTMLElement; | ||
let sourceContent: string; | ||
let previewContent: string = ''; | ||
onDestroy(() => { | ||
if (editor) { | ||
editor.destroy(); | ||
} | ||
}); | ||
$: previewContent = md.render(sourceContent); | ||
beforeUpdate(() => { | ||
saveNote(); | ||
}); | ||
currentNote.subscribe((value: number) => { | ||
if (value !== -1) { | ||
const index = $notes.findIndex( | ||
(o: { id: number }) => o.id === value, | ||
); | ||
sourceContent = $notes[index]?.content; | ||
} else { | ||
sourceContent = ''; | ||
if (editor) { | ||
if (value !== -1) { | ||
const index = $notes.findIndex( | ||
(o: { id: number }) => o.id === value, | ||
); | ||
editor.commands.setContent($notes[index]?.content); | ||
editor.setEditable(true); | ||
} else { | ||
editor.commands.setContent(defaultText); | ||
editor.setEditable(false); | ||
} | ||
} | ||
}); | ||
let autoSynced = false; | ||
function syncScroll(this: HTMLElement) { | ||
let first: HTMLElement | HTMLTextAreaElement = preview; | ||
let second: HTMLElement | HTMLTextAreaElement = source; | ||
if (this === source) { | ||
first = source; | ||
second = preview; | ||
} else if (this !== preview) { | ||
return; | ||
} | ||
const firstHeight = first.scrollHeight - first.clientHeight; | ||
const secondHeight = second.scrollHeight - second.clientHeight; | ||
if (!autoSynced) { | ||
autoSynced = true; | ||
second.scrollTop = (first.scrollTop / firstHeight) * secondHeight; | ||
} else { | ||
autoSynced = false; | ||
function saveNote() { | ||
if (editor) { | ||
const index = $notes.findIndex( | ||
(o: { id: number }) => o.id === $currentNote, | ||
); | ||
const updated = $notes; | ||
if (updated[index]) { | ||
updated[index].content = editor.getHTML(); | ||
} | ||
notes.set(updated); | ||
} | ||
} | ||
function handleKeydown(this: HTMLTextAreaElement, event: KeyboardEvent) { | ||
if (event.key !== 'Tab') return; | ||
event.preventDefault(); | ||
({ selectionStart, selectionEnd } = source); | ||
sourceContent = | ||
sourceContent.substring(0, selectionStart) + | ||
'\t' + | ||
sourceContent.substring(selectionEnd); | ||
source.setSelectionRange(selectionStart + 1, selectionEnd + 1); | ||
} | ||
function handleInput() { | ||
const index = $notes.findIndex( | ||
(o: { id: number }) => o.id === $currentNote, | ||
); | ||
const updated = $notes; | ||
updated[index].content = sourceContent; | ||
notes.set(updated); | ||
} | ||
beforeUpdate(() => { | ||
if (source) { | ||
({ selectionStart, selectionEnd } = source); | ||
} | ||
}); | ||
afterUpdate(() => { | ||
source.setSelectionRange(selectionStart, selectionEnd); | ||
source.focus(); | ||
}); | ||
const handleInput = debounce(saveNote, 1000); | ||
</script> | ||
|
||
<div class="flex flex-grow basis-0 flex-col overflow-hidden p-10 lg:flex-row"> | ||
<div | ||
class={`mb-4 flex h-1/2 flex-grow flex-col overflow-hidden rounded-3xl border-2 border-neutral-300 shadow-lg transition-all duration-300 lg:mb-0 lg:h-full lg:w-1/2 dark:border-neutral-500 ${ | ||
$previewExtended && !$sourceExtended | ||
? 'lg:-ml-[110%] lg:mr-24' | ||
: 'lg:mr-2' | ||
}`} | ||
> | ||
<div | ||
class="flex w-full bg-neutral-200 bg-opacity-70 p-4 text-center backdrop-blur dark:bg-neutral-800 dark:bg-opacity-70" | ||
> | ||
<a | ||
href="https://commonmark.org/help/" | ||
title="Help" | ||
class="flex items-center rounded-xl px-2 hover:bg-gray-300 dark:hover:bg-gray-700" | ||
> | ||
<FontAwesomeIcon icon={faQuestion} /> | ||
</a> | ||
<strong class="mx-auto"> | ||
<FontAwesomeIcon icon={faMarkdown} class="align-middle" /> | ||
<span class="ml-1 align-middle">Markdown Editor</span> | ||
</strong> | ||
<button | ||
title={sourceExtended ? 'Shrink' : 'Extend'} | ||
data-testid="resize-editor" | ||
class="invisible cursor-pointer rounded-xl px-2 hover:bg-gray-300 lg:visible dark:hover:bg-gray-700" | ||
on:click={() => { | ||
sourceExtended.set(!$sourceExtended); | ||
}} | ||
> | ||
{#if $sourceExtended} | ||
<FontAwesomeIcon icon={faArrowLeft} /> | ||
{:else} | ||
<FontAwesomeIcon icon={faArrowRight} /> | ||
{/if} | ||
</button> | ||
</div> | ||
<div class="-mt-16 flex h-full w-full flex-grow overflow-auto"> | ||
<textarea | ||
data-testid="editor" | ||
class="w-full resize-none bg-transparent p-6 pb-8 pt-[5.5em] font-mono outline-none" | ||
placeholder={$currentNote === -1 | ||
? 'Choose a note from the left-hand side panel to edit.' | ||
: 'Dump your mind here.'} | ||
disabled={$currentNote === -1} | ||
bind:this={source} | ||
bind:value={sourceContent} | ||
on:scroll={syncScroll} | ||
on:keydown={handleKeydown} | ||
on:input={handleInput} | ||
name="editor" | ||
></textarea> | ||
</div> | ||
</div> | ||
<div | ||
class={`mt-4 flex h-1/2 flex-grow flex-col overflow-hidden rounded-3xl border-2 border-neutral-300 shadow-lg transition-all duration-300 lg:mt-0 lg:h-full lg:w-1/2 dark:border-neutral-500 ${ | ||
$sourceExtended && !$previewExtended | ||
? 'lg:-mr-[110%] lg:ml-24' | ||
: 'lg:ml-2' | ||
}`} | ||
> | ||
<div | ||
class="flex w-full bg-neutral-200 bg-opacity-70 p-4 text-center backdrop-blur dark:bg-neutral-800 dark:bg-opacity-70" | ||
> | ||
<button | ||
title={$previewExtended ? 'Shrink' : 'Extend'} | ||
data-testid="resize-result-text" | ||
class="invisible cursor-pointer rounded-xl px-2 hover:bg-gray-300 lg:visible dark:hover:bg-gray-700" | ||
on:click={() => { | ||
previewExtended.set(!$previewExtended); | ||
}} | ||
> | ||
{#if $previewExtended} | ||
<FontAwesomeIcon icon={faArrowRight} /> | ||
{:else} | ||
<FontAwesomeIcon icon={faArrowLeft} /> | ||
{/if} | ||
</button> | ||
<strong class="mx-auto"> | ||
<FontAwesomeIcon icon={faAlignLeft} class="align-middle" /> | ||
<span class="ml-1 align-middle">Formatted Text</span> | ||
</strong> | ||
<div class="w-[30px]"></div> | ||
</div> | ||
<div | ||
class="-mt-16 overflow-auto px-6 pt-[5.5em]" | ||
bind:this={preview} | ||
on:scroll={syncScroll} | ||
> | ||
<article | ||
data-testid="result-text" | ||
class="prose mx-auto break-words pb-8 dark:prose-invert dark:prose-pre:bg-gray-900" | ||
> | ||
<!-- eslint-disable-next-line svelte/no-at-html-tags --> | ||
{@html previewContent} | ||
</article> | ||
</div> | ||
</div> | ||
</div> | ||
<EditorMenu {editor} bind:menuElement /> | ||
|
||
<div | ||
bind:this={editorElement} | ||
on:input={handleInput} | ||
data-testid="editor" | ||
class="flex max-h-full flex-grow justify-center overflow-scroll py-14" | ||
/> |
Oops, something went wrong.