Added ability to resize images in editor
This commit is contained in:
parent
59929caa60
commit
43cafc363e
127
frontend/editable-ui/src/components/shared/tiptap-custom-extensions/custom-image-extension.jsx
Normal file → Executable file
127
frontend/editable-ui/src/components/shared/tiptap-custom-extensions/custom-image-extension.jsx
Normal file → Executable file
@ -1,50 +1,109 @@
|
||||
/*
|
||||
extension credits: Angelika Tyborska: https://angelika.me/2023/02/26/how-to-add-editing-image-alt-text-tiptap/
|
||||
*/
|
||||
|
||||
import Image from '@tiptap/extension-image'
|
||||
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
||||
import {
|
||||
Button
|
||||
} from 'reactstrap';
|
||||
import Image from '@tiptap/extension-image';
|
||||
import { Button } from 'reactstrap';
|
||||
import CustomImagePropertiesModal from './custom-image-properties-modal.jsx';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
|
||||
const CustomImageExtension = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
height: {
|
||||
default: '300',
|
||||
parseHTML: element => element.getAttribute('height'),
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.height) {
|
||||
return {};
|
||||
}
|
||||
return { height: attributes.height };
|
||||
},
|
||||
},
|
||||
width: {
|
||||
default: '300',
|
||||
parseHTML: element => element.getAttribute('width'),
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.width) {
|
||||
return {};
|
||||
}
|
||||
return { width: attributes.width };
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ImageNode);
|
||||
},
|
||||
});
|
||||
|
||||
function ImageNode(props) {
|
||||
const { src, alt } = props.node.attrs
|
||||
const { updateAttributes } = props
|
||||
const onEditAlt = () => {
|
||||
const newAlt = prompt('Set alt text:', alt || '')
|
||||
updateAttributes({ alt: newAlt })
|
||||
}
|
||||
const imageRef = useRef(null);
|
||||
const [modal, setModal] = useState(false);
|
||||
const [height, setHeight] = useState(props.node.attrs.height || '');
|
||||
const [width, setWidth] = useState(props.node.attrs.width || '');
|
||||
|
||||
let className = 'image'
|
||||
if (props.selected) { className += ' ProseMirror-selectednode'}
|
||||
const toggle = () => setModal(!modal);
|
||||
|
||||
const { src, alt } = props.node.attrs;
|
||||
const { updateAttributes } = props;
|
||||
|
||||
const setAlt = (alt) => {
|
||||
updateAttributes({ alt });
|
||||
};
|
||||
|
||||
const handleSetWidth = (width) => {
|
||||
setWidth(width);
|
||||
updateAttributes({ width });
|
||||
};
|
||||
|
||||
const handleSetHeight = (height) => {
|
||||
setHeight(height);
|
||||
updateAttributes({ height });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (imageRef.current) {
|
||||
imageRef.current.height = height;
|
||||
imageRef.current.width = width;
|
||||
}
|
||||
}, [height, width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imageRef.current) {
|
||||
if (imageRef.current.height !== ''){
|
||||
setHeight(imageRef.current.height)
|
||||
}
|
||||
if (imageRef.current.width !== ''){
|
||||
setWidth(imageRef.current.width)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let className = 'image';
|
||||
if (props.selected) className += ' ProseMirror-selectednode';
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className={className} data-drag-handle>
|
||||
<div className="image-container d-md-block">
|
||||
<img className='mx-auto d-block' src={src} alt={alt} />
|
||||
<CustomImagePropertiesModal
|
||||
modal={modal}
|
||||
toggle={toggle}
|
||||
setAlt={setAlt}
|
||||
alt={alt}
|
||||
imageRef={imageRef}
|
||||
setWidth={handleSetWidth}
|
||||
setHeight={handleSetHeight}
|
||||
/>
|
||||
<div className='image-container d-md-block'>
|
||||
<img ref={imageRef} className='mx-auto d-block' src={src} alt={alt} height={height === '' ? '300' : height} width={width === '' ? '300' : width} />
|
||||
<div className="image-overlay">
|
||||
<span className="image-text mx-auto d-block">
|
||||
{ alt ?
|
||||
<span>✔</span> :
|
||||
<span>!</span>
|
||||
}
|
||||
{ alt ?
|
||||
<span className="text">Alt text: “{alt}“.</span>:
|
||||
<span className="text">Alt text missing.</span>
|
||||
}
|
||||
<Button className="edit" type="button" onClick={onEditAlt}>
|
||||
Edit
|
||||
<Button className="edit" type="button" onClick={toggle}>
|
||||
Edit Image Properties
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</NodeViewWrapper>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default Image.extend({
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(ImageNode)
|
||||
}
|
||||
})
|
||||
export default CustomImageExtension;
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, InputGroup, InputGroupText, Input } from 'reactstrap';
|
||||
import RangeSlider from './range-slider.jsx';
|
||||
import { useRef } from 'react';
|
||||
|
||||
function CustomImagePropertiesModal(props) {
|
||||
|
||||
const altField = useRef(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal isOpen={props.modal} toggle={props.toggle}>
|
||||
<ModalHeader toggle={props.toggle}>Change Image Properties</ModalHeader>
|
||||
<ModalBody>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
Image Alt Text
|
||||
</InputGroupText>
|
||||
<Input innerRef={altField} defaultValue={props.alt} onChange={() => props.setAlt(altField.current.value)} />
|
||||
<RangeSlider setRange={props.setHeight} min={0} max={1000} step={1} defaultValue={300} value={props.height} label="Select Height" />
|
||||
<RangeSlider setRange={props.setWidth} min={0} max={1000} step={1} defaultValue={300} value={props.width} label="Select Width" />
|
||||
</InputGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onClick={props.toggle}>
|
||||
Ok
|
||||
</Button>{' '}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default CustomImagePropertiesModal;
|
||||
@ -0,0 +1,30 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Input, Label, FormGroup, Container } from 'reactstrap';
|
||||
function RangeSlider (props) {
|
||||
const [value, setValue] = useState(props.defaultValue || props.min);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
props.setRange(e.target.value)
|
||||
};
|
||||
|
||||
return (
|
||||
<Container className='mt-5'>
|
||||
<FormGroup>
|
||||
<Label for="rangeSlider">{props.label}: {value} pixels</Label>
|
||||
<Input
|
||||
type="range"
|
||||
name="range"
|
||||
id="rangeSlider"
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
step={props.step}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default RangeSlider;
|
||||
@ -348,7 +348,9 @@ const extensions = [
|
||||
}),
|
||||
Underline,
|
||||
Blockquote,
|
||||
CustomImageExtension,
|
||||
CustomImageExtension.configure({
|
||||
htmlAttributes: ['height', 'width']
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
|
||||
@ -35,7 +35,6 @@ a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
|
||||
@ -25,7 +25,7 @@ a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 65%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 765px){
|
||||
|
||||
Loading…
Reference in New Issue
Block a user