Skip to content
Open
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
9 changes: 8 additions & 1 deletion client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ function getSynchedProject(currentState, responseProject) {
export function saveProject(
selectedFile = null,
autosave = false,
mobile = false
mobile = false,
previewImage = null
) {
return (dispatch, getState) => {
const state = getState();
Expand All @@ -151,6 +152,9 @@ export function saveProject(
}
const formParams = Object.assign({}, state.project);
formParams.files = [...state.files];
if (previewImage) {
formParams.previewImage = previewImage;
}

if (selectedFile) {
const fileToUpdate = formParams.files.find(
Expand Down Expand Up @@ -331,6 +335,9 @@ export function cloneProject(project) {
{ name: `${projectName} copy` },
{ files: newFiles }
);
if (project?.previewImage) {
formParams.previewImage = project.previewImage;
}
apiClient
.post('/projects', formParams)
.then((response) => {
Expand Down
6 changes: 4 additions & 2 deletions client/modules/IDE/components/IDEKeyHandlers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getIsUserOwner,
getSketchOwner
} from '../selectors/users';
import capturePreviewImage from '../utils/capturePreviewImage';

export const useIDEKeyHandlers = ({ getContent }) => {
const dispatch = useDispatch();
Expand All @@ -40,11 +41,12 @@ export const useIDEKeyHandlers = ({ getContent }) => {
};

useKeyDownHandlers({
'ctrl-s': (e) => {
'ctrl-s': async (e) => {
e.preventDefault();
e.stopPropagation();
if (isUserOwner || (isAuthenticated && !sketchOwner)) {
dispatch(saveProject(getContent()));
const previewImage = await capturePreviewImage();
dispatch(saveProject(getContent(), false, false, previewImage));
} else if (isAuthenticated) {
dispatch(cloneProject());
} else {
Expand Down
4 changes: 4 additions & 0 deletions client/modules/IDE/components/SketchList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,9 @@ const SketchList = ({
>
<thead>
<tr>
<th scope="col" className="sketches-table__preview-header">
{t('SketchList.HeaderPreview')}
</th>
{renderFieldHeader('name', t('SketchList.HeaderName'))}
{renderFieldHeader(
'createdAt',
Expand Down Expand Up @@ -193,6 +196,7 @@ SketchList.propTypes = {
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired,
previewImage: PropTypes.string,
visibility: PropTypes.string
})
).isRequired,
Expand Down
15 changes: 15 additions & 0 deletions client/modules/IDE/components/SketchListRowBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,22 @@ const SketchListRowBase = ({
</>
);

const previewAlt = t('SketchList.PreviewAlt', { name: sketch.name });
const previewCell = sketch.previewImage ? (
<img
className="sketches-table__preview-image"
src={sketch.previewImage}
alt={previewAlt}
/>
) : (
<div className="sketches-table__preview-placeholder">
{t('SketchList.NoPreview')}
</div>
);

return (
<tr className="sketches-table__row">
<td className="sketches-table__preview-cell">{previewCell}</td>
<th scope="row">{name}</th>
<td>{formatDateCell(sketch.createdAt, mobile)}</td>
<td>{formatDateCell(sketch.updatedAt, mobile)}</td>
Expand Down Expand Up @@ -166,6 +180,7 @@ SketchListRowBase.propTypes = {
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired,
previewImage: PropTypes.string,
visibility: PropTypes.string
}).isRequired,
username: PropTypes.string.isRequired,
Expand Down
8 changes: 6 additions & 2 deletions client/modules/IDE/hooks/useSketchActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { showToast } from '../actions/toast';
import { showErrorModal, showShareModal } from '../actions/ide';
import { selectCanEditSketch } from '../selectors/users';
import capturePreviewImage from '../utils/capturePreviewImage';

const useSketchActions = () => {
const unsavedChanges = useSelector((state) => state.ide.unsavedChanges);
Expand All @@ -32,9 +33,12 @@ const useSketchActions = () => {
}
}

function saveSketch(cmController) {
async function saveSketch(cmController) {
if (authenticated) {
dispatch(saveProject(cmController?.getContent()));
const previewImage = await capturePreviewImage();
dispatch(
saveProject(cmController?.getContent(), false, false, previewImage)
);
} else {
dispatch(showErrorModal('forceAuthentication'));
}
Expand Down
3 changes: 3 additions & 0 deletions client/modules/IDE/reducers/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const initialState = () => {
return {
name: generatedName,
updatedAt: '',
previewImage: null,
isSaving: false,
visibility: 'Public'
};
Expand All @@ -31,6 +32,7 @@ const project = (state, action) => {
id: action.project.id,
name: action.project.name,
updatedAt: action.project.updatedAt,
previewImage: action.project.previewImage || null,
owner: action.owner,
isSaving: false,
visibility: action.project.visibility
Expand All @@ -40,6 +42,7 @@ const project = (state, action) => {
id: action.project.id,
name: action.project.name,
updatedAt: action.project.updatedAt,
previewImage: action.project.previewImage || null,
owner: action.owner,
isSaving: false,
visibility: action.project.visibility
Expand Down
31 changes: 31 additions & 0 deletions client/modules/IDE/utils/capturePreviewImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { dispatchMessage, MessageTypes } from '../../../utils/dispatcher';

const PREVIEW_RESPONSE_TIMEOUT = 2000;

export default function capturePreviewImage() {
return new Promise((resolve) => {
let timer;

function handleMessage(event) {
const { data } = event;
if (!data || data.type !== MessageTypes.PREVIEW_IMAGE) return;

window.removeEventListener('message', handleMessage);
window.clearTimeout(timer);

if (data?.payload?.image) {
resolve(data.payload.image);
} else {
resolve(null);
}
}

timer = window.setTimeout(() => {
window.removeEventListener('message', handleMessage);
resolve(null);
}, PREVIEW_RESPONSE_TIMEOUT);

window.addEventListener('message', handleMessage);
dispatchMessage({ type: MessageTypes.REQUEST_PREVIEW_IMAGE });
});
}
28 changes: 26 additions & 2 deletions client/modules/Preview/EmbedFrame.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,14 @@ function getHtmlFile(files) {
return files.filter((file) => file.name.match(/.*\.html$/i))[0];
}

function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) {
function EmbedFrame({
files,
isPlaying,
basePath,
gridOutput,
textOutput,
frameRef
}) {
const iframe = useRef();
const htmlFile = useMemo(() => getHtmlFile(files), [files]);
const srcRef = useRef();
Expand All @@ -272,6 +279,16 @@ function EmbedFrame({ files, isPlaying, basePath, gridOutput, textOutput }) {
};
});

useEffect(() => {
if (!frameRef) {
return () => {};
}
frameRef.current = iframe.current;
return () => {
frameRef.current = null;
};
}, [frameRef]);

function renderSketch() {
const doc = iframe.current;
if (isPlaying) {
Expand Down Expand Up @@ -321,7 +338,14 @@ EmbedFrame.propTypes = {
isPlaying: PropTypes.bool.isRequired,
basePath: PropTypes.string.isRequired,
gridOutput: PropTypes.bool.isRequired,
textOutput: PropTypes.bool.isRequired
textOutput: PropTypes.bool.isRequired,
frameRef: PropTypes.shape({
current: PropTypes.instanceOf(Element)
})
};

EmbedFrame.defaultProps = {
frameRef: null
};

export default EmbedFrame;
59 changes: 58 additions & 1 deletion client/modules/Preview/previewIndex.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import React, { useReducer, useState, useEffect } from 'react';
import React, {
useReducer,
useState,
useEffect,
useRef,
useCallback
} from 'react';
import { render } from 'react-dom';
import { createGlobalStyle } from 'styled-components';
import {
Expand All @@ -24,8 +30,55 @@ const App = () => {
const [basePath, setBasePath] = useState('');
const [textOutput, setTextOutput] = useState(false);
const [gridOutput, setGridOutput] = useState(false);
const sketchFrameRef = useRef(null);
registerFrame(window.parent, getConfig('EDITOR_URL'));

const sendPreviewImage = useCallback(() => {
try {
const iframeDocument = sketchFrameRef.current?.contentDocument;
const canvas = iframeDocument?.querySelector('canvas');
if (!canvas || !canvas.width || !canvas.height) {
dispatchMessage({
type: MessageTypes.PREVIEW_IMAGE,
payload: { image: null }
});
return;
}

const maxDimension = 400;
const scale = Math.min(
maxDimension / canvas.width,
maxDimension / canvas.height,
1
);
const targetWidth = Math.max(1, Math.floor(canvas.width * scale));
const targetHeight = Math.max(1, Math.floor(canvas.height * scale));
const previewCanvas = document.createElement('canvas');
previewCanvas.width = targetWidth;
previewCanvas.height = targetHeight;
const ctx = previewCanvas.getContext('2d');
if (!ctx) {
dispatchMessage({
type: MessageTypes.PREVIEW_IMAGE,
payload: { image: null }
});
return;
}
ctx.drawImage(canvas, 0, 0, targetWidth, targetHeight);
const image = previewCanvas.toDataURL('image/png');

dispatchMessage({
type: MessageTypes.PREVIEW_IMAGE,
payload: { image }
});
} catch (error) {
dispatchMessage({
type: MessageTypes.PREVIEW_IMAGE,
payload: { image: null }
});
}
}, []);

function handleMessageEvent(message) {
const { type, payload } = message;
switch (type) {
Expand All @@ -47,6 +100,9 @@ const App = () => {
case MessageTypes.EXECUTE:
dispatchMessage(payload);
break;
case MessageTypes.REQUEST_PREVIEW_IMAGE:
sendPreviewImage();
break;
default:
break;
}
Expand Down Expand Up @@ -75,6 +131,7 @@ const App = () => {
<React.Fragment>
<GlobalStyle />
<EmbedFrame
frameRef={sketchFrameRef}
files={addCacheBustingToAssets(state)}
isPlaying={isPlaying}
basePath={basePath}
Expand Down
48 changes: 42 additions & 6 deletions client/styles/components/_sketch-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,17 @@
align-items: center;
}

.sketches-table__preview-cell {
width: 100%;
padding-left: 0;
}

.sketches-table__preview-image,
.sketches-table__preview-placeholder {
width: 100%;
height: #{math.div(180, $base-font-size)}rem;
}

.sketch-list__dropdown-column {
position: absolute;
top: 0;
Expand Down Expand Up @@ -188,10 +199,6 @@
}
}

.sketches-table thead th:nth-child(1) {
padding-left: #{math.div(12, $base-font-size)}rem;
}

.sketches-table__row {
margin: #{math.div(10, $base-font-size)}rem;
height: #{math.div(72, $base-font-size)}rem;
Expand All @@ -204,7 +211,7 @@
}
}

.sketches-table__row>th:nth-child(1) {
.sketches-table__row>th:nth-child(2) {
padding-left: #{math.div(12, $base-font-size)}rem;
}

Expand Down Expand Up @@ -242,6 +249,35 @@
align-items: center;
}

.sketches-table__preview-header {
padding-left: #{math.div(12, $base-font-size)}rem;
}

.sketches-table__preview-cell {
width: #{math.div(120, $base-font-size)}rem;
}

.sketches-table__preview-image,
.sketches-table__preview-placeholder {
width: #{math.div(96, $base-font-size)}rem;
height: #{math.div(64, $base-font-size)}rem;
border-radius: #{math.div(6, $base-font-size)}rem;
object-fit: cover;
display: block;
}

.sketches-table__preview-placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: #{math.div(12, $base-font-size)}rem;
@include themify() {
background: getThemifyVariable("search-background-color");
color: getThemifyVariable("inactive-text-color");
border: 1px dashed getThemifyVariable("modal-border-color");
}
}

.sketches-table__icon-cell {
width: #{math.div(35, $base-font-size)}rem;
}
Expand All @@ -256,4 +292,4 @@
color: getThemifyVariable("logo-color");
}
text-decoration-thickness: 0.1em;
}
}
Loading