started to implement a horizontal rule command

updated customKeymap with a keyboard shortcut to insert hr element
updated editor appearance
implemented a custom schema filler doc that prefilled a couple of nodes into the editor
moved css to component because it wasn't reading from a separate component
This commit is contained in:
miggymofongo 2025-01-08 01:05:20 -04:00
parent d4d85804bd
commit 468a454323
3 changed files with 148 additions and 130 deletions

View file

@ -13,14 +13,19 @@ import { Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view'; import { EditorView } from 'prosemirror-view';
import { Schema } from 'prosemirror-model'; import { Schema } from 'prosemirror-model';
import { MenuBar } from './write-menu';
import { keymap } from 'prosemirror-keymap'; import { keymap } from 'prosemirror-keymap';
import { toggleMark } from 'prosemirror-commands'; import { toggleMark } from 'prosemirror-commands';
import {undo, redo, history} from 'prosemirror-history' import {undo, redo, history} from 'prosemirror-history'
import { baseKeymap } from 'prosemirror-commands'; 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({ export const customSchema = new Schema({
nodes: { nodes: {
text: { text: {
@ -49,7 +54,15 @@ export const customSchema = new Schema({
toDOM() { toDOM() {
return ['p', { class: 'boring' }, 0]; return ['p', { class: 'boring' }, 0];
}, },
parseDOM: [{ tag: 'p.boring', priority: 60 }], parseDOM: [{ tag: 'p', priority: 60 }],
},
hr: {
group: 'block',
selectable: true,
parseDOM: [{ tag: 'horizontal_rule' }],
toDOM() {
return ['hr', { class: 'horizontal-rule'}]
}
}, },
doc: { doc: {
content: 'block+', content: 'block+',
@ -77,9 +90,20 @@ export const customSchema = new Schema({
], ],
inclusive: false, inclusive: false,
}, },
bold: {
},
emphasis: {
}
}, },
}); });
// function for error reports // function for error reports
function errorReport(message: string): void { function errorReport(message: string): void {
@ -87,25 +111,53 @@ export const customSchema = new Schema({
} }
// Commands
/* 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 { function insertStar(state: EditorState, dispatch?: (tr: Transaction) => void): boolean {
const type = customSchema.nodes.star; const star = customSchema.nodes.star;
const { $from } = state.selection; const { $from } = state.selection;
// If the parent cannot replace with the 'star' node, return false // If the parent cannot replace with the 'star' node, return false
if (!$from.parent.canReplaceWith($from.index(), $from.index(), type)) { if (!$from.parent.canReplaceWith($from.index(), $from.index(), star)) {
return false; return false;
} }
// If dispatch is provided, apply the transaction // If dispatch is provided, apply the transaction
if (dispatch) { if (dispatch) {
dispatch(state.tr.replaceSelectionWith(type.create())); dispatch(state.tr.replaceSelectionWith(star.create()));
} }
// Always return true if insertion conditions are met // Always return true if insertion conditions are met
return true; return true;
} }
/* 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
if (!hr) {
console.error('HR node is not defined in the schema.');
return false;
}
if (dispatch) {
dispatch(state.tr.replaceSelectionWith(hr.create()));
}
return true;
}
/* this command will prompt you for a url, which will /* this command will prompt you for a url, which will
apply link mark to your selection and make your selection a hyperlink */ apply link mark to your selection and make your selection a hyperlink */
@ -122,47 +174,29 @@ function toggleLink(state: EditorState, dispatch?: (tr: Transaction) => void): b
} }
/* starting to build a function that inserts an <hr> line break
function insertHR(state: EditorState, dispatch?: (tr: Transaction) => void): boolean {
const type = customSchema.nodes.boring_paragraph
return false
} */
// custom keymap to apply to state // custom keymap to apply to state
const customKeymap = keymap({ const customKeymap = keymap({
'Ctrl-Space': insertStar, 'Ctrl-Shift-Space': insertStar,
'Ctrl-b': (state, dispatch) => { 'Ctrl-b': (state, dispatch) => {
console.log("Ctrl-b pressed, toggling shouting mark..."); console.log("Ctrl-b pressed, toggling shouting mark...");
return toggleMark(customSchema.marks.shouting)(state, dispatch); return toggleMark(customSchema.marks.shouting)(state, dispatch);
}, },
'Ctrl-q': (state, dispatch) => { 'Ctrl-k': (state, dispatch) => {
console.log("you should have just gotten an alert"); console.log("you should have just gotten an alert to place a url into a hyperlink");
return toggleLink(state, dispatch); return toggleLink(state, dispatch);
}, },
'Ctrl-Shift-h': insertHR,
}); });
const menuItems = [
{
label: 'Bold',
command: (state, dispatch, view) => {
// Example command logic for bold
console.log('Bold clicked');
return true; // Return true if active, false otherwise
},
},
{
label: 'Italic',
command: (state, dispatch, view) => {
console.log('Italic clicked');
return true;
},
},
];
@ -177,86 +211,60 @@ export class AppWrite extends LitElement {
static styles = [ static styles = [
styles, styles,
css` css`
.ProseMirror { .ProseMirror {
background: black;
color: white;
background-clip: padding-box;
padding: 5px 0;
position: relative; position: relative;
white-space: pre-wrap;
word-wrap: break-word;
outline: none;
background-color: white;
color: black;
border-radius: 4px;
padding: 10px;
min-height: 200px; /* Ensure there's enough height to type */
}
.ProseMirror:empty::before {
content: "Start typing here...";
color: black;
pointer-events: none; /* Ensures the placeholder doesn't block typing */
}
.ProseMirror {
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
white-space: break-spaces;
-webkit-font-variant-ligatures: none; -webkit-font-variant-ligatures: none;
font-variant-ligatures: none; font-variant-ligatures: none;
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ 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 { .ProseMirror pre {
white-space: pre-wrap; white-space: pre-wrap;
} }
.ProseMirror li { .ProseMirror a {
position: relative; color: var(--markdown-editor-typography-anchor-color, -webkit-link);
text-decoration: var(--markdown-editor-typography-anchor-text-decoration);
} }
.ProseMirror-hideselection *::selection { background: transparent; } .ProseMirror-focused .ProseMirror-gapcursor {
.ProseMirror-hideselection *::-moz-selection { background: transparent; } display: block;
.ProseMirror-hideselection { caret-color: transparent; }
/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */
.ProseMirror [draggable][contenteditable=false] { user-select: text }
.ProseMirror-selectednode {
outline: 2px solid #8cf;
} }
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px; top: -2px; bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
/* Protect against generic img rules */
img.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
}
shouting { shouting {
all: unset; /* Remove inherited or conflicting styles */ all: unset; /* Remove inherited or conflicting styles */
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
color: red; color: red; }
}
.boring {
background: grey;
}
` `
]; ];
@ -271,6 +279,7 @@ export class AppWrite extends LitElement {
errorReport('Editor container not here'); errorReport('Editor container not here');
return; return;
} }
} }
private initializeEditor() { private initializeEditor() {
@ -280,22 +289,29 @@ export class AppWrite extends LitElement {
return; return;
} }
/* this is a schema of initial content to pre-populate the */ /* this is a schema of initial content to pre-populate the editor
with guidance */
const initialContent = customSchema.nodes.doc.createAndFill([
customSchema.nodes.paragraph.create(
null,
customSchema.text("Welcome to the editor! Start typing here.")
)]);
if (!initialContent) { 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")])
])
if (!doc) {
console.error("failed to create initial document") console.error("failed to create initial document")
return; return;
} }
/* here i create a state with doc as the schema,
which includes a couple */
const state = EditorState.create({ const state = EditorState.create({
doc: initialContent, doc: doc,
plugins: [ plugins: [
history(), history(),
keymap({ keymap({
@ -304,11 +320,13 @@ export class AppWrite extends LitElement {
}), }),
customKeymap, customKeymap,
keymap(baseKeymap), keymap(baseKeymap),
], ],
}); });
let view = new EditorView(this.editorContainer, { let view = new EditorView(this.editorContainer, {
state, state,
dispatchTransaction(transaction) { dispatchTransaction(transaction) {
@ -322,6 +340,7 @@ export class AppWrite extends LitElement {
console.log(state)
} }
@ -344,13 +363,13 @@ export class AppWrite extends LitElement {
protected render() { protected render() {
return html` return html`
<app-header ?enableBack="${true}"></app-header> <main><app-header ?enableBack="${true}"></app-header>
<main>
<div class="ProseMirror"></div> <div class="ProseMirror"></div>
</main> </main>
`; `;
} }
} }

View file

@ -4,6 +4,7 @@ export const editorStyles = css` {
.ProseMirror { .ProseMirror {
background: white; background: white;
color: black; color: black;
@ -52,8 +53,7 @@ shouting {
all: unset; /* Remove inherited or conflicting styles */ all: unset; /* Remove inherited or conflicting styles */
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
color: red; color: red; }
}
.boring { .boring {
background: grey; background: grey;

View file

@ -45,6 +45,5 @@ export const styles = css`
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
} }}
` `