Skip to content

Commit

Permalink
Merge branch 'main' of github.com:collective/volto-hydra into add-exa…
Browse files Browse the repository at this point in the history
…mples
  • Loading branch information
MAX-786 committed Jun 29, 2024
2 parents ab50efb + c629dc0 commit 60d57df
Show file tree
Hide file tree
Showing 10 changed files with 560 additions and 511 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,15 @@ function handleEditChange(updatedData) {
onEditChange(handleEditChange);
```

### Level 4: Enable Managing Blocks directly on your frontend ([TODO](https://github.com/collective/volto-hydra/issues/4))
### Level 4: Enable Managing Blocks directly on your frontend

If you completed level 2 & 3 (made blocks clickable and enabled live updates) then the editor will automatically gain the management of blocks on the frontend using the quanta toolbar
- Add blocks ([TODO](https://github.com/collective/volto-hydra/issues/27))
- remove blocks ([TODO](https://github.com/collective/volto-hydra/issues/26))
If you completed level 2 & 3 (made blocks clickable and enabled live updates) then the editor will automatically gain the management of blocks on the frontend using the quanta toolbar.

With Quanta toobar, you can use following features:

- You can click on '+' Icon (appears at the bottom-right of the container in which you added `data-bloc-uid="<<BLOCK_UID>>>"` attribute) to add a block below the current block by choosing a type from BlockChooser popup.
- You can click on three dots icon on Quanta toolbar (appears at the top-left) and it will open up a dropdown menu, you can click on 'Remove' to delete the current block.
- Settings option is yet to be implemented [TODO](https://github.com/collective/volto-hydra/issues/81)
- drag and drop blocks ([TODO](https://github.com/collective/volto-hydra/issues/65))
- cut, copy and paste blocks ([TODO](https://github.com/collective/volto-hydra/issues/67))
- and more ([TODO](https://github.com/collective/volto-hydra/issues/4))
Expand Down
308 changes: 286 additions & 22 deletions packages/hydra-js/hydra.js

Large diffs are not rendered by default.

215 changes: 184 additions & 31 deletions packages/volto-hydra/src/components/Iframe/View.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import { useSelector, useDispatch } from 'react-redux';
import Cookies from 'js-cookie';
import isValidUrl from '../../utils/isValidUrl';
import {
applyBlockDefaults,
deleteBlock,
getBlocksFieldname,
insertBlock,
mutateBlock,
previousBlockId,
} from '@plone/volto/helpers';
import './styles.css';
import { setSelectedBlock } from '../../actions';
import usePresetUrls from '../../utils/usePresetsUrls';
import { useIntl } from 'react-intl';
import config from '@plone/volto/registry';
import usePresetUrls from '../../utils/usePreseturls';
import isValidUrl from '../../utils/isValidUrl';
import { BlockChooser } from '@plone/volto/components';
import { createPortal } from 'react-dom';
import { usePopper } from 'react-popper';
import UrlInput from '../UrlInput';

/**
Expand All @@ -16,26 +27,98 @@ import UrlInput from '../UrlInput';
*/
const getUrlWithAdminParams = (url, token) => {
return typeof window !== 'undefined'
? `${url}${window.location.pathname.replace('/edit', '')}?access_token=${token}&_edit=true`
? window.location.pathname.endsWith('/edit')
? `${url}${window.location.pathname.replace('/edit', '')}?access_token=${token}&_edit=true`
: `${url}${window.location.pathname}?access_token=${token}&_edit=false`
: null;
};

const Iframe = () => {
const dispatch = useDispatch();
const [url, setUrl] = useState('');
const Iframe = (props) => {
// ----Experimental----
const {
onSelectBlock,
properties,
onChangeFormData,
metadata,
formData: form,
token,
allowedBlocks,
showRestricted,
blocksConfig = config.blocks.blocksConfig,
navRoot,
type: contentType,
selectedBlock,
} = props;
// const [ready, setReady] = useState(false);
// useEffect(() => {
// setReady(true);
// }, []);
const [addNewBlockOpened, setAddNewBlockOpened] = useState(false);
const [popperElement, setPopperElement] = useState(null);
const [referenceElement, setReferenceElement] = useState(null);
const blockChooserRef = useRef();
const { styles, attributes } = usePopper(referenceElement, popperElement, {
strategy: 'fixed',
placement: 'bottom',
modifiers: [
{
name: 'offset',
options: {
offset: [0, -250],
},
},
{
name: 'flip',
options: {
fallbackPlacements: ['right-end', 'top-start'],
},
},
],
});
//-------------------------

const [url, setUrl] = useState('');
const [src, setSrc] = useState('');
const history = useHistory();
const token = useSelector((state) => state.userSession.token);
const form = useSelector((state) => state.form.global);
const presetUrls = usePresetUrls();

const defaultUrl = presetUrls[0] || 'http://localhost:3002';
const presetUrls = usePresetUrls();
const defaultUrl = presetUrls[0];
const savedUrl = Cookies.get('iframe_url');
const initialUrl = savedUrl
? getUrlWithAdminParams(savedUrl, token)
: getUrlWithAdminParams(defaultUrl, token);

//-----Experimental-----
const intl = useIntl();

const onInsertBlock = (id, value, current) => {
const [newId, newFormData] = insertBlock(
properties,
id,
value,
current,
config.experimental.addBlockButton.enabled ? 1 : 0,
);

const blocksFieldname = getBlocksFieldname(newFormData);
const blockData = newFormData[blocksFieldname][newId];
newFormData[blocksFieldname][newId] = applyBlockDefaults({
data: blockData,
intl,
metadata,
properties,
});

onChangeFormData(newFormData);
return newId;
};

const onMutateBlock = (id, value) => {
const newFormData = mutateBlock(properties, id, value);
onChangeFormData(newFormData);
};
//---------------------------

const handleNavigateToUrl = useCallback(
(givenUrl = null) => {
if (!isValidUrl(givenUrl) && !isValidUrl(url)) {
Expand All @@ -46,17 +129,7 @@ const Iframe = () => {
const newOrigin = formattedUrl.origin;
Cookies.set('iframe_url', newOrigin, { expires: 7 });

if (formattedUrl.pathname !== '/') {
history.push(
window.location.pathname.endsWith('/edit')
? `${formattedUrl.pathname}/edit`
: `${formattedUrl.pathname}`,
);
} else {
history.push(
window.location.pathname.endsWith('/edit') ? `/edit` : `/`,
);
}
history.push(`${formattedUrl.pathname}`);
},
[history, url],
);
Expand All @@ -69,7 +142,23 @@ const Iframe = () => {
}, [savedUrl, defaultUrl, initialUrl]);

useEffect(() => {
const initialUrlOrigin = new URL(initialUrl).origin;
//----------------Experimental----------------
const onDeleteBlock = (id, selectPrev) => {
const previous = previousBlockId(properties, id);
const newFormData = deleteBlock(properties, id);
onChangeFormData(newFormData);

onSelectBlock(selectPrev ? previous : null);
const origin = new URL(src).origin;
document
.getElementById('previewIframe')
.contentWindow.postMessage(
{ type: 'SELECT_BLOCK', uid: previous },
origin,
);
};
//----------------------------------------------
const initialUrlOrigin = initialUrl ? new URL(initialUrl).origin : '';
const messageHandler = (event) => {
if (event.origin !== initialUrlOrigin) {
return;
Expand All @@ -82,10 +171,20 @@ const Iframe = () => {

case 'OPEN_SETTINGS':
if (history.location.pathname.endsWith('/edit')) {
dispatch(setSelectedBlock(event.data.uid));
onSelectBlock(event.data.uid);
setAddNewBlockOpened(false);
}
break;

case 'ADD_BLOCK':
//----Experimental----
setAddNewBlockOpened(true);
break;

case 'DELETE_BLOCK':
onDeleteBlock(event.data.uid, true);
break;

default:
break;
}
Expand All @@ -99,29 +198,83 @@ const Iframe = () => {
window.removeEventListener('message', messageHandler);
};
}, [
dispatch,
handleNavigateToUrl,
history.location.pathname,
initialUrl,
onChangeFormData,
onSelectBlock,
properties,
src,
token,
]);

useEffect(() => {
if (Object.keys(form).length > 0 && isValidUrl(initialUrl)) {
if (form && Object.keys(form).length > 0 && isValidUrl(src)) {
// Send the form data to the iframe
const origin = new URL(initialUrl).origin;
const origin = new URL(src).origin;
document
.getElementById('previewIframe')
.contentWindow.postMessage({ type: 'FORM', data: form }, origin);
}
}, [form, initialUrl]);
}, [form, initialUrl, src]);

return (
<div id="iframeContainer">
<div className="input-container">
<UrlInput urls={presetUrls} onSelect={handleNavigateToUrl} />
</div>
<iframe id="previewIframe" title="Preview" src={src} />
{addNewBlockOpened &&
createPortal(
<div
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<BlockChooser
onMutateBlock={
onMutateBlock
? (id, value) => {
setAddNewBlockOpened(false);
onMutateBlock(id, value);
}
: null
}
onInsertBlock={
onInsertBlock
? (id, value) => {
setAddNewBlockOpened(false);
const newId = onInsertBlock(id, value);
const origin = new URL(src).origin;
document
.getElementById('previewIframe')
.contentWindow.postMessage(
{
type: 'SELECT_BLOCK',
uid: newId,
},
origin,
);
}
: null
}
currentBlock={selectedBlock}
allowedBlocks={allowedBlocks}
blocksConfig={blocksConfig}
properties={properties}
showRestricted={showRestricted}
ref={blockChooserRef}
navRoot={navRoot}
contentType={contentType}
/>
</div>,
document.body,
)}
<iframe
id="previewIframe"
title="Preview"
src={src}
ref={setReferenceElement}
/>
</div>
);
};
Expand Down
21 changes: 18 additions & 3 deletions packages/volto-hydra/src/components/UrlInput/UrlInput.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import React, { useState, useEffect } from 'react';
import { Dropdown, Icon } from 'semantic-ui-react';
import { Dropdown } from 'semantic-ui-react';
import Cookies from 'js-cookie';
import './styles.css';

const getSavedUrls = () => {
const savedUrls = Cookies.get('saved_urls');
return savedUrls ? savedUrls.split(',') : [];
};

const setSavedUrls = (urls) => {
Cookies.set('saved_urls', urls.join(','), { expires: 7 });
};

const UrlInput = ({ urls, onSelect }) => {
const [searchQuery, setSearchQuery] = useState('');
const [value, setValue] = useState(Cookies.get('iframe_url') || '');
Expand All @@ -14,15 +23,21 @@ const UrlInput = ({ urls, onSelect }) => {
};

useEffect(() => {
setUrlList((prev) => (prev ? [...prev, ...urls] : urls));
const savedUrls = getSavedUrls();
setUrlList((prev) =>
prev ? [...new Set([...savedUrls, ...prev, ...urls])] : urls,
);
}, [urls]);

const handleSearchChange = (e, { searchQuery }) => {
setSearchQuery(searchQuery);
};

const handleOnAddItem = (e, { value }) => {
setUrlList((prev) => (prev ? [...prev, value] : [value]));
const updatedUrlList = [...urlList, value];
setUrlList(updatedUrlList);
setSavedUrls(updatedUrlList);
onSelect(value);
};
const renderDropdown = () => {
const dropdownOptions = urlList.map((url) => ({
Expand Down
Loading

0 comments on commit 60d57df

Please sign in to comment.