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
JavaScript
'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