From 8abd046affd158e69d9c7fdb8bb3366d75c54864 Mon Sep 17 00:00:00 2001 From: miggymofongo Date: Wed, 8 Jan 2025 15:12:26 -0400 Subject: [PATCH] - updated prefilled doc - refactor separated editor syles and schema into separate components --- README.md | 27 +- src/components/schema.ts | 157 ++++++++++++ src/pages/app-about/app-about.ts | 30 ++- src/pages/app-write/app-write.ts | 383 +++++++++++----------------- src/pages/app-write/write-styles.ts | 36 +-- 5 files changed, 351 insertions(+), 282 deletions(-) create mode 100644 src/components/schema.ts diff --git a/README.md b/README.md index 18792b7..a6bbd11 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,31 @@

Aren't you tired of distracting social media platforms? -So many buttons and advertising makes a simple task of reading -and writing notes to your community very draining. You can use +So many buttons and advertising makes a simple task of +reading and composing notes to your community very draining. You can use a next generation social media protocol to transform -your personal website into a micro social media client. +your personal website into a micro social media client +that will clear up space on your phone for media.

-Use a library like ProseMirror, + +

+I'm using ProseMirror, to build a WYSIWYM style rich text editor for visitors -to compose notes with. They can then post it to a set of -programmed relays. +to compose notes with. I'm taking inspiration from tumblr +and medium's text editors to build something minimal and +intuitive that will run easily via browsers.

-

This is an installable personal website that is accessible through chrome-based and firefox browsers. The website is built with PWA Builder and Lit Web Components. It utilizes the Nostr -protocol to fetch profile metadata, short text, and long form notes (event kinds 0, 1, and 30023) from a relay.

+

I scaffolded the project with PWA Builder and am using +Lit to power up my Web Components. The website utilizes +the Nostr protocol to sign in via browser extension to +fetch profile metadata and pull event kinds 0, 1, and 30023 +from a relay.

-

If you're on any chrome-based, firefox or safari browser try visiting the webpage then tapping on the arrow -pointing up in the bottom toolbar. Scroll down a bit to tap on "Add to Home Screen". - +

If you're on any chrome-based, firefox or safari browser try visiting the webpage then tapping on the arrow pointing up in the bottom toolbar. Scroll down a bit to tap on "Add to Home Screen". If you're on a chromium-based browser you should be able to do the same.

diff --git a/src/components/schema.ts b/src/components/schema.ts new file mode 100644 index 0000000..74a9a18 --- /dev/null +++ b/src/components/schema.ts @@ -0,0 +1,157 @@ +// custom-schema.ts +import { Schema, NodeSpec, MarkSpec } from "prosemirror-model"; + +// Define custom nodes +export const nodes = { + text: { + group: "inline", + } as NodeSpec, + star: { + inline: true, + group: "inline", + toDOM() { + return ["star", "⭐"]; + }, + parseDOM: [{ tag: "star" }], + } as NodeSpec, + paragraph: { + group: "block", + content: "inline*", + toDOM() { + return ["p", 0]; + }, + parseDOM: [{ tag: "p" }], + } as NodeSpec, + boring_paragraph: { + group: "block", + content: "text*", + marks: "", + toDOM() { + return ["p", { class: "boring" }, 0]; + }, + parseDOM: [{ tag: "p", priority: 60 }], + } as NodeSpec, + blockquote: { + content: "block+", + group: "block", + parseDOM: [{ tag: "blockquote" }], + toDOM() { + return ["blockquote", 0]; + }, + } as NodeSpec, + horizontal_rule: { + group: "block", + selectable: false, + parseDOM: [{ tag: "hr" }], + toDOM() { + return ["hr"]; + }, + } as NodeSpec, + heading: { + attrs: { level: { default: 1 } }, + content: "inline*", + group: "block", + defining: true, + parseDOM: [ + { tag: "h1", attrs: { level: 1 } }, + { tag: "h2", attrs: { level: 2 } }, + { tag: "h3", attrs: { level: 3 } }, + ], + toDOM(node) { + return ["h" + node.attrs.level, 0]; + }, + } as NodeSpec, + code_block: { + content: "text*", + group: "block", + marks: "", + defining: true, + code: true, + parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], + toDOM() { + return ["pre", ["code", 0]]; + }, + } as NodeSpec, + image: { + inline: true, + attrs: { + src: {}, + alt: { default: null }, + title: { default: null }, + }, + group: "inline", + draggable: true, + parseDOM: [ + { + tag: "img[src]", + getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + alt: dom.getAttribute("alt"), + title: dom.getAttribute("title"), + }; + }, + }, + ], + toDOM(node) { + return ["img", node.attrs]; + }, + } as NodeSpec, + doc: { + content: "block+", + } as NodeSpec, +}; + +// Define custom marks +export const marks = { + shouting: { + toDOM() { + return ["shouting", 0]; + }, + parseDOM: [{ tag: "shouting" }], + } as MarkSpec, + link: { + attrs: { href: {} }, + toDOM(node) { + return ["a", { href: node.attrs.href }, 0]; + }, + parseDOM: [ + { + tag: "a", + getAttrs(dom) { + return { href: dom.getAttribute("href") }; + }, + }, + ], + inclusive: false, + } as MarkSpec, + bold: { + parseDOM: [ + { tag: "strong" }, + { tag: "b", getAttrs: () => null }, + { + style: "font-weight", + getAttrs: (value: string) => (value === "bold" ? null : false), + }, + ], + toDOM() { + return ["strong", 0]; + }, + } as MarkSpec, + emphasis: { + parseDOM: [ + { tag: "em" }, + { tag: "i", getAttrs: () => null }, + { + style: "font-style", + getAttrs: (value: string) => (value === "italic" ? null : false), + }, + ], + toDOM() { + return ["em", 0]; + }, + } as MarkSpec, +}; + +// Export the schema +export const customSchema = new Schema({ nodes, marks }); diff --git a/src/pages/app-about/app-about.ts b/src/pages/app-about/app-about.ts index c5e17a6..9f93cde 100644 --- a/src/pages/app-about/app-about.ts +++ b/src/pages/app-about/app-about.ts @@ -1,4 +1,4 @@ -import { LitElement, html, css } from 'lit'; +import { LitElement, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { styles } from './about-styles'; @@ -11,9 +11,7 @@ export class AppAbout extends LitElement { static styles = [ sharedStyles, styles, - css` - ` ] connectedCallback(): void { @@ -31,10 +29,21 @@ export class AppAbout extends LitElement {

What am I looking at?

- This is a personal micro-social media client that can be installed to - your desktop or smartphone home screen. You can sign in via an extension - to display your profile data and notes from my relay. You - can compose a note using a rich text editor I built with Prosemirror. + This is a personal PWA installable + to any home screen built with web + components, the nostr protocol, + and ProseMirror. + +

What can I do with this?

+

@@ -43,7 +52,12 @@ export class AppAbout extends LitElement {

Look for "Add to Home Screen" in your browser toolbar to install it to your homescreen.

-

What is Nostr?

+

What is ProseMirror

+

ProseMirror is the rich text editor library under the + hood of Tip Tap. It seems overwhelming at first because + it's basically a lego set, but it gives you a cool + amount of control over the end product.

+

What is nostr?

Notes and Other Stuff Transmitted Over Relays is a simple open source social media protocol that enables anybody to implement social media functionalities into their websites.

diff --git a/src/pages/app-write/app-write.ts b/src/pages/app-write/app-write.ts index 2a8786f..9e46305 100644 --- a/src/pages/app-write/app-write.ts +++ b/src/pages/app-write/app-write.ts @@ -1,4 +1,4 @@ -import { LitElement, css, html } from 'lit'; +import { LitElement, html } from 'lit'; import { customElement, query } from 'lit/decorators.js'; import '@shoelace-style/shoelace/dist/components/card/card.js'; @@ -6,122 +6,35 @@ import '@shoelace-style/shoelace/dist/components/card/card.js'; import '@shoelace-style/shoelace/dist/components/button/button.js'; import { styles } from '../../styles/shared-styles'; +import { editorStyles } from './write-styles'; import {EditorState} from 'prosemirror-state' import { Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; -import { Schema } from 'prosemirror-model'; - +import { customSchema } from '../../components/schema'; import { keymap } from 'prosemirror-keymap'; import { toggleMark } from 'prosemirror-commands'; import {undo, redo, history} from 'prosemirror-history' import { baseKeymap } from 'prosemirror-commands'; -/* i begin by creating a custom schema. following the guide -on prosemirror.net, i created a basic schema with a few -types of nodes. - -texts are - -*/ -export const customSchema = new Schema({ - nodes: { - text: { - group: 'inline', - }, - star: { - inline: true, - group: 'inline', - toDOM() { - return ['star', '⭐']; - }, - parseDOM: [{ tag: 'star' }], - }, - paragraph: { - group: 'block', - content: 'inline*', - toDOM() { - return ['p', 0]; - }, - parseDOM: [{ tag: 'p' }], - }, - boring_paragraph: { - group: 'block', - content: 'text*', - marks: '', - toDOM() { - return ['p', { class: 'boring' }, 0]; - }, - parseDOM: [{ tag: 'p', priority: 60 }], - }, - hr: { - group: 'block', - selectable: true, - parseDOM: [{ tag: 'horizontal_rule' }], - toDOM() { - return ['hr', { class: 'horizontal-rule'}] - } - }, - doc: { - content: 'block+', - }, - }, - marks: { - shouting: { - toDOM() { - return ['shouting', 0]; - }, - parseDOM: [{ tag: 'shouting' }], - }, - link: { - attrs: { href: {} }, - toDOM(node) { - return ['a', { href: node.attrs.href }, 0]; - }, - parseDOM: [ - { - tag: 'a', - getAttrs(dom) { - return { href: dom }; - }, - }, - ], - inclusive: false, - }, - bold: { - - }, - emphasis: { - - } - }, - }); - - - - - - - // function for error reports +// function for error reports function errorReport(message: string): void { console.error(message); } - - - - /* COMMANDS - commands are functions that take an editor state - and a dispatch function and returns a boolean - to implement editing actions.*/ +/* COMMANDS +commands are functions that take an editor state +and a dispatch function and returns a boolean +to implement editing actions.*/ /**this command inserts a star by your cursor. it sets * type to the star node, and creates one when * dispatch is provided */ + function insertStar(state: EditorState, dispatch?: (tr: Transaction) => void): boolean { const star = customSchema.nodes.star; const { $from } = state.selection; @@ -140,10 +53,10 @@ function insertStar(state: EditorState, dispatch?: (tr: Transaction) => void): b return true; } - /* function that inserts an
line break -*/ + /* function that inserts an
line break*/ + function insertHR(state: EditorState, dispatch?: (tr: Transaction) => void): boolean { - const hr = customSchema.nodes.hr; // Access the HR node from the schema + const hr = customSchema.nodes.horizontal_rule; // attach the HR node from the schema to a variable if (!hr) { console.error('HR node is not defined in the schema.'); return false; @@ -155,32 +68,69 @@ function insertHR(state: EditorState, dispatch?: (tr: Transaction) => void): boo return true; } +/**command function that inserts a blockquote */ + +function insertQuote(state: EditorState, dispatch?: (tr: Transaction) => void): boolean { + console.log("you just put a blockquote"); + + const { schema, tr, selection } = state; + const blockquote = schema.nodes.blockquote; + + // Handle when no selection is made (empty selection) + if (selection.empty) { + console.log("No selection, inserting a blockquote..."); + + const blockquoteNode = blockquote.createAndFill(); + if (blockquoteNode) { + // Only dispatch if `dispatch` is defined + if (dispatch) { + dispatch(tr.replaceSelectionWith(blockquoteNode)); + } + } + } else { + console.log("Inserting blockquote at selection"); + + // Handle the case where there is a selection + const blockquoteNode = blockquote.createAndFill(); + if (blockquoteNode) { + // Only dispatch if `dispatch` is defined + if (dispatch) { + dispatch(tr.replaceSelectionWith(blockquoteNode)); + } + } + } + + return true; +} - -/* this command will prompt you for a url, which will -apply link mark to your selection and make your selection a hyperlink */ - +/* this command will apply the link mark to hyperlink your selection */ function toggleLink(state: EditorState, dispatch?: (tr: Transaction) => void): boolean { - let {doc, selection} = state - if (selection.empty) return false + let {doc, selection} = state; + if (selection.empty) return false; let attrs = null if (!doc.rangeHasMark(selection.from, selection.to, customSchema.marks.link)) { - attrs = {href: prompt("Link to where?", "")} + attrs = {href: prompt("Paste your URL in here please", "")} if (!attrs.href) return false } return toggleMark(customSchema.marks.link, attrs)(state, dispatch) } + /* these functions add the bold and emphasis marks */ + function toggleBold(state: EditorState, dispatch?: (tr: Transaction) => void): boolean { + return toggleMark(customSchema.marks.bold)(state, dispatch); + } - + function toggleEmphasis(state: EditorState, dispatch?: (tr: Transaction) => void): boolean { + return toggleMark(customSchema.marks.emphasis)(state, dispatch) + } // custom keymap to apply to state const customKeymap = keymap({ - 'Ctrl-Shift-Space': insertStar, - 'Ctrl-b': (state, dispatch) => { + 'Shift-Space': insertStar, + 'Ctrl-g': (state, dispatch) => { console.log("Ctrl-b pressed, toggling shouting mark..."); return toggleMark(customSchema.marks.shouting)(state, dispatch); }, @@ -188,17 +138,15 @@ const customKeymap = keymap({ console.log("you should have just gotten an alert to place a url into a hyperlink"); return toggleLink(state, dispatch); }, + 'Ctrl-e': toggleEmphasis, + 'Ctrl-b': toggleBold, - 'Ctrl-Shift-h': insertHR, - - - }); - - - - + 'Ctrl-h': insertHR, + 'Ctrl-q': insertQuote + }, + ); @customElement('app-write') export class AppWrite extends LitElement { @@ -212,76 +160,44 @@ export class AppWrite extends LitElement { static styles = [ styles, - css` -.ProseMirror { - background: black; - color: white; - background-clip: padding-box; - padding: 5px 0; - position: relative; - word-wrap: break-word; - white-space: pre-wrap; - -webkit-font-variant-ligatures: none; - font-variant-ligatures: none; - padding: 1rem; - line-height: 1.2; - outline: none; - font-family: var( - --markdown-editor-typography-font-family, - var(--mdc-typography-font-family, Montserrat, sans-serif) - ); - font-size: var( - --markdown-editor-typography-font-size, - var(--mdc-typography-subtitle1-font-size, 1rem) - ); - font-weight: var( - --markdown-editor-typography-font-weight, - var(--mdc-typography-subtitle1-font-weight, 400) - ); - letter-spacing: var( - --markdown-editor-typography-letter-spacing, - var(--mdc-typography-subtitle1-letter-spacing, 0.009375em) - ); - } - - .ProseMirror pre { - white-space: pre-wrap; - } - - .ProseMirror a { - color: var(--markdown-editor-typography-anchor-color, -webkit-link); - text-decoration: var(--markdown-editor-typography-anchor-text-decoration); - } - - .ProseMirror-focused .ProseMirror-gapcursor { - display: block; - } - shouting { - all: unset; /* Remove inherited or conflicting styles */ - font-weight: bold; - text-transform: uppercase; - color: red; } - - .boring { - background: grey; - } -` + editorStyles ]; - - protected async firstUpdated() { - console.log("Welcome to the compose page"); - await this.updateComplete; - console.log('Rendered HTML:', this.shadowRoot?.innerHTML); - - this.initializeEditor() - if (!this.editorContainer) { - errorReport('Editor container not here'); - return; - } - + constructor() { + super(); } + destroy() { + this.editorView.destroy + } + + + connectedCallback(): void { + + super.connectedCallback(); + console.log('AppWrite added to the DOM') + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + if (this.editorView) { + this.editorView.destroy(); + } + } + + + + protected async firstUpdated() { + console.log("Welcome to the compose page"); + await this.updateComplete; + + this.initializeEditor() + if (!this.editorContainer) { + errorReport('Editor container not here'); + return; + } + } + private initializeEditor() { if (!this.editorContainer) { @@ -289,84 +205,75 @@ export class AppWrite extends LitElement { return; } - /* this is a schema of initial content to pre-populate the editor - with guidance */ + /* a prepopulated document of initial content to guide the user */ - let doc = customSchema.node("doc", null, [ - customSchema.node("paragraph", null, [customSchema.text("write anything")]), - customSchema.node("boring_paragraph", null, [customSchema.text("you can't apply any marks to text in this boring paragraph")]) + let doc = customSchema.node('doc', null, [ + customSchema.node('heading', null, [customSchema.text('Título')]), + + customSchema.node('blockquote', null, [ + customSchema.node('paragraph', null, [customSchema.text('"WEPA"')]), + ]), + customSchema.node('paragraph', null, [customSchema.text('Escribe algo')]), + customSchema.node('boring_paragraph', null, [ + customSchema.text('no se puede marcar ningún texto en párrafos aburridos como este. mira el
debajo de este párafo. puedes colocar uno con Ctrl + h'), + ]), + customSchema.node('horizontal_rule', null), + customSchema.node('paragraph', null, [customSchema.text('Escribe mas aquí...')]), + customSchema.node('paragraph', null, [customSchema.text('Presiona Shift + Space para colocar una estrella')]), + customSchema.node('paragraph', null, [customSchema.text('Presiona Ctrl + g para marcar la selección con la marca "shouting" ')]) + + ]) - ]) - - if (!doc) { + if (!doc) { console.error("failed to create initial document") return; } - /* here i create a state with doc as the schema, - which includes a couple */ + /* here i create a state with doc to, + which places the prepopulated document of content + and include history and keymap as plugins + */ - const state = EditorState.create({ - doc: doc, - plugins: [ - history(), - keymap({ - 'Mod-z': undo, - 'Mod-y': redo, - }), - customKeymap, - keymap(baseKeymap), + const state = EditorState.create({ + doc: doc, + plugins: [ + history(), + keymap({ + 'Mod-z': undo, + 'Mod-y': redo, + }), + customKeymap, + keymap(baseKeymap), - ], - }); + ], + }); + /* this */ - - let view = new EditorView(this.editorContainer, { - state, - dispatchTransaction(transaction) { - console.log('Document size went from', transaction.before.content.size, "to", - transaction.doc.content.size - ) - let newState = view.state.apply(transaction) - view.updateState(newState) - } - }); - - - - console.log(state) - - + this.editorView = new EditorView(this.editorContainer, { + state, + dispatchTransaction: (transaction) => { + console.log('Document size went from', transaction.before.content.size, "to", + transaction.doc.content.size + ) + let newState = this.editorView.state.apply(transaction); + this.editorView.updateState(newState); } + }); + console.log(state) - connectedCallback(): void { - super.connectedCallback(); - - console.log('AppWrite added to the DOM') - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - if (this.editorView) { - this.editorView.destroy(); - this.editorView = null! - } - } - - + } protected render() { return html`
-
diff --git a/src/pages/app-write/write-styles.ts b/src/pages/app-write/write-styles.ts index 83dcf10..c637cce 100644 --- a/src/pages/app-write/write-styles.ts +++ b/src/pages/app-write/write-styles.ts @@ -1,13 +1,10 @@ import { css } from "lit" -export const editorStyles = css` { +export const editorStyles = css` - - - - .ProseMirror { - background: white; - color: black; +:host .ProseMirror { + background: black; + color: white; background-clip: padding-box; padding: 5px 0; position: relative; @@ -36,7 +33,7 @@ export const editorStyles = css` { ); } - .ProseMirror pre { + .ProseMirror pre { white-space: pre-wrap; } @@ -48,8 +45,7 @@ export const editorStyles = css` { .ProseMirror-focused .ProseMirror-gapcursor { display: block; } - - shouting { + shouting { all: unset; /* Remove inherited or conflicting styles */ font-weight: bold; text-transform: uppercase; @@ -58,21 +54,11 @@ export const editorStyles = css` { .boring { background: grey; } - - .plus { - position: absolute; - - padding: 8px; - background-color: #4CAF50; - color: red; - border: none; - cursor: pointer; -} - -.plus:hover { - background-color: #45a049; -} - + blockquote { + font-style: italic; + border-left: 2px solid gray; + padding-left: 10px; + color: darkgray;