Commit d9581f2c by yakoff94

update resize-image

parent 5107392c
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="JavaScriptSettings"> <component name="JavaScriptSettings">
<option name="languageLevel" value="JSX" /> <option name="languageLevel" value="FLOW" />
</component> </component>
<component name="ProjectRootManager"> <component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />
......
...@@ -6,7 +6,8 @@ import 'react-ag-qeditor/dist/index.css' ...@@ -6,7 +6,8 @@ import 'react-ag-qeditor/dist/index.css'
const App = () => { const App = () => {
return <div style={{padding:40}}> return <div style={{padding:40}}>
<QEditor <QEditor
value={`<iframe src="https://cdn.atmaguru.online/2/atmacompany/8/b/8bTfGoWtAuv5waVabQRTtWaNOrve5uv8UBXFbGOH9cowQ1K56dYi7TFz6h5jUfzr.pdf" width="100%" /><iframe src="https://www.youtube.com/embed/YmZGP7YP8c4" frameborder="0" allowfullscreen="true"></iframe><video src="https://cdn.atmaguru.online/2/demo/V/k/VkrEXjkxnutXLgcJPt5CLXNgEj4RaL9Zk4SQhIMUjOeIRpu0dSKtQCIMl49pJM6N.webm" controls="true"></video><p>Так исторически сложилось, что взрослым людям стараются дать максимум материалов: часовые лекции, объемные массивы текста и должностных инструкций. Сотрудник изучает огромный объем информации. Пытается его запомнить, а потом в конце курса сдать большой аттестационный экзамен. Вы не учитывете при этом, что мозг взрослого человека перегружен, ему нужно выполнять обязанности по работе, думать о домашних делах, его постоянно отвлекают менеджеры и коллеги по работе… Единственный правильный способ — это давать информацию небольшими кусочками и после каждой порции проверять усвоена она или нет.</p><p></p><p>что-то новое о компании<br><a href="https://cdn.atmaguru.online/1/demo/T/G/TGvSAoLawONkteJ47yyNfmsC8zNe3ZRG4iO0ZfAjmvOIZkm20BWp8KdWCH5p1Rrx.gif" target="_blank" download="Редактор.gif" data-size="37 Мб">РСкачать книгу</a> <br></p>`} // value={`<iframe src="https://cdn.atmaguru.online/2/atmacompany/8/b/8bTfGoWtAuv5waVabQRTtWaNOrve5uv8UBXFbGOH9cowQ1K56dYi7TFz6h5jUfzr.pdf" width="100%" /><iframe src="https://www.youtube.com/embed/YmZGP7YP8c4" frameborder="0" allowfullscreen="true"></iframe><video src="https://cdn.atmaguru.online/2/demo/V/k/VkrEXjkxnutXLgcJPt5CLXNgEj4RaL9Zk4SQhIMUjOeIRpu0dSKtQCIMl49pJM6N.webm" controls="true"></video><p>Так исторически сложилось, что взрослым людям стараются дать максимум материалов: часовые лекции, объемные массивы текста и должностных инструкций. Сотрудник изучает огромный объем информации. Пытается его запомнить, а потом в конце курса сдать большой аттестационный экзамен. Вы не учитывете при этом, что мозг взрослого человека перегружен, ему нужно выполнять обязанности по работе, думать о домашних делах, его постоянно отвлекают менеджеры и коллеги по работе… Единственный правильный способ — это давать информацию небольшими кусочками и после каждой порции проверять усвоена она или нет.</p><p></p><p>что-то новое о компании<br><a href="https://cdn.atmaguru.online/1/demo/T/G/TGvSAoLawONkteJ47yyNfmsC8zNe3ZRG4iO0ZfAjmvOIZkm20BWp8KdWCH5p1Rrx.gif" target="_blank" download="Редактор.gif" data-size="37 Мб">РСкачать книгу</a> <br></p>`}
value={'<p><img src="https://cdn.atmaguru.online/2/atmacompany/L/K/LKKVK05QnvrFIFUohGtRdFbaEweB03TFNQTXItMyOhk0JmmhApvwCWJQu5SzLzNd.jpeg"><img src="https://cdn.atmaguru.online/2/atmacompany/x/Q/xQYFBvh6u7m8YqMUb3mKW2lL19psIyqCNXHurhiG3ozBfZbqiIzUzfP64sgvpcEf.jpeg"></p>'}
onChange={(value)=>{ onChange={(value)=>{
console.log(value); console.log(value);
}} }}
......
...@@ -82,6 +82,8 @@ ...@@ -82,6 +82,8 @@
"react-media-recorder": "^1.6.6", "react-media-recorder": "^1.6.6",
"react-stopwatch": "^2.0.4", "react-stopwatch": "^2.0.4",
"react-webcam": "^7.0.1", "react-webcam": "^7.0.1",
"sass": "^1.49.9" "sass": "^1.49.9",
"tiptap-extension-resize-image": "^1.1.8",
"tiptap-imagresize": "^1.1.0"
} }
} }
...@@ -33,7 +33,11 @@ import { useReactMediaRecorder } from 'react-media-recorder' ...@@ -33,7 +33,11 @@ import { useReactMediaRecorder } from 'react-media-recorder'
import axios from 'axios' import axios from 'axios'
import ReactStopwatch from 'react-stopwatch' import ReactStopwatch from 'react-stopwatch'
import Audio from './extensions/Audio' import Audio from './extensions/Audio'
import CustomImage from './extensions/Image' // import Image from '@tiptap/extension-image'
// import ImageResize from 'tiptap-extension-resize-image';
import ImageResize from './extensions/Image.jsx'
// import ImageResize from 'tiptap-imagresize';
// import ImageResize from 'tiptap-imagresize'; // import ImageResize from 'tiptap-imagresize';
import IframeModal from './modals/IframeModal' import IframeModal from './modals/IframeModal'
...@@ -430,7 +434,9 @@ const QEditor = ({ ...@@ -430,7 +434,9 @@ const QEditor = ({
extensions: [ extensions: [
StarterKit, StarterKit,
Underline, Underline,
CustomImage, // Image,
ImageResize,
// CustomImage,
// Link.configure({ // Link.configure({
// autolink: true, // autolink: true,
// linkOnPaste: true, // linkOnPaste: true,
......
import React from 'react'
import Image from '@tiptap/extension-image'
import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react';
import '../Image.css'
import { mergeAttributes } from "@tiptap/core";
export default Image.extend({
addAttributes() {
return {
alt: {
parseHTML: element => element.getAttribute('alt'),
},
src: {
parseHTML: element => element.getAttribute('src'),
},
width: {
parseHTML: element => element.getAttribute('width'),
}
}
},
addNodeView () {
return ReactNodeViewRenderer(ImageNode)
},
renderHTML({HTMLAttributes }) {
// console.log(node.updateAttributes());
return ['img', mergeAttributes(HTMLAttributes)]
},
addOptions () {
return {
inline: true,
types: ["image"],
handlerStyle: {
width: "8px",
height: "8px",
background: "#07c160",
},
layerStyle: {
border: "2px solid #07c160",
},
};
},
addStorage () {
return {
resizeElement: null,
};
},
onCreate (data) {
create(data, this.options, this.storage);
},
onUpdate(data) {
// create(data, this.options, this.storage);
setTimeout(()=>{
selectionUpdate(data, this.options, this.storage)
},100)
},
onSelectionUpdate (data) {
setTimeout(()=>{
selectionUpdate(data, this.options, this.storage)
},100)
},
})
function ImageNode (props) {
// let width = typeof props.editor.resizeLayer !== 'undefined' ? props.editor.resizeLayer.style.width : props.node.attrs.width;
const {src, alt, width} = props.node.attrs
const { updateAttributes } = props
let className = 'image'
if (props.selected) {
className += ' ProseMirror-selectednode'
}
const onEditAlt = (e) => {
e.preventDefault();
const newAlt = prompt('Введите Alt:', alt || '')
updateAttributes({ alt: newAlt })
}
const onEditWidth = (width) => {
console.log('edit width');
try {
props.editor.commands.updateAttributes({ width: width });
updateAttributes({ width: width })
} catch (e) {
console.log(e);
}
}
let resizeLayer = document.getElementById('resize-layer');
if (resizeLayer) {
const handler = (e) => {
const element = props.editor.options.element;
const resizeElement = props.extension.storage.resizeElement;
if ( ! resizeElement) return;
if (/bottom/.test(e.target.className)) {
let startX = e.screenX;
const dir = e.target.classList.contains("bottom-left") ? -1 : 1;
let last_width = 0
const mousemoveHandle = (e) => {
e.preventDefault();
const width = resizeElement.clientWidth;
const distanceX = e.screenX - startX;
const total = width + dir * distanceX;
// resizeElement
resizeElement.style.width = total + "px";
const clientWidth = resizeElement.clientWidth;
const clientHeight = resizeElement.clientHeight;
resizeElement.style.width = clientWidth + "px"; // max width
last_width = clientWidth;
const pos = getRelativePosition(resizeElement, element);
// console.log(pos);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = clientWidth + "px";
resizeLayer.style.height = clientHeight + "px";
startX = e.screenX;
onEditWidth(last_width);
};
document.addEventListener("mousemove", mousemoveHandle);
document.addEventListener("mouseup", (e) => {
document.removeEventListener("mousemove", mousemoveHandle)
});
}
}
// resizeLayer.removeEventListener('mousedown', handler);
if (resizeLayer.getAttribute('listener') !== 'true') {
resizeLayer.setAttribute('listener', 'true');
resizeLayer.addEventListener("mousedown", handler);
}
}
return (
<NodeViewWrapper className={className} data-drag-handle>
<img src={src} width={width} alt={alt}/>
{
props.selected &&
<span className="alt-text-indicator" style={{zIndex: '999'}}>
{alt ?
<span className="symbol symbol-positive"></span> :
<span className="symbol symbol-negative">!</span>
}
<button className="edit" type="button" onClick={onEditAlt}>Alt</button>
</span>
}
</NodeViewWrapper>
)
}
function selectionUpdate(data, options, storage)
{
let editor = data.editor,
transaction = data.transaction;
const element = editor.options.element;
const node = transaction.curSelection.node;
const resizeLayer = editor.resizeLayer;
// console.log("res layer", resizeLayer)
if (node && options.types.includes(node.type.name)) {
// resizeLayer
resizeLayer.style.display = "block";
let dom = editor.view.domAtPos(transaction.curSelection.from).node;
if (dom.getAttribute("src") !== node.attrs.src) {
dom = dom.querySelector(`[src="${node.attrs.src}"]`);
}
storage.resizeElement = dom;
const pos = getRelativePosition(dom, element);
resizeLayer.style.top = pos.top + "px";
resizeLayer.style.left = pos.left + "px";
resizeLayer.style.width = dom.width + "px";
resizeLayer.style.height = dom.height + "px";
} else {
resizeLayer.style.display = "none";
}
}
function getRelativePosition (element, ancestor) {
const elementRect = element.getBoundingClientRect();
const ancestorRect = ancestor.getBoundingClientRect();
const relativePosition = {
top: parseInt(elementRect.top - ancestorRect.top + ancestor.scrollTop),
left: parseInt(elementRect.left - ancestorRect.left + ancestor.scrollLeft),
};
return relativePosition;
}
function create(data, options, storage) {
let editor = data.editor,
node = data.node;
const element = editor.options.element;
element.style.position = "relative";
// resizeLayer
// console.log('onCreate', data);
const resizeLayer = document.createElement("div");
resizeLayer.className = "resize-layer";
resizeLayer.id = "resize-layer";
resizeLayer.style.display = "none";
resizeLayer.style.position = "absolute";
Object.entries(options.layerStyle).forEach(([key, value]) => {
resizeLayer.style[key] = value;
});
const handlers = ["top-left", "top-right", "bottom-left", "bottom-right"];
const fragment = document.createDocumentFragment();
for (let name of handlers) {
const item = document.createElement("div");
item.className = `handler ${name}`;
item.style.position = "absolute";
Object.entries(options.handlerStyle).forEach(([key, value]) => {
item.style[key] = value;
});
const dir = name.split("-");
item.style[dir[0]] = parseInt(item.style.width) / -2 + "px";
item.style[dir[1]] = parseInt(item.style.height) / -2 + "px";
if (name === "bottom-left") item.style.cursor = "sw-resize";
if (name === "bottom-right") item.style.cursor = "se-resize";
fragment.appendChild(item);
}
resizeLayer.appendChild(fragment);
editor.resizeLayer = resizeLayer;
element.appendChild(resizeLayer);
}
import { NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react";
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState, Fragment } from "react";
import TipTapImage from "@tiptap/extension-image";
const useEvent = (handler) => {
const handlerRef = useRef(null);
useLayoutEffect(() => {
handlerRef.current = handler;
}, [handler]);
return useCallback((...args) => {
if (handlerRef.current === null) {
throw new Error('Handler is not assigned');
}
return handlerRef.current(...args);
}, []);
};
const MIN_WIDTH = 60;
const BORDER_COLOR = '#0096fd';
const ResizableImageTemplate = ({ node, updateAttributes }) => {
const containerRef = useRef(null);
const imgRef = useRef(null);
const [editing, setEditing] = useState(false);
const [resizingStyle, setResizingStyle] = useState(undefined);
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setEditing(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [editing]);
const handleMouseDown = useEvent((event) => {
if (!imgRef.current) return;
event.preventDefault();
const direction = event.currentTarget.dataset.direction || "--";
const initialXPosition = event.clientX;
const currentWidth = imgRef.current.width;
let newWidth = currentWidth;
const transform = direction[1] === "w" ? -1 : 1;
const removeListeners = () => {
window.removeEventListener("mousemove", mouseMoveHandler);
window.removeEventListener("mouseup", removeListeners);
updateAttributes({ width: newWidth });
setResizingStyle(undefined);
};
const mouseMoveHandler = (event) => {
newWidth = Math.max(currentWidth + (transform * (event.clientX - initialXPosition)), MIN_WIDTH);
setResizingStyle({ width: newWidth });
if (!event.buttons) removeListeners();
};
window.addEventListener("mousemove", mouseMoveHandler);
window.addEventListener("mouseup", removeListeners);
});
const dragCornerButton = (direction) => (
<div
role="button"
tabIndex={0}
onMouseDown={handleMouseDown}
data-direction={direction}
style={{
position: 'absolute',
height: '10px',
width: '10px',
backgroundColor: BORDER_COLOR,
...(direction[0] === 'n' ? { top: 0 } : { bottom: 0 }),
...(direction[1] === 'w' ? { left: 0 } : { right: 0 }),
cursor: `${direction}-resize`,
}}
>
</div>
);
return (
<NodeViewWrapper
ref={containerRef}
as="div" draggable data-drag-handle
onClick={() => setEditing(true)}
onBlur={() => setEditing(false)}
style={{
overflow: 'hidden',
position: 'relative',
display: 'inline-block',
lineHeight: '0px',
}}
>
<img
{...node.attrs} ref={imgRef}
style={{
...resizingStyle,
cursor: 'default',
}}
/>
{editing && (
<>
{[
{left: 0, top: 0, height: '100%', width: '1px'}, {right: 0, top: 0, height: '100%', width: '1px'},
{top: 0, left: 0, width: '100%', height: '1px'}, {bottom: 0, left: 0, width: '100%', height: '1px'}
].map((style, i) => (
<div key={i} style={{ position: 'absolute', backgroundColor: BORDER_COLOR, ...style }}></div>
))}
{dragCornerButton("nw")}
{dragCornerButton("ne")}
{dragCornerButton("sw")}
{dragCornerButton("se")}
</>
)}
</NodeViewWrapper>
);
};
const ResizableImageExtension = TipTapImage.extend({
addAttributes() {
return {
...this.parent?.(),
width: { renderHTML: ({ width }) => ({ width }) },
height: { renderHTML: ({ height }) => ({ height }) },
};
},
addNodeView() {
return ReactNodeViewRenderer(ResizableImageTemplate);
},
}).configure({ inline: true });
export default ResizableImageExtension;
...@@ -1089,3 +1089,19 @@ body{ ...@@ -1089,3 +1089,19 @@ body{
//min-height: 700px; //min-height: 700px;
} }
} }
.image-resizer {
display: inline-flex;
position: relative;
flex-grow: 0;
.resize-trigger {
position: absolute;
right: -6px;
bottom: -9px;
opacity: 0;
transition: opacity .3s ease;
color: #3259a5;
}
&:hover .resize-trigger {
opacity: 1;
}
}
...@@ -2120,6 +2120,11 @@ ...@@ -2120,6 +2120,11 @@
resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz"
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
"@types/prop-types@*":
version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/prosemirror-commands@*", "@types/prosemirror-commands@^1.0.4": "@types/prosemirror-commands@*", "@types/prosemirror-commands@^1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.npmjs.org/@types/prosemirror-commands/-/prosemirror-commands-1.0.4.tgz" resolved "https://registry.npmjs.org/@types/prosemirror-commands/-/prosemirror-commands-1.0.4.tgz"
...@@ -2208,6 +2213,14 @@ ...@@ -2208,6 +2213,14 @@
resolved "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz" resolved "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz"
integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
"@types/react@^18.0.38":
version "18.3.3"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f"
integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/resolve@0.0.8": "@types/resolve@0.0.8":
version "0.0.8" version "0.0.8"
resolved "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz"
...@@ -4312,6 +4325,11 @@ cssstyle@^1.0.0, cssstyle@^1.1.1: ...@@ -4312,6 +4325,11 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
dependencies: dependencies:
cssom "0.3.x" cssom "0.3.x"
csstype@^3.0.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
cyclist@^1.0.1: cyclist@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz" resolved "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz"
...@@ -11953,6 +11971,18 @@ tippy.js@^6.3.7: ...@@ -11953,6 +11971,18 @@ tippy.js@^6.3.7:
dependencies: dependencies:
"@popperjs/core" "^2.9.0" "@popperjs/core" "^2.9.0"
tiptap-extension-resize-image@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/tiptap-extension-resize-image/-/tiptap-extension-resize-image-1.1.8.tgz#a45e450a0b50f26d87e3c923cf20112408ebb9f4"
integrity sha512-dRPCfkCCUPtlVCn7w9HHE01ANJE6pRQMPAYXmsd1Qlk8KUasePdvCQIVJMqKriAqvKYJtnRc3HfojmzZtkQq0w==
tiptap-imagresize@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/tiptap-imagresize/-/tiptap-imagresize-1.1.0.tgz#db6be6f4f1bf699e5648ef9127f6ea97003b89ae"
integrity sha512-lX3rIhyDzO17ngZALLj5dg3QG1jXqLg3rsOp4AU30m4i553u572z+uas1GIhODYWDrFU9cywOwNhu1HhgAaggw==
dependencies:
"@types/react" "^18.0.38"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment