Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bangle.dev/core

Package Overview
Dependencies
Maintainers
1
Versions
143
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bangle.dev/core - npm Package Compare versions

Comparing version 0.1.16 to 0.2.0

components/__tests__/collapsible-heading.test.js

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,
};
}

2

package.json
{
"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)));
};
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc