'use strict'; var core = require('@tiptap/core'); var React = require('react'); var ReactDOM = require('react-dom'); var jsxRuntime = require('react/jsx-runtime'); var reResizable = require('re-resizable'); const mergeRefs = (...refs) => { return (node) => { refs.forEach(ref => { if (typeof ref === 'function') { ref(node); } else if (ref) { ref.current = node; } }); }; }; const Portals = ({ renderers }) => { return (React.createElement(React.Fragment, null, Object.entries(renderers).map(([key, renderer]) => { return ReactDOM.createPortal(renderer.reactElement, renderer.element, key); }))); }; class PureEditorContent extends React.Component { constructor(props) { super(props); this.editorContentRef = React.createRef(); this.initialized = false; this.state = { renderers: {}, }; } componentDidMount() { this.init(); } componentDidUpdate() { this.init(); } init() { const { editor } = this.props; if (editor && editor.options.element) { if (editor.contentComponent) { return; } const element = this.editorContentRef.current; element.append(...editor.options.element.childNodes); editor.setOptions({ element, }); editor.contentComponent = this; editor.createNodeViews(); this.initialized = true; } } maybeFlushSync(fn) { // Avoid calling flushSync until the editor is initialized. // Initialization happens during the componentDidMount or componentDidUpdate // lifecycle methods, and React doesn't allow calling flushSync from inside // a lifecycle method. if (this.initialized) { ReactDOM.flushSync(fn); } else { fn(); } } setRenderer(id, renderer) { this.maybeFlushSync(() => { this.setState(({ renderers }) => ({ renderers: { ...renderers, [id]: renderer, }, })); }); } removeRenderer(id) { this.maybeFlushSync(() => { this.setState(({ renderers }) => { const nextRenderers = { ...renderers }; delete nextRenderers[id]; return { renderers: nextRenderers }; }); }); } componentWillUnmount() { const { editor } = this.props; if (!editor) { return; } this.initialized = false; if (!editor.isDestroyed) { editor.view.setProps({ nodeViews: {}, }); } editor.contentComponent = null; if (!editor.options.element.firstChild) { return; } const newElement = document.createElement('div'); newElement.append(...editor.options.element.childNodes); editor.setOptions({ element: newElement, }); } render() { const { editor, innerRef, ...rest } = this.props; return (React.createElement(React.Fragment, null, React.createElement("div", { ref: mergeRefs(innerRef, this.editorContentRef), ...rest }), React.createElement(Portals, { renderers: this.state.renderers }))); } } // EditorContent should be re-created whenever the Editor instance changes const EditorContentWithKey = React.forwardRef((props, ref) => { const key = React.useMemo(() => { return Math.floor(Math.random() * 0xFFFFFFFF).toString(); }, [props.editor]); // Can't use JSX here because it conflicts with the type definition of Vue's JSX, so use createElement return React.createElement(PureEditorContent, { key, innerRef: ref, ...props, }); }); React.memo(EditorContentWithKey); const EditorContext = React.createContext({ editor: null, }); EditorContext.Consumer; const ReactNodeViewContext = React.createContext({ onDragStart: undefined, }); const useReactNodeView = () => React.useContext(ReactNodeViewContext); const NodeViewWrapper = React.forwardRef((props, ref) => { const { onDragStart } = useReactNodeView(); const Tag = props.as || 'div'; return (React.createElement(Tag, { ...props, ref: ref, "data-node-view-wrapper": "", onDragStart: onDragStart, style: { whiteSpace: 'normal', ...props.style, } })); }); function isClassComponent(Component) { return !!(typeof Component === 'function' && Component.prototype && Component.prototype.isReactComponent); } function isForwardRefComponent(Component) { var _a; return !!(typeof Component === 'object' && ((_a = Component.$$typeof) === null || _a === void 0 ? void 0 : _a.toString()) === 'Symbol(react.forward_ref)'); } class ReactRenderer { constructor(component, { editor, props = {}, as = 'div', className = '', attrs, }) { this.ref = null; this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString(); this.component = component; this.editor = editor; this.props = props; this.element = document.createElement(as); this.element.classList.add('react-renderer'); if (className) { this.element.classList.add(...className.split(' ')); } if (attrs) { Object.keys(attrs).forEach(key => { this.element.setAttribute(key, attrs[key]); }); } this.render(); } render() { var _a, _b; const Component = this.component; const props = this.props; if (isClassComponent(Component) || isForwardRefComponent(Component)) { props.ref = (ref) => { this.ref = ref; }; } this.reactElement = React.createElement(Component, { ...props }); (_b = (_a = this.editor) === null || _a === void 0 ? void 0 : _a.contentComponent) === null || _b === void 0 ? void 0 : _b.setRenderer(this.id, this); } updateProps(props = {}) { this.props = { ...this.props, ...props, }; this.render(); } destroy() { var _a, _b; (_b = (_a = this.editor) === null || _a === void 0 ? void 0 : _a.contentComponent) === null || _b === void 0 ? void 0 : _b.removeRenderer(this.id); } } class ReactNodeView extends core.NodeView { mount() { const props = { editor: this.editor, node: this.node, decorations: this.decorations, selected: false, extension: this.extension, getPos: () => this.getPos(), updateAttributes: (attributes = {}) => this.updateAttributes(attributes), deleteNode: () => this.deleteNode(), }; if (!this.component.displayName) { const capitalizeFirstChar = (string) => { return string.charAt(0).toUpperCase() + string.substring(1); }; this.component.displayName = capitalizeFirstChar(this.extension.name); } const ReactNodeViewProvider = componentProps => { const Component = this.component; const onDragStart = this.onDragStart.bind(this); const nodeViewContentRef = element => { if (element && this.contentDOMElement && element.firstChild !== this.contentDOMElement) { element.appendChild(this.contentDOMElement); } }; return (React.createElement(React.Fragment, null, React.createElement(ReactNodeViewContext.Provider, { value: { onDragStart, nodeViewContentRef } }, React.createElement(Component, { ...componentProps })))); }; ReactNodeViewProvider.displayName = 'ReactNodeView'; if (this.node.isLeaf) { this.contentDOMElement = null; } else if (this.options.contentDOMElementTag) { this.contentDOMElement = document.createElement(this.options.contentDOMElementTag); } else { this.contentDOMElement = document.createElement(this.node.isInline ? 'span' : 'div'); } if (this.contentDOMElement) { // For some reason the whiteSpace prop is not inherited properly in Chrome and Safari // With this fix it seems to work fine // See: https://github.com/ueberdosis/tiptap/issues/1197 this.contentDOMElement.style.whiteSpace = 'inherit'; } let as = this.node.isInline ? 'span' : 'div'; if (this.options.as) { as = this.options.as; } const { className = '' } = this.options; this.handleSelectionUpdate = this.handleSelectionUpdate.bind(this); this.editor.on('selectionUpdate', this.handleSelectionUpdate); this.renderer = new ReactRenderer(ReactNodeViewProvider, { editor: this.editor, props, as, className: `node-${this.node.type.name} ${className}`.trim(), attrs: this.options.attrs, }); } get dom() { var _a; if (this.renderer.element.firstElementChild && !((_a = this.renderer.element.firstElementChild) === null || _a === void 0 ? void 0 : _a.hasAttribute('data-node-view-wrapper'))) { throw Error('Please use the NodeViewWrapper component for your node view.'); } return this.renderer.element; } get contentDOM() { if (this.node.isLeaf) { return null; } return this.contentDOMElement; } handleSelectionUpdate() { const { from, to } = this.editor.state.selection; if (from <= this.getPos() && to >= this.getPos() + this.node.nodeSize) { if (this.renderer.props.selected) { return; } this.selectNode(); } else { if (!this.renderer.props.selected) { return; } this.deselectNode(); } } update(node, decorations) { const updateProps = (props) => { this.renderer.updateProps(props); }; if (node.type !== this.node.type) { return false; } if (typeof this.options.update === 'function') { const oldNode = this.node; const oldDecorations = this.decorations; this.node = node; this.decorations = decorations; return this.options.update({ oldNode, oldDecorations, newNode: node, newDecorations: decorations, updateProps: () => updateProps({ node, decorations }), }); } if (node === this.node && this.decorations === decorations) { return true; } this.node = node; this.decorations = decorations; updateProps({ node, decorations }); return true; } selectNode() { this.renderer.updateProps({ selected: true, }); this.renderer.element.classList.add('ProseMirror-selectednode'); } deselectNode() { this.renderer.updateProps({ selected: false, }); this.renderer.element.classList.remove('ProseMirror-selectednode'); } destroy() { this.renderer.destroy(); this.editor.off('selectionUpdate', this.handleSelectionUpdate); this.contentDOMElement = null; } } function ReactNodeViewRenderer(component, options) { return (props) => { // try to get the parent component // this is important for vue devtools to show the component hierarchy correctly // maybe it’s `undefined` because isn’t rendered yet if (!props.editor.contentComponent) { return {}; } return new ReactNodeView(component, props, options); }; } const parsePosition = (positionString) => { const positions = positionString.split(";").filter(Boolean); const styleObject = {}; positions.forEach((pos) => { const [property, value] = pos.split(":").map((s) => s.trim()); styleObject[property] = value; }); return styleObject; }; function ImageResizeWrapper(props) { var _a, _b; const [isClicked, setIsClicked] = React.useState(false); const [marginLeft, setMarginLeft] = React.useState((_b = (_a = props.node.attrs) === null || _a === void 0 ? void 0 : _a.marginLeft) !== null && _b !== void 0 ? _b : 0); const defWidth = props.node.attrs.width; const defHeight = props.node.attrs.height; const handleContainerClick = () => { setIsClicked(true); }; const handleMouseLeave = () => { setIsClicked(false); }; const handleLeftArrowClick = () => { props.updateAttributes({ marginLeft, }); setMarginLeft(marginLeft - 10); }; const handleRightArrowClick = () => { props.updateAttributes({ marginLeft, }); setMarginLeft(marginLeft + 10); }; const dotsPosition = [ 'top: -4px; left: -4px; cursor: nwse-resize;', 'top: -4px; right: -4px; cursor: nesw-resize;', 'bottom: -4px; left: -4px; cursor: nesw-resize;', 'bottom: -4px; right: -4px; cursor: nwse-resize;', ]; return (jsxRuntime.jsx(NodeViewWrapper, { className: "image-resizer", children: jsxRuntime.jsx("div", { style: { marginTop: "10px", position: "relative", }, onClick: handleContainerClick, onMouseLeave: handleMouseLeave, children: jsxRuntime.jsxs(reResizable.Resizable // className="resizable-image" , { // className="resizable-image" defaultSize: { width: defWidth ? defWidth : "200", height: defHeight ? defHeight : "80", }, onResize: (e, direction, ref) => { props.updateAttributes({ width: ref.style.width, height: ref.style.height, }); }, maxWidth: "100%", style: { backgroundImage: `url(${props.node.attrs.src})`, backgroundSize: "100% 100%", backgroundRepeat: "no-repeat", marginLeft: `${marginLeft}px`, position: "relative", }, lockAspectRatio: false, children: [isClicked && dotsPosition.map((position, index) => (jsxRuntime.jsx("div", { style: Object.assign({ position: "absolute", width: "8px", height: "8px", backgroundColor: "#8c8c8c", borderRadius: "30%" }, parsePosition(position)) }, index))), isClicked && (jsxRuntime.jsxs("div", { style: { position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)", backgroundColor: "rgba(0, 0, 0, 0.5)", }, children: [jsxRuntime.jsx("button", { className: "arrow-button", onClick: handleLeftArrowClick, style: { transform: "scale(1.5)", marginRight: "10px", background: "gray", border: "none", color: "#fff", cursor: "pointer", transition: "transform 0.5s ease" }, children: "\u2190" }), jsxRuntime.jsx("button", { className: "arrow-button", onClick: handleRightArrowClick, style: { transform: "scale(1.5)", background: "gray", border: "none", color: "#fff", cursor: "pointer", transition: "transform 0.5s ease" }, children: "\u2192" })] }))] }) }) })); } const inputRegex = /(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/; const ResizableImage = core.Node.create({ name: "image", addOptions() { return { inline: false, allowBase64: false, HTMLAttributes: {}, }; }, inline() { return this.options.inline; }, group() { return this.options.inline ? "inline" : "block"; }, draggable: true, addAttributes() { return { src: { default: null, }, alt: { default: null, }, title: { default: null, }, height: { default: null, }, width: { default: null, }, marginLeft: { default: null, }, dataID: { default: null, }, }; }, parseHTML() { return [ { tag: "img", }, ]; }, renderHTML({ HTMLAttributes }) { const { height, width, marginLeft } = HTMLAttributes; const attributes = Object.assign(Object.assign({}, HTMLAttributes), { style: `height: ${height} !important; width: ${width} !important; margin-left: ${marginLeft}px !important;` }); return ["img", core.mergeAttributes(this.options.HTMLAttributes, attributes)]; }, addCommands() { return { setImage: (options) => ({ commands }) => { return commands.insertContent({ type: this.name, attrs: options, }); }, }; }, addNodeView() { return ReactNodeViewRenderer(ImageResizeWrapper); }, addInputRules() { return [ core.nodeInputRule({ find: inputRegex, type: this.type, getAttributes: (match) => { const [, , alt, src, title, height, width] = match; return { src, alt, title, height, width }; }, }), ]; }, }); exports.ResizableImage = ResizableImage;