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 { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
|
||||||
import {
|
import Image from '@tiptap/extension-image';
|
||||||
Button
|
import { Button } from 'reactstrap';
|
||||||
} 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) {
|
function ImageNode(props) {
|
||||||
const { src, alt } = props.node.attrs
|
const imageRef = useRef(null);
|
||||||
const { updateAttributes } = props
|
const [modal, setModal] = useState(false);
|
||||||
const onEditAlt = () => {
|
const [height, setHeight] = useState(props.node.attrs.height || '');
|
||||||
const newAlt = prompt('Set alt text:', alt || '')
|
const [width, setWidth] = useState(props.node.attrs.width || '');
|
||||||
updateAttributes({ alt: newAlt })
|
|
||||||
}
|
|
||||||
|
|
||||||
let className = 'image'
|
const toggle = () => setModal(!modal);
|
||||||
if (props.selected) { className += ' ProseMirror-selectednode'}
|
|
||||||
|
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 (
|
return (
|
||||||
<NodeViewWrapper className={className} data-drag-handle>
|
<NodeViewWrapper className={className} data-drag-handle>
|
||||||
<div className="image-container d-md-block">
|
<CustomImagePropertiesModal
|
||||||
<img className='mx-auto d-block' src={src} alt={alt} />
|
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">
|
<div className="image-overlay">
|
||||||
<span className="image-text mx-auto d-block">
|
<span className="image-text mx-auto d-block">
|
||||||
{ alt ?
|
<Button className="edit" type="button" onClick={toggle}>
|
||||||
<span>✔</span> :
|
Edit Image Properties
|
||||||
<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>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Image.extend({
|
export default CustomImageExtension;
|
||||||
addNodeView() {
|
|
||||||
return ReactNodeViewRenderer(ImageNode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|||||||
@ -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,
|
Underline,
|
||||||
Blockquote,
|
Blockquote,
|
||||||
CustomImageExtension,
|
CustomImageExtension.configure({
|
||||||
|
htmlAttributes: ['height', 'width']
|
||||||
|
}),
|
||||||
TextAlign.configure({
|
TextAlign.configure({
|
||||||
types: ['heading', 'paragraph'],
|
types: ['heading', 'paragraph'],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -35,7 +35,6 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 70%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-container {
|
.image-container {
|
||||||
|
|||||||
@ -25,7 +25,7 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 65%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 765px){
|
@media only screen and (max-width: 765px){
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user