@bangle.dev/core
Advanced tools
Comparing version 0.1.16 to 0.2.0
35
api.md
@@ -45,3 +45,3 @@ --- | ||
- **markdown**: ?{toMarkdown: fn(), parseMarkdown: object}\ | ||
- **markdown**: ?{toMarkdown: fn(), parseMarkdown: object}\\ | ||
@@ -512,3 +512,3 @@ - **options**: ?object\ | ||
Enables headings of various levels in your editor. {{global.link.MarkdownSupport}} | ||
Enables headings of various levels in your editor {{global.link.MarkdownSupport}}. | ||
@@ -557,2 +557,4 @@ #### spec({ ... }): {{core.link.NodeSpec}} | ||
- **toggleCollapse:** `-`: Toggle collapsing of heading. | ||
#### commands: {{core.link.CommandObject}} | ||
@@ -566,2 +568,31 @@ | ||
- **queryIsCollapseActive**(): {{core.link.Command}}\ | ||
Query if the current heading is collapsed. | ||
- **collapseHeading**(): {{core.link.Command}}\ | ||
Hides every node below that is not heading or has a heading `level` less than that of the current heading. | ||
- **uncollapseHeading**(): {{core.link.Command}}\ | ||
Brings back all the hidden nodes of a collapsed heading. Will only uncollapse shallowly i.e a collapse heading inside of a collapsed heading will not be uncollapsed. | ||
- **toggleHeadingCollapse**(): {{core.link.Command}}\ | ||
Collapses an uncollapsed heading and vice versa. | ||
- **uncollapseAllHeadings**(): {{core.link.Command}}\ | ||
Uncollapses all headings in the `doc`. Will also deep uncollapse every heading that was inside of a collapsed heading. | ||
**On Heading collapse** | ||
The heading component also allows you to collapse and uncollapse any content, after the current heading, that is not of type heading or has a heading of level greater than the current heading. A collapsed heading will have a class name of `bangle-heading-collapsed` to allow for styling. A collapsed heading will save the collapsed data in a JSON string under the dom attribute `data-bangle-attrs`. | ||
:warning: For serializing to Markdown you will have to uncollapse your document, since markdown doesn't support collapsing. You can run the command`uncollapseAllHeadings` before serializing to markdown to avoid this problem. | ||
On top of the collapse commands, the component also exports the following helper functions to help with collapse functionality: | ||
- **listCollapsibleHeading**(state: {{Prosemirror.EditorState}}): \[{node: {{Prosemirror.Node}}, pos: number}\] \ | ||
Lists all the headings that can be collapsed or uncollapsed. | ||
- **listCollapsedHeading**(state: {{Prosemirror.EditorState}}): \[{node: {{Prosemirror.Node}}, pos: number}\]\ | ||
Lists all the headings that are currently collapsed. | ||
**Usage** | ||
@@ -568,0 +599,0 @@ |
import { setBlockType } from 'prosemirror-commands'; | ||
import { textblockTypeInputRule } from 'prosemirror-inputrules'; | ||
import { findChildren } from 'prosemirror-utils'; | ||
import { Fragment } from 'prosemirror-model'; | ||
import { TextSelection } from 'prosemirror-state'; | ||
import { keymap } from '../utils/keymap'; | ||
@@ -26,2 +30,3 @@ import { copyEmptyCommand, cutEmptyCommand, moveNode } from '../core-commands'; | ||
insertEmptyParaBelow: 'Mod-Enter', | ||
toggleCollapse: null, | ||
}; | ||
@@ -49,2 +54,5 @@ | ||
}, | ||
collapseContent: { | ||
default: null, | ||
}, | ||
}, | ||
@@ -58,7 +66,27 @@ content: 'inline*', | ||
tag: `h${level}`, | ||
attrs: { level: parseLevel(level) }, | ||
getAttrs: (dom) => { | ||
const result = { level: parseLevel(level) }; | ||
const attrs = dom.getAttribute('data-bangle-attrs'); | ||
if (!attrs) { | ||
return result; | ||
} | ||
const obj = JSON.parse(attrs); | ||
return Object.assign({}, result, obj); | ||
}, | ||
}; | ||
}), | ||
toDOM: (node) => { | ||
return [`h${node.attrs.level}`, {}, 0]; | ||
const result = [`h${node.attrs.level}`, {}, 0]; | ||
if (node.attrs.collapseContent) { | ||
result[1]['data-bangle-attrs'] = JSON.stringify({ | ||
collapseContent: node.attrs.collapseContent, | ||
}); | ||
result[1]['class'] = 'bangle-heading-collapsed'; | ||
} | ||
return result; | ||
}, | ||
@@ -119,2 +147,3 @@ }, | ||
), | ||
[keybindings.toggleCollapse]: toggleHeadingCollapse(), | ||
}), | ||
@@ -162,1 +191,288 @@ ...(markdownShortcut ? levels : []).map((level) => | ||
} | ||
export function queryIsCollapseActive() { | ||
return (state) => { | ||
const match = findParentNodeOfType(state.schema.nodes[name])( | ||
state.selection, | ||
); | ||
if (!match || !isCollapsible(match)) { | ||
return false; | ||
} | ||
return Boolean(match.node.attrs.collapseContent); | ||
}; | ||
} | ||
export function collapseHeading() { | ||
return (state, dispatch) => { | ||
const match = findParentNodeOfType(state.schema.nodes[name])( | ||
state.selection, | ||
); | ||
if (!match || !isCollapsible(match)) { | ||
return false; | ||
} | ||
const isCollapsed = queryIsCollapseActive()(state, dispatch); | ||
if (isCollapsed) { | ||
return false; | ||
} | ||
const result = findCollapseFragment(match.node, state.doc); | ||
if (!result) { | ||
return false; | ||
} | ||
const { fragment, start, end } = result; | ||
let tr = state.tr.replaceWith( | ||
start, | ||
end, | ||
state.schema.nodes[name].createChecked( | ||
{ | ||
...match.node.attrs, | ||
collapseContent: fragment.toJSON(), | ||
}, | ||
match.node.content, | ||
), | ||
); | ||
if (state.selection instanceof TextSelection) { | ||
tr = tr.setSelection(TextSelection.create(tr.doc, state.selection.from)); | ||
} | ||
if (dispatch) { | ||
dispatch(tr); | ||
} | ||
return true; | ||
}; | ||
} | ||
export function uncollapseHeading() { | ||
return (state, dispatch) => { | ||
const match = findParentNodeOfType(state.schema.nodes[name])( | ||
state.selection, | ||
); | ||
if (!match || !isCollapsible(match)) { | ||
return false; | ||
} | ||
const isCollapsed = queryIsCollapseActive()(state, dispatch); | ||
if (!isCollapsed) { | ||
return false; | ||
} | ||
const frag = Fragment.fromJSON( | ||
state.schema, | ||
match.node.attrs.collapseContent, | ||
); | ||
let tr = state.tr.replaceWith( | ||
match.pos, | ||
match.pos + match.node.nodeSize, | ||
Fragment.fromArray([ | ||
state.schema.nodes[name].createChecked( | ||
{ | ||
...match.node.attrs, | ||
collapseContent: null, | ||
}, | ||
match.node.content, | ||
), | ||
]).append(frag), | ||
); | ||
if (state.selection instanceof TextSelection) { | ||
tr = tr.setSelection(TextSelection.create(tr.doc, state.selection.from)); | ||
} | ||
if (dispatch) { | ||
dispatch(tr); | ||
} | ||
return true; | ||
}; | ||
} | ||
export function toggleHeadingCollapse() { | ||
return (state, dispatch) => { | ||
const match = findParentNodeOfType(state.schema.nodes[name])( | ||
state.selection, | ||
); | ||
if (!match || match.depth !== 1) { | ||
return null; | ||
} | ||
const isCollapsed = queryIsCollapseActive()(state, dispatch); | ||
return isCollapsed | ||
? uncollapseHeading()(state, dispatch) | ||
: collapseHeading()(state, dispatch); | ||
}; | ||
} | ||
export function uncollapseAllHeadings() { | ||
const flattenFragmentJSON = (fragJSON) => { | ||
let result = []; | ||
fragJSON.forEach((nodeJSON) => { | ||
if (nodeJSON.type === 'heading' && nodeJSON.attrs.collapseContent) { | ||
const collapseContent = nodeJSON.attrs.collapseContent; | ||
result.push({ | ||
...nodeJSON, | ||
attrs: { | ||
...nodeJSON.attrs, | ||
collapseContent: null, | ||
}, | ||
}); | ||
result.push(...flattenFragmentJSON(collapseContent)); | ||
} else { | ||
result.push(nodeJSON); | ||
} | ||
}); | ||
return result; | ||
}; | ||
return (state, dispatch) => { | ||
const collapsibleNodes = listCollapsedHeading(state); | ||
let tr = state.tr; | ||
let offset = 0; | ||
for (const { node, pos } of collapsibleNodes) { | ||
let baseFrag = Fragment.fromJSON( | ||
state.schema, | ||
flattenFragmentJSON(node.attrs.collapseContent), | ||
); | ||
tr = tr.replaceWith( | ||
offset + pos, | ||
offset + pos + node.nodeSize, | ||
Fragment.fromArray([ | ||
state.schema.nodes[name].createChecked( | ||
{ | ||
...node.attrs, | ||
collapseContent: null, | ||
}, | ||
node.content, | ||
), | ||
]).append(baseFrag), | ||
); | ||
offset += baseFrag.size; | ||
} | ||
if (state.selection instanceof TextSelection) { | ||
tr = tr.setSelection(TextSelection.create(tr.doc, state.selection.from)); | ||
} | ||
if (dispatch) { | ||
dispatch(tr); | ||
} | ||
return true; | ||
}; | ||
} | ||
export function listCollapsedHeading(state) { | ||
return findChildren( | ||
state.doc, | ||
(node) => | ||
node.type === state.schema.nodes[name] && | ||
Boolean(node.attrs.collapseContent), | ||
false, | ||
); | ||
} | ||
export function listCollapsibleHeading(state) { | ||
return findChildren( | ||
state.doc, | ||
(node) => node.type === state.schema.nodes[name], | ||
false, | ||
); | ||
} | ||
// TODO | ||
/** | ||
* | ||
* collapse all headings of given level | ||
*/ | ||
// export function collapseHeadings(level) {} | ||
/** | ||
* Collapsible headings are only allowed at depth of 1 | ||
*/ | ||
function isCollapsible(match) { | ||
if (match.depth !== 1) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
function findCollapseFragment(matchNode, doc) { | ||
// Find the last child that will be inside of the collapse | ||
let start = undefined; | ||
let end = undefined; | ||
let isDone = false; | ||
const breakCriteria = (node) => { | ||
if (node.type !== matchNode.type) { | ||
return false; | ||
} | ||
if (node.attrs.level <= matchNode.attrs.level) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
doc.forEach((node, offset, index) => { | ||
if (isDone) { | ||
return; | ||
} | ||
if (node === matchNode) { | ||
start = { index, offset, node }; | ||
return; | ||
} | ||
if (start) { | ||
if (breakCriteria(node)) { | ||
isDone = true; | ||
return; | ||
} | ||
// Avoid including trailing empty nodes | ||
// (like empty paragraphs inserted by trailing-node-plugins) | ||
// This is done to prevent trailing-node from inserting a new empty node | ||
// every time we toggle on off the collapse. | ||
if (node.content.size !== 0) { | ||
end = { index, offset, node }; | ||
} | ||
} | ||
}); | ||
if (!end) { | ||
return null; | ||
} | ||
// We are not adding parents position (doc will be parent always) to | ||
// the offsets since it will be 0 | ||
const slice = doc.slice( | ||
start.offset + start.node.nodeSize, | ||
end.offset + end.node.nodeSize, | ||
); | ||
return { | ||
fragment: slice.content, | ||
start: start.offset, | ||
end: end.offset + end.node.nodeSize, | ||
}; | ||
} |
{ | ||
"name": "@bangle.dev/core", | ||
"version": "0.1.16", | ||
"version": "0.2.0", | ||
"homepage": "https://bangle.dev", | ||
@@ -5,0 +5,0 @@ "authors": [ |
import { NodeSelection } from 'prosemirror-state'; | ||
import { Fragment, Slice } from 'prosemirror-model'; | ||
@@ -16,1 +17,10 @@ export * from './commands-helpers'; | ||
}; | ||
/** | ||
* | ||
* @param {*} schema | ||
* @param {*} psxArray An array of psx nodes eg. [<para>hi</para>, <para>bye</para>] | ||
*/ | ||
export const createPSXFragment = (schema, psxArray) => { | ||
return Fragment.fromArray(psxArray.map((r) => r(schema))); | ||
}; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
489609
93
17615