Skip to content

Commit

Permalink
Implement macros support in nextjs-enonic-demo #262
Browse files Browse the repository at this point in the history
  • Loading branch information
pmi committed May 13, 2022
1 parent a611e08 commit bdcf995
Show file tree
Hide file tree
Showing 12 changed files with 1,878 additions and 1,443 deletions.
54 changes: 53 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"next": "^12.1.5",
"node-html-parser": "^5.3.3",
"react": "18.1.0",
"react-dom": "18.1.0"
"react-dom": "18.1.0",
"unescape": "^1.0.1"
},
"devDependencies": {
"@types/node": "^17.0.31",
Expand Down
13 changes: 12 additions & 1 deletion src/_enonicAdapter/ComponentRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface ComponentDefinition {
view?: React.FunctionComponent<any>
}

type SelectorName = "contentType" | "page" | "component" | "part" | "layout";
type SelectorName = "contentType" | "page" | "component" | "part" | "layout" | "macro";

function toSelectorName(type: XP_COMPONENT_TYPE): SelectorName | undefined {
switch (type) {
Expand Down Expand Up @@ -61,6 +61,7 @@ export class ComponentRegistry {
private static components: ComponentDictionary = {};
private static parts: ComponentDictionary = {};
private static layouts: ComponentDictionary = {};
private static macros: ComponentDictionary = {};
private static commonQuery: SelectedQueryMaybeVariablesFunc;

private static getSelector(name: SelectorName): ComponentDictionary {
Expand All @@ -75,6 +76,8 @@ export class ComponentRegistry {
return this.layouts;
case 'part':
return this.parts;
case 'macro':
return this.macros;
}
}

Expand Down Expand Up @@ -113,6 +116,14 @@ export class ComponentRegistry {
return this.commonQuery;
}

public static addMacro(name: string, obj: ComponentDefinition): void {
return ComponentRegistry.addType('macro', name, obj);
}

public static getMacro(name: string): ComponentDefinition | undefined {
return ComponentRegistry.getType('macro', name);
}

public static addContentType(name: string, obj: ComponentDefinition): void {
return ComponentRegistry.addType('contentType', name, obj);
}
Expand Down
43 changes: 41 additions & 2 deletions src/_enonicAdapter/RichTextProcessor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import {commonChars, CONTENT_API_URL, getUrl, RENDER_MODE} from './utils';
import {parse} from 'node-html-parser';
import HTMLElement from 'node-html-parser/dist/nodes/html';
import * as ReactDOMServer from 'react-dom/server';
import {MetaData} from './guillotine/getMetaData';
import BaseMacro from './views/BaseMacro';

export interface TextData {
processedHtml: string,
links: LinkData[]
links: LinkData[],
macrosAsJson: MacroData[],
}

export interface LinkData {
Expand All @@ -16,14 +20,31 @@ export interface LinkData {
} | null,
}

export interface MacroConfig {
[key: string]: any;
}

export interface MacroData {
ref: string;
name: string;
descriptor: string;
config: {
[name: string]: MacroConfig;
};
}

export class RichTextProcessor {
private static urlFunction: (url: string) => string;
private static apiUrl: string;
private static macroAttr = 'data-macro-ref';
private static imageAttr = 'data-image-ref';
private static linkAttr = 'data-link-ref';

public static process(data: TextData, mode: RENDER_MODE): string {
public static process(data: TextData, meta: MetaData): string {
const root: HTMLElement = parse(data.processedHtml);
const mode = meta?.renderMode || RENDER_MODE.NEXT;
// run first to make sure contents is updated before processing links and images
this.processMacros(root, data.macrosAsJson, meta);
this.processLinks(root, data.links, mode);
if (mode !== RENDER_MODE.NEXT) {
// images have absolute urls to XP so no need to process them in next mode rendering
Expand All @@ -32,6 +53,24 @@ export class RichTextProcessor {
return root.toString();
}

private static processMacros(root: HTMLElement, macroData: MacroData[], meta: MetaData): void {
const macros = root.querySelectorAll('editor-macro[' + this.macroAttr + ']');
macros.forEach(macroEl => {
const ref = macroEl.getAttribute(this.macroAttr);
if (ref) {
const data = macroData.find(d => d.ref === ref);
if (data) {
const MacroElement = BaseMacro({meta, data});
// don't replace if macro output is null
if (MacroElement) {
const macroOutput = ReactDOMServer.renderToStaticMarkup(MacroElement);
macroEl.replaceWith(macroOutput);
}
}
}
})
}

private static processImages(root: HTMLElement): void {
const imgs = root.querySelectorAll('img[' + this.imageAttr + ']');
imgs.forEach(img => {
Expand Down
1 change: 1 addition & 0 deletions src/_enonicAdapter/guillotine/getMetaData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const COMPONENTS_QUERY = `
text {
value(processHtml:{type:absolute, imageWidths:[400, 800, 1200], imageSizes:"(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"}) {
processedHtml
macrosAsJson
links {
ref
media {
Expand Down
37 changes: 37 additions & 0 deletions src/_enonicAdapter/views/BaseMacro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react"
import {MetaData} from "../guillotine/getMetaData";
import {ComponentRegistry} from '../ComponentRegistry';
import {MacroConfig, MacroData} from '../RichTextProcessor';

const unescape = require('unescape');

interface BaseMacroProps {
data: MacroData;
meta: MetaData;
}

export interface MacroProps {
name: string;
config: MacroConfig;
meta: MetaData;
}

const BaseMacro = (props: BaseMacroProps) => {
const {data, meta} = props;

console.info(`Looking for macro definition for: ${data.descriptor}`)
const macro = ComponentRegistry.getMacro(data.descriptor);
const MacroView = macro?.view;
if (MacroView) {
const config = data.config[data.name];
if (config?.body) {
config.body = unescape(config.body);
}
return <MacroView name={data.name} config={config} meta={meta}/>;
} else if (data.descriptor) {
console.warn(`BaseMacro: can not render macro '${data.descriptor}': no next view or catch-all defined`);
}
return null;
}

export default BaseMacro;
9 changes: 4 additions & 5 deletions src/_enonicAdapter/views/RichTextView.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import React from "react"
import {RENDER_MODE} from '../utils';
import {RichTextProcessor, TextData} from '../RichTextProcessor';
import {MetaData} from '../guillotine/getMetaData';

type Props = {
data: TextData,
mode?: RENDER_MODE,
meta: MetaData,
tag?: string,
}

const RichTextView = ({tag, mode, data}: Props) => {
const RichTextView = ({tag, data, meta}: Props) => {
const CustomTag = tag as keyof JSX.IntrinsicElements || 'section';
const customMode = mode || RENDER_MODE.NEXT;
return <CustomTag dangerouslySetInnerHTML={{__html: RichTextProcessor.process(data, customMode)}}/>
return <CustomTag dangerouslySetInnerHTML={{__html: RichTextProcessor.process(data, meta)}}/>
}

export default RichTextView;
7 changes: 4 additions & 3 deletions src/_enonicAdapter/views/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react"
import {LinkData} from '../RichTextProcessor';
import {LinkData, MacroData} from '../RichTextProcessor';
import {MetaData} from '../guillotine/getMetaData';
import RichTextView from './RichTextView';

Expand All @@ -8,15 +8,16 @@ type Props = {
component: {
value: {
processedHtml: string,
links: LinkData[]
links: LinkData[],
macros: MacroData[],
}
}
}

const DefaultTextView = ({component, meta}: Props) => (
// temporary workaround for TextComponent expecting section inside of a root element
<div>
<RichTextView data={component.value} mode={meta.renderMode}/>
<RichTextView data={component.value} meta={meta}/>
</div>
);

Expand Down
13 changes: 11 additions & 2 deletions src/components/_mappings.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {APP_NAME} from '../_enonicAdapter/utils'
import {ComponentRegistry, CATCH_ALL} from '../_enonicAdapter/ComponentRegistry';
import PropsView from './views/Props';
import {CATCH_ALL, ComponentRegistry} from '../_enonicAdapter/ComponentRegistry';
import Person from './views/Person';
import getPerson from './queries/getPerson';
import MainPage from './pages/Main';
Expand All @@ -9,6 +8,8 @@ import Heading from './parts/Heading';
import MovieDetails, {getMovie} from './parts/MovieDetails';
import TwoColumnLayout from './layouts/TwoColumnLayout';
import {commonQuery, commonVariables} from './queries/common';
import DefaultMacro from './macros/DefaultMacro';
import YoutubeMacro from './macros/YoutubeMacro';


// You can set common query for all views here
Expand Down Expand Up @@ -44,6 +45,14 @@ ComponentRegistry.addPart(`${APP_NAME}:child-list`, {
view: ChildList
});

// Macro mappings
ComponentRegistry.addMacro('com.enonic.app.socialmacros:youtube', {
view: YoutubeMacro
});
ComponentRegistry.addMacro(CATCH_ALL, {
view: DefaultMacro
});

/*
// Debug
ComponentRegistry.addContentType(CATCH_ALL, {
Expand Down
14 changes: 14 additions & 0 deletions src/components/macros/DefaultMacro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from "react"
import {RENDER_MODE} from '../../_enonicAdapter/utils';
import {MacroProps} from '../../_enonicAdapter/views/BaseMacro';
import HTMLReactParser from 'html-react-parser';

const DefaultMacro = ({name, config, meta}: MacroProps) => {
if (meta?.renderMode === RENDER_MODE.EDIT) {
return <>{`[${name}]${config.body}[/${name}]`}</>
} else {
return HTMLReactParser(config.body) as JSX.Element;
}
};

export default DefaultMacro;
13 changes: 13 additions & 0 deletions src/components/macros/YoutubeMacro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react"
import {RENDER_MODE} from '../../_enonicAdapter/utils';
import {MacroProps} from '../../_enonicAdapter/views/BaseMacro';

const YoutubeMacro = ({name, config, meta}: MacroProps) => {
if (meta?.renderMode === RENDER_MODE.EDIT) {
return <>{`[${name} title="${config.title}" url="${config.url}"/]`}</>
} else {
return <>Youtube here</>
}
};

export default YoutubeMacro;
Loading

0 comments on commit bdcf995

Please sign in to comment.