Skip to content

Commit

Permalink
Merge pull request #61 from 10play/dynamic-css
Browse files Browse the repository at this point in the history
Add `injectCSS` for Dynamic CSS
  • Loading branch information
GuySerfaty authored Feb 21, 2024
2 parents 512898b + 6c5ed6e commit 900a5dc
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 50 deletions.
48 changes: 44 additions & 4 deletions example/src/Examples/CustomCss.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import React from 'react';
import {
SafeAreaView,
View,
KeyboardAvoidingView,
Platform,
StyleSheet,
Button,
} from 'react-native';
import {
CodeBridge,
Expand Down Expand Up @@ -52,9 +52,49 @@ export const CustomCss = ({}: NativeStackScreenProps<any, any, any>) => {

return (
<SafeAreaView style={exampleStyles.fullScreen}>
<View style={exampleStyles.fullScreen}>
<RichText editor={editor} />
</View>
<Button
title={'Random CodeBlock Color'}
onPress={() => {
editor.injectCSS(
`
code {
background-color: ${
'#' + Math.floor(Math.random() * 16777215).toString(16)
};
border-radius: 0.25em;
border-color: ${
'#' + Math.floor(Math.random() * 16777215).toString(16)
};
border-width: 1px;
border-style: solid;
box-decoration-break: clone;
color: ${'#' + Math.floor(Math.random() * 16777215).toString(16)};
font-size: 0.9rem;
padding: 0.25em;
}
`,
// Because we are passing CodeBridge name here, the existing css from CodeBridge will be replaced
// With the css we are injecting here
CodeBridge.name
);
}}
/>
<Button
title={'Random Font Size'}
onPress={() => {
editor.injectCSS(
`
* {
font-size: ${Math.random() * 60}px;
}
`,
// We are passing a custom tag here, so no bridge css will be replaced, instead a new stylesheet with be created with
// the tag 'font-size', and it will only be replaced with we injectCSS again with the same tag
'font-size'
);
}}
/>
<RichText editor={editor} />
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={exampleStyles.keyboardAvoidingView}
Expand Down
32 changes: 7 additions & 25 deletions src/RichText/RichText.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Platform, StyleSheet, TextInput } from 'react-native';
import {
WebView,
Expand All @@ -11,6 +11,7 @@ import { editorHtml } from '../simpleWebEditor/build/editorHtml';
import { type EditorMessage } from '../types/Messaging';
import { useKeyboard } from '../utils';
import type { EditorBridge } from '../types';
import { getInjectedJS } from './utils';

interface RichTextProps extends WebViewProps {
editor: EditorBridge;
Expand All @@ -33,22 +34,6 @@ const DEV_SERVER_URL = 'http://localhost:3000';
// TODO: make it a prop
const TOOLBAR_HEIGHT = 44;

const getStyleSheetCSS = (css: string[]) => {
return `
let css = \`${css.join(' ')}\`,
head = document.head || document.getElementsByTagName('head')[0],
style = document.createElement('style');
head.appendChild(style);
style.type = 'text/css';
if (style.styleSheet){
// This is required for IE8 and below.
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
`;
};

export const RichText = ({ editor, ...props }: RichTextProps) => {
const [loaded, setLoaded] = useState(false);
const { keyboardHeight: iosKeyboardHeight, isKeyboardUp } = useKeyboard();
Expand Down Expand Up @@ -107,13 +92,10 @@ export const RichText = ({ editor, ...props }: RichTextProps) => {
}
}, [editor.avoidIosKeyboard, editor, iosKeyboardHeight, isKeyboardUp]);

const getInjectedJS = () => {
let injectJS = '';
const css =
editor.bridgeExtensions?.map(({ extendCSS }) => extendCSS || '') || [];
injectJS += getStyleSheetCSS(css);
return injectJS;
};
const injectedJavaScript = useMemo(
() => getInjectedJS(editor.bridgeExtensions || []),
[editor.bridgeExtensions]
);

return (
<>
Expand All @@ -129,7 +111,7 @@ export const RichText = ({ editor, ...props }: RichTextProps) => {
]}
containerStyle={editor.theme.webviewContainer}
source={source}
injectedJavaScript={getInjectedJS()}
injectedJavaScript={injectedJavaScript}
injectedJavaScriptBeforeContentLoaded={`${
editor.bridgeExtensions
? `
Expand Down
48 changes: 29 additions & 19 deletions src/RichText/useEditorBridge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TenTapStartKit } from '../bridges/StarterKit';
import { uniqueBy } from '../utils';
import { defaultEditorTheme } from './theme';
import type { Subscription } from '../types/Subscription';
import { getStyleSheetCSS } from './utils';

type RecursivePartial<T> = {
[P in keyof T]?: RecursivePartial<T[P]>;
Expand Down Expand Up @@ -92,8 +93,19 @@ export const useEditorBridge = (options?: {
});
};

/**
* Injects custom css stylesheet, if stylesheet exists with the same tag, it will be replaced
* @param cssString css to inject
* @param tag optional - tag to identify the style element
*/
const injectCSS = (cssString: string, tag: string = 'custom-css') => {
// Generate custom stylesheet with `custom-css` tag
const customCSS = getStyleSheetCSS(cssString, tag);
webviewRef.current?.injectJavaScript(customCSS);
};

const editorBridge = {
bridgeExtensions: bridgeExtensions,
bridgeExtensions,
initialContent: options?.initialContent,
autofocus: options?.autofocus,
avoidIosKeyboard: options?.avoidIosKeyboard,
Expand All @@ -104,32 +116,30 @@ export const useEditorBridge = (options?: {
webviewRef,
theme: mergedTheme,
getEditorState,
injectCSS,
_updateEditorState,
_subscribeToEditorStateUpdate,
_onContentUpdate,
_subscribeToContentUpdate,
};

const editorInstanceExtendByPlugins = (bridgeExtensions || []).reduce(
(acc, cur) => {
if (!cur.extendEditorInstance) return acc;
return Object.assign(
acc,
cur.extendEditorInstance(
sendAction,
webviewRef,
editorStateRef,
_updateEditorState
),
const editorInstance = (bridgeExtensions || []).reduce((acc, cur) => {
if (!cur.extendEditorInstance) return acc;
return Object.assign(
acc,
cur.extendEditorInstance(
sendAction,
webviewRef,
editorStateRef.current,
editorStateRef,
_updateEditorState
);
},
editorBridge
) as unknown as EditorBridge;
),
webviewRef,
editorStateRef.current,
_updateEditorState
);
}, editorBridge) as EditorBridge; // TODO fix type

EditorHelper.setEditorLastInstance(editorInstanceExtendByPlugins);
EditorHelper.setEditorLastInstance(editorInstance);

return editorInstanceExtendByPlugins;
return editorInstance;
};
36 changes: 36 additions & 0 deletions src/RichText/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type BridgeExtension from '../bridges/base';

/**
* Creates a new style element and appends it to the head of the document.
* If the style element already exists, it will update the content of the existing element.
* @param css - array of css strings
* @param styleSheetTag - a unique tag to identify the style element - if not provided, a new style element will be created
* @returns a string of javascript that is ready to be injected into the rich text webview
*/
export const getStyleSheetCSS = (css: string, styleSheetTag: string) => {
return `
cssContent = \`${css}\`;
head = document.head || document.getElementsByTagName('head')[0],
styleElement = head.querySelector('style[data-tag="${styleSheetTag}"]');
if (!styleElement) {
// If no such element exists, create a new <style> element.
styleElement = document.createElement('style');
styleElement.setAttribute('data-tag', '${styleSheetTag}'); // Assign the unique 'data-tag' attribute.
styleElement.type = 'text/css'; // Specify the type attribute for clarity.
head.appendChild(styleElement); // Append the newly created <style> element to the <head>.
}
styleElement.innerHTML = cssContent;
`;
};

export const getInjectedJS = (bridgeExtensions: BridgeExtension[]) => {
let injectJS = '';
// For each bridge extension, we create a stylesheet with it's name as the tag
const styleSheets = bridgeExtensions.map(({ extendCSS, name }) =>
getStyleSheetCSS(extendCSS || '', name)
);
injectJS += styleSheets.join(' ');
return injectJS;
};
2 changes: 2 additions & 0 deletions src/bridges/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class BridgeExtension<T = any, E = any, M = any> {
clone(): BridgeExtension<T, E, M> {
return new BridgeExtension<T, E, M>({
...this,
forceName: this.name,
});
}

Expand All @@ -78,6 +79,7 @@ class BridgeExtension<T = any, E = any, M = any> {
cloned.config = config;
return cloned;
}

configureCSS(css: string) {
const cloned = this.clone();
cloned.extendCSS = css;
Expand Down
8 changes: 6 additions & 2 deletions src/bridges/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type CoreEditorInstance = {
updateScrollThresholdAndMargin: (offset: number) => void;
focus: (pos: FocusArgs) => void;
blur: () => void;
injectJS: (js: string) => void;
injectCSS: (css: string, tag?: string) => void;
theme: EditorTheme;
};

Expand Down Expand Up @@ -133,7 +135,7 @@ export type CoreMessages =

export const CoreBridge = new BridgeExtension<
CoreEditorState,
Omit<CoreEditorInstance, 'theme'>,
Omit<CoreEditorInstance, 'theme' | 'injectCSS'>,
CoreMessages
>({
forceName: 'coreBridge',
Expand Down Expand Up @@ -162,7 +164,6 @@ export const CoreBridge = new BridgeExtension<
});
}
if (message.type === CoreEditorActionType.GetText) {
console.log('!!!!!');
sendMessageBack({
type: CoreEditorActionType.SendTextToNative,
payload: {
Expand Down Expand Up @@ -306,6 +307,9 @@ export const CoreBridge = new BridgeExtension<
payload: undefined,
});
},
injectJS: (js: string) => {
webviewRef?.current?.injectJavaScript(js);
},
};
},
extendEditorState: (editor) => {
Expand Down
10 changes: 10 additions & 0 deletions website/docs/api/EditorBridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ a function that get's html as string and set set's it as the editors content <br
`(from: number, to: number) => void`<br />
sets the selection of the editor <br /> extended by [CoreBridge](./BridgeExtensions#coreextension)

#### injectCSS

`(css: string, tag?: string) => void`<br />
creates or updates the stylesheet with the given tag, see [Dynamically Updating CSS](../examples/customCss/#dynamically-updating-css) <br /> <u>default</u> `tag`: `custom-css`<br /> extended by [CoreBridge](./BridgeExtensions#coreextension)

#### injectJS

`(js: string) => void`<br />
inject custom javascript into the editor's webview <br /> extended by [CoreBridge](./BridgeExtensions#coreextension)

#### updateScrollThresholdAndMargin

`(offset: number) => void`<br />
Expand Down
27 changes: 27 additions & 0 deletions website/docs/examples/customCss.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ const editor = useEditorBridge({

> <strong>NOTE</strong> calling configureCSS more than once will override the previous css.
## Dynamically Updating CSS

Let's say we want to dynamically update css after the editor is initialized.
We can do this with [injectCSS](../api/EditorBridge/#injectcss).
`injectCSS` receives two parameters:

- `css`: the css to inject
- `tag`: the tag of the stylesheet of which to inject the css into

When we call `injectCSS` it gets or creates a stylesheet with the given `tag` and updates it's css.

If we wanted to update our `CodeBridge` css after it has been initialized we could run

```ts
editor.injectCSS(ourCustomCSS, CodeBridge.name);
```

then this would replace or create the existing css with whatever we have given it.

Now let's say that we don't want to override a bridges existing css, we could do this by providing a custom tag

```ts
editor.injectCSS(ourCustomCSS, 'our-custom-tag');
```

This will create a new stylesheet with `our-custom-tag` and it will not override any bridge's custom css.

## Adding Custom Fonts

Let's add a custom font to our Editor (we can also add custom css)
Expand Down

0 comments on commit 900a5dc

Please sign in to comment.