1 import * as _ from 'lodash';
2 import { v4 as uuid } from 'uuid';
3 import { marked } from 'marked';
5 export interface RawOutline {
10 contentNodes: Record<string, OutlineNode>
13 export interface OutlineTree {
15 children: OutlineTree[]
19 export interface OutlineNode {
24 strikethrough: boolean;
27 export class Outline {
30 constructor(outlineData: RawOutline) {
31 this.data = outlineData;
34 findNodeInTree(root: OutlineTree, id: string, action: (item: OutlineTree, parent: OutlineTree) => void, runState: boolean = false) {
39 _.each(root.children, (childNode, idx) => {
40 if(childNode.id === id) {
41 action(childNode, root);
45 else if(childNode.children) {
46 this.findNodeInTree(childNode, id, action, run);
51 fold(nodeId: string) {
52 this.findNodeInTree(this.data.tree, nodeId, item => {
53 item.collapsed = true;
57 unfold(nodeId: string) {
58 this.findNodeInTree(this.data.tree, nodeId, item => {
59 item.collapsed = false;
63 flattenOutlineTreeChildren(tree: OutlineTree): string[] {
64 return tree.children.map(node => node.id);
67 liftNodeToParent(nodeId: string) {
69 let targetNode: OutlineTree, parentNode: OutlineTree;
70 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
72 this.findNodeInTree(this.data.tree, pNode.id, (originalParentNode, newParentNode) => {;
76 parentNode = newParentNode;
79 const flatId = newParentNode.children.map(n => n.id);
81 const originalNodePosition = originalParentNode.children.map(n => n.id).indexOf(targetNode.id);
82 const newNodePosition = flatId.indexOf(originalParentNode.id);
84 originalParentNode.children.splice(originalNodePosition, 1);
86 newParentNode.children.splice(newNodePosition + 1, 0, targetNode);
96 lowerNodeToChild(nodeId: string) {
98 // find the previous sibling
99 // make this node a child of the sibling node
100 let targetNode: OutlineTree, newParentNode: OutlineTree, oldParentNode: OutlineTree;
101 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
108 let idList = pNode.children.map(n => n.id);
109 // there are no other siblings so we can't do anything
110 if(idList.length === 1) {
114 const position = idList.indexOf(targetNode.id);
115 const prevSiblingPosition = position - 1;
117 pNode.children[prevSiblingPosition].children.splice(0, 0, targetNode);
118 pNode.children.splice(position, 1);
120 newParentNode = pNode.children[prevSiblingPosition];
121 oldParentNode = pNode;
130 swapNodeWithNextSibling(nodeId: string) {
131 let targetNode: OutlineTree, parentNode: OutlineTree;
132 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
135 const flatId = parentNode.children.map(n => n.id);
136 const nodePosition = flatId.indexOf(targetNode.id);
138 if(nodePosition === (flatId.length - 1)) {
139 // this is the last node in the list, there's nothing to swap
143 // remove the node from this point, and push it one later
144 parentNode.children.splice(nodePosition, 1);
145 parentNode.children.splice(nodePosition + 1, 0, targetNode);
154 swapNodeWithPreviousSibling(nodeId: string) {
155 let targetNode: OutlineTree, parentNode: OutlineTree;
156 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
159 const flatId = parentNode.children.map(n => n.id);
160 const nodePosition = flatId.indexOf(targetNode.id);
162 if(nodePosition === 0) {
163 // this is the first node in the list, there's nothing to swap
167 // remove the node from this point, and push it one later
168 parentNode.children.splice(nodePosition, 1);
169 parentNode.children.splice(nodePosition - 1, 0, targetNode);
178 createSiblingNode(targetNode: string, nodeData?: OutlineNode) {
179 const outlineNode: OutlineNode = nodeData || {
187 this.data.contentNodes[outlineNode.id] = outlineNode;
189 let parentNode: OutlineTree;
191 this.findNodeInTree(this.data.tree, targetNode, (node, parent) => {
192 const position = parent.children.map(n => n.id).indexOf(targetNode);
193 parent.children.splice(position + 1, 0, {
208 createChildNode(currentNode: string, nodeId?: string) {
209 const node: OutlineNode = nodeId ? this.data.contentNodes[nodeId] :
219 this.data.contentNodes[node.id] = node;
222 let parentNode: OutlineTree;
224 this.findNodeInTree(this.data.tree, currentNode, (foundNode, parent) => {
225 foundNode.children.unshift({
231 parentNode = foundNode;
240 removeNode(nodeId: string) {
242 let removedNode: OutlineTree, parentNode: OutlineTree;
243 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
251 let position = parentNode.children.map(n => n.id).indexOf(tNode.id);
253 parentNode.children.splice(position, 1);
262 updateContent(id: string, content: string) {
263 if(!this.data.contentNodes[id]) {
264 throw new Error('Invalid node');
267 this.data.contentNodes[id].content = content;
270 renderContent(nodeId: string): string {
271 let node = this.data.contentNodes[nodeId];
275 content = marked.parse(node.content);
278 content = node.content;
285 renderNode(node: OutlineTree): string {
286 if(node.id === '000000') {
287 return this.render();
289 const collapse = node.collapsed ? 'collapsed': 'expanded';
290 const content: OutlineNode = this.data.contentNodes[node.id] || {
298 const strikethrough = content.strikethrough ? 'strikethrough' : '';
300 let html = `<div class="node ${collapse} ${strikethrough}" data-id="${node.id}" id="id-${node.id}">
301 <div class="nodeContent" data-type="${content.type}">
302 ${this.renderContent(node.id)}
304 ${node.children.length ? _.map(node.children, this.renderNode.bind(this)).join("\n") : ''}
312 * render starts at the root node and only renders its children. The root
313 * node only exists as a container around the rest to ensure a standard format
316 return _.map(this.data.tree.children, this.renderNode.bind(this)).join("\n");