I have a requirement to access state when theming extensions. The reason is that I need access to a React theme object which is passed down via context. The examples of theming I have seen are all static and use EditorView.theme(…).
I came up with a solution using a computed EditorView.styleModule however I’m not sure if this is the best way to do it.
As you can see this createTheme
function takes a callback which is given the chakra theme object. However I wasn’t able to access the theme
facet or the buildTheme
function so my work arounds are a bit hacky.
Keen to hear if there is a better way. Or maybe a computed theme option exposed via EditorView is something to consider?
export function createTheme(
stylesFn: (props: StylesFnProps) => { [selector: string]: StyleSpec },
): Extension {
// The EditorView.theme function returns an array of extensions, the first element is the theme extension
// See https://github.com/codemirror/view/blob/6.33.0/src/editorview.ts#L1098-L1103
const [theme] = EditorView.theme({}) as Extension[];
return [
theme,
EditorView.styleModule.compute([chakraThemeField], (state) => {
const chakraTheme = state.field(chakraThemeField);
if (!chakraTheme) {
throw new Error(
'No chakraThemeField found, did you forget to add the chakraTheme(...) extension?',
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const prefix = (theme as any).value; // Using the internal API of theme facet, not ideal
if (!prefix) throw new Error('No theme prefix found');
const spec = stylesFn({ theme: chakraTheme });
return buildTheme(`.${prefix}`, spec);
}),
];
}
// Taken from https://github.com/codemirror/view/blob/6.33.0/src/theme.ts#L12-L22
function buildTheme(
main: string,
spec: { [name: string]: StyleSpec },
scopes?: { [name: string]: string },
) {
return new StyleModule(spec, {
finish(sel) {
return /&/.test(sel)
? sel.replace(/&\w*/, (m) => {
if (m == '&') return main;
if (!scopes || !scopes[m])
throw new RangeError(`Unsupported selector: ${m}`);
return scopes[m];
})
: main + ' ' + sel;
},
});
}