Skip to content

Instantly share code, notes, and snippets.

@davwheat
Last active December 2, 2021 10:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davwheat/5a375bdb01682c6e1be50f54d5bd2ce2 to your computer and use it in GitHub Desktop.
Save davwheat/5a375bdb01682c6e1be50f54d5bd2ce2 to your computer and use it in GitHub Desktop.
Flarum VDOM node searching

Flarum - Find VDOM child

Easily find and modify a VDOM node's children.

This util was sponsored by Kagi Search, an ad-free search engine and is released as open source for the wider Flarum community to benefit.

Requirements

  1. Download the attached Typescript file
  2. yarn add css-what OR npm i css-what
  3. Enjoy!

Example usage

To add a Dropdown to .DiscussionListItem-content...

extend(DiscussionListItem.prototype, 'view', function (this: DiscussionListItem, vdom: Mithril.Vnode) {
  findFirstVdomChild(vdom, '.DiscussionListItem-content', (vdom) => {
    // Must be an array (Mithril fragment)
    if (!Array.isArray(vdom.children)) {
      vdom.children = [vdom.children];
    }

    // Add a new item to the fragment
    vdom.children.push(
      <Dropdown className="MyDropdown" buttonClassName="Button" label={label}>
        <Button onclick={() => {}}>
          My button
        </Button>
        <Button onclick={() => {}}>
          My other button
        </Button>
      </Dropdown>
    );
  });
});
import { parse as cssWhat, Selector } from 'css-what';
import type Mithril from 'mithril';
type VdomChildren = Mithril.Vnode<Record<string, unknown>> | Mithril.Vnode<Record<string, unknown>>[];
type ManipulationFunction = (vnode: Mithril.Vnode<Record<string, unknown>>) => void;
export function findAndRemoveFirstVdomChild(vdom: VdomChildren, selector: string): boolean {
const node = findFirstVdomChild(vdom, selector);
if (node === undefined) return false;
// Set to empty fragment
node.tag = '[';
node.attrs = { removedViaVdomUtils: true };
node.children = [];
return true;
}
export function findFirstVdomChild(
vdom: VdomChildren,
selector: string,
manipulationFunc?: ManipulationFunction
): Mithril.Vnode<Record<string, unknown>> | undefined {
const parsedSelector = cssWhat(selector);
return searchChildren(vdom, parsedSelector, manipulationFunc);
}
function searchChildren(
vdom: (Mithril.Child | Mithril.Children)[] | (Mithril.Child | Mithril.Children),
selectors: Selector[][],
manipulationFunc?: ManipulationFunction
): Mithril.Vnode<Record<string, unknown>> | undefined {
let nodes: (Mithril.Child | Mithril.Children)[];
if (!Array.isArray(vdom)) {
nodes = [vdom];
} else {
nodes = vdom;
}
let found: Mithril.Vnode<Record<string, unknown>, {}> | undefined = undefined;
nodes.some((node): boolean => {
if (typeof node === 'boolean' || typeof node === 'number' || typeof node === 'string' || node === null || node === undefined) return false;
if (Array.isArray(node)) {
let result = searchChildren(node, selectors, manipulationFunc);
if (result) {
found = result;
return true;
}
return false;
}
if (doesChildMatchSelectors(node, selectors)) {
found = node;
return true;
}
if (Array.isArray(node.children)) {
let result = searchChildren(node.children, selectors);
if (result) {
found = result;
return true;
}
return false;
}
return false;
});
if (manipulationFunc && found) manipulationFunc(found);
return found;
}
function doesChildMatchSelectors(child: Mithril.Vnode<Record<string, any>>, selectors: Selector[][]): boolean {
return selectors.some((selector) => {
return selector.every((criterion): boolean => {
switch (criterion.type) {
case 'tag':
return child.tag === criterion.name;
case 'attribute':
if (!child.attrs) return false;
if (criterion.name === 'class') criterion.name = 'className';
let attr = child.attrs[criterion.name];
if (criterion.name === 'className' && typeof attr === 'string') {
attr = attr.trim();
}
if (criterion.ignoreCase && typeof attr === 'string') {
attr = attr.toLowerCase();
criterion.value = criterion.value.toLowerCase();
}
switch (criterion.action) {
case 'exists':
return child.attrs.hasOwnProperty(criterion.name);
case 'equals':
return attr === criterion.value;
case 'start':
return attr?.startsWith?.(criterion.value) || false;
case 'end':
return attr?.endsWith?.(criterion.value) || false;
case 'element':
return (attr as string)?.split?.(' ')?.includes?.(criterion.value) || false;
}
}
return false;
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment