${e.content}`;
86 | }
87 | });
88 | }
89 | }}
90 | tinymceScriptSrc={tinymceScriptSrc}
91 | />
92 | }
93 |
94 | ;
95 | }
96 | }
97 |
98 | export default withFieldWrapper(MceEditor);
99 |
--------------------------------------------------------------------------------
/src/components/editFields/Multiple.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Form, Button } from 'antd';
3 | import { Field } from 'react-final-form';
4 | import uniqid from 'uniqid';
5 | import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
6 | import cx from 'classnames';
7 | import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
8 |
9 | import ImageUploader from './ImageUploader';
10 | import styles from '../../css/multipleField.scss';
11 | import MceEditor from './MceEditor';
12 | import Input from './Input';
13 |
14 | export default class Multiple extends Component {
15 | static defaultProps = {
16 | editor: true
17 | };
18 |
19 | addItem = () => {
20 | const { fields, variantPlaceholder } = this.props;
21 |
22 | fields.push({
23 | label: `${variantPlaceholder} ${fields.length}`,
24 | id: uniqid('radio_option_')
25 | });
26 | }
27 |
28 | onDragEnd = result => {
29 | if (!result.destination) {
30 | return;
31 | }
32 |
33 | this.props.fields.move(result.source.index, result.destination.index);
34 | }
35 |
36 | render() {
37 | const { label, fields, editor, description } = this.props;
38 |
39 | return
40 |
41 |
42 | { provided =>
43 |
46 | { fields.map((name, index) =>
47 |
48 | { dragProvided =>
49 |
50 |
51 |
52 |
53 |
54 |
55 |
59 | { editor &&
60 |
63 | }
64 | { fields.length > 1 &&
65 | } onClick={() => fields.remove(index)} />
66 | }
67 |
68 |
69 | { description && (
70 |
71 |
75 |
76 | )}
77 |
78 | }
79 |
80 | )}
81 | { provided.placeholder }
82 |
83 | }
84 |
85 |
86 | } onClick={this.addItem}>Добавить
87 | ;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/components/editFields/RadioButtons.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Radio } from 'antd';
3 |
4 | import withFieldWrapper from '../../hocs/withFieldWrapper';
5 |
6 | class RadioButtons extends Component {
7 | static defaultProps = {
8 | options: []
9 | };
10 |
11 | onChange = e => this.props.onChange(e.target.value);
12 |
13 | render() {
14 | const { options, defaultValue, input: { value }} = this.props;
15 |
16 | return
19 | { options.map(option =>
20 |
21 | { option || '-' }
22 |
23 | )}
24 | ;
25 | }
26 | }
27 |
28 | export default withFieldWrapper(RadioButtons);
29 |
--------------------------------------------------------------------------------
/src/components/editFields/Switch.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Switch } from 'antd';
3 |
4 | import withFieldWrapper from '../../hocs/withFieldWrapper';
5 |
6 | class SwitchComponent extends Component {
7 | render() {
8 | const { input: { value }, onChange } = this.props;
9 |
10 | return ;
13 | }
14 | }
15 |
16 | export default withFieldWrapper(SwitchComponent);
17 |
--------------------------------------------------------------------------------
/src/components/editFields/Uploader.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { Button, Upload } from 'antd';
3 | import { DeleteOutlined, UploadOutlined } from '@ant-design/icons';
4 |
5 | import withFieldWrapper from '../../hocs/withFieldWrapper';
6 | import withFileUrlContext from '../../hocs/withFileUrlContext';
7 | import { getUrl } from '../../utils/files';
8 |
9 | class Uploader extends Component {
10 | state = {
11 | error: false,
12 | uploading: false
13 | };
14 |
15 | reader = new FileReader();
16 |
17 | beforeUpload = file => {
18 | this.reader.readAsDataURL(file);
19 | this.reader.onload = () => this.props.onChange({
20 | body: this.reader.result,
21 | name: file.name
22 | });
23 |
24 | return false;
25 | }
26 |
27 | onChange = info => {
28 | const { status, response, name } = info.file;
29 |
30 | switch (status) {
31 | case 'uploading':
32 | this.setState({ uploading: true });
33 | break;
34 | case 'done':
35 | this.props.onChange({ id: response.id, name });
36 | this.state.error && this.setState({ error: false, uploading: false });
37 | break;
38 | case 'error':
39 | this.setState({ error: true, uploading: false });
40 | break;
41 | default:
42 | return;
43 | }
44 | }
45 |
46 | delete = () => this.props.onChange(null);
47 |
48 | render() {
49 | const { input: { value }, uploadUrl, uploadImages, accept, withoutUrl } = this.props;
50 | const props = uploadUrl && (!withoutUrl || uploadImages) ? {
51 | action: getUrl(uploadUrl),
52 | onChange: this.onChange
53 | } : {
54 | beforeUpload: this.beforeUpload
55 | };
56 |
57 | return value ?
58 |
59 | { value.name }
60 |
63 | :
64 |
65 |
70 | }>
71 | Загрузить файл
72 |
73 |
74 | { this.state.error && Не удалось загрузить файл
}
75 | ;
76 | }
77 | }
78 |
79 | export default withFieldWrapper(withFileUrlContext(Uploader));
80 |
--------------------------------------------------------------------------------
/src/components/formElements/Checkboxes.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Checkbox, Button } from 'antd';
4 | import { append, remove, contains, memoizeWith, identity, without, path, find, propEq, any } from 'ramda';
5 | import uniqid from 'uniqid';
6 | import cx from 'classnames';
7 | import { Droppable, Draggable } from 'react-beautiful-dnd';
8 | import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
9 |
10 | import { withElementWrapper } from '../../hocs/withElementWrapper';
11 | import withFieldWrapper from '../../hocs/withFieldWrapper';
12 | import styles from '../../css/options.scss';
13 | import { shuffle } from '../../utils/methods';
14 | import Editor from './Editor';
15 |
16 | class Checkboxes extends Component {
17 | static propTypes = {
18 | options: PropTypes.array,
19 | input: PropTypes.object,
20 | isEditor: PropTypes.bool,
21 | onChange: PropTypes.func
22 | };
23 |
24 | static defaultProps = {
25 | input: {},
26 | options: [],
27 | correct: []
28 | };
29 |
30 | onChange = option => {
31 | const { onChange, isEditor } = this.props;
32 |
33 | isEditor ?
34 | onChange('correct', option.length ? option : null) :
35 | onChange(option.length ? option : null);
36 | }
37 |
38 | removeItem = index => {
39 | const { options, onChange } = this.props;
40 |
41 | onChange('options', remove(index, 1, options));
42 | }
43 |
44 | addItem = () => {
45 | const { options, onChange, placeholder } = this.props;
46 | const option = {
47 | label: `${placeholder} ${options.length}`,
48 | id: uniqid('checkboxes_option_')
49 | };
50 |
51 | onChange('options', append(option, options));
52 | }
53 |
54 | renderCheckbox = (option, index) => {
55 | const { isEditor, correct, allowCorrect, input: { value = [] }, disabled, options, downloadUrl } = this.props;
56 | const selected = contains(option.id, value);
57 | const isCorrect = contains(option.id, correct);
58 | const hasImages = any(o => o.image, options);
59 |
60 | return
61 | { provided =>
62 |
63 |
64 | { isEditor &&
65 |
66 |
67 |
68 | }
69 |
70 | { option.image &&
71 |
77 | }
78 |
84 | { !isEditor && }
85 |
86 | { isEditor &&
87 |
88 | }
92 | size='small'
93 | danger
94 | shape='circle'
95 | onClick={() => this.removeItem(index)} />
96 |
100 |
101 | }
102 |
103 |
104 |
105 | }
106 | ;
107 | }
108 |
109 | getShuffleOptions = memoizeWith(identity, options => shuffle(options))
110 |
111 | renderCheckboxes() {
112 | const { options, allowShuffle, isEditor } = this.props;
113 | const items = allowShuffle && !isEditor ? this.getShuffleOptions(options) : options;
114 |
115 | return items.map(this.renderCheckbox);
116 | }
117 |
118 | render() {
119 | const { input: { value = [] }, disabled, isEditor, correct, options, id, inline } = this.props;
120 | const incorrect = without(value, correct || []) || [];
121 |
122 | return
123 |
128 |
129 | { provided =>
130 |
131 | { this.renderCheckboxes() }
132 | { provided.placeholder }
133 |
134 | }
135 |
136 |
137 | { isEditor &&
138 |
139 | }
145 | size='small'
146 | onClick={this.addItem} />
147 |
148 | }
149 | { !!(disabled && incorrect.length) &&
150 |
151 | Правильные ответы:
152 | { correct.map((id, index) =>
153 |
156 | )}
157 |
158 | }
159 |
;
160 | }
161 | }
162 |
163 | export default withElementWrapper(Checkboxes);
164 | export const CheckboxesField = withFieldWrapper(Checkboxes);
165 |
--------------------------------------------------------------------------------
/src/components/formElements/DownloadFile.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { Button } from 'antd';
3 | import { DownloadOutlined } from '@ant-design/icons';
4 |
5 | import { withElementWrapper } from '../../hocs/withElementWrapper';
6 | import withFileUrlContext from '../../hocs/withFileUrlContext';
7 |
8 | class DownloadFileComponent extends Component {
9 | render() {
10 | const { label, downloadUrl, file } = this.props;
11 |
12 | return
13 |
14 | { file &&
15 | }
18 | target='_blank'
19 | href={file.id ? downloadUrl(file.id) : file.body}
20 | download>
21 | Скачать
22 | }
23 | ;
24 | }
25 | }
26 |
27 | export const DownloadFile = withFileUrlContext(DownloadFileComponent);
28 | export default withElementWrapper(withFileUrlContext(DownloadFileComponent));
29 |
--------------------------------------------------------------------------------
/src/components/formElements/Editor.js:
--------------------------------------------------------------------------------
1 | import { withElementWrapper } from '../../hocs/withElementWrapper';
2 | import EditorComponent from './EditorComponent';
3 |
4 | export default withElementWrapper(EditorComponent);
5 |
--------------------------------------------------------------------------------
/src/components/formElements/EditorComponent.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Editor, EditorState, RichUtils, CompositeDecorator } from 'draft-js';
4 | import { stateToHTML } from 'draft-js-export-html';
5 | import { stateFromHTML } from 'draft-js-import-html';
6 | import { path, identical, without, contains } from 'ramda';
7 | import cx from 'classnames';
8 |
9 | import InlineToolbar from './InlineToolbar';
10 | import { getSelectionRange, getBlockAlignment, styleWholeSelectedBlocksModifier, confirmLink, removeLink, findEntities } from '../../utils/editor';
11 | import Link from './Link';
12 | import styles from '../../css/editorToolbar.scss';
13 |
14 | const ALIGNMENTS = ['left', 'right', 'center'];
15 |
16 | export default class EditorComponent extends Component {
17 | static defaultProps = {
18 | isEditor: PropTypes.bool,
19 | onChange: PropTypes.func,
20 | short: PropTypes.bool
21 | };
22 |
23 | static defaultProps = {
24 | path: 'content'
25 | };
26 |
27 | constructor(props) {
28 | super(props);
29 |
30 | const value = path(props.path.split('.').map(i => identical(NaN, +i) ? i : +i), props);
31 |
32 | this.state = {
33 | showToolbar: false,
34 | linkToolbar: false,
35 | selectionRange: null,
36 | editorState: !props.isEditor ? null : value ?
37 | EditorState.createWithContent(stateFromHTML(value)) :
38 | EditorState.createEmpty()
39 | };
40 | }
41 |
42 | componentDidMount() {
43 | this.props.isEditor && this.onChange(
44 | EditorState.set(this.state.editorState, {
45 | decorator: new CompositeDecorator([
46 | {
47 | strategy: (contentBlock, callback, contentState) => {
48 | return findEntities('LINK', contentBlock, callback, contentState);
49 | },
50 | component: Link
51 | }
52 | ])
53 | })
54 | );
55 | }
56 |
57 | onChange = editorState => {
58 | const html = stateToHTML(editorState.getCurrentContent(), {
59 | inlineStyleFn: styles => {
60 | const textAlign = styles.filter((value) => contains(value, ['center', 'left', 'right'])).first();
61 |
62 | return textAlign ? {
63 | element: 'p',
64 | style: {
65 | textAlign
66 | }
67 | } : null;
68 | }
69 | });
70 |
71 | this.setState({ editorState }, () => {
72 | if (!editorState.getSelection().isCollapsed()) {
73 | const selectionRange = getSelectionRange();
74 |
75 | if (!selectionRange) {
76 | !this.state.linkToolbar && this.setState({ showToolbar: false });
77 | return;
78 | }
79 |
80 | this.setState({
81 | showToolbar: true,
82 | selectionRange
83 | });
84 | } else {
85 | this.setState({ showToolbar: false, linkToolbar: false });
86 | }
87 | });
88 | this.props.onChange(this.props.path, html);
89 | }
90 |
91 | handleKeyCommand = (command, editorState) => {
92 | const newState = RichUtils.handleKeyCommand(editorState, command);
93 |
94 | if (newState) {
95 | this.onChange(newState);
96 | return 'handled';
97 | }
98 |
99 | return 'not-handled';
100 | }
101 |
102 | toggleInlineStyle = (style, inline) => {
103 | const { editorState, urlValue } = this.state;
104 | const newState = style === 'link' ?
105 | (inline ? confirmLink(editorState, urlValue) : removeLink(editorState)) :
106 | contains(style, ALIGNMENTS) ? styleWholeSelectedBlocksModifier(editorState, style, without([style], ALIGNMENTS)) :
107 | inline ? RichUtils.toggleInlineStyle(editorState, style) :
108 | RichUtils.toggleBlockType(editorState, style);
109 |
110 | this.onChange(newState);
111 | }
112 |
113 | blockStyleFn = block => {
114 | let alignment = getBlockAlignment(block);
115 |
116 | if (!block.getText()) {
117 | const previousBlock = this.state.editorState.getCurrentContent().getBlockBefore(block.getKey());
118 |
119 | if (previousBlock) {
120 | alignment = getBlockAlignment(previousBlock);
121 | }
122 | }
123 |
124 | return `alignment--${alignment}`;
125 | }
126 |
127 | toggleLinkToolbar = linkToolbar => this.setState({ linkToolbar, showToolbar: linkToolbar });
128 |
129 | render() {
130 | const { isEditor, path, short, staticContent } = this.props;
131 | const { editorState, showToolbar, selectionRange, linkToolbar } = this.state;
132 |
133 | return isEditor ?
134 | this.container = node} className={cx(styles.editorContainer, 'editor-container', { [styles.block]: staticContent })}>
135 |
144 |
this.editor.focus()}>
145 | this.editor = node}
147 | editorState={this.state.editorState}
148 | onChange={this.onChange}
149 | placeholder='Введите ваш текст...'
150 | handleKeyCommand={this.handleKeyCommand}
151 | blockStyleFn={this.blockStyleFn} />
152 |
153 |
: ;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/components/formElements/EmptyComponent.js:
--------------------------------------------------------------------------------
1 |
2 | import React from 'react';
3 |
4 | import { withElementWrapper } from '../../hocs/withElementWrapper';
5 |
6 | export default withElementWrapper(() => );
7 |
--------------------------------------------------------------------------------
/src/components/formElements/File.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { Button, Upload } from 'antd';
3 | import { DownloadOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons';
4 |
5 | import { withElementWrapper } from '../../hocs/withElementWrapper';
6 | import withFieldWrapper from '../../hocs/withFieldWrapper';
7 | import withFileUrlContext from '../../hocs/withFileUrlContext';
8 | import { getUrl } from '../../utils/files';
9 |
10 | class File extends Component {
11 | state = {
12 | error: false
13 | };
14 |
15 | reader = new FileReader();
16 |
17 | beforeUpload = file => {
18 | this.reader.readAsDataURL(file);
19 | this.reader.onload = () => this.props.onChange(this.reader.result);
20 |
21 | return false;
22 | }
23 |
24 | onChange = info => {
25 | switch (info.file.status) {
26 | case 'done':
27 | this.props.onChange(info.file.response.id);
28 | this.state.error && this.setState({ error: false });
29 | break;
30 | case 'error':
31 | this.setState({ error: true });
32 | break;
33 | default:
34 | break;
35 | }
36 | }
37 |
38 | delete = () => this.props.onChange(null);
39 |
40 | render() {
41 | const { input: { value }, uploadUrl, downloadUrl } = this.props;
42 | const props = uploadUrl ? {
43 | action: getUrl(uploadUrl),
44 | onChange: this.onChange
45 | } : {
46 | beforeUpload: this.beforeUpload
47 | };
48 |
49 | return value ?
50 |
51 |
54 |
57 | :
58 |
59 |
60 |
63 |
64 | { this.state.error && Не удалось загрузить файл }
65 | ;
66 | }
67 | }
68 |
69 | export default withElementWrapper(withFileUrlContext(File));
70 | export const FileField = withFieldWrapper(withFileUrlContext(File));
71 |
--------------------------------------------------------------------------------
/src/components/formElements/Image.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { isEmpty, path, pathOr } from 'ramda';
3 | import cx from 'classnames';
4 | import Zoom from 'react-medium-image-zoom';
5 |
6 | import { withElementWrapper } from '../../hocs/withElementWrapper';
7 | import styles from '../../css/image.scss';
8 |
9 | export class ImageComponent extends Component {
10 | state = {
11 | height: 0,
12 | coverHeight: 0
13 | };
14 |
15 | componentDidMount() {
16 | this.getHeight();
17 | }
18 |
19 | componentDidUpdate(prev) {
20 | if (path(['url', 'name'], prev) !== path(['url', 'name'], this.props)) {
21 | this.getHeight();
22 | }
23 | }
24 |
25 | getHeight = () => {
26 | const { downloadUrl } = this.props;
27 | const url = pathOr({}, ['url'], this.props);
28 |
29 | if (url) {
30 | const img = new Image();
31 | img.src = url.id ? downloadUrl(url.id) : url.body;
32 | img.onload = () => {
33 | const height = this.container.clientWidth < img.width ?
34 | (((this.container.clientWidth * 100) / img.width) * img.height) / 100 :
35 | img.height;
36 |
37 | this.setState({
38 | height,
39 | coverHeight: this.container.clientWidth > img.width ?
40 | (((this.container.clientWidth * 100) / img.width) * img.height) / 100 :
41 | height
42 | });
43 | };
44 | } else {
45 | this.setState({ height: 0, coverHeight: 0 });
46 | }
47 | }
48 |
49 | render() {
50 | const { id, width, cover, float, repeat, downloadUrl } = this.props;
51 | const url = pathOr({}, ['url'], this.props);
52 | const isPreview = (id === 'preview');
53 |
54 | return url && !isEmpty(url) ? this.container = node}
65 | >
66 | { repeat ? (
67 |
77 | ) : (
78 |
79 |
86 |
87 | )}
88 |
: null;
89 | }
90 | }
91 |
92 | export default withElementWrapper(ImageComponent);
93 |
--------------------------------------------------------------------------------
/src/components/formElements/InlineToolbar.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import cx from 'classnames';
4 |
5 | import { getSelectionCoords, findEntityInSelection } from '../../utils/editor';
6 | import LinkInput from './LinkInput';
7 | import styles from '../../css/editorToolbar.scss';
8 |
9 | const INLINE_STYLES = [
10 | { label: 'B', style: 'BOLD', inline: true },
11 | { label: 'I', style: 'ITALIC', inline: true },
12 | { label: 'U', style: 'UNDERLINE', inline: true }
13 | ];
14 |
15 | const BLOCK_STYLES = [
16 | { label: 'H1', style: 'header-one' },
17 | { label: 'H2', style: 'header-two' },
18 | { label: 'H3', style: 'header-three' },
19 | { label: , style: 'unordered-list-item' },
20 | { label: , style: 'ordered-list-item' },
21 | { label: , style: 'left' },
22 | { label: , style: 'right' },
23 | { label: , style: 'center' }
24 | ];
25 |
26 | export default class InlineToolbar extends Component {
27 | static propTypes = {
28 | editorState: PropTypes.object,
29 | selectionRange: PropTypes.object,
30 | onToggle: PropTypes.func,
31 | show: PropTypes.bool,
32 | short: PropTypes.bool
33 | };
34 |
35 | onMouseDown = (e, type) => {
36 | e.preventDefault();
37 | this.props.onToggle(type.style, type.inline);
38 | }
39 |
40 | openLinkEditor = e => {
41 | e.preventDefault();
42 | this.props.toggleLinkToolbar(true);
43 | }
44 |
45 | getPositionData = () => {
46 | const { selectionRange } = this.props;
47 |
48 | if (selectionRange) {
49 | return getSelectionCoords(this.toolbar, selectionRange);
50 | }
51 |
52 | return {};
53 | }
54 |
55 | render() {
56 | const { editorState, show, short, linkToolbar } = this.props;
57 | const currentStyle = editorState.getCurrentInlineStyle();
58 | const positionData = this.getPositionData();
59 | const LIST = short ? INLINE_STYLES : [...INLINE_STYLES, ...BLOCK_STYLES];
60 | const blockStyle = editorState.getCurrentContent().getBlockForKey(editorState.getSelection().getStartKey()).getType();
61 |
62 | return this.toolbar = node}
64 | className={cx(styles.toolbar, 'toolbar', { [styles.showToolbar]: show })}
65 | style={positionData.coords}>
66 |
67 | { linkToolbar ?
:
68 |
69 | { LIST.map(type =>
70 | this.onMouseDown(e, type)}>
77 | { type.label }
78 |
79 | )}
80 |
85 |
86 |
87 |
88 | }
89 |
90 |
91 |
;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/formElements/Link.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default ({ contentState, entityKey, children }) => {
4 | const { value } = contentState.getEntity(entityKey).getData();
5 |
6 | return { children };
7 | };
8 |
--------------------------------------------------------------------------------
/src/components/formElements/LinkInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { RichUtils } from 'draft-js';
3 | import onClickOutside from 'react-onclickoutside';
4 | import cx from 'classnames';
5 |
6 | import { extendSelectionByData, findEntityInSelection, createEntity, removeEntity, getEditorData, getEntities } from '../../utils/editor';
7 | import styles from '../../css/editorToolbar.scss';
8 |
9 | class LinkInput extends Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.entity = findEntityInSelection(props.editorState, 'LINK');
14 | this.state = {
15 | href: this.entity !== null ? this.entity.entity.data.href : ''
16 | };
17 | }
18 |
19 | componentWillMount() {
20 | const { editorState, onChange } = this.props;
21 |
22 | if (this.entity !== null) {
23 | this.editorStateBackup = extendSelectionByData(
24 | editorState,
25 | getEntities(editorState, 'LINK', this.entity.entityKey)
26 | );
27 | } else {
28 | this.editorStateBackup = editorState;
29 | }
30 |
31 | onChange(RichUtils.toggleInlineStyle(this.editorStateBackup, 'SELECTED'));
32 | }
33 |
34 | componentWillUnmount() {
35 | this.props.onChange(this.editorStateBackup);
36 | }
37 |
38 | handleClickOutside = () => this.props.toggleLinkToolbar(false);
39 |
40 | applyValue = () => {
41 | const { href } = this.state;
42 | const { contentState } = getEditorData(this.editorStateBackup);
43 | if (this.entity === null) {
44 | this.editorStateBackup = createEntity(
45 | this.editorStateBackup,
46 | 'LINK',
47 | { href }
48 | );
49 | } else {
50 | contentState.mergeEntityData(this.entity.entityKey, { href });
51 | }
52 | this.props.toggleLinkToolbar(false);
53 | };
54 |
55 | remove = () => {
56 | this.editorStateBackup = removeEntity(this.editorStateBackup, 'LINK');
57 | this.props.toggleLinkToolbar(false);
58 | };
59 |
60 | render() {
61 | return
62 | this.setState({ href: e.target.value })} />
63 |
64 | { this.entity !== null && }
65 | ;
66 | }
67 | }
68 |
69 | export default onClickOutside(LinkInput);
70 |
--------------------------------------------------------------------------------
/src/components/formElements/Pdf.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { Document, Page, pdfjs } from 'react-pdf';
3 | import { Button, Spin, Modal } from 'antd';
4 | import cx from 'classnames';
5 | import { pathOr, range, filter } from 'ramda';
6 | import { LeftOutlined, RightOutlined, FullscreenOutlined } from '@ant-design/icons';
7 |
8 | import { withElementWrapper } from '../../hocs/withElementWrapper';
9 | import withFileUrlContext from '../../hocs/withFileUrlContext';
10 | import styles from '../../css/pdf.scss';
11 |
12 | pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/${pdfjs.version}/pdf.worker.js`;
13 |
14 | const Spinner = () =>
15 |
16 |
17 |
;
18 |
19 | class PdfField extends Component {
20 | state = {
21 | numPages: null,
22 | pageNumber: 1,
23 | fullScreen: false,
24 | loaded: false
25 | };
26 |
27 | componentDidUpdate(prev) {
28 | if (this.props.file && !prev.file) {
29 | this.setState({ loaded: false });
30 | }
31 | }
32 |
33 | onLoadSuccess = ({ numPages }) => this.setState({ numPages, pageNumber: 1, loaded: true });
34 |
35 | back = () => this.setState(prev => ({ pageNumber: prev.pageNumber - 1 }));
36 |
37 | next = () => this.setState(prev => ({ pageNumber: prev.pageNumber + 1 }));
38 |
39 | getWidth = () => {
40 | const clientWidth = pathOr(450, ['clientWidth'], this.container);
41 |
42 | return this.props.width || (this.pageRef ? (
43 | pathOr(clientWidth, ['ref', 'clientWidth'], this.pageRef)
44 | ) : clientWidth);
45 | }
46 |
47 | openFullPdf = () => {
48 | this.setState({ fullScreen: true });
49 | }
50 |
51 | closeFullPdf = () => {
52 | this.setState({ fullScreen: false });
53 | }
54 |
55 | getPages = () => {
56 | const { pageNumber, numPages } = this.state;
57 | const pages = range(pageNumber - 2, pageNumber + 3);
58 |
59 | return this.props.allPages ? range(1, numPages + 1) : filter(num => num > 0 && num < numPages + 1, pages);
60 | }
61 |
62 | renderPdf = () => {
63 | const { file, downloadUrl, allPages } = this.props;
64 | const fileUrl = file.id ? downloadUrl(file.id) : file.body;
65 | const { pageNumber, numPages, loaded } = this.state;
66 | const pages = this.getPages();
67 |
68 | return this.container = node}>
69 |
70 |
71 | { !allPages &&
72 |
73 | } onClick={this.back} disabled={pageNumber < 2} />
74 | } onClick={this.next} disabled={pageNumber >= numPages} />
75 |
76 | }
77 | } onClick={this.openFullPdf} />
78 |
79 |
80 |
81 |
this.pdf = node}
83 | file={fileUrl}
84 | onLoadSuccess={this.onLoadSuccess}
85 | loading={}>
86 | { pages.map(p =>
87 |
88 |
this.pageRef = node) : null}
90 | pageNumber={p}
91 | width={this.getWidth()}
92 | loading={} />
93 |
94 | )}
95 |
96 |
Закрыть}
101 | width='100%'
102 | destroyOnClose>
103 |
104 |
105 |
106 | { !allPages && loaded &&
107 |
108 | { pageNumber } / { numPages }
109 |
110 | }
111 |
;
112 | }
113 |
114 | render() {
115 | const { file, label } = this.props;
116 |
117 | return
118 |
119 | { file ? this.renderPdf() : null }
120 | ;
121 | }
122 | }
123 |
124 | export const PdfComponent = withFileUrlContext(PdfField);
125 |
126 | export default withElementWrapper(PdfComponent);
127 |
--------------------------------------------------------------------------------
/src/components/formElements/RadioButtons.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Radio, Button } from 'antd';
4 | import uniqid from 'uniqid';
5 | import { append, remove, path, propEq, find, any, memoizeWith, identity } from 'ramda';
6 | import cx from 'classnames';
7 | import { Droppable, Draggable } from 'react-beautiful-dnd';
8 | import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
9 |
10 | import { withElementWrapper } from '../../hocs/withElementWrapper';
11 | import withFieldWrapper from '../../hocs/withFieldWrapper';
12 | import styles from '../../css/options.scss';
13 | import { shuffle } from '../../utils/methods';
14 | import Editor from './Editor';
15 |
16 | class RadioButtons extends Component {
17 | static propTypes = {
18 | options: PropTypes.array,
19 | input: PropTypes.object,
20 | isEditor: PropTypes.bool,
21 | onChange: PropTypes.func
22 | };
23 |
24 | static defaultProps = {
25 | input: {},
26 | options: []
27 | };
28 |
29 | onChange = e => {
30 | const { onChange, isEditor } = this.props;
31 | const { value } = e.target;
32 |
33 | isEditor ?
34 | onChange('correct', value) :
35 | onChange(value);
36 | }
37 |
38 | removeItem = index => {
39 | const { options, onChange } = this.props;
40 |
41 | onChange('options', remove(index, 1, options));
42 | }
43 |
44 | addItem = () => {
45 | const { options, onChange, placeholder } = this.props;
46 | const option = {
47 | label: `${placeholder} ${options.length}`,
48 | id: uniqid('radio_option_')
49 | };
50 |
51 | onChange('options', append(option, options));
52 | }
53 |
54 | renderRadio = (option, index) => {
55 | const { isEditor, disabled, correct, allowCorrect, input: { value }, options, downloadUrl } = this.props;
56 | const selected = value === option.id;
57 | const isCorrect = option.id === correct;
58 | const hasImages = any(o => o.image, options);
59 |
60 | return
61 | { provided =>
62 |
63 |
64 | { isEditor &&
65 |
66 |
67 |
68 | }
69 |
70 | { option.image &&
71 |
77 | }
78 |
84 | { !isEditor && }
85 |
86 | { isEditor &&
87 |
88 | }
92 | size='small'
93 | danger
94 | shape='circle'
95 | onClick={() => this.removeItem(index)} />
96 |
100 |
101 | }
102 |
103 |
104 |
105 | }
106 | ;
107 | }
108 |
109 | getShuffleOptions = memoizeWith(identity, options => shuffle(options))
110 |
111 | renderRadioButtons() {
112 | const { options, allowShuffle, isEditor } = this.props;
113 | const items = allowShuffle && !isEditor ? this.getShuffleOptions(options) : options;
114 |
115 | return items.map(this.renderRadio);
116 | }
117 |
118 | render() {
119 | const { input: { value }, disabled, isEditor, correct, options, id, inline } = this.props;
120 |
121 | return
122 |
127 |
128 | { provided =>
129 |
130 | { this.renderRadioButtons() }
131 | { provided.placeholder }
132 |
133 | }
134 |
135 |
136 | { !!(disabled && correct && correct !== value) &&
137 |
138 | Правильный ответ:
139 |
140 |
141 | }
142 |
;
143 | }
144 | }
145 |
146 | export default withElementWrapper(RadioButtons);
147 | export const RadioButtonsField = withFieldWrapper(RadioButtons);
148 |
--------------------------------------------------------------------------------
/src/components/formElements/Range.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Slider, InputNumber } from 'antd';
4 | import cx from 'classnames';
5 |
6 | import { withElementWrapper } from '../../hocs/withElementWrapper';
7 | import Editor from './Editor';
8 | import withFieldWrapper from '../../hocs/withFieldWrapper';
9 | import styles from '../../css/range.scss';
10 |
11 | class Range extends Component {
12 | static propTypes = {
13 | step: PropTypes.number,
14 | minValue: PropTypes.number,
15 | maxValue: PropTypes.number,
16 | fieldName: PropTypes.string,
17 | minLabel: PropTypes.PropTypes.oneOfType([
18 | PropTypes.string,
19 | PropTypes.element
20 | ]),
21 | maxLabel: PropTypes.PropTypes.oneOfType([
22 | PropTypes.string,
23 | PropTypes.element
24 | ])
25 | };
26 |
27 | static defaultProps = {
28 | input: {}
29 | };
30 |
31 | onChange = value => {
32 | const { onChange } = this.props;
33 |
34 | onChange && onChange(value);
35 | }
36 |
37 | render() {
38 | const { step, minValue, maxValue, minLabel, maxLabel, disabled, input: { value }, isEditor, onChange } = this.props;
39 |
40 | return
41 |
: minLabel
49 | },
50 | [maxValue]: {
51 | label: isEditor ?
: maxLabel
52 | }
53 | }}
54 | disabled={disabled}
55 | onChange={this.onChange}
56 | />
57 | { isEditor &&
58 |
59 | onChange('minValue', v)} />
60 | Шаг: onChange('step', v)} />
61 | onChange('maxValue', v)} />
62 |
63 | }
64 |
;
65 | }
66 | }
67 |
68 | export default withElementWrapper(Range);
69 | export const RangeField = withFieldWrapper(Range);
70 |
--------------------------------------------------------------------------------
/src/components/formElements/Video.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Input } from 'antd';
4 | import Player from 'react-player';
5 | import cx from 'classnames';
6 |
7 | import { withElementWrapper } from '../../hocs/withElementWrapper';
8 | import styles from '../../css/video.scss';
9 |
10 | class VideoPlayer extends Component {
11 | state = {
12 | minWidth: 0
13 | };
14 |
15 | componentDidMount() {
16 | this.setSize();
17 | window.addEventListener('resize', this.setSize);
18 | }
19 |
20 | componentWillUnmount() {
21 | window.removeEventListener('resize', this.setSize);
22 | }
23 |
24 | setSize = () => {
25 | const minWidth = this.container.clientWidth;
26 | const minHeight = (9 * minWidth) / 16;
27 | const height = this.props.height || (this.props.width && (this.props.width / 3)) || minHeight;
28 |
29 | this.setState({
30 | minWidth,
31 | minHeight: this.props.responsive ?
32 | (((100 * minWidth) / minWidth) * minHeight) / 100
33 | : (((100 * minWidth) / (this.props.width || minWidth)) * height) / 100
34 | });
35 | }
36 |
37 | render() {
38 | const { isDraggingOver, width, height, responsive, src } = this.props;
39 | const useMinSizes = responsive || !(width || height) || (width > this.state.minWidth);
40 |
41 | return this.container = node}>
42 |
43 |
;
44 | }
45 | }
46 |
47 | export class Video extends Component {
48 | static propTypes = {
49 | src: PropTypes.string,
50 | isEditor: PropTypes.bool,
51 | isDraggingOver: PropTypes.bool
52 | };
53 |
54 | static defaultProps = {
55 | src: ''
56 | };
57 |
58 | onChange = e => {
59 | this.props.onChange('src', e.target.value);
60 | }
61 |
62 | render() {
63 | const { src, isEditor, isDraggingOver, responsive, width, height } = this.props;
64 |
65 | return
66 | { isEditor &&
67 |
71 | }
72 | { src ? (
73 |
80 | ) : null}
81 |
;
82 | }
83 | }
84 |
85 | export default withElementWrapper(Video);
86 |
--------------------------------------------------------------------------------
/src/constants/componentsDefaults.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import uniqid from 'uniqid';
3 | import range from 'ramda/src/range';
4 | import length from 'ramda/src/length';
5 | import symmetricDifference from 'ramda/src/symmetricDifference';
6 |
7 | import Checkboxes, { CheckboxesField } from '../components/formElements/Checkboxes';
8 | import RadioButtons, { RadioButtonsField } from '../components/formElements/RadioButtons';
9 | import Range, { RangeField } from '../components/formElements/Range';
10 | import Editor from '../components/formElements/Editor';
11 | import EditorComponent from '../components/formElements/EditorComponent';
12 | import Video, { Video as VideoComponent } from '../components/formElements/Video';
13 | import File, { FileField } from '../components/formElements/File';
14 | import DownloadFile, { DownloadFile as DownloadFileComponent } from '../components/formElements/DownloadFile';
15 | import Pdf, { PdfComponent } from '../components/formElements/Pdf';
16 | import Image, { ImageComponent } from '../components/formElements/Image';
17 |
18 | const arrayIncorrect = (value, correct) => length(symmetricDifference(value || [], correct || [])) ? 'Неправильный ответ' : undefined;
19 |
20 | const renderInfo = (prop = 'label') => props => ;
21 |
22 | const COMPONENTS_DEFAULTS = placeholder => ([
23 | {
24 | type: 'Editor',
25 | name: 'Текст',
26 | icon: 'font',
27 | renderInfo: renderInfo('content'),
28 | component: Editor,
29 | formComponent: EditorComponent,
30 | props: {
31 | content: '
'
32 | },
33 | staticContent: true,
34 | hidePreview: true,
35 | fields: [
36 | { type: 'editor', prop: 'content' }
37 | ]
38 | },
39 | {
40 | type: 'Checkboxes',
41 | name: 'Множественный выбор',
42 | icon: 'check-square-o',
43 | renderInfo: renderInfo(),
44 | component: Checkboxes,
45 | formComponent: CheckboxesField,
46 | fieldType: 'checkbox',
47 | props: {
48 | label: 'Множественный выбор',
49 | options: range(0, 3).map(i => ({
50 | label: `${placeholder || 'выбор'} ${i}`,
51 | id: uniqid('checkboxes_option_')
52 | }))
53 | },
54 | ableCorrect: true,
55 | correctValidator: arrayIncorrect,
56 | fields: [
57 | { type: 'editor', label: 'Название поля', prop: 'label', props: { short: true }},
58 | { type: 'multiple', label: 'Ответы', prop: 'options', fieldArray: true },
59 | { type: 'input', label: 'Вес вопроса', prop: 'questionWeight', props: { number: true }},
60 | { type: 'switch', label: 'Обязательное поле', prop: 'required' },
61 | { type: 'switch', label: 'Выводить варианты в случайном порядке', prop: 'allowShuffle' },
62 | { type: 'switch', label: 'Отображать варианты по горизонтали', prop: 'inline' }
63 | ]
64 | },
65 | {
66 | type: 'Radio',
67 | name: 'Выбор',
68 | icon: 'dot-circle-o',
69 | renderInfo: renderInfo(),
70 | component: RadioButtons,
71 | formComponent: RadioButtonsField,
72 | fieldType: 'radio',
73 | props: {
74 | label: 'Выбор',
75 | options: range(0, 3).map(i => ({
76 | label: `${placeholder || 'выбор'} ${i}`,
77 | id: uniqid('radiobuttons_option_')
78 | }))
79 | },
80 | ableCorrect: true,
81 | fields: [
82 | { type: 'editor', label: 'Название поля', prop: 'label', props: { short: true }},
83 | { type: 'multiple', label: 'Ответы', prop: 'options', fieldArray: true },
84 | { type: 'input', label: 'Вес вопроса', prop: 'questionWeight', props: { number: true }},
85 | { type: 'switch', label: 'Обязательное поле', prop: 'required' },
86 | { type: 'switch', label: 'Выводить варианты в случайном порядке', prop: 'allowShuffle' },
87 | { type: 'switch', label: 'Отображать варианты по горизонтали', prop: 'inline' }
88 | ]
89 | },
90 | {
91 | type: 'Range',
92 | name: 'Слайдер',
93 | icon: 'sliders',
94 | renderInfo: renderInfo(),
95 | component: Range,
96 | formComponent: RangeField,
97 | fieldType: 'range',
98 | props: {
99 | label: 'Слайдер',
100 | step: 1,
101 | defaultValue: 3,
102 | minValue: 1,
103 | maxValue: 5,
104 | minLabel: 'Минимум',
105 | maxLabel: 'Максимум'
106 | },
107 | fields: [
108 | { type: 'editor', label: 'Название поля', prop: 'label', props: { short: true }},
109 | { type: 'input', label: 'Значение по-умолчанию', prop: 'defaultValue', props: { number: true }},
110 | { type: 'input', label: 'Шаг', prop: 'step', props: { number: true, min: 1 }},
111 | { type: 'input', label: 'Минимальное значение', prop: 'minValue', props: { number: true }},
112 | { type: 'input', label: 'Название минимального значения', prop: 'minLabel'},
113 | { type: 'input', label: 'Максимальное значение', prop: 'maxValue', props: { number: true }},
114 | { type: 'input', label: 'Название максимального значения', prop: 'maxLabel'},
115 | { type: 'switch', label: 'Обязательное поле', prop: 'required' },
116 | { type: 'input', label: 'Вес вопроса', prop: 'questionWeight', props: { number: true }}
117 | ]
118 | },
119 | {
120 | type: 'Video',
121 | name: 'Видео',
122 | icon: 'video-camera',
123 | component: Video,
124 | formComponent: VideoComponent,
125 | staticContent: true,
126 | props: {
127 | responsive: true,
128 | width: 560,
129 | height: 350
130 | },
131 | fields: [
132 | { type: 'input', label: 'Ссылка на видео', prop: 'src' },
133 | { type: 'input', label: 'Высота', prop: 'height', props: { number: true }},
134 | { type: 'input', label: 'Ширина', prop: 'width', props: { number: true }},
135 | { type: 'switch', label: 'Адаптивный размер', prop: 'responsive' },
136 | ]
137 | },
138 | {
139 | type: 'File',
140 | name: 'Файл',
141 | icon: 'file-o',
142 | renderInfo: renderInfo(),
143 | component: File,
144 | formComponent: FileField,
145 | props: {
146 | label: 'Файл'
147 | },
148 | fields: [
149 | { type: 'editor', label: 'Название поля', prop: 'label', props: { short: true }}
150 | ]
151 | },
152 | {
153 | type: 'DownloadFile',
154 | name: 'Скачивание файла',
155 | icon: 'download',
156 | component: DownloadFile,
157 | formComponent: DownloadFileComponent,
158 | staticContent: true,
159 | props: {
160 | label: 'Файл'
161 | },
162 | fields: [
163 | { type: 'editor', label: 'Название поля', prop: 'label', props: { short: true }},
164 | { type: 'uploader', label: 'Файл', prop: 'file' }
165 | ]
166 | },
167 | {
168 | type: 'Pdf',
169 | name: 'PDF-презентация',
170 | icon: 'file-pdf-o',
171 | component: Pdf,
172 | formComponent: PdfComponent,
173 | staticContent: true,
174 | props: {
175 | label: 'PDF-презентация'
176 | },
177 | fields: [
178 | { type: 'editor', label: 'Название поля', prop: 'label', props: { short: true }},
179 | { type: 'uploader', label: 'Файл', prop: 'file', props: { accept: '.pdf' }},
180 | { type: 'input', label: 'Ширина', prop: 'width', props: { number: true }},
181 | { type: 'switch', label: 'Выводить все страницы', prop: 'allPages' }
182 | ]
183 | },
184 | {
185 | type: 'Image',
186 | name: 'Изображение',
187 | icon: 'picture-o',
188 | component: Image,
189 | formComponent: ImageComponent,
190 | staticContent: true,
191 | fields: [
192 | { type: 'uploader', label: 'Изображение', prop: 'url', props: { withoutUrl: true, accept: 'image/*' }},
193 | { type: 'radiobuttons', label: 'Выравнивание текста', prop: 'float', props: { options: ['left', null, 'right']} },
194 | { type: 'input', label: 'Ширина (%)', prop: 'width', props: { number: true, min: 0, max: 100 } },
195 | { type: 'switch', label: 'Растянуть на всю ширину', prop: 'cover' },
196 | { type: 'switch', label: 'Повторять по горизонтали', prop: 'repeat' }
197 | ]
198 | }
199 | ]);
200 |
201 | export default COMPONENTS_DEFAULTS;
202 |
--------------------------------------------------------------------------------
/src/contexts/ComponentsContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const ComponentsContext = createContext([]);
4 |
5 | export default ComponentsContext;
6 |
--------------------------------------------------------------------------------
/src/contexts/EditModalContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const EditModalContext = createContext();
4 |
5 | export default EditModalContext;
6 |
--------------------------------------------------------------------------------
/src/contexts/FileUrlContext.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const FileUrlContext = createContext({});
4 |
5 | export default FileUrlContext;
6 |
--------------------------------------------------------------------------------
/src/contexts/MceLanguageUrl.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 |
3 | const MceLanguageUrl = createContext();
4 |
5 | export default MceLanguageUrl;
6 |
--------------------------------------------------------------------------------
/src/css/colorPicker.scss:
--------------------------------------------------------------------------------
1 | .swatch {
2 | padding: 5px;
3 | background: #fff;
4 | border-radius: 1px;
5 | box-shadow: 0 0 0 1px rgba(0,0,0,.1);
6 | display: inline-block;
7 | cursor: pointer;
8 | margin-left: 1px;
9 | margin-top: 4px;
10 | }
11 |
12 | .color {
13 | width: 36px;
14 | height: 22px;
15 | border-radius: 2px;
16 | }
17 |
18 | .cover {
19 | position: fixed;
20 | top: 0;
21 | right: 0;
22 | bottom: 0;
23 | left: 0;
24 | z-index: 999;
25 | }
26 |
27 | .popover {
28 | position: absolute;
29 | z-index: 1000;
30 | top: 4px;
31 | left: 48px;
32 | :global {
33 | .chrome-picker {
34 | box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 2px, rgba(0, 0, 0, 0.1) 0px 4px 8px !important;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/css/editor.scss:
--------------------------------------------------------------------------------
1 | .editor {
2 | display: flex;
3 | flex-direction: row;
4 | }
5 |
6 | .editorCol {
7 | width: 50%;
8 | border-right: 1px solid #e8e8e8;
9 | :global {
10 | .ant-form {
11 | height: 100%;
12 | }
13 | }
14 | }
15 |
16 | .editorPreviewCol {
17 | width: 50%;
18 | padding: 15px;
19 | max-height: calc(100vh - 154px);
20 | overflow-y: auto;
21 | }
22 |
23 | .editorCol p,
24 | .editorPreviewCol p {
25 | margin-bottom: 0 !important
26 | }
27 |
28 | .editorField {
29 | padding: 10px;
30 | border: 1px solid #e8e8e8;
31 | }
32 |
33 | .editorModal {
34 | top: 50px !important;
35 | :global {
36 | .ant-modal-body {
37 | max-height: calc(100vh - 154px);
38 | overflow: hidden;
39 | padding: 0;
40 | }
41 | }
42 | }
43 |
44 | .editorFooter {
45 | border-top: 1px solid #e8e8e8;
46 | padding: 15px;
47 | width: 100%;
48 | background: #fff;
49 | }
50 |
51 | .editorFields {
52 | padding: 15px;
53 | max-height: calc(100vh - 216px);
54 | overflow-y: auto;
55 | height: calc(100% - 62px);
56 | }
57 |
58 | .editorColHidePreview {
59 | width: 100%;
60 | border: none;
61 | }
62 |
63 | .editor-view {
64 | overflow: hidden;
65 | }
66 |
--------------------------------------------------------------------------------
/src/css/editorInput.scss:
--------------------------------------------------------------------------------
1 | .editorInput {
2 | border: 1px solid #d9d9d9;
3 | border-radius: 6px;
4 | padding: 7px 10px;
5 | min-height: 80px;
6 | :global {
7 | .mce-content-body {
8 | min-height: 70px;
9 | * {
10 | line-height: 20px;
11 | }
12 | }
13 | .mce-edit-focus {
14 | outline: none;
15 | }
16 | }
17 | }
18 |
19 | .editorInputShort {
20 | min-height: auto;
21 | :global {
22 | .mce-content-body {
23 | min-height: auto;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/css/editorToolbar.scss:
--------------------------------------------------------------------------------
1 | .toolbar {
2 | position: absolute;
3 | z-index: 9999;
4 | height: 35px;
5 | cursor: default;
6 | visibility: hidden;
7 | }
8 |
9 | .showToolbar {
10 | visibility: visible;
11 | }
12 |
13 | .showToolbar .toolbarItems,
14 | .showToolbar .toolbarItem {
15 | opacity: 1;
16 | }
17 |
18 | .toolbarItems {
19 | opacity: 0;
20 | position: absolute;
21 | z-index: 1;
22 | display: flex;
23 | align-items: center;
24 | justify-content: space-between;
25 | list-style: none;
26 | height: 100%;
27 | margin: 0;
28 | padding: 0;
29 | background: linear-gradient(to bottom, #f5f5f5 0%, #ededed 100%);
30 | border: 1px solid rgba(0, 0, 0, 0.05);
31 | box-shadow: 0 0 25px -4px rgba(0, 0, 0, 0.25);
32 | }
33 |
34 | .toolbarItemsArrow {
35 | position: absolute;
36 | bottom: -5px;
37 | margin-left: -5px;
38 | border-top: 5px solid #444;
39 | border-right: 5px solid transparent;
40 | border-left: 5px solid transparent;
41 | }
42 |
43 | .toolbarItem {
44 | display: flex;
45 | opacity: 0;
46 | align-items: center;
47 | justify-content: center;
48 | height: 100%;
49 | width: 35px;
50 | max-width: 35px;
51 | color: #444;
52 | border-bottom: 3px solid #444;
53 | transition: all 0.15s linear;
54 | cursor: pointer;
55 | }
56 |
57 | .toolbarItem:hover svg path {
58 | fill: #1890ff;
59 | }
60 |
61 | .toolbarItem:hover,
62 | .active {
63 | color: #1890ff;
64 | border-color: #1890ff;
65 | }
66 |
67 | .bold {
68 | font-weight: bold;
69 | }
70 |
71 | .italic {
72 | font-style: italic;
73 | }
74 |
75 | .underline {
76 | text-decoration: underline;
77 | }
78 |
79 | .toolbarLinkContainer {
80 | color: #444;
81 | border-bottom: 3px solid #444;
82 | display: flex;
83 | padding: 3px;
84 | }
85 |
86 | .toolbarLinkContainer:after {
87 | content: '';
88 | position: absolute;
89 | bottom: -5px;
90 | margin-left: -5px;
91 | border-top: 5px solid #444;
92 | border-right: 5px solid transparent;
93 | border-left: 5px solid transparent;
94 | }
95 |
96 | .toolbarLinkContainer input {
97 | border: none;
98 | background: none;
99 | border-bottom: 1px solid #ccc;
100 | }
101 |
102 | .toolbarLinkContainer input:focus {
103 | outline: none;
104 | }
105 |
106 | .toolbarLinkContainer button {
107 | background: none;
108 | border: none;
109 | transition: color .3s;
110 | cursor: pointer;
111 | }
112 |
113 | .toolbarLinkContainer button:hover {
114 | color: #1890ff;
115 | }
116 |
117 | .toolbarLinkContainer button:focus {
118 | outline: none;
119 | }
120 |
--------------------------------------------------------------------------------
/src/css/formBuilder.scss:
--------------------------------------------------------------------------------
1 | .reactFormBuilder {
2 | padding: 15px;
3 | }
4 |
5 | .reactFormBuilder input[type=checkbox],
6 | .reactFormBuilder input[type=radio] {
7 | margin-right: 5px;
8 | }
9 |
10 | .reactFormBuilderPreview {
11 | position: relative;
12 | width: calc(100% - 270px);
13 | border: 1px solid #ddd;
14 | background: #fafafa;
15 | padding: 10px;
16 | box-shadow: 0 0 2px 1px rgba(0,0,0,0.1);
17 | min-height: 200px;
18 | }
19 |
20 | .reactFormBuilderPreview label {
21 | font-weight: normal;
22 | }
23 |
24 | .reactFormBuilderPreview {
25 | :global {
26 | .form-label {
27 | display: block !important;
28 | }
29 | }
30 | }
31 |
32 | .reactFormBuilderPreviewBtn {
33 | padding-bottom: 20px;
34 | padding-right: 270px;
35 | }
36 |
37 | .droppableContainer > div {
38 | width: 100%;
39 | min-height: 200px;
40 | }
41 |
42 | .droppableContainer > div > li {
43 | list-style-type: none;
44 | }
45 |
46 | .reactFormBuilderPreviewModal {
47 | :global {
48 | .ant-modal-body {
49 | padding: 0;
50 | }
51 | }
52 | }
53 |
54 | .experiumPlayerBuilder {
55 | :global {
56 | .ant-checkbox-disabled + span,
57 | .ant-radio-disabled + span {
58 | color: rgba(0, 0, 0, 0.65) !important;
59 | cursor: default !important;
60 | }
61 |
62 | .ant-checkbox-disabled input,
63 | .ant-radio-disabled input {
64 | cursor: default !important;
65 | }
66 |
67 | .ant-checkbox-disabled .ant-checkbox-inner,
68 | .ant-radio-disabled .ant-radio-inner {
69 | background: #fff;
70 | }
71 |
72 | .DraftEditor-root {
73 | cursor: initial;
74 | }
75 |
76 | .public-DraftEditorPlaceholder-inner {
77 | white-space: nowrap !important;
78 | }
79 |
80 | .ant-checkbox-group,
81 | .ant-radio-group {
82 | line-height: 1.5 !important;
83 | }
84 |
85 | .ant-checkbox-group .ant-checkbox-wrapper,
86 | .ant-radio-group .ant-radio-wrapper {
87 | display: inline-block;
88 | margin-left: 0 !important;
89 | }
90 |
91 | .ant-checkbox-group .ant-checkbox-wrapper p,
92 | .ant-radio-group .ant-radio-wrapper p {
93 | display: inline-block;
94 | margin-bottom: 0 !important;
95 | }
96 |
97 | .ant-form-item-label {
98 | width: 100%;
99 | overflow: visible !important;
100 | text-align: left !important;
101 | }
102 |
103 | .ant-form-item-label p {
104 | margin-bottom: 0;
105 | display: inline;
106 | }
107 |
108 | .ant-form-item-control.has-error .ant-radio-inner,
109 | .ant-form-item-control.has-error .ant-checkbox-inner {
110 | border-color: #f5222d;
111 | }
112 |
113 | .ant-slider-mark .ant-slider-mark-text:first-child {
114 | width: 50% !important;
115 | margin-left: -6px !important;
116 | text-align: left;
117 | }
118 |
119 | .ant-slider-mark .ant-slider-mark-text:last-child {
120 | width: 50% !important;
121 | right: 0;
122 | left: auto !important;
123 | margin-left: 0 !important;
124 | margin-right: -6px;
125 | text-align: right;
126 | }
127 |
128 | .ant-slider-mark .ant-slider-mark-text:last-child .public-DraftEditorPlaceholder-root {
129 | right: 0;
130 | }
131 |
132 | .ant-slider-disabled {
133 | cursor: default !important;
134 | }
135 |
136 | .ant-slider-disabled .ant-slider-mark-text,
137 | .ant-slider-disabled .ant-slider-step .ant-slider-dot,
138 | .ant-slider-disabled .ant-slider-handle {
139 | cursor: default !important;
140 | }
141 |
142 | .ant-slider-disabled .ant-slider:hover .ant-slider-rail {
143 | background: #f5f5f5;
144 | }
145 |
146 | .ant-form-item {
147 | margin-bottom: 10px !important;
148 | }
149 |
150 | .public-DraftStyleDefault-block,
151 | .public-DraftEditorPlaceholder-inner,
152 | .public-DraftStyleDefault-orderedListItem,
153 | .public-DraftStyleDefault-unorderedListItem {
154 | line-height: normal !important;
155 | }
156 |
157 | .alignment--left .public-DraftStyleDefault-block {
158 | text-align: left;
159 | }
160 |
161 | .alignment--center .public-DraftStyleDefault-block {
162 | text-align: center;
163 | }
164 |
165 | .alignment--right .public-DraftStyleDefault-block {
166 | text-align: right;
167 | }
168 |
169 | .ant-checkbox-wrapper.required {
170 | color: #f5222d;
171 | }
172 |
173 | .ant-checkbox-wrapper.required:after {
174 | margin: 0 !important;
175 | }
176 |
177 | .ant-checkbox-wrapper.correct .ant-checkbox,
178 | .ant-checkbox-wrapper.incorrect .ant-checkbox,
179 | .ant-radio-wrapper.correct .ant-radio,
180 | .ant-radio-wrapper.incorrect .ant-radio {
181 | cursor: default !important;
182 | }
183 |
184 | .ant-checkbox-wrapper.correct .ant-checkbox-inner,
185 | .ant-checkbox-wrapper.incorrect .ant-checkbox-inner,
186 | .ant-radio-wrapper.correct .ant-radio-inner,
187 | .ant-radio-wrapper.incorrect .ant-radio-inner {
188 | border: none;
189 | position: relative;
190 | }
191 |
192 | .ant-checkbox-wrapper.correct .ant-checkbox-inner:before,
193 | .ant-checkbox-wrapper.incorrect .ant-checkbox-inner:before,
194 | .ant-radio-wrapper.correct .ant-radio-inner:before,
195 | .ant-radio-wrapper.incorrect .ant-radio-inner:before {
196 | font: normal normal normal 14px/1 FontAwesome;
197 | position: absolute;
198 | left: 2px;
199 | }
200 |
201 | .ant-checkbox-wrapper.correct .ant-checkbox-inner:after,
202 | .ant-checkbox-wrapper.incorrect .ant-checkbox-inner:after,
203 | .ant-radio-wrapper.correct .ant-radio-inner:after,
204 | .ant-radio-wrapper.incorrect .ant-radio-inner:after {
205 | opacity: 0;
206 | }
207 |
208 | .ant-checkbox-wrapper.correct .ant-checkbox-inner:before,
209 | .ant-radio-wrapper.correct .ant-radio-inner:before {
210 | content: '\f00c';
211 | color: #41b567;
212 | }
213 |
214 | .ant-checkbox-wrapper.incorrect .ant-checkbox-inner:before,
215 | .ant-radio-wrapper.incorrect .ant-radio-inner:before {
216 | content: '\f00d';
217 | color: #f5222d;
218 | }
219 |
220 | .ant-form-item-has-error .option-item-correct,
221 | .option-item-correct .ant-checkbox-wrapper-disabled,
222 | .option-item-correct .ant-radio-wrapper-disabled {
223 | .ant-checkbox .ant-checkbox-inner {
224 | border-color: #A6B622 !important;
225 | }
226 | .ant-radio .ant-radio-inner {
227 | border-color: #A6B622 !important;
228 | }
229 | }
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/css/image.scss:
--------------------------------------------------------------------------------
1 | .imageFull {
2 | div[data-rmiz-wrap="visible"] {
3 | display: flex;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/css/imageUploader.scss:
--------------------------------------------------------------------------------
1 | .image {
2 | background-position: center;
3 | width: 15px;
4 | height: 15px;
5 | background-size: cover;
6 | margin-right: 10px;
7 | }
8 |
9 | .imageInfo {
10 | position: absolute;
11 | display: flex;
12 | align-items: center;
13 | width: 100%;
14 | bottom: -30px;
15 | }
16 |
17 | .imageRemoveBtn {
18 | margin-left: 15px;
19 | cursor: pointer;
20 | transition: all .3s;
21 | &:hover {
22 | color: #000;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/css/multipleField.scss:
--------------------------------------------------------------------------------
1 | .item {
2 | display: flex;
3 | padding-bottom: 10px;
4 | background: #fff;
5 | :global {
6 | .ant-row.ant-form-item {
7 | width: 100%;
8 | margin-bottom: 0;
9 | }
10 | .ant-btn.ant-btn-icon-only {
11 | height: 39px;
12 | }
13 | .ant-btn-danger {
14 | width: 40px;
15 | }
16 | .ant-btn {
17 | border: none;
18 | box-shadow: none;
19 | }
20 | .ant-upload-select {
21 | margin-left: 5px;
22 | }
23 | .ant-form-item-label {
24 | display: none;
25 | }
26 | }
27 | }
28 |
29 | .itemHasImage {
30 | padding-bottom: 32px;
31 | }
32 |
33 | .itemContent {
34 | display: flex;
35 | position: relative;
36 | width: 100%;
37 | }
38 |
39 | .itemDescription {
40 | display: flex;
41 | position: relative;
42 | width: 100%;
43 | flex: 1;
44 |
45 | :global {
46 | .ant-row {
47 | width: 100%;
48 | padding-bottom: 10px;
49 | }
50 | }
51 | }
52 |
53 | .dragHandler {
54 | display: flex;
55 | align-items: center;
56 | padding: 0 10px;
57 | }
58 |
--------------------------------------------------------------------------------
/src/css/options.scss:
--------------------------------------------------------------------------------
1 | .optionItem:hover .optionRemoveBtn {
2 | display: inline-block;
3 | }
4 |
5 | .optionReorder {
6 | display: inline-block;
7 | padding: 5px 10px 5px 5px;
8 | cursor: grab;
9 | }
10 |
11 | .optionAddBtn,
12 | .optionRemoveBtn {
13 | border: none !important;
14 | font-weight: 700 !important;
15 | float: right;
16 | line-height: 60px;
17 | }
18 |
19 | .optionRemoveBtn {
20 | display: none;
21 | }
22 |
23 | .optionLabel p {
24 | display: inline;
25 | }
26 |
27 | .correctAnswers p {
28 | display: inline-block;
29 | margin-bottom: 0 !important;
30 | }
31 |
32 | .correctAnswers em {
33 | font-style: normal !important;
34 | }
35 |
36 | .correctAnswers u {
37 | text-decoration: none !important;
38 | }
39 |
40 | .correctAnswers strong {
41 | font-weight: normal !important;
42 | }
43 |
44 | .image {
45 | width: 100px;
46 | height: 100px;
47 | background-size: cover;
48 | background-position: center;
49 | margin-bottom: 10px;
50 | }
51 |
52 | .contentWithImage {
53 | padding: 10px;
54 | display: flex;
55 | align-items: center;
56 | flex-direction: column;
57 | box-shadow: 0 2px 8px 0 rgba(0,0,0,.2);
58 | margin-bottom: 10px;
59 | }
60 |
61 | .inlineOptions div[data-react-beautiful-dnd-draggable] {
62 | display: inline-flex;
63 | margin-right: 10px;
64 | vertical-align: bottom;
65 | }
66 |
--------------------------------------------------------------------------------
/src/css/pdf.scss:
--------------------------------------------------------------------------------
1 | .pdfFooter {
2 | text-align: center;
3 | margin-top: 15px;
4 | }
5 |
6 | .pdfPageButtons {
7 | text-align: center;
8 | margin-bottom: 15px;
9 | }
10 |
11 | .pdf {
12 | :global {
13 | .react-pdf__Page {
14 | text-align: center;
15 | }
16 | .react-pdf__Page__canvas {
17 | display: inline-block !important;
18 | }
19 | }
20 | }
21 |
22 | .pdfSpin {
23 | text-align: center;
24 | }
25 |
26 | .pdfFullView {
27 | top: 0 !important;
28 | padding-bottom: 0 !important;
29 | :global {
30 | .ant-modal-content {
31 | height: 100vh;
32 | border-radius: 0;
33 | }
34 | .ant-modal-body {
35 | padding: 0;
36 | height: calc(100% - 53px);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/css/range.scss:
--------------------------------------------------------------------------------
1 | .rangeSettings {
2 | position: relative;
3 | }
4 |
5 | .maxValueInput {
6 | float: right;
7 | }
8 | .minValueInput {
9 | float: left;
10 | }
11 |
12 | .stepInput {
13 | position: absolute;
14 | left: calc(50% - 66px);
15 | top: -4px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/css/sortableRow.scss:
--------------------------------------------------------------------------------
1 | .sortableRowWrapper {
2 | padding-bottom: 10px;
3 | }
4 |
5 | .sortableRow {
6 | border: 1px dashed #ddd;
7 | background: #fafafa;
8 | display: inline-block;
9 | width: 100%;
10 | position: relative;
11 | display: table;
12 | :global {
13 | .ant-form-item-control {
14 | .ant-checkbox-group,
15 | .ant-radio-group {
16 | width: 100%;
17 | }
18 | .ant-radio-group .ant-radio-wrapper {
19 | margin-right: 0 !important;
20 | }
21 | .ant-checkbox-group .ant-checkbox + span,
22 | .ant-radio-group .ant-radio + span {
23 | padding-left: 0 !important;
24 | }
25 | }
26 | }
27 | }
28 |
29 | .sortableRowContent {
30 | display: table-cell;
31 | width: calc(100% - 50px);
32 | padding: 15px 15px 15px 0;
33 | }
34 |
35 | .sortableRowDragHandle {
36 | display: table-cell;
37 | width: 40px;
38 | text-align: center;
39 | font-size: 15px;
40 | vertical-align: middle;
41 | }
42 |
43 | .sortableRow:hover .toolbarRemoveBtn,
44 | .sortableRow:hover .toolbarCopyBtn,
45 | .sortableRow:hover .toolbarEditBtn {
46 | opacity: 1;
47 | }
48 |
49 | .toolbarEditBtn,
50 | .toolbarCopyBtn,
51 | .toolbarRemoveBtn {
52 | position: absolute;
53 | opacity: 0;
54 | top: -15px;
55 | right: -15px;
56 | height: 30px;
57 | width: 30px;
58 | background: #fff;
59 | border-radius: 50%;
60 | box-shadow: 0 0 8px rgba(0, 0, 0, .2);
61 | border: none;
62 | cursor: pointer;
63 | z-index: 1;
64 | }
65 |
66 | .toolbarEditBtn:focus,
67 | .toolbarCopyBtn:focus,
68 | .toolbarRemoveBtn:focus {
69 | outline: none;
70 | }
71 |
72 | .toolbarEditBtn {
73 | right: 20px;
74 | }
75 |
76 | .toolbarCopyBtn {
77 | right: 55px;
78 | }
79 |
80 | .sortableRowInfo {
81 | max-height: 23px;
82 | overflow: hidden;
83 | * {
84 | margin: 0;
85 | padding: 0;
86 | display: inline;
87 | font-size: 14px !important;
88 | font-family: inherit !important;
89 | color: rgba(0, 0, 0, .65) !important;
90 | background-color: transparent !important;
91 | text-decoration: none !important;
92 | font-weight: 400 !important;
93 | font-style: normal !important;
94 | padding: 0 !important;
95 | pointer-events: none;
96 | }
97 | img, table, br {
98 | display: none;
99 | }
100 | }
101 |
102 | .sortableRowContentInfo {
103 | color: #aaa;
104 | i {
105 | margin-right: 8px;
106 | }
107 | }
108 |
109 | .elementEditor {
110 | :global {
111 | .ant-radio-wrapper, .ant-checkbox-wrapper {
112 | float: left;
113 | line-height: 60px;
114 | }
115 |
116 | .sortable-row-wrapper {
117 | margin-left: 30px;
118 | margin-right: 50px;
119 |
120 | .sortable-row-content {
121 | padding-left: 15px;
122 | }
123 | }
124 |
125 | .option-item {
126 | clear: both;
127 |
128 | .option-reorder {
129 | display: none;
130 | }
131 | }
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/css/toolbar.scss:
--------------------------------------------------------------------------------
1 | .draggableToolbarItemWrapper {
2 | padding: 5px;
3 | list-style: none;
4 | }
5 |
6 | .draggingToolbarItemWrapper {
7 | transform: none !important;
8 | }
9 |
10 | .draggableToolbarItem {
11 | padding: 10px;
12 | border: 1px dashed #ddd;
13 | background: #fff;
14 | position: relative;
15 | }
16 |
17 | .draggableToolbarItem i {
18 | margin-right: 10px;
19 | }
20 |
21 | .toolbar {
22 | float: right;
23 | width: 250px;
24 | background: #fff;
25 | margin-top: -28px;
26 | }
27 |
28 | .toolbar h4 {
29 | margin-top: 0;
30 | text-align: center;
31 | }
32 |
33 | .toolbar ul {
34 | padding: 0;
35 | }
36 |
--------------------------------------------------------------------------------
/src/css/video.scss:
--------------------------------------------------------------------------------
1 | .video input {
2 | margin-top: 5px;
3 | }
4 |
5 | .videoWrapper {
6 | position: relative;
7 | padding-bottom: 56.25%;
8 | padding-top: 25px;
9 | height: 0;
10 | margin-top: 10px;
11 | }
12 |
13 | .videoWrapper.dragging-over:before {
14 | content: '';
15 | width: 100%;
16 | height: 100%;
17 | position: absolute;
18 | top: 0;
19 | left: 0;
20 | }
21 |
--------------------------------------------------------------------------------
/src/hocs/withComponentsContext.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ComponentsContext from '../contexts/ComponentsContext';
3 |
4 | export default WrappedComponent =>
5 | class ComponentsContextWrapper extends Component {
6 | render() {
7 | return
8 | { components => }
9 | ;
10 | }
11 | };
12 |
--------------------------------------------------------------------------------
/src/hocs/withData.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { assocPath, without, dissoc, insert, pathOr, identical, equals, isNil, propEq, find, concat } from 'ramda';
4 | import uniqid from 'uniqid';
5 |
6 | import { reorder } from '../utils/dnd';
7 | import EditModalContext from '../contexts/EditModalContext';
8 | import COMPONENTS_DEFAULTS from '../constants/componentsDefaults';
9 | import ComponentsContext from '../contexts/ComponentsContext';
10 |
11 | export default WrappedComponent =>
12 | class extends Component {
13 | static propTypes = {
14 | data: PropTypes.object
15 | };
16 |
17 | static defaultProps = {
18 | placeholder: 'вариант',
19 | components: []
20 | };
21 |
22 | constructor(props) {
23 | super(props);
24 |
25 | const { items = [], elements = {}, common = {} } = props.data || {};
26 |
27 | this.state = {
28 | items,
29 | elements,
30 | common
31 | };
32 | }
33 |
34 | getSnapshotBeforeUpdate(prevProps, prevState) {
35 | return !equals(prevState, this.state);
36 | }
37 |
38 | componentDidUpdate(prevProps, prevState, snapshot) {
39 | if (snapshot) {
40 | const { onChange } = this.props;
41 | onChange && onChange(this.state.items.length ? this.state : null);
42 | }
43 | }
44 |
45 | reorder = (startIndex, endIndex) => {
46 | this.setState(prev => ({
47 | items: reorder(prev.items, startIndex, endIndex)
48 | }));
49 | }
50 |
51 | add = (type, index) => {
52 | const id = uniqid();
53 | const defaultProps = pathOr({}, ['props'], find(propEq('type', type), this.getComponents()));
54 |
55 | this.setState(prev => ({
56 | items: insert(isNil(index) ? prev.items.length : index, id, prev.items),
57 | elements: assocPath([id], { type, ...defaultProps }, prev.elements),
58 | openedEditModal: id
59 | }));
60 | }
61 |
62 | addCopy = item => {
63 | const id = uniqid();
64 |
65 | this.setState(prev => ({
66 | items: insert(prev.items.length, id, prev.items),
67 | elements: assocPath([id], item, prev.elements)
68 | }));
69 | }
70 |
71 | edit = (id, prop, content) => {
72 | this.setState(prev => ({
73 | elements: assocPath([id, ...prop.split('.').map(i => identical(NaN, +i) ? i : +i)], content, prev.elements)
74 | }));
75 | }
76 |
77 | editAllItem = (id, props) => {
78 | this.setState(prev => ({
79 | elements: assocPath([id], {...prev.elements[id], ...props}, prev.elements)
80 | }));
81 | }
82 |
83 | remove = id => {
84 | this.setState(prev => ({
85 | items: without([id], prev.items),
86 | elements: dissoc(id, prev.elements)
87 | }));
88 | }
89 |
90 | copy = id => {
91 | this.props.onCopy(this.state.elements[id]);
92 | }
93 |
94 | setOpenedEditModal = openedEditModal => this.setState({ openedEditModal });
95 |
96 | getComponents = () => {
97 | const { placeholder, components, getComponents } = this.props;
98 |
99 | const items = concat(COMPONENTS_DEFAULTS(placeholder), components);
100 |
101 | return getComponents ? getComponents(items) : items;
102 | }
103 |
104 | editCommon = common => this.setState({ common });
105 |
106 | render() {
107 | const props = {
108 | items: this.state.items,
109 | elements: this.state.elements,
110 | commonSettings: this.state.common,
111 | reorderItems: this.reorder,
112 | editItem: this.edit,
113 | editAllItem: this.editAllItem,
114 | removeItem: this.remove,
115 | copyItem: this.props.onCopy ? this.copy : undefined,
116 | addItem: this.add,
117 | addCopy: this.addCopy,
118 | editCommonSettings: this.editCommon
119 | };
120 |
121 | return
125 |
126 |
127 |
128 | ;
129 | }
130 | };
131 |
--------------------------------------------------------------------------------
/src/hocs/withElementWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Form, Modal } from 'antd';
4 | import { find, propEq } from 'ramda';
5 | import cx from 'classnames';
6 |
7 | import EditorComponent from '../components/formElements/EditorComponent';
8 | import EditElement from '../components/EditElement';
9 | import styles from '../css/sortableRow.scss';
10 | import editorStyles from '../css/editor.scss';
11 | import EditModalContext from '../contexts/EditModalContext';
12 | import withComponentsContext from './withComponentsContext';
13 |
14 | export const withElementWrapper = WrappedComponent => {
15 | class ElementWrapper extends Component {
16 | static propTypes = {
17 | removeItem: PropTypes.func,
18 | copyItem: PropTypes.func,
19 | editItem: PropTypes.func,
20 | id: PropTypes.oneOfType([
21 | PropTypes.number,
22 | PropTypes.string
23 | ]),
24 | simpleView: PropTypes.bool
25 | };
26 |
27 | static defaultProps = {
28 | simpleView: true
29 | };
30 |
31 | renderComponent = (staticContent, ableCorrect) => {
32 | const { editItem, id } = this.props;
33 |
34 | return editItem(id, prop, content)}
37 | isEditor
38 | disabled={!ableCorrect}
39 | staticContent={staticContent} />;
40 | }
41 |
42 | renderLabel = () => {
43 | const { id, editItem } = this.props;
44 |
45 | return editItem(id, prop, content)}
50 | isEditor />;
51 | }
52 |
53 | onSubmit = values => {
54 | this.props.editAllItem(this.props.id, values);
55 | }
56 |
57 | render() {
58 | const { removeItem, copyItem, id, type, dragHandleProps, placeholder, simpleView, item, components, isEditor } = this.props;
59 | const { staticContent, ableCorrect, renderInfo, name, icon } = find(propEq('type', type), components) || {};
60 |
61 | return
62 |
63 | { !isEditor &&
64 |
65 | { ({ setOpened }) => !!name &&
66 |
69 | }
70 |
71 | { !!copyItem && }
74 |
77 |
78 |
79 |
80 | }
81 |
82 | { simpleView || staticContent ?
83 |
84 |
{ renderInfo &&
{ renderInfo(item) }
}
85 |
{ name }
86 |
: (
87 |
91 | { this.renderComponent(staticContent, ableCorrect) }
92 |
93 | )
94 | }
95 |
96 |
97 |
98 | { ({ opened, setOpened }) =>
99 | setOpened(null)}
104 | style={{ minWidth: 1000 }}
105 | destroyOnClose
106 | footer={null}>
107 | setOpened(null)}
111 | onSubmit={values => {
112 | this.onSubmit(values);
113 | setOpened(null);
114 | }} />
115 |
116 | }
117 |
118 |
;
119 | }
120 | }
121 |
122 | return withComponentsContext(ElementWrapper);
123 | };
124 |
--------------------------------------------------------------------------------
/src/hocs/withFieldWrapper.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Form } from 'antd';
4 |
5 | export default WrappedComponent =>
6 | class extends Component {
7 | static propTypes = {
8 | type: PropTypes.string,
9 | required: PropTypes.bool
10 | };
11 |
12 | onChange = value => {
13 | const { onChange, input } = this.props;
14 |
15 | input.onChange(value);
16 | onChange && onChange(value);
17 | }
18 |
19 | render() {
20 | const { label, required, meta: { invalid, submitFailed, error }} = this.props;
21 | const showError = invalid && submitFailed;
22 |
23 | return : null}
25 | colon={false}
26 | required={required}
27 | validateStatus={showError ? 'error' : null}
28 | help={showError ? error : null}>
29 |
30 | ;
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/src/hocs/withFileUrlContext.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | import FileUrlContext from '../contexts/FileUrlContext';
4 |
5 | export default WrappedComponent =>
6 | class FileUrlContextWrapper extends Component {
7 | render() {
8 | return
9 | { ({ uploadUrl, downloadUrl, uploadImages }) =>
10 |
15 | }
16 | ;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { FormGenerator } from './components/FormGenerator';
2 | export { FormBuilder } from './components/FormBuilder';
3 | export { withElementWrapper } from './hocs/withElementWrapper';
4 |
5 | import 'font-awesome/css/font-awesome.min.css';
6 | import 'draft-js/dist/Draft.css';
7 | import 'react-pdf/dist/Page/AnnotationLayer.css';
8 | import 'react-medium-image-zoom/dist/styles.css';
9 |
--------------------------------------------------------------------------------
/src/ru.js:
--------------------------------------------------------------------------------
1 | tinymce.addI18n('ru',{
2 | "Redo": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c",
3 | "Undo": "\u0412\u0435\u0440\u043d\u0443\u0442\u044c",
4 | "Cut": "\u0412\u044b\u0440\u0435\u0437\u0430\u0442\u044c",
5 | "Copy": "\u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c",
6 | "Paste": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c",
7 | "Select all": "\u0412\u044b\u0434\u0435\u043b\u0438\u0442\u044c \u0432\u0441\u0435",
8 | "New document": "\u041d\u043e\u0432\u044b\u0439 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
9 | "Ok": "\u041e\u043a",
10 | "Cancel": "\u041e\u0442\u043c\u0435\u043d\u0438\u0442\u044c",
11 | "Visual aids": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0442\u0443\u0440\u044b",
12 | "Bold": "\u041f\u043e\u043b\u0443\u0436\u0438\u0440\u043d\u044b\u0439",
13 | "Italic": "\u041a\u0443\u0440\u0441\u0438\u0432",
14 | "Underline": "\u041f\u043e\u0434\u0447\u0435\u0440\u043a\u043d\u0443\u0442\u044b\u0439",
15 | "Strikethrough": "\u0417\u0430\u0447\u0435\u0440\u043a\u043d\u0443\u0442\u044b\u0439",
16 | "Superscript": "\u0412\u0435\u0440\u0445\u043d\u0438\u0439 \u0438\u043d\u0434\u0435\u043a\u0441",
17 | "Subscript": "\u041d\u0438\u0436\u043d\u0438\u0439 \u0438\u043d\u0434\u0435\u043a\u0441",
18 | "Clear formatting": "\u041e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0444\u043e\u0440\u043c\u0430\u0442",
19 | "Align left": "\u041f\u043e \u043b\u0435\u0432\u043e\u043c\u0443 \u043a\u0440\u0430\u044e",
20 | "Align center": "\u041f\u043e \u0446\u0435\u043d\u0442\u0440\u0443",
21 | "Align right": "\u041f\u043e \u043f\u0440\u0430\u0432\u043e\u043c\u0443 \u043a\u0440\u0430\u044e",
22 | "Justify": "\u041f\u043e \u0448\u0438\u0440\u0438\u043d\u0435",
23 | "Bullet list": "\u041c\u0430\u0440\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a",
24 | "Numbered list": "\u041d\u0443\u043c\u0435\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u0441\u043f\u0438\u0441\u043e\u043a",
25 | "Decrease indent": "\u0423\u043c\u0435\u043d\u044c\u0448\u0438\u0442\u044c \u043e\u0442\u0441\u0442\u0443\u043f",
26 | "Increase indent": "\u0423\u0432\u0435\u043b\u0438\u0447\u0438\u0442\u044c \u043e\u0442\u0441\u0442\u0443\u043f",
27 | "Close": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c",
28 | "Formats": "\u0424\u043e\u0440\u043c\u0430\u0442",
29 | "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u0412\u0430\u0448 \u0431\u0440\u0430\u0443\u0437\u0435\u0440 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u044f\u043c\u043e\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0431\u0443\u0444\u0435\u0440\u0443 \u043e\u0431\u043c\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0441\u043e\u0447\u0435\u0442\u0430\u043d\u0438\u044f \u043a\u043b\u0430\u0432\u0438\u0448: Ctrl+X\/C\/V.",
30 | "Headers": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438",
31 | "Header 1": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 1",
32 | "Header 2": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 2",
33 | "Header 3": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 3",
34 | "Header 4": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 4",
35 | "Header 5": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 5",
36 | "Header 6": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 6",
37 | "Headings": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438",
38 | "Heading 1": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 1",
39 | "Heading 2": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 2",
40 | "Heading 3": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 3",
41 | "Heading 4": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 4",
42 | "Heading 5": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 5",
43 | "Heading 6": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a 6",
44 | "Preformatted": "\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435",
45 | "Div": "\u0411\u043b\u043e\u043a",
46 | "Pre": "\u041f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0435 \u0444\u043e\u0440\u043c\u0430\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435",
47 | "Code": "\u041a\u043e\u0434",
48 | "Paragraph": "\u041f\u0430\u0440\u0430\u0433\u0440\u0430\u0444",
49 | "Blockquote": "\u0426\u0438\u0442\u0430\u0442\u0430",
50 | "Inline": "\u0421\u0442\u0440\u043e\u0447\u043d\u044b\u0435",
51 | "Blocks": "\u0411\u043b\u043e\u043a\u0438",
52 | "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u0412\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e \u0442\u0435\u043a\u0441\u0442\u0430, \u043f\u043e\u043a\u0430 \u043d\u0435 \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u0443\u044e \u043e\u043f\u0446\u0438\u044e.",
53 | "Font Family": "\u0428\u0440\u0438\u0444\u0442",
54 | "Font Sizes": "\u0420\u0430\u0437\u043c\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430",
55 | "Class": "\u041a\u043b\u0430\u0441\u0441",
56 | "Browse for an image": "\u0412\u044b\u0431\u043e\u0440 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f",
57 | "OR": "\u0418\u041b\u0418",
58 | "Drop an image here": "\u041f\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 \u0441\u044e\u0434\u0430",
59 | "Upload": "\u0417\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c",
60 | "Block": "\u0411\u043b\u043e\u043a",
61 | "Align": "\u0412\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435",
62 | "Default": "\u0421\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439",
63 | "Circle": "\u041e\u043a\u0440\u0443\u0436\u043d\u043e\u0441\u0442\u0438",
64 | "Disc": "\u041a\u0440\u0443\u0433\u0438",
65 | "Square": "\u041a\u0432\u0430\u0434\u0440\u0430\u0442\u044b",
66 | "Lower Alpha": "\u0421\u0442\u0440\u043e\u0447\u043d\u044b\u0435 \u043b\u0430\u0442\u0438\u043d\u0441\u043a\u0438\u0435 \u0431\u0443\u043a\u0432\u044b",
67 | "Lower Greek": "\u0421\u0442\u0440\u043e\u0447\u043d\u044b\u0435 \u0433\u0440\u0435\u0447\u0435\u0441\u043a\u0438\u0435 \u0431\u0443\u043a\u0432\u044b",
68 | "Lower Roman": "\u0421\u0442\u0440\u043e\u0447\u043d\u044b\u0435 \u0440\u0438\u043c\u0441\u043a\u0438\u0435 \u0446\u0438\u0444\u0440\u044b",
69 | "Upper Alpha": "\u0417\u0430\u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u043b\u0430\u0442\u0438\u043d\u0441\u043a\u0438\u0435 \u0431\u0443\u043a\u0432\u044b",
70 | "Upper Roman": "\u0417\u0430\u0433\u043b\u0430\u0432\u043d\u044b\u0435 \u0440\u0438\u043c\u0441\u043a\u0438\u0435 \u0446\u0438\u0444\u0440\u044b",
71 | "Anchor": "\u042f\u043a\u043e\u0440\u044c",
72 | "Name": "\u0418\u043c\u044f",
73 | "Id": "Id",
74 | "Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "Id \u0434\u043e\u043b\u0436\u0435\u043d \u043d\u0430\u0447\u0438\u043d\u0430\u0442\u044c\u0441\u044f \u0441 \u0431\u0443\u043a\u0432\u044b, \u043f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u0431\u0443\u043a\u0432\u044b, \u0446\u0438\u0444\u0440\u044b, \u0442\u0438\u0440\u0435, \u0442\u043e\u0447\u043a\u0438, \u0434\u0432\u043e\u0435\u0442\u043e\u0447\u0438\u044f \u0438\u043b\u0438 \u043f\u043e\u0434\u0447\u0435\u0440\u043a\u0438\u0432\u0430\u043d\u0438\u044f.",
75 | "You have unsaved changes are you sure you want to navigate away?": "\u0423 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c \u043d\u0435 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u043d\u044b\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f. \u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0443\u0439\u0442\u0438?",
76 | "Restore last draft": "\u0412\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u043f\u0440\u043e\u0435\u043a\u0442\u0430",
77 | "Special character": "\u0421\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0435 \u0441\u0438\u043c\u0432\u043e\u043b\u044b",
78 | "Source code": "\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434",
79 | "Insert\/Edit code sample": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c\/\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u0440\u0438\u043c\u0435\u0440 \u043a\u043e\u0434\u0430",
80 | "Language": "\u042f\u0437\u044b\u043a",
81 | "Code sample": "\u041f\u0440\u0438\u043c\u0435\u0440 \u043a\u043e\u0434\u0430",
82 | "Color": "\u0426\u0432\u0435\u0442",
83 | "R": "R",
84 | "G": "G",
85 | "B": "B",
86 | "Left to right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043b\u0435\u0432\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u043e",
87 | "Right to left": "\u041d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0441\u043f\u0440\u0430\u0432\u0430 \u043d\u0430\u043b\u0435\u0432\u043e",
88 | "Emoticons": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u043c\u0430\u0439\u043b",
89 | "Document properties": "\u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430",
90 | "Title": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
91 | "Keywords": "\u041a\u043b\u044e\u0447\u0438\u0432\u044b\u0435 \u0441\u043b\u043e\u0432\u0430",
92 | "Description": "\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435",
93 | "Robots": "\u0420\u043e\u0431\u043e\u0442\u044b",
94 | "Author": "\u0410\u0432\u0442\u043e\u0440",
95 | "Encoding": "\u041a\u043e\u0434\u0438\u0440\u043e\u0432\u043a\u0430",
96 | "Fullscreen": "\u041f\u043e\u043b\u043d\u043e\u044d\u043a\u0440\u0430\u043d\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c",
97 | "Action": "\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435",
98 | "Shortcut": "\u042f\u0440\u043b\u044b\u043a",
99 | "Help": "\u041f\u043e\u043c\u043e\u0449\u044c",
100 | "Address": "\u0410\u0434\u0440\u0435\u0441",
101 | "Focus to menubar": "\u0424\u043e\u043a\u0443\u0441 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043c\u0435\u043d\u044e",
102 | "Focus to toolbar": "\u0424\u043e\u043a\u0443\u0441 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
103 | "Focus to element path": "\u0424\u043e\u043a\u0443\u0441 \u043d\u0430 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u0435 \u043f\u0443\u0442\u0438",
104 | "Focus to contextual toolbar": "\u0424\u043e\u043a\u0443\u0441 \u043d\u0430 \u043a\u043e\u043d\u0442\u0435\u043a\u0441\u0442\u043d\u043e\u0439 \u043f\u0430\u043d\u0435\u043b\u0438 \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432",
105 | "Insert link (if link plugin activated)": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443 (\u0435\u0441\u043b\u0438 \u043f\u043b\u0430\u0433\u0438\u043d link \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d)",
106 | "Save (if save plugin activated)": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c (\u0435\u0441\u043b\u0438 \u043f\u043b\u0430\u0433\u0438\u043d save \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d)",
107 | "Find (if searchreplace plugin activated)": "\u041d\u0430\u0439\u0442\u0438 (\u0435\u0441\u043b\u0438 \u043f\u043b\u0430\u0433\u0438\u043d searchreplace \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d)",
108 | "Plugins installed ({0}):": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044b\u0435 \u043f\u043b\u0430\u0433\u0438\u043d\u044b ({0}):",
109 | "Premium plugins:": "\u041f\u0440\u0435\u043c\u0438\u0443\u043c \u043f\u043b\u0430\u0433\u0438\u043d\u044b:",
110 | "Learn more...": "\u0423\u0437\u043d\u0430\u0442\u044c \u0431\u043e\u043b\u044c\u0448\u0435...",
111 | "You are using {0}": "\u0412\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0435 {0}",
112 | "Plugins": "\u041f\u043b\u0430\u0433\u0438\u043d\u044b",
113 | "Handy Shortcuts": "\u0413\u043e\u0440\u044f\u0447\u0438\u0435 \u043a\u043b\u0430\u0432\u0438\u0448\u0438",
114 | "Horizontal line": "\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043b\u0438\u043d\u0438\u044f",
115 | "Insert\/edit image": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c\/\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435",
116 | "Image description": "\u041e\u043f\u0438\u0441\u0430\u043d\u0438\u0435 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f",
117 | "Source": "\u0418\u0441\u0442\u043e\u0447\u043d\u0438\u043a",
118 | "Dimensions": "\u0420\u0430\u0437\u043c\u0435\u0440",
119 | "Constrain proportions": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u043f\u0440\u043e\u043f\u043e\u0440\u0446\u0438\u0438",
120 | "General": "\u041e\u0431\u0449\u0435\u0435",
121 | "Advanced": "\u0420\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u044b\u0435",
122 | "Style": "\u0421\u0442\u0438\u043b\u044c",
123 | "Vertical space": "\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b",
124 | "Horizontal space": "\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u044b\u0439 \u0438\u043d\u0442\u0435\u0440\u0432\u0430\u043b",
125 | "Border": "\u0420\u0430\u043c\u043a\u0430",
126 | "Insert image": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435",
127 | "Image": "\u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f",
128 | "Image list": "\u0421\u043f\u0438\u0441\u043e\u043a \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0439",
129 | "Rotate counterclockwise": "\u041f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044c \u043f\u0440\u043e\u0442\u0438\u0432 \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0438",
130 | "Rotate clockwise": "\u041f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u044c \u043f\u043e \u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0441\u0442\u0440\u0435\u043b\u043a\u0435",
131 | "Flip vertically": "\u041e\u0442\u0440\u0430\u0437\u0438\u0442\u044c \u043f\u043e \u0432\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438",
132 | "Flip horizontally": "\u041e\u0442\u0440\u0430\u0437\u0438\u0442\u044c \u043f\u043e \u0433\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u0438",
133 | "Edit image": "\u0420\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435",
134 | "Image options": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f",
135 | "Zoom in": "\u041f\u0440\u0438\u0431\u043b\u0438\u0437\u0438\u0442\u044c",
136 | "Zoom out": "\u041e\u0442\u0434\u0430\u043b\u0438\u0442\u044c",
137 | "Crop": "\u041e\u0431\u0440\u0435\u0437\u0430\u0442\u044c",
138 | "Resize": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0430\u0437\u043c\u0435\u0440",
139 | "Orientation": "\u041e\u0440\u0438\u0435\u043d\u0442\u0430\u0446\u0438\u044f",
140 | "Brightness": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c",
141 | "Sharpen": "\u0427\u0435\u0442\u043a\u043e\u0441\u0442\u044c",
142 | "Contrast": "\u041a\u043e\u043d\u0442\u0440\u0430\u0441\u0442",
143 | "Color levels": "\u0426\u0432\u0435\u0442\u043e\u0432\u044b\u0435 \u0443\u0440\u043e\u0432\u043d\u0438",
144 | "Gamma": "\u0413\u0430\u043c\u043c\u0430",
145 | "Invert": "\u0418\u043d\u0432\u0435\u0440\u0441\u0438\u044f",
146 | "Apply": "\u041f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c",
147 | "Back": "\u041d\u0430\u0437\u0430\u0434",
148 | "Insert date\/time": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0434\u0430\u0442\u0443\/\u0432\u0440\u0435\u043c\u044f",
149 | "Date\/time": "\u0414\u0430\u0442\u0430\/\u0432\u0440\u0435\u043c\u044f",
150 | "Insert link": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443",
151 | "Insert\/edit link": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c\/\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443",
152 | "Text to display": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u043c\u044b\u0439 \u0442\u0435\u043a\u0441\u0442",
153 | "Url": "\u0410\u0434\u0440\u0435\u0441 \u0441\u0441\u044b\u043b\u043a\u0438",
154 | "Target": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443",
155 | "None": "\u041d\u0435\u0442",
156 | "New window": "\u0412 \u043d\u043e\u0432\u043e\u043c \u043e\u043a\u043d\u0435",
157 | "Remove link": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0441\u044b\u043b\u043a\u0443",
158 | "Anchors": "\u042f\u043a\u043e\u0440\u044f",
159 | "Link": "\u0421\u0441\u044b\u043b\u043a\u0430",
160 | "Paste or type a link": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043b\u0438 \u0432\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0441\u0441\u044b\u043b\u043a\u0443",
161 | "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u0412\u0432\u0435\u0434\u0451\u043d\u043d\u044b\u0439 URL \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u044b\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b. \u0412\u044b \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u00abmailto:\u00bb?",
162 | "The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u0412\u0432\u0435\u0434\u0451\u043d\u043d\u044b\u0439 URL \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u043d\u0435\u0448\u043d\u0435\u0439 \u0441\u0441\u044b\u043b\u043a\u043e\u0439. \u0412\u044b \u0436\u0435\u043b\u0430\u0435\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043f\u0440\u0435\u0444\u0438\u043a\u0441 \u00abhttp:\/\/\u00bb?",
163 | "Link list": "\u0421\u043f\u0438\u0441\u043e\u043a \u0441\u0441\u044b\u043b\u043e\u043a",
164 | "Insert video": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432\u0438\u0434\u0435\u043e",
165 | "Insert\/edit video": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c\/\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0438\u0434\u0435\u043e",
166 | "Insert\/edit media": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c\/\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0438\u0434\u0435\u043e",
167 | "Alternative source": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a",
168 | "Poster": "\u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435",
169 | "Paste your embed code below:": "\u0412\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u0432\u0430\u0448 \u043a\u043e\u0434 \u043d\u0438\u0436\u0435:",
170 | "Embed": "\u041a\u043e\u0434 \u0434\u043b\u044f \u0432\u0441\u0442\u0430\u0432\u043a\u0438",
171 | "Media": "\u0412\u0438\u0434\u0435\u043e",
172 | "Nonbreaking space": "\u041d\u0435\u0440\u0430\u0437\u0440\u044b\u0432\u043d\u044b\u0439 \u043f\u0440\u043e\u0431\u0435\u043b",
173 | "Page break": "\u0420\u0430\u0437\u0440\u044b\u0432 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b",
174 | "Paste as text": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043a\u0430\u043a \u0442\u0435\u043a\u0441\u0442",
175 | "Preview": "\u041f\u0440\u0435\u0434\u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440",
176 | "Print": "\u041f\u0435\u0447\u0430\u0442\u044c",
177 | "Save": "\u0421\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c",
178 | "Find": "\u041d\u0430\u0439\u0442\u0438",
179 | "Replace with": "\u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u043d\u0430",
180 | "Replace": "\u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c",
181 | "Replace all": "\u0417\u0430\u043c\u0435\u043d\u0438\u0442\u044c \u0432\u0441\u0435",
182 | "Prev": "\u0412\u0432\u0435\u0440\u0445",
183 | "Next": "\u0412\u043d\u0438\u0437",
184 | "Find and replace": "\u041f\u043e\u0438\u0441\u043a \u0438 \u0437\u0430\u043c\u0435\u043d\u0430",
185 | "Could not find the specified string.": "\u0417\u0430\u0434\u0430\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430",
186 | "Match case": "\u0423\u0447\u0438\u0442\u044b\u0432\u0430\u0442\u044c \u0440\u0435\u0433\u0438\u0441\u0442\u0440",
187 | "Whole words": "\u0421\u043b\u043e\u0432\u043e \u0446\u0435\u043b\u0438\u043a\u043e\u043c",
188 | "Spellcheck": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043f\u0440\u0430\u0432\u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435",
189 | "Ignore": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c",
190 | "Ignore all": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0432\u0441\u0435",
191 | "Finish": "\u0417\u0430\u043a\u043e\u043d\u0447\u0438\u0442\u044c",
192 | "Add to Dictionary": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432 \u0441\u043b\u043e\u0432\u0430\u0440\u044c",
193 | "Insert table": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0442\u0430\u0431\u043b\u0438\u0446\u0443",
194 | "Table properties": "\u0421\u0432\u043e\u0439\u0441\u0442\u0432\u0430 \u0442\u0430\u0431\u043b\u0438\u0446\u044b",
195 | "Delete table": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0442\u0430\u0431\u043b\u0438\u0446\u0443",
196 | "Cell": "\u042f\u0447\u0435\u0439\u043a\u0430",
197 | "Row": "\u0421\u0442\u0440\u043e\u043a\u0430",
198 | "Column": "\u0421\u0442\u043e\u043b\u0431\u0435\u0446",
199 | "Cell properties": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u044f\u0447\u0435\u0439\u043a\u0438",
200 | "Merge cells": "\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u0438\u0442\u044c \u044f\u0447\u0435\u0439\u043a\u0438",
201 | "Split cell": "\u0420\u0430\u0437\u0431\u0438\u0442\u044c \u044f\u0447\u0435\u0439\u043a\u0443",
202 | "Insert row before": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443 \u0441\u0432\u0435\u0440\u0445\u0443",
203 | "Insert row after": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u043f\u0443\u0441\u0442\u0443\u044e \u0441\u0442\u0440\u043e\u043a\u0443 \u0441\u043d\u0438\u0437\u0443",
204 | "Delete row": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443",
205 | "Row properties": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0441\u0442\u0440\u043e\u043a\u0438",
206 | "Cut row": "\u0412\u044b\u0440\u0435\u0437\u0430\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443",
207 | "Copy row": "\u041a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443",
208 | "Paste row before": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443 \u0441\u0432\u0435\u0440\u0445\u0443",
209 | "Paste row after": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0441\u0442\u0440\u043e\u043a\u0443 \u0441\u043d\u0438\u0437\u0443",
210 | "Insert column before": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0442\u043e\u043b\u0431\u0435\u0446 \u0441\u043b\u0435\u0432\u0430",
211 | "Insert column after": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0441\u0442\u043e\u043b\u0431\u0435\u0446 \u0441\u043f\u0440\u0430\u0432\u0430",
212 | "Delete column": "\u0423\u0434\u0430\u043b\u0438\u0442\u044c \u0441\u0442\u043e\u043b\u0431\u0435\u0446",
213 | "Cols": "\u0421\u0442\u043e\u043b\u0431\u0446\u044b",
214 | "Rows": "\u0421\u0442\u0440\u043e\u043a\u0438",
215 | "Width": "\u0428\u0438\u0440\u0438\u043d\u0430",
216 | "Height": "\u0412\u044b\u0441\u043e\u0442\u0430",
217 | "Cell spacing": "\u0412\u043d\u0435\u0448\u043d\u0438\u0439 \u043e\u0442\u0441\u0442\u0443\u043f",
218 | "Cell padding": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0439 \u043e\u0442\u0441\u0442\u0443\u043f",
219 | "Caption": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
220 | "Left": "\u041f\u043e \u043b\u0435\u0432\u043e\u043c\u0443 \u043a\u0440\u0430\u044e",
221 | "Center": "\u041f\u043e \u0446\u0435\u043d\u0442\u0440\u0443",
222 | "Right": "\u041f\u043e \u043f\u0440\u0430\u0432\u043e\u043c\u0443 \u043a\u0440\u0430\u044e",
223 | "Cell type": "\u0422\u0438\u043f \u044f\u0447\u0435\u0439\u043a\u0438",
224 | "Scope": "Scope",
225 | "Alignment": "\u0412\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435",
226 | "H Align": "\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435",
227 | "V Align": "\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u044b\u0440\u0430\u0432\u043d\u0438\u0432\u0430\u043d\u0438\u0435",
228 | "Top": "\u041f\u043e \u0432\u0435\u0440\u0445\u0443",
229 | "Middle": "\u041f\u043e \u0441\u0435\u0440\u0435\u0434\u0438\u043d\u0435",
230 | "Bottom": "\u041f\u043e \u043d\u0438\u0437\u0443",
231 | "Header cell": "\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a",
232 | "Row group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u0441\u0442\u0440\u043e\u043a",
233 | "Column group": "\u0413\u0440\u0443\u043f\u043f\u0430 \u043a\u043e\u043b\u043e\u043d\u043e\u043a",
234 | "Row type": "\u0422\u0438\u043f \u0441\u0442\u0440\u043e\u043a\u0438",
235 | "Header": "\u0428\u0430\u043f\u043a\u0430",
236 | "Body": "\u0422\u0435\u043b\u043e",
237 | "Footer": "\u041d\u0438\u0437",
238 | "Border color": "\u0426\u0432\u0435\u0442 \u0440\u0430\u043c\u043a\u0438",
239 | "Insert template": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0448\u0430\u0431\u043b\u043e\u043d",
240 | "Templates": "\u0428\u0430\u0431\u043b\u043e\u043d\u044b",
241 | "Template": "\u0428\u0430\u0431\u043b\u043e\u043d",
242 | "Text color": "\u0426\u0432\u0435\u0442 \u0442\u0435\u043a\u0441\u0442\u0430",
243 | "Background color": "\u0426\u0432\u0435\u0442 \u0444\u043e\u043d\u0430",
244 | "Custom...": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c\u2026",
245 | "Custom color": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0439 \u0446\u0432\u0435\u0442",
246 | "No color": "\u0411\u0435\u0437 \u0446\u0432\u0435\u0442\u0430",
247 | "Table of Contents": "\u0421\u043e\u0434\u0435\u0440\u0436\u0430\u043d\u0438\u0435",
248 | "Show blocks": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0431\u043b\u043e\u043a\u0438",
249 | "Show invisible characters": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043d\u0435\u0432\u0438\u0434\u0438\u043c\u044b\u0435 \u0441\u0438\u043c\u0432\u043e\u043b\u044b",
250 | "Words: {0}": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u0441\u043b\u043e\u0432: {0}",
251 | "{0} words": "\u0441\u043b\u043e\u0432: {0}",
252 | "File": "\u0424\u0430\u0439\u043b",
253 | "Edit": "\u0418\u0437\u043c\u0435\u043d\u0438\u0442\u044c",
254 | "Insert": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c",
255 | "View": "\u0412\u0438\u0434",
256 | "Format": "\u0424\u043e\u0440\u043c\u0430\u0442",
257 | "Table": "\u0422\u0430\u0431\u043b\u0438\u0446\u0430",
258 | "Tools": "\u0418\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u044b",
259 | "Powered by {0}": "\u041f\u0440\u0438 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u043a\u0435 {0}",
260 | "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u0422\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0435 \u043f\u043e\u043b\u0435. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 ALT-F9 \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u0437\u0432\u0430\u0442\u044c \u043c\u0435\u043d\u044e, ALT-F10 \u043f\u0430\u043d\u0435\u043b\u044c \u0438\u043d\u0441\u0442\u0440\u0443\u043c\u0435\u043d\u0442\u043e\u0432, ALT-0 \u0434\u043b\u044f \u0432\u044b\u0437\u043e\u0432\u0430 \u043f\u043e\u043c\u043e\u0449\u0438.",
261 | "Image...": "Изображение...",
262 | "Link...": "Ссылку...",
263 | "Open link in...": "Открыть ссылку в...",
264 | "Current window": "В текущем окне"
265 | });
266 |
--------------------------------------------------------------------------------
/src/utils/dnd.js:
--------------------------------------------------------------------------------
1 | export const reorder = (list, startIndex, endIndex) => {
2 | const result = Array.from(list);
3 | const [removed] = result.splice(startIndex, 1);
4 | result.splice(endIndex, 0, removed);
5 |
6 | return result;
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/editor.js:
--------------------------------------------------------------------------------
1 | import { EditorState, Modifier, RichUtils } from 'draft-js';
2 |
3 | export const getSelectionRange = () => {
4 | const selection = window.getSelection();
5 | if (selection.rangeCount === 0) return null;
6 |
7 | return selection.getRangeAt(0);
8 | };
9 |
10 | export const getSelectionCoords = (toolbar, selectionRange) => {
11 | const editorBounds = toolbar.closest('.editor-container').getBoundingClientRect();
12 | const rangeBounds = selectionRange.getBoundingClientRect();
13 | const rangeWidth = rangeBounds.right - rangeBounds.left;
14 | const content = toolbar.querySelector('.toolbar-items');
15 | const outOfViewPort = content.offsetWidth / 2 > rangeBounds.left + (rangeBounds.width / 2);
16 |
17 | const offsetTop = rangeBounds.top - editorBounds.top - (content.offsetHeight + 5);
18 | const offsetLeft = ((rangeBounds.left - editorBounds.left) + (rangeWidth / 2)) - (content.offsetWidth / 2);
19 |
20 | return {
21 | arrowPosition: outOfViewPort ? `${rangeBounds.left + (rangeBounds.width / 2)}px` : '50%',
22 | coords: {
23 | left: outOfViewPort ? -editorBounds.left : offsetLeft,
24 | top: offsetTop
25 | }
26 | };
27 | };
28 |
29 | export function getBlockAlignment(block) {
30 | let style = 'left';
31 |
32 | block.findStyleRanges(e => {
33 | if (e.hasStyle('center')) style = 'center';
34 | if (e.hasStyle('right')) style = 'right';
35 | });
36 |
37 | return style;
38 | }
39 |
40 | export function styleWholeSelectedBlocksModifier(editorState, style, removeStyles = []) {
41 | const currentContent = editorState.getCurrentContent();
42 | const selection = editorState.getSelection();
43 | const focusBlock = currentContent.getBlockForKey(selection.getFocusKey());
44 | const anchorBlock = currentContent.getBlockForKey(selection.getAnchorKey());
45 | const selectionIsBackward = selection.getIsBackward();
46 |
47 | let changes = {
48 | anchorOffset: 0,
49 | focusOffset: focusBlock.getLength()
50 | };
51 |
52 | if (selectionIsBackward) {
53 | changes = {
54 | focusOffset: 0,
55 | anchorOffset: anchorBlock.getLength()
56 | };
57 | }
58 |
59 | const selectWholeBlocks = selection.merge(changes);
60 | const modifiedContent = Modifier.applyInlineStyle(currentContent, selectWholeBlocks, style);
61 | const finalContent = removeStyles.reduce((content, style) => {
62 | return Modifier.removeInlineStyle(content, selectWholeBlocks, style);
63 | }, modifiedContent);
64 |
65 | return EditorState.push(editorState, finalContent, 'change-inline-style');
66 | }
67 |
68 | export function getEditorData(editorState) {
69 | return {
70 | contentState: editorState.getCurrentContent(),
71 | inlineStyle: editorState.getCurrentInlineStyle(),
72 | selectionState: editorState.getSelection(),
73 | hasFocus: editorState.getSelection().getHasFocus(),
74 | isCollapsed: editorState.getSelection().isCollapsed(),
75 | startKey: editorState.getSelection().getStartKey(),
76 | startOffset: editorState.getSelection().getStartOffset(),
77 | endKey: editorState.getSelection().getEndKey(),
78 | endOffset: editorState.getSelection().getEndOffset()
79 | };
80 | }
81 |
82 | export function extendSelectionByData(editorState, data) {
83 | const {
84 | selectionState,
85 | startKey,
86 | startOffset,
87 | endKey,
88 | endOffset
89 | } = getEditorData(editorState);
90 | let anchorKey = startKey;
91 | let focusKey = endKey;
92 | let anchorOffset = startOffset;
93 | let focusOffset = endOffset;
94 |
95 | data.forEach(({ blockKey, start, end }, key) => {
96 | if (key === 0) {
97 | anchorKey = blockKey;
98 | anchorOffset = start;
99 | }
100 | if (key === data.length - 1) {
101 | focusKey = blockKey;
102 | focusOffset = end;
103 | }
104 | });
105 | const state = Object.assign({}, anchorKey ? { anchorKey } : {}, {
106 | focusKey,
107 | anchorOffset,
108 | focusOffset,
109 | isBackward: false
110 | });
111 |
112 | const newSelectionState = selectionState.merge(state);
113 | return EditorState.acceptSelection(editorState, newSelectionState);
114 | }
115 |
116 | export function getEntitiesByBlockKey(
117 | editorState,
118 | entityType = null,
119 | blockKey = null
120 | ) {
121 | return getEntities(editorState, entityType).filter(
122 | entity => entity.blockKey === blockKey
123 | );
124 | }
125 |
126 | export function getEntities(
127 | editorState,
128 | entityType = null,
129 | selectedEntityKey = null
130 | ) {
131 | const { contentState } = getEditorData(editorState);
132 | const entities = [];
133 | contentState.getBlocksAsArray().forEach(block => {
134 | let selectedEntity = null;
135 | block.findEntityRanges(
136 | character => {
137 | const entityKey = character.getEntity();
138 | if (entityKey !== null) {
139 | const entity = contentState.getEntity(entityKey);
140 | if (!entityType || (entityType && entity.getType() === entityType)) {
141 | if (
142 | selectedEntityKey === null ||
143 | (selectedEntityKey !== null && entityKey === selectedEntityKey)
144 | ) {
145 | selectedEntity = {
146 | entityKey,
147 | blockKey: block.getKey(),
148 | entity: contentState.getEntity(entityKey)
149 | };
150 | return true;
151 | } else {
152 | return false;
153 | }
154 | }
155 | }
156 | return false;
157 | },
158 | (start, end) => {
159 | entities.push({ ...selectedEntity, start, end });
160 | }
161 | );
162 | });
163 | return entities;
164 | }
165 |
166 | export function findEntityInSelection(editorState, entityType) {
167 | const { startKey, startOffset, endOffset } = getEditorData(editorState);
168 | const entities = getEntitiesByBlockKey(editorState, entityType, startKey);
169 | if (entities.length === 0) return null;
170 |
171 | let selectedEntity = null;
172 | entities.forEach(entity => {
173 | const { blockKey, start, end } = entity;
174 | if (
175 | blockKey === startKey &&
176 | ((startOffset > start && startOffset < end) ||
177 | (endOffset > start && endOffset < end) ||
178 | (startOffset === start && endOffset === end))
179 | ) {
180 | selectedEntity = entity;
181 | }
182 | });
183 |
184 | return selectedEntity;
185 | }
186 |
187 | export function createEntity(editorState, entityType, data = {}) {
188 | const { contentState, selectionState } = getEditorData(editorState);
189 | const contentStateWithEntity = contentState.createEntity(
190 | entityType,
191 | 'MUTABLE',
192 | data
193 | );
194 | const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
195 | const newEditorState = EditorState.set(editorState, {
196 | currentContent: Modifier.applyEntity(
197 | contentStateWithEntity,
198 | selectionState,
199 | entityKey
200 | )
201 | });
202 | return newEditorState;
203 | }
204 |
205 | function getSelectedBlocks(editorState) {
206 | const { contentState, startKey, endKey } = getEditorData(editorState);
207 | const blocks = [];
208 | let block = contentState.getBlockForKey(startKey);
209 | // eslint-disable-next-line no-constant-condition
210 | while (true) {
211 | blocks.push(block);
212 | const blockKey = block.getKey();
213 | if (blockKey === endKey) {
214 | break;
215 | } else {
216 | block = contentState.getBlockAfter(blockKey);
217 | }
218 | }
219 | return blocks;
220 | }
221 |
222 | function getSelectedBlocksByType(editorState, blockType) {
223 | const {
224 | contentState,
225 | startKey,
226 | endKey,
227 | startOffset,
228 | endOffset
229 | } = getEditorData(editorState);
230 | const blocks = [];
231 | getSelectedBlocks(editorState).forEach(block => {
232 | const blockKey = block.getKey();
233 | const blockStartOffset = blockKey === startKey ? startOffset : 0;
234 | const blockEndOffset = blockKey === endKey ? endOffset : block.getLength();
235 | findEntities(
236 | blockType,
237 | block,
238 | (start, end) => {
239 | if (
240 | Math.max(start, blockStartOffset) <= Math.min(end, blockEndOffset)
241 | ) {
242 | const entityKey = block.getEntityAt(start);
243 | const text = block.getText().slice(start, end);
244 | const url = contentState.getEntity(entityKey).getData().url;
245 | blocks.push({ text, url, block, start, end });
246 | }
247 | },
248 | contentState
249 | );
250 | });
251 | return blocks;
252 | }
253 |
254 | export function removeEntity(editorState, entityType) {
255 | const { contentState, selectionState } = getEditorData(editorState);
256 | const blocks = getSelectedBlocksByType(editorState, entityType);
257 | if (blocks.length !== 0) {
258 | let anchorKey;
259 | let focusKey;
260 | let anchorOffset;
261 | let focusOffset;
262 | blocks.forEach(({ block, start, end }, key) => {
263 | const blockKey = block.getKey();
264 | if (key === 0) {
265 | anchorKey = blockKey;
266 | anchorOffset = start;
267 | }
268 | if (key === blocks.length - 1) {
269 | focusKey = blockKey;
270 | focusOffset = end;
271 | }
272 | });
273 | const newContentState = Modifier.applyEntity(
274 | contentState,
275 | selectionState.merge({
276 | anchorKey,
277 | focusKey,
278 | anchorOffset,
279 | focusOffset,
280 | isBackward: false
281 | }),
282 | null
283 | );
284 | return EditorState.set(editorState, {
285 | currentContent: newContentState
286 | });
287 | }
288 | }
289 |
290 | function getEntity(character) {
291 | return character.getEntity();
292 | }
293 |
294 | function entityFilter(character, entityType, contentState) {
295 | const entityKey = getEntity(character);
296 | return (
297 | entityKey !== null &&
298 | contentState.getEntity(entityKey).getType() === entityType
299 | );
300 | }
301 |
302 | export function findEntities(entityType, contentBlock, callback, contentState) {
303 | return contentBlock.findEntityRanges(
304 | character => entityFilter(character, entityType, contentState),
305 | callback
306 | );
307 | }
308 |
309 | export function confirmLink(editorState, url) {
310 | const contentState = editorState.getCurrentContent();
311 | const contentStateWithEntity = contentState.createEntity(
312 | 'LINK',
313 | 'MUTABLE',
314 | { url }
315 | );
316 |
317 | return EditorState.set(editorState, { currentContent: contentStateWithEntity });
318 | }
319 |
320 | export function removeLink(editorState) {
321 | const selection = editorState.getSelection();
322 | if (!selection.isCollapsed()) {
323 | return RichUtils.toggleLink(editorState, selection, null);
324 | } else {
325 | return editorState;
326 | }
327 | }
328 |
--------------------------------------------------------------------------------
/src/utils/files.js:
--------------------------------------------------------------------------------
1 | import { is } from 'ramda';
2 |
3 | export const getUrl = (url, ...attrs) => is(Function, url) ? url(...attrs) : url;
4 |
--------------------------------------------------------------------------------
/src/utils/methods.js:
--------------------------------------------------------------------------------
1 | import { sort } from 'ramda';
2 |
3 | export function shuffle(array) {
4 | return sort(() => Math.random() - 0.5, array);
5 | }
6 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const merge = require('webpack-merge');
2 | const common = require('./webpack/common.config');
3 |
4 | const NODE_ENV = process.env.NODE_ENV || 'dev';
5 |
6 | const config = require(`./webpack/${NODE_ENV}.config`);
7 |
8 | module.exports = merge.smart(config, common);
9 |
--------------------------------------------------------------------------------
/webpack/common.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const CopyWebpackPlugin = require('copy-webpack-plugin');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 |
5 | module.exports = {
6 | entry: './demo/app.js',
7 | module: {
8 | rules: [
9 | {
10 | test: /\.jsx?$/,
11 | loader: 'babel-loader',
12 | exclude: /node_modules/,
13 | options: {
14 | presets: ['@babel/preset-env', '@babel/preset-react'],
15 | plugins: [
16 | '@babel/plugin-proposal-class-properties',
17 | ['import', { 'libraryName': 'antd', 'style': 'css' }]
18 | ],
19 | babelrc: false
20 | }
21 | },
22 | {
23 | test: /\.css$/,
24 | use: ['style-loader', 'css-loader']
25 | },
26 | {
27 | test: /\.s[ac]ss$/,
28 | use: [
29 | (process.env.NODE_ENV === 'production' ? MiniCssExtractPlugin.loader : 'style-loader'),
30 | { loader: 'css-loader', options: {
31 | modules: true,
32 | importLoaders: 2,
33 | modules: {
34 | localIdentName: '[name]__[local]___[hash:base64:5]'
35 | }
36 | }},
37 | 'sass-loader'
38 | ]
39 | },
40 | {
41 | test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/,
42 | loader: 'url-loader',
43 | options: {
44 | limit: 10000,
45 | name: 'assets/fonts/[name].[ext]'
46 | }
47 | },
48 | {
49 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
50 | loader: 'file-loader',
51 | options: {
52 | name: 'assets/fonts/[name].[ext]'
53 | }
54 | },
55 | {
56 | test: /\.(jpe?g|png|gif|ico)$/,
57 | loader: 'url-loader',
58 | options: {
59 | name: 'assets/fonts/[name].[ext]'
60 | }
61 | }
62 | ]
63 | },
64 | resolve: {
65 | extensions: ['.js', '.json', '.jsx', '.css', '.scss']
66 | },
67 | plugins: [
68 | new CopyWebpackPlugin([
69 | {
70 | from: 'demo/index.html',
71 | to: 'index.html'
72 | },
73 | {
74 | from: 'src/ru.js',
75 | to: 'translations'
76 | }
77 | ])
78 | ]
79 | }
80 |
--------------------------------------------------------------------------------
/webpack/dev.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
4 |
5 | module.exports = {
6 | output: {
7 | path: path.resolve(__dirname, '../.tmp'),
8 | filename: 'app.js'
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.jsx?$/,
14 | enforce: 'pre',
15 | exclude: /node_modules/,
16 | loader: 'eslint-loader',
17 | options: {
18 | configFile: './.eslintrc',
19 | fix: true
20 | }
21 | },
22 | ]
23 | },
24 | plugins: [
25 | new webpack.SourceMapDevToolPlugin({
26 | filename: '[file].map'
27 | }),
28 | new webpack.DefinePlugin({
29 | 'process.env': {
30 | 'NODE_ENV': '"development"'
31 | }
32 | }),
33 | new CleanWebpackPlugin(),
34 | new webpack.HotModuleReplacementPlugin()
35 | ],
36 | devServer: {
37 | publicPath: '/',
38 | contentBase: path.resolve(__dirname, '../.tmp'),
39 | watchContentBase: true,
40 | port: 9000,
41 | disableHostCheck: true,
42 | proxy: {
43 | '/api': {
44 | target: 'http://meconsultant.dev.experium.net',
45 | changeOrigin: true,
46 | },
47 | },
48 | }
49 | };
50 |
--------------------------------------------------------------------------------
/webpack/production.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 |
6 | module.exports = {
7 | output: {
8 | path: path.resolve(__dirname, '../dist'),
9 | filename: 'app.js'
10 | },
11 | plugins: [
12 | new CleanWebpackPlugin(),
13 | new webpack.DefinePlugin({
14 | 'process.env': {
15 | 'NODE_ENV': '"production"'
16 | }
17 | }),
18 | new MiniCssExtractPlugin({
19 | filename: '[name].css',
20 | chunkFilename: '[id].css',
21 | })
22 | ]
23 | };
24 |
--------------------------------------------------------------------------------