/tests/mock',
9 | ],
10 | moduleNameMapper: { '^(.*):(.*)$': '$1_$2' },
11 | unmockedModulePathPatterns: ['/^imports\\/.*\\.jsx?$/', '/^node_modules/'],
12 | // suppress warning for renderAnimationFrame (since React 16)
13 | setupFilesAfterEnv: ['raf/polyfill'],
14 | // use Enzyme to serialize jest Snapshots
15 | snapshotSerializers: ['enzyme-to-json/serializer'],
16 | }
17 |
--------------------------------------------------------------------------------
/manual/doc.md:
--------------------------------------------------------------------------------
1 | # SVG-edit-react
2 | ###### © OptimistikSAS 2020
3 | ## Introduction
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@svg-edit/svgedit-react",
3 | "version": "0.1.0",
4 | "description": "Sample React Editor based on SVGEdit",
5 | "main": "dist/editor.js",
6 | "scripts": {
7 | "start": "python -m http.server",
8 | "build": "npx webpack --mode production",
9 | "build-dev": "npx webpack --mode development --watch",
10 | "build-doc": "./node_modules/.bin/esdoc .esdoc.json",
11 | "lint": "eslint src --ext .jsx --ext .js"
12 | },
13 | "files": [
14 | "LICENSE",
15 | "README.md",
16 | "CHANGELOG.md",
17 | "dist/index.html",
18 | "dist/editor.js",
19 | "dist/editor.map.js",
20 | "dist/editor.css",
21 | "dist/arbelos.svg"
22 | ],
23 | "author": "OptimistikSAS",
24 | "license": "MIT",
25 | "dependencies": {
26 | "@svgedit/svgcanvas": "^7.2.2",
27 | "color-string": "^1.9.1",
28 | "prop-types": "^15.8.1",
29 | "react": "^18.3.1",
30 | "react-color": "^2.19.3",
31 | "react-dom": "^18.3.1",
32 | "url-loader": "^4.1.1"
33 | },
34 | "devDependencies": {
35 | "@babel/cli": "^7.18.10",
36 | "@babel/code-frame": "^7.18.6",
37 | "@babel/core": "^7.18.10",
38 | "@babel/eslint-parser": "^7.18.9",
39 | "@babel/plugin-proposal-class-properties": "^7.18.6",
40 | "@babel/plugin-transform-runtime": "^7.18.10",
41 | "@babel/preset-env": "^7.18.10",
42 | "@babel/preset-react": "^7.18.6",
43 | "@babel/runtime": "^7.18.9",
44 | "babel-jest": "^28.1.3",
45 | "babel-loader": "^8.2.5",
46 | "babel-plugin-import": "^1.13.5",
47 | "css-loader": "^6.7.1",
48 | "enzyme": "^3.11.0",
49 | "enzyme-to-json": "^3.6.2",
50 | "esdoc": "^1.1.0",
51 | "esdoc-ecmascript-proposal-plugin": "^1.0.0",
52 | "esdoc-exclude-source-plugin": "^1.0.0",
53 | "esdoc-standard-plugin": "^1.0.0",
54 | "eslint": "^8.22.0",
55 | "eslint-config-airbnb": "^19.0.4",
56 | "eslint-import-resolver-meteor": "^0.4.0",
57 | "eslint-plugin-import": "^2.26.0",
58 | "eslint-plugin-jsx-a11y": "^6.6.1",
59 | "eslint-plugin-react": "^7.30.1",
60 | "eslint-plugin-react-hooks": "^4.6.0",
61 | "file-loader": "^6.2.0",
62 | "html-loader": "^4.1.0",
63 | "imports-loader": "^4.0.1",
64 | "jest": "^28.1.3",
65 | "jest-enzyme": "^7.1.2",
66 | "jquery": "^3.6.0",
67 | "less": "^4.1.3",
68 | "less-loader": "^11.0.0",
69 | "mini-css-extract-plugin": "^2.6.1",
70 | "node-fetch": "^3.2.10",
71 | "node-sass": "^7.0.1",
72 | "postcss-loader": "^7.0.1",
73 | "raw-loader": "^4.0.2",
74 | "react-intl": "^6.0.5",
75 | "regenerator-runtime": "^0.13.9",
76 | "sass-loader": "^13.0.2",
77 | "style-loader": "^3.3.1",
78 | "stylelint": "^14.10.0",
79 | "svg-inline-loader": "^0.8.2",
80 | "svg-url-loader": "^7.1.1",
81 | "webpack": "^5.74.0",
82 | "webpack-cli": "^4.10.0"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "config:base"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/Canvas/BottomBar/BottomBar.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/label-has-associated-control */
2 | import React from 'react'
3 |
4 | import './BottomBar.less'
5 | import ColorButton from '../ColorButton/ColorButton.jsx'
6 | import Icon from '../Icon/Icon.jsx'
7 |
8 | import { canvasContext } from '../Context/canvasContext.jsx'
9 |
10 | const zoomOptions = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
11 |
12 | const BottomBar = () => {
13 | const [canvasState, canvasStateDispatcher] = React.useContext(canvasContext)
14 | const { layerName, mode, zoom, selectedElement } = canvasState
15 |
16 | const onChangeFillColor = (color) => {
17 | canvasStateDispatcher({ type: 'color', colorType: 'fill', color })
18 | }
19 |
20 | const onChangeStrokeColor = (color) => {
21 | canvasStateDispatcher({ type: 'color', colorType: 'stroke', color })
22 | }
23 |
24 | const selectedFillColor = selectedElement?.getAttribute('fill')
25 | const selectedStrokeColor = selectedElement?.getAttribute('stroke')
26 |
27 | const handleZoom = (newZoom) => {
28 | canvasStateDispatcher({ type: 'zoom', zoom: Number(newZoom) })
29 | }
30 |
31 | let fullContext = ''
32 | if (canvasState.context) {
33 | let currentChild = canvasState.context
34 | do {
35 | fullContext = `${currentChild.id ?? ''} ${fullContext}`
36 | currentChild = currentChild.parentNode
37 | }
38 | while (currentChild?.id === 'svgcontent')
39 | }
40 |
41 | return (
42 |
43 |
44 |
45 |
58 |
{`Mode:${mode}`}
59 |
{`Layer:${layerName}`}
60 |
{`Context:${fullContext}`}
61 |
62 | )
63 | }
64 |
65 | export default BottomBar
66 |
--------------------------------------------------------------------------------
/src/Canvas/BottomBar/BottomBar.less:
--------------------------------------------------------------------------------
1 | @import '../../variables.less';
2 |
3 | .bottom-bar {
4 | position: fixed;
5 | display: flex;
6 | border-right: none;
7 | left: 0;
8 | bottom: 0;
9 | right: 0;
10 | height: 35px;
11 | padding-left: 50px;
12 | z-index: 1;
13 | background-color: @backgroundColor;
14 | font-size: 0.5em;
15 | justify-content: space-evenly;
16 | align-items: center;
17 | }
18 |
19 | .bottom-bar .OIe-mode {
20 | display: inline;
21 | }
22 |
23 | .bottom-bar select {
24 | font-size: 1em;
25 | }
26 |
27 | .bottom-bar .OIe-context {
28 | display: inline;
29 | }
30 |
31 | .bottom-bar .OIe-layer {
32 | display: inline;
33 | }
34 |
35 | .bottom-bar .OIe-zoom {
36 | position: relative;
37 | width: 16px;
38 | left: -1px;
39 | top: 5px;
40 | }
41 |
--------------------------------------------------------------------------------
/src/Canvas/Canvas.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import SvgCanvas from '@svgedit/svgcanvas'
4 | import svg from '../services/svg'
5 | import config from './editor/config'
6 | import TopBar from './TopBar/TopBar.jsx'
7 | import LeftBar from './LeftBar/LeftBar.jsx'
8 | import BottomBar from './BottomBar/BottomBar.jsx'
9 | import updateCanvas from './editor/updateCanvas'
10 |
11 | import { canvasContext, CanvasContextProvider } from './Context/canvasContext.jsx'
12 |
13 | const Canvas = ({ svgContent, locale, svgUpdate, onClose, log }) => {
14 | const textRef = React.useRef(null)
15 | const svgcanvasRef = React.useRef(null)
16 | const oiAttributes = React.useRef(svg.saveOIAttr(svgContent))
17 | // const [open, setOpen] = React.useState(true)
18 | const [canvasState, dispatchCanvasState] = React.useContext(canvasContext)
19 | log('Canvas', { locale, canvasState })
20 | const updateContextPanel = () => {
21 | let elem = canvasState.selectedElement
22 | // If element has just been deleted, consider it null
23 | if (elem && !elem.parentNode) {
24 | elem = null
25 | }
26 | if (elem) {
27 | const { tagName } = elem
28 | if (tagName === 'text') {
29 | // we should here adapt the context to a text field
30 | textRef.current.value = elem.textContent
31 | }
32 | }
33 | }
34 |
35 | const selectedHandler = (win, elems) => {
36 | log('selectedHandler', elems)
37 | const selectedElement = elems.length === 1 || !elems[1] ? elems[0] : null
38 | const multiselected = (elems.length >= 2 && !!elems[1])
39 | dispatchCanvasState({
40 | type: 'selectedElement',
41 | selectedElement,
42 | multiselected,
43 | })
44 | }
45 |
46 | const changedHandler = (win, elems) => {
47 | log('changedHandler', { elems })
48 | dispatchCanvasState({ type: 'updated', updated: true })
49 | }
50 |
51 | const contextsetHandler = (win, context) => {
52 | dispatchCanvasState({ type: 'context', context })
53 | }
54 |
55 | const svgUpdateHandler = (svgString) => {
56 | svgUpdate(svg.restoreOIAttr(svgString, oiAttributes.current))
57 | console.log(canvasState)
58 | dispatchCanvasState({ type: 'updated', updated: false })
59 | }
60 |
61 | const onKeyUp = (event) => {
62 | dispatchCanvasState({ type: 'setTextContent', text: event.target.value })
63 | }
64 |
65 | const onKeyDown = (event) => {
66 | if (event.key === 'Backspace' && event.target.tagName !== 'INPUT') {
67 | event.preventDefault()
68 | dispatchCanvasState({ type: 'deleteSelectedElements' })
69 | }
70 | }
71 | // unused events -> we just log them in debug mode.
72 | const eventList = {
73 | selected: selectedHandler,
74 | changed: changedHandler,
75 | contextset: contextsetHandler,
76 | 'extension-added': () => log('extensionAddedHandler'),
77 | cleared: () => log('clearedHandler'),
78 | exported: () => log('exportedHandler'),
79 | exportedPDF: () => log('exportedPDFHandler'),
80 | message: () => log('messageHandler'),
81 | pointsAdded: () => log('pointsAddedHandler'),
82 | saved: () => log('savedHandler'),
83 | setnonce: () => log('setnonceHandler'),
84 | unsetnonce: () => log('unsetnonceHandler'),
85 | transition: () => log('transitionHandler'),
86 | zoomed: () => log('zoomedHandler'),
87 | zoomDone: () => log('zoomDoneHandler'),
88 | updateCanvas: () => log('updateCanvasHandler'),
89 | extensionsAdded: () => log('extensionsAddedHandler'),
90 | }
91 | React.useLayoutEffect(() => {
92 | const editorDom = svgcanvasRef.current
93 | const canvas = new SvgCanvas(editorDom, config)
94 | updateCanvas(canvas, svgcanvasRef.current, config, true)
95 | console.log(canvas)
96 | canvas.textActions.setInputElem(textRef.current)
97 | Object.entries(eventList).forEach(([eventName, eventHandler]) => {
98 | canvas.bind(eventName, eventHandler)
99 | })
100 | dispatchCanvasState({ type: 'init', canvas, svgcanvas: editorDom, config })
101 | document.addEventListener('keydown', onKeyDown.bind(canvas))
102 | return () => {
103 | // cleanup function
104 | console.log('cleanup')
105 | document.removeEventListener('keydown', onKeyDown.bind(canvas))
106 | }
107 | }, [])
108 |
109 | React.useLayoutEffect(() => {
110 | log('new svgContent', svgContent.length)
111 | if (!canvasState.canvas) return
112 | oiAttributes.current = svg.saveOIAttr(svgContent)
113 | canvasState.canvas.clear()
114 | const success = canvasState.canvas.setSvgString(svgContent.replace(/'/g, "\\'"), true) // true => prevent undo
115 | updateCanvas(canvasState.canvas, svgcanvasRef.current, config, true)
116 | if (!success) throw new Error('Error loading SVG')
117 | dispatchCanvasState({ type: 'updated', updated: false })
118 | }, [svgContent, canvasState.canvas])
119 |
120 | updateContextPanel()
121 | return (
122 | <>
123 |
127 |
128 |
129 |
130 |
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
42 | case 'Line':
43 | return
44 | case 'Circle':
45 | return
46 | case 'Ellipse':
47 | return
48 | case 'Text':
49 | return
50 | case 'Delete':
51 | return
52 | case 'Clone':
53 | return
54 | case 'Path':
55 | return
56 | case 'Square':
57 | return
58 | case 'Rect':
59 | return
60 | case 'Close':
61 | return
62 | case 'Save':
63 | return
64 | case 'Undo':
65 | return
66 | case 'Redo':
67 | return
68 | case 'Group':
69 | return
70 | case 'Ungroup':
71 | return
72 | case 'AlignBottom':
73 | return
74 | case 'AlignCenter':
75 | return
76 | case 'AlignTop':
77 | return
78 | case 'AlignLeft':
79 | return
80 | case 'AlignRight':
81 | return
82 | case 'AlignMiddle':
83 | return
84 | case 'Align':
85 | return
86 | case 'MoveBottom':
87 | return
88 | case 'MoveTop':
89 | return
90 | case 'Bold':
91 | return
92 | case 'Italic':
93 | return
94 | case 'Fill':
95 | return
96 | case 'Stroke':
97 | return
98 | case 'FontSize':
99 | return
100 | case 'NoColor':
101 | return
102 | case 'Zoom':
103 | return
104 | default:
105 | return
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/clone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/ellipse.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Canvas/Icon/images/fontsize.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------