got a basic working text area
This commit is contained in:
parent
2dfbc08788
commit
d914db1c1d
9 changed files with 888 additions and 58 deletions
144
package-lock.json
generated
144
package-lock.json
generated
|
@ -9,14 +9,20 @@
|
|||
"version": "0.0.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.30.7",
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@shoelace-style/shoelace": "^2.18.0",
|
||||
"@thepassle/app-tools": "^0.9.12",
|
||||
"axios": "^1.7.7",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.1",
|
||||
"lazy.js": "^0.5.1",
|
||||
"lit": "^3.2.1",
|
||||
"nostr-tools": "^2.10.1",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-view": "^1.37.1",
|
||||
"urlpattern-polyfill": "^10.0.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-core": "^7.3.0",
|
||||
|
@ -1505,6 +1511,11 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@editorjs/editorjs": {
|
||||
"version": "2.30.7",
|
||||
"resolved": "https://registry.npmjs.org/@editorjs/editorjs/-/editorjs-2.30.7.tgz",
|
||||
"integrity": "sha512-FfdeUqrgcKWC+Cy2GW6Dxup6s2TaRKokR4FL+HKXshu6h9Y//rrx4SQkURgkZOCSbV77t9btbmAXdFXWGB+diw=="
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
|
@ -2464,6 +2475,25 @@
|
|||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
|
||||
|
@ -2558,6 +2588,11 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/array-buffer-byte-length": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
|
||||
|
@ -3203,6 +3238,17 @@
|
|||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-abstract": {
|
||||
"version": "1.23.3",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz",
|
||||
|
@ -4302,6 +4348,11 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lazy.js": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lazy.js/-/lazy.js-0.5.1.tgz",
|
||||
"integrity": "sha512-p9v24vaKrzS2mEx3yuzva/3M6I3+HwvXd0pB1Xf/IvsFIMdhQgmym7JBO0e7c0OZmKTo07sCuiCIm6jazSWFNw=="
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
|
@ -4311,6 +4362,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lit": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz",
|
||||
|
@ -4378,6 +4437,27 @@
|
|||
"sourcemap-codec": "^1.4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
|
||||
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
|
@ -4580,6 +4660,11 @@
|
|||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g=="
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
|
@ -4677,6 +4762,52 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.1.tgz",
|
||||
"integrity": "sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==",
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"prosemirror-model": "^1.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.24.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.24.1.tgz",
|
||||
"integrity": "sha512-YM053N+vTThzlWJ/AtPtF1j0ebO36nvbmDy4U7qA2XQB8JVaQp1FmB9Jhrps8s+z+uxhhVTny4m20ptUvhk0Mg==",
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.3.tgz",
|
||||
"integrity": "sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.10.2.tgz",
|
||||
"integrity": "sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.37.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.37.1.tgz",
|
||||
"integrity": "sha512-MEAnjOdXU1InxEmhjgmEzQAikaS6lF3hD64MveTPpjOGNTl87iRLA1HupC/DEV6YuK7m4Q9DHFNTjwIVtqz5NA==",
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
|
@ -4703,6 +4834,14 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/qr-creator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/qr-creator/-/qr-creator-1.0.0.tgz",
|
||||
|
@ -5534,6 +5673,11 @@
|
|||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
|
||||
},
|
||||
"node_modules/unbox-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
|
||||
|
|
|
@ -15,14 +15,20 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@editorjs/editorjs": "^2.30.7",
|
||||
"@lit/localize": "^0.12.2",
|
||||
"@shoelace-style/shoelace": "^2.18.0",
|
||||
"@thepassle/app-tools": "^0.9.12",
|
||||
"axios": "^1.7.7",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.21.1",
|
||||
"lazy.js": "^0.5.1",
|
||||
"lit": "^3.2.1",
|
||||
"nostr-tools": "^2.10.1",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-view": "^1.37.1",
|
||||
"urlpattern-polyfill": "^10.0.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-core": "^7.3.0",
|
||||
|
|
40
src/components/menu-plugin.ts
Normal file
40
src/components/menu-plugin.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { Plugin } from 'prosemirror-state';
|
||||
|
||||
export const plusButtonPlugin = new Plugin({
|
||||
view(editorView) {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = '+';
|
||||
button.className = 'plus-button'
|
||||
|
||||
button.style.position = 'absolute';
|
||||
button.style.zIndex = '10';
|
||||
button.style.display = 'none'; // Hide initially
|
||||
|
||||
editorView.dom.appendChild(button);
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
console.log('Plus button clicked');
|
||||
// Add logic to open dropdown or execute commands
|
||||
});
|
||||
|
||||
return {
|
||||
update(view) {
|
||||
const { $from } = view.state.selection;
|
||||
|
||||
// Only show the button when the cursor is in a text block with no content
|
||||
if ($from.parent.isTextblock && $from.parent.content.size === 0) {
|
||||
const coords = view.coordsAtPos($from.pos);
|
||||
// Position the button relative to the cursor
|
||||
button.style.top = `${coords.top + window.scrollY}px`; // Added scroll offset for better positioning
|
||||
button.style.left = `${coords.left + window.scrollX - 30}px`; // Added scroll offset
|
||||
button.style.display = 'block'; // Make the button visible
|
||||
} else {
|
||||
button.style.display = 'none'; // Hide the button when not in valid position
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
button.remove(); // Cleanup on destroy
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,12 +1,9 @@
|
|||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
// You can also import styles from another file
|
||||
// if you prefer to keep your CSS seperate from your component
|
||||
import { styles } from './about-styles';
|
||||
|
||||
import { styles as sharedStyles } from '../../styles/shared-styles'
|
||||
|
||||
import '@shoelace-style/shoelace/dist/components/card/card.js';
|
||||
|
||||
@customElement('app-about')
|
||||
|
@ -16,29 +13,6 @@ export class AppAbout extends LitElement {
|
|||
styles,
|
||||
css`
|
||||
|
||||
#welcomeBar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#welcomeCard,
|
||||
#infoCard {
|
||||
padding: 18px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
sl-card::part(footer) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media(min-width: 750px) {
|
||||
sl-card {
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
`
|
||||
]
|
||||
|
||||
|
@ -74,7 +48,8 @@ export class AppAbout extends LitElement {
|
|||
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://www.npmjs.com/package/nostr-tools?activeTab=dependencies">Nostr Tools</a>
|
||||
I am using <a href="https://www.npmjs.com/package/nostr-tools?activeTab=dependencies">Nostr Tools</a> to implement an extension
|
||||
sign in and pull notes from my personal relay.
|
||||
</li>
|
||||
|
||||
<li>
|
||||
|
|
|
@ -4,11 +4,13 @@ import { resolveRouterPath } from '../router';
|
|||
import { Relay } from 'nostr-tools';
|
||||
import { WindowNostr } from 'nostr-tools/nip07';
|
||||
|
||||
|
||||
import '@shoelace-style/shoelace/dist/components/card/card.js';
|
||||
import '@shoelace-style/shoelace/dist/components/button/button.js';
|
||||
|
||||
import { styles } from '../styles/shared-styles';
|
||||
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
nostr?: WindowNostr;
|
||||
|
@ -31,29 +33,7 @@ export class AppHome extends LitElement {
|
|||
static styles = [
|
||||
styles,
|
||||
css`
|
||||
#welcomeBar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#welcomeCard,
|
||||
#infoCard {
|
||||
padding: 18px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
sl-card::part(footer) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media(min-width: 750px) {
|
||||
sl-card {
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (horizontal-viewport-segments: 2) {
|
||||
|
@ -209,16 +189,16 @@ Profile Picture Container
|
|||
<div id="welcomeBar">
|
||||
<sl-card id="welcomeCard">
|
||||
<div slot="header">
|
||||
<h2>Find me on any nostr client by searching ${this.nostrAddy || 'Guest'}</h2>
|
||||
<h2>Welcome to ${this.nostrAddy || 'Guest'}'s Profile</h2>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You can use the Nostr protocol to transform your personal website into
|
||||
a micro social media client. It eliminates the need to use a corporate
|
||||
social media account. I am using nostr-tools to implement an extension
|
||||
sign in and pull notes from my personal relay.
|
||||
a micro blog client. You can compose a note and post it to your social
|
||||
network.
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
<div class="profile-picture-container">
|
||||
<img class="profile-pic" src="${this.profilePic || '/assets/img/default_pfp.png'}" alt="Profile Picture" width="200px" height="200px">
|
||||
|
||||
|
@ -237,7 +217,9 @@ Profile Picture Container
|
|||
|
||||
<sl-button href="${resolveRouterPath('about')}" variant="primary">Navigate to About</sl-button>
|
||||
<sl-button href="${resolveRouterPath('note-wall')}" variant="primary">Navigate to Note Wall</sl-button>
|
||||
</div>
|
||||
<sl-button href="${resolveRouterPath('write')}" variant="primary">Navigate to Note Compose</sl-button>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
|
|
242
src/pages/app-write/app-write.ts
Normal file
242
src/pages/app-write/app-write.ts
Normal file
|
@ -0,0 +1,242 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { property, customElement, query } from 'lit/decorators.js';
|
||||
|
||||
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 {EditorState} from 'prosemirror-state'
|
||||
import { Transaction } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { Schema } from 'prosemirror-model';
|
||||
import { keymap } from 'prosemirror-keymap';
|
||||
import { toggleMark } from 'prosemirror-commands';
|
||||
import {undo, redo, history} from 'prosemirror-history'
|
||||
import { baseKeymap } from 'prosemirror-commands';
|
||||
|
||||
import { plusButtonPlugin } from '../../components/menu-plugin';
|
||||
|
||||
|
||||
|
||||
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.boring', priority: 60 }],
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Commands
|
||||
function insertStar(state: EditorState, dispatch?: (tr: Transaction) => void): boolean {
|
||||
const type = customSchema.nodes.star;
|
||||
const { $from } = state.selection;
|
||||
|
||||
// If the parent cannot replace with the 'star' node, return false
|
||||
if (!$from.parent.canReplaceWith($from.index(), $from.index(), type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If dispatch is provided, apply the transaction
|
||||
if (dispatch) {
|
||||
dispatch(state.tr.replaceSelectionWith(type.create()));
|
||||
}
|
||||
|
||||
// Always return true if insertion conditions are met
|
||||
return true;
|
||||
}
|
||||
|
||||
function toggleLink(state: EditorState, dispatch?: (tr: Transaction) => void): boolean {
|
||||
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?", "")}
|
||||
if (!attrs.href) return false
|
||||
}
|
||||
return toggleMark(customSchema.marks.link, attrs)(state, dispatch)
|
||||
}
|
||||
|
||||
|
||||
// Keymap
|
||||
const customKeymap = keymap({
|
||||
'Ctrl-Space': insertStar,
|
||||
"Ctrl-b": (state, dispatch) => {
|
||||
console.log("Ctrl-b pressed, toggling shouting mark...");
|
||||
return toggleMark(customSchema.marks.shouting)(state, dispatch);
|
||||
},
|
||||
'Ctrl-q': (state, dispatch) => {
|
||||
console.log("you should have just gotten an alert");
|
||||
return toggleLink(state, dispatch);
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@customElement('app-write')
|
||||
export class AppWrite extends LitElement {
|
||||
@property({ type: String }) placeholder: string = 'Compose your note...';
|
||||
@property({ type: String }) public value?: string;
|
||||
|
||||
// Reference to the ProseMirror container
|
||||
@query('#editor') editorContainer!: HTMLElement;
|
||||
|
||||
private editorView!: EditorView
|
||||
|
||||
|
||||
|
||||
|
||||
static styles = [
|
||||
styles, css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
shouting {
|
||||
all: unset; /* Remove inherited or conflicting styles */
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: red; /* Add a visible color for debugging */
|
||||
}
|
||||
|
||||
`
|
||||
|
||||
];
|
||||
|
||||
protected async firstUpdated() {
|
||||
console.log("Welcome to the compose page");
|
||||
await this.updateComplete;
|
||||
this.initializeEditor()
|
||||
|
||||
}
|
||||
private initializeEditor() {
|
||||
if (!this.editorContainer) {
|
||||
console.error('Editor container not here');
|
||||
return
|
||||
}
|
||||
const state = EditorState.create({
|
||||
schema: customSchema,
|
||||
plugins: [
|
||||
|
||||
history(),
|
||||
keymap({
|
||||
'Mod-z': undo,
|
||||
'Mod-y': redo,
|
||||
}),
|
||||
keymap(baseKeymap),
|
||||
// Add the plus-button plugin here
|
||||
plusButtonPlugin
|
||||
],
|
||||
});
|
||||
|
||||
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)
|
||||
}
|
||||
});
|
||||
this.editorView = view;
|
||||
console.log('editor initialized')
|
||||
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this.editorView) {
|
||||
this.editorView.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<app-header ?enableBack="${true}"></app-header>
|
||||
|
||||
<main>
|
||||
<div id="welcomeBar">
|
||||
|
||||
<sl-card id="welcomeCard">
|
||||
<div slot="header">
|
||||
<h2>Editor Demo</h2>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Aren't you tired of distracting interfaces on mainstream blogging platforms?
|
||||
So many buttons and advertising makes a single task very draining. This is
|
||||
a platform-agnostic browser application that is installable to any homescreen! </p>
|
||||
<p>
|
||||
You can use a protocol to transform your personal website into
|
||||
a micro blog client. Using WYSIWYG style editor builders like
|
||||
ProseMirror, you can render a simple rich text editor for visitors to compose
|
||||
notes with. You can type out a note and post it to a set of relays.
|
||||
</p>
|
||||
<h3>Basic Intructions</h3>
|
||||
<p>click inside the white text area to place your curson there.
|
||||
You can type and press enter to go to the next line down.
|
||||
<br>
|
||||
Click and hold down to select a piece of text, then CTRL+X to cut and
|
||||
CTRL+V to paste.</p>
|
||||
|
||||
</sl-card>
|
||||
<sl-card>
|
||||
<div id="editor" class="ProseMirror"></div>
|
||||
</sl-card>
|
||||
</main>
|
||||
`}
|
||||
}
|
|
@ -14,6 +14,7 @@ import { title } from '@thepassle/app-tools/router/plugins/title.js';
|
|||
|
||||
import './pages/app-home.js';
|
||||
|
||||
|
||||
const baseURL: string = (import.meta as any).env.BASE_URL;
|
||||
|
||||
export const router = new Router({
|
||||
|
@ -35,7 +36,15 @@ export const router = new Router({
|
|||
path: resolveRouterPath('note-wall'),
|
||||
title: 'Note Wall',
|
||||
render: () => html`<note-wall></note-wall>`
|
||||
}
|
||||
},
|
||||
{
|
||||
path: resolveRouterPath('write'),
|
||||
title: 'Write',
|
||||
plugins: [
|
||||
lazy(() => import('./pages/app-write/app-write.js')),
|
||||
],
|
||||
render: () => html`<app-write><app-write>`
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
|
|
|
@ -10,7 +10,439 @@ export const styles = css`
|
|||
}
|
||||
|
||||
main {
|
||||
margin-top: 34px;
|
||||
margin-top: 70px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.ProseMirror {
|
||||
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 li {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror p:first-child,
|
||||
.ProseMirror h1:first-child,
|
||||
.ProseMirror h2:first-child,
|
||||
.ProseMirror h3:first-child,
|
||||
.ProseMirror h4:first-child,
|
||||
.ProseMirror h5:first-child,
|
||||
.ProseMirror h6:first-child {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ProseMirror a {
|
||||
color: var(--markdown-editor-typography-anchor-color, -webkit-link);
|
||||
text-decoration: var(--markdown-editor-typography-anchor-text-decoration);
|
||||
}
|
||||
|
||||
.ProseMirror p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection {
|
||||
caret-color: transparent;
|
||||
}
|
||||
|
||||
.ProseMirror-hideselection *::selection,
|
||||
.ProseMirror-hideselection *::-moz-selection {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.ProseMirror-textblock-dropdown {
|
||||
min-width: 3em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu {
|
||||
margin: 0 -4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ProseMirror-tooltip .ProseMirror-menu {
|
||||
width: -webkit-fit-content;
|
||||
width: fit-content;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.ProseMirror-menuitem {
|
||||
margin: 0.25rem 0.25rem 0.25rem 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ProseMirror-menuitem:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.ProseMirror-menuseparator {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown,
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown {
|
||||
vertical-align: 1px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-wrap {
|
||||
padding: 1px 0 1px 4px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown:after {
|
||||
content: '';
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 4px solid currentColor;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 2px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu,
|
||||
.ProseMirror-menu-submenu {
|
||||
position: absolute;
|
||||
background: white;
|
||||
color: #666;
|
||||
border: 1px solid #aaa;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-menu {
|
||||
z-index: 1;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item:hover {
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-dropdown-item > div {
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap {
|
||||
position: relative;
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-label:after {
|
||||
content: '';
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 4px solid currentColor;
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: calc(50% - 4px);
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu {
|
||||
display: none;
|
||||
min-width: 4em;
|
||||
left: 100%;
|
||||
top: -3px;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-active {
|
||||
background: #eee;
|
||||
border-radius: 4px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu,
|
||||
.ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ProseMirror-menubar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
background: white;
|
||||
min-height: 1em;
|
||||
overflow: visible;
|
||||
z-index: 2;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
color: #666;
|
||||
padding: 1px 6px;
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
border-bottom: 1px solid
|
||||
var(--markdown-editor-outline-idle-border-color, rgba(0, 0, 0, 0.38));
|
||||
box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
display: inline-block;
|
||||
line-height: 0.8;
|
||||
vertical-align: -2px;
|
||||
/* Compensate for padding */
|
||||
padding: 2px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled {
|
||||
color: rgba(0,0,0,0.37);
|
||||
background-color: rgba(0,0,0,0.12);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ProseMirror-menu-disabled.ProseMirror-icon {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ProseMirror-icon svg {
|
||||
fill: currentColor;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.ProseMirror-icon span {
|
||||
vertical-align: text-top;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor {
|
||||
display: none;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ProseMirror-gapcursor:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
width: 20px;
|
||||
border-top: 1px solid black;
|
||||
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
|
||||
}
|
||||
|
||||
@keyframes ProseMirror-cursor-blink {
|
||||
to {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-focused .ProseMirror-gapcursor {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Add space around the hr to make clicking it easier */
|
||||
.ProseMirror-example-setup-style hr {
|
||||
padding: 2px 10px;
|
||||
border: none;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style hr:after {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 1px;
|
||||
background-color: silver;
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
.ProseMirror ul,
|
||||
.ProseMirror ol {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
.ProseMirror blockquote {
|
||||
padding-left: 1em;
|
||||
border-left: 3px solid #eee;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ProseMirror-example-setup-style img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ProseMirror-invalid {
|
||||
background: #ffc;
|
||||
border: 1px solid #cc7;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
position: absolute;
|
||||
min-width: 10em;
|
||||
}
|
||||
|
||||
.ProseMirror h1.title.empty-node::before,
|
||||
.ProseMirror h2.instructional-prompt.empty-node::before,
|
||||
.ProseMirror h3.mechanical-promp.empty-node::before {
|
||||
content: 'Enter title here...';
|
||||
}
|
||||
|
||||
.ProseMirror div.passage-subtitle.empty-node:first-child::before {
|
||||
content: 'Enter subtitle here...';
|
||||
}
|
||||
|
||||
.ProseMirror div.passage-author.empty-node:first-child::before,
|
||||
.ProseMirror div.passage-cast-title.empty-node:first-child::before,
|
||||
.ProseMirror div.passage-act-title.empty-node:first-child::before,
|
||||
.ProseMirror div.passage-scene-title.empty-node:first-child::before,
|
||||
.ProseMirror div.passage-verse.empty-node:first-child::before,
|
||||
.ProseMirror div.passage-footnotes.empty-node:first-child::before,
|
||||
.ProseMirror div.paragraph.empty-node:first-child::before {
|
||||
content: 'Enter text here...';
|
||||
}
|
||||
|
||||
div[contenteditable]:focus h1.title.empty-node::before,
|
||||
div[contenteditable]:focus h2.instructional-prompt.empty-node::before,
|
||||
div[contenteditable]:focus h3.mechanical-promp.empty-node::before,
|
||||
div[contenteditable]:focus
|
||||
div.passage-subtitle.empty-node:first-child::before,
|
||||
div[contenteditable]:focus div.passage-author.empty-node:first-child::before,
|
||||
div[contenteditable]:focus
|
||||
div.passage-cast-title.empty-node:first-child::before,
|
||||
div[contenteditable]:focus
|
||||
div.passage-act-title.empty-node:first-child::before,
|
||||
div[contenteditable]:focus
|
||||
div.passage-scene-title.empty-node:first-child::before,
|
||||
div[contenteditable]:focus div.passage-verse.empty-node:first-child::before,
|
||||
div[contenteditable]:focus
|
||||
div.passage-footnotes.empty-node:first-child::before,
|
||||
div[contenteditable]:focus div.paragraph.empty-node:first-child::before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.ProseMirror .empty-node::before {
|
||||
position: absolute;
|
||||
color: #aaa;
|
||||
cursor: text;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#editor, .editor {
|
||||
background: white;
|
||||
color: black;
|
||||
background-clip: padding-box;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
#editor[disabled] .ProseMirror-menubar {
|
||||
display: none!important;
|
||||
}
|
||||
|
||||
drop-down-editor rich-text .inline-component-button,
|
||||
expand-collapse rich-text .inline-component-button,
|
||||
flip-reveal rich-text .inline-component-button,
|
||||
hint-list rich-text .inline-component-button,
|
||||
option-list rich-text .inline-component-button,
|
||||
plankton-passage rich-text .inline-component-button {
|
||||
pointer-events: none;
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
#ProseMirror-icon-collection path {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
#welcomeBar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#welcomeCard,
|
||||
#infoCard {
|
||||
padding: 18px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
sl-card::part(footer) {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
@media(min-width: 750px) {
|
||||
sl-card {
|
||||
width: 70vw;
|
||||
}
|
||||
}
|
||||
|
||||
.boring {
|
||||
background: grey;
|
||||
}
|
||||
|
||||
.plus {
|
||||
position: absolute;
|
||||
|
||||
padding: 8px;
|
||||
background-color: #4CAF50;
|
||||
color: red;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.plus:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
`;
|
|
@ -23,6 +23,6 @@
|
|||
"vite/client"
|
||||
]
|
||||
},
|
||||
"include": ["src/**/*.ts", "router.d.ts"],
|
||||
"include": ["src/**/*.ts", "router.d.ts", "src/pages/app-write"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue