jodit
Version:
Jodit is awesome and usefully wysiwyg editor with filebrowser
807 lines (699 loc) • 17.4 kB
text/typescript
/*!
* 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
};
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
*/
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
*/
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
*/
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');
}
}