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?
+ - You can use it to sign in with a
+ nostr identity and display your
+ profile data.
- Visit the note
+ wall to view recent notes from my relay.
+ - You can also compose a note using a rich
+ text editor I built with Prosemirror.
+ Keep a folder of your top friends on
+ your home screen to show off and use to
+ leave them notes.
@@ -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;