diff --git a/package.json b/package.json index 770d0dc8..913ef3c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-design-editor", - "version": "0.0.56", + "version": "0.0.57", "description": "Design Editor Tools with React.js + ant.design + fabric.js", "main": "dist/react-design-editor.min.js", "typings": "lib/index.d.ts", diff --git a/public/images/sample/chiller.svg b/public/images/sample/chiller.svg new file mode 100644 index 00000000..8494eb77 --- /dev/null +++ b/public/images/sample/chiller.svg @@ -0,0 +1,430 @@ +Copyright Opto 22 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/canvas/handlers/Handler.ts b/src/canvas/handlers/Handler.ts index 70675c1b..947c19ba 100644 --- a/src/canvas/handlers/Handler.ts +++ b/src/canvas/handlers/Handler.ts @@ -30,6 +30,7 @@ import { defaults } from '../constants'; import { LinkObject } from '../objects/Link'; import { NodeObject } from '../objects/Node'; import { PortObject } from '../objects/Port'; +import { SvgObject } from '../objects/Svg'; import { CanvasOption, FabricCanvas, @@ -616,17 +617,36 @@ class Handler implements HandlerOptions { * Set the image * @param {FabricImage} obj * @param {(File | string)} [source] + * @param {boolean} [keepSize] * @returns */ - public setImage = (obj: FabricImage, source?: File | string): Promise => { + public setImage = ( + obj: FabricImage, + source?: File | string, + keepSize?: boolean, + options?: fabric.IImageOptions, + ): Promise => { + const { height, scaleY } = obj; + const renderCallbaack = (imgObj: FabricImage, src: string) => { + if (keepSize) { + const scale = (height * scaleY) / imgObj.height; + imgObj.set({ scaleY: scale, scaleX: scale, src }); + } + this.canvas.requestRenderAll(); + }; return new Promise(resolve => { if (!source) { obj.set('file', null); obj.set('src', null); resolve( - obj.setSrc('./images/sample/transparentBg.png', () => this.canvas.renderAll(), { - dirty: true, - }) as FabricImage, + obj.setSrc( + './images/sample/transparentBg.png', + (imgObj: FabricImage) => renderCallbaack(imgObj, null), + { + dirty: true, + ...options, + }, + ) as FabricImage, ); } if (source instanceof File) { @@ -635,9 +655,14 @@ class Handler implements HandlerOptions { obj.set('file', source); obj.set('src', null); resolve( - obj.setSrc(reader.result as string, () => this.canvas.renderAll(), { - dirty: true, - }) as FabricImage, + obj.setSrc( + reader.result as string, + (imgObj: FabricImage) => renderCallbaack(imgObj, reader.result as string), + { + dirty: true, + ...options, + }, + ) as FabricImage, ); }; reader.readAsDataURL(source); @@ -645,9 +670,10 @@ class Handler implements HandlerOptions { obj.set('file', null); obj.set('src', source); resolve( - obj.setSrc(source, () => this.canvas.renderAll(), { + obj.setSrc(source, (imgObj: FabricImage) => renderCallbaack(imgObj, source), { dirty: true, crossOrigin: 'anonymous', + ...options, }) as FabricImage, ); } @@ -660,9 +686,38 @@ class Handler implements HandlerOptions { * @param {*} source * @returns */ - public setImageById = (id: string, source: any) => { + public setImageById = (id: string, source: any, keepSize?: boolean) => { const findObject = this.findById(id) as FabricImage; - return Promise.resolve(this.setImage(findObject, source)); + return Promise.resolve(this.setImage(findObject, source, keepSize)); + }; + + /** + * Set Svg + * + * @param {SvgObject} obj + * @param {(File | string)} [source] + * @param {boolean} [setSvg] + * @param {boolean} [keepSize] + */ + public setSvg = ( + obj: SvgObject, + source?: File | string, + isPath?: boolean, + keepSize?: boolean, + ): Promise => { + return new Promise(resolve => { + if (!source) { + resolve(obj.loadSvg({ src: './images/sample/chiller.svg', loadType: 'file', keepSize })); + } + if (source instanceof File) { + const reader = new FileReader(); + reader.readAsDataURL(source); + reader.onload = () => + resolve(obj.loadSvg({ src: reader.result as string, loadType: 'file', keepSize })); + } else { + resolve(obj.loadSvg({ src: source, loadType: isPath ? 'svg' : 'file', keepSize })); + } + }); }; /** diff --git a/src/canvas/objects/Svg.ts b/src/canvas/objects/Svg.ts index 6293d90b..642e11ef 100644 --- a/src/canvas/objects/Svg.ts +++ b/src/canvas/objects/Svg.ts @@ -3,13 +3,14 @@ import { FabricGroup, FabricObject, FabricObjectOption, toObject } from '../util export type SvgObject = (FabricGroup | FabricObject) & { loadSvg(option: SvgOption): Promise; - setFill(value: string): SvgObject; - setStroke(value: string): SvgObject; + setFill(value: string, filter?: (obj: FabricObject) => boolean): SvgObject; + setStroke(value: string, filter?: (obj: FabricObject) => boolean): SvgObject; }; export interface SvgOption extends FabricObjectOption { - svg?: string; + src?: string; loadType?: 'file' | 'svg'; + keepSize?: boolean; } const Svg = fabric.util.createClass(fabric.Group, { @@ -18,9 +19,16 @@ const Svg = fabric.util.createClass(fabric.Group, { this.callSuper('initialize', [], option); this.loadSvg(option); }, - addSvgElements(objects: FabricObject[], options: any, path: string) { - const createdObj = fabric.util.groupSVGElements(objects, options, path) as SvgObject; - this.set(options); + addSvgElements(objects: FabricObject[], options: SvgOption) { + const createdObj = fabric.util.groupSVGElements(objects, options) as SvgObject; + const { height, scaleY } = this; + const scale = height ? (height * scaleY) / createdObj.height : createdObj.scaleY; + this.set({ ...options, scaleX: scale, scaleY: scale }); + if (this._objects?.length) { + (this as FabricGroup).getObjects().forEach(obj => { + this.remove(obj); + }); + } if (createdObj.getObjects) { (createdObj as FabricGroup).getObjects().forEach(obj => { this.add(obj); @@ -51,10 +59,6 @@ const Svg = fabric.util.createClass(fabric.Group, { } this.add(createdObj); } - this.set({ - fill: options.fill, - stroke: options.stroke, - }); this.setCoords(); if (this.canvas) { this.canvas.requestRenderAll(); @@ -62,25 +66,31 @@ const Svg = fabric.util.createClass(fabric.Group, { return this; }, loadSvg(option: SvgOption) { - const { svg, loadType, fill, stroke } = option; + const { src, loadType, fill, stroke } = option; return new Promise(resolve => { if (loadType === 'svg') { - fabric.loadSVGFromString(svg, (objects, options) => { - resolve(this.addSvgElements(objects, { ...options, fill, stroke }, svg)); + fabric.loadSVGFromString(src, (objects, options) => { + resolve(this.addSvgElements(objects, { ...options, fill, stroke })); }); } else { - fabric.loadSVGFromURL(svg, (objects, options) => { - resolve(this.addSvgElements(objects, { ...options, fill, stroke }, svg)); + fabric.loadSVGFromURL(src, (objects, options) => { + resolve(this.addSvgElements(objects, { ...options, fill, stroke })); }); } }); }, - setFill(value: any) { - this.getObjects().forEach((obj: FabricObject) => obj.set('fill', value)); + setFill(value: any, filter: (obj: FabricObject) => boolean = () => true) { + this.getObjects() + .filter(filter) + .forEach((obj: FabricObject) => obj.set('fill', value)); + this.canvas.requestRenderAll(); return this; }, - setStroke(value: any) { - this.getObjects().forEach((obj: FabricObject) => obj.set('stroke', value)); + setStroke(value: any, filter: (obj: FabricObject) => boolean = () => true) { + this.getObjects() + .filter(filter) + .forEach((obj: FabricObject) => obj.set('stroke', value)); + this.canvas.requestRenderAll(); return this; }, toObject(propertiesToInclude: string[]) { diff --git a/src/editors/imagemap/Descriptors.json b/src/editors/imagemap/Descriptors.json index b07f0ef0..c5dfdb6f 100644 --- a/src/editors/imagemap/Descriptors.json +++ b/src/editors/imagemap/Descriptors.json @@ -259,7 +259,8 @@ "type": "svg", "superType": "svg", "name": "New SVG", - "loadType": "svg" + "loadType": "file", + "src": "./images/sample/chiller.svg" } } ] diff --git a/src/editors/imagemap/ImageMapEditor.js b/src/editors/imagemap/ImageMapEditor.js index b79615cc..f49bc7a9 100644 --- a/src/editors/imagemap/ImageMapEditor.js +++ b/src/editors/imagemap/ImageMapEditor.js @@ -192,11 +192,13 @@ class ImageMapEditor extends Component { }); return; } - if (changedKey === 'file' || changedKey === 'src' || changedKey === 'code') { + if (changedKey === 'file' || changedKey === 'src' || changedKey === 'code' || changedKey === 'svg') { if (selectedItem.type === 'image') { - this.canvasRef.handler.setImageById(selectedItem.id, changedValue); + this.canvasRef.handler.setImageById(selectedItem.id, changedValue, true); } else if (selectedItem.superType === 'element') { this.canvasRef.handler.elementHandler.setById(selectedItem.id, changedValue); + } else if (selectedItem.superType === 'svg') { + this.canvasRef.handler.setSvg(selectedItem, changedValue); } return; } @@ -331,7 +333,11 @@ class ImageMapEditor extends Component { } return; } - this.canvasRef.handler.set(changedKey, changedValue); + if (selectedItem.type === 'svg' && changedKey === 'fill') { + selectedItem.setFill(changedValue); + } else { + this.canvasRef.handler.set(changedKey, changedValue); + } }, onChangeWokarea: (changedKey, changedValue, allValues) => { if (changedKey === 'layout') { diff --git a/src/editors/imagemap/ImageMapItems.js b/src/editors/imagemap/ImageMapItems.js index 1133fe60..988d2c9b 100644 --- a/src/editors/imagemap/ImageMapItems.js +++ b/src/editors/imagemap/ImageMapItems.js @@ -1,15 +1,14 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Collapse, notification, Input, message } from 'antd'; +import { Collapse, Input, message, notification } from 'antd'; import classnames from 'classnames'; import i18n from 'i18next'; +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { uuid } from 'uuidv4'; +import CommonButton from '../../components/common/CommonButton'; +import Scrollbar from '../../components/common/Scrollbar'; import { Flex } from '../../components/flex'; import Icon from '../../components/icon/Icon'; -import Scrollbar from '../../components/common/Scrollbar'; -import CommonButton from '../../components/common/CommonButton'; -import { SVGModal } from '../../components/common'; -import { uuid } from 'uuidv4'; notification.config({ top: 80, @@ -104,17 +103,8 @@ class ImageMapItems extends Component { } const id = uuid(); const option = Object.assign({}, item.option, { id }); - if (item.option.superType === 'svg' && item.type === 'default') { - this.handlers.onSVGModalVisible(item.option); - return; - } canvasRef.handler.add(option, centered); }, - onAddSVG: (option, centered) => { - const { canvasRef } = this.props; - canvasRef.handler.add({ ...option, type: 'svg', superType: 'svg', id: uuid(), name: 'New SVG' }, centered); - this.handlers.onSVGModalVisible(); - }, onDrawingItem: item => { const { canvasRef } = this.props; if (canvasRef.handler.interactionMode === 'polygon') { @@ -318,12 +308,6 @@ class ImageMapItems extends Component { - ); } diff --git a/src/editors/imagemap/properties/PropertyDefinition.js b/src/editors/imagemap/properties/PropertyDefinition.js index ee3ff065..9c9c3d26 100644 --- a/src/editors/imagemap/properties/PropertyDefinition.js +++ b/src/editors/imagemap/properties/PropertyDefinition.js @@ -1,20 +1,21 @@ -import MarkerProperty from './MarkerProperty'; +import AnimationProperty from './AnimationProperty'; +import ChartProperty from './ChartProperty'; +import ElementProperty from './ElementProperty'; import GeneralProperty from './GeneralProperty'; -import StyleProperty from './StyleProperty'; -import TooltipProperty from './TooltipProperty'; +import IframeProperty from './IframeProperty'; +import ImageFilterProperty from './ImageFilterProperty'; import ImageProperty from './ImageProperty'; -import TextProperty from './TextProperty'; -import MapProperty from './MapProperty'; import LinkProperty from './LinkProperty'; -import VideoProperty from './VideoProperty'; -import ElementProperty from './ElementProperty'; -import IframeProperty from './IframeProperty'; -import AnimationProperty from './AnimationProperty'; +import MapProperty from './MapProperty'; +import MarkerProperty from './MarkerProperty'; import ShadowProperty from './ShadowProperty'; -import UserProperty from './UserProperty'; +import StyleProperty from './StyleProperty'; +import SvgProperty from './SvgProperty'; +import TextProperty from './TextProperty'; +import TooltipProperty from './TooltipProperty'; import TriggerProperty from './TriggerProperty'; -import ImageFilterProperty from './ImageFilterProperty'; -import ChartProperty from './ChartProperty'; +import UserProperty from './UserProperty'; +import VideoProperty from './VideoProperty'; export default { map: { @@ -394,6 +395,10 @@ export default { title: 'General', component: GeneralProperty, }, + svg: { + title: 'SVG', + component: SvgProperty, + }, link: { title: 'Link', component: LinkProperty, diff --git a/src/editors/imagemap/properties/SvgProperty.js b/src/editors/imagemap/properties/SvgProperty.js new file mode 100644 index 00000000..9ca850a5 --- /dev/null +++ b/src/editors/imagemap/properties/SvgProperty.js @@ -0,0 +1,40 @@ +import { Form, Radio } from 'antd'; +import i18n from 'i18next'; +import React from 'react'; + +import FileUpload from '../../../components/common/FileUpload'; + +export default { + render(canvasRef, form, data) { + if (!data) { + return null; + } + const loadType = data.loadType || 'file'; + return ( + + + {form.getFieldDecorator('loadType', { + initialValue: loadType, + })( + + {i18n.t('common.file')} + {i18n.t('common.svg')} + , + )} + + + {form.getFieldDecorator('src', { + rules: [ + { + required: true, + message: i18n.t('validation.enter-property', { + arg: loadType === 'svg' ? i18n.t('common.svg') : i18n.t('common.file'), + }), + }, + ], + })(loadType === 'svg' ? : )} + + + ); + }, +};