From 83f9295bc738e1a63937d2986b9f246de5793396 Mon Sep 17 00:00:00 2001 From: Yurii Moroz Date: Sun, 3 May 2020 20:11:27 +0300 Subject: [PATCH] Implement simple formatting, add required packages --- package-lock.json | 25 ++++ package.json | 4 + src/App.js | 46 ++++--- src/control-panel/ControlPanel.css | 12 -- src/control-panel/ControlPanel.js | 18 --- src/file-zone/FileZone.js | 16 --- src/index.css | 6 +- src/text-editor/TextEditor.js | 19 +++ .../ControlPanelActionGroup.js | 24 ++++ .../components/control-panel/ControlPanel.css | 13 ++ .../components/control-panel/ControlPanel.js | 23 ++++ .../components}/file-zone/FileZone.css | 0 .../components/file-zone/FileZone.js | 14 ++ src/text-editor/plugins/index.js | 7 + .../plugins/simple-action/SimpleAction.css | 17 +++ .../plugins/simple-action/SimpleAction.js | 39 ++++++ .../plugins/simple-action/index.js | 128 ++++++++++++++++++ 17 files changed, 342 insertions(+), 69 deletions(-) delete mode 100644 src/control-panel/ControlPanel.css delete mode 100644 src/control-panel/ControlPanel.js delete mode 100644 src/file-zone/FileZone.js create mode 100644 src/text-editor/TextEditor.js create mode 100644 src/text-editor/components/control-panel-action-group/ControlPanelActionGroup.js create mode 100644 src/text-editor/components/control-panel/ControlPanel.css create mode 100644 src/text-editor/components/control-panel/ControlPanel.js rename src/{ => text-editor/components}/file-zone/FileZone.css (100%) create mode 100644 src/text-editor/components/file-zone/FileZone.js create mode 100644 src/text-editor/plugins/index.js create mode 100644 src/text-editor/plugins/simple-action/SimpleAction.css create mode 100644 src/text-editor/plugins/simple-action/SimpleAction.js create mode 100644 src/text-editor/plugins/simple-action/index.js diff --git a/package-lock.json b/package-lock.json index 0e5ac97..ca11b0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2049,6 +2049,11 @@ } } }, + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, "clean-css": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", @@ -8405,6 +8410,11 @@ "performance-now": "^2.1.0" } }, + "ramda": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.27.0.tgz", + "integrity": "sha512-pVzZdDpWwWqEVVLshWUHjNwuVP7SfcmPraYuqocJp1yo2U1R7P+5QAfDhdItkuoGqIBnBYrtPp7rEPqDn9HlZA==" + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -8529,6 +8539,21 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.1.tgz", "integrity": "sha512-xXUbDAZkU08aAkjtUvldqbvI04ogv+a1XdHxvYuHPYKIVk/42BIOD0zSKTHAWV4+gDy3yGm283z2072rA2gdtw==" }, + "react-icons": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz", + "integrity": "sha512-WsQ5n1JToG9VixWilSo1bHv842Cj5aZqTGiS3Ud47myF6aK7S/IUY2+dHcBdmkQcCFRuHsJ9OMUI0kTDfjyZXQ==", + "requires": { + "camelcase": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + } + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 55c4110..658d27d 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,12 @@ "version": "0.1.0", "private": true, "dependencies": { + "classnames": "^2.2.6", + "prop-types": "^15.7.2", + "ramda": "^0.27.0", "react": "^16.2.0", "react-dom": "^16.2.0", + "react-icons": "^3.10.0", "react-scripts": "1.1.1" }, "scripts": { diff --git a/src/App.js b/src/App.js index 9ae8704..160710d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,28 +1,30 @@ -import React, {Component} from 'react'; +import React, { useEffect, useState } from 'react'; import './App.css'; -import ControlPanel from "./control-panel/ControlPanel"; -import FileZone from "./file-zone/FileZone"; +import PLUGINS from './text-editor/plugins'; +import { groupBy, isNil } from 'ramda'; +import TextEditor from './text-editor/TextEditor'; import getMockText from './text.service'; -class App extends Component { - getText() { - getMockText().then(function (result) { - console.log(result); - }); - } - render() { - return ( -
-
- Simple Text Editor -
-
- - -
-
- ); - } +const groupedPlugins = groupBy( + plugin => String(!isNil(plugin.groupId) ? plugin.groupId : 0), + PLUGINS +); + +function App() { + const [text, setText] = useState(''); + + useEffect(() => {getMockText().then(text => setText(text))}, []); + + return ( +
+
+ Simple Text Editor +
+
+ {text} +
+
+ ); } export default App; diff --git a/src/control-panel/ControlPanel.css b/src/control-panel/ControlPanel.css deleted file mode 100644 index 2f94ccc..0000000 --- a/src/control-panel/ControlPanel.css +++ /dev/null @@ -1,12 +0,0 @@ -#control-panel { - background-color: #fff; - height: 25px; - display: flex; - align-items: center; - flex-direction: column; - padding-top: 5px; -} -#format-actions { - width: 200px; - margin-right: 400px; -} diff --git a/src/control-panel/ControlPanel.js b/src/control-panel/ControlPanel.js deleted file mode 100644 index 596c690..0000000 --- a/src/control-panel/ControlPanel.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { Component } from 'react'; -import './ControlPanel.css'; - -class ControlPanel extends Component { - render() { - return ( -
-
- - - -
-
- ); - } -} - -export default ControlPanel; diff --git a/src/file-zone/FileZone.js b/src/file-zone/FileZone.js deleted file mode 100644 index a351171..0000000 --- a/src/file-zone/FileZone.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from 'react'; -import './FileZone.css'; - -class FileZone extends Component { - render() { - return ( -
-
- -
-
- ); - } -} - -export default FileZone; diff --git a/src/index.css b/src/index.css index b3607f2..340e86e 100644 --- a/src/index.css +++ b/src/index.css @@ -16,4 +16,8 @@ footer { #root { height: 100%; -} \ No newline at end of file +} + +:root { + --content-width: 600px; +} diff --git a/src/text-editor/TextEditor.js b/src/text-editor/TextEditor.js new file mode 100644 index 0000000..4579153 --- /dev/null +++ b/src/text-editor/TextEditor.js @@ -0,0 +1,19 @@ +import PropTypes from 'prop-types'; +import React, { Fragment } from 'react'; +import ControlPanel from './components/control-panel/ControlPanel'; +import FileZone from './components/file-zone/FileZone'; + +function TextEditor(props) { + return ( + + + {props.children} + + ); +} + +TextEditor.propTypes = { + groupedPlugins: PropTypes.object +}; + +export default TextEditor; diff --git a/src/text-editor/components/control-panel-action-group/ControlPanelActionGroup.js b/src/text-editor/components/control-panel-action-group/ControlPanelActionGroup.js new file mode 100644 index 0000000..430c067 --- /dev/null +++ b/src/text-editor/components/control-panel-action-group/ControlPanelActionGroup.js @@ -0,0 +1,24 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +function ControlPanelActionGroup(props) { + const renderedPlugins = props.plugins.map((plugin, i) => { + const TagName = plugin.component; + return ; + }); + return ( +
+ {renderedPlugins} +
+ ); +} + +ControlPanelActionGroup.propTypes = { + plugins: PropTypes.array +}; + +ControlPanelActionGroup.defaultProps = { + plugins: [] +}; + +export default ControlPanelActionGroup; diff --git a/src/text-editor/components/control-panel/ControlPanel.css b/src/text-editor/components/control-panel/ControlPanel.css new file mode 100644 index 0000000..c1bf3a6 --- /dev/null +++ b/src/text-editor/components/control-panel/ControlPanel.css @@ -0,0 +1,13 @@ +.control-panel { + background-color: #fff; + height: 25px; + display: flex; + align-items: center; + padding: 5px 0; + width: var(--content-width); + margin: 0 auto; +} + +.control-panel__group:not(:last-child) { + border-right: 1px solid gray; +} diff --git a/src/text-editor/components/control-panel/ControlPanel.js b/src/text-editor/components/control-panel/ControlPanel.js new file mode 100644 index 0000000..a3f5890 --- /dev/null +++ b/src/text-editor/components/control-panel/ControlPanel.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './ControlPanel.css'; +import ControlPanelActionGroup from '../control-panel-action-group/ControlPanelActionGroup'; + +function ControlPanel(props) { + return ( +
+ {Object.keys(props.groups).map(groupId => ( +
+ +
+ ))} +
+ ); +} + +ControlPanel.propTypes = { + groups: PropTypes.object.isRequired +}; + +export default ControlPanel; diff --git a/src/file-zone/FileZone.css b/src/text-editor/components/file-zone/FileZone.css similarity index 100% rename from src/file-zone/FileZone.css rename to src/text-editor/components/file-zone/FileZone.css diff --git a/src/text-editor/components/file-zone/FileZone.js b/src/text-editor/components/file-zone/FileZone.js new file mode 100644 index 0000000..484e228 --- /dev/null +++ b/src/text-editor/components/file-zone/FileZone.js @@ -0,0 +1,14 @@ +import React from 'react'; +import './FileZone.css'; + +function FileZone(props) { + return ( +
+
+ {props.children} +
+
+ ); +} + +export default FileZone; diff --git a/src/text-editor/plugins/index.js b/src/text-editor/plugins/index.js new file mode 100644 index 0000000..063f07d --- /dev/null +++ b/src/text-editor/plugins/index.js @@ -0,0 +1,7 @@ +import { SIMPLE_ACTION_PLUGIN_CONFIG } from './simple-action'; + +const PLUGINS = [ + ...SIMPLE_ACTION_PLUGIN_CONFIG +]; + +export default PLUGINS; diff --git a/src/text-editor/plugins/simple-action/SimpleAction.css b/src/text-editor/plugins/simple-action/SimpleAction.css new file mode 100644 index 0000000..21f164c --- /dev/null +++ b/src/text-editor/plugins/simple-action/SimpleAction.css @@ -0,0 +1,17 @@ +.action-button { + background-color: transparent; + border: none; + padding: 5px 5px; +} + +.action-button:hover { + background-color: lightgrey; +} + +.action-button__icon { + vertical-align: middle; +} + +.action-button--active { + background-color: grey; +} diff --git a/src/text-editor/plugins/simple-action/SimpleAction.js b/src/text-editor/plugins/simple-action/SimpleAction.js new file mode 100644 index 0000000..da9d835 --- /dev/null +++ b/src/text-editor/plugins/simple-action/SimpleAction.js @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; +import classNames from 'classnames'; + +import './SimpleAction.css'; + +function SimpleAction(props) { + const [isActive, setIsActive] = useState(document.queryCommandState(props.command)); + const Icon = props.icon; + + useEffect( + () => { + const listener = () => setIsActive(document.queryCommandState(props.command)); + document.addEventListener('selectionchange', listener); + return () => document.removeEventListener('selectionchange', listener); + }, + [] + ); + + return ( + + ); +} + +SimpleAction.propTypes = { + command: PropTypes.string.isRequired, + icon: PropTypes.func.isRequired +}; + +export default SimpleAction; diff --git a/src/text-editor/plugins/simple-action/index.js b/src/text-editor/plugins/simple-action/index.js new file mode 100644 index 0000000..3a16f96 --- /dev/null +++ b/src/text-editor/plugins/simple-action/index.js @@ -0,0 +1,128 @@ +import SimpleAction from './SimpleAction'; +import { + FaAlignCenter, + FaAlignJustify, + FaAlignLeft, FaAlignRight, + FaBold, + FaIndent, + FaItalic, + FaOutdent, FaRemoveFormat, + FaStrikethrough, + FaUnderline +} from 'react-icons/fa'; + +const ALIGN_ACTIONS_GROUP_ID = 1; +const INDENT_ACTIONS_GROUP_ID = 2; +const GENERAL_ACTIONS_GROUP_ID = 3; + +const BOLD_ACTION = { + component: SimpleAction, + props: { + command: 'bold', + icon: FaBold + } +}; + +const ITALIC_ACTION = { + component: SimpleAction, + props: { + command: 'italic', + icon: FaItalic + } +}; + +const UNDERLINE_ACTION = { + component: SimpleAction, + props: { + command: 'underline', + icon: FaUnderline + } +}; + +const STRIKE_THROUGH_ACTION = { + component: SimpleAction, + props: { + command: 'strikeThrough', + icon: FaStrikethrough + } +}; + +const ALIGN_JUSTIFY_ACTION = { + groupId: ALIGN_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'justifyFull', + icon: FaAlignJustify + } +}; + +const ALIGN_LEFT_ACTION = { + groupId: ALIGN_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'justifyLeft', + icon: FaAlignLeft + } +}; + +const ALIGN_CENTER_ACTION = { + groupId: ALIGN_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'justifyCenter', + icon: FaAlignCenter + } +}; + +const ALIGN_RIGHT_ACTION = { + groupId: ALIGN_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'justifyRight', + icon: FaAlignRight + } +}; + +const INDENT_INCREASE_ACTION = { + groupId: INDENT_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'indent', + icon: FaIndent + } +}; + +const INDENT_DECREASE_ACTION = { + groupId: INDENT_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'outdent', + icon: FaOutdent + } +}; + +const CLEAR_FORMAT_ACTION = { + groupId: GENERAL_ACTIONS_GROUP_ID, + component: SimpleAction, + props: { + command: 'removeFormat', + icon: FaRemoveFormat + } +}; + +export const SIMPLE_ACTION_PLUGIN_CONFIG = [ + BOLD_ACTION, + ITALIC_ACTION, + UNDERLINE_ACTION, + STRIKE_THROUGH_ACTION, + + ALIGN_JUSTIFY_ACTION, + ALIGN_LEFT_ACTION, + ALIGN_RIGHT_ACTION, + ALIGN_CENTER_ACTION, + + INDENT_INCREASE_ACTION, + INDENT_DECREASE_ACTION, + + CLEAR_FORMAT_ACTION +];