131 |
132 |
133 |
134 |
135 | {/* below input is intentionnally kept off the visible window and is used for text edition */} 136 | 137 | 138 | ) 139 | } 140 | 141 | Canvas.propTypes = { 142 | svgContent: PropTypes.string, 143 | locale: PropTypes.string.isRequired, 144 | svgUpdate: PropTypes.func.isRequired, 145 | onClose: PropTypes.func.isRequired, 146 | log: PropTypes.func.isRequired, 147 | } 148 | 149 | Canvas.defaultProps = { svgContent: '' } 150 | 151 | const CanvasWithContext = (props) => () 152 | export default CanvasWithContext 153 | -------------------------------------------------------------------------------- /src/Canvas/ColorButton/ColorButton.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 2 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 3 | // General imports 4 | import PropTypes from 'prop-types' 5 | import React from 'react' 6 | import colorString from 'color-string' 7 | import { SketchPicker } from 'react-color' 8 | import Icon from '../Icon/Icon.jsx' 9 | import './ColorButton.less' 10 | 11 | const ColorButton = ({ onChange, value, title }) => { 12 | const [display, setDisplay] = React.useState(false) 13 | const handleClick = () => setDisplay(!display) 14 | const onChangeComplete = (color) => { 15 | onChange(color?.hex) 16 | setDisplay(false) 17 | } 18 | 19 | const rgb = colorString.get.rgb(value) || [255, 255, 255] // or white 20 | return ( 21 |
22 | {display && rgb && ( 23 | 28 | )} 29 | 30 |
35 |
36 | ) 37 | } 38 | 39 | // Properties restrictions 40 | ColorButton.propTypes = { 41 | value: PropTypes.string, 42 | onChange: PropTypes.func.isRequired, 43 | title: PropTypes.string, 44 | } 45 | 46 | ColorButton.defaultProps = { value: '', title: '' } 47 | 48 | export default ColorButton 49 | -------------------------------------------------------------------------------- /src/Canvas/ColorButton/ColorButton.less: -------------------------------------------------------------------------------- 1 | div.OIe-tools-color { 2 | font-size: 0.5em; 3 | display: block; 4 | text-align: center; 5 | width: 40px; 6 | height: 35px; 7 | &.button { 8 | cursor: pointer; 9 | } 10 | } 11 | .OIe-tools-color-sample { 12 | width: 15px; 13 | height: 15px; 14 | text-align: center; 15 | display: inline-block; 16 | vertical-align: middle; 17 | } 18 | 19 | .OIe-tools-color-title { 20 | width: 16px; 21 | position: relative; 22 | left: -1px; 23 | top: 5px; 24 | } 25 | .OIe-tools-color-panel { 26 | z-index: 10; 27 | position: fixed; 28 | bottom: 40px; 29 | } 30 | -------------------------------------------------------------------------------- /src/Canvas/Context/canvasContext.jsx: -------------------------------------------------------------------------------- 1 | // Global Context 2 | 3 | import React from 'react' 4 | import PropTypes from 'prop-types' 5 | 6 | import updateCanvas from '../editor/updateCanvas' 7 | 8 | const reducer = (state, action) => { 9 | let newMode 10 | const { canvas } = state 11 | switch (action.type) { 12 | case 'init': 13 | return { ...state, canvas: action.canvas, svgcanvas: action.svgcanvas, config: action.config } 14 | case 'mode': 15 | canvas.setMode(action.mode) 16 | return { ...state, mode: action.mode } 17 | case 'selectedElement': 18 | newMode = (canvas?.getMode() === 'select') ? { mode: 'select' } : {} 19 | return { ...state, selectedElement: action.selectedElement, multiselected: action.multiselected, ...newMode } 20 | case 'zoom': 21 | canvas.setZoom(action.zoom / 100) 22 | updateCanvas(canvas, state.svgcanvas, state.config, true) 23 | return { ...state, zoom: action.zoom } 24 | case 'context': 25 | return { ...state, context: action.context, layerName: canvas.getCurrentDrawing().getCurrentLayerName() } 26 | case 'color': 27 | canvas.setColor(action.colorType, action.color, false) 28 | // no need to memorize state for color 29 | return state 30 | case 'deleteSelectedElements': 31 | canvas.deleteSelectedElements() 32 | return state 33 | case 'setTextContent': 34 | canvas.setTextContent(action.text) 35 | return state 36 | case 'updated': 37 | newMode = (canvas?.getMode() !== 'textedit') ? { mode: 'select' } : {} 38 | return { ...state, updated: action.updated } 39 | default: 40 | throw new Error(`unknown action type: ${action.type}`) 41 | } 42 | } 43 | 44 | const canvasInitialState = { 45 | mode: 'select', 46 | selectedElement: null, 47 | multiselected: false, 48 | updated: false, 49 | zoom: 100, 50 | context: null, 51 | layerName: '', 52 | } 53 | 54 | const canvasContext = React.createContext() 55 | 56 | const CanvasContextProvider = ({ children }) => ( 57 | 60 | {children} 61 | 62 | ) 63 | 64 | CanvasContextProvider.propTypes = { children: PropTypes.element.isRequired } 65 | 66 | export { canvasContext, CanvasContextProvider } 67 | -------------------------------------------------------------------------------- /src/Canvas/Icon/Icon.jsx: -------------------------------------------------------------------------------- 1 | // General imports 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | import group from './images/group_elements.svg' 6 | import ungroup from './images/ungroup.svg' 7 | import undo from './images/undo.svg' 8 | import redo from './images/redo.svg' 9 | import select from './images/select.svg' 10 | import line from './images/line.svg' 11 | import circle from './images/circle.svg' 12 | import ellipse from './images/ellipse.svg' 13 | import square from './images/square.svg' 14 | import rect from './images/rect.svg' 15 | import save from './images/save.svg' 16 | import text from './images/text.svg' 17 | import del from './images/delete.svg' 18 | import clone from './images/clone.svg' 19 | import path from './images/path.svg' 20 | import alignBottom from './images/align_bottom.svg' 21 | import alignCenter from './images/align_center.svg' 22 | import alignTop from './images/align_top.svg' 23 | import alignLeft from './images/align_left.svg' 24 | import alignRight from './images/align_right.svg' 25 | import alignMiddle from './images/align_middle.svg' 26 | import align from './images/align.svg' 27 | import moveBottom from './images/move_bottom.svg' 28 | import moveTop from './images/move_top.svg' 29 | import bold from './images/bold.svg' 30 | import italic from './images/italic.svg' 31 | import fill from './images/fill.svg' 32 | import stroke from './images/stroke.svg' 33 | import fontSize from './images/fontsize.svg' 34 | import noColor from './images/no_color.svg' 35 | import zoom from './images/zoom.svg' 36 | import close from './images/close.svg' 37 | 38 | const Icon = ({ name, ...otherProps }) => { 39 | switch (name) { 40 | case 'Select': 41 | return select 42 | case 'Line': 43 | return line 44 | case 'Circle': 45 | return circle 46 | case 'Ellipse': 47 | return ellipse 48 | case 'Text': 49 | return text 50 | case 'Delete': 51 | return delete 52 | case 'Clone': 53 | return clone 54 | case 'Path': 55 | return path 56 | case 'Square': 57 | return square 58 | case 'Rect': 59 | return rect 60 | case 'Close': 61 | return close 62 | case 'Save': 63 | return save 64 | case 'Undo': 65 | return undo 66 | case 'Redo': 67 | return redo 68 | case 'Group': 69 | return group 70 | case 'Ungroup': 71 | return group 72 | case 'AlignBottom': 73 | return group 74 | case 'AlignCenter': 75 | return group 76 | case 'AlignTop': 77 | return group 78 | case 'AlignLeft': 79 | return group 80 | case 'AlignRight': 81 | return group 82 | case 'AlignMiddle': 83 | return group 84 | case 'Align': 85 | return group 86 | case 'MoveBottom': 87 | return group 88 | case 'MoveTop': 89 | return group 90 | case 'Bold': 91 | return group 92 | case 'Italic': 93 | return group 94 | case 'Fill': 95 | return group 96 | case 'Stroke': 97 | return group 98 | case 'FontSize': 99 | return group 100 | case 'NoColor': 101 | return group 102 | case 'Zoom': 103 | return group 104 | default: 105 | return group 106 | } 107 | } 108 | 109 | // Properties restriction 110 | Icon.propTypes = { name: PropTypes.string.isRequired } 111 | 112 | export default Icon 113 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align_bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align_center.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align_left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align_middle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align_right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/align_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | B 7 | 8 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/clone.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/ellipse.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/fontsize.svg: -------------------------------------------------------------------------------- 1 | 2 | T 3 | T 4 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/group_elements.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | i 7 | 8 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/line.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/move_bottom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/move_top.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/no_color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/path.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/rect.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/redo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/select.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/square.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/stroke.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/text.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/undo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/ungroup.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Canvas/Icon/images/zoom.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Canvas/IconButton/IconButton.jsx: -------------------------------------------------------------------------------- 1 | // General imports 2 | import PropTypes from 'prop-types' 3 | import React from 'react' 4 | 5 | import Icon from '../Icon/Icon.jsx' 6 | import './IconButton.less' 7 | 8 | const IconButton = ({ onClick, className, icon }) => ( 9 | 14 | ) 15 | 16 | // Properties restrictions 17 | IconButton.propTypes = { 18 | className: PropTypes.string, 19 | onClick: PropTypes.func.isRequired, 20 | icon: PropTypes.string.isRequired, 21 | } 22 | IconButton.defaultProps = { className: 'button' } 23 | 24 | export default IconButton 25 | -------------------------------------------------------------------------------- /src/Canvas/IconButton/IconButton.less: -------------------------------------------------------------------------------- 1 | .OIe-tools-icon { 2 | width: 12px 3 | } -------------------------------------------------------------------------------- /src/Canvas/LeftBar/LeftBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import IconButton from '../IconButton/IconButton.jsx' 4 | import './LeftBar.less' 5 | 6 | import { canvasContext } from '../Context/canvasContext.jsx' 7 | 8 | const LeftBar = () => { 9 | const [canvasState, canvasStateDispatcher] = React.useContext(canvasContext) 10 | const { mode } = canvasState 11 | 12 | const setMode = (newMode) => canvasStateDispatcher({ type: 'mode', mode: newMode }) 13 | 14 | return ( 15 |
16 | setMode('select')} 20 | /> 21 | setMode('ellipse')} 25 | /> 26 | setMode('rect')} /> 27 | setMode('path')} /> 28 | setMode('line')} /> 29 | setMode('text')} /> 30 |
31 | ) 32 | } 33 | 34 | export default LeftBar 35 | -------------------------------------------------------------------------------- /src/Canvas/LeftBar/LeftBar.less: -------------------------------------------------------------------------------- 1 | @import '../../variables.less'; 2 | 3 | .left-bar { 4 | position: relative; 5 | top: 0px; 6 | right: 0; 7 | padding-top: 50px; 8 | border-color: #808080; 9 | border-style: solid; 10 | border-width: 0px; 11 | border: none; 12 | // overflow-x:hidden; 13 | background-color: @backgroundColor; 14 | overflow-y:visible; 15 | width: 42px; 16 | z-index: 1; 17 | } 18 | 19 | .left-bar button { 20 | border: 0px; 21 | margin: 1px; 22 | padding: 2px; 23 | border-radius: 5px; 24 | font-size: 0.5em; 25 | width: 40px; 26 | height: 30px; 27 | background-color: @backgroundColor; 28 | &:hover, &.selected { 29 | background-color: @focusColor; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/AttributesTools/AttributesTools.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/label-has-associated-control */ 2 | import React from 'react' 3 | import PropTypes from 'prop-types' 4 | 5 | import './AttributesTools.less' 6 | 7 | import Input from './Input.jsx' 8 | 9 | const AttributesTools = ({ selectedElement, handleChange, attributes }) => ( 10 |
11 | 15 | 19 | {Object.entries(attributes).map(([attribute, type]) => { 20 | const round = (val) => { 21 | if (Number.isNaN(Number(val))) return val 22 | return Math.round((Number(val) + Number.EPSILON) * 1000) / 1000 23 | } 24 | const value = round(selectedElement.getAttribute(attribute)) ?? '' 25 | if (Array.isArray(type)) { 26 | // type is a list of values 27 | return ( 28 | 38 | ) 39 | } 40 | if (type === 'text') { 41 | return ( 42 | 51 | ) 52 | } 53 | if (type === 'number') { 54 | return ( 55 | 64 | ) 65 | } 66 | // 'readonly field 67 | return ( 68 | 72 | ) 73 | })} 74 |
75 | ) 76 | 77 | AttributesTools.propTypes = { 78 | attributes: PropTypes.object.isRequired, 79 | handleChange: PropTypes.func.isRequired, 80 | selectedElement: PropTypes.object.isRequired, 81 | } 82 | 83 | export default AttributesTools 84 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/AttributesTools/AttributesTools.less: -------------------------------------------------------------------------------- 1 | @import '../../../variables.less'; 2 | 3 | .OIe-attributes-tools { 4 | position: absolute; 5 | left: 300px; 6 | display: inline-flex; 7 | flex-flow: row wrap; 8 | justify-content: flex-start; // start on primary axis (row) 9 | align-items: center; // center sur secondary axis (col) 10 | background-color: @backgroundColor; 11 | width: 600px; 12 | font-size: 10px; 13 | &>label { 14 | margin-left: 2px; 15 | } 16 | & [name='tagName'] { 17 | width: 40px; 18 | height: 9px; 19 | font-size: 10px; 20 | } 21 | & [name='id'] { 22 | width: 80px; 23 | height: 9px; 24 | font-size: 10px; 25 | } 26 | & [name='x'], 27 | & [name='y'], 28 | & [name='cx'], 29 | & [name='cy'], 30 | & [name='rx'], 31 | & [name='ry'], 32 | & [name='r'], 33 | & [name='width'], 34 | & [name='height'], 35 | & [name='stroke'], 36 | & [name='stroke-width'] { 37 | width: 35px; 38 | height: 9px; 39 | font-size: 10px; 40 | } 41 | & [name='font-size'] { 42 | width: 40px; 43 | height: 9px; 44 | font-size: 10px; 45 | } 46 | & [name='font-family'] { 47 | width: 90px; 48 | height: 16px; 49 | font-size: 10px; 50 | } 51 | & [name='d'] { 52 | width: 120px; 53 | height: 9px; 54 | font-size: 10px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/AttributesTools/Input.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | const Input = ({ type, defaultValue, handleChange, name }) => { 5 | const [value, setValue] = React.useState(defaultValue) 6 | const input = ( 7 | setValue(e.target.value)} 10 | onBlur={(e) => handleChange(name, e.target.value)} 11 | type={type} 12 | name={name} 13 | /> 14 | ) 15 | return input 16 | } 17 | 18 | Input.propTypes = { 19 | type: PropTypes.string.isRequired, 20 | handleChange: PropTypes.func.isRequired, 21 | defaultValue: PropTypes.string.isRequired, 22 | name: PropTypes.string.isRequired, 23 | } 24 | 25 | export default Input 26 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/CircleTools/CircleTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AttributesTools from '../AttributesTools/AttributesTools.jsx' 5 | 6 | const CircleTools = ({ selectedElement }) => ( 7 | {}} 10 | attributes={{ 11 | cx: 'readOnly', 12 | cy: 'readOnly', 13 | r: 'readOnly', 14 | }} 15 | /> 16 | ) 17 | 18 | CircleTools.propTypes = { selectedElement: PropTypes.object.isRequired } 19 | 20 | export default CircleTools 21 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/DelDupTools/DelDupTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import IconButton from '../../IconButton/IconButton.jsx' 5 | 6 | const DelDupTools = ({ canvas }) => ( 7 | <> 8 | canvas.deleteSelectedElements()} /> 9 | canvas.cloneSelectedElements(20, 20)} /> 10 | 11 | ) 12 | 13 | DelDupTools.propTypes = { canvas: PropTypes.object } 14 | DelDupTools.defaultProps = { canvas: null } 15 | 16 | export default DelDupTools 17 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/EllipseTools/EllipseTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AttributesTools from '../AttributesTools/AttributesTools.jsx' 5 | 6 | const EllipseTools = ({ selectedElement }) => ( 7 | {}} 10 | attributes={{ 11 | cx: 'readOnly', 12 | cy: 'readOnly', 13 | rx: 'readOnly', 14 | ry: 'readOnly', 15 | }} 16 | /> 17 | ) 18 | 19 | EllipseTools.propTypes = { selectedElement: PropTypes.object.isRequired } 20 | 21 | export default EllipseTools 22 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/GenericTools/GenericTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import IconButton from '../../IconButton/IconButton.jsx' 5 | 6 | const GenericTools = ({ canvas, canvasUpdated, svgUpdate, onClose }) => { 7 | const onClickUndo = () => { 8 | canvas.undoMgr.undo() 9 | // populateLayers() 10 | } 11 | const onClickRedo = () => { 12 | canvas.undoMgr.redo() 13 | // populateLayers() 14 | } 15 | const onClickClose = () => { 16 | if (canvasUpdated) { 17 | // eslint-disable-next-line no-alert 18 | if (!window.confirm('A change was not saved, do you really want to exit?')) return 19 | } 20 | onClose() 21 | } 22 | return ( 23 | <> 24 | 28 | { 32 | svgUpdate(canvas.getSvgString()) 33 | }} 34 | /> 35 | 36 | 37 | 38 | ) 39 | } 40 | 41 | GenericTools.propTypes = { 42 | canvas: PropTypes.object, 43 | svgUpdate: PropTypes.func.isRequired, 44 | canvasUpdated: PropTypes.bool.isRequired, 45 | onClose: PropTypes.func.isRequired, 46 | } 47 | GenericTools.defaultProps = { canvas: null } 48 | 49 | export default GenericTools 50 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/GroupTools/GroupTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import IconButton from '../../IconButton/IconButton.jsx' 5 | 6 | const GroupTools = ({ canvas, selectedElement, multiselected }) => ( 7 | <> 8 | {multiselected && ( 9 | { 12 | canvas.groupSelectedElements() 13 | }} 14 | /> 15 | )} 16 | {selectedElement?.tagName === 'g' && ( 17 | { 20 | canvas.ungroupSelectedElement() 21 | }} 22 | /> 23 | )} 24 | 25 | ) 26 | 27 | GroupTools.propTypes = { 28 | canvas: PropTypes.object, 29 | selectedElement: PropTypes.object, 30 | multiselected: PropTypes.bool.isRequired, 31 | } 32 | GroupTools.defaultProps = { canvas: null, selectedElement: null } 33 | 34 | export default GroupTools 35 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/PathTools/PathTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AttributesTools from '../AttributesTools/AttributesTools.jsx' 5 | 6 | const PathTools = ({ selectedElement }) => ( 7 | {}} attributes={{ d: 'readOnly' }} /> 8 | ) 9 | 10 | PathTools.propTypes = { selectedElement: PropTypes.object.isRequired } 11 | 12 | export default PathTools 13 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/RectTools/RectTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AttributesTools from '../AttributesTools/AttributesTools.jsx' 5 | 6 | const RectTools = ({ selectedElement }) => ( 7 | {}} 10 | attributes={{ 11 | x: 'readOnly', 12 | y: 'readOnly', 13 | width: 'readOnly', 14 | height: 'readOnly', 15 | stroke: 'readOnly', 16 | 'stroke-width': 'readOnly', 17 | }} 18 | /> 19 | ) 20 | 21 | RectTools.propTypes = { selectedElement: PropTypes.object.isRequired } 22 | 23 | export default RectTools 24 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/TextTools/TextTools.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import AttributesTools from '../AttributesTools/AttributesTools.jsx' 5 | 6 | const TextTools = ({ selectedElement, handleChange }) => ( 7 | 17 | ) 18 | 19 | TextTools.propTypes = { 20 | handleChange: PropTypes.func.isRequired, 21 | selectedElement: PropTypes.object.isRequired, 22 | } 23 | 24 | export default TextTools 25 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/TopBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import RectTools from './RectTools/RectTools.jsx' 5 | import EllipseTools from './EllipseTools/EllipseTools.jsx' 6 | import CircleTools from './CircleTools/CircleTools.jsx' 7 | import PathTools from './PathTools/PathTools.jsx' 8 | import TextTools from './TextTools/TextTools.jsx' 9 | import GenericTools from './GenericTools/GenericTools.jsx' 10 | import DelDupTools from './DelDupTools/DelDupTools.jsx' 11 | import GroupTools from './GroupTools/GroupTools.jsx' 12 | import AttributesTools from './AttributesTools/AttributesTools.jsx' 13 | 14 | import { canvasContext } from '../Context/canvasContext.jsx' 15 | 16 | import './TopBar.less' 17 | 18 | const TopBar = ({ svgUpdate, onClose }) => { 19 | const [canvasState] = React.useContext(canvasContext) 20 | const { canvas, selectedElement, mode, updated } = canvasState 21 | console.info(mode, selectedElement?.tagName) 22 | const handleChange = (type, newVal) => { 23 | const elem = selectedElement 24 | switch (type) { 25 | case 'font-family': 26 | canvasState.canvas.setFontFamily(newVal) 27 | break 28 | case 'font-size': 29 | canvasState.canvas.setFontSize(newVal) 30 | break 31 | case 'id': 32 | // if the user is changing the id, then de-select the element first 33 | // change the ID, then re-select it with the new ID 34 | canvasState.canvas.clearSelection() 35 | elem.id = newVal 36 | canvasState.canvas.addToSelection([elem], true) 37 | break 38 | default: 39 | console.error(`type (${type}) not supported`) 40 | } 41 | } 42 | 43 | let ElementTools 44 | switch (canvasState.selectedElement?.tagName) { 45 | case 'rect': 46 | ElementTools = ( 47 | 54 | ) 55 | break 56 | 57 | case 'circle': 58 | ElementTools = ( 59 | 66 | ) 67 | break 68 | 69 | case 'ellipse': 70 | ElementTools = ( 71 | 78 | ) 79 | break 80 | 81 | case 'text': 82 | ElementTools = ( 83 | 87 | ) 88 | break 89 | 90 | case 'path': 91 | ElementTools = ( 92 | 99 | ) 100 | break 101 | 102 | case 'g': 103 | case 'image': 104 | case 'line': 105 | case 'polygon': 106 | case 'polyline': 107 | case 'textPath': 108 | default: 109 | ElementTools = selectedElement && ( 110 | 111 | ) 112 | } 113 | return ( 114 |
115 | 122 | 123 | 124 | {ElementTools && ElementTools} 125 |
126 | ) 127 | } 128 | 129 | TopBar.propTypes = { 130 | svgUpdate: PropTypes.func.isRequired, 131 | onClose: PropTypes.func.isRequired, 132 | } 133 | 134 | export default TopBar 135 | 136 | /* 137 | 138 | The rect element 139 | 140 | x The x position of the top left corner of the rectangle. 141 | y The y position of the top left corner of the rectangle. 142 | width The width of the rectangle 143 | height The height of the rectangle 144 | rx The x radius of the corners of the rectangle 145 | ry The y radius of the corners of the rectangle 146 | 147 | The circle element 148 | 149 | r The radius of the circle. 150 | cx The x position of the center of the circle. 151 | cy The y position of the center of the circle. 152 | 153 | Ellipse 154 | 155 | rx The x radius of the ellipse. 156 | ry The y radius of the ellipse. 157 | cx The x position of the center of the ellipse. 158 | cy The y position of the center of the ellipse. 159 | 160 | Line 161 | 162 | x1 The x position of point 1. 163 | y1 The y position of point 1. 164 | x2 The x position of point 2. 165 | y2 The y position of point 2. 166 | 167 | Polyline 168 | 169 | points A list of points, each number separated by a space, comma, EOL, or a line feed character. 170 | Each point must contain two numbers, an x coordinate and a y coordinate. 171 | So the list (0,0), (1,1), and (2,2) would be written as 0, 0 1, 1 2, 2. 172 | 173 | Polygon 174 | 175 | points A list of points, each number separated by a space, comma, EOL, or a line feed character. 176 | Each point must contain two numbers, an x coordinate and a y coordinate. 177 | So the list (0,0), (1,1), and (2,2) would be written as 0, 0 1, 1 2, 2. 178 | The drawing then closes the path, so a final straight line would be drawn from (2,2) to (0,0). 179 | 180 | Path 181 | 182 | d A list of points and other information about how to draw the path. See the Paths section for more information. 183 | 184 | */ 185 | -------------------------------------------------------------------------------- /src/Canvas/TopBar/TopBar.less: -------------------------------------------------------------------------------- 1 | @import '../../variables.less'; 2 | 3 | .top-bar { 4 | position: relative; 5 | border-right: none; 6 | left: 0px; 7 | top: 0px; 8 | width: 100%; 9 | height: 35px; 10 | padding-left: 50px; 11 | z-index: 1; 12 | background-color: @backgroundColor; 13 | } 14 | 15 | .top-bar .canvas-mode { 16 | position: absolute; 17 | top: 0px; 18 | left: 225px; 19 | } 20 | 21 | .top-bar .tag-name { 22 | position: absolute; 23 | bottom: 0px; 24 | left: 225px; 25 | } 26 | .top-bar button { 27 | border: 0px; 28 | margin: 1px; 29 | padding: 2px; 30 | border-radius: 5px; 31 | font-size: 0.5em; 32 | width: 30px; 33 | height: 30px; 34 | background-color: @backgroundColor; 35 | &:hover { 36 | background-color: @focusColor; 37 | } 38 | &.disabled { 39 | color: lightgrey; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Canvas/editor/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | canvasName: 'default', 3 | // The minimum area visible outside the canvas, as a multiple of the image dimensions. 4 | // The larger the number, the more one can scroll outside the canvas. 5 | canvas_expansion: 1.5, 6 | initFill: { 7 | color: 'FF0000', // solid red 8 | opacity: 1, 9 | }, 10 | initStroke: { 11 | width: 5, 12 | color: '000000', // solid black 13 | opacity: 1, 14 | }, 15 | text: { 16 | stroke_width: 0, 17 | font_size: 24, 18 | font_family: 'serif', 19 | }, 20 | initOpacity: 1, 21 | colorPickerCSS: null, 22 | initTool: 'select', 23 | exportWindowType: 'new', // 'same' (todo: also support 'download') 24 | wireframe: false, 25 | showlayers: false, 26 | no_save_warning: false, 27 | // PATH CONFIGURATION 28 | // The following path configuration items are disallowed in the URL (as should any future path configurations) 29 | langPath: 'locale/', // Default will be changed if this is a non-modular load 30 | extPath: 'extensions/', // Default will be changed if this is a non-modular load 31 | canvgPath: 'canvg/', // Default will be changed if this is a non-modular load 32 | jspdfPath: 'jspdf/', // Default will be changed if this is a non-modular load 33 | imgPath: 'images/', 34 | jGraduatePath: 'jgraduate/images/', 35 | extIconsPath: 'extensions/', 36 | // DOCUMENT PROPERTIES 37 | // Change the following to a preference (already in the Document Properties dialog)? 38 | dimensions: [640, 480], 39 | // EDITOR OPTIONS 40 | // Change the following to preferences (already in the Editor Options dialog)? 41 | gridSnapping: false, 42 | gridColor: '#000', 43 | baseUnit: 'px', 44 | snappingStep: 10, 45 | showRulers: true, 46 | // EXTENSION-RELATED (GRID) 47 | showGrid: false, // Set by ext-grid.js 48 | } 49 | 50 | export default config 51 | -------------------------------------------------------------------------------- /src/Canvas/editor/updateCanvas.js: -------------------------------------------------------------------------------- 1 | // code derived from svg-editor.js 2 | const updateCanvas = (svgCanvas, cnvs, curConfig, center, newCtr = {}) => { 3 | // workarea node is the parent of the svg canvas 4 | const workarea = cnvs.parentNode 5 | // let w = workarea.width(), h = workarea.height(); 6 | let { width: w, height: h } = workarea.getBoundingClientRect() 7 | const wOrig = w 8 | const hOrig = h 9 | const oldCtr = { 10 | x: workarea.scrollLeft + wOrig / 2, 11 | y: workarea.scrollTop + hOrig / 2, 12 | } 13 | // multi: The minimum area visible outside the canvas, as a multiple of the image dimensions. 14 | const multi = curConfig.canvas_expansion 15 | const zoom = svgCanvas.getZoom() 16 | w = Math.max(wOrig, svgCanvas.contentW * zoom * multi) 17 | h = Math.max(hOrig, svgCanvas.contentH * zoom * multi) 18 | 19 | if (w === wOrig && h === hOrig) { 20 | workarea.style.overflow = 'hidden' 21 | } else { 22 | workarea.style.overflow = 'scroll' 23 | } 24 | // const oldCanY = cnvs.height() / 2; 25 | // const oldCanX = cnvs.width() / 2; 26 | // cnvs.width(w).height(h); 27 | const { width: cw, height: ch } = cnvs.getBoundingClientRect() 28 | const oldCanY = ch / 2 29 | const oldCanX = cw / 2 30 | cnvs.style.width = w 31 | cnvs.style.height = h 32 | 33 | const newCanY = h / 2 34 | const newCanX = w / 2 35 | const offset = svgCanvas.updateCanvas(w, h) 36 | 37 | const ratio = newCanX / oldCanX 38 | 39 | const scrollX = w / 2 - wOrig / 2 40 | const scrollY = h / 2 - hOrig / 2 41 | 42 | // if (!newCtr) { 43 | if (!newCtr.x) { 44 | const oldDistX = oldCtr.x - oldCanX 45 | const newX = newCanX + oldDistX * ratio 46 | 47 | const oldDistY = oldCtr.y - oldCanY 48 | const newY = newCanY + oldDistY * ratio 49 | newCtr.x = newX 50 | newCtr.y = newY 51 | } else { 52 | newCtr.x += offset.x 53 | newCtr.y += offset.y 54 | } 55 | 56 | if (center) { 57 | // Go to top-left for larger documents 58 | if (svgCanvas.contentW > workarea.getBoundingClientRect().width) { 59 | // Top-left 60 | // workarea[0].scrollLeft = offset.x - 10 61 | // workarea[0].scrollTop = offset.y - 10 62 | workarea.scrollLeft = offset.x - 10 63 | workarea.scrollTop = offset.y - 10 64 | } else { 65 | // Center 66 | // wArea[0].scrollLeft = scrollX 67 | // wArea[0].scrollTop = scrollY 68 | workarea.scrollLeft = scrollX 69 | workarea.scrollTop = scrollY 70 | } 71 | } else { 72 | // wArea[0].scrollLeft = newCtr.x - wOrig / 2 73 | // wArea[0].scrollTop = newCtr.y - hOrig / 2 74 | workarea.scrollLeft = newCtr.x - wOrig / 2 75 | workarea.scrollTop = newCtr.y - hOrig / 2 76 | } 77 | } 78 | 79 | export default updateCanvas 80 | -------------------------------------------------------------------------------- /src/editor.class.js: -------------------------------------------------------------------------------- 1 | /* 2 | Optimistik 3 | SVG-Edit-react 4 | */ 5 | import React from 'react' 6 | import ReactDOM from 'react-dom/client' 7 | import Canvas from './Canvas/Canvas.jsx' 8 | 9 | const VERSION = require('../package.json').version 10 | 11 | class Editor { 12 | /** 13 | * Creates an instance of Editor 14 | * @param {HTMLElement} div DOM element where the SVG will be loaded (usually a div) 15 | * @example 16 | * const element = document.getElementById('OIm-container') 17 | * const editor = new Editor(element) 18 | * editor.load('./circle.svg') 19 | * 20 | */ 21 | constructor(div) { 22 | /** @private the div that holds the whole thing */ 23 | this.div = div 24 | this.root = ReactDOM.createRoot(this.div) 25 | this.config = { 26 | debug: true, 27 | i18n: 'fr', 28 | saveHandler: null, 29 | onCloseHandler: null, 30 | debugPrefix: 'editor', 31 | } 32 | } 33 | 34 | /** 35 | * Manage SVG update 36 | */ 37 | svgUpdate = (svgContent) => { 38 | this.logDebugData('svgUpdate', this.config.saveHandler !== null) 39 | this.svgContent = svgContent 40 | if (this.config.saveHandler !== null) { 41 | this.config.saveHandler(this.svgContent) 42 | } 43 | } 44 | 45 | /** 46 | * Manage Close 47 | */ 48 | 49 | onClose = () => { 50 | this.logDebugData('onClose', this.config.onCloseHandler !== null) 51 | this.root.unmount() 52 | if (this.config.onCloseHandler !== null) { 53 | this.config.onCloseHandler() 54 | } 55 | } 56 | 57 | /** 58 | * Load svg 59 | * @async 60 | * @memberof Editor 61 | * @method load 62 | * @param {String} svgPath svg content 63 | * @returns {Promise} return the current editor instance after the SVG is loaded 64 | * @example 65 | * editor.load('./circle.svg') 66 | * .then((editorInstance) => { 67 | * console.log('loaded and ready') 68 | * }) 69 | */ 70 | load(svgContent) { 71 | this.logDebugData('load', svgContent?.length) 72 | try { 73 | // add the React based editor in the panel div 74 | this.root.render( 75 | React.createElement( 76 | Canvas, 77 | { 78 | svgContent, 79 | locale: this.config.i18n, 80 | svgUpdate: this.svgUpdate, 81 | onClose: this.onClose, 82 | log: this.logDebugData, 83 | }, 84 | null, 85 | ), 86 | ) 87 | } catch (err) { 88 | console.error('could not load the SVG content', err) 89 | throw err 90 | } 91 | } 92 | 93 | /** 94 | * info displays a log to the console with information about the Editor version 95 | * as well as technical informations. 96 | * @memberof Editor 97 | * @method info 98 | * @returns an object containing current Editor configuration 99 | */ 100 | info() { 101 | console.info('Editor version:', VERSION) 102 | const info = { 103 | version: VERSION, 104 | currentConfig: this.config, 105 | container: this.div, 106 | } 107 | return info 108 | } 109 | 110 | /** 111 | * getSVG returns the SVG as modified by Editor. 112 | * 113 | * @memberof Editor 114 | * @method getSvg 115 | * @returns the SVG that should be saved to keep animation parameters as a string 116 | */ 117 | getSvg() { 118 | this.logDebugData('getSvg') 119 | return this.svgContent 120 | } 121 | 122 | /** 123 | * configure allow to edit the current configuration. 124 | * Use case example editor.configure(useCache, true). 125 | * Calling configure will put the animation to idle mode 126 | * @memberof Editor 127 | * @method configure 128 | * @param {string} name the setting name 129 | * @param {string} value the setting value 130 | * @returns {Object} the new configuration if succesful 131 | * @throws {Error} when the configuration does not exist 132 | * @example 133 | * // Toggle the debug mode, logging to the console each function call and its parameters 134 | * configure('debug', true) possible values: true|false, default: false 135 | * @example 136 | * configure('i18n', 'fr') 137 | */ 138 | configure(name, value) { 139 | this.logDebugData('configure', { name, value }) 140 | 141 | if (typeof this.config[name] === 'undefined') { 142 | throw new Error(`${name} is not a valid configuration`) 143 | } 144 | this.config[name] = value 145 | return this.config 146 | } 147 | 148 | /** 149 | * Log debug data to the console 150 | * @access private 151 | * @memberof Editor 152 | * @method logDebugData 153 | */ 154 | logDebugData = (functionName, args) => { 155 | if (this.config.debug) { 156 | console.info('%c%s', 'color:green', this.config.debugPrefix, functionName, args, new Error().stack.split(/\n/)[2]) 157 | } 158 | } 159 | } 160 | 161 | export default Editor 162 | -------------------------------------------------------------------------------- /src/editor.js: -------------------------------------------------------------------------------- 1 | /* 2 | Optimistik 3 | SVG-Edit-react 4 | */ 5 | 6 | import './editor.less' 7 | import Editor from './editor.class' 8 | 9 | // make the class available globally in the browser 10 | if (!window.Editor) { 11 | window.Editor = Editor 12 | } 13 | -------------------------------------------------------------------------------- /src/editor.less: -------------------------------------------------------------------------------- 1 | @import './variables.less'; 2 | 3 | .OIe-editor { 4 | font-size: 8pt; 5 | font-family: Verdana, Helvetica, Arial; 6 | color: #000000; 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width:100%; 11 | height: 100%; 12 | } 13 | 14 | .workarea { 15 | position: relative; 16 | top: 95px; 17 | left: 50px; 18 | height: 100%; 19 | background-color: #A0A0A0; 20 | border: 1px solid #808080; 21 | overflow: auto; 22 | text-align: center; 23 | } 24 | 25 | .svgcanvas { 26 | line-height: normal; 27 | text-align: center; 28 | vertical-align: middle; 29 | width: 1920px; // depends on multi (3) 30 | height: 1440px; 31 | position: relative; 32 | top: 0; 33 | left: 0; 34 | /* 35 | A subtle gradient effect in the canvas. 36 | Just experimenting - not sure if this is worth it. 37 | */ 38 | background: -moz-radial-gradient(45deg, #bbb, #222); 39 | background: -webkit-gradient(radial, center center, 3, center center, 1000, from(#bbb), to(#222)); 40 | } 41 | 42 | #svgroot { 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | } 47 | 48 | #canvasBackground > rect { 49 | fill: #fff !important; 50 | } 51 | 52 | *:focus { 53 | outline: none; 54 | } 55 | -------------------------------------------------------------------------------- /src/locales/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "open": "Open" 4 | }, 5 | "fr": { 6 | "open": "Ouvrir" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/services/svg.js: -------------------------------------------------------------------------------- 1 | /** 2 | SVGEDit canvas will "sanitize" the SVG and remove all oimotion animations. 3 | we capture them on load and restore them on save. 4 | @important This code is needed for a specific usage and will be removed in the future. 5 | */ 6 | 7 | const saveOIAttr = (svgContent) => { 8 | // eslint-disable-next-line prefer-regex-literals 9 | const result = svgContent.match(new RegExp('oi:animations="(.*?)"')) ?? {} 10 | return result['0'] ?? '' 11 | } 12 | 13 | const restoreOIAttr = (svgContent, attributes) => { 14 | if (!attributes) return svgContent 15 | const oiNameSpace = 'xmlns:oi="http://oimotion.optimistik.fr/namespace/svg/OIdata"' 16 | return svgContent.replace(' 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | SVG-edit 2.4 Arbelos 111 | 112 | Layer 2 113 | 114 | 115 | 116 | 117 | 118 | Layer 3 119 | 120 | 121 | 122 | 123 | 124 | 125 | Layer 4 126 | 127 | 128 | 129 | Background 130 | 131 | 132 | 133 | 134 | Arbelos 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | Manta Ray 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | Text - Link 179 | 180 | 181 | SVG-edit 182 | SVG-edit 183 | 184 | 185 | 186 | 187 | 188 | SVG-edit 189 | 190 | 2.4 Arbelos 191 | 2.4 Arbelos 192 | 2.4 Arbelos 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | SVG-Edit.react sample HTML 12 | 13 | 14 | 15 | 16 | 17 | 18 |

SVG-Edit.react sample HTML

19 |
20 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 2 | 3 | const config = { 4 | entry: ['regenerator-runtime/runtime', './src/editor.js'], 5 | output: { 6 | path: `${__dirname}/dist/`, 7 | filename: 'editor.js', 8 | library: 'editor', 9 | libraryTarget: 'umd', 10 | }, 11 | plugins: [ 12 | new MiniCssExtractPlugin({ 13 | // Options similar to the same options in webpackOptions.output 14 | // both options are optional 15 | filename: 'editor.css', 16 | }), 17 | ], 18 | module: { 19 | rules: [ 20 | { 21 | test: /^(?!.*?\.module).*\.scss$/, 22 | use: ['css-loader', 'sass-loader'], 23 | }, 24 | { 25 | test: /\.less$/i, 26 | use: [ 27 | { loader: 'style-loader' }, 28 | { loader: 'css-loader' }, 29 | { 30 | loader: 'less-loader', 31 | options: { lessOptions: { strictMath: true } }, 32 | }, 33 | ], 34 | }, 35 | { test: /\.(js|jsx)$/, exclude: /node_modules/, loader: 'babel-loader' }, 36 | { 37 | test: /\.html$/, 38 | use: [{ loader: 'raw-loader' }], 39 | }, 40 | { 41 | test: /\.svg$/, 42 | loader: 'svg-url-loader', 43 | }, 44 | { 45 | test: /\.(png|jpg|gif)$/i, 46 | use: [ 47 | { 48 | loader: 'url-loader', 49 | options: { limit: 40000 }, 50 | }, 51 | ], 52 | }, 53 | ], 54 | }, 55 | performance: { 56 | maxEntrypointSize: 1600000, 57 | maxAssetSize: 1600000, 58 | }, 59 | optimization: { 60 | splitChunks: { 61 | cacheGroups: { 62 | styles: { 63 | name: 'styles', 64 | test: /\.css$/, 65 | chunks: 'all', 66 | enforce: true, 67 | }, 68 | }, 69 | }, 70 | }, 71 | } 72 | 73 | module.exports = (env, argv) => { 74 | if (argv.mode === 'development') { 75 | config.devtool = 'source-map' 76 | } 77 | return config 78 | } 79 | --------------------------------------------------------------------------------