Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/2.x' into 3.x
Browse files Browse the repository at this point in the history
  • Loading branch information
dimaip committed Aug 30, 2019
2 parents 6136f78 + 15d2163 commit 75e6f01
Show file tree
Hide file tree
Showing 19 changed files with 272 additions and 71 deletions.
13 changes: 8 additions & 5 deletions Classes/Controller/BackendServiceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,15 @@ public function discardAction(array $nodeContextPaths)
$updateNodeInfo = new UpdateNodeInfo();
$updateNodeInfo->setNode($node);
$updateNodeInfo->recursive();

$updateParentNodeInfo = new UpdateNodeInfo();
$updateParentNodeInfo->setNode($node->getParent());

$this->feedbackCollection->add($updateNodeInfo);
$this->feedbackCollection->add($updateParentNodeInfo);

// handle parent node, if needed
$parentNode = $node->getParent();
if ($parentNode instanceof NodeInterface) {
$updateParentNodeInfo = new UpdateNodeInfo();
$updateParentNodeInfo->setNode($parentNode);
$this->feedbackCollection->add($updateParentNodeInfo);
}

// Reload document for content node changes
// (as we can't RenderContentOutOfBand from here, we don't know dom addresses)
Expand Down
2 changes: 1 addition & 1 deletion Migrations/Code/Version20190319094900.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function up()
function (&$configuration) {
foreach ($configuration as &$nodeType) {
if (!isset($nodeType['properties'])) {
return;
continue;
}

foreach ($nodeType['properties'] as &$propertyConfiguration) {
Expand Down
26 changes: 24 additions & 2 deletions Tests/IntegrationTests/Fixtures/1Dimension/createNewNodes.e2e.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import {Selector} from 'testcafe';
import {Selector, RequestLogger} from 'testcafe';
import {ReactSelector} from 'testcafe-react-selectors';
import {beforeEach, subSection, checkPropTypes} from './../../utils.js';
import {Page} from './../../pageModel';

/* global fixture:true */

const changeRequestLogger = RequestLogger(request => request.url.endsWith('/neos/ui-services/change') && request.method === 'post' && request.isAjax);

fixture`Create new nodes`
.beforeEach(beforeEach)
.afterEach(() => checkPropTypes());
.afterEach(() => checkPropTypes())
.requestHooks(changeRequestLogger);

test('Create an Image node from ContentTree', async t => {
await t.switchToIframe('[name="neos-content-main"]');
Expand Down Expand Up @@ -78,6 +81,25 @@ test('Can create content node from inside InlineUI', async t => {
.typeText(Selector('.test-headline h1'), headlineTitle)
.expect(Selector('.neos-contentcollection').withText(headlineTitle).exists).ok('Typed headline text exists');

subSection('Inline validation');
// We have to wait for ajax requests to be triggered, since they are debounced for 0.5s
await t.wait(600);
await changeRequestLogger.clear();
await t
.expect(Selector('.test-headline h1').exists).ok('Validation tooltip appeared')
.click('.test-headline h1')
.pressKey('ctrl+a delete')
.switchToMainWindow()
.wait(600)
.expect(ReactSelector('InlineValidationTooltips').exists).ok('Validation tooltip appeared');
await t
.expect(changeRequestLogger.count(() => true)).eql(0, 'No requests were fired with invalid state')
await t
.switchToIframe('[name="neos-content-main"]')
.typeText(Selector('.test-headline h1'), 'Some text')
.wait(600)
await t.expect(changeRequestLogger.count(() => true)).eql(1, 'Request fired when field became valid')

subSection('Create a link to node');
const linkTargetPage = 'Link target';
await t
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@
h4: true
h5: true
a: true
validation:
'Neos.Neos/Validation/NotEmptyValidator': []
'Neos.Neos/Validation/StringLengthValidator':
minimum: 1
maximum: 255
2 changes: 2 additions & 0 deletions packages/build-essentials/.browserslistrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Browsers that we support
last 2 versions
4 changes: 1 addition & 3 deletions packages/build-essentials/src/postcss.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ const styleVars = styles.generateCssVarsObject(styles.config);

module.exports = {
plugins: [
require('autoprefixer')({
browsers: ['last 2 versions']
}),
require('autoprefixer'),
require('postcss-css-variables')({
variables: Object.assign(styleVars)
}),
Expand Down
2 changes: 1 addition & 1 deletion packages/build-essentials/src/styles/styleConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const config = {
sideBar: ['dropTargetBefore', 'dropTargetAfter'],
wrapperDropdown: ['context'],
unappliedChangesOverlay: ['context'],
nodeToolBar: '2147483647'
nodeToolBar: '2147483646'
},
fontSize: {
base: '14px',
Expand Down
14 changes: 2 additions & 12 deletions packages/neos-ui-ckeditor-bindings/src/createCkEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {$get} from 'plow-js';

import {getGuestFrameWindow} from '@neos-project/neos-ui-guest-frame/src/dom';

export default ({propertyDomNode, propertyName, contextPath, editorOptions, globalRegistry, userPreferences, persistChange}) => {
export default ({propertyDomNode, propertyName, editorOptions, globalRegistry, userPreferences, onChange}) => {
const formattingRulesRegistry = globalRegistry.get('ckEditor').get('formattingRules');
const pluginsRegistry = globalRegistry.get('ckEditor').get('plugins');
const i18nRegistry = globalRegistry.get('i18n');
Expand Down Expand Up @@ -39,15 +39,5 @@ export default ({propertyDomNode, propertyName, contextPath, editorOptions, glob
placeholder ? {neosPlaceholder: placeholder} : {}
));

getGuestFrameWindow().NeosCKEditorApi.createEditor(propertyDomNode, ckEditorConfiguration, propertyName, contents => {
persistChange({
type: 'Neos.Neos.Ui:Property',
subject: contextPath,
payload: {
propertyName,
value: contents,
isInline: true
}
});
});
getGuestFrameWindow().NeosCKEditorApi.createEditor(propertyDomNode, ckEditorConfiguration, propertyName, onChange);
};
12 changes: 2 additions & 10 deletions packages/neos-ui-ckeditor5-bindings/src/ckEditorApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const bootstrap = _editorConfig => {
};

export const createEditor = options => {
const {propertyDomNode, propertyName, contextPath, editorOptions, globalRegistry, userPreferences, persistChange} = options;
const {propertyDomNode, propertyName, editorOptions, globalRegistry, userPreferences, onChange} = options;
const ckEditorConfig = editorConfig.configRegistry.getCkeditorConfig({
editorOptions,
userPreferences,
Expand All @@ -70,15 +70,7 @@ export const createEditor = options => {
editor.neos = options;

editor.model.document.on('change', () => handleUserInteractionCallback());
editor.model.document.on('change:data', debounce(() => persistChange({
type: 'Neos.Neos.Ui:Property',
subject: contextPath,
payload: {
propertyName,
value: cleanupContentBeforeCommit(editor.getData()),
isInline: true
}
}), 500, {maxWait: 5000}));
editor.model.document.on('change:data', debounce(() => onChange(cleanupContentBeforeCommit(editor.getData())), 500, {maxWait: 5000}));
}).catch(e => console.error(e));
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, {PureComponent, Fragment} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {$transform} from 'plow-js';
import {selectors} from '@neos-project/neos-ui-redux-store';
import {neos} from '@neos-project/neos-ui-decorators';
import {Tooltip} from '@neos-project/react-ui-components';
import throttle from 'lodash.throttle';
import style from './style.css';

import {findAllOccurrencesOfNodePropertyInGuestFrame, getAbsolutePositionOfElementInGuestFrame, getGuestFrameWindow, getGuestFrameBody} from '@neos-project/neos-ui-guest-frame/src/dom';

@neos(globalRegistry => ({
nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository')
}))
@connect($transform({
inlineValidationErrors: selectors.CR.Nodes.inlineValidationErrorsSelector
}))
export default class InlineValidationTooltips extends PureComponent {
static propTypes = {
inlineValidationErrors: PropTypes.object
};

iframeWindow = getGuestFrameWindow();

tooltipRefs = new WeakMap();

invalidInlinePropertiesDomNodes = [];

updateTooltipsPosition = () => {
this.invalidInlinePropertiesDomNodes.forEach(domNode => {
const position = getAbsolutePositionOfElementInGuestFrame(domNode);
const tooltipNode = this.tooltipRefs.get(domNode) && this.tooltipRefs.get(domNode).current;
if (tooltipNode && tooltipNode.children.length === 2) {
const [border, tooltip] = tooltipNode.children;

border.style.top = position.top + 'px';
border.style.left = position.left + 'px';
border.style.width = position.width + 'px';
border.style.height = position.height + 'px';

tooltip.style.top = position.top + position.height + 'px';
tooltip.style.left = position.left + 'px';
}
});
}

updateTooltipsPositionDebounced = throttle(this.updateTooltipsPosition, 5);

mutationObserver = new MutationObserver(this.updateTooltipsPositionDebounced);

componentDidMount() {
this.iframeWindow.addEventListener('resize', this.updateTooltipsPositionDebounced);

this.mutationObserver.observe(getGuestFrameBody(), {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
}

componentWillUnmount() {
this.iframeWindow.removeEventListener('resize', this.updateTooltipsPositionDebounced);
this.mutationObserver.disconnect();
}

render() {
const {inlineValidationErrors} = this.props;

return <Fragment>{Object.keys(inlineValidationErrors).map(contextAndPropertyName => {
const validationErrorsForProperty = inlineValidationErrors[contextAndPropertyName];
const [contextPath, propertyName] = contextAndPropertyName.split(' ');
const domNodes = findAllOccurrencesOfNodePropertyInGuestFrame(contextPath, propertyName);
return domNodes.map((domNode, index) => {
this.invalidInlinePropertiesDomNodes.push(domNode);
const position = getAbsolutePositionOfElementInGuestFrame(domNode);
const ref = React.createRef();
this.tooltipRefs.set(domNode, ref);
return <div key={index} ref={ref}>
<div className={style.border} style={{
top: position.top,
left: position.left,
width: position.width,
height: position.height
}}/>
<div className={style.tooltip} style={{
top: position.top + position.height,
left: position.left
}}>
<Tooltip renderInline asError><ul>{validationErrorsForProperty.map((error, index) => <li key={index}>{error}</li>)}</ul></Tooltip>
</div>
</div>;
});
})}</Fragment>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.border {
border: 2px solid var(--colors-Error);
position: absolute;
pointer-events: none;
}

.tooltip {
position: absolute;
}
10 changes: 4 additions & 6 deletions packages/neos-ui-guest-frame/src/InlineUI/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {neos} from '@neos-project/neos-ui-decorators';
import NodeToolbar from './NodeToolbar/index';

import style from './style.css';
import InlineValidationErrors from './InlineValidationErrors/index';

@neos(globalRegistry => ({
nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository')
Expand Down Expand Up @@ -39,10 +40,6 @@ export default class InlineUI extends PureComponent {
const focusedNodeContextPath = focused.contextPath;
const {nodeTypesRegistry, focusedNode, shouldScrollIntoView, requestScrollIntoView, destructiveOperationsAreDisabled, clipboardMode, clipboardNodeContextPath} = this.props;
const isDocument = nodeTypesRegistry.hasRole($get('nodeType', focusedNode), 'document');
// Don't render toolbar for the document nodes
if (isDocument) {
return null;
}
const isCut = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Move';
const isCopied = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Copy';
const canBeDeleted = $get('policy.canRemove', this.props.focusedNode) || false;
Expand All @@ -51,7 +48,7 @@ export default class InlineUI extends PureComponent {

return (
<div className={style.inlineUi} data-__neos__inline-ui="TRUE">
<NodeToolbar
{!isDocument && <NodeToolbar
shouldScrollIntoView={shouldScrollIntoView}
requestScrollIntoView={requestScrollIntoView}
destructiveOperationsAreDisabled={destructiveOperationsAreDisabled}
Expand All @@ -61,7 +58,8 @@ export default class InlineUI extends PureComponent {
canBeEdited={canBeEdited}
visibilityCanBeToggled={visibilityCanBeToggled}
{...focused}
/>
/>}
<InlineValidationErrors />
</div>
);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/neos-ui-guest-frame/src/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export const findAllNodesInGuestFrame = () =>
export const findAllPropertiesInGuestFrame = () =>
findAllInGuestFrame('[data-__neos-property]');

//
// Find all DOM nodes that represent a particular node property in the guest frame
//
export const findAllOccurrencesOfNodePropertyInGuestFrame = (contextPath, propertyName) => findAllInGuestFrame(`[data-__neos-editable-node-contextpath="${contextPath}"][data-__neos-property="${propertyName}"]`);

//
// Find all DOM nodes that represent CR node properties in the guest frame
//
Expand Down Expand Up @@ -208,7 +213,9 @@ export const getAbsolutePositionOfElementInGuestFrame = element => {
top: relativeElementDimensions.top - relativeDocumentDimensions.top,
left: relativeElementDimensions.left - relativeDocumentDimensions.left,
bottom: relativeDocumentDimensions.bottom - relativeElementDimensions.bottom,
right: relativeDocumentDimensions.right - relativeElementDimensions.right
right: relativeDocumentDimensions.right - relativeElementDimensions.right,
width: relativeElementDimensions.width,
height: relativeElementDimensions.height
};
}

Expand Down
25 changes: 24 additions & 1 deletion packages/neos-ui-guest-frame/src/initializePropertyDomNode.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {$get, $contains} from 'plow-js';

import {actions} from '@neos-project/neos-ui-redux-store';
import {validateElement} from '@neos-project/neos-ui-validators';

import {getGuestFrameWindow, closestContextPathInGuestFrame} from './dom';

Expand Down Expand Up @@ -64,7 +65,29 @@ export default ({store, globalRegistry, nodeTypesRegistry, inlineEditorRegistry,
userPreferences,
persistChange: change => store.dispatch(
actions.Changes.persistChanges([change])
)
),
onChange: value => {
const validationResult = validateElement(value, $get(['properties', propertyName], nodeType), globalRegistry.get('validators'));
// Update inline validation errors
store.dispatch(
actions.CR.Nodes.setInlineValidationErrors(contextPath, propertyName, validationResult)
);
// If there are no validation errors, update
if (validationResult === null) {
const change = {
type: 'Neos.Neos.Ui:Property',
subject: contextPath,
payload: {
propertyName,
value,
isInline: true
}
};
store.dispatch(
actions.Changes.persistChanges([change])
);
}
}
});

propertyDomNode.dataset.neosInlineEditorIsInitialized = true;
Expand Down
Loading

0 comments on commit 75e6f01

Please sign in to comment.