- Move ← ↑ → ↓ or tap on target to navigate
- Double tap to view overflow content or select text
[Mouse]
- Click & drag (← ↑ → ↓)
[Scroll wheel]
- Scroll (↑ ↓)
- Shift key + scroll (← →)
[Keyboard]
- Arrow keys (← ↑ → ↓)
- Spacebar / Shift + Spacebar to skip multiple nodes (↑ ↓)
- PageUp / PageDown to skip multiple nodes (↑ ↓)
- Home / End to skip to first or last node (↑ ↓)
- Shift + [PageUp / PageDown / Home / End] (← →)
[Touch]
- Swipe (← ↑ → ↓)
[Mouse/Touch]
- Tap on target
(to view overflow content or select text)
[Mouse/Touch]
- Double tap
[Keyboard]
- Enter key
Noodel - which stands for node view model - is a beautiful, dynamic user interface for digital content. It offers a way to present content that is modern, intuitive, and adaptable.
Noodel is currently available as Noodel.js, an open source JavaScript library for use in web-based projects.
Noodel requires you to arrange your content into a tree of nodes, where each node contains a distinct chunk of content.
Your content tree will be presented on an infinite canvas that you can navigate by simply moving up, down, left and right.
Blocks of content will show or hide as you move through the tree, keeping only the relevant subtree visible. Think of it like an infinite cascade of drop-down menus - but much more elegant and useful.
As websites and applications grow larger and more complicated, so too will UI features provided for navigation - like table of contents, menus and sidebars. However, these components add clutter to the UI, and are generally hard to use on small screens and touch devices.
Noodel is a system designed from the ground up that aims to present content in a better way. Originally intended as an idea for navigating tree hierarchies, it has evolved to become an incredibly versatile system for displaying content in general. Inspired by the "right = enter, left = exit" paradigm of UI design, as well as 4-way swipe motions that are increasingly utilised by mobile apps, Noodel allows you to integrate navigation with the content organically without requiring any extra UI components.
At the maximum, you can use Noodel to present a full website or app as an interactive content tree. It can be used for virtually anything - from static blogs or wikis, to dynamic apps with complex control flow.
At the minimum, you can use Noodel as a versatile UI plugin for responsive slideshows or hierachical data.
Noodel is simple and intuitive. Most people immediately understand how it works. It aims to leverage the user's spatial memory for navigation, and reduce screen clutter and cognitive load.
Noodel's layout and navigation mechanism remains identical regardless of screen size and input method. By reducing the essential controls to 4-way movements, it can be easily used in contexts where control ability is limited.
Everything about Noodel is designed to be flexible - from its fully customizable styles and layout, to what kind of content you can put in.
Noodel is more than just a static content viewer, although you can certainly use it that way. It provides an extensive API with navigation methods and getters, and allows you to insert, update or delete nodes at any time.
Noodel is built with performance in mind using modern best practices. It uses the powerful Vue.js library under the hood for DOM management and updates.
And finally, who doesn't like an interface that feels like it walked out from a sci-fi movie? Noodel is responsive, interactive and comes with fancy animation effects. It's just cool.
The container for a unit of content in Noodel is called a node - just like the one you're looking at right now. Nodes are the basic building blocks for your content.
Each node can have one or more ordered child nodes that have their own units of sub-content. This can repeat infinitely to form a tree of any size.
Noodel encourages you to break down large blocks of content into smaller chunks, which is generally good for readability.
A node should never exceed the size of the viewport - a precondition for node-based navigation.
A noodel (i.e. node view model) refers to the whole content tree formed by nodes.
If a node has children, it must have exactly 1 child that's active. The descendants of the active child will be shown while the others hidden.
At any given time, only the children of nodes on the lineage of active nodes starting from the root are shown. This tree of visible content is called the active tree.
The container (slider) for a list of sibling nodes is called a branch.
In other words, a branch refers to a node's list of children. Each branch is always uniquely associated with a node - the parent node of the branch.
The axis on which nodes are ordered in branches is called the branch axis. It is vertical (top to bottom) by default.
The container (slider) for the list of branches in the active tree, ordered from ancestors to descendants, is called the trunk.
The axis on which branches are ordered in the trunk is called the trunk axis. It is horizontal (left to right) by default.
The root node is an invisible node that serves as the parent of the topmost branch (i.e. the root of the tree), and always exists.
The level refers to the depth of a node (or branch) in the tree. The root node has level 0, its children (and branch) has level 1, and so on.
The branch that the view is currently focusing on is the focal branch. Moving in the trunk axis changes the focal branch.
The active node inside the focal branch is the focal node. Moving in the branch axis changes the active node of the focal branch.
The focal node is the node the view is centred on, and currently interacting with the user.
You can use Noodel by directly embedding its script and CSS into your HTML:
// You should specify the version number (e.g noodel@3.0.0) in production
<script src="https://cdn.jsdelivr.net/npm/noodel/dist/noodel.umd.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/noodel/dist/noodel.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/noodel/dist/noodel-default.min.css">
Noodel can be obtained via npm-linked CDNs such as jsdelivr and unpkg. It is bundled as a UMD package and available in minimized (production) and non-minimized (development) versions.
There is a separate build of Noodel called noodel-no-vue.umd.js. This version comes
without Vue.js bundled and is intended to be used in Vue projects where the Vue
script is already included, to prevent including Vue twice. See Usage with Vue for more details.
For more complex projects that use bundlers, install from npm:
npm install noodel
And import into your code:
import Noodel from 'noodel'
// Don't forget to also include the CSS from Noodel's module directory
Noodel comes with a "core" stylesheet, noodel.css,
and a theming stylesheet, noodel-default.css.
The core stylesheet contains styles critical for Noodel's functioning and needs to be included in all cases.
The theming stylesheet provides a default grayscale theme with basic transitions for Noodel's visual effects. You can include this stylesheet and tweak it for customization, or replace it altogether with your own custom theme. See Styling for more details.
Creating a noodel is simple. The basic operations are:
// Creates a view model based on the given content tree
const noodel = new Noodel(contentTree);
// Renders the view into an element on the page
noodel.mount(container);
The contentTree is an element selector or array of objects that specifies the content tree, and container is the target container element for placing the noodel.
For sites driven by static HTML templates, you can define the tree via HTML and give the root element to the constructor:
<div id="noodelDefinition" style="display: none;">
<div class="node">
<p>My first node</p>
</div>
<div class="node">
<p>My second node</p>
<div class="node">
<p>First child of my second node</p>
</div>
</div>
</div>
<div id="noodel"></div>
<script>
const noodel = new Noodel("#noodelDefinition");
noodel.mount("#noodel");
</script>
Noodel will traverse the descendants of the container element and convert
all <div>s with
class="node" into
Noodel's nodes, keeping the parent-child relationships. Any text or
elements inside a node that are not child nodes will be treated as the
content of that node, concatenated in the order they appear.
This concatenation preserves the markup of the content elements so you have complete freedom in structuring and styling the content of each node.
Note that the actual DOM nodes in the definition will not be placed into Noodel's element tree. This means that side effects such as event listeners attached to them will not be carried over.
Alternatively, for dynamic sites/apps driven by JavaScript, you can specify the content tree as an array of objects corresponding to nodes on the first level:
const contentTree = [
{
content: "<p>My first node</p>"
},
{
content: "<p>My second node</p>"
children: [
{
content: "<p>First child of my second node</p>"
}
]
}
];
const noodel = new Noodel(contentTree);
noodel.mount("#noodel");
⚠️ Always use trusted HTML strings as content. Never use unsanitized strings from user input.
Setting options can only be done via JavaScript. If your noodel is defined by an HTML template you still need to set your noodel and node options programmatically.
// setting options for individual nodes
const nodeDefinition = {
options: {
showChildIndicator: false,
focalAnchorTrunk: (size) => size / 3
}
}
// setting global options for the noodel
const noodel = new Noodel([nodeDefinition], {
visibleSubtreeDepth: 5,
focalAnchorTrunk: (size) => size / 4
});
By default, a unique ID will be generated for each node, which will be used for various purposes. You can provide your own ID for nodes using a data attribute:
<div class="node" data-id="myCustomId">
...
</div>
or in JavaScript:
const nodeDefinition = {
id: "myCustomId",
}
Your custom ID must be unique and should not start with "_" as it may conflict with Noodel's default IDs.
By default, the initial active child for each node is at index 0 (the first child). You can specify another child to be initially active by using:
<div class="node" data-active>
...
</div>
or in JavaScript:
const nodeDefinitions = [
{
content: "First child"
},
{
isActive: true, // this node will be active instead
content: "Second child"
},
];
You can assign custom CSS classes/styles to the various DOM elements associated with each node. See here for the list of different elements you can style.
<div
class="node"
data-class-node="myClassA myClassB"
data-class-content-box="myClassC"
data-style-content-box="color: green; max-width: 900px;"
>
...
</div>
or in JavaScript:
const nodeWithCustomStyles = {
classNames: {
node: "myClassA myClassB",
contentBox: "myClassC"
},
style: {
contentBox: "color: green; max-width: 900px;"
}
}
You can optionally define content that will be associated with a node's child branch rather than individual nodes. This branch content will only be displayed if the node has a branch (i.e. has at least 1 child).
For HTML templates this is specified via a div with the class "branch-content":
<div class="node" >
Some node content...
<div class="branch-content">
Any content inside here will be treated as branch content
</div>
</div>
or in JavaScript:
const nodeDefinition = {
content: "node with branch content",
branchContent: "this is a special branch"
}
You can attach one or more event listeners for each event on both the noodel and its individual nodes. See here for the list of events available.
const noodel = new Noodel([]);
const onFocalNodeChange = () => console.log("focal node has changed");
const anotherListener = () => console.log("another listener triggered");
noodel.on("focalNodeChange", onFocalNodeChange); // attach listener
noodel.on("focalNodeChange", anotherListener); // attach another listener
noodel.off("focalNodeChange", onFocalNodeChange); // detach listener
By default, a noodel responds to four types of user input for navigation: swipe(drag), keyboard, scroll wheel, and tap. Navigation will occur even when such input originates from content elements inside nodes.
This behaviour can be undesirable when you have UI elements that should capture those inputs and prevent them from causing navigation, e.g. a slider or a textbox.
To address this issue, Noodel provides the following "prevention classes" that you can selectively apply to individual elements in your content:
nd-prevent-swipend-prevent-keynd-prevent-wheelnd-prevent-tapNoodel will ignore the input that originates from an element (or its descendants) with the corresponding prevention class and stops it from triggering any Noodel-specific behaviour.
<div class="node">
<input type="text" class="nd-prevent-swipe nd-prevent-key"></input>
<button class="nd-prevent-tap">Submit</button>
</div>
You should use prevention classes sparingly, on elements that really need them. Having large blocks of content with navigation prevention may impact usability, especially on smaller screens.
For situations that require extensive navigation prevention, you should rely on the inspect mode instead. Alternatively, you can disable the corresponding input in Noodel's options and provide your own means of navigation via Noodel's API.
For swipe-based interfaces such as Noodel, there is always an unavoidable conflict between the swipe mechanism and native mechanisms that depend on the same motion, such as text selection.
The inspect mode is the solution Noodel offers to address this issue. By default, text selection is disabled on a noodel and all input directed towards navigation. Once inspect mode is on, all navigation methods will be suspended and the user is free to select text or perform other conflicting interactions e.g. drag-and-drop in the focal node.
It is recommended that you rely on inspect mode, in addition to prevention classes, to deal with situations where UI elements in your content conflict with Noodel's mechanism.
The default trigger for inspect mode is the double tap (on any area of the noodel), or the Enter key on a keyboard.
This trigger is chosen for maximum convenience and compatibility, but like anything else it does end up conflicting with interactions such as multiple clicks to select a word/line. If you wish to prevent the default trigger you can always disable it through Noodel's options and provide your own means of toggling inspect mode (e.g a custom button) via Noodel's API.
The inspect mode is also used for viewing overflow content in nodes. Scrolling inside nodes is disabled normally but enabled during inspect mode.
Noodel is based on the principle of content chunking. Because each node has a limited space, as a general rule, you should try to break down large blocks of content into bite-sized pieces that can fit into individual nodes.
However, this can be impractical in many situations, so inspect mode is provided as a fallback for viewing overflow content.
The object instance you get when you create a noodel is its "view model". You can also obtain the view model of individual nodes via methods on the noodel instance.
These view models have 2-way sync with the view, and provide extensive API methods for you to dynamically query and update almost every aspect of the content tree.
// view model of whole noodel
const noodel = new Noodel(...);
// view model of an individual node
const currentFocalNode = noodel.getFocalNode();
Each view model of a node is uniquely associated with a node in the tree. This means you can do things such as compare the equality of two nodes by simply comparing the reference of their view model objects.
const noodel = new Noodel(...);
const node = noodel.findNodeById("someNode");
// move to the next node
noodel.moveForward();
// move 2 steps back towards ancestors
noodel.moveOut(2);
// move to the 3rd branch in the active tree
noodel.setFocalLevel(3);
// jump to a specific node
node.jumpToFocus();
const node = noodel.findNodeById("someNode");
// update node content
node.setContent("new content");
// update node styles
node.setStyles({
contentBox: "color: green"
});
// update node options
node.setOptions({
focalAnchorTrunk: () => 100
})
Note that updating content replaces the original content by replacing the innerHTML of the container. The original content elements will be removed.
const rootNode = new Noodel([{}]).getRoot();
// insert nodes and their descendants at index 0
rootNode.insertChildren([
{
content: "Fruits!",
children: [
{
content: "Tomatoes!"
}
]
},
{
content: "Vegetables!"
}
], 0);
// you can also insert new nodes from an HTML template
rootNode.insertChildren("#nodeDefinition");
// delete nodes
rootNode.deleteChildren(1, 2);
Noodel will always try to preserve the current focal node during insertion and deletion, if possible. This means that if the current focal node is at index 3, and you insert a node at index 1, the focal node becomes the node at index 4, and no navigation will occur.
Navigation only happens when a deletion includes the focal node. In that case the focus will go to an adjacent node or branch, as necessary.
The noodel's view state is independent of whether it's mounted or not. You can mutate it while the noodel is not mounted and the change will persist.
const noodel = new Noodel(...);
// make jump before view is mounted
noodel.jumpTo(noodel.findNodeById("firstFocus"));
// the view will focus on the "firstFocus" node initially
noodel.mount("#container");
// the view model is still focused on the same node after unmount
noodel.unmount();
noodel.getFocalNode().getId(); // ==> "firstFocus"
When using Noodel's API, all changes to Noodel's view model are synchronous, while any DOM updates triggered by these changes will be asynchronous. This means that, if you need to do operations that depends on the updated DOM state, you must wait for the updates to settle before doing so.
The nextTick() function is Noodel's preferred
way to wait for DOM updates to fully complete and settle.
One common use case is to wait for the view to mount before querying a DOM element:
noodel.mount("#noodel");
noodel.nextTick(() => {
// gets the canvas element after the view is actually mounted
const canvasEl = noodel.getEl();
});
Another use for nextTick() is to properly sequence
certain visual effects:
// this operation inserts a node, then jumps to it
const newNode = someNode.insertChildren([{}], 1)[0];
// Because it takes one DOM update cycle to capture the size of new nodes, the movement may glitch if you do this synchronously. Using nextTick will make the animation smooth.
noodel.nextTick(() => {
newNode.jumpToFocus();
});
If you find the visual effects of some compound operations not going as expected, often
nextTick() will do the trick.
Noodel has a variety of layout options that, when used in combination, can customize the layout in almost every way you can think of.
To use these options properly, you need to understand the concepts of the branch axis and the trunk axis, which are used in place of the horizontal and vertical axes depending on the noodel's orientation.
The orientation option specifies the direction of the
trunk axis, and can be either ltr (default), rtl, ttb (top-to-bottom) and btt
(bottom-to-top).
The branch direction option specifies how a branch layout
its nodes, and can be either "normal" (default) or "reverse". When
the trunk axis is the horizontal axis (i.e. ltr or rtl), "normal" puts nodes from top to
bottom, and "reverse" the opposite. When the trunk axis is the vertical axis,
"normal" puts nodes from left to right, and "reverse" the opposite.
The focal offsets specify the "snap-to lines" on the canvas where the focal branch and active nodes align to, on their respective axis. These are calculated as a function of the canvas length, and defaults to 1/2 of the canvas length (i.e. the center).
The anchor offsets specify the "snap-to lines" on each branch/node that should be aligned to the focal offset, on their respective axis, when they become focal or active. These are calculated as a function of the length of the branch/node, and defaults to 1/2 of their length (i.e. the center).
These offsets can be specified globally and/or customized on each individual node. When used in combination, you can decide how and where the branches and nodes align in a highly flexible way.
Note that you can return just a constant in these functions for a fixed offset.
Also note that the "origins" referenced by these offsets are different depending on the orientation and branch direction. E.g. For an ltr layout with normal branch direction, the origin for the trunk axis is the left edge and the origin for the branch axis is the top edge. For an rtl layout with reverse branch direction, the origin for the trunk axis is the right edge and the origin for the branch axis is the bottom edge.
In Noodel v3, automatic resize detection has been removed in favor of a manual approach, to address some limitations and improve performance.
If the noodel is
mounted,
and something happens that causes the size of nodes/branches/canvas to change,
you should immediately call one of Noodel's various realign()
methods to register the size change and reposition elements as necessary.
// this operation will cause the size of someNode to change on both axes
someNode.setStyle({
contentBox: "width: 200px; height: 300px"
});
// immediately call realign to capture node resize on the branch-axis
someNode.realign();
// parent need to realign branch since the change also caused branch resize on the trunk-axis
someNode.getParent().realignBranch();
The realignAll() method is a convenience method for realigning everything at once. This
is useful
in situations such as when everything is styled based on viewport units, and the
viewport size changed.
Since this is a more expensive operation it should be used carefully.
// if everything is dependent on viewport units...
window.addEventListener("resize", () => {
noodel.realignAll();
});
// ideally, you should debounce the function above for better performance
Noodel has a routing capability which is enabled by default. This allows Noodel to control the hash of your URL to sync with the location of the current focal node. It will use the ID of the focal node as the fragment identifier.
This allows you to use the browser's history navigation to jump between nodes, as well as create URL links that target specific nodes in a noodel.
If routing is enabled at a noodel's creation, it will check the URL to jump to an initial focal node if its ID is equal to the hash. The sync between the focal node and the URL hash happens regardless whether the noodel is mounted.
Only explicit changes to the URL (e.g. by clicking on a link) will cause a new entry to be pushed to the browser history. Noodel uses history.replaceState() to replace the URL hash as you navigate around the noodel to prevent creating too many unnecessary entries. This includes when you use Noodel's API to navigate.
If you need to do a programmatic navigation that pushes to the history stack, you should change the URL hash to point to the target node instead of using Noodel's API.
You should use routing on one noodel per page only (ideally, when you use a single noodel to control a site). Routing behaviour for multiple noodels on the same page is not well defined.
Due to the fact that Noodel does not use native scrolling for its navigation, basic Ctrl+F search in browsers will not be able to jump to individual occurrences. You may need to implement your own search function for your noodel.
For a simple DOM based text search plugin for Noodel, see noodel-search.
You can control most aspects of Noodel's appearance using plain CSS, by extending/overriding Noodel's CSS classes, or by setting custom class/styles on individual nodes.
In addition, you have complete freedom in structuring and styling the content inside your nodes.
Noodel comes with a "core" stylesheet that determines the layout, and a theming stylesheet with a default theme. While you must keep the core stylesheet, you can override or replace the CSS rules in the theming stylesheet as you see fit.
All of Noodel's DOM elements have various built-in CSS classes attached dynamically. See CSS classes for the list of classes you can work with.
You can specify custom class/styles for each node that will be applied to the
nd-node element. This is useful when you need
to have different types of nodes (e.g large vs small) in the same tree.
See specifying custom class/styles for example.
The size of nodes are completely flexible - they are simply placed in a flex container, and will grow or shrink to fit their content while respecting any CSS rules applied.
This means you can control the size of nodes by specifying their various height, width and flex properties. In addition, you can style your content and let them influence the size of their node containers.
As an example, it's very easy to use Noodel to create a full-page carousel site, with some simple CSS overrides:
.nd-canvas {
height: 100vh;
width: 100vw;
}
.nd-node {
height: 100vh;
width: 100vw;
}
All animations in Noodel are done via CSS transitions. This means you can control the duration and timing functions of all animations via CSS overrides of the corresponding classes.
Enter/leave animations make use of the transition classes of Vue.js. You can apply any combination of them according to your needs.
Movement animations use transitions of the CSS transform property. These should be specified on the nd-xxx-move classes for the corresponding element, and should only be specified on these classes. The move classes are always attached on their elements and temporarily removed when necessary to disable transform transitions.
Noodel uses Vue.js internally and hence provides integration with Vue out of the box. Noodel is available as a Vue component, and also allows you to use Vue components as the content for individual nodes.
If you are including Noodel and Vue's scripts directly in your HTML, it is recommended that you use Noodel's Vue-less build, alongside the Vue script used in your main project (Noodel requires Vue V3). This prevents Vue from being included twice.
If you are using Noodel via a bundler, the default entry is the Vue-less build and the bundler should be able to take care of bundling Vue as a dependency.
Noodel only needs Vue's runtime script. If you need to compile templates in runtime you need to include Vue's compiler script separately.
Noodel's Vue component is exposed as a static variable on the Noodel class, which can be used like any other Vue components. The way to use it is as follows:
import Noodel from 'noodel';
import { createApp } from 'vue';
const NoodelComponent = Noodel.VueComponent;
const app = createApp({
component: {
NoodelComponent
},
template: '<NoodelComponent :noodel="noodel.getState()"/>',
data: {
noodel: new Noodel([])
}
});
app.mount("#el");
Noodel's Vue component takes a single prop, noodel, which must be the internal
state tree of a Noodel instance that you can obtain via getState().
It is important that you do not modify this state tree yourself, but instead use the API methods provided on the Noodel instance to do so.
You are not required to register the Noodel instance as reactive data. However, there is no harm in doing so (like in the example) if it suits how you manage your state in your Vue project.
⚠️ You should NOT call Noodel's
mount()
or unmount() when using Noodel
as a Vue component. It is simply a component that gets mounted and unmounted with your
Vue instance.
You can pass Vue components as node content and they will be rendered via Vue's dynamic
component feature, i.e.
<component :is="yourComponent"></component>.
To do so, you need to provide an object as node content:
const noodel = new Noodel([
{
content: {
component: YourVueComponent, // your Vue component reference
props: { // name-value pairs of props to pass to this component
"propA": "abc",
"propB": 20
},
eventListeners: { // name-value pairs of event listeners to attach onto the component
"onButtonClick": () => console.log("button click")
}
}
}
]);
Noodel is completely written in TypeScript, fully typed and documented.
You don't need to install any additional type declarations - they come bundled out of the box when you install via npm.
Noodel is targeted to work on all major modern browsers.
Support for IE 10 and 11 is best-effort only - Noodel should work on these browsers, but there may be quirks and bugs in some features.
IE 9 and below are not supported.
The view model of a noodel. Has 2-way binding with the view.
constructor(contentTree: NodeDefinition[] | Element | string, options?: NoodelOptions)
Creates the view model of a noodel based on the given content tree.
contentTree Initial content tree for the noodel.
Can be
an array of NodeDefinition
objects that specify nodes on the first level, an HTMLElement that contain templates
for nodes on
the first level, or a selector string for such an element. If nothing is provided,
will
create an empty
noodel with just the root.
options Global options for the noodelmount(el: string | Element)
Mounts the noodel's view into the target container element.
unmount()
Destroys the noodel's view and removes it from the DOM, but keeps the current state of the view model.
nextTick(callback: () => any)
Schedules a callback function to be called after Noodel's current processing cycle. Use this to wait for an operation to be fully processed and reflected in the DOM before the next operation.
getState(): object
Get the internal reactive state tree of this noodel. Only intended to be used as props to the Vue component in a Vue app. Should not be modified directly.
getOptions(): NoodelOptions
Gets the options applied to this node. Returns a cloned object.
getEl(): HTMLDivElement
Gets the container element of this noodel (i.e. nd-canvas), if mounted.
getFocalLevel(): number
Gets the level of the current focal branch. The first branch has level 1.
getActiveTreeHeight(): number
Gets the height (total number of levels) in the current active tree, excluding the root.
getNodeCount(): number
Gets the number of nodes in this noodel (excluding the root).
getRoot(): NoodelNode
Gets the root node.
getFocalParent(): NoodelNode
Gets the parent node of the current focal branch. Returns the root if there's no focal branch (i.e. noodel is empty).
getFocalNode(): NoodelNode
Gets the current focal node. Returns null if noodel is empty.
findNodeByPath(path: number[]): NoodelNode
Gets the node at the given path, an array of 0-based indices starting from the root. Returns null if no such node exist.
findNodeById(id: string): NoodelNode
Gets the node with the given ID. Returns null if no such node exist.
isInInspectMode(): boolean
Check if this noodel is in inspect mode.
setOptions(options: NoodelOptions)
Updates the options of the noodel. Properties of the given object will be merged into the current options.
setFocalLevel(level: number)
Navigates the noodel to focus on the branch at the given level of the current active tree. If the level is greater or smaller than the possible limits, will navigate to the furthest level in that direction.
moveIn(levelCount?: number)
Navigates toward the child branches of the current focal node.
levelCount number of levels to move, defaults to 1
moveOut(levelCount?: number)
Navigates toward the parent branches of the current focal node.
levelCount number of levels to move, defaults to 1
moveForward(nodeCount?: number)
Navigates toward the next siblings of the current focal node.
nodeCount number of nodes to move, defaults to 1
moveBack(nodeCount?: number)
Navigates toward the previous siblings of the current focal node.
nodeCount number of nodes to move, defaults to 1
toggleInspectMode(on: boolean)
Turns inspect mode on/off.
realignCanvas()
Recapture the size of the canvas and adjust the noodel's position if necessary. Use immediately after an operation that changes the size of the canvas. Does nothing if the noodel is not mounted.
realignAll()
Recapture the size of the canvas, ALL nodes and ALL branches, and realign positions if necessary. Use this if you need to align everything after a size change. Does nothing if the noodel is not mounted.
on(ev: string, listener: Function)
Attach a listener for the given event.
off(ev: string, listener: Function)
Remove a listener for the given event.
focalNodeChange
Event triggered every time the focal node has changed.
(current: Node, prev: Node) => anycurrent the current focal nodeprev the previous focal nodefocalParentChange
Event triggered every time the focal parent has changed.
(current: Node, prev: Node) => anycurrent the current focal nodeprev the previous focal nodeenterInspectMode
Event triggered after entering inspect mode.
() => anyexitInspectMode
Event triggered after exiting inspect mode.
() => anyThe view model of a node. Has 2-way binding with the view.
getEl(): HTMLDivElement
Gets the nd-node element associated with this node. Returns null if noodel is not mounted.
getBranchEl(): HTMLDivElement
Gets the nd-branch element associated with this node's child branch. Returns null if noodel is not mounted or branch does not exist.
getParent(): NoodelNode
Gets the parent of this node. Return null if this is the root or has been detached from its parent by a delete operation.
getPath(): number[]
Gets the path (an array of zero-based indices counting from the root) of this node.
getDefinition(deep?: boolean): NodeDefinition
Extracts the definition of this node, returning a NodeDefinition object containing this node's base properties. Useful for serialization or cloning.
deep whether to extract a deep definition tree
including descendants, defaults to falsegetChild(index: number): NoodelNode
Gets the child of this node at the given index. Return null if does not exist.
getChildren(): NoodelNode[]
Gets a copied array of this node's list of children.
getChildCount(): number
Gets the number of children of this node.
getActiveChildIndex(): number
Gets the index of the active child. Returns null if there's no active child.
getActiveChild(): NoodelNode
Gets the active child of this node. Return null if there's no active child.
getId(): string
Gets the ID of this node.
getContent(): string | ComponentContent
Gets the content of this node. If content is a ComponentContent object, will return a deeply cloned object except the 'component' property which is shallowly copied.
getBranchContent(): string | ComponentContent
Gets the branch content of this node. If content is a ComponentContent object, will return a deeply cloned object except the 'component' property which is shallowly copied.
getClassNames(): NodeCSS
Gets a cloned object containing the custom CSS classes applied to this node.
getStyle(): NodeCSS
Gets a cloned object containing the custom styles applied to this node.
getOptions(): NodeOptions
Gets a cloned object containing the options applied to this node.
getIndex(): number
Gets the 0-based index (position among siblings) of this node. Returns 0 if this node has been detached from its parent by a delete operation.
getLevel(): number
Gets the level of this node. Returns null if this node has been deleted.
isRoot(): boolean
Check whether this node is the root.
isActive(): boolean
Check whether this node is active.
isFocalParent(): boolean
Check whether this node is the parent of the focal branch.
isInFocalBranch(): boolean
Check whether this node is inside the focal branch.
isFocalNode(): boolean
Check whether this node is the focal node.
isVisible(): boolean
Check whether this node is visible (i.e is part of the active tree in display).
isBranchVisible(): boolean
Check whether this node's child branch is visible (i.e is part of the active tree in display).
isDeleted(): boolean
Check whether this node has been deleted from its noodel.
setId(id: string)
Sets the ID of this node.
id new ID for this node, should not start with '_'
setContent(content: string | ComponentContent)
Sets the content of this node.
setClassNames(className: NodeCss)
Sets the custom class names for this node. Properties of the provided object will be merged into the existing object.
setStyles(styles: NodeCSS)
Sets the custom inline styles for this node. Properties of the provided object will be merged into the existing object.
setOptions(options: NodeOptions)
Sets the options for this node. Properties of the provided object will be merged into the existing object.
setActiveChild(index: number)
Changes the active child of this node. If doing so will toggle the visibility of the focal branch (i.e this node is an ancestor of the focal branch), the view will jump to focus on the new active child.
jumpToFocus()
Performs a navigational jump to focus on this node. Cannot jump to the root.
insertChildren(defs: NodeDefinition[] | string | Element, index?: number): NoodelNode[]
Inserts one or more new nodes (and their descendants) as children of this node. Will always preserve the current active child if possible. Returns the list of inserted nodes.
defs array or HTML template that specifies the
definitions of the new node(s)index index to insert at, will append to existing
children if omittedinsertBefore(defs: NodeDefinition[] | string | Element): NoodelNode[]
Convenience method for inserting sibling node(s) before this node.
insertAfter(defs: NodeDefinition[] | string | Element): NoodelNode[]
Convenience method for inserting sibling node(s) after this node.
deleteChildren(index: number, count: number): NoodelNode[]
Deletes one or more children (and their descendants) of this node. If the active child is removed, will set the next child active, unless the child is the last in the list, where the previous child will be set active. If the focal branch is deleted, will move focus to the nearest ancestor branch. Returns the list of deleted nodes.
index index to delete fromcount number of children to delete, will limit to
maximum if greater than possible rangedeleteBefore(count: number): NoodelNode[]
Convenience method for deleting sibling node(s) before this node.
count number of nodes to remove, will limit to
maximum if greater than possible rangedeleteAfter(count: number): NoodelNode[]
Convenience method for deleting sibling node(s) after this node.
count number of nodes to remove, will limit to
maximum if greater than possible rangedeleteSelf()
Convenience method for deleting this node itself.
reorderChildren(func: (children: NoodelNode[]) => NoodelNode[])
Reorders the children of this node without using insert/delete operations, and triggers FLIP animation (if enabled). Will always preserve the current active child.
func The reorder function. Takes the array of
children of this node as parameter,
and must return an array containing the exact same set of nodes, possibly in a
different ordertraverseSubtree(func: (node: NoodelNode) => any, includeSelf: boolean)
Do a preorder traversal of this node's subtree and perform the specified action on each descendant.
func the action to performincludeSelf whether to include this node in the
traversalrealign()
Recapture the size of this node (on the branch axis) and adjust the branch's position if necessary. Use immediately after an operation that changes the size of this node (on the branch axis). Does nothing if this is the root or noodel is not mounted.
realignBranch()
Recapture the size of this node's child branch (on the trunk axis) and adjust the trunk's position if necessary. Use immediately after an operation that changes the size of this node's child branch (on the trunk axis). Does nothing if this has no children or noodel is not mounted.
on(ev: string, listener: Function)
Attach an event listener on this node.
off(ev: string, listener: Function)
Remove an event listener from this node.
enterFocus
Event triggered after this node entered focus.
(prev: NoodelNode) => anyprev the previous focal nodeexitFocus
Event triggered after this node exited focus.
(current: NoodelNode) => anycurrent the current focal nodechildrenEnterFocus
Event triggered after this node's child branch entered focus.
(prev: NoodelNode) => anyprev the previous focal parentchildrenExitFocus
Event triggered after this node's child branch exited focus.
(current: NoodelNode) => anycurrent the current focal parentObject template used for node creation and insertion.
id?: string
ID of this node. If provided, must be unique and should NOT start with "_" .
children?: NodeDefinition[]
Children nodes of this node. Defaults to an empty array.
isActive?: boolean
If provided, will mark this node as active, overriding the default (first child). If multiple siblings are marked as active, only the first one will take effect.
content?: string | ComponentContent
Content of this node. If is a string, will be inserted as innerHTML of the nd-content-box element. Can also be a ComponentContent object that wraps a Vue component.
branchContent?: string | ComponentContent
Content for this node's child branch. If is a string, will be inserted as innerHTML of the nd-branch-content-box element. Can also be a ComponentContent object that wraps a Vue component. Only rendered if this node has a child branch (i.e. has at least 1 child).
classNames?: NodeCss
An object specifying custom CSS class(es) to apply to various elements associated with a node. Each property should be a string of one or more classes delimited by spaces.
styles?: NodeCss
An object specifying custom CSS styles to apply to various elements associated with a node. Each property should be a string in inline style format.
options?: NodeOptions
Options for this node.
Global options for a noodel.
visibleSubtreeDepth?: number
The number of levels of descendant branches to show after the current focal branch. Defaults to 1.
retainDepthOnTapNavigation?: boolean
If true, will keep the current branch depth (if possible) when tapping on the sibling of an ancestor. Useful to create an effect similar to conventional navigation menus. Defaults to false.
useKeyNavigation?: boolean
Whether to apply the default keyboard navigation. Defaults to true.
useWheelNavigation?: boolean
Whether to apply the default wheel navigation. Defaults to true.
useSwipeNavigation?: boolean
Whether to apply the default swipe navigation. Defaults to true.
useTapNavigation?: boolean
Whether to apply the default tap navigation. Defaults to true.
useInspectModeKey?: boolean
Whether to allow toggling inspect mode via the Enter key. Defaults to true.
useInspectModeDoubleTap?: boolean
Whether to allow toggling inspect mode via the double tap. Defaults to true.
useRouting?: boolean
Whether routing should be enabled for this noodel. Defaults to true.
swipeMultiplierBranch?: number
Number of pixels to move for every pixel swiped in the branch axis. Defaults to 1.
swipeMultiplierTrunk?: number
Number of pixels to move for every pixel swiped in the trunk axis. Defaults to 1.
snapMultiplierBranch?: number
Number of nodes (per unit of velocity) to snap across after a swipe is released. Defaults to 1.
snapMultiplierTrunk?: number
Number of levels (per unit of velocity) to snap across after a swipe is released. Defaults to 1.
showLimitIndicators?: boolean
Whether to render the limit indicators of the canvas. Defaults to true.
showChildIndicators?: boolean
Whether to render the child indicators of nodes. Defaults to true.
orientation?: 'ltr' | 'rtl' | 'ttb' | 'btt'
Determines the direction of the trunk axis. Defaults to 'ltr' (left to right).
branchDirection?: 'normal' | 'reverse'
Determines the direction of the branch axis under the current trunk orientation. Defaults to 'normal'.
focalOffsetTrunk?: (canvasLengthTrunk: number) => number
A function that determines the trunk-axis offset on the canvas where each branch should align to if they become focal, given the trunk-axis length of the canvas as reference. Should return a number of pixels counting from the trunk-axis origin of the canvas. If the number is greater than the canvas length, will use the canvas length. Defaults to 1/2 of the canvas length.
focalOffsetBranch?: (canvasLengthBranch: number) => number
A function that determines the branch-axis offset on the canvas where each node should align to if they become active, given the branch-axis length of the canvas as reference. Should return a number of pixels counting from the branch-axis origin of the canvas. If the number is greater than the canvas length, will use the canvas length. Defaults to 1/2 of the canvas length.
anchorOffsetTrunk?: (branchLength: number) => number
A function that determines the trunk-axis offset on each branch that should align to the focal offset if they become focal, given their length on the trunk axis. Should return a number of pixels counting from the trunk-axis origin of the branch. If the number exceeds the branch length, will use the branch length. Defaults to 1/2 of the branch length.
anchorOffsetBranch?: (nodeLength: number) => number
A function that determines the branch-axis offset on each node that should align to the focal position if they become active, given their length on the branch axis. Should return a number of pixels counting from the branch-axis origin of the node. If the number exceeds the node length, will use the node length. Defaults to 1/2 of the node length.
useFlipAnimation?: boolean
Whether to apply FLIP animations when inserting, deleting or reordering nodes. If true, you must also specify CSS transition of the transform property for nodes via the nd-node-move class. Defaults to false.
Options for an individual node.
showChildIndicator?: boolean | null
If set to a boolean, will override the global showChildIndicators option for this specific node. Defaults to null.
focalOffsetTrunk?: (canvasLengthTrunk: number) => number
If set to a function, will override the global focalOffsetTrunk option for this specific node's child branch. Defaults to null.
focalOffsetBranch?: (canvasLengthBranch: number) => number
If set to a function, will override the global focalOffsetBranch option for this specific node. Defaults to null.
anchorOffsetTrunk?: (branchLength: number) => number
If set to a function, will override the global anchorOffsetTrunk option for this specific node's child branch. Defaults to null.
anchorOffsetBranch?: (nodeLength: number) => number
If set to a function, will override the global anchorOffsetBranch option for this specific node. Defaults to null.
useFlipAnimation?: boolean
If set to a boolean, will override the global useFlipAnimation option for this specific node's child branch. Defaults to null.
Object template used for passing a Vue component as node content.
component: string | object | Function
Vue component to be rendered using Vue's <component> tag. Can be name or component reference.
props?: object
Name-value pairs of props to pass to this component.
eventListeners?: object
Name-value pairs of event listeners to pass to this component.
Object template used for specifying custom CSS classes and styles on nodes.
node: string
CSS applied to the nd-node element.
contentBox: string
CSS applied to the nd-content-box element.
childIndicator: string
CSS applied to the nd-child-indicator element.
branch: string
CSS applied to the nd-branch element.
branchContentBox: string
CSS applied to the nd-branch-content-box element.
branchSlider: string
CSS applied to the nd-branch-slider element.
Lists of CSS classes provided and their associated elements.
nd-canvas
Outermost container of the noodel that processes all inputs.
nd-canvas-ltr, nd-canvas-rtl, nd-canvas-ttb, nd-canvas-btt, nd-canvas-normal, nd-canvas-reverse
Classes applied to the nd-canvas element depending on the current orientation and branch direction.
nd-canvas-inspect
Class applied to the nd-canvas element when in inspect mode.
nd-limit
The limit indicators (i.e. the bars that appear when you try to go beyond the view limits).
nd-limit, nd-limit-left, nd-limit-right, nd-limit-top
Classes applied to the nd-limit elements based on their absolute locations.
nd-limit-trunk-start, nd-limit-trunk-end, nd-limit-branch-start, nd-limit-branch-end
Classes applied to the nd-limit elements based on their locations relative to the trunk and branch axes.
nd-limit-[transition class]
Transition classes for enter/leave transitions of limit indicators.
nd-trunk
The trunk element. You should not need to style this element directly in most cases.
nd-trunk-move
Class applied to the nd-trunk element for specifying the transition effects of snap movements in the trunk axis.
nd-branch
Outermost container of a branch that occupies 100% length of the canvas on the branch axis.
nd-branch-focal
Applied to the nd-branch element of the focal branch.
nd-branch-level-[x]
Applied to every nd-branch element where x is the level of the branch.
nd-branch-[transition class]
Transition classes for enter/leave transitions of branches.
nd-branch-content-box
The container element for branch content specified for the branch.
nd-branch-slider
Direct container of nodes in a branch, and responsible for its movement in the branch axis.
nd-branch-slider-move
Applied to the focal nd-branch element.
nd-branch-move
Class applied to nd-branch elements for specifying the transition effects of snap movements in the branch axis.
nd-node
Outermost container of a node.
nd-node-active
Applied to nd-node elements that are active.
nd-node-[transition class]
Transition classes for enter/leave of nodes during node insertion and deletion.
nd-node-move
Class applied to nd-node elements for specifying the transition effects of FLIP animations during node insertion, deletion and reordering.
nd-content-box
Direct container of the content in a node.
nd-inspect-backdrop
The backdrop element that appears during inspect mode. Note that this element is a child of the nd-node element.
nd-inspect-backdrop-[transition class]
Transition classes for enter/leave of the backdrop.
nd-child-indicator
The child indicator element in a node that is rendered when the node has children.
nd-child-indicator-expanded
Applied to the nd-child-indicator element of a node that has visible children.
nd-child-indicator-[transition class]
Transition classes for enter/leave of child indicators during node insert/delete.
Contributions are most welcome! Please see here for details.
MIT License
© 2019-present Jonny Lu