Skip to content

Commit

Permalink
Merge pull request sadanandpai#93 from siva-kannan3/editor
Browse files Browse the repository at this point in the history
🚀  Richtext editor
  • Loading branch information
gopal1996 authored Aug 6, 2022
2 parents 3a38539 + dd2e204 commit 897c0ac
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 29 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"@splidejs/splide": "^4.0.1",
"dayjs": "^1.11.1",
"framer-motion": "^6.3.0",
"html-react-parser": "^3.0.1",
"immer": "^9.0.12",
"jodit": "^3.18.6",
"next": "12.1.4",
"react": "18.0.0",
"react-dom": "18.0.0",
Expand Down
9 changes: 9 additions & 0 deletions src/__test__/dayjs.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import dayjs from 'dayjs';
import { dateParser } from 'src/helpers/utils';

it('should test the dateParser util function', async () => {
const dayjsDate = dayjs('12-03-1900');
expect(dateParser('2016')).toBe('Jan 2016');
expect(dateParser('01/01/2010')).toBe('Jan 2010');
expect(dateParser(dayjsDate)).toBe('Dec 1900');
});
25 changes: 25 additions & 0 deletions src/helpers/common/components/HTMLRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useMemo } from 'react';
import parseHtmlStringToHtml, { domToReact } from 'html-react-parser';
import Link from 'next/link';

import styles from './richtext/jodit.module.css';

export const HTMLRenderer = ({ htmlString }: { htmlString: string }) => {
const parsedElement = useMemo(() => {
return parseHtmlStringToHtml(htmlString, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
replace: (domNode: any) => {
if (domNode.attribs && domNode.attribs.href && domNode.name === 'a') {
return (
<Link href={domNode.attribs.href}>
<a>{domToReact(domNode.children)}</a>
</Link>
);
} else if (domNode.name === 'script') {
return <></>;
}
},
});
}, [htmlString]);
return <div className={`${styles.richtextRuntimeWrapper} text-xs`}>{parsedElement}</div>;
};
62 changes: 62 additions & 0 deletions src/helpers/common/components/richtext/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useRef, useEffect } from 'react';
import 'jodit/build/jodit.min.css';

import { LinkPlugin } from './plugins/link';

import styles from './jodit.module.css';

interface IRichtext {
label: string;
onChange: (htmlOutput: string) => void;
value: string;
name: string;
}

export const RichtextEditor = ({ label, onChange, value }: IRichtext) => {
const editorRef = useRef<HTMLTextAreaElement | null>(null);

useEffect(() => {
if (editorRef.current) {
const initEditor = async () => {
const { Jodit } = await import('jodit');
const editor = Jodit.make(editorRef.current as HTMLTextAreaElement, {
showCharsCounter: false,
showWordsCounter: false,
showXPathInStatusbar: false,
buttons: ['bold', 'italic', 'link', 'ul', 'ol', 'undo', 'redo'],
disablePlugins:
'add-new-line,print,preview,table,table-keyboard-navigation,select-cells,resize-cells,file,video,media,image,image-processor,image-properties,xpath,tab,stat,search,powered-by-jodit,mobile,justify,inline-popup,indent,iframe,fullsize',
useSearch: false,
askBeforePasteHTML: false,
askBeforePasteFromWord: false,
defaultActionOnPaste: 'insert_only_text',
maxHeight: 200,
link: LinkPlugin,
});
editor.value = value;
editor.events.on('change', (htmlOutput) => {
onChange(htmlOutput);
});
};
initEditor();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className={`${styles.editor_wrapper}`}>
<div
style={{
padding: '8px 16px 0px',
}}
className="text-resume-800 text-xs mb-1"
>
<span>{label}</span>
</div>
<textarea
ref={editorRef}
className={`min-h-[200px] min-w-full bg-[rgba(0,0,0,0.06)]`}
></textarea>
</div>
);
};
173 changes: 173 additions & 0 deletions src/helpers/common/components/richtext/jodit.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
.editor_wrapper {
background: rgba(0, 0, 0, 0.06);
border-radius: 5px;
}

.editor_wrapper :global(.jodit-container) {
position: relative;
border: none;
}

.editor_wrapper :global(.jodit-toolbar__box) {
position: absolute;
bottom: 0;
width: 100%;
border-radius: 0px;
border-inline: 1px solid #dadada;
}

.editor_wrapper :global(.jodit-workplace) {
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-weight: 400;
font-size: 16px;
line-height: 1.4375em;
letter-spacing: 0.00938em;
color: rgba(0, 0, 0, 0.87);
box-sizing: border-box;
position: relative;
cursor: text;
display: inline-flex;
align-items: center;
width: 100%;
position: relative;
/* background-color: rgba(0, 0, 0, 0.06); */
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}

.editor_wrapper :global(.jodit-wysiwyg) {
/* background-color: rgba(0, 0, 0, 0.06); */
transition: background-color 200ms cubic-bezier(0, 0, 0.2, 1) 0ms;
padding: 0px 16px 8px !important;
}

.editor_wrapper :global(.jodit-placeholder) {
padding: 0px 16px;
}

.editor_wrapper :global(.jodit-wysiwyg > ol),
.editor_wrapper :global(.jodit-wysiwyg > ul) {
display: block;

margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
}

.editor_wrapper :global(.jodit-wysiwyg ol) {
list-style-type: decimal;
}

.editor_wrapper :global(.jodit-wysiwyg ul) {
list-style-type: disc;
}

.editor_wrapper :global(.jodit-wysiwyg a) {
text-decoration: underline;
color: #007bff;
}

.editor_wrapper :global(.jodit-wysiwyg > ol),
.editor_wrapper :global(.jodit-wysiwyg > ul) {
display: block;

margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 40px;
}

.editor_wrapper :global(.jodit-wysiwyg ol) {
list-style-type: decimal;
}

.editor_wrapper :global(.jodit-wysiwyg ul) {
list-style-type: disc;
}

.editor_wrapper :global(.jodit-wysiwyg a) {
text-decoration: underline;
color: #007bff;
}

.jodit_form_group {
display: flex;
flex-direction: column;
margin-bottom: 8px;
}

.jodit-input-label {
font-size: 10px;
color: gray;
margin-bottom: 2px;
}

.jodit_input {
height: 32px;
line-height: 32px;
padding: 0 8px;
outline: none;
min-width: 200px;
border: 1px solid #dadada;
font-size: 14px;
}

.jodit_buttons {
display: flex;
justify-content: space-between;
}

.jodit_button {
appearance: none;
background: 0 0;
border: 0;
border-radius: 3px;
box-shadow: none;
box-sizing: border-box;
cursor: pointer;
font-style: normal;
height: 34px;
min-width: 38px;
outline: 0;
padding: 0 8px;
position: relative;
text-align: center;
text-decoration: none;
text-transform: none;
user-select: none;
}

.jodit_unlink_button {
background-color: #e3e3e3;
color: #212529;
}

.jodit_link_insert_button {
background-color: #007bff;
color: #fff;
}

.richtextRuntimeWrapper a {
color: #007bff;
text-decoration: underline;
}

.richtextRuntimeWrapper ul,
.richtextRuntimeWrapper ol {
margin-block-start: 1em;
margin-block-end: 1em;
margin-inline-start: 0px;
margin-inline-end: 0px;
padding-inline-start: 20px;
}

.richtextRuntimeWrapper ul {
list-style-type: disc;
}

.richtextRuntimeWrapper ol {
list-style-type: decimal;
}
43 changes: 43 additions & 0 deletions src/helpers/common/components/richtext/plugins/link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { IJodit } from 'jodit/types';
import styles from '../jodit.module.css';

const linkFormOverride = (editor: IJodit) => {
const i18n = editor.i18n.bind(editor);

return `<form class="jodit_form">
<div class="${styles.jodit_form_group}">
<label class="${styles['jodit-input-label']}" for="url">URL</label>
<input ref="url_input" class="${
styles.jodit_input
}" required type="text" id="url" name="url" placeholder="http://" type="text"/>
</div>
<div ref="content_input_box" class="${styles.jodit_form_group}">
<label class="${styles['jodit-input-label']}" for="text">Text</label>
<input ref="content_input" class="${
styles.jodit_input
}" id="text" required name="text" placeholder="${i18n('Text')}" type="text"/>
</div>
<label ref="target_checkbox_box">
<input ref="target_checkbox" class="jodit_checkbox" name="target" type="checkbox" checked/>
<span>${i18n('Open in new tab')}</span>
</label>
<label ref="nofollow_checkbox_box">
<input ref="nofollow_checkbox" class="jodit_checkbox" name="nofollow" type="checkbox" checked/>
<span>${i18n('No follow')}</span>
</label>
<div class="${styles.jodit_buttons}">
<button ref="unlink" class="${styles.jodit_button} ${
styles.jodit_unlink_button
}" type="button">${i18n('Unlink')}</button>
<button ref="insert" class="${styles.jodit_button} ${
styles.jodit_link_insert_button
}" type="submit">${i18n('Insert')}</button>
</div>
<form/>`;
};

export const LinkPlugin = {
formTemplate: linkFormOverride,
noFollowCheckbox: false,
openInNewTabCheckbox: false,
};
9 changes: 7 additions & 2 deletions src/helpers/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
export const sum = (a: number, b: number): number => {
return a + b;
import type { Dayjs } from 'dayjs/index';
import dayjs from 'dayjs';

export const dateParser = (dateValue: string | Dayjs | null, outputFormat = 'MMM YYYY') => {
if (dateValue === null) return;
const dayjsDate = dayjs(dateValue);
return dayjsDate.format(outputFormat);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { useExperiences } from 'src/stores/experience';
import { ExperienceItem } from 'src/stores/experience.interface';
import { SwitchWidget } from 'src/helpers/common/atoms/Switch';
import { RichtextEditor } from 'src/helpers/common/components/richtext';

interface Props {
experienceInfo: ExperienceItem;
Expand Down Expand Up @@ -113,20 +114,13 @@ const Experience: React.FC<Props> = memo(({ experienceInfo, currentIndex }) => {
)}
disabled={experienceInfo.isWorkingHere}
/>
<TextField
id="filled-multiline-static"
<RichtextEditor
label="Few points on this work experience"
multiline
rows={4}
variant="filled"
autoComplete="off"
fullWidth
name="summary"
value={experienceInfo.summary}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
onChangeHandler('summary', value);
onChange={(htmlOutput) => {
onChangeHandler('summary', htmlOutput);
}}
name="summary"
/>
</Fragment>
);
Expand Down
5 changes: 4 additions & 1 deletion src/modules/builder/resume/ResumeLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export const ResumeLayout = () => {

return (
<div className="mx-5">
<div style={{ transform: `scale(${zoom})` }} className="origin-top">
<div
style={{ transform: `scale(${zoom})` }}
className="origin-top transition-all duration-300 ease-linear "
>
<div className="w-[210mm] h-[296mm] bg-white my-0 mx-auto">
<StateContext.Provider value={resumeData}>
<ProfessionalTemplate />
Expand Down
Loading

0 comments on commit 897c0ac

Please sign in to comment.