castopod/app/Views/_assets/modules/MarkdownEditor.ts
Yassine Doghri 2f525c0f6e feat(fediverse): implement activitypub protocols + update user interface
- add "ActivityPub" library to handle server to server federation and basic
  client to server protocols using activitypub:
  - add webfinger endpoint to look for actor
  - add actor definition with inbox / outbox / followers
  - remote follow an actor
  - create notes with possible preview cards
  - interract with favourites, reblogs and replies
  - block incoming actors and/or domains
  - broadcast/schedule activities to fediverse followers using a cron task
- For castopod, the podcast is the actor:
  - overwrite the activitypub library for castopod's specific needs
  - perform basic interactions administrating a podcast to interact with fediverse users:
    - create notes with episode attachment
    - favourite and share a note + reply
    - add specific castopod_namespaces for podcasts and episodes definitions
- overwrite CodeIgniter's Route service to include alternate-content option for
  activitystream requests
- update episode publication logic:
  - remove publication inputs in create / edit episode form
  - publish / schedule or unpublish an episode after creation
  - the podcaster publishes a note when publishing an episode
- Javascript / Typescript modules:
  - fix Dropdown.ts to keep dropdown menu in foreground
  - add Modal.ts for funding links modal
  - add Toggler.ts to toggle various css states in ui
- User Interface:
  - update tailwindcss to v2
  - use castopod's pine and rose colors
  - update public layout to a 3 column layout
  - add pages in public for podcast activity, episode list and notes
  - update episode page to include linked notes
  - remove previous and next episodes from episode pages
  - show different public views depending on whether user is authenticated or not
  - use Kumbh Sans and Montserrat fonts
- update CodeIgniter's config files
- with CodeIgniter's new requirements, update docker environments are now based on
  php v7.3 image
- move Image entity to Libraries
- update composer and npm packages to latest versions

closes #69 #65 #85, fixes #51 #91 #92 #88
2021-04-02 17:20:02 +00:00

160 lines
4.3 KiB
TypeScript

import { exampleSetup } from "prosemirror-example-setup";
import "prosemirror-example-setup/style/style.css";
import {
defaultMarkdownParser,
defaultMarkdownSerializer,
schema,
} from "prosemirror-markdown";
import "prosemirror-menu/style/menu.css";
import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import "prosemirror-view/style/prosemirror.css";
class MarkdownView {
textarea: HTMLTextAreaElement;
constructor(target: HTMLTextAreaElement) {
this.textarea = target;
this.textarea.classList.add("w-full", "h-full");
}
get content() {
return this.textarea.innerHTML;
}
focus() {
this.textarea.focus();
}
show() {
this.textarea.classList.remove("hidden");
}
hide() {
this.textarea.classList.add("hidden");
}
}
class ProseMirrorView {
editorContainer: HTMLDivElement;
view: EditorView;
constructor(target: HTMLTextAreaElement, content: string) {
this.editorContainer = document.createElement("div");
this.editorContainer.classList.add("bg-white", "border");
this.editorContainer.style.minHeight = "200px";
const editor = target.parentNode?.insertBefore(
this.editorContainer,
target.nextSibling
);
this.view = new EditorView(editor, {
state: EditorState.create({
doc: defaultMarkdownParser.parse(content),
plugins: exampleSetup({ schema }),
}),
dispatchTransaction: (transaction) => {
const newState = this.view.state.apply(transaction);
this.view.updateState(newState);
if (transaction.docChanged) {
target.innerHTML = this.content;
}
},
attributes: {
class: "prose-sm px-3 py-2 overflow-y-auto focus:ring",
style: "min-height: 200px; max-height: 500px",
},
});
}
get content() {
return defaultMarkdownSerializer.serialize(this.view.state.doc);
}
focus() {
this.view.focus();
}
show() {
this.editorContainer.classList.remove("hidden");
}
hide() {
this.editorContainer.classList.add("hidden");
}
}
const MarkdownEditor = (): void => {
const targets: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll(
"textarea[data-editor='markdown']"
);
const activeClass = "font-semibold";
for (let i = 0; i < targets.length; i++) {
const target = targets[i];
const wysiwygBtn = document.createElement("button");
wysiwygBtn.classList.add(
activeClass,
"py-1",
"px-2",
"bg-white",
"border",
"text-xs",
"outline-none",
"focus:ring"
);
wysiwygBtn.setAttribute("type", "button");
wysiwygBtn.innerHTML = "Wysiwyg";
const markdownBtn = document.createElement("button");
markdownBtn.classList.add(
"py-1",
"px-2",
"bg-white",
"border",
"text-xs",
"outline-none",
"focus:ring"
);
markdownBtn.setAttribute("type", "button");
markdownBtn.innerHTML = "Markdown";
const viewButtons = document.createElement("div");
viewButtons.appendChild(wysiwygBtn);
viewButtons.appendChild(markdownBtn);
viewButtons.classList.add(
"inline-flex",
"absolute",
"top-0",
"right-0",
"-mt-6"
);
const markdownEditorContainer = document.createElement("div");
markdownEditorContainer.classList.add("relative");
markdownEditorContainer.style.minHeight = "200px";
target.parentNode?.appendChild(markdownEditorContainer);
markdownEditorContainer.appendChild(target);
// show WYSIWYG editor by default
target.classList.add("hidden");
const markdownView = new MarkdownView(target);
const wysiwygView = new ProseMirrorView(target, markdownView.content);
markdownEditorContainer.appendChild(viewButtons);
markdownBtn.addEventListener("click", () => {
if (markdownBtn.classList.contains(activeClass)) return;
markdownBtn.classList.add(activeClass);
wysiwygBtn.classList.remove(activeClass);
wysiwygView.hide();
markdownView.show();
});
wysiwygBtn.addEventListener("click", () => {
if (wysiwygBtn.classList.contains(activeClass)) return;
wysiwygBtn.classList.add(activeClass);
markdownBtn.classList.remove(activeClass);
markdownView.hide();
wysiwygView.show();
});
}
};
export default MarkdownEditor;