UNPKG

tiptap-extension-resize-image

Version:

A tiptap image resizing extension for React, Vue, Next, and VanillaJS. Additionally, it can align the image position.

390 lines (378 loc) 15.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var Image = require('@tiptap/extension-image'); const CONSTANTS = { MOBILE_BREAKPOINT: 768, ICON_SIZE: '24px', CONTROLLER_HEIGHT: '25px', DOT_SIZE: { MOBILE: 16, DESKTOP: 9, }, DOT_POSITION: { MOBILE: '-8px', DESKTOP: '-4px', }, COLORS: { BORDER: '#6C6C6C', BACKGROUND: 'rgba(255, 255, 255, 1)', }, ICONS: { LEFT: 'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/format_align_left/default/20px.svg', CENTER: 'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/format_align_center/default/20px.svg', RIGHT: 'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/format_align_right/default/20px.svg', }, }; const utils = { isMobile() { return document.documentElement.clientWidth < CONSTANTS.MOBILE_BREAKPOINT; }, getDotPosition() { return utils.isMobile() ? CONSTANTS.DOT_POSITION.MOBILE : CONSTANTS.DOT_POSITION.DESKTOP; }, getDotSize() { return utils.isMobile() ? CONSTANTS.DOT_SIZE.MOBILE : CONSTANTS.DOT_SIZE.DESKTOP; }, clearContainerBorder(container) { const containerStyle = container.getAttribute('style'); const newStyle = containerStyle === null || containerStyle === void 0 ? void 0 : containerStyle.replace('border: 1px dashed #6C6C6C;', '').replace('border: 1px dashed rgb(108, 108, 108)', ''); container.setAttribute('style', newStyle); }, removeResizeElements(container) { if (container.childElementCount > 3) { for (let i = 0; i < 5; i++) { container.removeChild(container.lastChild); } } }, }; class StyleManager { static getContainerStyle(inline, width) { const baseStyle = `width: ${width || '100%'}; height: auto; cursor: pointer;`; const inlineStyle = inline ? 'display: inline-block;' : ''; return `${baseStyle} ${inlineStyle}`; } static getWrapperStyle(inline) { return inline ? 'display: inline-block; float: left; padding-right: 8px;' : 'display: flex'; } static getPositionControllerStyle(inline) { const width = inline ? '66px' : '100px'; return ` position: absolute; top: 0%; left: 50%; width: ${width}; height: ${CONSTANTS.CONTROLLER_HEIGHT}; z-index: 999; background-color: ${CONSTANTS.COLORS.BACKGROUND}; border-radius: 3px; border: 1px solid ${CONSTANTS.COLORS.BORDER}; cursor: pointer; transform: translate(-50%, -50%); display: flex; justify-content: space-between; align-items: center; padding: 0 6px; ` .replace(/\s+/g, ' ') .trim(); } static getDotStyle(index) { const dotPosition = utils.getDotPosition(); const dotSize = utils.getDotSize(); const positions = [ `top: ${dotPosition}; left: ${dotPosition}; cursor: nwse-resize;`, `top: ${dotPosition}; right: ${dotPosition}; cursor: nesw-resize;`, `bottom: ${dotPosition}; left: ${dotPosition}; cursor: nesw-resize;`, `bottom: ${dotPosition}; right: ${dotPosition}; cursor: nwse-resize;`, ]; return ` position: absolute; width: ${dotSize}px; height: ${dotSize}px; border: 1.5px solid ${CONSTANTS.COLORS.BORDER}; border-radius: 50%; ${positions[index]} ` .replace(/\s+/g, ' ') .trim(); } } class AttributeParser { static parseImageAttributes(nodeAttrs, imgElement) { Object.entries(nodeAttrs).forEach(([key, value]) => { if (value === undefined || value === null || key === 'wrapperStyle') return; if (key === 'containerStyle') { const width = value.match(/width:\s*([0-9.]+)px/); if (width) { imgElement.setAttribute('width', width[1]); } return; } imgElement.setAttribute(key, value); }); } static extractWidthFromStyle(style) { const width = style.match(/width:\s*([0-9.]+)px/); return width ? width[1] : null; } } class PositionController { constructor(elements, inline, dispatchNodeView) { this.elements = elements; this.inline = inline; this.dispatchNodeView = dispatchNodeView; } createControllerIcon(src) { const controller = document.createElement('img'); controller.setAttribute('src', src); controller.setAttribute('style', `width: ${CONSTANTS.ICON_SIZE}; height: ${CONSTANTS.ICON_SIZE}; cursor: pointer;`); controller.addEventListener('mouseover', (e) => { e.target.style.opacity = '0.6'; }); controller.addEventListener('mouseout', (e) => { e.target.style.opacity = '1'; }); return controller; } handleLeftClick() { if (!this.inline) { this.elements.container.setAttribute('style', `${this.elements.container.style.cssText} margin: 0 auto 0 0;`); } else { const style = 'display: inline-block; float: left; padding-right: 8px;'; this.elements.wrapper.setAttribute('style', style); this.elements.container.setAttribute('style', style); } this.dispatchNodeView(); } handleCenterClick() { this.elements.container.setAttribute('style', `${this.elements.container.style.cssText} margin: 0 auto;`); this.dispatchNodeView(); } handleRightClick() { if (!this.inline) { this.elements.container.setAttribute('style', `${this.elements.container.style.cssText} margin: 0 0 0 auto;`); } else { const style = 'display: inline-block; float: right; padding-left: 8px;'; this.elements.wrapper.setAttribute('style', style); this.elements.container.setAttribute('style', style); } this.dispatchNodeView(); } createPositionControls() { const controller = document.createElement('div'); controller.setAttribute('style', StyleManager.getPositionControllerStyle(this.inline)); const leftController = this.createControllerIcon(CONSTANTS.ICONS.LEFT); leftController.addEventListener('click', () => this.handleLeftClick()); controller.appendChild(leftController); if (!this.inline) { const centerController = this.createControllerIcon(CONSTANTS.ICONS.CENTER); centerController.addEventListener('click', () => this.handleCenterClick()); controller.appendChild(centerController); } const rightController = this.createControllerIcon(CONSTANTS.ICONS.RIGHT); rightController.addEventListener('click', () => this.handleRightClick()); controller.appendChild(rightController); this.elements.container.appendChild(controller); return this; } } class ResizeController { constructor(elements, dispatchNodeView) { this.state = { isResizing: false, startX: 0, startWidth: 0, }; this.handleMouseMove = (e, index) => { if (!this.state.isResizing) return; const deltaX = index % 2 === 0 ? -(e.clientX - this.state.startX) : e.clientX - this.state.startX; const newWidth = this.state.startWidth + deltaX; this.elements.container.style.width = newWidth + 'px'; this.elements.img.style.width = newWidth + 'px'; }; this.handleMouseUp = () => { if (this.state.isResizing) { this.state.isResizing = false; } this.dispatchNodeView(); }; this.handleTouchMove = (e, index) => { if (!this.state.isResizing) return; const deltaX = index % 2 === 0 ? -(e.touches[0].clientX - this.state.startX) : e.touches[0].clientX - this.state.startX; const newWidth = this.state.startWidth + deltaX; this.elements.container.style.width = newWidth + 'px'; this.elements.img.style.width = newWidth + 'px'; }; this.handleTouchEnd = () => { if (this.state.isResizing) { this.state.isResizing = false; } this.dispatchNodeView(); }; this.elements = elements; this.dispatchNodeView = dispatchNodeView; } createResizeHandle(index) { const dot = document.createElement('div'); dot.setAttribute('style', StyleManager.getDotStyle(index)); dot.addEventListener('mousedown', (e) => { e.preventDefault(); this.state.isResizing = true; this.state.startX = e.clientX; this.state.startWidth = this.elements.container.offsetWidth; const onMouseMove = (e) => this.handleMouseMove(e, index); const onMouseUp = () => { this.handleMouseUp(); document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }); dot.addEventListener('touchstart', (e) => { e.cancelable && e.preventDefault(); this.state.isResizing = true; this.state.startX = e.touches[0].clientX; this.state.startWidth = this.elements.container.offsetWidth; const onTouchMove = (e) => this.handleTouchMove(e, index); const onTouchEnd = () => { this.handleTouchEnd(); document.removeEventListener('touchmove', onTouchMove); document.removeEventListener('touchend', onTouchEnd); }; document.addEventListener('touchmove', onTouchMove); document.addEventListener('touchend', onTouchEnd); }, { passive: false }); return dot; } } class ImageNodeView { constructor(context, inline) { this.clearContainerBorder = () => { utils.clearContainerBorder(this.elements.container); }; this.dispatchNodeView = () => { const { view, getPos } = this.context; if (typeof getPos === 'function') { this.clearContainerBorder(); const newAttrs = Object.assign(Object.assign({}, this.context.node.attrs), { containerStyle: `${this.elements.container.style.cssText}`, wrapperStyle: `${this.elements.wrapper.style.cssText}` }); view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, newAttrs)); } }; this.removeResizeElements = () => { utils.removeResizeElements(this.elements.container); }; this.context = context; this.inline = inline; this.elements = this.createElements(); } createElements() { return { wrapper: document.createElement('div'), container: document.createElement('div'), img: document.createElement('img'), }; } setupImageAttributes() { AttributeParser.parseImageAttributes(this.context.node.attrs, this.elements.img); } setupDOMStructure() { const { wrapperStyle, containerStyle } = this.context.node.attrs; this.elements.wrapper.setAttribute('style', wrapperStyle); this.elements.wrapper.appendChild(this.elements.container); this.elements.container.setAttribute('style', containerStyle); this.elements.container.appendChild(this.elements.img); } createPositionController() { const positionController = new PositionController(this.elements, this.inline, this.dispatchNodeView); positionController.createPositionControls(); } createResizeHandler() { const resizeHandler = new ResizeController(this.elements, this.dispatchNodeView); Array.from({ length: 4 }, (_, index) => { const dot = resizeHandler.createResizeHandle(index); this.elements.container.appendChild(dot); }); } setupContainerClick() { this.elements.container.addEventListener('click', () => { var _a; const isMobile = utils.isMobile(); isMobile && ((_a = document.querySelector('.ProseMirror-focused')) === null || _a === void 0 ? void 0 : _a.blur()); this.removeResizeElements(); this.createPositionController(); this.elements.container.setAttribute('style', `position: relative; border: 1px dashed ${CONSTANTS.COLORS.BORDER}; ${this.context.node.attrs.containerStyle}`); this.createResizeHandler(); }); } setupContentClick() { document.addEventListener('click', (e) => { const target = e.target; const isClickInside = this.elements.container.contains(target) || target.style.cssText === `width: ${CONSTANTS.ICON_SIZE}; height: ${CONSTANTS.ICON_SIZE}; cursor: pointer;`; if (!isClickInside) { this.clearContainerBorder(); this.removeResizeElements(); } }); } initialize() { this.setupDOMStructure(); this.setupImageAttributes(); const { editable } = this.context.editor.options; if (!editable) return { dom: this.elements.container }; this.setupContainerClick(); this.setupContentClick(); return { dom: this.elements.wrapper, }; } } const ImageResize = Image.extend({ name: 'imageResize', addOptions() { var _a; return Object.assign(Object.assign({}, (_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)), { inline: false }); }, addAttributes() { var _a; const inline = this.options.inline; return Object.assign(Object.assign({}, (_a = this.parent) === null || _a === void 0 ? void 0 : _a.call(this)), { containerStyle: { default: StyleManager.getContainerStyle(inline), parseHTML: (element) => { const width = element.getAttribute('width'); return width ? StyleManager.getContainerStyle(inline, `${width}px`) : `${element.style.cssText}`; }, }, wrapperStyle: { default: StyleManager.getWrapperStyle(inline), } }); }, addNodeView() { return ({ node, editor, getPos }) => { const inline = this.options.inline; const context = { node, editor, view: editor.view, getPos: typeof getPos === 'function' ? getPos : undefined, }; const nodeView = new ImageNodeView(context, inline); return nodeView.initialize(); }; }, }); exports.ImageResize = ImageResize; exports.default = ImageResize; //# sourceMappingURL=index.js.map