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 (
-
-
-
-
-
-
-
- );
- }
+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 (
+
+
+
+ {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 (
+
+ );
+}
+
+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
+];