UNPKG

jodit

Version:

Jodit is awesome and usefully wysiwyg editor with filebrowser

807 lines (699 loc) 17.4 kB
/*! * Jodit Editor (https://xdsoft.net/jodit/) * Released under MIT see LICENSE.txt in the project root for license information. * Copyright (c) 2013-2020 Valeriy Chupurnov. All rights reserved. https://xdsoft.net */ import './image-properties.less'; import autobind from 'autobind-decorator'; import { Config } from '../../../config'; import { Alert, Confirm, Dialog, Dom, Popup, Icon, Plugin } from '../../../modules'; import { css, trim, clearCenterAlign, attr, position, isArray, markOwner, isString, refs, kebabCase } from '../../../core/helpers'; import { IDialog, IFileBrowserCallBackData, IJodit, IUploaderData } from '../../../types'; import { FileSelectorWidget, TabsWidget } from '../../../modules/widget'; import { Button } from '../../../core/ui/button'; import { form, mainTab, positionTab } from './templates/'; import { watch } from '../../../core/decorators'; /** * Plug-in for image editing window * */ /** * @property{object} image Plugin {@link Image|Image}'s options * @property{boolean} image.openOnDblClick=true Open editing dialog after double click on image * @property{boolean} image.editSrc=true Show edit 'src' input * @property{boolean} image.useImageEditor=true Show crop/resize btn * @property{boolean} image.editTitle=true Show edit 'title' input * @property{boolean} image.editAlt=true Show edit 'alt' input * @property{boolean} image.editLink=true Show edit image link's options * @property{boolean} image.editSize=true Show edit image size's inputs * @property{boolean} image.editMargins=true Show edit margin inputs * @property{boolean} image.editStyle=true Show style edit input * @property{boolean} image.editClass=true Show edit classNames input * @property{boolean} image.editId=true Show edit ID input * @property{boolean} image.editAlign=true Show Alignment selector * @property{boolean} image.showPreview=true Show preview image * @property{boolean} image.selectImageAfterClose=true Select image after close dialog * @example * ```javascript * var editor = new Jodit('#editor', { * image: { * editSrc: false, * editLink: false * } * }); * ``` */ declare module '../../../config' { interface Config { image: { dialogWidth: number; openOnDblClick: boolean; editSrc: boolean; useImageEditor: boolean; editTitle: boolean; editAlt: boolean; editLink: boolean; editSize: boolean; editMargins: boolean; editBorderRadius: boolean; editClass: boolean; editStyle: boolean; editId: boolean; editAlign: boolean; showPreview: boolean; selectImageAfterClose: boolean; }; } } Config.prototype.image = { dialogWidth: 600, openOnDblClick: true, editSrc: true, useImageEditor: true, editTitle: true, editAlt: true, editLink: true, editSize: true, editBorderRadius: true, editMargins: true, editClass: true, editStyle: true, editId: true, editAlign: true, showPreview: true, selectImageAfterClose: true }; /** * Show dialog with image's options */ export class imageProperties extends Plugin { state: { image: HTMLImageElement; ratio: number; sizeIsLocked: boolean; marginIsLocked: boolean; } = { image: new Image(), get ratio(): number { return this.image.naturalWidth / this.image.naturalHeight || 1; }, sizeIsLocked: true, marginIsLocked: true }; @watch('state.marginIsLocked') onChangeMarginIsLocked(): void { if (!this.form) { return; } const { marginRight, marginBottom, marginLeft, lockMargin } = refs< HTMLInputElement >(this.form); [marginRight, marginBottom, marginLeft].forEach(elm => { attr(elm, 'disabled', this.state.marginIsLocked || null); }); lockMargin.innerHTML = Icon.get( this.state.marginIsLocked ? 'lock' : 'unlock' ); } private form!: HTMLElement; /** * Dialog for form */ private dialog!: IDialog; /** * Open dialog editing image properties * * @example * ```javascript * const editor = Jodit.makeJodit('#editor'); * img = editor.createInside.element('img'); * * img.setAttribute('src', 'images/some-image.png'); * editor.s.insertImage(img); * // open the properties of the editing window * editor.events.fire('openImageProperties', img); * ``` */ protected open(): void | false { this.makeForm(); this.j.e.fire('hidePopup'); markOwner(this.j, this.dialog.container); this.state.marginIsLocked = true; this.state.sizeIsLocked = true; this.updateValues(); this.dialog .open() .setModal(true) .setPosition(); return false; } /** * Create form for edit image properties */ private makeForm(): void { if (this.dialog) { return; } this.dialog = new Dialog({ fullsize: this.j.o.fullsize, globalFullSize: this.j.o.globalFullSize, language: this.j.o.language, buttons: ['fullsize', 'dialog.close'] }); const editor = this.j, opt = editor.o, i18n = editor.i18n.bind(editor), buttons = { check: Button(editor, 'ok', 'Apply'), remove: Button(editor, 'bin', 'Delete') }; editor.e.on(this.dialog, 'afterClose', () => { if ( this.state.image.parentNode && opt.image.selectImageAfterClose ) { editor.s.select(this.state.image); } }); buttons.remove.onAction(() => { editor.s.removeNode(this.state.image); this.dialog.close(); }); const { dialog } = this; dialog.setHeader(i18n('Image properties')); const mainForm = form(editor); this.form = mainForm; dialog.setContent(mainForm); const { tabsBox } = refs<HTMLInputElement>(this.form); if (tabsBox) { tabsBox.appendChild( TabsWidget(editor, [ { name: 'Image', content: mainTab(editor) }, { name: 'Advanced', content: positionTab(editor) } ]) ); } buttons.check.onAction(this.onApply); const { changeImage, editImage } = refs<HTMLInputElement>(this.form); editor.e.on(changeImage, 'click', this.openImagePopup); if (opt.image.useImageEditor) { editor.e.on(editImage, 'click', this.openImageEditor); } const { lockSize, lockMargin, imageWidth, imageHeight } = refs< HTMLInputElement >(mainForm); if (lockSize) { editor.e.on(lockSize, 'click', () => { this.state.sizeIsLocked = !this.state.sizeIsLocked; lockSize.innerHTML = Icon.get( this.state.sizeIsLocked ? 'lock' : 'unlock' ); editor.e.fire(imageWidth, 'change'); }); } editor.e.on(lockMargin, 'click', () => { this.state.marginIsLocked = !this.state.marginIsLocked; }); const changeSizes = (event: any): void => { const w = parseInt(imageWidth.value, 10), h = parseInt(imageHeight.value, 10); if (event.target === imageWidth) { imageHeight.value = Math.round(w / this.state.ratio).toString(); } else { imageWidth.value = Math.round(h * this.state.ratio).toString(); } }; editor.e.on( [imageWidth, imageHeight], 'change keydown mousedown paste', (event: any) => { if (!this.state.sizeIsLocked) { return; } editor.async.setTimeout(changeSizes.bind(this, event), { timeout: editor.defaultTimeout, label: 'image-properties-changeSize' }); } ); dialog.setFooter([buttons.remove, buttons.check]); dialog.setSize(this.j.o.image.dialogWidth); } /** * Set input values from image */ private updateValues(): void { const opt = this.j.o; const { image } = this.state; const { marginTop, marginRight, marginBottom, marginLeft, lockMargin, imageSrc, id, classes, align, style, imageTitle, imageAlt, borderRadius, imageLink, imageWidth, imageHeight, imageLinkOpenInNewTab, imageViewSrc, lockSize } = refs<HTMLInputElement>(this.form); const updateLock = () => { lockMargin.checked = this.state.marginIsLocked; lockSize.checked = this.state.sizeIsLocked; }, updateAlign = () => { if ( image.style.cssFloat && ['left', 'right'].indexOf( image.style.cssFloat.toLowerCase() ) !== -1 ) { align.value = css(image, 'float') as string; } else { if ( (css(image, 'display') as string) === 'block' && image.style.marginLeft === 'auto' && image.style.marginRight === 'auto' ) { align.value = 'center'; } } }, updateBorderRadius = () => { borderRadius.value = ( parseInt(image.style.borderRadius || '0', 10) || '0' ).toString(); }, updateId = () => { id.value = attr(image, 'id') || ''; }, updateStyle = () => { style.value = attr(image, 'style') || ''; }, updateClasses = () => { classes.value = (attr(image, 'class') || '').replace( /jodit_focused_image[\s]*/, '' ); }, updateMargins = () => { if (!opt.image.editMargins) { return; } let equal = true, wasEmptyField = false; [marginTop, marginRight, marginBottom, marginLeft].forEach( elm => { const id = attr(elm, 'data-ref') || ''; let value: | number | string = image.style.getPropertyValue( kebabCase(id) ); if (!value) { wasEmptyField = true; elm.value = ''; return; } if (/^[0-9]+(px)?$/.test(value)) { value = parseInt(value, 10); } elm.value = value.toString() || ''; if ( (wasEmptyField && elm.value) || (equal && id !== 'marginTop' && elm.value !== marginTop.value) ) { equal = false; } } ); this.state.marginIsLocked = equal; }, updateSizes = () => { imageWidth.value = image.offsetWidth.toString(); imageHeight.value = image.offsetHeight.toString(); }, updateText = () => { if (image.hasAttribute('title')) { imageTitle.value = attr(image, 'title') || ''; } if (image.hasAttribute('alt')) { imageAlt.value = attr(image, 'alt') || ''; } const a = Dom.closest(image, 'a', this.j.editor); if (a) { imageLink.value = attr(a, 'href') || ''; imageLinkOpenInNewTab.checked = attr(a, 'target') === '_blank'; } }, updateSrc = () => { imageSrc.value = attr(image, 'src') || ''; if (imageViewSrc) { attr(imageViewSrc, 'src', attr(image, 'src') || ''); } }; updateLock(); updateSrc(); updateText(); updateSizes(); updateMargins(); updateClasses(); updateId(); updateBorderRadius(); updateAlign(); updateStyle(); } /** * Apply form's values to image */ @autobind private onApply(): void { const { style, imageSrc, borderRadius, imageTitle, imageAlt, imageLink, imageWidth, imageHeight, marginTop, marginRight, marginBottom, marginLeft, imageLinkOpenInNewTab, align, classes, id } = refs<HTMLInputElement>(this.form); const opt = this.j.o; const { image } = this.state; // styles if (opt.image.editStyle) { attr(image, 'style', style.value || null); } // Src if (imageSrc.value) { attr(image, 'src', imageSrc.value); } else { Dom.safeRemove(image); this.dialog.close(); return; } // Border radius if (borderRadius.value !== '0' && /^[0-9]+$/.test(borderRadius.value)) { image.style.borderRadius = borderRadius.value + 'px'; } else { image.style.borderRadius = ''; } // Title attr(image, 'title', imageTitle.value || null); // Alt attr(image, 'alt', imageAlt.value || null); // Link let link = Dom.closest(image, 'a', this.j.editor); if (imageLink.value) { if (!link) { link = Dom.wrap(image, 'a', this.j); } attr(link, 'href', imageLink.value); attr( link, 'target', imageLinkOpenInNewTab.checked ? '_blank' : null ); } else { if (link && link.parentNode) { link.parentNode.replaceChild(image, link); } } const normalSize = (value: string): string => { value = trim(value); return /^[0-9]+$/.test(value) ? value + 'px' : value; }; // Size if ( imageWidth.value !== image.offsetWidth.toString() || imageHeight.value !== image.offsetHeight.toString() ) { css(image, { width: trim(imageWidth.value) ? normalSize(imageWidth.value) : null, height: trim(imageHeight.value) ? normalSize(imageHeight.value) : null }); } const margins = [marginTop, marginRight, marginBottom, marginLeft]; if (opt.image.editMargins) { if (!this.state.marginIsLocked) { margins.forEach((margin: HTMLInputElement) => { const side = attr(margin, 'data-ref') || ''; css(image, side, normalSize(margin.value)); }); } else { css(image, 'margin', normalSize(marginTop.value)); } } if (opt.image.editClass) { attr(image, 'class', classes.value || null); } if (opt.image.editId) { attr(image, 'id', id.value || null); } if (opt.image.editAlign) { if (align.value) { if (['right', 'left'].includes(align.value.toLowerCase())) { css(image, 'float', align.value); clearCenterAlign(image); } else { css(image, { float: '', display: 'block', marginLeft: 'auto', marginRight: 'auto' }); } } else { if ( css(image, 'float') && ['right', 'left'].indexOf( css(image, 'float') .toString() .toLowerCase() ) !== -1 ) { css(image, 'float', ''); } clearCenterAlign(image); } } this.j.setEditorValue(); this.dialog.close(); } /** * Open image editor dialog */ @autobind private openImageEditor(): void { const url: string = attr(this.state.image, 'src') || '', a = this.j.c.element('a'), loadExternal = () => { if (a.host !== location.host) { Confirm( this.j.i18n( 'You can only edit your own images. Download this image on the host?' ), (yes: boolean) => { if (yes && this.j.uploader) { this.j.uploader.uploadRemoteImage( a.href.toString(), (resp: IUploaderData) => { Alert( this.j.i18n( 'The image has been successfully uploaded to the host!' ), () => { if ( isString(resp.newfilename) ) { attr( this.state.image, 'src', resp.baseurl + resp.newfilename ); this.updateValues(); } } ).bindDestruct(this.j); }, (error: Error) => { Alert( this.j.i18n( 'There was an error loading %s', error.message ) ).bindDestruct(this.j); } ); } } ).bindDestruct(this.j); return; } }; a.href = url; this.j.filebrowser.dataProvider.getPathByUrl( a.href.toString(), (path: string, name: string, source: string) => { this.j.filebrowser.openImageEditor( a.href, name, path, source, () => { const timestamp: number = new Date().getTime(); attr( this.state.image, 'src', url + (url.indexOf('?') !== -1 ? '' : '?') + '&_tmp=' + timestamp.toString() ); this.updateValues(); }, (error: Error) => { Alert(error.message).bindDestruct(this.j); } ); }, (error: Error) => { Alert(error.message, loadExternal).bindDestruct(this.j); } ); } /** * Open popup with filebrowser/uploader buttons for image * @param event */ @autobind private openImagePopup(event: MouseEvent): void { const popup = new Popup(this.j), { changeImage } = refs(this.form); popup .setContent( FileSelectorWidget( this.j, { upload: (data: IFileBrowserCallBackData) => { if (data.files && data.files.length) { attr( this.state.image, 'src', data.baseurl + data.files[0] ); } this.updateValues(); popup.close(); }, filebrowser: (data: IFileBrowserCallBackData) => { if ( data && isArray(data.files) && data.files.length ) { attr(this.state.image, 'src', data.files[0]); popup.close(); this.updateValues(); } } }, this.state.image, popup.close ) ) .open(() => position(changeImage)); event.stopPropagation(); } /** @override **/ protected afterInit(editor: IJodit): void { const self = this; editor.e .on('afterConstructor changePlace', () => { editor.e .off(editor.editor, '.imageproperties') .on( editor.editor, 'dblclick.imageproperties', (e: MouseEvent) => { const image = e.target; if (!Dom.isTag(image, 'img')) { return; } if (editor.o.image.openOnDblClick) { self.state.image = image; if (!editor.o.readonly) { e.stopImmediatePropagation(); e.preventDefault(); self.open(); } } else { e.stopImmediatePropagation(); editor.s.select(image); } } ); }) .on( 'openImageProperties.imageproperties', (image: HTMLImageElement) => { this.state.image = image; this.open(); } ); } /** @override */ protected beforeDestruct(editor: IJodit): void { this.dialog && this.dialog.destruct(); editor.e.off(editor.editor, '.imageproperties').off('.imageproperties'); } }