Skip to content

Instantly share code, notes, and snippets.

@nxta

nxta/vdom.js

Created Jan 8, 2020
Embed
What would you like to do?
Helper function for Flarum to traverse a Mithril Virtual DOM using basic CSS selector syntax
/**
* // Desired usage:
* import vdom from 'flarum/helpers/vdom';
* import {find, vdom} from 'flarum/helpers/vdom';
*
* // Supports tagname, class and id selectors
* vdom('.container ul#logo li.item.item-one a') // Returns array of vnodes
* find('.username') // Returns array of objects containing the vnode and more information {vnode, parent, index}
*
* // Example:
* vdom('.Avatar', vnode).forEach((vnode) => {
* vnode.attrs.style.borderColor = '#f00';
* });
*
* // Example: inserting something before a vnode
* find('.username', vnode).forEach((info) => {
* const {vnode, parent, index} = info;
* const el = avatar(user);
* parent.children.splice(index, 0, el);
* });
*/
function matchTag(tag) {
return node => node && node.tag && node.tag === tag;
}
function matchClass(className) {
className = className.substr(1);
return node => node && node.attrs && node.attrs.className && node.attrs.className.split(' ').includes(className);
}
function matchId(id) {
id = id.substr(1);
return node => node && ( (node.id && node.id === id) || (node.attrs && node.attrs.id && node.attrs.id === id) );
}
function matchAll() {
return node => node && node.tag;
}
function matchesMultiple(s) {
s = s.substr(1);
return s.includes('.') || s.includes('#')
}
function multiMatcher(s) {
// get all parts starting with ^, # or .
const parts = s.match(/(^\w|(#|\.))[^(#|\.|\b)]*/gi);
const matchers = parts.map(getMatcher);
return node => !parts.filter((selector, i) => !matchers[i](node)).length;
}
function getMatcher(selector) {
const type = (matchesMultiple(selector)) ? '!' : selector[0];
return ({'#': matchId, '.': matchClass, '*': matchAll, '!': multiMatcher}[type] || matchTag)(selector);
}
function parseQuery(q) {
return q.split(' ').map(getMatcher);
}
function findMatchingChildrensIndices(vnode, matcher) {
return vnode.children.reduce(function(a, child, i) {
if (matcher(child))
a.push(i);
return a;
}, []);
}
export function find(query, vnode) {
const matchers = parseQuery(query);
return (vnode.children || []).reduce(
(function dig(depth, parent) {
parent = parent || vnode;
depth = depth || 0;
const matcher = matchers[depth];
return (result, vnode, index, arr) => {
if (!vnode)
return result;
let found = 0;
if (matcher && matcher(vnode)) {
found = 1;
if (matchers.length === depth+found)
result.push({vnode, parent, index});
}
if (vnode.children && Array.isArray(vnode.children))
return vnode.children.reduce(dig(depth+found, vnode), result);
return result;
}
})(),
[]
);
}
export function vdom(query, vnode) {
return find(query, vnode).map(v => v.vnode);
}
export default vdom;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment