Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
dimaip committed Aug 3, 2018
0 parents commit 66aadb4
Show file tree
Hide file tree
Showing 14 changed files with 7,801 additions and 0 deletions.
7 changes: 7 additions & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Neos:
Neos:
Ui:
resources:
javascript:
'Psmb.Footnote:Footnote':
resource: resource://Psmb.Footnote/Public/JavaScript/Footnote/Plugin.js
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
This package provides a footnote plugin for CKeditor5 integraion in Neos CMS.

## Installation

1. Switch to using CKeditor 5
2. `composer require '@psmb/footnote'`
3. Enable footnote button on node properties that should support it, e.g.:

```
'Neos.NodeTypes:TextMixin':
properties:
text:
ui:
inline:
editorOptions:
formatting:
footnote: true
```
14 changes: 14 additions & 0 deletions Resources/Private/Footnotes/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4

[*.{yml,yaml}]
indent_size = 2

[*.{md}]
indent_size = 2
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions Resources/Private/Footnotes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
19 changes: 19 additions & 0 deletions Resources/Private/Footnotes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"description": "Footnote",
"license": "MIT",
"private": true,
"scripts": {
"build": "neos-react-scripts build",
"watch": "neos-react-scripts watch"
},
"devDependencies": {
"@neos-project/neos-ui-extensibility": "^1.3"
},
"neos": {
"buildTargetDirectory": "../../Public/JavaScript/Footnote"
},
"dependencies": {
"@ckeditor/ckeditor5-utils": "^10.2.1",
"lodash.upperfirst": "^4.3.1"
}
}
102 changes: 102 additions & 0 deletions Resources/Private/Footnotes/src/FootnoteButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, {PureComponent, Fragment} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {$get, $transform} from 'plow-js';

import {IconButton, TextArea} from '@neos-project/react-ui-components';
import {neos} from '@neos-project/neos-ui-decorators';
import {executeCommand} from '@neos-project/neos-ui-ckeditor5-bindings';

import {selectors} from '@neos-project/neos-ui-redux-store';

import style from './style.css';

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

state = {
isOpen: false
};

componentWillReceiveProps(nextProps) {
// if new selection doesn't have a footnote, close the footnote dialog
if (!$get('footnote', nextProps.formattingUnderCursor)) {
this.setState({isOpen: false});
}
}

handleFootnoteButtonClick = () => {
if (this.isOpen()) {
if ($get('footnote', this.props.formattingUnderCursor) !== undefined) {
executeCommand('footnote');
}
this.setState({isOpen: false});
} else {
this.setState({isOpen: true});
}
}

render() {
const {i18nRegistry, formattingUnderCursor, inlineEditorOptions } = this.props;

return (
<div>
<IconButton
title={this.getFootnote() ? `${i18nRegistry.translate('Psmb.Footnote:Main:removeFootnote', 'Remove footnote')}` : `${i18nRegistry.translate('Psmb.Footnote:Main:insertFootnote', 'Insert footnote')}`}
isActive={this.isOpen()}
icon={this.getFootnote() ? 'ban' : 'asterisk'}
onClick={this.handleFootnoteButtonClick}
/>
{this.isOpen() ? <FootnoteTextField footnoteValue={this.getFootnote()} formattingUnderCursor={formattingUnderCursor} inlineEditorOptions={inlineEditorOptions} /> : null}
</div>
);
}

isOpen() {
return Boolean(this.state.isOpen || this.getFootnote());
}

getFootnote() {
return $get('footnote', this.props.formattingUnderCursor) || '';
}
}

@neos(globalRegistry => ({
i18nRegistry: globalRegistry.get('i18n')
}))
class FootnoteTextField extends PureComponent {
static propTypes = {
i18nRegistry: PropTypes.object,
footnoteValue: PropTypes.string,
inlineEditorOptions: PropTypes.object
};

render() {
return (
<div className={style.flyout}>
<TextArea
value={this.props.footnoteValue}
placeholder={this.props.i18nRegistry.translate('Psmb.Footnote:Main:placeholder', 'Enter footnote text')}
onChange={value => {
executeCommand('footnote', value, false);
}}
/>
</div>
);
}
}
113 changes: 113 additions & 0 deletions Resources/Private/Footnotes/src/footnotePlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
import {Command, Plugin, UpcastConverters, DowncastConverters, ModelRange as Range, ModelPosition as Position} from 'ckeditor5-exports';
const {downcastAttributeToElement} = DowncastConverters;
const {upcastElementToAttribute} = UpcastConverters;

const FOOTNOTE = 'footnote';

function findFootnote(position, value) {
return new Range(_findBound(position, value, true), _findBound(position, value, false));
}

function _findBound(position, value, lookBack) {
let node = position.textNode || (lookBack ? position.nodeBefore : position.nodeAfter);

let lastNode = null;

while (node && node.getAttribute(FOOTNOTE) === value) {
lastNode = node;
node = lookBack ? node.previousSibling : node.nextSibling;
}

return lastNode ? Position.createAt(lastNode, lookBack ? 'before' : 'after') : position;
}

class FootnoteCommand extends Command {
constructor(editor, attributeKey) {
super(editor);

this.attributeKey = attributeKey;
}

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

this.value = doc.selection.getAttribute(this.attributeKey);
this.isEnabled = model.schema.checkAttributeInSelection(doc.selection, this.attributeKey);
}

execute(value) {
const model = this.editor.model;
const doc = model.document;
const selection = doc.selection;
const toggleMode = value === undefined;
value = toggleMode ? !this.value : value;

model.change(writer => {
if (toggleMode && !value) {
const rangesToUnset = selection.isCollapsed ?
[findFootnote(selection.getFirstPosition(), selection.getAttribute(FOOTNOTE))] : selection.getRanges();
for (const range of rangesToUnset) {
writer.removeAttribute(this.attributeKey, range);
}
} else if (selection.isCollapsed) {
const position = selection.getFirstPosition();

if (selection.hasAttribute(FOOTNOTE)) {
const footnoteRange = findFootnote(selection.getFirstPosition(), selection.getAttribute(FOOTNOTE));
if (value === false) {
writer.removeAttribute(this.attributeKey, footnoteRange);
} else {
writer.setAttribute(this.attributeKey, value, footnoteRange);
writer.setSelection(footnoteRange);
}
} else if (value !== '') {
const attributes = toMap(selection.getAttributes());
attributes.set(this.attributeKey, value);
const node = writer.createText(value, attributes);
writer.insert(node, position);
writer.setSelection(Range.createOn(node));
}
} else {
const ranges = model.schema.getValidRanges(selection.getRanges(), this.attributeKey);

for (const range of ranges) {
if (value === false) {
writer.removeAttribute(this.attributeKey, range);
} else {
writer.setAttribute(this.attributeKey, value, range);
}
}
}
});
}
}

export default class Footnote extends Plugin {
static get pluginName() {
return 'Footnote';
}
init() {
const editor = this.editor;
editor.model.schema.extend('$text', {allowAttributes: FOOTNOTE});
editor.conversion.for('downcast').add(downcastAttributeToElement({
model: FOOTNOTE,
view: (footnote, writer) => writer.createAttributeElement('span', {'data-footnote': footnote})
}));
editor.conversion.for('upcast')
.add(upcastElementToAttribute({
view: {
name: 'span',
attributes: {
'data-footnote': true
}
},
model: {
key: FOOTNOTE,
value: viewElement => viewElement.getAttribute('data-footnote')
}
}));
editor.commands.add(FOOTNOTE, new FootnoteCommand(this.editor, FOOTNOTE));
}
}
1 change: 1 addition & 0 deletions Resources/Private/Footnotes/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('./manifest');
23 changes: 23 additions & 0 deletions Resources/Private/Footnotes/src/manifest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import manifest from '@neos-project/neos-ui-extensibility';
import FootnotePlugin from './footnotePlugin';
import FootnoteButton from './FootnoteButton';
import {$add} from 'plow-js'

const addPlugin = (Plugin, isEnabled) => (ckEditorConfiguration, options) => {
if (!isEnabled || isEnabled(options.editorOptions, options)) {
ckEditorConfiguration.plugins = ckEditorConfiguration.plugins || [];
return $add('plugins', Plugin, ckEditorConfiguration);
}
return ckEditorConfiguration;
};

manifest('Psmb.Footnote:Footnote', {}, globalRegistry => {
const richtextToolbar = globalRegistry.get('ckEditor5').get('richtextToolbar');
richtextToolbar.set('footnote', {
component: FootnoteButton,
isVisible: $get('formatting.footnote')
}, 'before strong');

const config = globalRegistry.get('ckEditor5').get('config');
config.set('footnote', addPlugin(FootnotePlugin));
});
7 changes: 7 additions & 0 deletions Resources/Private/Footnotes/src/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.flyout {
background-color: var(--colors-ContrastDarker);
position: fixed;
z-index: var(--zIndex-SecondaryToolbar-LinkIconButtonFlyout);
width: 460px;
border: var(--spacing-Half) solid var(--colors-ContrastDarker);
}
Loading

0 comments on commit 66aadb4

Please sign in to comment.