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 {
26 export class Outline {
29 constructor(outlineData: RawOutline) {
30 this.data = outlineData;
33 findNodeInTree(root: OutlineTree, id: string, action: (item: OutlineTree, parent: OutlineTree) => void, runState: boolean = false) {
38 _.each(root.children, (childNode, idx) => {
39 if(childNode.id === id) {
40 action(childNode, root);
44 else if(childNode.children) {
45 this.findNodeInTree(childNode, id, action, run);
50 fold(nodeId: string) {
51 this.findNodeInTree(this.data.tree, nodeId, item => {
52 item.collapsed = true;
56 unfold(nodeId: string) {
57 this.findNodeInTree(this.data.tree, nodeId, item => {
58 item.collapsed = true;
62 flattenOutlineTreeChildren(tree: OutlineTree): string[] {
63 return tree.children.map(node => node.id);
66 liftNodeToParent(nodeId: string) {
68 let targetNode: OutlineTree, parentNode: OutlineTree;
69 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
71 this.findNodeInTree(this.data.tree, pNode.id, (originalParentNode, newParentNode) => {;
75 parentNode = newParentNode;
78 const flatId = newParentNode.children.map(n => n.id);
80 const originalNodePosition = originalParentNode.children.map(n => n.id).indexOf(targetNode.id);
81 const newNodePosition = flatId.indexOf(originalParentNode.id);
83 originalParentNode.children.splice(originalNodePosition, 1);
85 newParentNode.children.splice(newNodePosition + 1, 0, targetNode);
95 lowerNodeToChild(nodeId: string) {
97 // find the previous sibling
98 // make this node a child of the sibling node
99 let targetNode: OutlineTree, newParentNode: OutlineTree, oldParentNode: OutlineTree;
100 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
107 let idList = pNode.children.map(n => n.id);
108 // there are no other siblings so we can't do anything
109 if(idList.length === 1) {
113 const position = idList.indexOf(targetNode.id);
114 const prevSiblingPosition = position - 1;
116 pNode.children[prevSiblingPosition].children.splice(0, 0, targetNode);
117 pNode.children.splice(position, 1);
119 newParentNode = pNode.children[prevSiblingPosition];
120 oldParentNode = pNode;
129 swapNodeWithNextSibling(nodeId: string) {
130 let targetNode: OutlineTree, parentNode: OutlineTree;
131 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
134 const flatId = parentNode.children.map(n => n.id);
135 const nodePosition = flatId.indexOf(targetNode.id);
137 if(nodePosition === (flatId.length - 1)) {
138 // this is the last node in the list, there's nothing to swap
142 // remove the node from this point, and push it one later
143 parentNode.children.splice(nodePosition, 1);
144 parentNode.children.splice(nodePosition + 1, 0, targetNode);
153 swapNodeWithPreviousSibling(nodeId: string) {
154 let targetNode: OutlineTree, parentNode: OutlineTree;
155 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
158 const flatId = parentNode.children.map(n => n.id);
159 const nodePosition = flatId.indexOf(targetNode.id);
161 if(nodePosition === 0) {
162 // this is the first node in the list, there's nothing to swap
166 // remove the node from this point, and push it one later
167 parentNode.children.splice(nodePosition, 1);
168 parentNode.children.splice(nodePosition - 1, 0, targetNode);
177 createSiblingNode(targetNode: string, nodeData?: OutlineNode) {
178 const outlineNode: OutlineNode = nodeData || {
185 this.data.contentNodes[outlineNode.id] = outlineNode;
187 let parentNode: OutlineTree;
189 this.findNodeInTree(this.data.tree, targetNode, (node, parent) => {
190 const position = parent.children.map(n => n.id).indexOf(targetNode);
191 parent.children.splice(position + 1, 0, {
206 createChildNode(currentNode: string, nodeId?: string) {
207 const node: OutlineNode = nodeId ? this.data.contentNodes[nodeId] :
216 this.data.contentNodes[node.id] = node;
219 let parentNode: OutlineTree;
221 this.findNodeInTree(this.data.tree, currentNode, (foundNode, parent) => {
222 foundNode.children.unshift({
228 parentNode = foundNode;
237 removeNode(nodeId: string) {
239 let removedNode: OutlineTree, parentNode: OutlineTree;
240 this.findNodeInTree(this.data.tree, nodeId, (tNode, pNode) => {
248 let position = parentNode.children.map(n => n.id).indexOf(tNode.id);
250 parentNode.children.splice(position, 1);
259 updateContent(id: string, content: string) {
260 if(!this.data.contentNodes[id]) {
261 throw new Error('Invalid node');
264 this.data.contentNodes[id].content = content;
267 renderContent(nodeId: string): string {
268 let node = this.data.contentNodes[nodeId];
272 content = marked.parse(node.content);
275 content = node.content;
282 renderNode(node: OutlineTree): string {
283 if(node.id === '000000') {
284 return this.render();
286 const collapse = node.collapsed ? 'collapsed': 'expanded';
287 const content: OutlineNode = this.data.contentNodes[node.id] || {
294 let html = `<div class="node ${collapse}" data-id="${node.id}" id="id-${node.id}">
295 <div class="nodeContent" data-type="${content.type}">
296 ${this.renderContent(node.id)}
298 ${node.children.length ? _.map(node.children, this.renderNode.bind(this)).join("\n") : ''}
306 * render starts at the root node and only renders its children. The root
307 * node only exists as a container around the rest to ensure a standard format
310 return _.map(this.data.tree.children, this.renderNode.bind(this)).join("\n");