Added jsdoc types for prosemirror
Also added link markdown handling when target is set.
This commit is contained in:
parent
89194a3f85
commit
7622106665
9 changed files with 198 additions and 17 deletions
6
TODO
6
TODO
|
@ -30,3 +30,9 @@
|
||||||
- Remove links button? (Action already in place if link href is empty).
|
- Remove links button? (Action already in place if link href is empty).
|
||||||
- Links - Limit target attribute options and validate URL.
|
- Links - Limit target attribute options and validate URL.
|
||||||
- Links - Integrate entity picker.
|
- Links - Integrate entity picker.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
- Use NodeViews for embedded content (Code, Drawings) where control is needed.
|
||||||
|
- Probably still easiest to have seperate (codemirror) MD editor. Can alter display output via NodeViews to make MD like
|
||||||
|
but its tricky since editing the markdown content would change the block definition/type while editing.
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* @param {String} attrName
|
||||||
|
* @param {String} attrValue
|
||||||
|
* @return {PmCommandHandler}
|
||||||
|
*/
|
||||||
export function setBlockAttr(attrName, attrValue) {
|
export function setBlockAttr(attrName, attrValue) {
|
||||||
return function (state, dispatch) {
|
return function (state, dispatch) {
|
||||||
const ref = state.selection;
|
const ref = state.selection;
|
||||||
|
@ -37,6 +42,10 @@ export function setBlockAttr(attrName, attrValue) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PmNodeType} blockType
|
||||||
|
* @return {PmCommandHandler}
|
||||||
|
*/
|
||||||
export function insertBlockBefore(blockType) {
|
export function insertBlockBefore(blockType) {
|
||||||
return function (state, dispatch) {
|
return function (state, dispatch) {
|
||||||
const startPosition = state.selection.$from.before(1);
|
const startPosition = state.selection.$from.before(1);
|
||||||
|
@ -49,6 +58,9 @@ export function insertBlockBefore(blockType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {PmCommandHandler}
|
||||||
|
*/
|
||||||
export function removeMarks() {
|
export function removeMarks() {
|
||||||
return function (state, dispatch) {
|
return function (state, dispatch) {
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
|
|
|
@ -48,6 +48,10 @@ parser.tokenHandlers.html_inline = function(state, tok, tokens, i) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} html
|
||||||
|
* @return {PmMark[]}
|
||||||
|
*/
|
||||||
function extractMarksFromHtml(html) {
|
function extractMarksFromHtml(html) {
|
||||||
const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>');
|
const contentDoc = htmlToDoc('<p>' + (html || '') + '</p>');
|
||||||
const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks;
|
const marks = contentDoc?.content?.content?.[0]?.content?.content?.[0]?.marks;
|
||||||
|
|
|
@ -1,14 +1,45 @@
|
||||||
import {MarkdownSerializer, defaultMarkdownSerializer} from "prosemirror-markdown";
|
import {MarkdownSerializer, defaultMarkdownSerializer, MarkdownSerializerState} from "prosemirror-markdown";
|
||||||
import {docToHtml} from "./util";
|
import {docToHtml} from "./util";
|
||||||
|
|
||||||
const nodes = defaultMarkdownSerializer.nodes;
|
const nodes = defaultMarkdownSerializer.nodes;
|
||||||
const marks = defaultMarkdownSerializer.marks;
|
const marks = defaultMarkdownSerializer.marks;
|
||||||
|
|
||||||
|
|
||||||
nodes.callout = function(state, node) {
|
nodes.callout = function (state, node) {
|
||||||
writeNodeAsHtml(state, node);
|
writeNodeAsHtml(state, node);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isPlainURL(link, parent, index, side) {
|
||||||
|
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const content = parent.child(index + (side < 0 ? -1 : 0));
|
||||||
|
if (!content.isText || content.text != link.attrs.href || content.marks[content.marks.length - 1] != link) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (index == (side < 0 ? 1 : parent.childCount - 1)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const next = parent.child(index + (side < 0 ? -2 : 1));
|
||||||
|
return !link.isInSet(next.marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
marks.link = {
|
||||||
|
open(state, mark, parent, index) {
|
||||||
|
const attrs = mark.attrs;
|
||||||
|
if (attrs.target) {
|
||||||
|
return `<a href="${attrs.target}" ${attrs.title ? `title="${attrs.title}"` : ''} target="${attrs.target}">`
|
||||||
|
}
|
||||||
|
return isPlainURL(mark, parent, index, 1) ? "<" : "["
|
||||||
|
},
|
||||||
|
close(state, mark, parent, index) {
|
||||||
|
if (mark.attrs.target) {
|
||||||
|
return `</a>`;
|
||||||
|
}
|
||||||
|
return isPlainURL(mark, parent, index, -1) ? ">"
|
||||||
|
: "](" + state.esc(mark.attrs.href) + (mark.attrs.title ? " " + state.quote(mark.attrs.title) : "") + ")"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
marks.underline = {
|
marks.underline = {
|
||||||
open: '<span style="text-decoration: underline;">',
|
open: '<span style="text-decoration: underline;">',
|
||||||
|
@ -44,9 +75,12 @@ marks.background_color = {
|
||||||
close: '</span>',
|
close: '</span>',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {MarkdownSerializerState} state
|
||||||
|
* @param node
|
||||||
|
*/
|
||||||
function writeNodeAsHtml(state, node) {
|
function writeNodeAsHtml(state, node) {
|
||||||
const html = docToHtml({ content: [node] });
|
const html = docToHtml({content: [node]});
|
||||||
state.write(html);
|
state.write(html);
|
||||||
state.ensureNewLine();
|
state.ensureNewLine();
|
||||||
state.write('\n');
|
state.write('\n');
|
||||||
|
@ -57,7 +91,7 @@ function writeNodeAsHtml(state, node) {
|
||||||
// or element that cannot be represented in commonmark without losing
|
// or element that cannot be represented in commonmark without losing
|
||||||
// formatting or content.
|
// formatting or content.
|
||||||
for (const [nodeType, serializerFunction] of Object.entries(nodes)) {
|
for (const [nodeType, serializerFunction] of Object.entries(nodes)) {
|
||||||
nodes[nodeType] = function(state, node, parent, index) {
|
nodes[nodeType] = function (state, node, parent, index) {
|
||||||
if (node.attrs.align) {
|
if (node.attrs.align) {
|
||||||
writeNodeAsHtml(state, node);
|
writeNodeAsHtml(state, node);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -6,7 +6,11 @@ import schema from "../schema";
|
||||||
import {MenuItem} from "./menu";
|
import {MenuItem} from "./menu";
|
||||||
import {icons} from "./icons";
|
import {icons} from "./icons";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PmMarkType} markType
|
||||||
|
* @param {String} attribute
|
||||||
|
* @return {(function(PmEditorState): (string|null))}
|
||||||
|
*/
|
||||||
function getMarkAttribute(markType, attribute) {
|
function getMarkAttribute(markType, attribute) {
|
||||||
return function (state) {
|
return function (state) {
|
||||||
const marks = state.selection.$head.marks();
|
const marks = state.selection.$head.marks();
|
||||||
|
@ -20,6 +24,11 @@ function getMarkAttribute(markType, attribute) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {(function(FormData))} submitter
|
||||||
|
* @param {Function} closer
|
||||||
|
* @return {DialogBox}
|
||||||
|
*/
|
||||||
function getLinkDialog(submitter, closer) {
|
function getLinkDialog(submitter, closer) {
|
||||||
return new DialogBox([
|
return new DialogBox([
|
||||||
new DialogForm([
|
new DialogForm([
|
||||||
|
@ -64,6 +73,12 @@ function applyLink(formData, state, dispatch) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PmEditorState} state
|
||||||
|
* @param {PmDispatchFunction} dispatch
|
||||||
|
* @param {PmView} view
|
||||||
|
* @param {Event} e
|
||||||
|
*/
|
||||||
function onPress(state, dispatch, view, e) {
|
function onPress(state, dispatch, view, e) {
|
||||||
const dialog = getLinkDialog((data) => {
|
const dialog = getLinkDialog((data) => {
|
||||||
applyLink(data, state, dispatch);
|
applyLink(data, state, dispatch);
|
||||||
|
@ -77,6 +92,9 @@ function onPress(state, dispatch, view, e) {
|
||||||
document.body.appendChild(dom);
|
document.body.appendChild(dom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {MenuItem}
|
||||||
|
*/
|
||||||
function anchorButtonItem() {
|
function anchorButtonItem() {
|
||||||
return new MenuItem({
|
return new MenuItem({
|
||||||
title: "Insert/Edit Anchor Link",
|
title: "Insert/Edit Anchor Link",
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- Use NodeViews for embedded content (Code, Drawings) where control is needed.
|
|
||||||
- Probably still easiest to have seperate (codemirror) MD editor. Can alter display output via NodeViews to make MD like
|
|
||||||
but its tricky since editing the markdown content would change the block definition/type while editing.
|
|
||||||
-
|
|
|
@ -3,9 +3,10 @@ import {Schema} from "prosemirror-model";
|
||||||
import nodes from "./schema-nodes";
|
import nodes from "./schema-nodes";
|
||||||
import marks from "./schema-marks";
|
import marks from "./schema-marks";
|
||||||
|
|
||||||
const index = new Schema({
|
/** @var {PmSchema} schema */
|
||||||
|
const schema = new Schema({
|
||||||
nodes,
|
nodes,
|
||||||
marks,
|
marks,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default index;
|
export default schema;
|
106
resources/js/editor/types.js
Normal file
106
resources/js/editor/types.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmEditorState
|
||||||
|
* @property {PmNode} doc
|
||||||
|
* @property {PmSelection} selection
|
||||||
|
* @property {PmMark[]|null} storedMarks
|
||||||
|
* @property {PmSchema} schema
|
||||||
|
* @property {PmTransaction} tr
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmNode
|
||||||
|
* @property {PmNodeType} type
|
||||||
|
* @property {Object} attrs
|
||||||
|
* @property {PmFragment} content
|
||||||
|
* @property {PmMark[]} marks
|
||||||
|
* @property {String|null} text
|
||||||
|
* @property {Number} nodeSize
|
||||||
|
* @property {Number} childCount
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmNodeType
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmMark
|
||||||
|
* @property {PmMarkType} type
|
||||||
|
* @property {Object} attrs
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmMarkType
|
||||||
|
* @property {String} name
|
||||||
|
* @property {PmSchema} schema
|
||||||
|
* @property {PmMarkSpec} spec
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmMarkSpec
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmSchema
|
||||||
|
* @property {PmSchema} schema
|
||||||
|
* @property {Object<PmNodeType>} nodes
|
||||||
|
* @property {Object<PmMarkType>} marks
|
||||||
|
* @property {PmNodeType} topNodeType
|
||||||
|
* @property {Object} cached
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmSelection
|
||||||
|
* @property {PmSelectionRange[]} ranges
|
||||||
|
* @property {PmResolvedPos} $anchor
|
||||||
|
* @property {PmResolvedPos} $head
|
||||||
|
* @property {Number} anchor
|
||||||
|
* @property {Number} head
|
||||||
|
* @property {Number} from
|
||||||
|
* @property {Number} to
|
||||||
|
* @property {PmResolvedPos} $from
|
||||||
|
* @property {PmResolvedPos} $to
|
||||||
|
* @property {Boolean} empty
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmResolvedPos
|
||||||
|
* @property {Number} pos
|
||||||
|
* @property {Number} depth
|
||||||
|
* @property {Number} parentOffset
|
||||||
|
* @property {PmNode} parent
|
||||||
|
* @property {PmNode} doc
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmSelectionRange
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmTransaction
|
||||||
|
* @property {Number} time
|
||||||
|
* @property {PmMark[]|null} storedMarks
|
||||||
|
* @property {PmSelection} selection
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmFragment
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Function} PmCommandHandler
|
||||||
|
* @param {PmEditorState} state
|
||||||
|
* @param {PmDispatchFunction} dispatch
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Function} PmDispatchFunction
|
||||||
|
* @param {PmTransaction} tr
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PmView
|
||||||
|
* @param {PmEditorState} state
|
||||||
|
* @param {Element} dom
|
||||||
|
* @param {Boolean} editable
|
||||||
|
* @param {Boolean} composing
|
||||||
|
*/
|
|
@ -1,13 +1,20 @@
|
||||||
import schema from "./schema";
|
import schema from "./schema";
|
||||||
import {DOMParser, DOMSerializer} from "prosemirror-model";
|
import {DOMParser, DOMSerializer} from "prosemirror-model";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String} html
|
||||||
|
* @return {PmNode}
|
||||||
|
*/
|
||||||
export function htmlToDoc(html) {
|
export function htmlToDoc(html) {
|
||||||
const renderDoc = document.implementation.createHTMLDocument();
|
const renderDoc = document.implementation.createHTMLDocument();
|
||||||
renderDoc.body.innerHTML = html;
|
renderDoc.body.innerHTML = html;
|
||||||
return DOMParser.fromSchema(schema).parse(renderDoc.body);
|
return DOMParser.fromSchema(schema).parse(renderDoc.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {PmNode} doc
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
export function docToHtml(doc) {
|
export function docToHtml(doc) {
|
||||||
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
|
const fragment = DOMSerializer.fromSchema(schema).serializeFragment(doc.content);
|
||||||
const renderDoc = document.implementation.createHTMLDocument();
|
const renderDoc = document.implementation.createHTMLDocument();
|
||||||
|
|
Loading…
Reference in a new issue