UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

578 lines (577 loc) 22.6 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Released under MIT see LICENSE.txt in the project root for license information. * Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var ImageEditor_1; import { ViewComponent } from "../../core/component/index.js"; import { autobind, component, debounce, throttle } from "../../core/decorators/index.js"; import { Dom } from "../../core/dom/index.js"; import { $$, attr, call, css, refs, toArray, trim } from "../../core/helpers/index.js"; import { Button } from "../../core/ui/button/index.js"; import { Config } from "../../config.js"; import "./config.js"; import { form } from "./templates/form.js"; const jie = 'jodit-image-editor'; const TABS = { resize: 'resize', crop: 'crop' }; /** * The module allows you to edit the image: resize or cut any part of it * */ let ImageEditor = ImageEditor_1 = class ImageEditor extends ViewComponent { /** @override */ className() { return 'ImageEditor'; } get o() { return this.options; } /** * Hide image editor */ hide() { this._dialog.close(); } /** * Open image editor * @example * ```javascript * const jodit = Jodit.make('.editor', { * imageeditor: { * crop: false, * closeAfterSave: true, * width: 500 * } * }); * jodit.imageeditor.open('https://xdsoft.net/jodit/images/test.png', function (name, data, success, failed) { * var img = jodit.node.c('img'); * img.setAttribute('src', 'https://xdsoft.net/jodit/images/test.png'); * if (box.action !== 'resize') { * return failed('Sorry it is work only in resize mode. For croping use FileBrowser'); * } * img.style.width = data.w; * img.style.height = data.h; * jodit.s.insertNode(img); * success(); * }); * ``` */ open(url, save) { return this.j.async.promise((resolve) => { const timestamp = new Date().getTime(); this.image = this.j.c.element('img'); $$('img,.jodit-icon_loader', this.resize_box).forEach(Dom.safeRemove); $$('img,.jodit-icon_loader', this.crop_box).forEach(Dom.safeRemove); css(this.cropHandler, 'background', 'transparent'); this.onSave = save; this.resize_box.appendChild(this.j.c.element('i', { class: 'jodit-icon_loader' })); this.crop_box.appendChild(this.j.c.element('i', { class: 'jodit-icon_loader' })); if (/\?/.test(url)) { url += '&_tst=' + timestamp; } else { url += '?_tst=' + timestamp; } this.image.setAttribute('src', url); this._dialog.open(); const { widthInput, heightInput } = refs(this.editor); const onload = () => { if (this.isDestructed) { return; } this.image.removeEventListener('load', onload); this.naturalWidth = this.image.naturalWidth; this.naturalHeight = this.image.naturalHeight; widthInput.value = this.naturalWidth.toString(); heightInput.value = this.naturalHeight.toString(); this.ratio = this.naturalWidth / this.naturalHeight; this.resize_box.appendChild(this.image); this.cropImage = this.image.cloneNode(true); this.crop_box.appendChild(this.cropImage); Dom.safeRemove.apply(null, $$('.jodit-icon_loader', this.editor)); if (this.activeTab === TABS.crop) { this.showCrop(); } this.j.e.fire(this.resizeHandler, 'updatesize'); this.j.e.fire(this.cropHandler, 'updatesize'); this._dialog.setPosition(); this.j.e.fire('afterImageEditor'); resolve(this._dialog); }; this.image.addEventListener('load', onload); if (this.image.complete) { onload(); } }); } onTitleModeClick(e) { const self = this, title = e.target; const slide = title === null || title === void 0 ? void 0 : title.parentElement; if (!slide) { return; } $$(`.${jie}__slider,.${jie}__area`, self.editor).forEach(elm => elm.classList.remove(`${jie}_active`)); slide.classList.add(`${jie}_active`); this.activeTab = attr(slide, '-area') || TABS.resize; const tab = self.editor.querySelector(`.${jie}__area.${jie}__area_` + self.activeTab); if (tab) { tab.classList.add(`${jie}_active`); } if (self.activeTab === TABS.crop) { self.showCrop(); } } onChangeSizeInput(e) { const self = this, input = e.target, { widthInput, heightInput } = refs(this.editor), isWidth = attr(input, 'data-ref') === 'widthInput', x = parseInt(input.value, 10), minX = isWidth ? self.o.min_width : self.o.min_height, minY = !isWidth ? self.o.min_width : self.o.min_height; let y; if (x > minX) { css(self.image, isWidth ? 'width' : 'height', x); if (self.resizeUseRatio) { y = isWidth ? Math.round(x / self.ratio) : Math.round(x * self.ratio); if (y > minY) { css(self.image, !isWidth ? 'width' : 'height', y); if (isWidth) { heightInput.value = y.toString(); } else { widthInput.value = y.toString(); } } } } this.j.e.fire(self.resizeHandler, 'updatesize'); } onResizeHandleMouseDown(e) { const self = this; self.target = e.target; e.preventDefault(); e.stopImmediatePropagation(); self.clicked = true; self.start_x = e.clientX; self.start_y = e.clientY; if (self.activeTab === TABS.crop) { self.top_x = css(self.cropHandler, 'left'); self.top_y = css(self.cropHandler, 'top'); self.width = self.cropHandler.offsetWidth; self.height = self.cropHandler.offsetHeight; } else { self.width = self.image.offsetWidth; self.height = self.image.offsetHeight; } self.j.e .on(this.j.ow, 'mousemove', this.onGlobalMouseMove) .one(this.j.ow, 'mouseup', this.onGlobalMouseUp); } onGlobalMouseUp(e) { if (this.clicked) { this.clicked = false; e.stopImmediatePropagation(); this.j.e.off(this.j.ow, 'mousemove', this.onGlobalMouseMove); } } onGlobalMouseMove(e) { const self = this; if (!self.clicked) { return; } const { widthInput, heightInput } = refs(this.editor); self.diff_x = e.clientX - self.start_x; self.diff_y = e.clientY - self.start_y; if ((self.activeTab === TABS.resize && self.resizeUseRatio) || (self.activeTab === TABS.crop && self.cropUseRatio)) { if (self.diff_x) { self.new_w = self.width + self.diff_x; self.new_h = Math.round(self.new_w / self.ratio); } else { self.new_h = self.height + self.diff_y; self.new_w = Math.round(self.new_h * self.ratio); } } else { self.new_w = self.width + self.diff_x; self.new_h = self.height + self.diff_y; } if (self.activeTab === TABS.resize) { if (self.new_w > self.o.resizeMinWidth) { css(self.image, 'width', self.new_w + 'px'); widthInput.value = self.new_w.toString(); } if (self.new_h > self.o.resizeMinHeight) { css(self.image, 'height', self.new_h + 'px'); heightInput.value = self.new_h.toString(); } this.j.e.fire(self.resizeHandler, 'updatesize'); } else { if (self.target !== self.cropHandler) { if (self.top_x + self.new_w > self.cropImage.offsetWidth) { self.new_w = self.cropImage.offsetWidth - self.top_x; } if (self.top_y + self.new_h > self.cropImage.offsetHeight) { self.new_h = self.cropImage.offsetHeight - self.top_y; } css(self.cropHandler, { width: self.new_w, height: self.new_h }); } else { if (self.top_x + self.diff_x + self.cropHandler.offsetWidth > self.cropImage.offsetWidth) { self.diff_x = self.cropImage.offsetWidth - self.top_x - self.cropHandler.offsetWidth; } css(self.cropHandler, 'left', self.top_x + self.diff_x); if (self.top_y + self.diff_y + self.cropHandler.offsetHeight > self.cropImage.offsetHeight) { self.diff_y = self.cropImage.offsetHeight - self.top_y - self.cropHandler.offsetHeight; } css(self.cropHandler, 'top', self.top_y + self.diff_y); } this.j.e.fire(self.cropHandler, 'updatesize'); } } constructor(editor) { super(editor); this.resizeUseRatio = true; this.cropUseRatio = true; this.clicked = false; this.start_x = 0; this.start_y = 0; this.top_x = 0; this.top_y = 0; this.width = 0; this.height = 0; this.activeTab = TABS.resize; this.naturalWidth = 0; this.naturalHeight = 0; this.ratio = 0; this.new_h = 0; this.new_w = 0; this.diff_x = 0; this.diff_y = 0; this.cropBox = { x: 0, y: 0, w: 0, h: 0 }; this.resizeBox = { w: 0, h: 0 }; this.calcCropBox = () => { const node = this.crop_box.parentNode, w = node.offsetWidth * 0.8, h = node.offsetHeight * 0.8; let wn = w, hn = h; const { naturalWidth: nw, naturalHeight: nh } = this; if (w > nw && h > nh) { wn = nw; hn = nh; } else if (this.ratio > w / h) { wn = w; hn = nh * (w / nw); } else { wn = nw * (h / nh); hn = h; } css(this.crop_box, { width: wn, height: hn }); }; this.showCrop = () => { if (!this.cropImage) { return; } this.calcCropBox(); const w = this.cropImage.offsetWidth || this.image.offsetWidth || this.image.naturalWidth; this.new_w = ImageEditor_1.calcValueByPercent(w, this.o.cropDefaultWidth); const h = this.cropImage.offsetHeight || this.image.offsetHeight || this.image.naturalHeight; if (this.cropUseRatio) { this.new_h = this.new_w / this.ratio; } else { this.new_h = ImageEditor_1.calcValueByPercent(h, this.o.cropDefaultHeight); } css(this.cropHandler, { backgroundImage: 'url(' + attr(this.cropImage, 'src') + ')', width: this.new_w, height: this.new_h, left: w / 2 - this.new_w / 2, top: h / 2 - this.new_h / 2 }); this.j.e.fire(this.cropHandler, 'updatesize'); }; this.updateCropBox = () => { if (!this.cropImage) { return; } const ratioX = this.cropImage.offsetWidth / this.naturalWidth, ratioY = this.cropImage.offsetHeight / this.naturalHeight; this.cropBox.x = css(this.cropHandler, 'left') / ratioX; this.cropBox.y = css(this.cropHandler, 'top') / ratioY; this.cropBox.w = this.cropHandler.offsetWidth / ratioX; this.cropBox.h = this.cropHandler.offsetHeight / ratioY; this.sizes.textContent = this.cropBox.w.toFixed(0) + 'x' + this.cropBox.h.toFixed(0); }; this.updateResizeBox = () => { this.resizeBox.w = this.image.offsetWidth || this.naturalWidth; this.resizeBox.h = this.image.offsetHeight || this.naturalHeight; }; this.setHandlers = () => { const self = this; const { widthInput, heightInput } = refs(this.editor); self.j.e .on([ self.editor.querySelector('.jodit_bottomright'), self.cropHandler ], `mousedown.${jie}`, this.onResizeHandleMouseDown) .on(this.j.ow, `resize.${jie}`, () => { this.j.e.fire(self.resizeHandler, 'updatesize'); self.showCrop(); this.j.e.fire(self.cropHandler, 'updatesize'); }); self.j.e .on(toArray(this.editor.querySelectorAll(`.${jie}__slider-title`)), 'click', this.onTitleModeClick) .on([widthInput, heightInput], 'input', this.onChangeSizeInput); const { keepAspectRatioResize, keepAspectRatioCrop } = refs(this.editor); if (keepAspectRatioResize) { keepAspectRatioResize.addEventListener('change', () => { this.resizeUseRatio = keepAspectRatioResize.checked; }); } if (keepAspectRatioCrop) { keepAspectRatioCrop.addEventListener('change', () => { this.cropUseRatio = keepAspectRatioCrop.checked; }); } self.j.e .on(self.resizeHandler, 'updatesize', () => { css(self.resizeHandler, { top: 0, left: 0, width: self.image.offsetWidth || self.naturalWidth, height: self.image.offsetHeight || self.naturalHeight }); this.updateResizeBox(); }) .on(self.cropHandler, 'updatesize', () => { if (!self.cropImage) { return; } let new_x = css(self.cropHandler, 'left'), new_y = css(self.cropHandler, 'top'), new_width = self.cropHandler.offsetWidth, new_height = self.cropHandler.offsetHeight; if (new_x < 0) { new_x = 0; } if (new_y < 0) { new_y = 0; } if (new_x + new_width > self.cropImage.offsetWidth) { new_width = self.cropImage.offsetWidth - new_x; if (self.cropUseRatio) { new_height = new_width / self.ratio; } } if (new_y + new_height > self.cropImage.offsetHeight) { new_height = self.cropImage.offsetHeight - new_y; if (self.cropUseRatio) { new_width = new_height * self.ratio; } } css(self.cropHandler, { width: new_width, height: new_height, left: new_x, top: new_y, backgroundPosition: -new_x - 1 + 'px ' + (-new_y - 1) + 'px', backgroundSize: self.cropImage.offsetWidth + 'px ' + self.cropImage.offsetHeight + 'px' }); self.updateCropBox(); }); Object.values(self.buttons).forEach(button => { button.onAction(() => { const data = { action: self.activeTab, box: self.activeTab === TABS.resize ? self.resizeBox : self.cropBox }; switch (button) { case self.buttons.saveas: self.j.prompt('Enter new name', 'Save in new file', (name) => { if (!trim(name)) { self.j.alert('The name should not be empty'); return false; } self.onSave(name, data, self.hide, (e) => { self.j.alert(e.message); }); }); break; case self.buttons.save: self.onSave(undefined, data, self.hide, (e) => { self.j.alert(e.message); }); break; case self.buttons.reset: if (self.activeTab === TABS.resize) { css(self.image, { width: null, height: null }); widthInput.value = self.naturalWidth.toString(); heightInput.value = self.naturalHeight.toString(); self.j.e.fire(self.resizeHandler, 'updatesize'); } else { self.showCrop(); } break; } }); }); }; this.options = editor && editor.o && editor.o.imageeditor ? editor.o.imageeditor : Config.defaultOptions.imageeditor; const o = this.options; this.resizeUseRatio = o.resizeUseRatio; this.cropUseRatio = o.cropUseRatio; this.buttons = { reset: Button(this.j, 'update', 'Reset'), save: Button(this.j, 'save', 'Save'), saveas: Button(this.j, 'save', 'Save as ...') }; this.activeTab = o.resize ? TABS.resize : TABS.crop; this.editor = form(this.j, this.options); const { resizeBox, cropBox } = refs(this.editor); this.resize_box = resizeBox; this.crop_box = cropBox; this.sizes = this.editor.querySelector(`.${jie}__area.${jie}__area_crop .jodit-image-editor__sizes`); this.resizeHandler = this.editor.querySelector(`.${jie}__resizer`); this.cropHandler = this.editor.querySelector(`.${jie}__croper`); this._dialog = this.j.dlg({ buttons: ['fullsize', 'dialog.close'] }); this._dialog.setContent(this.editor); this._dialog.setSize(this.o.width, this.o.height); this._dialog.setHeader([ this.buttons.reset, this.buttons.save, this.buttons.saveas ]); this.setHandlers(); } /** @override */ destruct() { if (this.isDestructed) { return; } if (this._dialog && !this._dialog.isInDestruct) { this._dialog.destruct(); } Dom.safeRemove(this.editor); if (this.j.e) { this.j.e .off(this.j.ow, 'mousemove', this.onGlobalMouseMove) .off(this.j.ow, 'mouseup', this.onGlobalMouseUp) .off(this.ow, `.${jie}`) .off(`.${jie}`); } super.destruct(); } }; ImageEditor.calcValueByPercent = (value, percent) => { const percentStr = percent.toString(); const valueNbr = parseFloat(value.toString()); let match; match = /^[-+]?[0-9]+(px)?$/.exec(percentStr); if (match) { return parseInt(percentStr, 10); } match = /^([-+]?[0-9.]+)%$/.exec(percentStr); if (match) { return Math.round(valueNbr * (parseFloat(match[1]) / 100)); } return valueNbr || 0; }; __decorate([ autobind ], ImageEditor.prototype, "hide", null); __decorate([ autobind ], ImageEditor.prototype, "open", null); __decorate([ autobind ], ImageEditor.prototype, "onTitleModeClick", null); __decorate([ debounce(), autobind ], ImageEditor.prototype, "onChangeSizeInput", null); __decorate([ autobind ], ImageEditor.prototype, "onResizeHandleMouseDown", null); __decorate([ autobind ], ImageEditor.prototype, "onGlobalMouseUp", null); __decorate([ throttle(10) ], ImageEditor.prototype, "onGlobalMouseMove", null); ImageEditor = ImageEditor_1 = __decorate([ component ], ImageEditor); export { ImageEditor }; /** * Open Image Editor */ export function openImageEditor(href, name, path, source, onSuccess, onFailed) { return this.getInstance('ImageEditor', this.o).open(href, (newname, box, success, failed) => call(box.action === 'resize' ? this.dataProvider.resize : this.dataProvider.crop, path, source, name, newname, box.box) .then(ok => { if (ok) { success(); if (onSuccess) { onSuccess(); } } }) .catch(error => { failed(error); if (onFailed) { onFailed(error); } })); }