I'm trying to implement the Syncfusion rich text editor reactjs widget in my site. I've slightly modified the demo code to be a standalone class like this:
import { render } from 'react-dom';
import React, { Component } from 'react';
import { addClass, removeClass, Browser } from '#syncfusion/ej2-base';
import { RichTextEditorComponent, Toolbar, Inject, Image, Link, HtmlEditor, Count, QuickToolbar, Table } from '#syncfusion/ej2-react-richtexteditor';
import { createElement } from '#syncfusion/ej2-base';
import * as CodeMirror from 'codemirror';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/css/css.js';
import 'codemirror/mode/htmlmixed/htmlmixed.js';
class RichTextEd extends Component {
constructor(props) {
super(props);
// Rich Text Editor items list
this.items = ['Bold', 'Italic', 'Underline', 'StrikeThrough',
'FontName', 'FontSize', 'FontColor', 'BackgroundColor',
'LowerCase', 'UpperCase', '|',
'Formats', 'Alignments', 'OrderedList', 'UnorderedList',
'Outdent', 'Indent', 'SuperScript', 'SubScript', '|',
'CreateTable', 'CreateLink', 'Image', '|', 'ClearFormat', 'Print',
'SourceCode', 'FullScreen', '|', 'Undo', 'Redo'
];
//Rich Text Editor ToolbarSettings
this.toolbarSettings = {
items: this.items
};
}
mirrorConversion(e) {
this.textArea = this.rteObj.contentModule.getEditPanel();
let id = this.rteObj.getID() + 'mirror-view';
let mirrorView = this.rteObj.element.querySelector('#' + id);
let charCount = this.rteObj.element.querySelector('.e-rte-character-count');
if (e.targetItem === 'Preview') {
this.textArea.style.display = 'block';
mirrorView.style.display = 'none';
this.textArea.innerHTML = this.myCodeMirror.getValue();
charCount.style.display = 'block';
}
else {
if (!mirrorView) {
mirrorView = createElement('div', { className: 'e-content' });
mirrorView.id = id;
this.textArea.parentNode.appendChild(mirrorView);
}
else {
mirrorView.innerHTML = '';
}
this.textArea.style.display = 'none';
mirrorView.style.display = 'block';
this.renderCodeMirror(mirrorView, this.rteObj.value);
charCount.style.display = 'none';
}
}
renderCodeMirror(mirrorView, content) {
this.myCodeMirror = CodeMirror(mirrorView, {
value: content,
lineNumbers: true,
mode: 'text/html',
lineWrapping: true,
});
}
handleFullScreen(e) {
let sbCntEle = document.querySelector('.sb-content.e-view');
let sbHdrEle = document.querySelector('.sb-header.e-view');
let leftBar;
let transformElement;
if (Browser.isDevice) {
leftBar = document.querySelector('#right-sidebar');
transformElement = document.querySelector('.sample-browser.e-view.e-content-animation');
}
else {
leftBar = document.querySelector('#left-sidebar');
transformElement = document.querySelector('#right-pane');
}
if (e.targetItem === 'Maximize') {
if (Browser.isDevice && Browser.isIos) {
addClass([sbCntEle, sbHdrEle], ['hide-header']);
}
addClass([leftBar], ['e-close']);
removeClass([leftBar], ['e-open']);
if (!Browser.isDevice) {
transformElement.style.marginLeft = '0px';
}
transformElement.style.transform = 'inherit';
}
else if (e.targetItem === 'Minimize') {
if (Browser.isDevice && Browser.isIos) {
removeClass([sbCntEle, sbHdrEle], ['hide-header']);
}
removeClass([leftBar], ['e-close']);
if (!Browser.isDevice) {
addClass([leftBar], ['e-open']);
transformElement.style.marginLeft = leftBar.offsetWidth + 'px';
}
transformElement.style.transform = 'translateX(0px)';
}
}
actionCompleteHandler(e) {
if (e.targetItem && (e.targetItem === 'SourceCode' || e.targetItem === 'Preview')) {
this.rteObj.sourceCodeModule.getPanel().style.display = 'none';
this.mirrorConversion(e);
}
else {
setTimeout(() => { this.rteObj.toolbarModule.refreshToolbarOverflow(); }, 400);
}
}
render() {
return (<div className='control-pane'>
<div className='control-section' id="rteTools">
<div className='rte-control-section'>
<RichTextEditorComponent id="toolsRTE" ref={(richtexteditor) => { this.rteObj = richtexteditor; }} showCharCount={true} actionBegin={this.handleFullScreen.bind(this)} actionComplete={this.actionCompleteHandler.bind(this)} maxLength={2000} toolbarSettings={this.toolbarSettings}>
<div>
<p>The Rich Text Editor is WYSIWYG ("what you see is what you get") editor useful to create and edit content, and return the valid <a href='https://ej2.syncfusion.com/home/' target='_blank'>HTML markup</a> or <a href='https://ej2.syncfusion.com/home/' target='_blank'>markdown</a> of the content</p> <p><b>Toolbar</b></p><ol><li> <p>Toolbar contains commands to align the text, insert link, insert image, insert list, undo/redo operations, HTML view, etc</p></li><li><p>Toolbar is fully customizable </p></li></ol> <p><b>Links</b></p><ol><li><p>You can insert a hyperlink with its corresponding dialog </p></li><li><p>Attach a hyperlink to the displayed text. </p></li><li><p>Customize the quick toolbar based on the hyperlink </p> </li></ol><p><b>Image.</b></p><ol><li><p>Allows you to insert images from an online source as well as the local computer </p> </li><li><p>You can upload an image</p></li><li><p>Provides an option to customize quick toolbar for an image</p></li></ol><img alt="Logo" src="https://ej2.syncfusion.com/react/demos/src/rich-text-editor/images/RTEImage-Feather.png" style={{ width: '300px' }}/>
</div>
<Inject services={[Toolbar, Image, Link, HtmlEditor, Count, QuickToolbar, Table]}/>
</RichTextEditorComponent>
</div>
</div>
</div>);
}
}
export default RichTextEd;
I'm then using it in my component with
<RichTextEd />
The editor doesn't render as expected, and there's an error reported about the ref being used in strict mode:
Warning: findDOMNode is deprecated in StrictMode. findDOMNode was passed an instance of RichTextEditorComponent which is inside StrictMode. Instead, add a ref directly to the element you want to reference.
I think I understand that the problem is with the way the ref is set in the render method, but I'm unclear on where the this.rteObj is defined initially, and further how to make this work as expected from a parent component.
Greetings from Syncfusion support,
We have tried the shared code blocks of the RichTextEditor rendered from parent component, which works fine and we haven't faced any issues. Can you please check the below working example.
Sample: https://stackblitz.com/edit/react-render-parent-component?file=index.js
Related
I want to create custom block that works like columns block in Gutenberg. It's easy to make it horizontally in frontend with CSS but how can I make it appear that way in editor?
import { registerBlockType } from '#wordpress/blocks';
import { useBlockProps, InnerBlocks } from '#wordpress/block-editor';
import metadata from '../config/card-row-block.json';
const cardRowTemplate = [
[ 'cgbms/card-block' ]
];
const allowedBlocks = [
'cgbms/card-block',
];
registerBlockType(metadata, {
edit: (props) => {
const blockProps = useBlockProps();
return <div className="cgbms_cards_row" { ...blockProps }>
<InnerBlocks allowedBlocks={ allowedBlocks } renderAppender={ InnerBlocks.ButtonBlockAppender } template={cardRowTemplate} orientation="horizontal" />
</div>
},
save: (props) => {
const blockProps = useBlockProps.save();
return <div className="cgbms_cards_row" { ...blockProps }>
<InnerBlocks.Content />
</div>
}
});
So here is the solution: https://wordpress.stackexchange.com/questions/390696/innerblocks-breaks-flexbox-and-css-grid-styles/390699
I need to use useInnerBlocksProps instead of InnerBlocks which is how the blocks that come with core do it.
I have a table on my react website using Tabulator. For some reason, the column headings would not show even though I have all the titles setup. My guess is that maybe cause I am setting the data again on componentDidUpdate() which is loading the data from redux.
Please help me on how to fix this.
The attached screenshot below shows how it is currently looking. I would want the column headings like "Name, ID" be shown on top.
import React, {Component} from 'react';
import {connect} from 'react-redux'
import {withRouter} from "react-router";
import 'react-tabulator/lib/styles.css'; // required styles
import 'react-tabulator/lib/css/tabulator_modern.css'; // theme
import Tabulator from "tabulator-tables";
class AllCoursesTabulator extends Component {
constructor(props) {
super(props);
this.el = React.createRef();
this.tabulator = null;
this.ref = null;
this.dataEditedFunc = this.dataEditedFunc.bind(this);
}
componentDidMount() {
let columns = [
{title: "ID", width: 150, field: "course_id"},
{title: "Name", field: "course_name"},
{title: "Section", field: "course_section"},
{title: "Instructor ID", field: "employee_id"},
{title: "Instructor Email", field: "employee_email", width: 250},
{
title: "Ilearn Video Service Requested",
field: "ilearn_video_service_requested",
hozAlign: "center",
formatter: "tickCross"
},
{title: "Ilearn Page ID", field: "ilearn_id"}
];
this.tabulator = new Tabulator(this.el, {
columns: columns,
layout: "fitColumns",
data: this.props.data,
reactiveData: true,
height: "500px",
cellEdited: this.dataEditedFunc
})
}
componentDidUpdate(prevProps, prevState, snapshot) {
this.tabulator.replaceData(this.props.data)
}
dataEditedFunc(cellData) {
//this function is to edit the cell on click
};
render() {
return (
<div className="emailTabulatorContainer">
<div ref={el => (this.el = el)}/>
</div>
)
}
}
function mapStateToProps({
globalsReducer,
coursesReducer
}, {props}) {
let data = []
let columns = []
let formatData = (course) => {
let ilearn_data = course.ilearn_page_id
if (ilearn_data != null) {
return {
course_id: course.course_gen_id,
course_name: course.course_name,
course_section: course.course_section,
employee_id: course.employee_id,
employee_email: course.course_instructor.employee_email,
ilearn_video_service_requested: course.ilearn_video_service_requested,
ilearn_id: course.ilearn_page_id.ilearn_page_id
}
}
}
if (coursesReducer !== undefined) {
Object.keys(coursesReducer).forEach(function (key) {
data.push(formatData(coursesReducer[key]))
});
}
return {
data,
columns,
semester: globalsReducer['currentSemester'],
coursesReducer
}
}
export default withRouter(connect(mapStateToProps)(AllCoursesTabulator))
Please tell me how to fix this. Thank you for your help.
Try to modify css? Just Like this
.tabulator-header .tabulator-headers.tabulator-col {
height: 40px !important; }
But, that's not a good solution.
I am trying to build a React.js SharePoint modern web part, which have the following capabilities:-
Inside the Web Part settings page >> there are 2 fields named as "Who We Are" & "Our Value" which allow the user to enter HTML.
The web part will render 2 buttons "Who We Are" & "Our Value" >> and when the user clicks on any button >> a Popup will be shown with the entered HTML code in step-1
Something as follow:-
But to be able to render HTML code as Rich-Text inside my Web Part, i have to use the dangerouslySetInnerHTML attribute inside the .tsx file. as follow:-
import * as React from 'react';
import { useId, useBoolean } from '#fluentui/react-hooks';
import {
getTheme,
mergeStyleSets,
FontWeights,
Modal,
IIconProps,
IStackProps,
} from '#fluentui/react';
import { IconButton, IButtonStyles } from '#fluentui/react/lib/Button';
export const MYModal2 = (myprops) => {
const [isModalOpen, { setTrue: showModal, setFalse: hideModal }] = useBoolean(false);
const [isPopup, setisPopup] = React.useState(true);
const titleId = useId('title');
React.useEffect(() => {
showModal();
}, [isPopup]);
function ExitHandler() {
hideModal();
setisPopup(current => !current)
myprops.handler();
}
return (
<div>
<Modal
titleAriaId={titleId}
isOpen={isModalOpen}
onDismiss={ExitHandler}
isBlocking={true}
containerClassName={contentStyles.container}
>
<div className={contentStyles.header}>
<span id={titleId}>Modal Popup</span>
<IconButton
styles={iconButtonStyles}
iconProps={cancelIcon}
ariaLabel="Close popup modal"
onClick={ExitHandler}
/>
</div>
<div className={contentStyles.body}>
<p dangerouslySetInnerHTML={{__html:myprops.OurValue}}>
</p>
</div>
</Modal>
</div>
);
};
const cancelIcon: IIconProps = { iconName: 'Cancel' };
const theme = getTheme();
const contentStyles = mergeStyleSets({
container: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'stretch',
},
header: [
// eslint-disable-next-line deprecation/deprecation
theme.fonts.xLarge,
{
flex: '1 1 auto',
borderTop: '4px solid ${theme.palette.themePrimary}',
color: theme.palette.neutralPrimary,
display: 'flex',
alignItems: 'center',
fontWeight: FontWeights.semibold,
padding: '12px 12px 14px 24px',
},
],
body: {
flex: '4 4 auto',
padding: '0 24px 24px 24px',
overflowY: 'hidden',
selectors: {
p: { margin: '14px 0' },
'p:first-child': { marginTop: 0 },
'p:last-child': { marginBottom: 0 },
},
},
});
const stackProps: Partial<IStackProps> = {
horizontal: true,
tokens: { childrenGap: 40 },
styles: { root: { marginBottom: 20 } },
};
const iconButtonStyles: Partial<IButtonStyles> = {
root: {
color: theme.palette.neutralPrimary,
marginLeft: 'auto',
marginTop: '4px',
marginRight: '2px',
},
rootHovered: {
color: theme.palette.neutralDark,
},
};
And to secure the dangerouslySetInnerHTML, i did the following steps:-
1- Inside my Node.Js CMD >> i run this command inside my project directory:-
npm install dompurify eslint-plugin-risxss
2- Then inside my above .tsx i made the following modifications:-
I added this import import { sanitize } from 'dompurify';
An I replaced this unsafe code <p dangerouslySetInnerHTML={{__html:myprops.OurValue}}></p> with this <div dangerouslySetInnerHTML={{ __html: sanitize(myprops.OurValue) }} />
So I have the following question:-
Now my approach (of using sanitize(myprops.OurValue) will/should securely render HTML as Rich-Text inside the popup since i am using the sanitize function which is part of the dompurify eslint-plugin-risxss. but i read another approach which mentioned that to securely render HTML as Rich-Text inside the popup, we can use the html-react-parser package as follow {parse(myprops.OurValue)}. So what are the differences between using 'html-react-parser' & using 'dompurify eslint-plugin-risxss' to securely render an HTML code as Rich-Text inside the React web part's popup?
Here is my Full web part code:-
inside the MyModalPopupWebPart.ts:-
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version } from '#microsoft/sp-core-library';
import {
IPropertyPaneConfiguration,
PropertyPaneTextField
} from '#microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '#microsoft/sp-webpart-base';
import * as strings from 'MyModalPopupWebPartStrings';
import MyModalPopup from './components/MyModalPopup';
import { IMyModalPopupProps } from './components/IMyModalPopupProps';
export interface IMyModalPopupWebPartProps {
description: string;
WhoWeAre: string;
OurValue:string;
}
export default class MyModalPopupWebPart extends BaseClientSideWebPart<IMyModalPopupWebPartProps> {
public render(): void {
const element: React.ReactElement<IMyModalPopupProps> = React.createElement(
MyModalPopup,
{
description: this.properties.description,
WhoWeAre: this.properties.WhoWeAre,
OurValue: this.properties.OurValue
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('WhoWeAre', {
label: "who We Are",
multiline: true
}),
PropertyPaneTextField('OurValue', {
label: "Our value"
}), PropertyPaneTextField('description', {
label: "Description",
multiline: true
}),
]
}
]
}
]
};
}
}
inside the MyModalPopup.tsx:-
import * as React from 'react';
import { IMyModalPopupProps } from './IMyModalPopupProps';
import { DefaultButton } from '#fluentui/react/lib/Button';
import { MYModal } from './MYModal';
import { MYModal2 } from './MYModal2';
interface IPopupState {
showModal: string;
}
export default class MyModalPopup extends React.Component<IMyModalPopupProps, IPopupState> {
constructor(props: IMyModalPopupProps, state: IPopupState) {
super(props);
this.state = {
showModal: ''
};
this.handler = this.handler.bind(this);
this.Buttonclick = this.Buttonclick.bind(this);
}
handler() {
this.setState({
showModal: ''
})
}
private Buttonclick(e, whichModal) {
e.preventDefault();
this.setState({ showModal: whichModal });
}
public render(): React.ReactElement<IMyModalPopupProps> {
const { showModal } = this.state;
return (
<div>
<DefaultButton onClick={(e) => this.Buttonclick(e, 'our-value')} text="Our Value" />
{ showModal === 'our-value' && <MYModal2 OurValue={this.props.OurValue} myprops={this.state} handler={this.handler} />}
<DefaultButton onClick={(e) => this.Buttonclick(e, 'who-we-are')} text="Who We Are" />
{ showModal === 'who-we-are' && <MYModal WhoWeAre={this.props.WhoWeAre} myprops={this.state} handler={this.handler} />}
</div>
);
}
}
Actually, html-react-parser returns ReactJs object, and its return type is like React.createElement or like type of called JSX.
Using DOMPurify.sanitize will return safe pure HTML elements which those are different to the object that html-react-parser returns. the risxss ESLint plugin will force you to use sanitizing with any kind of sanitize function or library, that I left an answer to your other question to how to Sanitize your string HTML.
Eventually, using sanitizing is better because is the html-react-parser will convert your string HTML to ReactJs object with some tiny changes that would be dangerous because it is possible to have some script of string HTML in the project and it maybe will be harmful it just remove the onclick or onload, etc, from HTML tags but sanitizing will remove all possible harmful tags. also sanitizing will receive configuration, which means you can have your own options for sanitizing.
I am using better-react-mathjax for equation writing and reading. Basically, I used the MathJax in my equation with a question. When it first loads it does not create a problem.
But when I use to filter using React sate it creates the Typesetting problem and the next app is crashed.
How can I fix the problem?
MathJax
import React from 'react'
import { MathJax, MathJaxContext } from "better-react-mathjax";
const config = {
loader: { load: ["input/asciimath"] }
};
export const MathJaxOutput = ({ text }) => {
return <MathJaxContext config={config} version={3}>
<MathJax dynamic inline>
{text}
</MathJax>
</MathJaxContext>
}
And the error screenshot is
When mark and course name changed automatically loads the related questions.
state = {
courseName: '',
selectedTopics: [],
mark: null,
questions:[]
}
componentDidUpdate(prevProps, prevState) {
if (this.state.courseName !== prevState.courseName || this.state.selectedTopics !== prevState.selectedTopics || this.state.mark !== prevState.mark) {
if (this.state.courseName) {
this.props.getCourseQuestions(this.state.courseName, this.state.selectedTopics, this.state.mark);
}
}
}
Output render
{
this.state.questions.map((question, questionIndex) => (
<div className='input-question-field-items' key={questionIndex}>
<div className='preview-field-item'>
<MathJaxOutput text={<p>{question.questionInput.question}</p>} />
</div>
</div>
}
I found lots of libraries that somehow marry an external library (and their DOM elements) with Vue.js. All of them though seem to only add child elements to the Vue.js-managed DOM node.
I wrote Vue-Stripe-Elements to make the use of the new Stripe V3 easier but struggled to mount Stripes elements to the Vue component.
The obvious way would be a .vue component like this:
<template>
</template>
<script>
export default {
// could also be `mounted()`
beforeMount () {
const el = Stripe.elements.create('card')
el.mount(this.$el)
}
}
</script>
This would work if Stripe only adds children to the element it is mounted too but it looks like Stripe instead transcludes or replaces the given DOM node. Stripe of course also doesn't support any VNodes.
My current solution to the problem is to create a real DOM node and add it as a child:
<template>
</template>
<script>
export default {
mounted () {
const dom_node = document.createElement('div')
const el = Stripe.elements.create('card')
el.mount(dom_node)
this.$el.appendChild(el)
}
}
</script>
It works but it feels like I'm fighting against Vue.js here and I might create odd side effects here. Or am I just doing what other appending libraries do manually and it is the best way to go?
Is there an "official" way to do this?
Thanks in advance for any helpful comment about it.
Stripe Elements Vuejs 2
Use refs to get DOM elements in vuejs.
HTML
<div ref="cardElement"></div>
JS
mounted() {
const stripe = Stripe('pk');
const elements = stripe.elements();
const card = elements.create('card');
card.mount(this.$refs.cardElement);
}
I faced the same problem, so the method mounted is correct to add, but for the big applications where u call a specific vuejs i got the error
"please make sure the element you are attempting to use is still mounted."
HTML Snippet :
<div style="min-height:100px;">
<div class="group">
<h4><span class="label label-default"> Enter Card Details</span> </h4>
<label class="cardlabel">
<span>Card number</span>
<div id="card-number-element" class="field"></div>
<span class="brand"><i class="pf pf-credit-card" id="brand-icon"></i></span>
</label>
<label class="cardlabel">
<span>Expiry date</span>
<div id="card-expiry-element" class="field"></div>
</label>
<label class="cardlabel">
<span>CVC</span>
<div id="card-cvc-element" class="field"></div>
</label>
</div>
</div>
Vue.js
(function () {
let stripe = Stripe('keyhere');
elements = stripe.elements(),
cardNumberElementStripe = undefined;
cardExpiryElementStripe = undefined;
cardCvcElementStripe = undefined;
var style = {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '40px',
fontWeight: 300,
fontFamily: 'Helvetica Neue',
fontSize: '15px',
'::placeholder': {
color: '#CFD7E0',
},
},
};
var purchase= new Vue({
el: '#purchase',
mounted() {
cardNumberElementStripe = elements.create('cardNumber', {
style: style
});
cardExpiryElementStripe = elements.create('cardExpiry', {
style: style
});
cardCvcElementStripe = elements.create('cardCvc', {
style: style
});
cardNumberElementStripe.mount('#card-number-element');
cardExpiryElementStripe.mount('#card-expiry-element');
cardCvcElementStripe.mount('#card-cvc-element');
cardNumberElementStripe.on('change', function (event) {
// Switch brand logo
if (event.brand) {
if (event.error) { setBrandIcon("unknown"); } else { setBrandIcon(event.brand); }
}
//setOutcome(event);
});
function setBrandIcon(brand) {
var brandIconElement = document.getElementById('brand-icon');
var pfClass = 'pf-credit-card';
if (brand in cardBrandToPfClass) {
pfClass = cardBrandToPfClass[brand];
}
for (var i = brandIconElement.classList.length - 1; i >= 0; i--) {
brandIconElement.classList.remove(brandIconElement.classList[i]);
}
brandIconElement.classList.add('pf');
brandIconElement.classList.add(pfClass);
}
var cardBrandToPfClass = {
'visa': 'pf-visa',
'mastercard': 'pf-mastercard',
'amex': 'pf-american-express',
'discover': 'pf-discover',
'diners': 'pf-diners',
'jcb': 'pf-jcb',
'unknown': 'pf-credit-card',
}
},
created() {
//on the buttn click u are calling this using v-on:click.prevent="payment"
payment: function (e) {
stripe.createToken(cardNumberElementStripe).then(function (result) {
debugger;
if (result.token) {
cardId = result.token.id;
// $("#paymentform").get(0).submit();
} else if (result.error) {
errorElement.textContent = result.error.message;
return;
}
});
}
}