- updated prefilled doc

- refactor separated editor syles and schema into separate components
This commit is contained in:
miggymofongo 2025-01-08 15:12:26 -04:00
parent 468a454323
commit 8abd046aff
5 changed files with 351 additions and 282 deletions

View file

@ -2,26 +2,31 @@
<p>
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. </p>
Use a library like <a href="https://prosemirror.net/">ProseMirror</a>,
<p>
I'm using <a href="https://prosemirror.net/">ProseMirror</a>,
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.
</p>
<p>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. </p>
<p>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. </p>
<p>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".
<p>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.</p>

157
src/components/schema.ts Normal file
View file

@ -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 });

View file

@ -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 {
<h3>What am I looking at?</h3>
<p>
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.
<h3> What can I do with this?</h3>
<ul><li>You can use it to sign in with a
nostr identity and display your
profile data. </li><li>Visit the note
wall to view recent notes from my relay.</li>
<li>You can also compose a note using a rich
text editor I built with Prosemirror. </li>
Keep a folder of your top friends on
your home screen to show off and use to
leave them notes.</li></ul>
</p>
@ -43,7 +52,12 @@ export class AppAbout extends LitElement {
<p>Look for "Add to Home Screen" in your browser toolbar
to install it to your homescreen. </p>
<h3>What is Nostr?</h3>
<h3>What is ProseMirror</h3>
<p>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.</p>
<h3>What is nostr?</h3>
<p>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.</p>

View file

@ -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,113 +6,25 @@ 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 errorReport(message: string): void {
console.error(message);
}
/* COMMANDS
commands are functions that take an editor state
and a dispatch function and returns a boolean
@ -122,6 +34,7 @@ export const customSchema = new Schema({
/**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 <hr> line break
*/
/* function that inserts an <hr> 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,74 +160,42 @@ 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
];
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;
console.log('Rendered HTML:', this.shadowRoot?.innerHTML);
this.initializeEditor()
if (!this.editorContainer) {
errorReport('Editor container not here');
return;
}
}
private initializeEditor() {
@ -289,17 +205,27 @@ 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 <hr> 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) {
console.error("failed to create initial document")
return;
@ -307,8 +233,10 @@ export class AppWrite extends LitElement {
/* 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,
@ -325,48 +253,27 @@ export class AppWrite extends LitElement {
});
/* this */
let view = new EditorView(this.editorContainer, {
this.editorView = new EditorView(this.editorContainer, {
state,
dispatchTransaction(transaction) {
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)
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`
<main><app-header ?enableBack="${true}"></app-header>
<div class="ProseMirror"></div>
</main>

View file

@ -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;
@ -48,7 +45,6 @@ export const editorStyles = css` {
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
shouting {
all: unset; /* Remove inherited or conflicting styles */
font-weight: bold;
@ -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;