From c9f1afab659339a93c05d0d21a4f7e097d5395d2 Mon Sep 17 00:00:00 2001 From: Alex Jover Date: Fri, 19 Aug 2022 16:22:39 +0200 Subject: [PATCH] feat: implement richText API --- README.md | 45 +++++++++++++++++ lib/cypress/integration/index.spec.js | 58 ++++++++++++++++++++- lib/fixtures/richTextObject.json | 65 ++++++++++++++---------- lib/index.ts | 59 +++++++++++++++++++--- lib/types.ts | 10 +++- playground/index.html | 12 +++++ playground/main.ts | 73 ++++++++++++++++++++++++--- 7 files changed, 278 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4a9fb182..1fa2deec 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,51 @@ import { renderRichText } from "@storyblok/js"; const renderedRichText = renderRichText(blok.richtext); ``` +You can set a **custom Schema and component resolver globally** at init time by using the `richText` init option: + +```js +import { richTextSchema, storyblokInit } from "@storyblok/js"; + +const mySchema = deepClone(richTextSchema) // you can make a copy of the default richTextSchema +// ... and edit the nodes and marks, or add your own. +// Check the base richTextSchema source here https://github.com/storyblok/storyblok-js-client/blob/master/source/schema.js + +storyblokInit({ + accessToken: "" + richText: { + schema: mySchema, + resolver: (component, blok) => { + switch (component) { + case "my-custom-component": + return `
${blok.text}
`; + break; + default: + return "Resolver not defined"; + } + } + } +}) +``` + +You can also set a **custom Schema and component resolver only once** by passing the options as the second parameter to `renderRichText` function: + +```js +import { renderRichText } from "@storyblok/js"; + +renderRichText(blok.richTextField, { + schema: mySchema, + resolver: (component, blok) => { + switch (component) { + case "my-custom-component": + return `
${blok.text}
`; + break; + default: + return `Component ${component} not found`; + } + }, +}); +``` + ## 🔗 Related Links - **[Storyblok Technology Hub](https://www.storyblok.com/technologies?utm_source=github.com&utm_medium=readme&utm_campaign=storyblok-js)**: Storyblok integrates with every framework so that you are free to choose the best fit for your project. We prepared the technology hub so that you can find selected beginner tutorials, videos, boilerplates, and even cheatsheets all in one place. diff --git a/lib/cypress/integration/index.spec.js b/lib/cypress/integration/index.spec.js index 3a6b7350..f435c89d 100644 --- a/lib/cypress/integration/index.spec.js +++ b/lib/cypress/integration/index.spec.js @@ -1,4 +1,59 @@ describe("@storyblok/js", () => { + describe("RichText", () => { + it("should print a console error if the SDK is not initialized", () => { + cy.visit("http://localhost:3000/", { + onBeforeLoad(win) { + cy.spy(win.console, "error").as("consoleError"); + }, + }); + + cy.get(".render-rich-text").click(); + cy.get("@consoleError").should( + "be.calledWith", + "Please initialize the Storyblok SDK before calling the renderRichText function" + ); + cy.get("#rich-text-container").should("have.html", "undefined"); + }); + + it("should render the HTML using the default schema and resolver", () => { + cy.visit("http://localhost:3000/", { + onBeforeLoad(win) { + cy.spy(win.console, "error").as("consoleError"); + }, + }); + + cy.get(".without-bridge").click(); + cy.get(".render-rich-text").click(); + cy.get("@consoleError").should("not.be.called"); + cy.get("#rich-text-container").should( + "have.html", + "

Holain bold

" + ); + }); + + it("should render the HTML using a custom global schema and resolver", () => { + cy.visit("http://localhost:3000/"); + + cy.get(".init-custom-rich-text").click(); + cy.get(".render-rich-text").click(); + cy.get("#rich-text-container").should( + "have.html", + 'Holain bold
hey John
' + ); + }); + + it("should render the HTML using a one-time schema and resolver", () => { + cy.visit("http://localhost:3000/"); + + cy.get(".without-bridge").click(); + cy.get(".render-rich-text-options").click(); + cy.get("#rich-text-container").should( + "have.html", + 'Holain bold
hey John
' + ); + }); + }); + describe("Bridge", () => { it("Is loaded by default", () => { cy.visit("http://localhost:3000/"); @@ -12,6 +67,7 @@ describe("@storyblok/js", () => { cy.get("#storyblok-javascript-bridge").should("not.exist"); }); }); + describe("Bridge (added independently)", () => { it("Can be loaded", () => { cy.visit("http://localhost:3000/"); @@ -21,7 +77,7 @@ describe("@storyblok/js", () => { it("Can be loaded just once", () => { cy.visit("http://localhost:3000/"); cy.get(".load-bridge").click(); - cy.wait(1000); + cy.wait(1000); // eslint-disable-line cy.get(".load-bridge").click(); cy.get("#storyblok-javascript-bridge") .should("exist") diff --git a/lib/fixtures/richTextObject.json b/lib/fixtures/richTextObject.json index fb3c8088..19c77647 100644 --- a/lib/fixtures/richTextObject.json +++ b/lib/fixtures/richTextObject.json @@ -1,27 +1,40 @@ { - "type": "doc", - "content": [ - { - "type": "paragraph", - "content": [ - { - "text": "Experiamur igitur, inquit, etsi habet haec Stoicorum ratio difficilius quiddam et obscurius. Non enim iam stirpis bonum quaeret, sed animalis. ", - "type": "text" - }, - { - "text": "Quia dolori non voluptas contraria est, sed doloris privatio.", - "type": "text", - "marks": [ - { - "type": "bold" - } - ] - }, - { - "text": " Quis enim confidit semper sibi illud stabile et firmum permansurum, quod fragile et caducum sit? Stuprata per vim Lucretia a regis filio testata civis se ipsa interemit. Hic ambiguo ludimur.", - "type": "text" - } - ] - } - ] -} \ No newline at end of file + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "text": "Hola", + "type": "text" + }, + { + "text": "in bold", + "type": "text", + "marks": [ + { + "type": "bold" + } + ] + } + ] + }, + { + "type": "custom_link", + "attrs": { + "href": "https://storyblok.com" + } + }, + { + "type": "blok", + "attrs": { + "body": [ + { + "component": "custom_component", + "message": "hey John" + } + ] + } + } + ] +} diff --git a/lib/index.ts b/lib/index.ts index 44e8282f..c02c3bb9 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -7,11 +7,13 @@ import { SbInitResult, Richtext, StoryblokComponentType, + SbRichTextOptions, } from "./types"; import RichTextResolver from "storyblok-js-client/source/richTextResolver"; +export { default as RichTextSchema } from "storyblok-js-client/source/schema"; -const resolver = new RichTextResolver(); +let richTextResolver; const bridgeLatest = "https://app.storyblok.com/f/storyblok-v2-latest.js"; @@ -54,7 +56,13 @@ export { default as apiPlugin } from "./modules/api"; export { default as storyblokEditable } from "./modules/editable"; export const storyblokInit = (pluginOptions: SbSDKOptions = {}) => { - const { bridge, accessToken, use = [], apiOptions = {} } = pluginOptions; + const { + bridge, + accessToken, + use = [], + apiOptions = {}, + richText = {}, + } = pluginOptions; apiOptions.accessToken = apiOptions.accessToken || accessToken; @@ -71,19 +79,56 @@ export const storyblokInit = (pluginOptions: SbSDKOptions = {}) => { loadBridge(bridgeLatest); } + // Rich Text resolver + richTextResolver = new RichTextResolver(richText.schema); + if (richText.resolver) + setComponentResolver(richTextResolver, richText.resolver); + return result; }; -export const renderRichText = (text: Richtext): string => { - if ((text as any) === "") { +const setComponentResolver = (resolver, resolveFn) => { + resolver.addNode("blok", (node) => { + let html = ""; + + node.attrs.body.forEach((blok) => { + html += resolveFn(blok.component, blok); + }); + + return { + html: html, + }; + }); +}; + +export const renderRichText = ( + data: Richtext, + options?: SbRichTextOptions +): string => { + console.log(data); + if (!richTextResolver) { + console.error( + "Please initialize the Storyblok SDK before calling the renderRichText function" + ); + return; + } + + if ((data as any) === "") { return ""; - } else if (!text) { - console.warn(`${text} is not a valid Richtext object. This might be because the value of the richtext field is empty. + } else if (!data) { + console.warn(`${data} is not a valid Richtext object. This might be because the value of the richtext field is empty. For more info about the richtext object check https://github.com/storyblok/storyblok-js#rendering-rich-text`); return ""; } - return resolver.render(text); + + let localResolver = richTextResolver; + if (options) { + localResolver = new RichTextResolver(options.schema); + if (options.resolver) setComponentResolver(localResolver, options.resolver); + } + + return localResolver.render(data); }; export const loadStoryblokBridge = () => loadBridge(bridgeLatest); diff --git a/lib/types.ts b/lib/types.ts index 4146b133..2eb733d4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -8,7 +8,9 @@ export type StoryblokClient = StoryblokJSClient; declare global { interface Window { storyblokRegisterEvent: (cb: Function) => void; - StoryblokBridge: { new (options?: StoryblokBridgeConfigV2): StoryblokBridgeV2 } ; + StoryblokBridge: { + new (options?: StoryblokBridgeConfigV2): StoryblokBridgeV2; + }; } } @@ -22,12 +24,16 @@ export type SbBlokKeyDataTypes = string | number | object | boolean; export interface SbBlokData extends StoryblokComponent { [index: string]: SbBlokKeyDataTypes; } - +export interface SbRichTextOptions { + schema?: StoryblokConfig["richTextSchema"]; + resolver?: StoryblokConfig["componentResolver"]; +} export interface SbSDKOptions { bridge?: boolean; accessToken?: string; use?: any[]; apiOptions?: StoryblokConfig; + richText?: SbRichTextOptions; } // TODO: temporary till the right bridge types are updated on storyblok-js-client diff --git a/playground/index.html b/playground/index.html index 8852b4f7..5a64435f 100644 --- a/playground/index.html +++ b/playground/index.html @@ -16,6 +16,18 @@ + + +

Rich Text Renderer

diff --git a/playground/main.ts b/playground/main.ts index 32a9222a..503204e3 100644 --- a/playground/main.ts +++ b/playground/main.ts @@ -1,11 +1,46 @@ -import { storyblokInit, loadStoryblokBridge, renderRichText } from "@storyblok/js"; +import { + storyblokInit, + loadStoryblokBridge, + renderRichText, +} from "@storyblok/js"; import richTextFixture from "../lib/fixtures/richTextObject.json"; +const customSchema = { + nodes: {}, + marks: { + custom_link(node) { + const attrs = { ...node.attrs }; + + return { + tag: [ + { + tag: "a", + attrs: attrs, + }, + ], + }; + }, + }, +}; + +const customComponentResolver = (component, blok) => { + switch (component) { + case "custom_component": + return `
${blok.message}
`; + break; + default: + return `Component ${component} not found`; + } +}; + declare global { interface Window { initWithBridge: any; initWithoutBridge: any; loadStoryblokBridgeScript: any; + initCustomRichText: any; + renderRichText: any; + renderRichTextWithOptions: any; } } @@ -21,14 +56,36 @@ window.initWithoutBridge = () => { bridge: false, }); }; - -window.loadStoryblokBridgeScript = () => { - loadStoryblokBridge(); +window.initCustomRichText = () => { + storyblokInit({ + accessToken: "wANpEQEsMYGOwLxwXQ76Ggtt", + richText: { + schema: customSchema, + resolver: customComponentResolver, + }, + }); }; +window.renderRichText = () => { + const renderedRichText = renderRichText(richTextFixture); + const richTextContainer = document.getElementById( + "rich-text-container" + ) as any; + richTextContainer.innerHTML = renderedRichText; + console.log(renderedRichText); +}; -const renderedRichText = renderRichText(richTextFixture); - -const richTextContainer = document.getElementById('rich-text-container'); +window.renderRichTextWithOptions = () => { + const renderedRichText = renderRichText(richTextFixture, { + schema: customSchema, + resolver: customComponentResolver, + }); + const richTextContainer = document.getElementById( + "rich-text-container" + ) as any; + richTextContainer.innerHTML = renderedRichText; +}; -richTextContainer?.insertAdjacentHTML('afterbegin', renderedRichText); \ No newline at end of file +window.loadStoryblokBridgeScript = () => { + loadStoryblokBridge(); +};