diff --git a/src/config/RichTextEditor/LinkEntity.jsx b/src/config/RichTextEditor/LinkEntity.jsx
new file mode 100644
index 0000000000..2b3c5d8438
--- /dev/null
+++ b/src/config/RichTextEditor/LinkEntity.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import UniversalLink from '@plone/volto/components/manage/UniversalLink/UniversalLink';
+
+const LinkEntity = connect((state) => ({
+ token: state.userSession.token,
+}))((props) => {
+ const { token, url, target, targetUrl, download, children, dataElement } =
+ props;
+ const to = token ? url : targetUrl || url;
+
+ return (
+
+ {children}
+
+ );
+});
+
+export default LinkEntity;
diff --git a/src/config/RichTextEditor/Plugins/AnchorPlugin/components/Link/index.jsx b/src/config/RichTextEditor/Plugins/AnchorPlugin/components/Link/index.jsx
new file mode 100644
index 0000000000..21f908ab60
--- /dev/null
+++ b/src/config/RichTextEditor/Plugins/AnchorPlugin/components/Link/index.jsx
@@ -0,0 +1,43 @@
+/**
+ * Questo รจ il link quando viene mostrato dentro l'editor nelle viste di edit
+ * Customizzato
+ * - aggiunto data-element
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { isInternalURL, flattenToAppURL } from '@plone/volto/helpers';
+
+const propTypes = {
+ className: PropTypes.string,
+ children: PropTypes.node.isRequired,
+ entityKey: PropTypes.string,
+ getEditorState: PropTypes.func.isRequired,
+ target: PropTypes.string,
+};
+
+const Link = ({ children, className, entityKey, getEditorState, target }) => {
+ const entity = getEditorState().getCurrentContent().getEntity(entityKey);
+ const entityData = entity ? entity.get('data') : undefined;
+ const href = (entityData && entityData.url) || undefined;
+
+ return (
+
+ {children}
+
+ );
+};
+
+Link.propTypes = propTypes;
+Link.defaultProps = {
+ className: null,
+ entityKey: null,
+ target: null,
+};
+export default Link;
diff --git a/src/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/AddLinkForm.jsx b/src/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/AddLinkForm.jsx
new file mode 100644
index 0000000000..c3c5538bc7
--- /dev/null
+++ b/src/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/AddLinkForm.jsx
@@ -0,0 +1,346 @@
+/**
+ * Add link form.
+ * @module components/manage/AnchorPlugin/components/LinkButton/AddLinkForm
+ * Customizzato
+ * - Aggiunta gestione data-element
+ * - Aggiunte opzioni per la select del data-element
+ * - Modificate icone ed elementi per stilizzare il tooltip del link
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { compose } from 'redux';
+
+import cx from 'classnames';
+import {
+ addAppURL,
+ isInternalURL,
+ flattenToAppURL,
+ URLUtils,
+} from '@plone/volto/helpers';
+
+import { doesNodeContainClick } from 'semantic-ui-react/dist/commonjs/lib';
+import { Input, Form, Button } from 'semantic-ui-react';
+import { defineMessages, injectIntl } from 'react-intl';
+
+import clearSVG from '@plone/volto/icons/clear.svg';
+import navTreeSVG from '@plone/volto/icons/nav.svg';
+import aheadSVG from '@plone/volto/icons/ahead.svg';
+
+import withObjectBrowser from '@plone/volto/components/manage/Sidebar/ObjectBrowser';
+import { withRouter } from 'react-router';
+
+import { Icon, SelectWidget } from '@plone/volto/components';
+
+const messages = defineMessages({
+ placeholder: {
+ id: 'Enter URL or select an item',
+ defaultMessage: 'Enter URL or select an item',
+ },
+ placeholderLighthouse: {
+ id: 'Select a data-element ID',
+ defaultMessage: 'Seleziona un data-element ID', // TO DO: da sistemare quando si risolve il bug su i18n
+ },
+});
+
+/**
+ * Add link form class.
+ * @class AddLinkForm
+ * @extends Component
+ */
+class AddLinkForm extends Component {
+ static propTypes = {
+ onChangeValue: PropTypes.func.isRequired,
+ onClear: PropTypes.func.isRequired,
+ onOverrideContent: PropTypes.func.isRequired,
+ theme: PropTypes.objectOf(PropTypes.any).isRequired,
+ openObjectBrowser: PropTypes.func.isRequired,
+ };
+
+ /**
+ * Constructor
+ * @method constructor
+ * @param {Object} props Component properties
+ * @constructs AddLinkForm
+ */
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ dataElement: props.data.dataElement,
+ value: isInternalURL(props.data.url)
+ ? flattenToAppURL(props.data.url)
+ : props.data.url || '',
+ isInvalid: false,
+ };
+ this.onRef = this.onRef.bind(this);
+ this.onChange = this.onChange.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+
+ // Options to data-element select
+ this.selectOptions = [
+ ['appointment-booking', 'appointment-booking'],
+ ['faq', 'faq'],
+ ['report-inefficency', 'report-inefficency'],
+ ['accessibility-link', 'accessibility-link'],
+ ['privacy-policy-link', 'privacy-policy-link'],
+ ];
+ }
+
+ /**
+ * Component did mount
+ * @method componentDidMount
+ * @returns {undefined}
+ */
+ componentDidMount() {
+ setTimeout(() => this.input.focus(), 50);
+ document.addEventListener('mousedown', this.handleClickOutside, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleClickOutside, false);
+ }
+
+ handleClickOutside = (e) => {
+ if (
+ this.linkFormContainer.current &&
+ doesNodeContainClick(this.linkFormContainer.current, e)
+ )
+ return;
+ if (this.linkFormContainer.current && this.props.isObjectBrowserOpen)
+ return;
+ this.onClose();
+ };
+
+ /**
+ * Ref handler
+ * @method onRef
+ * @param {Object} node Node
+ * @returns {undefined}
+ */
+ onRef(node) {
+ this.input = node;
+ }
+
+ linkFormContainer = React.createRef();
+
+ /**
+ * Change handler
+ * @method onChange
+ * @param {Object} value Value
+ * @returns {undefined}
+ */
+ onChange(value, clear) {
+ let nextState = { value };
+ if (!clear) {
+ if (
+ this.state.isInvalid &&
+ URLUtils.isUrl(URLUtils.normalizeUrl(value))
+ ) {
+ nextState.isInvalid = false;
+ }
+
+ if (isInternalURL(value)) {
+ nextState = { value: flattenToAppURL(value) };
+ }
+ }
+ this.setState(nextState);
+
+ if (clear) this.props.onClear();
+ }
+
+ /**
+ * Select item handler
+ * @method onSelectItem
+ * @param {string} e event
+ * @param {string} url Url
+ * @returns {undefined}
+ */
+ onSelectItem = (e, url) => {
+ e.preventDefault();
+ this.setState({
+ value: url,
+ isInvalid: false,
+ });
+ this.props.onChangeValue(addAppURL(url));
+ };
+
+ /**
+ * Clear handler
+ * @method clear
+ * @param {Object} value Value
+ * @returns {undefined}
+ */
+ clear() {
+ const nextState = { value: '' };
+ this.setState(nextState);
+
+ this.props.onClear();
+ }
+
+ /**
+ * Close handler
+ * @method onClose
+ * @returns {undefined}
+ */
+ onClose = () => this.props.onOverrideContent(undefined);
+
+ /**
+ * Keydown handler
+ * @method onKeyDown
+ * @param {Object} e Event object
+ * @returns {undefined}
+ */
+ onKeyDown(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ e.stopPropagation();
+ this.onSubmit();
+ } else if (e.key === 'Escape') {
+ e.preventDefault();
+ this.onClose();
+ }
+ }
+
+ /**
+ * Submit handler
+ * @method onSubmit
+ * @returns {undefined}
+ */
+ onSubmit() {
+ let { value: url, dataElement } = this.state;
+
+ const checkedURL = URLUtils.checkAndNormalizeUrl(url);
+ url = checkedURL.url;
+ if (!checkedURL.isValid) {
+ this.setState({ isInvalid: true });
+ return;
+ }
+
+ const editorStateUrl = isInternalURL(url) ? addAppURL(url) : url;
+
+ this.props.onChangeValue(editorStateUrl, dataElement);
+ this.onClose();
+ }
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const { value, isInvalid, dataElement } = this.state;
+ const className = isInvalid
+ ? cx(
+ 'ui input editor-link',
+ 'input-anchorlink-theme',
+ 'input-anchorlink-theme-Invalid',
+ )
+ : cx('ui input editor-link', 'input-anchorlink-theme');
+
+ return (
+
+
+
+ {/* INPUT LINK */}
+
+ this.onChange(target.value)}
+ placeholder={this.props.intl.formatMessage(
+ messages.placeholder,
+ )}
+ onKeyDown={this.onKeyDown}
+ ref={this.onRef}
+ />
+ {value.length > 0 ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* DATA-ELEMENT SELECT */}
+
+ {
+ this.setState({ dataElement: value });
+ }}
+ choices={this.selectOptions}
+ noValueOption={false}
+ wrapped={false}
+ />
+
+
+
+ {/* BUTTON SUBMIT */}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default compose(injectIntl, withRouter, withObjectBrowser)(AddLinkForm);
diff --git a/src/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/index.js b/src/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/index.js
new file mode 100644
index 0000000000..d416a4d945
--- /dev/null
+++ b/src/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/index.js
@@ -0,0 +1,150 @@
+/**
+ * Customizzato:
+ * - Aggiunta gestione data-element
+ * - Importato AddLinkForm file customizzato
+ */
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import cx from 'classnames';
+import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
+import EditorUtils from '@plone/volto/components/manage/AnchorPlugin/utils/EditorUtils';
+import Icon from '@plone/volto/components/theme/Icon/Icon';
+
+import AddLinkForm from 'design-comuni-plone-theme/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton/AddLinkForm';
+
+import linkSVG from '@plone/volto/icons/link.svg';
+import unlinkSVG from '@plone/volto/icons/unlink.svg';
+
+/**
+ * Add link form class.
+ * @class LinkButton
+ * @extends Component
+ */
+class LinkButton extends Component {
+ static propTypes = {
+ placeholder: PropTypes.string,
+ theme: PropTypes.shape({}).isRequired,
+ ownTheme: PropTypes.shape({}).isRequired,
+ onRemoveLinkAtSelection: PropTypes.func.isRequired,
+ onOverrideContent: PropTypes.func.isRequired,
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.DraftEditorUtils = props.draftJsPluginsUtils.default;
+ this.EditorUtils = EditorUtils(props);
+ }
+
+ static defaultProps = {
+ placeholder: '',
+ };
+
+ onMouseDown = (event) => {
+ event.preventDefault();
+ };
+
+ onAddLinkClick = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const { ownTheme, placeholder, onOverrideContent } = this.props;
+ const data =
+ this.EditorUtils.getCurrentEntity(
+ this.props.getEditorState(),
+ )?.getData() ?? {};
+ const link = data.url || '';
+ const dataElement = data.dataElement || data['data-element'] || '';
+
+ const content = (props) => (
+ {}}
+ onClear={() => {
+ this.props.setEditorState(
+ this.DraftEditorUtils.removeLinkAtSelection(
+ this.props.getEditorState(),
+ ),
+ );
+ }}
+ onChangeValue={(url, dataElement) => {
+ this.props.setEditorState(
+ createLinkAtSelection(
+ this.props.draftJs,
+ this.props.getEditorState(),
+ url,
+ dataElement,
+ ),
+ );
+ }}
+ />
+ );
+ onOverrideContent(content);
+ };
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const { theme } = this.props;
+ const hasLinkSelected = this.EditorUtils.hasEntity(
+ this.props.getEditorState(),
+ 'LINK',
+ );
+ const className = hasLinkSelected
+ ? cx(theme.button, theme.active)
+ : theme.button;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default injectLazyLibs(['draftJs', 'draftJsPluginsUtils'])(LinkButton);
+
+function createLinkAtSelection(draftJs, editorState, url, dataElement) {
+ const contentState = editorState
+ .getCurrentContent()
+ .createEntity('LINK', 'MUTABLE', { url, dataElement });
+ const entityKey = contentState.getLastCreatedEntityKey();
+ const withLink = draftJs.RichUtils.toggleLink(
+ editorState,
+ editorState.getSelection(),
+ entityKey,
+ );
+ return draftJs.EditorState.forceSelection(
+ withLink,
+ editorState.getSelection(),
+ );
+}
diff --git a/src/config/RichTextEditor/Plugins/AnchorPlugin/index.js b/src/config/RichTextEditor/Plugins/AnchorPlugin/index.js
new file mode 100644
index 0000000000..0d9ac5b3d2
--- /dev/null
+++ b/src/config/RichTextEditor/Plugins/AnchorPlugin/index.js
@@ -0,0 +1,88 @@
+/**
+ * Customizzato
+ * - Cambiato import dei link per poter customizzare i file
+ */
+import decorateComponentWithProps from 'decorate-component-with-props';
+import linkStrategy, {
+ matchesEntityType,
+} from '@plone/volto/components/manage/AnchorPlugin/linkStrategy';
+
+import DefaultLink from 'design-comuni-plone-theme/config/RichTextEditor/Plugins/AnchorPlugin/components/Link';
+import LinkButton from 'design-comuni-plone-theme/config/RichTextEditor/Plugins/AnchorPlugin/components/LinkButton';
+
+function unboundRemoveEntity(editorState) {
+ const contentState = editorState.getCurrentContent();
+ const selectionState = editorState.getSelection();
+ const startKey = selectionState.getStartKey();
+ const contentBlock = contentState.getBlockForKey(startKey);
+ const startOffset = selectionState.getStartOffset();
+ const entity = contentBlock.getEntityAt(startOffset);
+
+ if (!entity) {
+ return editorState;
+ }
+
+ let entitySelection = null;
+
+ contentBlock.findEntityRanges(
+ (character) => character.getEntity() === entity,
+ (start, end) => {
+ entitySelection = selectionState.merge({
+ anchorOffset: start,
+ focusOffset: end,
+ isBackward: false,
+ });
+ },
+ );
+
+ const newContentState = this.Modifier.applyEntity(
+ contentState,
+ entitySelection,
+ null,
+ );
+
+ const newEditorState = this.EditorState.push(
+ editorState,
+ newContentState,
+ 'apply-entity',
+ );
+
+ return newEditorState;
+}
+
+export default (config = {}) => {
+ // ToDo: Get rif of the remainings of having the original CSS modules
+ const defaultTheme = {};
+
+ const {
+ theme = defaultTheme,
+ placeholder,
+ Link,
+ linkTarget,
+ libraries,
+ } = config;
+
+ const removeEntity = unboundRemoveEntity.bind(libraries);
+
+ return {
+ decorators: [
+ {
+ strategy: linkStrategy,
+ matchesEntityType,
+ component:
+ Link ||
+ decorateComponentWithProps(DefaultLink, {
+ className: 'link-anchorlink-theme',
+ target: linkTarget,
+ }),
+ },
+ ],
+
+ LinkButton: decorateComponentWithProps(LinkButton, {
+ ownTheme: theme,
+ placeholder,
+ onRemoveLinkAtSelection: (setEditorState, getEditorState) =>
+ setEditorState(removeEntity(getEditorState())),
+ }),
+ };
+};
diff --git a/src/config/RichTextEditor/config.js b/src/config/RichTextEditor/config.js
index c3ac60e905..6d0b7e673a 100644
--- a/src/config/RichTextEditor/config.js
+++ b/src/config/RichTextEditor/config.js
@@ -1,5 +1,4 @@
import React from 'react';
-
import Styles from '@plone/volto/config/RichTextEditor/Styles';
import ToHTMLRenderers from '@plone/volto/config/RichTextEditor/ToHTML';
@@ -15,6 +14,8 @@ import CalloutsButton from 'design-comuni-plone-theme/config/RichTextEditor/Tool
import ButtonsButton from 'design-comuni-plone-theme/config/RichTextEditor/ToolbarButtons/ButtonsButton';
import TextSizeButton from 'design-comuni-plone-theme/config/RichTextEditor/ToolbarButtons/TextSizeButton';
+import LinkEntity from 'design-comuni-plone-theme/config/RichTextEditor/LinkEntity';
+
const ItaliaRichTextEditorPlugins = (props) => [];
const ItaliaRichTextEditorInlineToolbarButtons = (props, plugins) => {
const linkPlugin = plugins.filter((p) => p.LinkButton != null)[0];
@@ -214,6 +215,16 @@ export default function applyConfig(config) {
// TODO: rimuovere questa customizzazione quando sistemano https://github.com/plone/volto/issues/1601
config.settings.richtextViewSettings.ToHTMLRenderers = {
...config.settings.richtextViewSettings.ToHTMLRenderers,
+ entities: {
+ ...config.settings.richtextViewSettings.ToHTMLRenderers.entities,
+ LINK: (children, props, other) => {
+ return (
+
+ {children}
+
+ );
+ },
+ },
blocks: {
...ToHTMLRenderers.blocks,
...ItaliaBlocksHtmlRenderers,
diff --git a/src/config/Widgets/widgets.js b/src/config/Widgets/widgets.js
index c71357d63a..762ba96e01 100644
--- a/src/config/Widgets/widgets.js
+++ b/src/config/Widgets/widgets.js
@@ -1,7 +1,7 @@
import React from 'react';
import CharCounterDescriptionWidget from 'design-comuni-plone-theme/components/ItaliaTheme/manage/Widgets/CharCounterDescriptionWidget';
import { DatetimeWidget } from '@plone/volto/config/Widgets';
-import { ArrayWidget } from '@plone/volto/components';
+import { ArrayWidget, WysiwygWidget } from '@plone/volto/components';
import { MultilingualWidget } from 'volto-multilingual-widget';
import IconWidget from 'design-comuni-plone-theme/components/ItaliaTheme/manage/Widgets/IconWidget';
import SubsiteSocialLinksWidget from 'design-comuni-plone-theme/components/ItaliaTheme/manage/Widgets/SubsiteSocialLinksWidget';
@@ -58,7 +58,7 @@ const getItaliaWidgets = (config) => {
},
widget: {
...config.widgets.widget,
- // richtext: TinymceWidget
+ richtext: WysiwygWidget,
color_list: ColorListWidget,
path_filters: PathFiltersWidget,
location_filter: LocationFiltersWidget,
diff --git a/src/customizations/volto/config/RichTextEditor/Plugins.jsx b/src/customizations/volto/config/RichTextEditor/Plugins.jsx
new file mode 100644
index 0000000000..4cbb573d1a
--- /dev/null
+++ b/src/customizations/volto/config/RichTextEditor/Plugins.jsx
@@ -0,0 +1,64 @@
+/**
+ * Customizzato:
+ * - cambiato l'import di AnchorPlugin per poterlo customizzare
+ */
+import createLinkPlugin from '@italia/config/RichTextEditor/Plugins/AnchorPlugin';
+
+import Styles from '@plone/volto/config/RichTextEditor/Styles';
+
+const breakOutOptions = {
+ doubleBreakoutBlocks: [
+ 'unordered-list-item',
+ 'ordered-list-item',
+ 'code-block',
+ ],
+ breakoutBlocks: [
+ 'header-one',
+ 'header-two',
+ 'header-three',
+ 'highlight',
+ 'blockquote',
+ 'callout',
+ ],
+};
+
+//const linkDetectionPlugin = createLinkDetectionPlugin();
+
+const plugins = (props) => {
+ const { draftJsInlineToolbarPlugin, draftJsBlockBreakoutPlugin } = props;
+ const { Separator } = draftJsInlineToolbarPlugin;
+ const blockBreakoutPlugin = draftJsBlockBreakoutPlugin.default(
+ breakOutOptions,
+ );
+
+ const linkPlugin = createLinkPlugin({ libraries: props });
+
+ const buttons = Styles(props);
+ const {
+ BoldButton,
+ ItalicButton,
+ HeadlineTwoButton,
+ HeadlineThreeButton,
+ UnorderedListButton,
+ OrderedListButton,
+ BlockquoteButton,
+ CalloutButton,
+ } = buttons;
+
+ const inlineToolbarButtons = [
+ BoldButton,
+ ItalicButton,
+ linkPlugin.LinkButton,
+ Separator,
+ HeadlineTwoButton,
+ HeadlineThreeButton,
+ UnorderedListButton,
+ OrderedListButton,
+ BlockquoteButton,
+ CalloutButton,
+ ];
+
+ return { inlineToolbarButtons, plugins: [linkPlugin, blockBreakoutPlugin] }; //linkDetectionPlugin
+};
+
+export default plugins;
diff --git a/theme/_cms-ui.scss b/theme/_cms-ui.scss
index 7193a219ab..2365e4e2b3 100644
--- a/theme/_cms-ui.scss
+++ b/theme/_cms-ui.scss
@@ -118,6 +118,78 @@ body.cms-ui {
}
}
+ // Link button and tooltip
+ .link-form-container {
+ display: flex;
+ padding: 0.3rem 0;
+
+ .field {
+ display: flex;
+
+ .wrap-field {
+ display: flex;
+ align-items: center;
+
+ .input-anchorlink-theme input:focus,
+ .ui.icon.button:focus,
+ .ui.basic.button:focus {
+ outline: solid 2px $focus-outline-color;
+ border-radius: 0;
+ }
+
+ .cancel .icon {
+ fill: #e40166 !important;
+ }
+ }
+
+ input#field-link {
+ color: #444;
+ font-weight: normal;
+ height: auto;
+ }
+
+ #field-data-element-select {
+ min-width: 220px;
+ width: 100%;
+ height: 34px;
+ font-family: 'Poppins', 'Helvetica Neue', Arial, Helvetica, sans-serif;
+ font-size: 0.8rem;
+ position: relative;
+ font-weight: normal;
+ font-style: normal;
+ padding: 0 7px 0 12px;
+ margin-top: 0.2rem;
+ background-color: transparent;
+ color: #444;
+
+ .react-select__control {
+ min-height: 0;
+ background-color: #fafafa;
+
+ .react-select__single-value {
+ color: #444;
+ font-weight: normal;
+ }
+
+ .react-select__placeholder {
+ opacity: 0.4;
+ }
+ }
+ }
+
+ .wrapper-submit {
+ display: flex;
+ justify-content: flex-end;
+ padding-left: 0.8rem;
+
+ .ui.basic.button:focus {
+ outline: solid 2px $focus-outline-color;
+ border-radius: 0;
+ }
+ }
+ }
+ }
+
.skiplinks {
position: absolute;
top: -100%;