Skip to content
This repository has been archived by the owner on May 2, 2024. It is now read-only.

Commit

Permalink
Grand refactoring 2
Browse files Browse the repository at this point in the history
  • Loading branch information
cosmicproc committed Mar 31, 2024
1 parent c4f61cc commit f05fd5d
Show file tree
Hide file tree
Showing 14 changed files with 1,126 additions and 593 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
build
build
test-results
4 changes: 2 additions & 2 deletions README.md
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.
1,063 changes: 857 additions & 206 deletions package-lock.json

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,31 @@
"test:integration": "playwright test"
},
"devDependencies": {
"@fortawesome/free-brands-svg-icons": "^6.5.1",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/svelte-fontawesome": "^0.2.2",
"@playwright/test": "^1.28.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.10",
"@tiptap/core": "^2.2.4",
"@tiptap/extension-bubble-menu": "^2.2.4",
"@tiptap/extension-code-block-lowlight": "^2.2.4",
"@tiptap/extension-typography": "^2.2.4",
"@tiptap/pm": "^2.2.4",
"@tiptap/starter-kit": "^2.2.4",
"@types/eslint": "^8.56.0",
"@types/markdown-it": "^13.0.7",
"@types/node": "^20.11.28",
"@types/turndown": "^5.0.4",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.18",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"highlight.js": "^11.9.0",
"markdown-it": "^14.0.0",
"lowlight": "^3.1.0",
"lucide-svelte": "^0.363.0",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.1.2",
Expand All @@ -42,8 +47,12 @@
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.4.1",
"turndown": "^7.1.3",
"typescript": "^5.0.0",
"vite": "^5.0.3"
},
"type": "module"
"type": "module",
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.13.2"
}
}
281 changes: 83 additions & 198 deletions src/lib/components/Editor.svelte
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"
/>
Loading

0 comments on commit f05fd5d

Please sign in to comment.