Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Select styles from a dropdown #9

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ Neos:
resources:
javascript:
'Breadlesscode.SimpleEditorExtend:UiPlugin':
resource: resource://Breadlesscode.SimpleEditorExtend/Public/UiPlugin/Plugin.js
resource: resource://Breadlesscode.SimpleEditorExtend/Public/UiPlugin/Plugin.js
stylesheets:
'Breadlesscode.SimpleEditorExtend:UiPlugin':
resource: resource://Breadlesscode.SimpleEditorExtend/Public/UiPlugin/Plugin.css
71 changes: 67 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@
[![GitHub stars](https://img.shields.io/github/stars/breadlesscode/neos-simple-editor-extend.svg?style=social&label=Stars)](https://github.com/breadlesscode/neos-simple-editor-extend/stargazers)
[![GitHub watchers](https://img.shields.io/github/watchers/breadlesscode/neos-simple-editor-extend.svg?style=social&label=Watch)](https://github.com/breadlesscode/neos-simple-editor-extend/subscription)

This is a small plugin to simply add some buttons to the Neos CMS CKEditor, without writing any JavaScript code. You only need to compose a YAML-File.
This is a small plugin to simply add some buttons and dropdowns for formatting text to the Neos CMS CKEditor, without writing any JavaScript code.
You only need to compose a YAML-File.

## Installation
Most of the time you have to make small adjustments to a package (e.g., the configuration in Settings.yaml). Because of that, it is important to add the corresponding package to the composer from your theme package. Mostly this is the site package located under Packages/Sites/. To install it correctly go to your theme package (e.g.Packages/Sites/Foo.Bar) and run following command:
Most of the time you have to make small adjustments to a package (e.g., the configuration in `Settings.yaml`).
Because of that, it is important to add the corresponding package to the composer from your theme package.
Mostly this is the site package located under `Packages/Sites/`.
To install it correctly go to your theme package (e.g. `Packages/Sites/Foo.Bar`) and run following command:

```bash
composer require breadlesscode/neos-simple-editor-extend --no-update
```

The --no-update command prevent the automatic update of the dependencies. After the package was added to your theme composer.json, go back to the root of the Neos installation and run composer update. Your desired package is now installed correctly.
The --no-update command prevent the automatic update of the dependencies.
After the package was added to your theme `composer.json`, go back to the root of the Neos installation and run composer update.
Your desired package is now installed correctly.

## Demo

![result demo image](Documentation/preview.gif "Example for the configuration below")

## Example configuration
## Example configuration for applying a single style via a button

```yaml
Neos:
Expand Down Expand Up @@ -65,5 +71,62 @@ Now you can use your new formattings like this:
'Test.Test:MyCustomSpan2': true
```

## Example configuration for applying one or more style via a dropdown

```yaml
Neos:
Neos:
Ui:
frontendConfiguration:
'Breadlesscode.SimpleEditorExtend:Dropdowns':
'Breadlesscode.SimpleEditorExtend:Dropdowns.Colors':
extensionName: 'colorAndSizeDropdown'
icon: 'tint'
tooltip: 'Mark the text in different colors and sizes'
position: 'before strong'
formatting:
attributes:
color:
label: 'Color'
placeholder: 'Choose color'
placeholderIcon: 'tint'
options:
- label: 'None'
icon: 'eraser'
model: ''
class: ''
- label: 'Gelb'
model: 'yellow'
class: 'font-color--primary'
- label: 'Blau'
model: 'blue'
class: 'font-color--secondary'
size:
label: 'Size'
placeholder: 'Choose size'
placeholderIcon: 'arrows-alt-v'
options:
- label: 'None'
icon: 'eraser'
model: ''
class: ''
- label: 'Very small'
model: 'xxxsmall'
class: 'font-size--xxxsmall'
```

Now you can use your new formattings like this:

```yaml
'Neos.NodeTypes.BaseMixins:TextMixin':
properties:
text:
ui:
inline:
editorOptions:
formatting:
'Breadlesscode.SimpleEditorExtend:Dropdowns.Colors': true
```

## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.
4 changes: 3 additions & 1 deletion Resources/Private/UiPlugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"watch": "neos-react-scripts watch"
},
"devDependencies": {
"@neos-project/neos-ui-extensibility": "*"
"@neos-project/neos-ui-extensibility": "*",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0"
},
"neos": {
"buildTargetDirectory": "../../Public/UiPlugin"
Expand Down
13 changes: 6 additions & 7 deletions Resources/Private/UiPlugin/src/CkeditorPluginUtils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import {Plugin} from 'ckeditor5-exports';
import AttributeCommand from '@ckeditor/ckeditor5-basic-styles/src/attributecommand';
import {$add, $get} from 'plow-js';
import ButtonComponent from './ButtonComponent';

const getCkeditorPlugin = function(extensionName, commandName, formatting) {
const attributeName = extensionName + 'Attribute';
Expand All @@ -25,7 +24,7 @@ const getCkeditorPlugin = function(extensionName, commandName, formatting) {
this.editor.commands.add(commandName, new AttributeCommand(this.editor, attributeName));
}
}
}
};

const getCkeditorPluginConfig = function(formattingName, ckeditorPlugin) {
return (ckEditorConfiguration, options) => {
Expand All @@ -41,15 +40,15 @@ const getCkeditorPluginConfig = function(formattingName, ckeditorPlugin) {
}
};

const getRichtextToolbarConfig = function(commandName, formattingName, icon, tooltip) {
const getRichtextToolbarConfig = function(commandName, formattingName, icon, tooltip, component, componentConfiguration) {
return {
commandName: commandName,
isActive: $get(commandName),
isVisible: $get(['formatting', formattingName]),
component: ButtonComponent(commandName),
component: component(commandName, componentConfiguration),
icon: icon,
tooltip: tooltip,
};
}
};

export { getCkeditorPlugin, getCkeditorPluginConfig, getRichtextToolbarConfig};
export {getCkeditorPlugin, getCkeditorPluginConfig, getRichtextToolbarConfig};
85 changes: 85 additions & 0 deletions Resources/Private/UiPlugin/src/Commands/SelectFormatCommand.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {Command} from 'ckeditor5-exports';

export default class SelectFormatCommand extends Command {
attributePrefix = 'textFormat-';

constructor(editor, attributeKeys) {
super(editor);
this.attributeKeys = attributeKeys;
}

execute(options = {}) {
const editor = this.editor;
const model = editor.model;
const document = model.document;
const selection = document.selection;

editor.model.change((writer) => {
for (const attributeSuffix of Object.keys(options)) {
const attributeName = this.attributePrefix + attributeSuffix;
const selectedClass = options[attributeSuffix];
const ranges = model.schema.getValidRanges(selection.getRanges(), attributeName);

if (selection.isCollapsed) {
const position = selection.getFirstPosition();

// When selection is inside text with `highlight` attribute.
if (selection.hasAttribute(attributeName)) {
// Find the full highlighted range.
const isSameHighlight = value => {
return value.item.hasAttribute(attributeName) && value.item.getAttribute(attributeName) === this.value;
};

const highlightStart = position.getLastMatchingPosition(isSameHighlight, {direction: 'backward'});
const highlightEnd = position.getLastMatchingPosition(isSameHighlight);
const highlightRange = writer.createRange(highlightStart, highlightEnd);

// Then depending on current value...
if (!selectedClass || this.value === selectedClass) {
// ...remove attribute when passing highlighter different then current or executing "eraser".
writer.removeAttribute(attributeName, highlightRange);
writer.removeSelectionAttribute(attributeName);
} else {
// ...update `highlight` value.
writer.setAttribute(attributeName, selectedClass, highlightRange);
writer.setSelectionAttribute(attributeName, selectedClass);
}
} else if (selectedClass) {
writer.setSelectionAttribute(attributeName, selectedClass);
}
} else {
for (const range of ranges) {
if (selectedClass) {
writer.setAttribute(attributeName, selectedClass, range);
} else {
writer.removeAttribute(attributeName, range);
}
}
}
}
});
}

refresh() {
const {model} = this.editor;
const {selection} = model.document;

this.value = {};
const attributes = selection.getAttributes();

for (let attribute of attributes) {
if (attribute[0].indexOf(this.attributePrefix) === 0) {
const suffix = attribute[0].substr(this.attributePrefix.length);
this.value[suffix] = attribute[1];
}
}

this.isEnabled = true;
for (let attributeName of this.attributeKeys) {
if (this.isEnabled) {
return;
}
this.isEnabled = this.isEnabled || model.schema.checkAttributeInSelection(selection, this.attributePrefix + attributeName);
}
}
}
12 changes: 12 additions & 0 deletions Resources/Private/UiPlugin/src/Components/SelectFormatButton.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.selectFormatButton__flyout {
--dialog-width: 460px;

background-color: var(--colors-ContrastDarker);
position: fixed;
z-index: var(--zIndex-SecondaryToolbar-LinkIconButtonFlyout);
width: var(--dialog-width);
left: 50%;
margin-left: calc(var(--dialog-width) * -.5);
border: var(--spacing-Half) solid var(--colors-ContrastDarker);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React, {PureComponent} from 'react';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import {$get, $transform} from 'plow-js';
import {selectors} from '@neos-project/neos-ui-redux-store';
import {IconButton, SelectBox} from '@neos-project/react-ui-components';
import {neos} from '@neos-project/neos-ui-decorators';
import {executeCommand} from '@neos-project/neos-ui-ckeditor5-bindings';
import style from './SelectFormatButton.css';

export default (commandName, configuration) => {
@connect($transform({
formattingUnderCursor: selectors.UI.ContentCanvas.formattingUnderCursor
}))
@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n')
}))
class SelectFormatButtonComponent extends PureComponent {
static propTypes = {
i18nRegistry: PropTypes.object,
tooltip: PropTypes.string,
formatOptions: PropTypes.object,
formattingUnderCursor: PropTypes.objectOf(PropTypes.oneOfType([
PropTypes.number,
PropTypes.bool,
PropTypes.string,
PropTypes.object
])),
};

constructor(props) {
super(props);
this.state = {
isOpen: false,
attributes: props.formattingUnderCursor[commandName],
};
}

componentWillReceiveProps = (nextProps) => {
// If the new selection doesn't have any formatting close the dialog
if (!$get(commandName, nextProps.formattingUnderCursor)) {
this.setState({isOpen: false});
} else {
this.setState({
isOpen: this.state.isOpen,
attributes: nextProps.formattingUnderCursor[commandName],
});
}
};

handleClick = () => {
this.setState({isOpen: !this.state.isOpen});
};

handleRemoveFormatClick = () => {
this.setState({
attributes: Object.keys(configuration.formatting.attributes).reduce((map, attribute) => {
map[attribute] = '';
return map;
}, {})
}, this.handleSelectionChange);
};

handleSelectionChange = () => {
executeCommand(commandName, this.state.attributes);
};

handleAttributeChange = (attributeName, value) => {
this.setState({
attributes: {
...this.state.attributes,
[attributeName]: value,
}
}, this.handleSelectionChange);
};

render() {
const {icon, id, tooltip, i18nRegistry} = this.props;
const {isOpen} = this.state;
const {attributes} = configuration.formatting;

return (
<div>
<IconButton
icon={icon}
id={id}
isActive={isOpen}
onClick={this.handleClick}
title={i18nRegistry.translate(tooltip)}
/>
{isOpen ? (
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you creating a group of SelectBoxes instead of a singel SelectBox? Is there a reason for that?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for each Attribute (in the example above color and size). There can be multiple attributes/styles in one styling button.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly

<div className={style.selectFormatButton__flyout}>
{Object.keys(attributes).map((attributeName) => {
const attribute = attributes[attributeName];
return (
<div key={attributeName}>
<label htmlFor={"selectFormat-" + attributeName}>{i18nRegistry.translate(attribute.label)}</label>
<SelectBox id={"selectFormat-" + attributeName}
placeholder={i18nRegistry.translate(attribute.placeholder)}
placeholderIcon={i18nRegistry.translate(attribute.placeholderIcon)}
options={attribute.options}
value={this.state.attributes[attributeName]}
onValueChange={(value) => this.handleAttributeChange(attributeName, value)}
optionValueField="model"/>
</div>
);
})}
<IconButton icon="eraser" hoverStyle="brand"
onClick={this.handleRemoveFormatClick}/>
</div>
) : null}
</div>
)
}
}

return SelectFormatButtonComponent;
}
Loading