diff --git a/.eslintignore b/.eslintignore index c0c396d6..b86d5417 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,14 +1,21 @@ node_modules # Vendor/minified files -editor/jquery.js -editor/jspdf/jspdf.min.js -editor/jspdf/underscore-min.js +editor/jquery.min.js +editor/jquery-ui + editor/jgraduate/jpicker.min.js editor/jgraduate/jquery.jgraduate.min.js -editor/jquery-ui + editor/jquerybbq editor/js-hotkeys editor/spinbtn/JQuerySpinBtn.min.js + +editor/jspdf/jspdf.min.js +editor/jspdf/underscore-min.js + +editor/extensions/imagelib/jquery.min.js +editor/extensions/mathjax + test/qunit test/sinon diff --git a/build/tools/ship.py b/build/tools/ship.py index d2c30521..2cb5df6a 100755 --- a/build/tools/ship.py +++ b/build/tools/ship.py @@ -121,7 +121,6 @@ def parseComment(line, line_num, enabled_flags): return line - def ship(inFileName, enabled_flags): # read in HTML file lines = file(inFileName, 'r').readlines() @@ -141,7 +140,7 @@ def ship(inFileName, enabled_flags): else: # else append line to the output list out_lines.append(line) i += 1 - + return ''.join(out_lines) if __name__ == '__main__': diff --git a/chrome-app/icon_128.png b/chrome-app/icon_128.png index 964027fe..6369b49b 100644 Binary files a/chrome-app/icon_128.png and b/chrome-app/icon_128.png differ diff --git a/docs/ConfigOptions.md b/docs/ConfigOptions.md new file mode 100644 index 00000000..c76035c9 --- /dev/null +++ b/docs/ConfigOptions.md @@ -0,0 +1,118 @@ +# Introduction + +As of version 2.5, SVG-edit has several configuration settings that can be overridden either by adding URL parameters or by setting the options in JavaScript. As of version 2.7, a few among these options related to paths are disallowed via URL though they can still be set by `svgEditor.setConfig`. + +## How to set the options + +Options can be set using `svgEditor.setConfig(options)`, where `options` is an object literal of keys and values. This must be run before the actual page or DOM is loaded, otherwise it will have no effect. Note that one may create a `config.js` file within the "editor" directory and add such configuration directives to it without needing to modify the repository editor code (and note version 2.8 adds support for a custom.css file for the same purpose). + +## Example: + +```js +svgEditor.setConfig({ + dimensions: [320, 240], + canvas_expansion: 5, + initFill: { + color: '0000FF' + } +}); +``` + +This will set the default width/height of the image, the size of the outside canvas, and the default "fill" color. + +The same options can be set in the URL like this: + +``` +.../svg-editor.html?dimensions=300,240&canvas_expansion=5&initFill[color]=0000FF +``` + +As of version 2.7, if options are set both using `.setConfig()` as well as in the URL, the `.setConfig()` value will be used. The reverse was true in previous versions but was changed for security reasons. + +One may optionally pass another object to `.setConfig()` as the second argument to further adjust configuration behavior. + +If an `overwrite` boolean is set to false on this additional object, it will, as occurs with all URL type configurations, prevent the current configuration from overwriting any explicitly set previous configurations. The default is true except for URLs which always are false. + +If an `allowInitialUserOverride` boolean is set to true, it will allow subsequent configuration overwriting via URL (e.g., if you do want the user to have the option to override certain or your (`config.js`) `.setConfig()` directives via URL while still wishing to provide them with your own default value, you should add this property). + +## Configurable options + +Note that those items marked as preferences are configuration items which can also be set via the UI (and specifically via Editor Options except where mentioned). Those items which appear in the UI but are not treated internally as preferences are marked with "Maybe" as their status may change. + +| Property | Description | Default | Preference | |:---------|:------------|:--------|:-----------| +| `lang` | Two-letter language code. The language must exist in the Editor Preferences language list | Default to "en" if `locale.js` detection does not detect another language | Yes | +| `bkgd_url` | Background raster image URL. This image will fill the background of the document, useful for tracing purposes | (none) | Yes | +| `img_save` | Defines whether included raster images should be saved as Data URIs when possible, or as URL references. Must be either 'embed' or 'ref'. Settable in the Document Properties dialog. | embed | Yes | +| `dimensions` | The default width/height of a new document. Use an array in `setConfig` (e.g., `[800, 600]`) and comma separated numbers in the URL | `[640, 480]` | Maybe | +| `initFill[color]` | The initial fill color. Must be a hex code string. | FF0000 (solid red) | No | +| `initFill[opacity]` | The initial fill opacity. Must be a number between 0 and 1 | 1 | No | +| `initStroke[color]` | The initial stroke color. Must be a hex code. | 000000 (solid black) | No | +| `initStroke[width]` | The initial stroke width. Must be a positive number. | 5 | No | +| `initStroke[opacity]` | The initial stroke opacity. Must be a number between 0 and 1 | 1 | No | +| `initTool` | The initially selected tool. Must be either the ID of the button for the tool, or the ID without "tool_" prefix_| select | No | +| `exportWindowType` | New as of 2.8. Can be "new" or "same" to indicate whether new windows will be generated for each export; the window.name of the export window is namespaced based on the `canvasName` (and incremented if "new" is selected as the type) | new | No | +| `imgPath` | The path where the SVG icons are located, with trailing slash. Note that as of version 2.7, this is not configurable by URL for security reasons. | `images/` | No | +| `jGraduatePath` | The path where jGraduate images are located. Note that as of version 2.7, this is not configurable by URL for security reasons. | `jgraduate/images/` | No | +| `langPath` | The path where the language files are located, with trailing slash. Note that as of version 2.7, this is not configurable by URL for security reasons. | `locale/` | No | +| `extPath` | The path used for extension files, with trailing slash. Note that as of version 2.7, this is not configurable by URL for security reasons. | `extensions/` | No | +| `extensions` | Extensions to load on startup. Use an array in setConfig and comma separated file names in the URL. Note that as of version 2.7, paths containing "/", "\", or ":", are disallowed for security reasons. Although previous versions of this list would entirely override the default list, as of version 2.7, the defaults will always be added to this explicit list unless the configuration `noDefaultExtensions` is included. | `['ext-overview_window.js','ext-markers.js','ext-connector.js','ext-eyedropper.js','ext-shapes.js','ext-imagelib.js','ext-grid.js','ext-polygon.js','ext-star.js','ext-panning.js','ext-storage.js']` | No | +| `showlayers` | Open the layers side-panel by default | `false` | No | +| `wireframe` | Start in wireframe mode | `false` | No | +| `gridSnapping` | Enable snap to grid by default. Set in Editor Options. | `false` | Maybe | +| `gridColor` | Set in Editor Options. | #000 (black) | Maybe | +| `baseUnit` | Set in Editor Options. | px | Maybe | +| `snappingStep` | Set the default grid snapping value. Set in Editor Options. | 10 | Maybe | +| `showRulers` | Initial state of ruler display (v2.6). Set in Editor Options. | `true` | Maybe | +| `no_save_warning` | A boolean that when `true` prevents the warning dialog box from appearing when closing/reloading the page. Mostly useful for testing. | `false` | No | +| `canvas_expansion` | The minimum area visible outside the canvas, as a multiple of the image dimensions. The larger the number, the more one can scroll outside the canvas. | 3 | No | +| `show_outside_canvas` | A boolean that defines whether or not elements outside the canvas should be visible; set by `svgcanvas.js` | `true` | No | +| `iconsize` | Size of the toolbar icons. Must be one of the following: 's', 'm', 'l', 'xl' | Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise | Yes | +| `bkgd_color` | Canvas background color | #FFF (white) | Yes | +| `selectNew` | Initial state of option to automatically select objects after they are created (v2.6) | `true` | No | +| `save_notice_done` | Used to track alert status | `false` | Yes | +| `export_notice_done` | Used to track alert status | `false` | Yes | +| `allowedOrigins` | Used by `ext-xdomain-messaging.js` to indicate which origins are permitted for cross-domain messaging (e.g., between the embedded editor and main editor code). Besides explicit domains, one might add '' to allow all domains (not recommended for privacy/data integrity of your user's content!), `window.location.origin` for allowing the same origin (should be safe if you trust all apps on your domain), 'null' to allow `file://` URL usage| `[]` | Maybe | +| `canvasName` | Used to namespace storage provided via `ext-storage.js`; you can use this if you wish to have multiple independent instances of SVG Edit on the same domain | default | No | +| `initOpacity` | Initial opacity (multiplied by 100) | 1 | No | +| `colorPickerCSS` | Object of CSS properties mapped to values (for jQuery) to apply to the color picker. A `null` value (the default) will cause the CSS to default to `left` with a position equal to that of the fill_color or stroke_color element minus 140, and a `bottom` equal to 40 | `null` (see description) | No | +| `preventAllURLConfig` | Set to `true` (in `config.js`; extension loading is too late!) to override the ability for URLs to set non-content configuration (including extension config) | `false` | No | +| `preventURLContentLoading` | Set to `true` (in `config.js`; extension loading is too late!) to override the ability for URLs to set URL-based SVG content | `false` | No | +| `lockExtensions` | Set to `true` (in config.js; extension loading is too late!) to override the ability for URLs to set their own extensions; disallowed in URL setting. There is no need for this when `preventAllURLConfig` is used. | `false` | No | +| `noDefaultExtensions` | If set to `true`, prohibits automatic inclusion of default extensions (though "extensions" can still be used to add back any desired default extensions along with any other extensions); can only be meaningfully used in `config.js` or in the URL | `false` | No | +| `showGrid` | Set by `ext-grid.js`; determines whether or not to show the grid by default | `false` | No | +| `noStorageOnLoad` | Some interaction with `ext-storage.js`; prevents even the loading of previously saved local storage | `false` | No | +| `forceStorage` | Some interaction with `ext-storage.js`; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not | `false` | No | +| `emptyStorageOnDecline` | Used by `ext-storage.js`; empty any prior storage if the user declines to store | `false` | No | +| `paramurl` | This is available via URL only. Deprecated and removed in trunk. Allowed an un-encoded URL within the query string (use "url" or "source" with a data: URI instead) | (None) | No | +| `selectNew` | Set by svgcanvas.js; used by mouseUp; it true, will replace the selection with the current element and show grips | `true` | No | + +## Preload a file + +It is also possible to start the editor with preloaded SVG file, using the following methods. + +However, one should bear in mind that if one wishes to immediately set a particular string, especially if in config.js (and prevent the user from saving their own text), one should first set the config option "noStorageOnLoad" to false or otherwise any previous local storage may overwrite your own string. + +```js +// Serialized string: +svgEditor.loadFromString('...'); + +// Data URI: +svgEditor.loadFromDataURI('data:image/svg+xml;base64,...'); + +// Local URL: +svgEditor.loadFromURL('images/logo.svg'); +``` + +Or as URL parameter: + +```js +// Data URI +'?source=' + encodeURIComponent('data:image/svg+xml;utf8,') + encodeURIComponent(/*...*/) + +// Data URI (base 64): +'?source=' + encodeURIComponent('data:image/svg+xml;base64,' + /* ... */); // data%3Aimage%2Fsvg%2Bxml%3Bbase64%2C ... + +// Local URL: +'?url=' + encodeURIComponent('images/logo.svg'); // images%2Flogo.svg +``` + +**Note:** There is currently a bug that prevents data URIs ending with equals (=) characters from being parsed. Removing these characters seem to allow the import to work as expected. diff --git a/docs/ExtensionDocs.md b/docs/ExtensionDocs.md new file mode 100644 index 00000000..a4b2131e --- /dev/null +++ b/docs/ExtensionDocs.md @@ -0,0 +1,314 @@ +# Introduction + +As of version 2.5, SVG-Edit has support for extensions. This an (in-progress) guide for creating SVG-Edit plugins. + +## Basic format + +SVG-Edit plugins are standalone JavaScript files that can be either included in the HTML file or loaded using setConfig or through the URL (see ConfigOptions for usage). + +Note that if you create a `config.js` file in the "editor" directory, this will be used to execute commands before extensions are loaded, e.g., if you wish to make configuration changes which affect extension loading behavior. Normally, however, it should be preferable for modularity to use the extension mechanism, as this can allow you or users to customize which extensions are loaded (whereas `config.js` will always run if present). + +This is the general format for an extension: + +```js +svgEditor.addExtension('extensionName', function (methods) { + return extensionData; +}); +``` + +The first parameter (`extensionName`) is the unique name for this extension. + +The second parameter is a function that supplies methods and variables from svgCanvas and can return an object that includes properties and functions related to the extension. + +The basic Hello world extension can be used as an example on how to create a basic extension. This extension adds a "mode" button to the bottom of the left panel that changes the mode, then shows a "Hello world" message whenever the canvas is clicked on. See [extension in action](https://svg-edit.github.io/svgedit/releases/svg-edit-2.8.1/svg-editor.html?extensions=ext-helloworld.js). + +The basic structure of this plugin looks like this: + +```js +svgEditor.addExtension('Hello World', function () { + + // Returning an object is optional as of v2.7+ + return { + // name: '', // A name has traditionally been added but apparently not needed? + svgicons: 'extensions/helloworld-icon.xml', + buttons: [{...}], + mouseDown() { + ... + }, + mouseUp(opts) { + ... + } + }; +}); +``` + +Note how the returned properties include information on the buttons, as well as the functions that should be run when certain events take place. + +## Creating buttons + +Buttons can appear either in the mode panel (left panel) or the context panel (top panel, changes depending on selection). Their icons can either consist of SVG icons (recommended) or just raster images. + +Each button is an object with the following properties (added to the array "buttons" on the object provided by the extension): + +| Property | Description | Required? | +|:---------|:------------|:----------| +| `id` (string) | A unique identifier for this button. If SVG icons are used, this must match the ID used in the icon file. | Yes | +| `type` (string) | Type of button. Must be either 'mode' or 'context' | Yes | +| `title` (string) | The tooltip text that will appear when the user hovers over the icon | Yes | +| `icon` (string) | The file path to the raster version of the icon. | Only if no svgicons is supplied | +| `svgicon` (string) | If absent, will utilize the button "id"; used to set "placement" on the svgIcons call | No | +| `list` (string) | Points to the "id" of a context_tools item of type "button-select" into which the button will be added as a panel list item | No | +| `position` (integer) | The numeric index for placement; defaults to last position (as of the time of extension addition) if not present | No | +| `panel` (string) | The ID of the context panel to be included, if type is "context". | Only if type is "context" | +| `events` (object) | DOM event names with associated functions. Example: {'click': function() { alert('Button was clicked') } } | Yes | +| `includeWith` (object) | Object with flyout menu data (see following properties) | No | +| `includeWith[button]` (string) | jQuery selector of the existing button to be joined. Example: '#tool_line' | Yes (if includeWith is used) | +| `includeWith[isDefault]` (boolean) | Option indicating whether button is default in flyout list or not | No | +| `isDefault` (boolean) | Whether or not the default is the default | No | +| `includeWith[position]` (integer) | Position of icon in flyout list, will be added to end if not indicated | No | +| `key` (string) | The key to bind to the button | No | + +## Creating SVG icons + +The SVG-Edit project uses icons created using basic SVG (generally using SVG-Edit as design tool), and extensions are encouraged to do so too. This allows the interface toolbars to be resized and icons to be reused at various sizes. If your extension uses multiple icons, they can all be stored in the same file. To specify icon file used, set the path under the extension's returned svgicons property. + +An SVG icon file is an XML document that consists of a root SVG element with child group elements (``). Each of these has an ID that should match the ID specified in the associated button object. Its content should be the SVG source of the icon. See the Hello World icon as an example. + +For further information, see the SVG Icon Loader project. + +## Creating context tools + +Context tools appear in the top toolbar whenever a certain type of element is selected. + +These are added by the extension returning an object with the property "context_tools". + +| Property | Description | Required? | +|:---------|:------------|:----------| +| `panel` (string) | The ID of the existing panel for the tool to be added to | Yes | +| `container_id` (string) | The ID to be given to the tool's container element | No | +| `type` (string) | The type of tool being added. Must be one of the following: 'tool_button', 'select', 'input' | Yes | +| `id` (string) | The ID of the actual tool element | Yes | +| `events` (object) | DOM event names with associated functions. Example: {'change': function() { alert('Option was changed') } } | Yes | +| `options` (object) | List of options and their labels for select tools. Example: `{'1': 'One', '2': 'Two', 'all': 'All' } | Only for "select" tools | +| `defval` (string) | Default value | No | +| `label` (string) | Label associated with the tool, visible in the UI | No | +| `title` (string) | The tooltip text that will appear when the user hovers over the tool | Yes | +| `size` (integer) | Value of the "size" attribute of the tool input | No | +| `spindata` (object) | When added to a tool of type "input", this tool becomes a "spinner" which allows the number to be in/decreased. For data required see The SpinButton script | No | +| `colnum` (integer) | Added as part of the option list class. | No | + +## SVG-Edit events + +Most plugins will want to run functions when certain events are triggered. This is a list of the current events that can be hooked onto. All events are optional. + +| Event | Description | Parameters | Return value expected | |:------|:------------|:-----------|:----------------------| +| `mouseDown` | The main (left) mouse button is held down on the canvas area | Supplies an object with these properties: `evt` (the event object), `start_x` (x coordinate on canvas), `start_y` (y coordinate on canvas), `selectedElements` (an array of the selected Elements) | An optional object with started: true to indicate that creating/editing has started | +| `mouseMove` | The mouse is moved on the canvas area | Same as for `mouseDown`, but with a selected property that refers to the first selected element | None | +| `mouseUp` | The main (left) mouse button is released (anywhere) | Same as for `mouseDown` | An optional object with these properties: `element` (the element being affected), `keep` (boolean that indicates if the current element should be kept) `started` (boolean that indicates if editing should still be considered as "started") | +| `zoomChanged` | The zoom level is changed | Supplies the new zoom level as a number (not percentage) | None | +| `selectedChanged` | The element selection has changed (elements were added/removed from selection | Supplies an object with these properties: `elems` (array of the newly selected elements), `selectedElement` (the single selected element), `multiselected` (a boolean that indicates whether one or more elements was selected) | None | +| `elementChanged` | One or more elements were changed | Object with properties: `elems` (array of the affected elements) | None | +| `elementTransition` | Called when part of element is in process of changing, generally on mousemove actions like rotate, move, etc. | Object with properties: `elems` (array of transitioning elements) | None | +| `toolButtonStateUpdate` | The bottom panel was updated | Object with these properties: `nofill` (boolean that indicates fill is disabled), `nostroke` (boolean that indicates stroke is disabled) | None | +| `langChanged` | The language was changed | Two-letter code of the new language | None | +| `langReady` | Invoked as soon as the locale is ready | An object with properties "lang" containing the two-letter language code and "uiStrings" as an alias for svgEditor.uiStrings | None | +| `addlangData` | Means for an extension to add locale data | The two-letter language code | Object with "data" property set to an object containing two-letter language codes keyed to an array of objects with "id" and "title" or "textContent" properties | +| `callback` | Invoked upon addition of the extension, or, if svgicons are set, then after the icons are ready | None | None | +| `canvasUpdated` | Invoked upon updates to the canvas | Object with properties: new_x, new_y, old_x, old_y, d_x, d_y | None | +| `onNewDocument` | Called when new image is created | None | None | +| `workareaResized` | Called when sidepanel is resized or toggled | None | None | + +## Helper functions + +A variety of methods can be accessed within plugins. In the future, we hope to have them all properly documented, for now here is the current list of function/variable names. + +## `svgCanvas` variables + +These are supplied in an object through the first parameter of the extension function (see "methods" variable in above example). + +| Name | Description | +|:-----|:------------| +| `svgroot` (element) | The workarea's root SVG element. NOT the root SVG element of the image being edited | +| `svgcontent` (element) | The root SVG element of the image being edited | +| `nonce` (number) | The unique identifier given to this image | +| `selectorManager` (object) | The object that manages selection information | + +## `svgEditor` public methods + +| Name | Description | Params | Return value | +|:-----|:------------|:-------|:-------------| +| `setCustomHandlers()` | Override default SVG open, save, and export behaviors | Accepts object with all optional properties, `open`, `save`, and `exportImage`; `open` is not passed any parameters; `saved` is passed a string containing the SVG; `exportImage` is passed an object containing the properties: `svg` with the SVG contents as a string, `datauri` with the contents as a data URI, `issues` with an array of UI strings pertaining to relevant known CanVG issues, `type` indicating the chosen image type ("PNG", "JPEG", "BMP", "WEBP") (or, as planned for 3.0.0, "PDF" type), `mimeType` for the MIME type, `exportWindowName` as a convenience for passing along a window.name to target a window on which the export could be added, and `quality` as a decimal between 0 and 1 (for use with JPEG or WEBP) | (None) | | ... | ... | ... | ... | + +## `svgCanvas` private methods + +These are supplied in an object through the first parameter of the extension function (see `methods` variable in above example). + +| Name | Description | +|:-----|:------------| +| `addCommandToHistory()` | | +| `addGradient()` | | +| `addSvgElementFromJson()` | | +| `assignAttributes()` | | +| `BatchCommand()` | | +| `call()` | | +| `ChangeElementCommand()` | | +| `cleanupElement()` | | +| `copyElem()` | | +| `ffClone()` | | +| `findDefs()` | | +| `findDuplicateGradient()` | | +| `fromXml()` | | +| `getElem()` | | +| `getId()` | | +| `getIntersectionList()` | | +| `getNextId()` | | +| `getPathBBox()` | | +| `getUrlFromAttr()` | | +| `hasMatrixTransform()` | | +| `identifyLayers()` | | +| `InsertElementCommand()` | | +| `isIdentity()` | | +| `logMatrix()` | | +| `matrixMultiply()` | | +| `MoveElementCommand()` | | +| `preventClickDefault()` | | +| `recalculateAllSelectedDimensions()` | | +| `recalculateDimensions()` | | +| `remapElement()` | | +| `RemoveElementCommand()` | | +| `removeUnusedGrads()` | | +| `resetUndoStack()` | | +| `round()` | | +| `runExtensions()` | | +| `sanitizeSvg()` | | +| `Selector()` | | +| `SelectorManager()` | | +| `shortFloat()` | | +| `svgCanvasToString()` | | +| `SVGEditTransformList()` | | +| `svgToString()` | | +| `toString()` | | +| `toXml()` | | +| `transformBox()` | | +| `transformListToTransform()` | | +| `transformPoint()` | | +| `transformToObj()` | | +| `walkTree()` | | + +## `svgCanvas` public methods + +| Name | Description | +|:-----|:------------| +| `addToSelection()` | | +| `alignSelectedElements()` | | +| `beginUndoableChange()` | | +| `bind()` | | +| `changeSelectedAttribute()` | | +| `changeSelectedAttributeNoUndo()` | | +| `clear()` | | +| `clearSelection()` | | +| `cloneSelectedElements()` | | +| `convertToPath()` | | +| `createLayer()` | | +| `cycleElement()` | | +| `deleteCurrentLayer()` | | +| `deleteSelectedElements()` | | +| `each()` | | +| `embedImage()` | | +| `finishUndoableChange()` | | +| `fixOperaXML()` | | +| `getBBox()` | | +| `getBold()` | | +| `getContentElem()` | | +| `getCurrentLayer()` | | +| `getEditorNS()` | | +| `getFillColor()` | | +| `getFillOpacity()` | | +| `getFontFamily()` | | +| `getFontSize()` | | +| `getHistoryPosition()` | | +| `getImageTitle()` | | +| `getItalic()` | | +| `getLayer()` | | +| `getLayerOpacity()` | | +| `getLayerVisibility()` | | +| `getMode()` | | +| `getNextRedoCommandText()` | | +| `getNextUndoCommandText()` | | +| `getNumLayers()` | | +| `getOffset()` | | +| `getOpacity()` | | +| `getPrivateMethods()` | | +| `getRedoStackSize()` | | +| `getResolution()` | | +| `getRootElem()` | | +| `getRotationAngle()` | | +| `getSelectedElems()` | | +| `getStrokeColor()` | | +| `getStrokeOpacity()` | | +| `getStrokeStyle()` | | +| `getStrokeWidth()` | | +| `getStrokedBBox()` | | +| `getSvgString()` | | +| `getText()` | | +| `getTransformList()` | | +| `getUndoStackSize()` | | +| `getUrlFromAttr()` | | +| `getVersion()` | | +| `getVisibleElements()` | | +| `getZoom()` | | +| `groupSelectedElements()` | | +| `importSvgString()` | | +| `isValidUnit()` | | +| `linkControlPoints()` | | +| `matrixMultiply()` | | +| `moveSelectedElements()` | | +| `moveSelectedToLayer()` | | +| `moveToBottomSelectedElement()` | | +| `moveToTopSelectedElement()` | | +| `open()` | | +| `randomizeIds()` | | +| `ready()` | | +| `redo()` | | +| `removeFromSelection()` | | +| `renameCurrentLayer()` | | +| `runExtensions()` | | +| `save()` | | +| `selectAllInCurrentLayer()` | | +| `setBBoxZoom()` | | +| `setBackground()` | | +| `setBold()` | | +| `setConfig()` | | +| `setCurrentLayer()` | | +| `setCurrentLayerPosition()` | | +| `setFillColor()` | | +| `setFillOpacity()` | | +| `setFillPaint()` | | +| `setFontFamily()` | | +| `setFontSize()` | | +| `setIdPrefix()` | | +| `setImageTitle()` | | +| `setImageURL()` | | +| `setItalic()` | | +| `setLayerOpacity()` | | +| `setLayerVisibility()` | | +| `setMode()` | | +| `setOpacity()` | | +| `setRectRadius()` | | +| `setResolution()` | | +| `setRotationAngle()` | | +| `setSegType()` | | +| `setStrokeColor()` | | +| `setStrokeOpacity()` | | +| `setStrokePaint()` | | +| `setStrokeStyle()` | | +| `setStrokeWidth()` | | +| `setSvgString()` | | +| `setTextContent()` | | +| `setUiStrings()` | | +| `setZoom()` | | +| `smoothControlPoints()` | | +| `undo()` | | +| `ungroupSelectedElement()` | | +| `updateCanvas()` | | +| `updateElementFromJson()` | | diff --git a/docs/FrequentlyAskedQuestions.md b/docs/FrequentlyAskedQuestions.md new file mode 100644 index 00000000..3dadab5a --- /dev/null +++ b/docs/FrequentlyAskedQuestions.md @@ -0,0 +1,54 @@ +**NOTE: The following may contain outdated content.** + +**Q: Why doesn't SVG-edit work in Internet Explorer 6-7-8?** + +A: SVG-edit only works in IE6-7-8 if you have installed the Google Chrome Frame plugin. Internet Explorer 8 and under do not natively support Scalable Vector Graphics (SVG), however IE9+ does, and thus is supported starting in SVG-edit 2.6 + +In theory there are several other possibilities that could allow SVG-edit to work in IE: * someone gets it to work with the SVG Web shim * someone gets it to work with IE and the Adobe SVG Viewer * someone gets it to work with another SVG plugin + +**Q: How can I make SVG-edit save files on my server?** + +A: As of SVG-edit 2.5.1, an extension is available that overrides the default open/save behavior and instead uses PHP files to allow proper open/save dialog boxes. You can include the extension by adding `ext-server_opensave.js` to the `curConfig.extension` array in `svg-editor.js` or through other methods mentioned on our ConfigOptions page. + +For other server-saving behavior you'll want to modify `ext-server_opensave.js` or the `filesave.php` file, both available under `editor/extensions/`. + +**Q: How can I set the stroke to 'none'?** + +A: Shift-clicking palette squares sets the Stroke paint value. Thus, you can shift-click on the None box (red x on white background) to clear the Stroke paint. + +**Q: How can I help?** + +A: See How to participate + +**Q: How can I select an element when it's hidden or behind another one?** + +A: Select an object. Shift+O will select the previous object Shift+P will select the next object. Using the wireframe mode may also help in seeing hidden objects. + +**Q: How can I edit shapes that have been grouped?** + +A: Double-click the group and you will shift the editing context to the group. The rest of the image will not be editable while you are in the group context. Once you are done editing inside the group, press Escape. + +**Q: Can I trace over a raster (PNG, JPEG...) image?** + +A: Yes, there are two methods you can use as of SVG-edit 2.4. 1. Go to the Document Properties, and enter the URL of the image under "Editor Background". This image will then fill the background without being saved as part of the image. + + Add a layer from the layer panel. Then draw a raster image (image icon) and enter your URL. Use the layer above this one to trace over the image without moving. Note that you can also hide/show layers to help your work. + +**Q: How do I use the Wave Gadget?** + +A: (Note that this information refers to the SVG-edit 2.3 Wave Gadget, the Wave Gadget has not been worked on for years though) Go to this wave wavesandbox.com!w+W7VzCLZk%A and there will be a button on the bottom that says "Install" and when you are editing things, you will see a SVG-edit icon in your toolbar that you can click to include the gadget into any blip. + +**Q: How do I copy the style of an object to other(s)?** + +A: + +- Select the object you want to copy the style from. You'll see its Fill and Stroke style attributes displayed in the bottom toolbar. +- Holding Shift to keep the first object selected, select one or several other objects. +- Open the colorpicker by clicking on the color blocks in the bottom toolbar. If you want to copy the fill, select the Fill block. If you want to copy the stroke, select the Stroke block. +- Hit "Ok" in the colorpicker + +The other objects will get the Fill or the Stroke of the first object. + +**Q: How can I serve SVG graphic editor from my own server?** + +A: You need to download the latest version to your server and unzip. The exact commands/instructions are here: diff --git a/editor/browser.js b/editor/browser.js index 6ab5263e..058d7fcc 100644 --- a/editor/browser.js +++ b/editor/browser.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals $, svgedit */ +/* globals jQuery */ /** * Package: svgedit.browser * @@ -12,177 +11,168 @@ // Dependencies: // 1) jQuery (for $.alert()) -(function () { -'use strict'; +import './pathseg.js'; +import {NS} from './svgedit.js'; -if (!svgedit.browser) { - svgedit.browser = {}; -} +const $ = jQuery; -// alias -var NS = svgedit.NS; - -var supportsSvg_ = (function () { - return !!document.createElementNS && !!document.createElementNS(NS.SVG, 'svg').createSVGRect; +const supportsSvg_ = (function () { +return !!document.createElementNS && !!document.createElementNS(NS.SVG, 'svg').createSVGRect; }()); -svgedit.browser.supportsSvg = function () { return supportsSvg_; }; -if (!svgedit.browser.supportsSvg()) { - window.location = 'browser-not-supported.html'; - return; -} +export const supportsSvg = () => supportsSvg_; -var userAgent = navigator.userAgent; -var svg = document.createElementNS(NS.SVG, 'svg'); +const {userAgent} = navigator; +const svg = document.createElementNS(NS.SVG, 'svg'); // Note: Browser sniffing should only be used if no other detection method is possible -var isOpera_ = !!window.opera; -var isWebkit_ = userAgent.indexOf('AppleWebKit') >= 0; -var isGecko_ = userAgent.indexOf('Gecko/') >= 0; -var isIE_ = userAgent.indexOf('MSIE') >= 0; -var isChrome_ = userAgent.indexOf('Chrome/') >= 0; -var isWindows_ = userAgent.indexOf('Windows') >= 0; -var isMac_ = userAgent.indexOf('Macintosh') >= 0; -var isTouch_ = 'ontouchstart' in window; +const isOpera_ = !!window.opera; +const isWebkit_ = userAgent.includes('AppleWebKit'); +const isGecko_ = userAgent.includes('Gecko/'); +const isIE_ = userAgent.includes('MSIE'); +const isChrome_ = userAgent.includes('Chrome/'); +const isWindows_ = userAgent.includes('Windows'); +const isMac_ = userAgent.includes('Macintosh'); +const isTouch_ = 'ontouchstart' in window; -var supportsSelectors_ = (function () { - return !!svg.querySelector; +const supportsSelectors_ = (function () { +return !!svg.querySelector; }()); -var supportsXpath_ = (function () { - return !!document.evaluate; +const supportsXpath_ = (function () { +return !!document.evaluate; }()); // segList functions (for FF1.5 and 2.0) -var supportsPathReplaceItem_ = (function () { - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 10,10'); - var seglist = path.pathSegList; - var seg = path.createSVGPathSegLinetoAbs(5, 5); - try { - seglist.replaceItem(seg, 1); - return true; - } catch (err) {} - return false; +const supportsPathReplaceItem_ = (function () { +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,10'); +const seglist = path.pathSegList; +const seg = path.createSVGPathSegLinetoAbs(5, 5); +try { + seglist.replaceItem(seg, 1); + return true; +} catch (err) {} +return false; }()); -var supportsPathInsertItemBefore_ = (function () { - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 10,10'); - var seglist = path.pathSegList; - var seg = path.createSVGPathSegLinetoAbs(5, 5); - try { - seglist.insertItemBefore(seg, 1); - return true; - } catch (err) {} - return false; +const supportsPathInsertItemBefore_ = (function () { +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,10'); +const seglist = path.pathSegList; +const seg = path.createSVGPathSegLinetoAbs(5, 5); +try { + seglist.insertItemBefore(seg, 1); + return true; +} catch (err) {} +return false; }()); // text character positioning (for IE9) -var supportsGoodTextCharPos_ = (function () { - var svgroot = document.createElementNS(NS.SVG, 'svg'); - var svgcontent = document.createElementNS(NS.SVG, 'svg'); - document.documentElement.appendChild(svgroot); - svgcontent.setAttribute('x', 5); - svgroot.appendChild(svgcontent); - var text = document.createElementNS(NS.SVG, 'text'); - text.textContent = 'a'; - svgcontent.appendChild(text); - var pos = text.getStartPositionOfChar(0).x; - document.documentElement.removeChild(svgroot); - return (pos === 0); +const supportsGoodTextCharPos_ = (function () { +const svgroot = document.createElementNS(NS.SVG, 'svg'); +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.appendChild(svgroot); +svgcontent.setAttribute('x', 5); +svgroot.appendChild(svgcontent); +const text = document.createElementNS(NS.SVG, 'text'); +text.textContent = 'a'; +svgcontent.appendChild(text); +const pos = text.getStartPositionOfChar(0).x; +document.documentElement.removeChild(svgroot); +return (pos === 0); }()); -var supportsPathBBox_ = (function () { - var svgcontent = document.createElementNS(NS.SVG, 'svg'); - document.documentElement.appendChild(svgcontent); - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 C0,0 10,10 10,0'); - svgcontent.appendChild(path); - var bbox = path.getBBox(); - document.documentElement.removeChild(svgcontent); - return (bbox.height > 4 && bbox.height < 5); +const supportsPathBBox_ = (function () { +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.appendChild(svgcontent); +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 C0,0 10,10 10,0'); +svgcontent.appendChild(path); +const bbox = path.getBBox(); +document.documentElement.removeChild(svgcontent); +return (bbox.height > 4 && bbox.height < 5); }()); // Support for correct bbox sizing on groups with horizontal/vertical lines -var supportsHVLineContainerBBox_ = (function () { - var svgcontent = document.createElementNS(NS.SVG, 'svg'); - document.documentElement.appendChild(svgcontent); - var path = document.createElementNS(NS.SVG, 'path'); - path.setAttribute('d', 'M0,0 10,0'); - var path2 = document.createElementNS(NS.SVG, 'path'); - path2.setAttribute('d', 'M5,0 15,0'); - var g = document.createElementNS(NS.SVG, 'g'); - g.appendChild(path); - g.appendChild(path2); - svgcontent.appendChild(g); - var bbox = g.getBBox(); - document.documentElement.removeChild(svgcontent); - // Webkit gives 0, FF gives 10, Opera (correctly) gives 15 - return (bbox.width === 15); +const supportsHVLineContainerBBox_ = (function () { +const svgcontent = document.createElementNS(NS.SVG, 'svg'); +document.documentElement.appendChild(svgcontent); +const path = document.createElementNS(NS.SVG, 'path'); +path.setAttribute('d', 'M0,0 10,0'); +const path2 = document.createElementNS(NS.SVG, 'path'); +path2.setAttribute('d', 'M5,0 15,0'); +const g = document.createElementNS(NS.SVG, 'g'); +g.appendChild(path); +g.appendChild(path2); +svgcontent.appendChild(g); +const bbox = g.getBBox(); +document.documentElement.removeChild(svgcontent); +// Webkit gives 0, FF gives 10, Opera (correctly) gives 15 +return (bbox.width === 15); }()); -var supportsEditableText_ = (function () { - // TODO: Find better way to check support for this - return isOpera_; +const supportsEditableText_ = (function () { +// TODO: Find better way to check support for this +return isOpera_; }()); -var supportsGoodDecimals_ = (function () { - // Correct decimals on clone attributes (Opera < 10.5/win/non-en) - var rect = document.createElementNS(NS.SVG, 'rect'); - rect.setAttribute('x', 0.1); - var crect = rect.cloneNode(false); - var retValue = (crect.getAttribute('x').indexOf(',') === -1); - if (!retValue) { - $.alert('NOTE: This version of Opera is known to contain bugs in SVG-edit.\n' + - 'Please upgrade to the latest version in which the problems have been fixed.'); - } - return retValue; +const supportsGoodDecimals_ = (function () { +// Correct decimals on clone attributes (Opera < 10.5/win/non-en) +const rect = document.createElementNS(NS.SVG, 'rect'); +rect.setAttribute('x', 0.1); +const crect = rect.cloneNode(false); +const retValue = (!crect.getAttribute('x').includes(',')); +if (!retValue) { + $.alert('NOTE: This version of Opera is known to contain bugs in SVG-edit.\n' + + 'Please upgrade to the latest version in which the problems have been fixed.'); +} +return retValue; }()); -var supportsNonScalingStroke_ = (function () { - var rect = document.createElementNS(NS.SVG, 'rect'); - rect.setAttribute('style', 'vector-effect:non-scaling-stroke'); - return rect.style.vectorEffect === 'non-scaling-stroke'; +const supportsNonScalingStroke_ = (function () { +const rect = document.createElementNS(NS.SVG, 'rect'); +rect.setAttribute('style', 'vector-effect:non-scaling-stroke'); +return rect.style.vectorEffect === 'non-scaling-stroke'; }()); -var supportsNativeSVGTransformLists_ = (function () { - var rect = document.createElementNS(NS.SVG, 'rect'); - var rxform = rect.transform.baseVal; - var t1 = svg.createSVGTransform(); - rxform.appendItem(t1); - var r1 = rxform.getItem(0); - return r1 instanceof SVGTransform && t1 instanceof SVGTransform && - r1.type === t1.type && r1.angle === t1.angle && - r1.matrix.a === t1.matrix.a && - r1.matrix.b === t1.matrix.b && - r1.matrix.c === t1.matrix.c && - r1.matrix.d === t1.matrix.d && - r1.matrix.e === t1.matrix.e && - r1.matrix.f === t1.matrix.f; +const supportsNativeSVGTransformLists_ = (function () { +const rect = document.createElementNS(NS.SVG, 'rect'); +const rxform = rect.transform.baseVal; +const t1 = svg.createSVGTransform(); +rxform.appendItem(t1); +const r1 = rxform.getItem(0); +// Todo: Do frame-independent instance checking +return r1 instanceof SVGTransform && t1 instanceof SVGTransform && + r1.type === t1.type && r1.angle === t1.angle && + r1.matrix.a === t1.matrix.a && + r1.matrix.b === t1.matrix.b && + r1.matrix.c === t1.matrix.c && + r1.matrix.d === t1.matrix.d && + r1.matrix.e === t1.matrix.e && + r1.matrix.f === t1.matrix.f; }()); // Public API -svgedit.browser.isOpera = function () { return isOpera_; }; -svgedit.browser.isWebkit = function () { return isWebkit_; }; -svgedit.browser.isGecko = function () { return isGecko_; }; -svgedit.browser.isIE = function () { return isIE_; }; -svgedit.browser.isChrome = function () { return isChrome_; }; -svgedit.browser.isWindows = function () { return isWindows_; }; -svgedit.browser.isMac = function () { return isMac_; }; -svgedit.browser.isTouch = function () { return isTouch_; }; +export const isOpera = () => isOpera_; +export const isWebkit = () => isWebkit_; +export const isGecko = () => isGecko_; +export const isIE = () => isIE_; +export const isChrome = () => isChrome_; +export const isWindows = () => isWindows_; +export const isMac = () => isMac_; +export const isTouch = () => isTouch_; -svgedit.browser.supportsSelectors = function () { return supportsSelectors_; }; -svgedit.browser.supportsXpath = function () { return supportsXpath_; }; +export const supportsSelectors = () => supportsSelectors_; +export const supportsXpath = () => supportsXpath_; -svgedit.browser.supportsPathReplaceItem = function () { return supportsPathReplaceItem_; }; -svgedit.browser.supportsPathInsertItemBefore = function () { return supportsPathInsertItemBefore_; }; -svgedit.browser.supportsPathBBox = function () { return supportsPathBBox_; }; -svgedit.browser.supportsHVLineContainerBBox = function () { return supportsHVLineContainerBBox_; }; -svgedit.browser.supportsGoodTextCharPos = function () { return supportsGoodTextCharPos_; }; -svgedit.browser.supportsEditableText = function () { return supportsEditableText_; }; -svgedit.browser.supportsGoodDecimals = function () { return supportsGoodDecimals_; }; -svgedit.browser.supportsNonScalingStroke = function () { return supportsNonScalingStroke_; }; -svgedit.browser.supportsNativeTransformLists = function () { return supportsNativeSVGTransformLists_; }; -}()); +export const supportsPathReplaceItem = () => supportsPathReplaceItem_; +export const supportsPathInsertItemBefore = () => supportsPathInsertItemBefore_; +export const supportsPathBBox = () => supportsPathBBox_; +export const supportsHVLineContainerBBox = () => supportsHVLineContainerBBox_; +export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_; +export const supportsEditableText = () => supportsEditableText_; +export const supportsGoodDecimals = () => supportsGoodDecimals_; +export const supportsNonScalingStroke = () => supportsNonScalingStroke_; +export const supportsNativeTransformLists = () => supportsNativeSVGTransformLists_; diff --git a/editor/canvg/canvg.js b/editor/canvg/canvg.js index eb9ed2fa..954ac92e 100644 --- a/editor/canvg/canvg.js +++ b/editor/canvg/canvg.js @@ -1,15 +1,14 @@ -/* eslint-disable no-var, no-redeclare, new-cap */ -/* globals RGBColor, stackBlurCanvasRGBA, ActiveXObject */ +/* eslint-disable new-cap */ +/* globals stackBlurCanvasRGBA, ActiveXObject */ /* * canvg.js - Javascript SVG parser and renderer on Canvas * MIT Licensed * Gabe Lerner (gabelerner@gmail.com) * http://code.google.com/p/canvg/ - * - * Requires: rgbcolor.js - https://www.phpied.com/rgb-color-parser-in-javascript/ */ -var canvg; -(function () { + +import RGBColor from './rgbcolor.js'; + // canvg(target, s) // empty parameters: replace all 'svg' elements on page with 'canvas' elements // target: canvas element or the id of a canvas element @@ -25,18 +24,18 @@ var canvg; // scaleHeight: int => scales vertically to height // renderCallback: function => will call the function after the first render is completed // forceRedraw: function => will call the function on every frame, if it returns true, will redraw -canvg = function (target, s, opts) { +export default function canvg (target, s, opts) { // no parameters if (target == null && s == null && opts == null) { - var svgTags = document.querySelectorAll('svg'); - for (var i = 0; i < svgTags.length; i++) { - var svgTag = svgTags[i]; - var c = document.createElement('canvas'); + const svgTags = document.querySelectorAll('svg'); + for (let i = 0; i < svgTags.length; i++) { + const svgTag = svgTags[i]; + const c = document.createElement('canvas'); c.width = svgTag.clientWidth; c.height = svgTag.clientHeight; svgTag.parentNode.insertBefore(c, svgTag); svgTag.parentNode.removeChild(svgTag); - var div = document.createElement('div'); + const div = document.createElement('div'); div.appendChild(svgTag); canvg(c, div.innerHTML); } @@ -49,13 +48,13 @@ canvg = function (target, s, opts) { // store class on canvas if (target.svg != null) target.svg.stop(); - var svg = build(opts || {}); + const svg = build(opts || {}); // on i.e. 8 for flash canvas, we can't assign the property so check for it if (!(target.childNodes.length === 1 && target.childNodes[0].nodeName === 'OBJECT')) { target.svg = svg; } - var ctx = target.getContext('2d'); + const ctx = target.getContext('2d'); if (typeof s.documentElement !== 'undefined') { // load from xml doc svg.loadXmlDoc(ctx, s); @@ -69,7 +68,7 @@ canvg = function (target, s, opts) { }; function build (opts) { - var svg = {opts: opts}; + const svg = {opts}; svg.FRAMERATE = 30; svg.MAX_VIRTUAL_PIXELS = 30000; @@ -81,7 +80,7 @@ function build (opts) { // globals svg.init = function (ctx) { - var uniqueId = 0; + let uniqueId = 0; svg.UniqueId = function () { uniqueId++; return 'canvg' + uniqueId; }; svg.Definitions = {}; svg.Styles = {}; @@ -91,7 +90,7 @@ function build (opts) { svg.ViewPort = new function () { this.viewPorts = []; this.Clear = function () { this.viewPorts = []; }; - this.SetCurrent = function (width, height) { this.viewPorts.push({ width: width, height: height }); }; + this.SetCurrent = function (width, height) { this.viewPorts.push({ width, height }); }; this.RemoveCurrent = function () { this.viewPorts.pop(); }; this.Current = function () { return this.viewPorts[this.viewPorts.length - 1]; }; this.width = function () { return this.Current().width; }; @@ -108,7 +107,7 @@ function build (opts) { // images loaded svg.ImagesLoaded = function () { - for (var i = 0; i < svg.Images.length; i++) { + for (let i = 0; i < svg.Images.length; i++) { if (!svg.Images[i].loaded) return false; } return true; @@ -122,12 +121,10 @@ function build (opts) { // ajax svg.ajax = function (url) { - var AJAX; - if (window.XMLHttpRequest) { - AJAX = new XMLHttpRequest(); - } else { - AJAX = new ActiveXObject('Microsoft.XMLHTTP'); - } + const AJAX = window.XMLHttpRequest + ? new XMLHttpRequest() + : new ActiveXObject('Microsoft.XMLHTTP'); + if (AJAX) { AJAX.open('GET', url, false); AJAX.send(null); @@ -139,158 +136,20 @@ function build (opts) { // parse xml svg.parseXml = function (xml) { if (window.DOMParser) { - var parser = new DOMParser(); + const parser = new DOMParser(); return parser.parseFromString(xml, 'text/xml'); } else { xml = xml.replace(/]*>/, ''); - var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + const xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); xmlDoc.async = 'false'; xmlDoc.loadXML(xml); return xmlDoc; } }; - svg.Property = function (name, value) { - this.name = name; - this.value = value; - }; - svg.Property.prototype.getValue = function () { - return this.value; - }; - - svg.Property.prototype.hasValue = function () { - return (this.value != null && this.value !== ''); - }; - - // return the numerical value of the property - svg.Property.prototype.numValue = function () { - if (!this.hasValue()) return 0; - - var n = parseFloat(this.value); - if ((this.value + '').match(/%$/)) { - n = n / 100.0; - } - return n; - }; - - svg.Property.prototype.valueOrDefault = function (def) { - if (this.hasValue()) return this.value; - return def; - }; - - svg.Property.prototype.numValueOrDefault = function (def) { - if (this.hasValue()) return this.numValue(); - return def; - }; - - // color extensions - // augment the current color value with the opacity - svg.Property.prototype.addOpacity = function (opacityProp) { - var newValue = this.value; - if (opacityProp.value != null && opacityProp.value !== '' && typeof this.value === 'string') { // can only add opacity to colors, not patterns - var color = new RGBColor(this.value); - if (color.ok) { - newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; - } - } - return new svg.Property(this.name, newValue); - }; - - // definition extensions - // get the definition from the definitions table - svg.Property.prototype.getDefinition = function () { - var name = this.value.match(/#([^)'"]+)/); - if (name) { name = name[1]; } - if (!name) { name = this.value; } - return svg.Definitions[name]; - }; - - svg.Property.prototype.isUrlDefinition = function () { - return this.value.indexOf('url(') === 0; - }; - - svg.Property.prototype.getFillStyleDefinition = function (e, opacityProp) { - var def = this.getDefinition(); - - // gradient - if (def != null && def.createGradient) { - return def.createGradient(svg.ctx, e, opacityProp); - } - - // pattern - if (def != null && def.createPattern) { - if (def.getHrefAttribute().hasValue()) { - var pt = def.attribute('patternTransform'); - def = def.getHrefAttribute().getDefinition(); - if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } - } - return def.createPattern(svg.ctx, e); - } - - return null; - }; - - // length extensions - svg.Property.prototype.getDPI = function (viewPort) { - return 96.0; // TODO: compute? - }; - - svg.Property.prototype.getEM = function (viewPort) { - var em = 12; - - var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); - if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); - - return em; - }; - - svg.Property.prototype.getUnits = function () { - var s = this.value + ''; - return s.replace(/[0-9.-]/g, ''); - }; - - // get the length as pixels - svg.Property.prototype.toPixels = function (viewPort, processPercent) { - if (!this.hasValue()) return 0; - var s = this.value + ''; - if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); - if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; - if (s.match(/px$/)) return this.numValue(); - if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); - if (s.match(/pc$/)) return this.numValue() * 15; - if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; - if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; - if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); - if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); - var n = this.numValue(); - if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); - return n; - }; - - // time extensions - // get the time as milliseconds - svg.Property.prototype.toMilliseconds = function () { - if (!this.hasValue()) return 0; - var s = this.value + ''; - if (s.match(/s$/)) return this.numValue() * 1000; - if (s.match(/ms$/)) return this.numValue(); - return this.numValue(); - }; - - // angle extensions - // get the angle as radians - svg.Property.prototype.toRadians = function () { - if (!this.hasValue()) return 0; - var s = this.value + ''; - if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); - if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); - if (s.match(/rad$/)) return this.numValue(); - return this.numValue() * (Math.PI / 180.0); - }; - // text extensions // get the text baseline - var textBaselineMapping = { + const textBaselineMapping = { 'baseline': 'alphabetic', 'before-edge': 'top', 'text-before-edge': 'top', @@ -303,9 +162,151 @@ function build (opts) { 'hanging': 'hanging', 'mathematical': 'alphabetic' }; - svg.Property.prototype.toTextBaseline = function () { - if (!this.hasValue()) return null; - return textBaselineMapping[this.value]; + + svg.Property = class Property { + constructor (name, value) { + this.name = name; + this.value = value; + } + + getValue () { + return this.value; + } + + hasValue () { + return (this.value != null && this.value !== ''); + } + + // return the numerical value of the property + numValue () { + if (!this.hasValue()) return 0; + + let n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + valueOrDefault (def) { + if (this.hasValue()) return this.value; + return def; + } + + numValueOrDefault (def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + // color extensions + // augment the current color value with the opacity + addOpacity (opacityProp) { + let newValue = this.value; + if (opacityProp.value != null && opacityProp.value !== '' && typeof this.value === 'string') { // can only add opacity to colors, not patterns + const color = new RGBColor(this.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacityProp.numValue() + ')'; + } + } + return new svg.Property(this.name, newValue); + } + + // definition extensions + // get the definition from the definitions table + getDefinition () { + let name = this.value.match(/#([^)'"]+)/); + if (name) { name = name[1]; } + if (!name) { name = this.value; } + return svg.Definitions[name]; + } + + isUrlDefinition () { + return this.value.startsWith('url('); + } + + getFillStyleDefinition (e, opacityProp) { + let def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e, opacityProp); + } + + // pattern + if (def != null && def.createPattern) { + if (def.getHrefAttribute().hasValue()) { + const pt = def.attribute('patternTransform'); + def = def.getHrefAttribute().getDefinition(); + if (pt.hasValue()) { def.attribute('patternTransform', true).value = pt.value; } + } + return def.createPattern(svg.ctx, e); + } + + return null; + } + + // length extensions + getDPI (viewPort) { + return 96.0; // TODO: compute? + } + + getEM (viewPort) { + let em = 12; + + const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.toPixels(viewPort); + + return em; + } + + getUnits () { + const s = this.value + ''; + return s.replace(/[0-9.-]/g, ''); + } + + // get the length as pixels + toPixels (viewPort, processPercent) { + if (!this.hasValue()) return 0; + const s = this.value + ''; + if (s.match(/em$/)) return this.numValue() * this.getEM(viewPort); + if (s.match(/ex$/)) return this.numValue() * this.getEM(viewPort) / 2.0; + if (s.match(/px$/)) return this.numValue(); + if (s.match(/pt$/)) return this.numValue() * this.getDPI(viewPort) * (1.0 / 72.0); + if (s.match(/pc$/)) return this.numValue() * 15; + if (s.match(/cm$/)) return this.numValue() * this.getDPI(viewPort) / 2.54; + if (s.match(/mm$/)) return this.numValue() * this.getDPI(viewPort) / 25.4; + if (s.match(/in$/)) return this.numValue() * this.getDPI(viewPort); + if (s.match(/%$/)) return this.numValue() * svg.ViewPort.ComputeSize(viewPort); + const n = this.numValue(); + if (processPercent && n < 1.0) return n * svg.ViewPort.ComputeSize(viewPort); + return n; + } + + // time extensions + // get the time as milliseconds + toMilliseconds () { + if (!this.hasValue()) return 0; + const s = this.value + ''; + if (s.match(/s$/)) return this.numValue() * 1000; + if (s.match(/ms$/)) return this.numValue(); + return this.numValue(); + } + + // angle extensions + // get the angle as radians + toRadians () { + if (!this.hasValue()) return 0; + const s = this.value + ''; + if (s.match(/deg$/)) return this.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return this.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return this.numValue(); + return this.numValue() * (Math.PI / 180.0); + } + + toTextBaseline () { + if (!this.hasValue()) return null; + return textBaselineMapping[this.value]; + } }; // fonts @@ -315,14 +316,14 @@ function build (opts) { this.Weights = 'normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit'; this.CreateFont = function (fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { - var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + const f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); return { fontFamily: fontFamily || f.fontFamily, fontSize: fontSize || f.fontSize, fontStyle: fontStyle || f.fontStyle, fontWeight: fontWeight || f.fontWeight, fontVariant: fontVariant || f.fontVariant, - toString: function () { + toString () { return [ this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily @@ -331,18 +332,18 @@ function build (opts) { }; }; - var that = this; + const that = this; this.Parse = function (s) { - var f = {}; - var d = svg.trim(svg.compressSpaces(s || '')).split(' '); - var set = {fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false}; - var ff = ''; - for (var i = 0; i < d.length; i++) { - if (!set.fontStyle && that.Styles.indexOf(d[i]) > -1) { + const f = {}; + const d = svg.trim(svg.compressSpaces(s || '')).split(' '); + const set = {fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false}; + let ff = ''; + for (let i = 0; i < d.length; i++) { + if (!set.fontStyle && that.Styles.includes(d[i])) { if (d[i] !== 'inherit') f.fontStyle = d[i]; set.fontStyle = true; - } else if (!set.fontVariant && that.Variants.indexOf(d[i]) > -1) { + } else if (!set.fontVariant && that.Variants.includes(d[i])) { if (d[i] !== 'inherit') f.fontVariant = d[i]; set.fontStyle = set.fontVariant = true; - } else if (!set.fontWeight && that.Weights.indexOf(d[i]) > -1) { + } else if (!set.fontWeight && that.Weights.includes(d[i])) { if (d[i] !== 'inherit') f.fontWeight = d[i]; set.fontStyle = set.fontVariant = set.fontWeight = true; } else if (!set.fontSize) { if (d[i] !== 'inherit') f.fontSize = d[i].split('/')[0]; set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; @@ -357,35 +358,38 @@ function build (opts) { // points and paths svg.ToNumberArray = function (s) { - var a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); - for (var i = 0; i < a.length; i++) { + const a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + for (let i = 0; i < a.length; i++) { a[i] = parseFloat(a[i]); } return a; }; - svg.Point = function (x, y) { - this.x = x; - this.y = y; - }; - svg.Point.prototype.angleTo = function (p) { - return Math.atan2(p.y - this.y, p.x - this.x); - }; + svg.Point = class { + constructor (x, y) { + this.x = x; + this.y = y; + } - svg.Point.prototype.applyTransform = function (v) { - var xp = this.x * v[0] + this.y * v[2] + v[4]; - var yp = this.x * v[1] + this.y * v[3] + v[5]; - this.x = xp; - this.y = yp; + angleTo (p) { + return Math.atan2(p.y - this.y, p.x - this.x); + } + + applyTransform (v) { + const xp = this.x * v[0] + this.y * v[2] + v[4]; + const yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + } }; svg.CreatePoint = function (s) { - var a = svg.ToNumberArray(s); + const a = svg.ToNumberArray(s); return new svg.Point(a[0], a[1]); }; svg.CreatePath = function (s) { - var a = svg.ToNumberArray(s); - var path = []; - for (var i = 0; i < a.length; i += 2) { + const a = svg.ToNumberArray(s); + const path = []; + for (let i = 0; i < a.length; i += 2) { path.push(new svg.Point(a[i], a[i + 1])); } return path; @@ -431,34 +435,34 @@ function build (opts) { }; this.addQuadraticCurve = function (p0x, p0y, p1x, p1y, p2x, p2y) { - var cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) - var cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) - var cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) - var cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + const cp1x = p0x + 2 / 3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + const cp1y = p0y + 2 / 3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + const cp2x = cp1x + 1 / 3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + const cp2y = cp1y + 1 / 3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); }; this.addBezierCurve = function (p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html - var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + const p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; this.addPoint(p0[0], p0[1]); this.addPoint(p3[0], p3[1]); - for (var i = 0; i <= 1; i++) { - var f = function (t) { + for (let i = 0; i <= 1; i++) { + const f = function (t) { return Math.pow(1 - t, 3) * p0[i] + 3 * Math.pow(1 - t, 2) * t * p1[i] + 3 * (1 - t) * Math.pow(t, 2) * p2[i] + Math.pow(t, 3) * p3[i]; }; - var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; - var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; - var c = 3 * p1[i] - 3 * p0[i]; + const b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + const a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + const c = 3 * p1[i] - 3 * p0[i]; if (a === 0) { if (b === 0) continue; - var t = -c / b; + const t = -c / b; if (t > 0 && t < 1) { if (i === 0) this.addX(f(t)); if (i === 1) this.addY(f(t)); @@ -466,14 +470,14 @@ function build (opts) { continue; } - var b2ac = Math.pow(b, 2) - 4 * c * a; + const b2ac = Math.pow(b, 2) - 4 * c * a; if (b2ac < 0) continue; - var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + const t1 = (-b + Math.sqrt(b2ac)) / (2 * a); if (t1 > 0 && t1 < 1) { if (i === 0) this.addX(f(t1)); if (i === 1) this.addY(f(t1)); } - var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + const t2 = (-b - Math.sqrt(b2ac)) / (2 * a); if (t2 > 0 && t2 < 1) { if (i === 0) this.addX(f(t2)); if (i === 1) this.addY(f(t2)); @@ -491,7 +495,7 @@ function build (opts) { // transforms svg.Transform = function (v) { - var that = this; + const that = this; this.Type = {}; // translate @@ -510,7 +514,7 @@ function build (opts) { // rotate this.Type.rotate = function (s) { - var a = svg.ToNumberArray(s); + const a = svg.ToNumberArray(s); this.angle = new svg.Property('angle', a[0]); this.cx = a[1] || 0; this.cy = a[2] || 0; @@ -525,7 +529,7 @@ function build (opts) { ctx.translate(-this.cx, -this.cy); }; this.applyToPoint = function (p) { - var a = this.angle.toRadians(); + const a = this.angle.toRadians(); p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); @@ -555,52 +559,58 @@ function build (opts) { }; }; - this.Type.SkewBase = function (s) { - this.base = that.Type.matrix; - this.base(s); - this.angle = new svg.Property('angle', s); + this.Type.SkewBase = class extends this.Type.matrix { + constructor (s) { + super(); + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + } }; - this.Type.SkewBase.prototype = new this.Type.matrix(); - this.Type.skewX = function (s) { - this.base = that.Type.SkewBase; - this.base(s); - this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; + this.Type.skewX = class extends this.Type.SkewBase { + constructor (s) { + super(); + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.toRadians()), 1, 0, 0]; + } }; - this.Type.skewX.prototype = new this.Type.SkewBase(); - this.Type.skewY = function (s) { - this.base = that.Type.SkewBase; - this.base(s); - this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; + this.Type.skewY = class extends this.Type.SkewBase { + constructor (s) { + super(); + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.toRadians()), 0, 1, 0, 0]; + } }; - this.Type.skewY.prototype = new this.Type.SkewBase(); this.transforms = []; this.apply = function (ctx) { - for (var i = 0; i < this.transforms.length; i++) { + for (let i = 0; i < this.transforms.length; i++) { this.transforms[i].apply(ctx); } }; this.unapply = function (ctx) { - for (var i = this.transforms.length - 1; i >= 0; i--) { + for (let i = this.transforms.length - 1; i >= 0; i--) { this.transforms[i].unapply(ctx); } }; this.applyToPoint = function (p) { - for (var i = 0; i < this.transforms.length; i++) { + for (let i = 0; i < this.transforms.length; i++) { this.transforms[i].applyToPoint(p); } }; - var data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); - for (var i = 0; i < data.length; i++) { - var type = svg.trim(data[i].split('(')[0]); - var s = data[i].split('(')[1].replace(')', ''); - var transform = new this.Type[type](s); + const data = svg.trim(svg.compressSpaces(v)).replace(/\)([a-zA-Z])/g, ') $1').replace(/\)(\s?,\s?)/g, ') ').split(/\s(?=[a-z])/); + for (let i = 0; i < data.length; i++) { + const type = svg.trim(data[i].split('(')[0]); + const s = data[i].split('(')[1].replace(')', ''); + const transform = new this.Type[type](s); transform.type = type; this.transforms.push(transform); } @@ -611,14 +621,14 @@ function build (opts) { // aspect ratio - https://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute aspectRatio = svg.compressSpaces(aspectRatio); aspectRatio = aspectRatio.replace(/^defer\s/, ''); // ignore defer - var align = aspectRatio.split(' ')[0] || 'xMidYMid'; - var meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; + const align = aspectRatio.split(' ')[0] || 'xMidYMid'; + const meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; // calculate scale - var scaleX = width / desiredWidth; - var scaleY = height / desiredHeight; - var scaleMin = Math.min(scaleX, scaleY); - var scaleMax = Math.max(scaleX, scaleY); + const scaleX = width / desiredWidth; + const scaleY = height / desiredHeight; + const scaleMin = Math.min(scaleX, scaleY); + const scaleMax = Math.max(scaleX, scaleY); if (meetOrSlice === 'meet') { desiredWidth *= scaleMin; desiredHeight *= scaleMin; } if (meetOrSlice === 'slice') { desiredWidth *= scaleMax; desiredHeight *= scaleMax; } @@ -655,7 +665,7 @@ function build (opts) { // get or create attribute this.attribute = function (name, createIfNotExists) { - var a = this.attributes[name]; + let a = this.attributes[name]; if (a != null) return a; if (createIfNotExists === true) { a = new svg.Property(name, ''); this.attributes[name] = a; } @@ -663,7 +673,7 @@ function build (opts) { }; this.getHrefAttribute = function () { - for (var a in this.attributes) { + for (const a in this.attributes) { if (a.match(/:href$/)) { return this.attributes[a]; } @@ -673,19 +683,19 @@ function build (opts) { // get or create style, crawls up node tree this.style = function (name, createIfNotExists, skipAncestors) { - var s = this.styles[name]; + let s = this.styles[name]; if (s != null) return s; - var a = this.attribute(name); + const a = this.attribute(name); if (a != null && a.hasValue()) { this.styles[name] = a; // move up to me to cache return a; } if (skipAncestors !== true) { - var p = this.parent; + const p = this.parent; if (p != null) { - var ps = p.style(name); + const ps = p.style(name); if (ps != null && ps.hasValue()) { return ps; } @@ -706,10 +716,10 @@ function build (opts) { ctx.save(); if (this.attribute('mask').hasValue()) { // mask - var mask = this.attribute('mask').getDefinition(); + const mask = this.attribute('mask').getDefinition(); if (mask != null) mask.apply(ctx, this); } else if (this.style('filter').hasValue()) { // filter - var filter = this.style('filter').getDefinition(); + const filter = this.style('filter').getDefinition(); if (filter != null) filter.apply(ctx, this); } else { this.setContext(ctx); @@ -731,24 +741,25 @@ function build (opts) { // base render children this.renderChildren = function (ctx) { - for (var i = 0; i < this.children.length; i++) { + for (let i = 0; i < this.children.length; i++) { this.children[i].render(ctx); } }; this.addChild = function (childNode, create) { - var child = childNode; - if (create) child = svg.CreateElement(childNode); + const child = create + ? svg.CreateElement(childNode) + : childNode; child.parent = this; if (child.type !== 'title') { this.children.push(child); } }; if (node != null && node.nodeType === 1) { // ELEMENT_NODE // add children - for (var i = 0, childNode; (childNode = node.childNodes[i]); i++) { + for (let i = 0, childNode; (childNode = node.childNodes[i]); i++) { if (childNode.nodeType === 1) this.addChild(childNode, true); // ELEMENT_NODE if (this.captureTextNodes && (childNode.nodeType === 3 || childNode.nodeType === 4)) { - var text = childNode.nodeValue || childNode.text || ''; + const text = childNode.nodeValue || childNode.text || ''; if (svg.trim(svg.compressSpaces(text)) !== '') { this.addChild(new svg.Element.tspan(childNode), false); // TEXT_NODE } @@ -756,32 +767,32 @@ function build (opts) { } // add attributes - for (var i = 0; i < node.attributes.length; i++) { - var attribute = node.attributes[i]; + for (let i = 0; i < node.attributes.length; i++) { + const attribute = node.attributes[i]; this.attributes[attribute.nodeName] = new svg.Property(attribute.nodeName, attribute.nodeValue); } // add tag styles - var styles = svg.Styles[node.nodeName]; + let styles = svg.Styles[node.nodeName]; if (styles != null) { - for (var name in styles) { + for (const name in styles) { this.styles[name] = styles[name]; } } // add class styles if (this.attribute('class').hasValue()) { - var classes = svg.compressSpaces(this.attribute('class').value).split(' '); - for (var j = 0; j < classes.length; j++) { + const classes = svg.compressSpaces(this.attribute('class').value).split(' '); + for (let j = 0; j < classes.length; j++) { styles = svg.Styles['.' + classes[j]]; if (styles != null) { - for (var name in styles) { + for (const name in styles) { this.styles[name] = styles[name]; } } styles = svg.Styles[node.nodeName + '.' + classes[j]]; if (styles != null) { - for (var name in styles) { + for (const name in styles) { this.styles[name] = styles[name]; } } @@ -790,9 +801,9 @@ function build (opts) { // add id styles if (this.attribute('id').hasValue()) { - var styles = svg.Styles['#' + this.attribute('id').value]; + const styles = svg.Styles['#' + this.attribute('id').value]; if (styles != null) { - for (var name in styles) { + for (const name in styles) { this.styles[name] = styles[name]; } } @@ -800,12 +811,12 @@ function build (opts) { // add inline styles if (this.attribute('style').hasValue()) { - var styles = this.attribute('style').value.split(';'); - for (var i = 0; i < styles.length; i++) { + const styles = this.attribute('style').value.split(';'); + for (let i = 0; i < styles.length; i++) { if (svg.trim(styles[i]) !== '') { - var style = styles[i].split(':'); - var name = svg.trim(style[0]); - var value = svg.trim(style[1]); + const style = styles[i].split(':'); + const name = svg.trim(style[0]); + const value = svg.trim(style[1]); this.styles[name] = new svg.Property(name, value); } } @@ -820,1581 +831,1651 @@ function build (opts) { } }; - svg.Element.RenderedElementBase = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.RenderedElementBase = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.setContext = function (ctx) { - // fill - if (this.style('fill').isUrlDefinition()) { - var fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); - if (fs != null) ctx.fillStyle = fs; - } else if (this.style('fill').hasValue()) { - var fillStyle = this.style('fill'); - if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; - ctx.fillStyle = (fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); - } - if (this.style('fill-opacity').hasValue()) { - var fillStyle = new svg.Property('fill', ctx.fillStyle); - fillStyle = fillStyle.addOpacity(this.style('fill-opacity')); - ctx.fillStyle = fillStyle.value; - } - - // stroke - if (this.style('stroke').isUrlDefinition()) { - var fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); - if (fs != null) ctx.strokeStyle = fs; - } else if (this.style('stroke').hasValue()) { - var strokeStyle = this.style('stroke'); - if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; - ctx.strokeStyle = (strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); - } - if (this.style('stroke-opacity').hasValue()) { - var strokeStyle = new svg.Property('stroke', ctx.strokeStyle); - strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity')); - ctx.strokeStyle = strokeStyle.value; - } - if (this.style('stroke-width').hasValue()) { - var newLineWidth = this.style('stroke-width').toPixels(); - ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 - } - if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; - if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; - if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; - if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { - var gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); - if (typeof ctx.setLineDash !== 'undefined') { - ctx.setLineDash(gaps); - } else if (typeof ctx.webkitLineDash !== 'undefined') { - ctx.webkitLineDash = gaps; - } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { - ctx.mozDash = gaps; + this.setContext = function (ctx) { + // fill + if (this.style('fill').isUrlDefinition()) { + const fs = this.style('fill').getFillStyleDefinition(this, this.style('fill-opacity')); + if (fs != null) ctx.fillStyle = fs; + } else if (this.style('fill').hasValue()) { + const fillStyle = this.style('fill'); + if (fillStyle.value === 'currentColor') fillStyle.value = this.style('color').value; + ctx.fillStyle = (fillStyle.value === 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); + } + if (this.style('fill-opacity').hasValue()) { + let fillStyle = new svg.Property('fill', ctx.fillStyle); + fillStyle = fillStyle.addOpacity(this.style('fill-opacity')); + ctx.fillStyle = fillStyle.value; } - var offset = this.style('stroke-dashoffset').numValueOrDefault(1); - if (typeof ctx.lineDashOffset !== 'undefined') { - ctx.lineDashOffset = offset; - } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { - ctx.webkitLineDashOffset = offset; - } else if (typeof ctx.mozDashOffset !== 'undefined') { - ctx.mozDashOffset = offset; + // stroke + if (this.style('stroke').isUrlDefinition()) { + const fs = this.style('stroke').getFillStyleDefinition(this, this.style('stroke-opacity')); + if (fs != null) ctx.strokeStyle = fs; + } else if (this.style('stroke').hasValue()) { + const strokeStyle = this.style('stroke'); + if (strokeStyle.value === 'currentColor') strokeStyle.value = this.style('color').value; + ctx.strokeStyle = (strokeStyle.value === 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); } - } - - // font - if (typeof ctx.font !== 'undefined') { - ctx.font = svg.Font.CreateFont( - this.style('font-style').value, - this.style('font-variant').value, - this.style('font-weight').value, - this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', - this.style('font-family').value).toString(); - } - - // transform - if (this.attribute('transform').hasValue()) { - var transform = new svg.Transform(this.attribute('transform').value); - transform.apply(ctx); - } - - // clip - if (this.style('clip-path', false, true).hasValue()) { - var clip = this.style('clip-path', false, true).getDefinition(); - if (clip != null) clip.apply(ctx); - } - - // opacity - if (this.style('opacity').hasValue()) { - ctx.globalAlpha = this.style('opacity').numValue(); - } - }; - }; - svg.Element.RenderedElementBase.prototype = new svg.Element.ElementBase(); - - svg.Element.PathElementBase = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); - - this.path = function (ctx) { - if (ctx != null) ctx.beginPath(); - return new svg.BoundingBox(); - }; - - this.renderChildren = function (ctx) { - this.path(ctx); - svg.Mouse.checkPath(this, ctx); - if (ctx.fillStyle !== '') { - if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { - ctx.fill(this.style('fill-rule').value); - } else { - ctx.fill(); + if (this.style('stroke-opacity').hasValue()) { + let strokeStyle = new svg.Property('stroke', ctx.strokeStyle); + strokeStyle = strokeStyle.addOpacity(this.style('stroke-opacity')); + ctx.strokeStyle = strokeStyle.value; } - } - if (ctx.strokeStyle !== '') ctx.stroke(); - - var markers = this.getMarkers(); - if (markers != null) { - if (this.style('marker-start').isUrlDefinition()) { - var marker = this.style('marker-start').getDefinition(); - marker.render(ctx, markers[0][0], markers[0][1]); + if (this.style('stroke-width').hasValue()) { + const newLineWidth = this.style('stroke-width').toPixels(); + ctx.lineWidth = newLineWidth === 0 ? 0.001 : newLineWidth; // browsers don't respect 0 } - if (this.style('marker-mid').isUrlDefinition()) { - var marker = this.style('marker-mid').getDefinition(); - for (var i = 1; i < markers.length - 1; i++) { - marker.render(ctx, markers[i][0], markers[i][1]); + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + if (this.style('stroke-dasharray').hasValue() && this.style('stroke-dasharray').value !== 'none') { + const gaps = svg.ToNumberArray(this.style('stroke-dasharray').value); + if (typeof ctx.setLineDash !== 'undefined') { + ctx.setLineDash(gaps); + } else if (typeof ctx.webkitLineDash !== 'undefined') { + ctx.webkitLineDash = gaps; + } else if (typeof ctx.mozDash !== 'undefined' && !(gaps.length === 1 && gaps[0] === 0)) { + ctx.mozDash = gaps; + } + + const offset = this.style('stroke-dashoffset').numValueOrDefault(1); + if (typeof ctx.lineDashOffset !== 'undefined') { + ctx.lineDashOffset = offset; + } else if (typeof ctx.webkitLineDashOffset !== 'undefined') { + ctx.webkitLineDashOffset = offset; + } else if (typeof ctx.mozDashOffset !== 'undefined') { + ctx.mozDashOffset = offset; } } - if (this.style('marker-end').isUrlDefinition()) { - var marker = this.style('marker-end').getDefinition(); - marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + + // font + if (typeof ctx.font !== 'undefined') { + ctx.font = svg.Font.CreateFont( + this.style('font-style').value, + this.style('font-variant').value, + this.style('font-weight').value, + this.style('font-size').hasValue() ? this.style('font-size').toPixels() + 'px' : '', + this.style('font-family').value).toString(); } - } - }; - this.getBoundingBox = function () { - return this.path(); - }; + // transform + if (this.attribute('transform').hasValue()) { + const transform = new svg.Transform(this.attribute('transform').value); + transform.apply(ctx); + } - this.getMarkers = function () { - return null; - }; + // clip + if (this.style('clip-path', false, true).hasValue()) { + const clip = this.style('clip-path', false, true).getDefinition(); + if (clip != null) clip.apply(ctx); + } + + // opacity + if (this.style('opacity').hasValue()) { + ctx.globalAlpha = this.style('opacity').numValue(); + } + }; + } + }; + + svg.Element.PathElementBase = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.path = function (ctx) { + if (ctx != null) ctx.beginPath(); + return new svg.BoundingBox(); + }; + + this.renderChildren = function (ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + if (ctx.fillStyle !== '') { + if (this.style('fill-rule').valueOrDefault('inherit') !== 'inherit') { + ctx.fill(this.style('fill-rule').value); + } else { + ctx.fill(); + } + } + if (ctx.strokeStyle !== '') ctx.stroke(); + + const markers = this.getMarkers(); + if (markers != null) { + if (this.style('marker-start').isUrlDefinition()) { + const marker = this.style('marker-start').getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + if (this.style('marker-mid').isUrlDefinition()) { + const marker = this.style('marker-mid').getDefinition(); + for (let i = 1; i < markers.length - 1; i++) { + marker.render(ctx, markers[i][0], markers[i][1]); + } + } + if (this.style('marker-end').isUrlDefinition()) { + const marker = this.style('marker-end').getDefinition(); + marker.render(ctx, markers[markers.length - 1][0], markers[markers.length - 1][1]); + } + } + }; + + this.getBoundingBox = function () { + return this.path(); + }; + + this.getMarkers = function () { + return null; + }; + } }; - svg.Element.PathElementBase.prototype = new svg.Element.RenderedElementBase(); // svg element - svg.Element.svg = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); - - this.baseClearContext = this.clearContext; - this.clearContext = function (ctx) { - this.baseClearContext(ctx); - svg.ViewPort.RemoveCurrent(); - }; - - this.baseSetContext = this.setContext; - this.setContext = function (ctx) { - // initial values and defaults - ctx.strokeStyle = 'rgba(0,0,0,0)'; - ctx.lineCap = 'butt'; - ctx.lineJoin = 'miter'; - ctx.miterLimit = 4; - if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { - ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); - } - - this.baseSetContext(ctx); - - // create new view port - if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; - if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; - ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); - - var width = svg.ViewPort.width(); - var height = svg.ViewPort.height(); - - if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; - if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; - if (typeof this.root === 'undefined') { - width = this.attribute('width').toPixels('x'); - height = this.attribute('height').toPixels('y'); - - var x = 0; - var y = 0; - if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { - x = -this.attribute('refX').toPixels('x'); - y = -this.attribute('refY').toPixels('y'); - } - - if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { - ctx.beginPath(); - ctx.moveTo(x, y); - ctx.lineTo(width, y); - ctx.lineTo(width, height); - ctx.lineTo(x, height); - ctx.closePath(); - ctx.clip(); - } - } - svg.ViewPort.SetCurrent(width, height); - - // viewbox - if (this.attribute('viewBox').hasValue()) { - var viewBox = svg.ToNumberArray(this.attribute('viewBox').value); - var minX = viewBox[0]; - var minY = viewBox[1]; - width = viewBox[2]; - height = viewBox[3]; - - svg.AspectRatio( - ctx, - this.attribute('preserveAspectRatio').value, - svg.ViewPort.width(), - width, - svg.ViewPort.height(), - height, - minX, - minY, - this.attribute('refX').value, - this.attribute('refY').value - ); + svg.Element.svg = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); + this.baseClearContext = this.clearContext; + this.clearContext = function (ctx) { + this.baseClearContext(ctx); svg.ViewPort.RemoveCurrent(); - svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); - } - }; + }; + + this.baseSetContext = this.setContext; + this.setContext = function (ctx) { + // initial values and defaults + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + if (typeof ctx.font !== 'undefined' && typeof window.getComputedStyle !== 'undefined') { + ctx.font = window.getComputedStyle(ctx.canvas).getPropertyValue('font'); + } + + this.baseSetContext(ctx); + + // create new view port + if (!this.attribute('x').hasValue()) this.attribute('x', true).value = 0; + if (!this.attribute('y').hasValue()) this.attribute('y', true).value = 0; + ctx.translate(this.attribute('x').toPixels('x'), this.attribute('y').toPixels('y')); + + let width = svg.ViewPort.width(); + let height = svg.ViewPort.height(); + + if (!this.attribute('width').hasValue()) this.attribute('width', true).value = '100%'; + if (!this.attribute('height').hasValue()) this.attribute('height', true).value = '100%'; + if (typeof this.root === 'undefined') { + width = this.attribute('width').toPixels('x'); + height = this.attribute('height').toPixels('y'); + + let x = 0; + let y = 0; + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').toPixels('x'); + y = -this.attribute('refY').toPixels('y'); + } + + if (this.attribute('overflow').valueOrDefault('hidden') !== 'visible') { + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + } + svg.ViewPort.SetCurrent(width, height); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + const viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + const minX = viewBox[0]; + const minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio( + ctx, + this.attribute('preserveAspectRatio').value, + svg.ViewPort.width(), + width, + svg.ViewPort.height(), + height, + minX, + minY, + this.attribute('refX').value, + this.attribute('refY').value + ); + + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + }; + } }; - svg.Element.svg.prototype = new svg.Element.RenderedElementBase(); // rect element - svg.Element.rect = function (node) { - this.base = svg.Element.PathElementBase; - this.base(node); + svg.Element.rect = class extends svg.Element.PathElementBase { + constructor (node) { + super(); + this.base = svg.Element.PathElementBase; + this.base(node); - this.path = function (ctx) { - var x = this.attribute('x').toPixels('x'); - var y = this.attribute('y').toPixels('y'); - var width = this.attribute('width').toPixels('x'); - var height = this.attribute('height').toPixels('y'); - var rx = this.attribute('rx').toPixels('x'); - var ry = this.attribute('ry').toPixels('y'); - if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; - if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; - rx = Math.min(rx, width / 2.0); - ry = Math.min(ry, height / 2.0); - if (ctx != null) { - ctx.beginPath(); - ctx.moveTo(x + rx, y); - ctx.lineTo(x + width - rx, y); - ctx.quadraticCurveTo(x + width, y, x + width, y + ry); - ctx.lineTo(x + width, y + height - ry); - ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); - ctx.lineTo(x + rx, y + height); - ctx.quadraticCurveTo(x, y + height, x, y + height - ry); - ctx.lineTo(x, y + ry); - ctx.quadraticCurveTo(x, y, x + rx, y); - ctx.closePath(); - } + this.path = function (ctx) { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + let rx = this.attribute('rx').toPixels('x'); + let ry = this.attribute('ry').toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + rx = Math.min(rx, width / 2.0); + ry = Math.min(ry, height / 2.0); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + ry); + ctx.lineTo(x + width, y + height - ry); + ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height); + ctx.lineTo(x + rx, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - ry); + ctx.lineTo(x, y + ry); + ctx.quadraticCurveTo(x, y, x + rx, y); + ctx.closePath(); + } - return new svg.BoundingBox(x, y, x + width, y + height); - }; + return new svg.BoundingBox(x, y, x + width, y + height); + }; + } }; - svg.Element.rect.prototype = new svg.Element.PathElementBase(); // circle element - svg.Element.circle = function (node) { - this.base = svg.Element.PathElementBase; - this.base(node); + svg.Element.circle = class extends svg.Element.PathElementBase { + constructor (node) { + super(); + this.base = svg.Element.PathElementBase; + this.base(node); - this.path = function (ctx) { - var cx = this.attribute('cx').toPixels('x'); - var cy = this.attribute('cy').toPixels('y'); - var r = this.attribute('r').toPixels(); + this.path = function (ctx) { + const cx = this.attribute('cx').toPixels('x'); + const cy = this.attribute('cy').toPixels('y'); + const r = this.attribute('r').toPixels(); - if (ctx != null) { - ctx.beginPath(); - ctx.arc(cx, cy, r, 0, Math.PI * 2, true); - ctx.closePath(); - } + if (ctx != null) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.closePath(); + } - return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); - }; + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + }; + } }; - svg.Element.circle.prototype = new svg.Element.PathElementBase(); // ellipse element - svg.Element.ellipse = function (node) { - this.base = svg.Element.PathElementBase; - this.base(node); + svg.Element.ellipse = class extends svg.Element.PathElementBase { + constructor (node) { + super(); + this.base = svg.Element.PathElementBase; + this.base(node); - this.path = function (ctx) { - var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); - var rx = this.attribute('rx').toPixels('x'); - var ry = this.attribute('ry').toPixels('y'); - var cx = this.attribute('cx').toPixels('x'); - var cy = this.attribute('cy').toPixels('y'); + this.path = function (ctx) { + const KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + const rx = this.attribute('rx').toPixels('x'); + const ry = this.attribute('ry').toPixels('y'); + const cx = this.attribute('cx').toPixels('x'); + const cy = this.attribute('cy').toPixels('y'); - if (ctx != null) { - ctx.beginPath(); - ctx.moveTo(cx, cy - ry); - ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); - ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); - ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); - ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); - ctx.closePath(); - } + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(cx, cy - ry); + ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); + ctx.closePath(); + } - return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); - }; + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + }; + } }; - svg.Element.ellipse.prototype = new svg.Element.PathElementBase(); // line element - svg.Element.line = function (node) { - this.base = svg.Element.PathElementBase; - this.base(node); + svg.Element.line = class extends svg.Element.PathElementBase { + constructor (node) { + super(); + this.base = svg.Element.PathElementBase; + this.base(node); - this.getPoints = function () { - return [ - new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), - new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))]; - }; + this.getPoints = function () { + return [ + new svg.Point(this.attribute('x1').toPixels('x'), this.attribute('y1').toPixels('y')), + new svg.Point(this.attribute('x2').toPixels('x'), this.attribute('y2').toPixels('y'))]; + }; - this.path = function (ctx) { - var points = this.getPoints(); + this.path = function (ctx) { + const points = this.getPoints(); - if (ctx != null) { - ctx.beginPath(); - ctx.moveTo(points[0].x, points[0].y); - ctx.lineTo(points[1].x, points[1].y); - } + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } - return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); - }; + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + }; - this.getMarkers = function () { - var points = this.getPoints(); - var a = points[0].angleTo(points[1]); - return [[points[0], a], [points[1], a]]; - }; + this.getMarkers = function () { + const points = this.getPoints(); + const a = points[0].angleTo(points[1]); + return [[points[0], a], [points[1], a]]; + }; + } }; - svg.Element.line.prototype = new svg.Element.PathElementBase(); // polyline element - svg.Element.polyline = function (node) { - this.base = svg.Element.PathElementBase; - this.base(node); + svg.Element.polyline = class extends svg.Element.PathElementBase { + constructor (node) { + super(); + this.base = svg.Element.PathElementBase; + this.base(node); - this.points = svg.CreatePath(this.attribute('points').value); - this.path = function (ctx) { - var bb = new svg.BoundingBox(this.points[0].x, this.points[0].y); - if (ctx != null) { - ctx.beginPath(); - ctx.moveTo(this.points[0].x, this.points[0].y); - } - for (var i = 1; i < this.points.length; i++) { - bb.addPoint(this.points[i].x, this.points[i].y); - if (ctx != null) ctx.lineTo(this.points[i].x, this.points[i].y); - } - return bb; - }; + this.points = svg.CreatePath(this.attribute('points').value); + this.path = function (ctx) { + const bb = new svg.BoundingBox(this.points[0].x, this.points[0].y); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + } + for (let i = 1; i < this.points.length; i++) { + bb.addPoint(this.points[i].x, this.points[i].y); + if (ctx != null) ctx.lineTo(this.points[i].x, this.points[i].y); + } + return bb; + }; - this.getMarkers = function () { - var markers = []; - for (var i = 0; i < this.points.length - 1; i++) { - markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); - } - markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); - return markers; - }; + this.getMarkers = function () { + const markers = []; + for (let i = 0; i < this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i + 1])]); + } + markers.push([this.points[this.points.length - 1], markers[markers.length - 1][1]]); + return markers; + }; + } }; - svg.Element.polyline.prototype = new svg.Element.PathElementBase(); // polygon element - svg.Element.polygon = function (node) { - this.base = svg.Element.polyline; - this.base(node); + svg.Element.polygon = class extends svg.Element.polyline { + constructor (node) { + super(); + this.base = svg.Element.polyline; + this.base(node); - this.basePath = this.path; - this.path = function (ctx) { - var bb = this.basePath(ctx); - if (ctx != null) { - ctx.lineTo(this.points[0].x, this.points[0].y); - ctx.closePath(); - } - return bb; - }; + this.basePath = this.path; + this.path = function (ctx) { + const bb = this.basePath(ctx); + if (ctx != null) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + return bb; + }; + } }; - svg.Element.polygon.prototype = new svg.Element.polyline(); // path element - svg.Element.path = function (node) { - this.base = svg.Element.PathElementBase; - this.base(node); + svg.Element.path = class extends svg.Element.PathElementBase { + constructor (node) { + super(); + this.base = svg.Element.PathElementBase; + this.base(node); - var d = this.attribute('d').value; - // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF - d = d.replace(/,/gm, ' '); // get rid of all commas - d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands - d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands - d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2'); // separate commands from points - d = d.replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from points - d = d.replace(/([0-9])([+-])/gm, '$1 $2'); // separate digits when no comma - d = d.replace(/(\.[0-9]*)(\.)/gm, '$1 $2'); // separate digits when no comma - d = d.replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax - d = svg.compressSpaces(d); // compress multiple spaces - d = svg.trim(d); - this.PathParser = new function (d) { - this.tokens = d.split(' '); + let d = this.attribute('d').value; + // TODO: convert to real lexer based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF + d = d.replace(/,/gm, ' '); // get rid of all commas + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from commands + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm, '$1 $2'); // separate commands from points + d = d.replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm, '$1 $2'); // separate commands from points + d = d.replace(/([0-9])([+-])/gm, '$1 $2'); // separate digits when no comma + d = d.replace(/(\.[0-9]*)(\.)/gm, '$1 $2'); // separate digits when no comma + d = d.replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm, '$1 $3 $4 '); // shorthand elliptical arc path syntax + d = svg.compressSpaces(d); // compress multiple spaces + d = svg.trim(d); + this.PathParser = new function (d) { + this.tokens = d.split(' '); - this.reset = function () { - this.i = -1; - this.command = ''; - this.previousCommand = ''; - this.start = new svg.Point(0, 0); - this.control = new svg.Point(0, 0); - this.current = new svg.Point(0, 0); - this.points = []; - this.angles = []; - }; + this.reset = function () { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + }; - this.isEnd = function () { - return this.i >= this.tokens.length - 1; - }; + this.isEnd = function () { + return this.i >= this.tokens.length - 1; + }; - this.isCommandOrEnd = function () { - if (this.isEnd()) return true; - return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; - }; + this.isCommandOrEnd = function () { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/^[A-Za-z]$/) != null; + }; - this.isRelativeCommand = function () { - switch (this.command) { - case 'm': - case 'l': - case 'h': - case 'v': - case 'c': - case 's': - case 'q': - case 't': - case 'a': - case 'z': - return true; - } - return false; - }; + this.isRelativeCommand = function () { + switch (this.command) { + case 'm': + case 'l': + case 'h': + case 'v': + case 'c': + case 's': + case 'q': + case 't': + case 'a': + case 'z': + return true; + } + return false; + }; - this.getToken = function () { - this.i++; - return this.tokens[this.i]; - }; + this.getToken = function () { + this.i++; + return this.tokens[this.i]; + }; - this.getScalar = function () { - return parseFloat(this.getToken()); - }; + this.getScalar = function () { + return parseFloat(this.getToken()); + }; - this.nextCommand = function () { - this.previousCommand = this.command; - this.command = this.getToken(); - }; + this.nextCommand = function () { + this.previousCommand = this.command; + this.command = this.getToken(); + }; - this.getPoint = function () { - var p = new svg.Point(this.getScalar(), this.getScalar()); - return this.makeAbsolute(p); - }; + this.getPoint = function () { + const p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + }; - this.getAsControlPoint = function () { - var p = this.getPoint(); - this.control = p; - return p; - }; + this.getAsControlPoint = function () { + const p = this.getPoint(); + this.control = p; + return p; + }; - this.getAsCurrentPoint = function () { - var p = this.getPoint(); - this.current = p; - return p; - }; + this.getAsCurrentPoint = function () { + const p = this.getPoint(); + this.current = p; + return p; + }; - this.getReflectedControlPoint = function () { - if (this.previousCommand.toLowerCase() !== 'c' && - this.previousCommand.toLowerCase() !== 's' && - this.previousCommand.toLowerCase() !== 'q' && - this.previousCommand.toLowerCase() !== 't') { - return this.current; - } + this.getReflectedControlPoint = function () { + if (this.previousCommand.toLowerCase() !== 'c' && + this.previousCommand.toLowerCase() !== 's' && + this.previousCommand.toLowerCase() !== 'q' && + this.previousCommand.toLowerCase() !== 't') { + return this.current; + } - // reflect point - var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); - return p; - }; + // reflect point + const p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + }; - this.makeAbsolute = function (p) { - if (this.isRelativeCommand()) { - p.x += this.current.x; - p.y += this.current.y; - } - return p; - }; + this.makeAbsolute = function (p) { + if (this.isRelativeCommand()) { + p.x += this.current.x; + p.y += this.current.y; + } + return p; + }; - this.addMarker = function (p, from, priorTo) { - // if the last angle isn't filled in because we didn't have this point yet ... - if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) { - this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); - } - this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); - }; + this.addMarker = function (p, from, priorTo) { + // if the last angle isn't filled in because we didn't have this point yet ... + if (priorTo != null && this.angles.length > 0 && this.angles[this.angles.length - 1] == null) { + this.angles[this.angles.length - 1] = this.points[this.points.length - 1].angleTo(priorTo); + } + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + }; - this.addMarkerAngle = function (p, a) { - this.points.push(p); - this.angles.push(a); - }; + this.addMarkerAngle = function (p, a) { + this.points.push(p); + this.angles.push(a); + }; - this.getMarkerPoints = function () { return this.points; }; - this.getMarkerAngles = function () { - for (var i = 0; i < this.angles.length; i++) { - if (this.angles[i] == null) { - for (var j = i + 1; j < this.angles.length; j++) { - if (this.angles[j] != null) { - this.angles[i] = this.angles[j]; - break; + this.getMarkerPoints = function () { return this.points; }; + this.getMarkerAngles = function () { + for (let i = 0; i < this.angles.length; i++) { + if (this.angles[i] == null) { + for (let j = i + 1; j < this.angles.length; j++) { + if (this.angles[j] != null) { + this.angles[i] = this.angles[j]; + break; + } } } } + return this.angles; + }; + }(d); + + this.path = function (ctx) { + const pp = this.PathParser; + pp.reset(); + + const bb = new svg.BoundingBox(); + if (ctx != null) ctx.beginPath(); + while (!pp.isEnd()) { + pp.nextCommand(); + switch (pp.command) { + case 'M': + case 'm': + const p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + const p = pp.getAsCurrentPoint(); + pp.addMarker(p, pp.start); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'L': + case 'l': + while (!pp.isCommandOrEnd()) { + const c = pp.current; + const p = pp.getAsCurrentPoint(); + pp.addMarker(p, c); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'H': + case 'h': + while (!pp.isCommandOrEnd()) { + const newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'V': + case 'v': + while (!pp.isCommandOrEnd()) { + const newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'C': + case 'c': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const p1 = pp.getPoint(); + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + case 's': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const p1 = pp.getReflectedControlPoint(); + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, p1); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'Q': + case 'q': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const cntrl = pp.getAsControlPoint(); + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'T': + case 't': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + const cntrl = pp.getReflectedControlPoint(); + pp.control = cntrl; + const cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'A': + case 'a': + while (!pp.isCommandOrEnd()) { + const curr = pp.current; + let rx = pp.getScalar(); + let ry = pp.getScalar(); + const xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + const largeArcFlag = pp.getScalar(); + const sweepFlag = pp.getScalar(); + const cp = pp.getAsCurrentPoint(); + + // Conversion from endpoint to center parameterization + // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter + + // x1', y1' + const currp = new svg.Point( + Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, + -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 + ); + // adjust radii + const l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + let s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) / + (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2)) + ); + if (isNaN(s)) s = 0; + const cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + const centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + const m = function (v) { return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); }; + // ratio between two vectors + const r = function (u, v) { return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); }; + // angle between two vectors + const a = function (u, v) { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); }; + // initial angle + const a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); + // angle delta + const u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; + const v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; + let ad = a(u, v); + if (r(u, v) <= -1) ad = Math.PI; + if (r(u, v) >= 1) ad = 0; + + // for markers + const dir = 1 - sweepFlag ? 1.0 : -1.0; + const ah = a1 + dir * (ad / 2.0); + const halfWay = new svg.Point( + centp.x + rx * Math.cos(ah), + centp.y + ry * Math.sin(ah) + ); + pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); + pp.addMarkerAngle(cp, ah - dir * Math.PI); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + const r = rx > ry ? rx : ry; + const sx = rx > ry ? 1 : rx / ry; + const sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1 / sx, 1 / sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + case 'z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } } - return this.angles; + + return bb; }; - }(d); - this.path = function (ctx) { - var pp = this.PathParser; - pp.reset(); + this.getMarkers = function () { + const points = this.PathParser.getMarkerPoints(); + const angles = this.PathParser.getMarkerAngles(); - var bb = new svg.BoundingBox(); - if (ctx != null) ctx.beginPath(); - while (!pp.isEnd()) { - pp.nextCommand(); - switch (pp.command) { - case 'M': - case 'm': - var p = pp.getAsCurrentPoint(); - pp.addMarker(p); - bb.addPoint(p.x, p.y); - if (ctx != null) ctx.moveTo(p.x, p.y); - pp.start = pp.current; - while (!pp.isCommandOrEnd()) { - var p = pp.getAsCurrentPoint(); - pp.addMarker(p, pp.start); - bb.addPoint(p.x, p.y); - if (ctx != null) ctx.lineTo(p.x, p.y); - } - break; - case 'L': - case 'l': - while (!pp.isCommandOrEnd()) { - var c = pp.current; - var p = pp.getAsCurrentPoint(); - pp.addMarker(p, c); - bb.addPoint(p.x, p.y); - if (ctx != null) ctx.lineTo(p.x, p.y); - } - break; - case 'H': - case 'h': - while (!pp.isCommandOrEnd()) { - var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); - pp.addMarker(newP, pp.current); - pp.current = newP; - bb.addPoint(pp.current.x, pp.current.y); - if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); - } - break; - case 'V': - case 'v': - while (!pp.isCommandOrEnd()) { - var newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); - pp.addMarker(newP, pp.current); - pp.current = newP; - bb.addPoint(pp.current.x, pp.current.y); - if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); - } - break; - case 'C': - case 'c': - while (!pp.isCommandOrEnd()) { - var curr = pp.current; - var p1 = pp.getPoint(); - var cntrl = pp.getAsControlPoint(); - var cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, p1); - bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'S': - case 's': - while (!pp.isCommandOrEnd()) { - var curr = pp.current; - var p1 = pp.getReflectedControlPoint(); - var cntrl = pp.getAsControlPoint(); - var cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, p1); - bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'Q': - case 'q': - while (!pp.isCommandOrEnd()) { - var curr = pp.current; - var cntrl = pp.getAsControlPoint(); - var cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, cntrl); - bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'T': - case 't': - while (!pp.isCommandOrEnd()) { - var curr = pp.current; - var cntrl = pp.getReflectedControlPoint(); - pp.control = cntrl; - var cp = pp.getAsCurrentPoint(); - pp.addMarker(cp, cntrl, cntrl); - bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); - if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); - } - break; - case 'A': - case 'a': - while (!pp.isCommandOrEnd()) { - var curr = pp.current; - var rx = pp.getScalar(); - var ry = pp.getScalar(); - var xAxisRotation = pp.getScalar() * (Math.PI / 180.0); - var largeArcFlag = pp.getScalar(); - var sweepFlag = pp.getScalar(); - var cp = pp.getAsCurrentPoint(); - - // Conversion from endpoint to center parameterization - // https://www.w3.org/TR/SVG11/implnote.html#ArcConversionEndpointToCenter - - // x1', y1' - var currp = new svg.Point( - Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, - -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 - ); - // adjust radii - var l = Math.pow(currp.x, 2) / Math.pow(rx, 2) + Math.pow(currp.y, 2) / Math.pow(ry, 2); - if (l > 1) { - rx *= Math.sqrt(l); - ry *= Math.sqrt(l); - } - // cx', cy' - var s = (largeArcFlag === sweepFlag ? -1 : 1) * Math.sqrt( - ((Math.pow(rx, 2) * Math.pow(ry, 2)) - (Math.pow(rx, 2) * Math.pow(currp.y, 2)) - (Math.pow(ry, 2) * Math.pow(currp.x, 2))) / - (Math.pow(rx, 2) * Math.pow(currp.y, 2) + Math.pow(ry, 2) * Math.pow(currp.x, 2)) - ); - if (isNaN(s)) s = 0; - var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); - // cx, cy - var centp = new svg.Point( - (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, - (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y - ); - // vector magnitude - var m = function (v) { return Math.sqrt(Math.pow(v[0], 2) + Math.pow(v[1], 2)); }; - // ratio between two vectors - var r = function (u, v) { return (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)); }; - // angle between two vectors - var a = function (u, v) { return (u[0] * v[1] < u[1] * v[0] ? -1 : 1) * Math.acos(r(u, v)); }; - // initial angle - var a1 = a([1, 0], [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]); - // angle delta - var u = [(currp.x - cpp.x) / rx, (currp.y - cpp.y) / ry]; - var v = [(-currp.x - cpp.x) / rx, (-currp.y - cpp.y) / ry]; - var ad = a(u, v); - if (r(u, v) <= -1) ad = Math.PI; - if (r(u, v) >= 1) ad = 0; - - // for markers - var dir = 1 - sweepFlag ? 1.0 : -1.0; - var ah = a1 + dir * (ad / 2.0); - var halfWay = new svg.Point( - centp.x + rx * Math.cos(ah), - centp.y + ry * Math.sin(ah) - ); - pp.addMarkerAngle(halfWay, ah - dir * Math.PI / 2); - pp.addMarkerAngle(cp, ah - dir * Math.PI); - - bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better - if (ctx != null) { - var r = rx > ry ? rx : ry; - var sx = rx > ry ? 1 : rx / ry; - var sy = rx > ry ? ry / rx : 1; - - ctx.translate(centp.x, centp.y); - ctx.rotate(xAxisRotation); - ctx.scale(sx, sy); - ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); - ctx.scale(1 / sx, 1 / sy); - ctx.rotate(-xAxisRotation); - ctx.translate(-centp.x, -centp.y); - } - } - break; - case 'Z': - case 'z': - if (ctx != null) ctx.closePath(); - pp.current = pp.start; + const markers = []; + for (let i = 0; i < points.length; i++) { + markers.push([points[i], angles[i]]); } - } - - return bb; - }; - - this.getMarkers = function () { - var points = this.PathParser.getMarkerPoints(); - var angles = this.PathParser.getMarkerAngles(); - - var markers = []; - for (var i = 0; i < points.length; i++) { - markers.push([points[i], angles[i]]); - } - return markers; - }; + return markers; + }; + } }; - svg.Element.path.prototype = new svg.Element.PathElementBase(); // pattern element - svg.Element.pattern = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.pattern = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.createPattern = function (ctx, element) { - var width = this.attribute('width').toPixels('x', true); - var height = this.attribute('height').toPixels('y', true); + this.createPattern = function (ctx, element) { + const width = this.attribute('width').toPixels('x', true); + const height = this.attribute('height').toPixels('y', true); - // render me using a temporary svg element - var tempSvg = new svg.Element.svg(); - tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); - tempSvg.attributes['width'] = new svg.Property('width', width + 'px'); - tempSvg.attributes['height'] = new svg.Property('height', height + 'px'); - tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value); - tempSvg.children = this.children; + // render me using a temporary svg element + const tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['width'] = new svg.Property('width', width + 'px'); + tempSvg.attributes['height'] = new svg.Property('height', height + 'px'); + tempSvg.attributes['transform'] = new svg.Property('transform', this.attribute('patternTransform').value); + tempSvg.children = this.children; - var c = document.createElement('canvas'); - c.width = width; - c.height = height; - var cctx = c.getContext('2d'); - if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { - cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); - } - // render 3x3 grid so when we transform there's no white space on edges - for (var x = -1; x <= 1; x++) { - for (var y = -1; y <= 1; y++) { - cctx.save(); - cctx.translate(x * c.width, y * c.height); - tempSvg.render(cctx); - cctx.restore(); + const c = document.createElement('canvas'); + c.width = width; + c.height = height; + const cctx = c.getContext('2d'); + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + cctx.translate(this.attribute('x').toPixels('x', true), this.attribute('y').toPixels('y', true)); } - } - var pattern = ctx.createPattern(c, 'repeat'); - return pattern; - }; + // render 3x3 grid so when we transform there's no white space on edges + for (let x = -1; x <= 1; x++) { + for (let y = -1; y <= 1; y++) { + cctx.save(); + cctx.translate(x * c.width, y * c.height); + tempSvg.render(cctx); + cctx.restore(); + } + } + const pattern = ctx.createPattern(c, 'repeat'); + return pattern; + }; + } }; - svg.Element.pattern.prototype = new svg.Element.ElementBase(); // marker element - svg.Element.marker = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.marker = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.baseRender = this.render; - this.render = function (ctx, point, angle) { - ctx.translate(point.x, point.y); - if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); - if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); - ctx.save(); + this.baseRender = this.render; + this.render = function (ctx, point, angle) { + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); - // render me using a temporary svg element - var tempSvg = new svg.Element.svg(); - tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); - tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value); - tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value); - tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value); - tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value); - tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); - tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); - tempSvg.children = this.children; - tempSvg.render(ctx); + // render me using a temporary svg element + const tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); - ctx.restore(); - if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); - if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle); - ctx.translate(-point.x, -point.y); - }; + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') === 'strokeWidth') ctx.scale(1 / ctx.lineWidth, 1 / ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') === 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + }; + } }; - svg.Element.marker.prototype = new svg.Element.ElementBase(); // definitions element - svg.Element.defs = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.defs = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.render = function (ctx) { - // NOOP - }; + this.render = function (ctx) { + // NOOP + }; + } }; - svg.Element.defs.prototype = new svg.Element.ElementBase(); // base for gradients - svg.Element.GradientBase = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.GradientBase = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); - this.stops = []; - for (var i = 0; i < this.children.length; i++) { - var child = this.children[i]; - if (child.type === 'stop') this.stops.push(child); - } - - this.getGradient = function () { - // OVERRIDE ME! - }; - - this.createGradient = function (ctx, element, parentOpacityProp) { - var stopsContainer = this; - if (this.getHrefAttribute().hasValue()) { - stopsContainer = this.getHrefAttribute().getDefinition(); + this.stops = []; + for (let i = 0; i < this.children.length; i++) { + const child = this.children[i]; + if (child.type === 'stop') this.stops.push(child); } - var addParentOpacity = function (color) { - if (parentOpacityProp.hasValue()) { - var p = new svg.Property('color', color); - return p.addOpacity(parentOpacityProp).value; - } - return color; + this.getGradient = function () { + // OVERRIDE ME! }; - var g = this.getGradient(ctx, element); - if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); - for (var i = 0; i < stopsContainer.stops.length; i++) { - g.addColorStop(stopsContainer.stops[i].offset, addParentOpacity(stopsContainer.stops[i].color)); - } + this.createGradient = function (ctx, element, parentOpacityProp) { + const stopsContainer = this.getHrefAttribute().hasValue() + ? this.getHrefAttribute().getDefinition() + : this; - if (this.attribute('gradientTransform').hasValue()) { - // render as transformed pattern on temporary canvas - var rootView = svg.ViewPort.viewPorts[0]; + const addParentOpacity = function (color) { + if (parentOpacityProp.hasValue()) { + const p = new svg.Property('color', color); + return p.addOpacity(parentOpacityProp).value; + } + return color; + }; - var rect = new svg.Element.rect(); - rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); - rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); - rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); - rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); + const g = this.getGradient(ctx, element); + if (g == null) return addParentOpacity(stopsContainer.stops[stopsContainer.stops.length - 1].color); + for (let i = 0; i < stopsContainer.stops.length; i++) { + g.addColorStop(stopsContainer.stops[i].offset, addParentOpacity(stopsContainer.stops[i].color)); + } - var group = new svg.Element.g(); - group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value); - group.children = [ rect ]; + if (this.attribute('gradientTransform').hasValue()) { + // render as transformed pattern on temporary canvas + const rootView = svg.ViewPort.viewPorts[0]; - var tempSvg = new svg.Element.svg(); - tempSvg.attributes['x'] = new svg.Property('x', 0); - tempSvg.attributes['y'] = new svg.Property('y', 0); - tempSvg.attributes['width'] = new svg.Property('width', rootView.width); - tempSvg.attributes['height'] = new svg.Property('height', rootView.height); - tempSvg.children = [ group ]; + const rect = new svg.Element.rect(); + rect.attributes['x'] = new svg.Property('x', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['y'] = new svg.Property('y', -svg.MAX_VIRTUAL_PIXELS / 3.0); + rect.attributes['width'] = new svg.Property('width', svg.MAX_VIRTUAL_PIXELS); + rect.attributes['height'] = new svg.Property('height', svg.MAX_VIRTUAL_PIXELS); - var c = document.createElement('canvas'); - c.width = rootView.width; - c.height = rootView.height; - var tempCtx = c.getContext('2d'); - tempCtx.fillStyle = g; - tempSvg.render(tempCtx); - return tempCtx.createPattern(c, 'no-repeat'); - } + const group = new svg.Element.g(); + group.attributes['transform'] = new svg.Property('transform', this.attribute('gradientTransform').value); + group.children = [ rect ]; - return g; - }; + const tempSvg = new svg.Element.svg(); + tempSvg.attributes['x'] = new svg.Property('x', 0); + tempSvg.attributes['y'] = new svg.Property('y', 0); + tempSvg.attributes['width'] = new svg.Property('width', rootView.width); + tempSvg.attributes['height'] = new svg.Property('height', rootView.height); + tempSvg.children = [ group ]; + + const c = document.createElement('canvas'); + c.width = rootView.width; + c.height = rootView.height; + const tempCtx = c.getContext('2d'); + tempCtx.fillStyle = g; + tempSvg.render(tempCtx); + return tempCtx.createPattern(c, 'no-repeat'); + } + + return g; + }; + } }; - svg.Element.GradientBase.prototype = new svg.Element.ElementBase(); // linear gradient element - svg.Element.linearGradient = function (node) { - this.base = svg.Element.GradientBase; - this.base(node); + svg.Element.linearGradient = class extends svg.Element.GradientBase { + constructor (node) { + super(); + this.base = svg.Element.GradientBase; + this.base(node); - this.getGradient = function (ctx, element) { - var bb = this.gradientUnits === 'objectBoundingBox' - ? element.getBoundingBox() - : null; + this.getGradient = function (ctx, element) { + const bb = this.gradientUnits === 'objectBoundingBox' + ? element.getBoundingBox() + : null; - if (!this.attribute('x1').hasValue() && - !this.attribute('y1').hasValue() && - !this.attribute('x2').hasValue() && - !this.attribute('y2').hasValue() - ) { - this.attribute('x1', true).value = 0; - this.attribute('y1', true).value = 0; - this.attribute('x2', true).value = 1; - this.attribute('y2', true).value = 0; - } + if (!this.attribute('x1').hasValue() && + !this.attribute('y1').hasValue() && + !this.attribute('x2').hasValue() && + !this.attribute('y2').hasValue() + ) { + this.attribute('x1', true).value = 0; + this.attribute('y1', true).value = 0; + this.attribute('x2', true).value = 1; + this.attribute('y2', true).value = 0; + } - var x1 = (this.gradientUnits === 'objectBoundingBox' - ? bb.x() + bb.width() * this.attribute('x1').numValue() - : this.attribute('x1').toPixels('x')); - var y1 = (this.gradientUnits === 'objectBoundingBox' - ? bb.y() + bb.height() * this.attribute('y1').numValue() - : this.attribute('y1').toPixels('y')); - var x2 = (this.gradientUnits === 'objectBoundingBox' - ? bb.x() + bb.width() * this.attribute('x2').numValue() - : this.attribute('x2').toPixels('x')); - var y2 = (this.gradientUnits === 'objectBoundingBox' - ? bb.y() + bb.height() * this.attribute('y2').numValue() - : this.attribute('y2').toPixels('y')); + const x1 = (this.gradientUnits === 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('x1').numValue() + : this.attribute('x1').toPixels('x')); + const y1 = (this.gradientUnits === 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('y1').numValue() + : this.attribute('y1').toPixels('y')); + const x2 = (this.gradientUnits === 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('x2').numValue() + : this.attribute('x2').toPixels('x')); + const y2 = (this.gradientUnits === 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('y2').numValue() + : this.attribute('y2').toPixels('y')); - if (x1 === x2 && y1 === y2) return null; - return ctx.createLinearGradient(x1, y1, x2, y2); - }; + if (x1 === x2 && y1 === y2) return null; + return ctx.createLinearGradient(x1, y1, x2, y2); + }; + } }; - svg.Element.linearGradient.prototype = new svg.Element.GradientBase(); // radial gradient element - svg.Element.radialGradient = function (node) { - this.base = svg.Element.GradientBase; - this.base(node); + svg.Element.radialGradient = class extends svg.Element.GradientBase { + constructor (node) { + super(); + this.base = svg.Element.GradientBase; + this.base(node); - this.getGradient = function (ctx, element) { - var bb = element.getBoundingBox(); + this.getGradient = function (ctx, element) { + const bb = element.getBoundingBox(); - if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; - if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; - if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; + if (!this.attribute('cx').hasValue()) this.attribute('cx', true).value = '50%'; + if (!this.attribute('cy').hasValue()) this.attribute('cy', true).value = '50%'; + if (!this.attribute('r').hasValue()) this.attribute('r', true).value = '50%'; - var cx = (this.gradientUnits === 'objectBoundingBox' - ? bb.x() + bb.width() * this.attribute('cx').numValue() - : this.attribute('cx').toPixels('x')); - var cy = (this.gradientUnits === 'objectBoundingBox' - ? bb.y() + bb.height() * this.attribute('cy').numValue() - : this.attribute('cy').toPixels('y')); + const cx = (this.gradientUnits === 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('cx').numValue() + : this.attribute('cx').toPixels('x')); + const cy = (this.gradientUnits === 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('cy').numValue() + : this.attribute('cy').toPixels('y')); - var fx = cx; - var fy = cy; - if (this.attribute('fx').hasValue()) { - fx = (this.gradientUnits === 'objectBoundingBox' - ? bb.x() + bb.width() * this.attribute('fx').numValue() - : this.attribute('fx').toPixels('x')); - } - if (this.attribute('fy').hasValue()) { - fy = (this.gradientUnits === 'objectBoundingBox' - ? bb.y() + bb.height() * this.attribute('fy').numValue() - : this.attribute('fy').toPixels('y')); - } + let fx = cx; + let fy = cy; + if (this.attribute('fx').hasValue()) { + fx = (this.gradientUnits === 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('fx').numValue() + : this.attribute('fx').toPixels('x')); + } + if (this.attribute('fy').hasValue()) { + fy = (this.gradientUnits === 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('fy').numValue() + : this.attribute('fy').toPixels('y')); + } - var r = (this.gradientUnits === 'objectBoundingBox' - ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() - : this.attribute('r').toPixels()); + const r = (this.gradientUnits === 'objectBoundingBox' + ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() + : this.attribute('r').toPixels()); - return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); - }; + return ctx.createRadialGradient(fx, fy, 0, cx, cy, r); + }; + } }; - svg.Element.radialGradient.prototype = new svg.Element.GradientBase(); // gradient stop element - svg.Element.stop = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.stop = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.offset = this.attribute('offset').numValue(); - if (this.offset < 0) this.offset = 0; - if (this.offset > 1) this.offset = 1; + this.offset = this.attribute('offset').numValue(); + if (this.offset < 0) this.offset = 0; + if (this.offset > 1) this.offset = 1; - var stopColor = this.style('stop-color'); - if (this.style('stop-opacity').hasValue()) stopColor = stopColor.addOpacity(this.style('stop-opacity')); - this.color = stopColor.value; + let stopColor = this.style('stop-color'); + if (this.style('stop-opacity').hasValue()) { + stopColor = stopColor.addOpacity(this.style('stop-opacity')); + } + this.color = stopColor.value; + } }; - svg.Element.stop.prototype = new svg.Element.ElementBase(); // animation base element - svg.Element.AnimateBase = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.AnimateBase = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - svg.Animations.push(this); + svg.Animations.push(this); - this.duration = 0.0; - this.begin = this.attribute('begin').toMilliseconds(); - this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); + this.duration = 0.0; + this.begin = this.attribute('begin').toMilliseconds(); + this.maxDuration = this.begin + this.attribute('dur').toMilliseconds(); - this.getProperty = function () { - var attributeType = this.attribute('attributeType').value; - var attributeName = this.attribute('attributeName').value; + this.getProperty = function () { + const attributeType = this.attribute('attributeType').value; + const attributeName = this.attribute('attributeName').value; - if (attributeType === 'CSS') { - return this.parent.style(attributeName, true); - } - return this.parent.attribute(attributeName, true); - }; - - this.initialValue = null; - this.initialUnits = ''; - this.removed = false; - - this.calcValue = function () { - // OVERRIDE ME! - return ''; - }; - - this.update = function (delta) { - // set initial value - if (this.initialValue == null) { - this.initialValue = this.getProperty().value; - this.initialUnits = this.getProperty().getUnits(); - } - - // if we're past the end time - if (this.duration > this.maxDuration) { - // loop for indefinitely repeating animations - if (this.attribute('repeatCount').value === 'indefinite' || - this.attribute('repeatDur').value === 'indefinite') { - this.duration = 0.0; - } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { - this.frozen = true; - this.parent.animationFrozen = true; - this.parent.animationFrozenValue = this.getProperty().value; - } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { - this.removed = true; - this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; - return true; + if (attributeType === 'CSS') { + return this.parent.style(attributeName, true); } - return false; - } - this.duration = this.duration + delta; + return this.parent.attribute(attributeName, true); + }; - // if we're past the begin time - var updated = false; - if (this.begin < this.duration) { - var newValue = this.calcValue(); // tween + this.initialValue = null; + this.initialUnits = ''; + this.removed = false; - if (this.attribute('type').hasValue()) { - // for transform, etc. - var type = this.attribute('type').value; - newValue = type + '(' + newValue + ')'; + this.calcValue = function () { + // OVERRIDE ME! + return ''; + }; + + this.update = function (delta) { + // set initial value + if (this.initialValue == null) { + this.initialValue = this.getProperty().value; + this.initialUnits = this.getProperty().getUnits(); } - this.getProperty().value = newValue; - updated = true; - } + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value === 'indefinite' || + this.attribute('repeatDur').value === 'indefinite') { + this.duration = 0.0; + } else if (this.attribute('fill').valueOrDefault('remove') === 'freeze' && !this.frozen) { + this.frozen = true; + this.parent.animationFrozen = true; + this.parent.animationFrozenValue = this.getProperty().value; + } else if (this.attribute('fill').valueOrDefault('remove') === 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.parent.animationFrozen ? this.parent.animationFrozenValue : this.initialValue; + return true; + } + return false; + } + this.duration = this.duration + delta; - return updated; - }; + // if we're past the begin time + let updated = false; + if (this.begin < this.duration) { + let newValue = this.calcValue(); // tween - this.from = this.attribute('from'); - this.to = this.attribute('to'); - this.values = this.attribute('values'); - if (this.values.hasValue()) this.values.value = this.values.value.split(';'); + if (this.attribute('type').hasValue()) { + // for transform, etc. + const type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } - // fraction of duration we've covered - this.progress = function () { - var ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; - if (this.values.hasValue()) { - var p = ret.progress * (this.values.value.length - 1); - var lb = Math.floor(p), ub = Math.ceil(p); - ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); - ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); - ret.progress = (p - lb) / (ub - lb); - } else { - ret.from = this.from; - ret.to = this.to; - } - return ret; - }; + this.getProperty().value = newValue; + updated = true; + } + + return updated; + }; + + this.from = this.attribute('from'); + this.to = this.attribute('to'); + this.values = this.attribute('values'); + if (this.values.hasValue()) this.values.value = this.values.value.split(';'); + + // fraction of duration we've covered + this.progress = function () { + const ret = { progress: (this.duration - this.begin) / (this.maxDuration - this.begin) }; + if (this.values.hasValue()) { + const p = ret.progress * (this.values.value.length - 1); + const lb = Math.floor(p), ub = Math.ceil(p); + ret.from = new svg.Property('from', parseFloat(this.values.value[lb])); + ret.to = new svg.Property('to', parseFloat(this.values.value[ub])); + ret.progress = (p - lb) / (ub - lb); + } else { + ret.from = this.from; + ret.to = this.to; + } + return ret; + }; + } }; - svg.Element.AnimateBase.prototype = new svg.Element.ElementBase(); // animate element - svg.Element.animate = function (node) { - this.base = svg.Element.AnimateBase; - this.base(node); + svg.Element.animate = class extends svg.Element.AnimateBase { + constructor (node) { + super(); + this.base = svg.Element.AnimateBase; + this.base(node); - this.calcValue = function () { - var p = this.progress(); + this.calcValue = function () { + const p = this.progress(); - // tween value linearly - var newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; - return newValue + this.initialUnits; - }; + // tween value linearly + const newValue = p.from.numValue() + (p.to.numValue() - p.from.numValue()) * p.progress; + return newValue + this.initialUnits; + }; + } }; - svg.Element.animate.prototype = new svg.Element.AnimateBase(); // animate color element - svg.Element.animateColor = function (node) { - this.base = svg.Element.AnimateBase; - this.base(node); + svg.Element.animateColor = class extends svg.Element.AnimateBase { + constructor (node) { + super(); + this.base = svg.Element.AnimateBase; + this.base(node); - this.calcValue = function () { - var p = this.progress(); - var from = new RGBColor(p.from.value); - var to = new RGBColor(p.to.value); + this.calcValue = function () { + const p = this.progress(); + const from = new RGBColor(p.from.value); + const to = new RGBColor(p.to.value); - if (from.ok && to.ok) { - // tween color linearly - var r = from.r + (to.r - from.r) * p.progress; - var g = from.g + (to.g - from.g) * p.progress; - var b = from.b + (to.b - from.b) * p.progress; - return 'rgb(' + parseInt(r, 10) + ',' + parseInt(g, 10) + ',' + parseInt(b, 10) + ')'; - } - return this.attribute('from').value; - }; + if (from.ok && to.ok) { + // tween color linearly + const r = from.r + (to.r - from.r) * p.progress; + const g = from.g + (to.g - from.g) * p.progress; + const b = from.b + (to.b - from.b) * p.progress; + return 'rgb(' + parseInt(r, 10) + ',' + parseInt(g, 10) + ',' + parseInt(b, 10) + ')'; + } + return this.attribute('from').value; + }; + } }; - svg.Element.animateColor.prototype = new svg.Element.AnimateBase(); // animate transform element - svg.Element.animateTransform = function (node) { - this.base = svg.Element.AnimateBase; - this.base(node); + svg.Element.animateTransform = class extends svg.Element.animate { + constructor (node) { + super(); + this.base = svg.Element.AnimateBase; + this.base(node); - this.calcValue = function () { - var p = this.progress(); + this.calcValue = function () { + const p = this.progress(); - // tween value linearly - var from = svg.ToNumberArray(p.from.value); - var to = svg.ToNumberArray(p.to.value); - var newValue = ''; - for (var i = 0; i < from.length; i++) { - newValue += from[i] + (to[i] - from[i]) * p.progress + ' '; - } - return newValue; - }; + // tween value linearly + const from = svg.ToNumberArray(p.from.value); + const to = svg.ToNumberArray(p.to.value); + let newValue = ''; + for (let i = 0; i < from.length; i++) { + newValue += from[i] + (to[i] - from[i]) * p.progress + ' '; + } + return newValue; + }; + } }; - svg.Element.animateTransform.prototype = new svg.Element.animate(); // font element - svg.Element.font = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.font = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); - this.isRTL = false; - this.isArabic = false; - this.fontFace = null; - this.missingGlyph = null; - this.glyphs = []; - for (var i = 0; i < this.children.length; i++) { - var child = this.children[i]; - if (child.type === 'font-face') { - this.fontFace = child; - if (child.style('font-family').hasValue()) { - svg.Definitions[child.style('font-family').value] = this; - } - } else if (child.type === 'missing-glyph') { - this.missingGlyph = child; - } else if (child.type === 'glyph') { - if (child.arabicForm !== '') { - this.isRTL = true; - this.isArabic = true; - if (typeof this.glyphs[child.unicode] === 'undefined') { - this.glyphs[child.unicode] = []; + this.isRTL = false; + this.isArabic = false; + this.fontFace = null; + this.missingGlyph = null; + this.glyphs = []; + for (let i = 0; i < this.children.length; i++) { + const child = this.children[i]; + if (child.type === 'font-face') { + this.fontFace = child; + if (child.style('font-family').hasValue()) { + svg.Definitions[child.style('font-family').value] = this; + } + } else if (child.type === 'missing-glyph') { + this.missingGlyph = child; + } else if (child.type === 'glyph') { + if (child.arabicForm !== '') { + this.isRTL = true; + this.isArabic = true; + if (typeof this.glyphs[child.unicode] === 'undefined') { + this.glyphs[child.unicode] = []; + } + this.glyphs[child.unicode][child.arabicForm] = child; + } else { + this.glyphs[child.unicode] = child; } - this.glyphs[child.unicode][child.arabicForm] = child; - } else { - this.glyphs[child.unicode] = child; } } } }; - svg.Element.font.prototype = new svg.Element.ElementBase(); // font-face element - svg.Element.fontface = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.fontface = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.ascent = this.attribute('ascent').value; - this.descent = this.attribute('descent').value; - this.unitsPerEm = this.attribute('units-per-em').numValue(); + this.ascent = this.attribute('ascent').value; + this.descent = this.attribute('descent').value; + this.unitsPerEm = this.attribute('units-per-em').numValue(); + } }; - svg.Element.fontface.prototype = new svg.Element.ElementBase(); // missing-glyph element - svg.Element.missingglyph = function (node) { - this.base = svg.Element.path; - this.base(node); + svg.Element.missingglyph = class extends svg.Element.path { + constructor (node) { + super(); + this.base = svg.Element.path; + this.base(node); - this.horizAdvX = 0; + this.horizAdvX = 0; + } }; - svg.Element.missingglyph.prototype = new svg.Element.path(); // glyph element - svg.Element.glyph = function (node) { - this.base = svg.Element.path; - this.base(node); + svg.Element.glyph = class extends svg.Element.path { + constructor (node) { + super(); + this.base = svg.Element.path; + this.base(node); - this.horizAdvX = this.attribute('horiz-adv-x').numValue(); - this.unicode = this.attribute('unicode').value; - this.arabicForm = this.attribute('arabic-form').value; + this.horizAdvX = this.attribute('horiz-adv-x').numValue(); + this.unicode = this.attribute('unicode').value; + this.arabicForm = this.attribute('arabic-form').value; + } }; - svg.Element.glyph.prototype = new svg.Element.path(); // text element - svg.Element.text = function (node) { - this.captureTextNodes = true; - this.base = svg.Element.RenderedElementBase; - this.base(node); + svg.Element.text = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.captureTextNodes = true; + this.base = svg.Element.RenderedElementBase; + this.base(node); - this.baseSetContext = this.setContext; - this.setContext = function (ctx) { - this.baseSetContext(ctx); + this.baseSetContext = this.setContext; + this.setContext = function (ctx) { + this.baseSetContext(ctx); - var textBaseline = this.style('dominant-baseline').toTextBaseline(); - if (textBaseline == null) textBaseline = this.style('alignment-baseline').toTextBaseline(); - if (textBaseline != null) ctx.textBaseline = textBaseline; - }; + let textBaseline = this.style('dominant-baseline').toTextBaseline(); + if (textBaseline == null) textBaseline = this.style('alignment-baseline').toTextBaseline(); + if (textBaseline != null) ctx.textBaseline = textBaseline; + }; - this.getBoundingBox = function () { - var x = this.attribute('x').toPixels('x'); - var y = this.attribute('y').toPixels('y'); - var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); - }; + this.getBoundingBox = function () { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + return new svg.BoundingBox(x, y - fontSize, x + Math.floor(fontSize * 2.0 / 3.0) * this.children[0].getText().length, y); + }; - this.renderChildren = function (ctx) { - this.x = this.attribute('x').toPixels('x'); - this.y = this.attribute('y').toPixels('y'); - this.x += this.getAnchorDelta(ctx, this, 0); - for (var i = 0; i < this.children.length; i++) { - this.renderChild(ctx, this, i); - } - }; - - this.getAnchorDelta = function (ctx, parent, startI) { - var textAnchor = this.style('text-anchor').valueOrDefault('start'); - if (textAnchor !== 'start') { - var width = 0; - for (var i = startI; i < parent.children.length; i++) { - var child = parent.children[i]; - if (i > startI && child.attribute('x').hasValue()) break; // new group - width += child.measureTextRecursive(ctx); + this.renderChildren = function (ctx) { + this.x = this.attribute('x').toPixels('x'); + this.y = this.attribute('y').toPixels('y'); + this.x += this.getAnchorDelta(ctx, this, 0); + for (let i = 0; i < this.children.length; i++) { + this.renderChild(ctx, this, i); } - return -1 * (textAnchor === 'end' ? width : width / 2.0); - } - return 0; - }; + }; - this.renderChild = function (ctx, parent, i) { - var child = parent.children[i]; - if (child.attribute('x').hasValue()) { - child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); - if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); - } else { - if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); - if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); - child.x = this.x; - } - this.x = child.x + child.measureText(ctx); + this.getAnchorDelta = function (ctx, parent, startI) { + const textAnchor = this.style('text-anchor').valueOrDefault('start'); + if (textAnchor !== 'start') { + let width = 0; + for (let i = startI; i < parent.children.length; i++) { + const child = parent.children[i]; + if (i > startI && child.attribute('x').hasValue()) break; // new group + width += child.measureTextRecursive(ctx); + } + return -1 * (textAnchor === 'end' ? width : width / 2.0); + } + return 0; + }; - if (child.attribute('y').hasValue()) { - child.y = child.attribute('y').toPixels('y'); - if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); - } else { - if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); - if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); - child.y = this.y; - } - this.y = child.y; + this.renderChild = function (ctx, parent, i) { + const child = parent.children[i]; + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').toPixels('x') + this.getAnchorDelta(ctx, parent, i); + if (child.attribute('dx').hasValue()) child.x += child.attribute('dx').toPixels('x'); + } else { + if (this.attribute('dx').hasValue()) this.x += this.attribute('dx').toPixels('x'); + if (child.attribute('dx').hasValue()) this.x += child.attribute('dx').toPixels('x'); + child.x = this.x; + } + this.x = child.x + child.measureText(ctx); - child.render(ctx); + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').toPixels('y'); + if (child.attribute('dy').hasValue()) child.y += child.attribute('dy').toPixels('y'); + } else { + if (this.attribute('dy').hasValue()) this.y += this.attribute('dy').toPixels('y'); + if (child.attribute('dy').hasValue()) this.y += child.attribute('dy').toPixels('y'); + child.y = this.y; + } + this.y = child.y; - for (var i = 0; i < child.children.length; i++) { - this.renderChild(ctx, child, i); - } - }; + child.render(ctx); + + for (let i = 0; i < child.children.length; i++) { + this.renderChild(ctx, child, i); + } + }; + } }; - svg.Element.text.prototype = new svg.Element.RenderedElementBase(); // text base - svg.Element.TextElementBase = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); + svg.Element.TextElementBase = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); - this.getGlyph = function (font, text, i) { - var c = text[i]; - var glyph = null; - if (font.isArabic) { - var arabicForm = 'isolated'; - if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; - if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; - if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; - if (typeof font.glyphs[c] !== 'undefined') { - glyph = font.glyphs[c][arabicForm]; - if (glyph == null && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; - } - } else { - glyph = font.glyphs[c]; - } - if (glyph == null) glyph = font.missingGlyph; - return glyph; - }; - - this.renderChildren = function (ctx) { - var customFont = this.parent.style('font-family').getDefinition(); - if (customFont != null) { - var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - var fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); - var text = this.getText(); - if (customFont.isRTL) text = text.split('').reverse().join(''); - - var dx = svg.ToNumberArray(this.parent.attribute('dx').value); - for (var i = 0; i < text.length; i++) { - var glyph = this.getGlyph(customFont, text, i); - var scale = fontSize / customFont.fontFace.unitsPerEm; - ctx.translate(this.x, this.y); - ctx.scale(scale, -scale); - var lw = ctx.lineWidth; - ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; - if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); - glyph.render(ctx); - if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); - ctx.lineWidth = lw; - ctx.scale(1 / scale, -1 / scale); - ctx.translate(-this.x, -this.y); - - this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; - if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { - this.x += dx[i]; + this.getGlyph = function (font, text, i) { + const c = text[i]; + let glyph = null; + if (font.isArabic) { + let arabicForm = 'isolated'; + if ((i === 0 || text[i - 1] === ' ') && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'terminal'; + if (i > 0 && text[i - 1] !== ' ' && i < text.length - 2 && text[i + 1] !== ' ') arabicForm = 'medial'; + if (i > 0 && text[i - 1] !== ' ' && (i === text.length - 1 || text[i + 1] === ' ')) arabicForm = 'initial'; + if (typeof font.glyphs[c] !== 'undefined') { + glyph = font.glyphs[c][arabicForm]; + if (glyph == null && font.glyphs[c].type === 'glyph') glyph = font.glyphs[c]; } + } else { + glyph = font.glyphs[c]; } - return; - } + if (glyph == null) glyph = font.missingGlyph; + return glyph; + }; - if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); - if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); - }; + this.renderChildren = function (ctx) { + const customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + const fontStyle = this.parent.style('font-style').valueOrDefault(svg.Font.Parse(svg.ctx.font).fontStyle); + let text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); - this.getText = function () { - // OVERRIDE ME - }; + const dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (let i = 0; i < text.length; i++) { + const glyph = this.getGlyph(customFont, text, i); + const scale = fontSize / customFont.fontFace.unitsPerEm; + ctx.translate(this.x, this.y); + ctx.scale(scale, -scale); + const lw = ctx.lineWidth; + ctx.lineWidth = ctx.lineWidth * customFont.fontFace.unitsPerEm / fontSize; + if (fontStyle === 'italic') ctx.transform(1, 0, 0.4, 1, 0, 0); + glyph.render(ctx); + if (fontStyle === 'italic') ctx.transform(1, 0, -0.4, 1, 0, 0); + ctx.lineWidth = lw; + ctx.scale(1 / scale, -1 / scale); + ctx.translate(-this.x, -this.y); - this.measureTextRecursive = function (ctx) { - var width = this.measureText(ctx); - for (var i = 0; i < this.children.length; i++) { - width += this.children[i].measureTextRecursive(ctx); - } - return width; - }; - - this.measureText = function (ctx) { - var customFont = this.parent.style('font-family').getDefinition(); - if (customFont != null) { - var fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); - var measure = 0; - var text = this.getText(); - if (customFont.isRTL) text = text.split('').reverse().join(''); - var dx = svg.ToNumberArray(this.parent.attribute('dx').value); - for (var i = 0; i < text.length; i++) { - var glyph = this.getGlyph(customFont, text, i); - measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; - if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { - measure += dx[i]; + this.x += fontSize * (glyph.horizAdvX || customFont.horizAdvX) / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + this.x += dx[i]; + } } + return; } - return measure; - } - var textToMeasure = svg.compressSpaces(this.getText()); - if (!ctx.measureText) return textToMeasure.length * 10; + if (ctx.fillStyle !== '') ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + if (ctx.strokeStyle !== '') ctx.strokeText(svg.compressSpaces(this.getText()), this.x, this.y); + }; - ctx.save(); - this.setContext(ctx); - var width = ctx.measureText(textToMeasure).width; - ctx.restore(); - return width; - }; + this.getText = function () { + // OVERRIDE ME + }; + + this.measureTextRecursive = function (ctx) { + let width = this.measureText(ctx); + for (let i = 0; i < this.children.length; i++) { + width += this.children[i].measureTextRecursive(ctx); + } + return width; + }; + + this.measureText = function (ctx) { + const customFont = this.parent.style('font-family').getDefinition(); + if (customFont != null) { + const fontSize = this.parent.style('font-size').numValueOrDefault(svg.Font.Parse(svg.ctx.font).fontSize); + let measure = 0; + let text = this.getText(); + if (customFont.isRTL) text = text.split('').reverse().join(''); + const dx = svg.ToNumberArray(this.parent.attribute('dx').value); + for (let i = 0; i < text.length; i++) { + const glyph = this.getGlyph(customFont, text, i); + measure += (glyph.horizAdvX || customFont.horizAdvX) * fontSize / customFont.fontFace.unitsPerEm; + if (typeof dx[i] !== 'undefined' && !isNaN(dx[i])) { + measure += dx[i]; + } + } + return measure; + } + + const textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + + ctx.save(); + this.setContext(ctx); + const {width} = ctx.measureText(textToMeasure); + ctx.restore(); + return width; + }; + } }; - svg.Element.TextElementBase.prototype = new svg.Element.RenderedElementBase(); // tspan - svg.Element.tspan = function (node) { - this.captureTextNodes = true; - this.base = svg.Element.TextElementBase; - this.base(node); + svg.Element.tspan = class extends svg.Element.TextElementBase { + constructor (node) { + super(); + this.captureTextNodes = true; + this.base = svg.Element.TextElementBase; + this.base(node); - this.text = node.nodeValue || node.text || ''; - this.getText = function () { - return this.text; - }; + this.text = node.nodeValue || node.text || ''; + this.getText = function () { + return this.text; + }; + } }; - svg.Element.tspan.prototype = new svg.Element.TextElementBase(); // tref - svg.Element.tref = function (node) { - this.base = svg.Element.TextElementBase; - this.base(node); + svg.Element.tref = class extends svg.Element.TextElementBase { + constructor (node) { + super(); + this.base = svg.Element.TextElementBase; + this.base(node); - this.getText = function () { - var element = this.getHrefAttribute().getDefinition(); - if (element != null) return element.children[0].getText(); - }; + this.getText = function () { + const element = this.getHrefAttribute().getDefinition(); + if (element != null) return element.children[0].getText(); + }; + } }; - svg.Element.tref.prototype = new svg.Element.TextElementBase(); // a element - svg.Element.a = function (node) { - this.base = svg.Element.TextElementBase; - this.base(node); + svg.Element.a = class extends svg.Element.TextElementBase { + constructor (node) { + super(); + this.base = svg.Element.TextElementBase; + this.base(node); - this.hasText = true; - for (var i = 0, childNode; (childNode = node.childNodes[i]); i++) { - if (childNode.nodeType !== 3) this.hasText = false; - } - - // this might contain text - this.text = this.hasText ? node.childNodes[0].nodeValue : ''; - this.getText = function () { - return this.text; - }; - - this.baseRenderChildren = this.renderChildren; - this.renderChildren = function (ctx) { - if (this.hasText) { - // render as text element - this.baseRenderChildren(ctx); - var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); - svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); - } else { - // render as temporary group - var g = new svg.Element.g(); - g.children = this.children; - g.parent = this; - g.render(ctx); + this.hasText = true; + for (let i = 0, childNode; (childNode = node.childNodes[i]); i++) { + if (childNode.nodeType !== 3) this.hasText = false; } - }; - this.onclick = function () { - window.open(this.getHrefAttribute().value); - }; + // this might contain text + this.text = this.hasText ? node.childNodes[0].nodeValue : ''; + this.getText = function () { + return this.text; + }; - this.onmousemove = function () { - svg.ctx.canvas.style.cursor = 'pointer'; - }; + this.baseRenderChildren = this.renderChildren; + this.renderChildren = function (ctx) { + if (this.hasText) { + // render as text element + this.baseRenderChildren(ctx); + const fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } else { + // render as temporary group + const g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + }; + + this.onclick = function () { + window.open(this.getHrefAttribute().value); + }; + + this.onmousemove = function () { + svg.ctx.canvas.style.cursor = 'pointer'; + }; + } }; - svg.Element.a.prototype = new svg.Element.TextElementBase(); // image element - svg.Element.image = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); + svg.Element.image = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); - var href = this.getHrefAttribute().value; - if (href === '') { - return; - } - var isSvg = href.match(/\.svg$/); - - svg.Images.push(this); - this.loaded = false; - if (!isSvg) { - this.img = document.createElement('img'); - if (svg.opts['useCORS'] === true) { this.img.crossOrigin = 'Anonymous'; } - var self = this; - this.img.onload = function () { self.loaded = true; }; - this.img.onerror = function () { svg.log('ERROR: image "' + href + '" not found'); self.loaded = true; }; - this.img.src = href; - } else { - this.img = svg.ajax(href); - this.loaded = true; - } - - this.renderChildren = function (ctx) { - var x = this.attribute('x').toPixels('x'); - var y = this.attribute('y').toPixels('y'); - - var width = this.attribute('width').toPixels('x'); - var height = this.attribute('height').toPixels('y'); - if (width === 0 || height === 0) return; - - ctx.save(); - if (isSvg) { - ctx.drawSvg(this.img, x, y, width, height); - } else { - ctx.translate(x, y); - svg.AspectRatio( - ctx, - this.attribute('preserveAspectRatio').value, - width, - this.img.width, - height, - this.img.height, - 0, - 0 - ); - ctx.drawImage(this.img, 0, 0); + const href = this.getHrefAttribute().value; + if (href === '') { + return; } - ctx.restore(); - }; + const isSvg = href.match(/\.svg$/); - this.getBoundingBox = function () { - var x = this.attribute('x').toPixels('x'); - var y = this.attribute('y').toPixels('y'); - var width = this.attribute('width').toPixels('x'); - var height = this.attribute('height').toPixels('y'); - return new svg.BoundingBox(x, y, x + width, y + height); - }; + svg.Images.push(this); + this.loaded = false; + if (!isSvg) { + this.img = document.createElement('img'); + if (svg.opts['useCORS'] === true) { this.img.crossOrigin = 'Anonymous'; } + const self = this; + this.img.onload = function () { self.loaded = true; }; + this.img.onerror = function () { svg.log('ERROR: image "' + href + '" not found'); self.loaded = true; }; + this.img.src = href; + } else { + this.img = svg.ajax(href); + this.loaded = true; + } + + this.renderChildren = function (ctx) { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + if (width === 0 || height === 0) return; + + ctx.save(); + if (isSvg) { + ctx.drawSvg(this.img, x, y, width, height); + } else { + ctx.translate(x, y); + svg.AspectRatio( + ctx, + this.attribute('preserveAspectRatio').value, + width, + this.img.width, + height, + this.img.height, + 0, + 0 + ); + ctx.drawImage(this.img, 0, 0); + } + ctx.restore(); + }; + + this.getBoundingBox = function () { + const x = this.attribute('x').toPixels('x'); + const y = this.attribute('y').toPixels('y'); + const width = this.attribute('width').toPixels('x'); + const height = this.attribute('height').toPixels('y'); + return new svg.BoundingBox(x, y, x + width, y + height); + }; + } }; - svg.Element.image.prototype = new svg.Element.RenderedElementBase(); // group element - svg.Element.g = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); + svg.Element.g = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); - this.getBoundingBox = function () { - var bb = new svg.BoundingBox(); - for (var i = 0; i < this.children.length; i++) { - bb.addBoundingBox(this.children[i].getBoundingBox()); - } - return bb; - }; + this.getBoundingBox = function () { + const bb = new svg.BoundingBox(); + for (let i = 0; i < this.children.length; i++) { + bb.addBoundingBox(this.children[i].getBoundingBox()); + } + return bb; + }; + } }; - svg.Element.g.prototype = new svg.Element.RenderedElementBase(); // symbol element - svg.Element.symbol = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); + svg.Element.symbol = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); - this.render = function (ctx) { - // NO RENDER - }; + this.render = function (ctx) { + // NO RENDER + }; + } }; - svg.Element.symbol.prototype = new svg.Element.RenderedElementBase(); // style element - svg.Element.style = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.style = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - // text, or spaces then CDATA - var css = ''; - for (var i = 0, childNode; (childNode = node.childNodes[i]); i++) { - css += childNode.nodeValue; - } - css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments - css = svg.compressSpaces(css); // replace whitespace - var cssDefs = css.split('}'); - for (var i = 0; i < cssDefs.length; i++) { - if (svg.trim(cssDefs[i]) !== '') { - var cssDef = cssDefs[i].split('{'); - var cssClasses = cssDef[0].split(','); - var cssProps = cssDef[1].split(';'); - for (var j = 0; j < cssClasses.length; j++) { - var cssClass = svg.trim(cssClasses[j]); - if (cssClass !== '') { - var props = {}; - for (var k = 0; k < cssProps.length; k++) { - var prop = cssProps[k].indexOf(':'); - var name = cssProps[k].substr(0, prop); - var value = cssProps[k].substr(prop + 1, cssProps[k].length - prop); - if (name != null && value != null) { - props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + // text, or spaces then CDATA + let css = ''; + for (let i = 0, childNode; (childNode = node.childNodes[i]); i++) { + css += childNode.nodeValue; + } + css = css.replace(/(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(^[\s]*\/\/.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + const cssDefs = css.split('}'); + for (let i = 0; i < cssDefs.length; i++) { + if (svg.trim(cssDefs[i]) !== '') { + const cssDef = cssDefs[i].split('{'); + const cssClasses = cssDef[0].split(','); + const cssProps = cssDef[1].split(';'); + for (let j = 0; j < cssClasses.length; j++) { + const cssClass = svg.trim(cssClasses[j]); + if (cssClass !== '') { + const props = {}; + for (let k = 0; k < cssProps.length; k++) { + const prop = cssProps[k].indexOf(':'); + const name = cssProps[k].substr(0, prop); + const value = cssProps[k].substr(prop + 1, cssProps[k].length - prop); + if (name != null && value != null) { + props[svg.trim(name)] = new svg.Property(svg.trim(name), svg.trim(value)); + } } - } - svg.Styles[cssClass] = props; - if (cssClass === '@font-face') { - var fontFamily = props['font-family'].value.replace(/"/g, ''); - var srcs = props['src'].value.split(','); - for (var s = 0; s < srcs.length; s++) { - if (srcs[s].indexOf('format("svg")') > 0) { - var urlStart = srcs[s].indexOf('url'); - var urlEnd = srcs[s].indexOf(')', urlStart); - var url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); - var doc = svg.parseXml(svg.ajax(url)); - var fonts = doc.getElementsByTagName('font'); - for (var f = 0; f < fonts.length; f++) { - var font = svg.CreateElement(fonts[f]); - svg.Definitions[fontFamily] = font; + svg.Styles[cssClass] = props; + if (cssClass === '@font-face') { + const fontFamily = props['font-family'].value.replace(/"/g, ''); + const srcs = props['src'].value.split(','); + for (let s = 0; s < srcs.length; s++) { + if (srcs[s].includes('format("svg")')) { + const urlStart = srcs[s].indexOf('url'); + const urlEnd = srcs[s].indexOf(')', urlStart); + const url = srcs[s].substr(urlStart + 5, urlEnd - urlStart - 6); + const doc = svg.parseXml(svg.ajax(url)); + const fonts = doc.getElementsByTagName('font'); + for (let f = 0; f < fonts.length; f++) { + const font = svg.CreateElement(fonts[f]); + svg.Definitions[fontFamily] = font; + } } } } @@ -2404,322 +2485,344 @@ function build (opts) { } } }; - svg.Element.style.prototype = new svg.Element.ElementBase(); // use element - svg.Element.use = function (node) { - this.base = svg.Element.RenderedElementBase; - this.base(node); + svg.Element.use = class extends svg.Element.RenderedElementBase { + constructor (node) { + super(); + this.base = svg.Element.RenderedElementBase; + this.base(node); - this.baseSetContext = this.setContext; - this.setContext = function (ctx) { - this.baseSetContext(ctx); - if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); - if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); - }; + this.baseSetContext = this.setContext; + this.setContext = function (ctx) { + this.baseSetContext(ctx); + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').toPixels('y')); + }; - var element = this.getHrefAttribute().getDefinition(); + const element = this.getHrefAttribute().getDefinition(); - this.path = function (ctx) { - if (element != null) element.path(ctx); - }; + this.path = function (ctx) { + if (element != null) element.path(ctx); + }; - this.getBoundingBox = function () { - if (element != null) return element.getBoundingBox(); - }; + this.getBoundingBox = function () { + if (element != null) return element.getBoundingBox(); + }; - this.renderChildren = function (ctx) { - if (element != null) { - var tempSvg = element; - if (element.type === 'symbol') { - // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement) - tempSvg = new svg.Element.svg(); - tempSvg.type = 'svg'; - tempSvg.attributes['viewBox'] = new svg.Property('viewBox', element.attribute('viewBox').value); - tempSvg.attributes['preserveAspectRatio'] = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); - tempSvg.attributes['overflow'] = new svg.Property('overflow', element.attribute('overflow').value); - tempSvg.children = element.children; + this.renderChildren = function (ctx) { + if (element != null) { + let tempSvg = element; + if (element.type === 'symbol') { + // render me using a temporary svg element in symbol cases (https://www.w3.org/TR/SVG/struct.html#UseElement) + tempSvg = new svg.Element.svg(); + tempSvg.type = 'svg'; + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', element.attribute('viewBox').value); + tempSvg.attributes['preserveAspectRatio'] = new svg.Property('preserveAspectRatio', element.attribute('preserveAspectRatio').value); + tempSvg.attributes['overflow'] = new svg.Property('overflow', element.attribute('overflow').value); + tempSvg.children = element.children; + } + if (tempSvg.type === 'svg') { + // if symbol or svg, inherit width/height from me + if (this.attribute('width').hasValue()) tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value); + if (this.attribute('height').hasValue()) tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value); + } + const oldParent = tempSvg.parent; + tempSvg.parent = null; + tempSvg.render(ctx); + tempSvg.parent = oldParent; } - if (tempSvg.type === 'svg') { - // if symbol or svg, inherit width/height from me - if (this.attribute('width').hasValue()) tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value); - if (this.attribute('height').hasValue()) tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value); - } - var oldParent = tempSvg.parent; - tempSvg.parent = null; - tempSvg.render(ctx); - tempSvg.parent = oldParent; - } - }; + }; + } }; - svg.Element.use.prototype = new svg.Element.RenderedElementBase(); // mask element - svg.Element.mask = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.mask = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.apply = function (ctx, element) { - // render as temp svg - var x = this.attribute('x').toPixels('x'); - var y = this.attribute('y').toPixels('y'); - var width = this.attribute('width').toPixels('x'); - var height = this.attribute('height').toPixels('y'); + this.apply = function (ctx, element) { + // render as temp svg + let x = this.attribute('x').toPixels('x'); + let y = this.attribute('y').toPixels('y'); + let width = this.attribute('width').toPixels('x'); + let height = this.attribute('height').toPixels('y'); - if (width === 0 && height === 0) { - var bb = new svg.BoundingBox(); - for (var i = 0; i < this.children.length; i++) { - bb.addBoundingBox(this.children[i].getBoundingBox()); + if (width === 0 && height === 0) { + const bb = new svg.BoundingBox(); + for (let i = 0; i < this.children.length; i++) { + bb.addBoundingBox(this.children[i].getBoundingBox()); + } + x = Math.floor(bb.x1); + y = Math.floor(bb.y1); + width = Math.floor(bb.width()); + height = Math.floor(bb.height()); } - var x = Math.floor(bb.x1); - var y = Math.floor(bb.y1); - var width = Math.floor(bb.width()); - var height = Math.floor(bb.height()); - } - // temporarily remove mask to avoid recursion - var mask = element.attribute('mask').value; - element.attribute('mask').value = ''; + // temporarily remove mask to avoid recursion + const mask = element.attribute('mask').value; + element.attribute('mask').value = ''; - var cMask = document.createElement('canvas'); - cMask.width = x + width; - cMask.height = y + height; - var maskCtx = cMask.getContext('2d'); - this.renderChildren(maskCtx); + const cMask = document.createElement('canvas'); + cMask.width = x + width; + cMask.height = y + height; + const maskCtx = cMask.getContext('2d'); + this.renderChildren(maskCtx); - var c = document.createElement('canvas'); - c.width = x + width; - c.height = y + height; - var tempCtx = c.getContext('2d'); - element.render(tempCtx); - tempCtx.globalCompositeOperation = 'destination-in'; - tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); - tempCtx.fillRect(0, 0, x + width, y + height); + const c = document.createElement('canvas'); + c.width = x + width; + c.height = y + height; + const tempCtx = c.getContext('2d'); + element.render(tempCtx); + tempCtx.globalCompositeOperation = 'destination-in'; + tempCtx.fillStyle = maskCtx.createPattern(cMask, 'no-repeat'); + tempCtx.fillRect(0, 0, x + width, y + height); - ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); - ctx.fillRect(0, 0, x + width, y + height); + ctx.fillStyle = tempCtx.createPattern(c, 'no-repeat'); + ctx.fillRect(0, 0, x + width, y + height); - // reassign mask - element.attribute('mask').value = mask; - }; + // reassign mask + element.attribute('mask').value = mask; + }; - this.render = function (ctx) { - // NO RENDER - }; + this.render = function (ctx) { + // NO RENDER + }; + } }; - svg.Element.mask.prototype = new svg.Element.ElementBase(); // clip element - svg.Element.clipPath = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.clipPath = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.apply = function (ctx) { - for (var i = 0; i < this.children.length; i++) { - var child = this.children[i]; - if (typeof child.path !== 'undefined') { - var transform = null; - if (child.attribute('transform').hasValue()) { - transform = new svg.Transform(child.attribute('transform').value); - transform.apply(ctx); + this.apply = function (ctx) { + for (let i = 0; i < this.children.length; i++) { + const child = this.children[i]; + if (typeof child.path !== 'undefined') { + let transform = null; + if (child.attribute('transform').hasValue()) { + transform = new svg.Transform(child.attribute('transform').value); + transform.apply(ctx); + } + child.path(ctx); + ctx.clip(); + if (transform) { transform.unapply(ctx); } } - child.path(ctx); - ctx.clip(); - if (transform) { transform.unapply(ctx); } } - } - }; + }; - this.render = function (ctx) { - // NO RENDER - }; + this.render = function (ctx) { + // NO RENDER + }; + } }; - svg.Element.clipPath.prototype = new svg.Element.ElementBase(); // filters - svg.Element.filter = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.filter = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.apply = function (ctx, element) { - // render as temp svg - var bb = element.getBoundingBox(); - var x = Math.floor(bb.x1); - var y = Math.floor(bb.y1); - var width = Math.floor(bb.width()); - var height = Math.floor(bb.height()); + this.apply = function (ctx, element) { + // render as temp svg + const bb = element.getBoundingBox(); + const x = Math.floor(bb.x1); + const y = Math.floor(bb.y1); + const width = Math.floor(bb.width()); + const height = Math.floor(bb.height()); - // temporarily remove filter to avoid recursion - var filter = element.style('filter').value; - element.style('filter').value = ''; + // temporarily remove filter to avoid recursion + const filter = element.style('filter').value; + element.style('filter').value = ''; - var px = 0, py = 0; - for (var i = 0; i < this.children.length; i++) { - var efd = this.children[i].extraFilterDistance || 0; - px = Math.max(px, efd); - py = Math.max(py, efd); - } - - var c = document.createElement('canvas'); - c.width = width + 2 * px; - c.height = height + 2 * py; - var tempCtx = c.getContext('2d'); - tempCtx.translate(-x + px, -y + py); - element.render(tempCtx); - - // apply filters - for (var i = 0; i < this.children.length; i++) { - this.children[i].apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); - } - - // render on me - ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); - - // reassign filter - element.style('filter', true).value = filter; - }; - - this.render = function (ctx) { - // NO RENDER - }; - }; - svg.Element.filter.prototype = new svg.Element.ElementBase(); - - svg.Element.feMorphology = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); - - this.apply = function (ctx, x, y, width, height) { - // TODO: implement - }; - }; - svg.Element.feMorphology.prototype = new svg.Element.ElementBase(); - - svg.Element.feComposite = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); - - this.apply = function (ctx, x, y, width, height) { - // TODO: implement - }; - }; - svg.Element.feComposite.prototype = new svg.Element.ElementBase(); - - svg.Element.feColorMatrix = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); - - var matrix = svg.ToNumberArray(this.attribute('values').value); - switch (this.attribute('type').valueOrDefault('matrix')) { // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement - case 'saturate': - var s = matrix[0]; - matrix = [ - 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, - 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, - 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, - 0, 0, 0, 1, 0, - 0, 0, 0, 0, 1 - ]; - break; - case 'hueRotate': - var a = matrix[0] * Math.PI / 180.0; - var c = function (m1, m2, m3) { return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; }; - matrix = [ - c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, - c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, - c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, - 0, 0, 0, 1, 0, - 0, 0, 0, 0, 1 - ]; - break; - case 'luminanceToAlpha': - matrix = [ - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - 0.2125, 0.7154, 0.0721, 0, 0, - 0, 0, 0, 0, 1 - ]; - break; - } - - function imGet (img, x, y, width, height, rgba) { - return img[y * width * 4 + x * 4 + rgba]; - } - - function imSet (img, x, y, width, height, rgba, val) { - img[y * width * 4 + x * 4 + rgba] = val; - } - - function m (i, v) { - var mi = matrix[i]; - return mi * (mi < 0 ? v - 255 : v); - } - - this.apply = function (ctx, x, y, width, height) { - // assuming x==0 && y==0 for now - var srcData = ctx.getImageData(0, 0, width, height); - for (var y = 0; y < height; y++) { - for (var x = 0; x < width; x++) { - var r = imGet(srcData.data, x, y, width, height, 0); - var g = imGet(srcData.data, x, y, width, height, 1); - var b = imGet(srcData.data, x, y, width, height, 2); - var a = imGet(srcData.data, x, y, width, height, 3); - imSet(srcData.data, x, y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1)); - imSet(srcData.data, x, y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1)); - imSet(srcData.data, x, y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1)); - imSet(srcData.data, x, y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1)); + let px = 0, py = 0; + for (let i = 0; i < this.children.length; i++) { + const efd = this.children[i].extraFilterDistance || 0; + px = Math.max(px, efd); + py = Math.max(py, efd); } - } - ctx.clearRect(0, 0, width, height); - ctx.putImageData(srcData, 0, 0); - }; + + const c = document.createElement('canvas'); + c.width = width + 2 * px; + c.height = height + 2 * py; + const tempCtx = c.getContext('2d'); + tempCtx.translate(-x + px, -y + py); + element.render(tempCtx); + + // apply filters + for (let i = 0; i < this.children.length; i++) { + this.children[i].apply(tempCtx, 0, 0, width + 2 * px, height + 2 * py); + } + + // render on me + ctx.drawImage(c, 0, 0, width + 2 * px, height + 2 * py, x - px, y - py, width + 2 * px, height + 2 * py); + + // reassign filter + element.style('filter', true).value = filter; + }; + + this.render = function (ctx) { + // NO RENDER + }; + } }; - svg.Element.feColorMatrix.prototype = new svg.Element.ElementBase(); - svg.Element.feGaussianBlur = function (node) { - this.base = svg.Element.ElementBase; - this.base(node); + svg.Element.feMorphology = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); - this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue()); - this.extraFilterDistance = this.blurRadius; + this.apply = function (ctx, x, y, width, height) { + // TODO: implement + }; + } + }; - this.apply = function (ctx, x, y, width, height) { - if (typeof stackBlurCanvasRGBA === 'undefined') { - svg.log('ERROR: StackBlur.js must be included for blur to work'); - return; + svg.Element.feComposite = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function (ctx, x, y, width, height) { + // TODO: implement + }; + } + }; + + svg.Element.feColorMatrix = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); + + let matrix = svg.ToNumberArray(this.attribute('values').value); + switch (this.attribute('type').valueOrDefault('matrix')) { // https://www.w3.org/TR/SVG/filters.html#feColorMatrixElement + case 'saturate': + const s = matrix[0]; + matrix = [ + 0.213 + 0.787 * s, 0.715 - 0.715 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 + 0.285 * s, 0.072 - 0.072 * s, 0, 0, + 0.213 - 0.213 * s, 0.715 - 0.715 * s, 0.072 + 0.928 * s, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + case 'hueRotate': + const a = matrix[0] * Math.PI / 180.0; + const c = function (m1, m2, m3) { return m1 + Math.cos(a) * m2 + Math.sin(a) * m3; }; + matrix = [ + c(0.213, 0.787, -0.213), c(0.715, -0.715, -0.715), c(0.072, -0.072, 0.928), 0, 0, + c(0.213, -0.213, 0.143), c(0.715, 0.285, 0.140), c(0.072, -0.072, -0.283), 0, 0, + c(0.213, -0.213, -0.787), c(0.715, -0.715, 0.715), c(0.072, 0.928, 0.072), 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + ]; + break; + case 'luminanceToAlpha': + matrix = [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0.2125, 0.7154, 0.0721, 0, 0, + 0, 0, 0, 0, 1 + ]; + break; } - // StackBlur requires canvas be on document - ctx.canvas.id = svg.UniqueId(); - ctx.canvas.style.display = 'none'; - document.body.appendChild(ctx.canvas); - stackBlurCanvasRGBA(ctx.canvas.id, x, y, width, height, this.blurRadius); - document.body.removeChild(ctx.canvas); - }; + function imGet (img, x, y, width, height, rgba) { + return img[y * width * 4 + x * 4 + rgba]; + } + + function imSet (img, x, y, width, height, rgba, val) { + img[y * width * 4 + x * 4 + rgba] = val; + } + + function m (i, v) { + const mi = matrix[i]; + return mi * (mi < 0 ? v - 255 : v); + } + + this.apply = function (ctx, x, y, width, height) { + // assuming x==0 && y==0 for now + const srcData = ctx.getImageData(0, 0, width, height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const r = imGet(srcData.data, x, y, width, height, 0); + const g = imGet(srcData.data, x, y, width, height, 1); + const b = imGet(srcData.data, x, y, width, height, 2); + const a = imGet(srcData.data, x, y, width, height, 3); + imSet(srcData.data, x, y, width, height, 0, m(0, r) + m(1, g) + m(2, b) + m(3, a) + m(4, 1)); + imSet(srcData.data, x, y, width, height, 1, m(5, r) + m(6, g) + m(7, b) + m(8, a) + m(9, 1)); + imSet(srcData.data, x, y, width, height, 2, m(10, r) + m(11, g) + m(12, b) + m(13, a) + m(14, 1)); + imSet(srcData.data, x, y, width, height, 3, m(15, r) + m(16, g) + m(17, b) + m(18, a) + m(19, 1)); + } + } + ctx.clearRect(0, 0, width, height); + ctx.putImageData(srcData, 0, 0); + }; + } + }; + + svg.Element.feGaussianBlur = class extends svg.Element.ElementBase { + constructor (node) { + super(); + this.base = svg.Element.ElementBase; + this.base(node); + + this.blurRadius = Math.floor(this.attribute('stdDeviation').numValue()); + this.extraFilterDistance = this.blurRadius; + + this.apply = function (ctx, x, y, width, height) { + if (typeof stackBlurCanvasRGBA === 'undefined') { + svg.log('ERROR: StackBlur.js must be included for blur to work'); + return; + } + + // StackBlur requires canvas be on document + ctx.canvas.id = svg.UniqueId(); + ctx.canvas.style.display = 'none'; + document.body.appendChild(ctx.canvas); + stackBlurCanvasRGBA(ctx.canvas.id, x, y, width, height, this.blurRadius); + document.body.removeChild(ctx.canvas); + }; + } }; - svg.Element.feGaussianBlur.prototype = new svg.Element.ElementBase(); // title element, do nothing - svg.Element.title = function (node) { + svg.Element.title = class extends svg.Element.ElementBase { + constructor (node) { + super(); + } }; - svg.Element.title.prototype = new svg.Element.ElementBase(); // desc element, do nothing - svg.Element.desc = function (node) { + svg.Element.desc = class extends svg.Element.ElementBase { + constructor (node) { + super(); + } }; - svg.Element.desc.prototype = new svg.Element.ElementBase(); - svg.Element.MISSING = function (node) { - svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + svg.Element.MISSING = class extends svg.Element.ElementBase { + constructor (node) { + super(); + svg.log('ERROR: Element \'' + node.nodeName + '\' not yet implemented.'); + } }; - svg.Element.MISSING.prototype = new svg.Element.ElementBase(); // element factory svg.CreateElement = function (node) { - var className = node.nodeName.replace(/^[^:]+:/, ''); // remove namespace - className = className.replace(/-/g, ''); // remove dashes - var e = null; + const className = node.nodeName + .replace(/^[^:]+:/, '') // remove namespace + .replace(/-/g, ''); // remove dashes + let e; if (typeof svg.Element[className] !== 'undefined') { e = new svg.Element[className](node); } else { @@ -2743,8 +2846,8 @@ function build (opts) { svg.loadXmlDoc = function (ctx, dom) { svg.init(ctx); - var mapXY = function (p) { - var e = ctx.canvas; + const mapXY = function (p) { + let e = ctx.canvas; while (e) { p.x -= e.offsetLeft; p.y -= e.offsetTop; @@ -2758,21 +2861,21 @@ function build (opts) { // bind mouse if (svg.opts['ignoreMouse'] !== true) { ctx.canvas.onclick = function (e) { - var p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); + const p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); svg.Mouse.onclick(p.x, p.y); }; ctx.canvas.onmousemove = function (e) { - var p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); + const p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); svg.Mouse.onmousemove(p.x, p.y); }; } - var e = svg.CreateElement(dom.documentElement); + const e = svg.CreateElement(dom.documentElement); e.root = true; // render loop - var isFirstRender = true; - var draw = function () { + let isFirstRender = true; + const draw = function () { svg.ViewPort.Clear(); if (ctx.canvas.parentNode) svg.ViewPort.SetCurrent(ctx.canvas.parentNode.clientWidth, ctx.canvas.parentNode.clientHeight); @@ -2787,8 +2890,8 @@ function build (opts) { ctx.canvas.style.height = ctx.canvas.height + 'px'; } } - var cWidth = ctx.canvas.clientWidth || ctx.canvas.width; - var cHeight = ctx.canvas.clientHeight || ctx.canvas.height; + let cWidth = ctx.canvas.clientWidth || ctx.canvas.width; + let cHeight = ctx.canvas.clientHeight || ctx.canvas.height; if (svg.opts['ignoreDimensions'] === true && e.style('width').hasValue() && e.style('height').hasValue()) { cWidth = e.style('width').toPixels('x'); cHeight = e.style('height').toPixels('y'); @@ -2798,7 +2901,8 @@ function build (opts) { if (svg.opts['offsetX'] != null) e.attribute('x', true).value = svg.opts['offsetX']; if (svg.opts['offsetY'] != null) e.attribute('y', true).value = svg.opts['offsetY']; if (svg.opts['scaleWidth'] != null || svg.opts['scaleHeight'] != null) { - var xRatio = null, yRatio = null, viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + const viewBox = svg.ToNumberArray(e.attribute('viewBox').value); + let xRatio = null, yRatio = null; if (svg.opts['scaleWidth'] != null) { if (e.attribute('width').hasValue()) xRatio = e.attribute('width').toPixels('x') / svg.opts['scaleWidth']; @@ -2832,13 +2936,13 @@ function build (opts) { } }; - var waitingForImages = true; + let waitingForImages = true; if (svg.ImagesLoaded()) { waitingForImages = false; draw(); } svg.intervalID = setInterval(function () { - var needUpdate = false; + let needUpdate = false; if (waitingForImages && svg.ImagesLoaded()) { waitingForImages = false; @@ -2852,7 +2956,7 @@ function build (opts) { // need update from animations? if (svg.opts['ignoreAnimation'] !== true) { - for (var i = 0; i < svg.Animations.length; i++) { + for (let i = 0; i < svg.Animations.length; i++) { needUpdate = needUpdate | svg.Animations[i].update(1000 / svg.FRAMERATE); } } @@ -2881,29 +2985,29 @@ function build (opts) { this.hasEvents = function () { return this.events.length !== 0; }; this.onclick = function (x, y) { - this.events.push({ type: 'onclick', x: x, y: y, - run: function (e) { if (e.onclick) e.onclick(); } + this.events.push({ type: 'onclick', x, y, + run (e) { if (e.onclick) e.onclick(); } }); }; this.onmousemove = function (x, y) { - this.events.push({ type: 'onmousemove', x: x, y: y, - run: function (e) { if (e.onmousemove) e.onmousemove(); } + this.events.push({ type: 'onmousemove', x, y, + run (e) { if (e.onmousemove) e.onmousemove(); } }); }; this.eventElements = []; this.checkPath = function (element, ctx) { - for (var i = 0; i < this.events.length; i++) { - var e = this.events[i]; + for (let i = 0; i < this.events.length; i++) { + const e = this.events[i]; if (ctx.isPointInPath && ctx.isPointInPath(e.x, e.y)) this.eventElements[i] = element; } }; this.checkBoundingBox = function (element, bb) { - for (var i = 0; i < this.events.length; i++) { - var e = this.events[i]; + for (let i = 0; i < this.events.length; i++) { + const e = this.events[i]; if (bb.isPointInBox(e.x, e.y)) this.eventElements[i] = element; } }; @@ -2911,9 +3015,9 @@ function build (opts) { this.runEvents = function () { svg.ctx.canvas.style.cursor = ''; - for (var i = 0; i < this.events.length; i++) { - var e = this.events[i]; - var element = this.eventElements[i]; + for (let i = 0; i < this.events.length; i++) { + const e = this.events[i]; + let element = this.eventElements[i]; while (element) { e.run(element); element = element.parent; @@ -2928,7 +3032,6 @@ function build (opts) { return svg; } -})(); if (typeof CanvasRenderingContext2D !== 'undefined') { CanvasRenderingContext2D.prototype.drawSvg = function (s, dx, dy, dw, dh) { diff --git a/editor/canvg/rgbcolor.js b/editor/canvg/rgbcolor.js index 18f427b1..1834a5bf 100644 --- a/editor/canvg/rgbcolor.js +++ b/editor/canvg/rgbcolor.js @@ -1,12 +1,10 @@ -/* eslint-disable no-var */ /** * A class to parse color values * @author Stoyan Stefanov * @link https://www.phpied.com/rgb-color-parser-in-javascript/ - * @license Use it if you like it + * @license MIT */ -function RGBColor (colorString) { // eslint-disable-line no-unused-vars - 'use strict'; +export default function RGBColor (colorString) { this.ok = false; // strip any leading # @@ -19,7 +17,7 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars // before getting into regexps, try simple matches // and overwrite the input - var simpleColors = { + const simpleColors = { aliceblue: 'f0f8ff', antiquewhite: 'faebd7', aqua: '00ffff', @@ -164,8 +162,7 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars yellow: 'ffff00', yellowgreen: '9acd32' }; - var key; - for (key in simpleColors) { + for (const key in simpleColors) { if (simpleColors.hasOwnProperty(key)) { if (colorString === key) { colorString = simpleColors[key]; @@ -175,11 +172,11 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars // emd of simple type-in colors // array of color definition objects - var colorDefs = [ + const colorDefs = [ { re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], - process: function (bits) { + process (bits) { return [ parseInt(bits[1], 10), parseInt(bits[2], 10), @@ -190,7 +187,7 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars { re: /^(\w{2})(\w{2})(\w{2})$/, example: ['#00ff00', '336699'], - process: function (bits) { + process (bits) { return [ parseInt(bits[1], 16), parseInt(bits[2], 16), @@ -201,7 +198,7 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars { re: /^(\w{1})(\w{1})(\w{1})$/, example: ['#fb0', 'f0f'], - process: function (bits) { + process (bits) { return [ parseInt(bits[1] + bits[1], 16), parseInt(bits[2] + bits[2], 16), @@ -211,14 +208,13 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars } ]; - var i; // search through the definitions to find a match - for (i = 0; i < colorDefs.length; i++) { - var re = colorDefs[i].re; - var processor = colorDefs[i].process; - var bits = re.exec(colorString); + for (let i = 0; i < colorDefs.length; i++) { + const {re} = colorDefs[i]; + const processor = colorDefs[i].process; + const bits = re.exec(colorString); if (bits) { - var channels = processor(bits); + const channels = processor(bits); this.r = channels[0]; this.g = channels[1]; this.b = channels[2]; @@ -236,9 +232,9 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; }; this.toHex = function () { - var r = this.r.toString(16); - var g = this.g.toString(16); - var b = this.b.toString(16); + let r = this.r.toString(16); + let g = this.g.toString(16); + let b = this.b.toString(16); if (r.length === 1) { r = '0' + r; } if (g.length === 1) { g = '0' + g; } if (b.length === 1) { b = '0' + b; } @@ -247,30 +243,28 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars // help this.getHelpXML = function () { - var i, j; - var examples = []; + const examples = []; // add regexps - for (i = 0; i < colorDefs.length; i++) { - var example = colorDefs[i].example; - for (j = 0; j < example.length; j++) { + for (let i = 0; i < colorDefs.length; i++) { + const {example} = colorDefs[i]; + for (let j = 0; j < example.length; j++) { examples[examples.length] = example[j]; } } // add type-in colors - var sc; - for (sc in simpleColors) { + for (const sc in simpleColors) { if (simpleColors.hasOwnProperty(sc)) { examples[examples.length] = sc; } } - var xml = document.createElement('ul'); + const xml = document.createElement('ul'); xml.setAttribute('id', 'rgbcolor-examples'); - for (i = 0; i < examples.length; i++) { + for (let i = 0; i < examples.length; i++) { try { - var listItem = document.createElement('li'); - var listColor = new RGBColor(examples[i]); - var exampleDiv = document.createElement('div'); + const listItem = document.createElement('li'); + const listColor = new RGBColor(examples[i]); + const exampleDiv = document.createElement('div'); exampleDiv.style.cssText = 'margin: 3px; ' + 'border: 1px solid black; ' + @@ -278,7 +272,7 @@ function RGBColor (colorString) { // eslint-disable-line no-unused-vars 'color:' + listColor.toHex() ; exampleDiv.appendChild(document.createTextNode('test')); - var listItemValue = document.createTextNode( + const listItemValue = document.createTextNode( ' ' + examples[i] + ' -> ' + listColor.toRGB() + ' -> ' + listColor.toHex() ); listItem.appendChild(exampleDiv); diff --git a/editor/config-sample.js b/editor/config-sample.js index 608ba217..121e355e 100644 --- a/editor/config-sample.js +++ b/editor/config-sample.js @@ -3,7 +3,6 @@ // CREATE A NEW FILE config.js AND ADD CONTENTS // SUCH AS SHOWN BELOW INTO THAT FILE. -/* globals svgEditor */ /* The config.js file is intended for the setting of configuration or preferences which must run early on; if this is not needed, it is @@ -19,6 +18,8 @@ See defaultConfig and defaultExtensions in svg-editor.js for a list See svg-editor.js for documentation on using setConfig(). */ +import svgEditor from './svg-editor.js'; + // URL OVERRIDE CONFIG svgEditor.setConfig({ /** @@ -117,25 +118,23 @@ As with configuration, one may use allowInitialUserOverride, but Failing to use allowInitialUserOverride will ensure preferences are hard-coded here regardless of URL or prior user storage setting. */ -svgEditor.setConfig( - { - // lang: '', // Set dynamically within locale.js if not previously set - // iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise - /** - * When showing the preferences dialog, svg-editor.js currently relies - * on curPrefs instead of $.pref, so allowing an override for bkgd_color - * means that this value won't have priority over block auto-detection as - * far as determining which color shows initially in the preferences - * dialog (though it can be changed and saved). - */ - // bkgd_color: '#FFF', - // bkgd_url: '', - // img_save: 'embed', - // Only shows in UI as far as alert notices - // save_notice_done: false, - // export_notice_done: false - } -); +svgEditor.setConfig({ + // lang: '', // Set dynamically within locale.js if not previously set + // iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise + /** + * When showing the preferences dialog, svg-editor.js currently relies + * on curPrefs instead of $.pref, so allowing an override for bkgd_color + * means that this value won't have priority over block auto-detection as + * far as determining which color shows initially in the preferences + * dialog (though it can be changed and saved). + */ + // bkgd_color: '#FFF', + // bkgd_url: '', + // img_save: 'embed', + // Only shows in UI as far as alert notices + // save_notice_done: false, + // export_notice_done: false +}); svgEditor.setConfig( { // Indicate pref settings here if you wish to allow user storage or URL settings diff --git a/editor/contextmenu.js b/editor/contextmenu.js index 0df08ab4..e04f69ba 100644 --- a/editor/contextmenu.js +++ b/editor/contextmenu.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals $, svgEditor */ +/* globals jQuery */ /** * Package: svgedit.contextmenu * @@ -9,58 +8,50 @@ */ // Dependencies: // 1) jQuery (for dom injection of context menus) -var svgedit = svgedit || {}; // eslint-disable-line no-use-before-define -(function () { -var self = this; -if (!svgedit.contextmenu) { - svgedit.contextmenu = {}; -} -self.contextMenuExtensions = {}; -var menuItemIsValid = function (menuItem) { + +const $ = jQuery; + +let contextMenuExtensions = {}; + +const menuItemIsValid = function (menuItem) { return menuItem && menuItem.id && menuItem.label && menuItem.action && typeof menuItem.action === 'function'; }; -var addContextMenuItem = function (menuItem) { +export const addContextMenuItem = function (menuItem) { // menuItem: {id, label, shortcut, action} if (!menuItemIsValid(menuItem)) { console.error('Menu items must be defined and have at least properties: id, label, action, where action must be a function'); return; } - if (menuItem.id in self.contextMenuExtensions) { + if (menuItem.id in contextMenuExtensions) { console.error('Cannot add extension "' + menuItem.id + '", an extension by that name already exists"'); return; } // Register menuItem action, see below for deferred menu dom injection console.log('Registed contextmenu item: {id:' + menuItem.id + ', label:' + menuItem.label + '}'); - self.contextMenuExtensions[menuItem.id] = menuItem; + contextMenuExtensions[menuItem.id] = menuItem; // TODO: Need to consider how to handle custom enable/disable behavior }; -var hasCustomHandler = function (handlerKey) { - return self.contextMenuExtensions[handlerKey] && true; +export const hasCustomMenuItemHandler = function (handlerKey) { + return Boolean(contextMenuExtensions[handlerKey]); }; -var getCustomHandler = function (handlerKey) { - return self.contextMenuExtensions[handlerKey].action; +export const getCustomMenuItemHandler = function (handlerKey) { + return contextMenuExtensions[handlerKey].action; }; -var injectExtendedContextMenuItemIntoDom = function (menuItem) { - if (Object.keys(self.contextMenuExtensions).length === 0) { +const injectExtendedContextMenuItemIntoDom = function (menuItem) { + if (!Object.keys(contextMenuExtensions).length) { // all menuItems appear at the bottom of the menu in their own container. // if this is the first extension menu we need to add the separator. $('#cmenu_canvas').append("
  • "); } - var shortcut = menuItem.shortcut || ''; + const shortcut = menuItem.shortcut || ''; $('#cmenu_canvas').append("
  • " + menuItem.label + "" + shortcut + '
  • '); }; -// Defer injection to wait out initial menu processing. This probably goes away once all context -// menu behavior is brought here. -svgEditor.ready(function () { - var menuItem; - for (menuItem in self.contextMenuExtensions) { - injectExtendedContextMenuItemIntoDom(self.contextMenuExtensions[menuItem]); + +export const injectExtendedContextMenuItemsIntoDom = function () { + for (const menuItem in contextMenuExtensions) { + injectExtendedContextMenuItemIntoDom(contextMenuExtensions[menuItem]); } -}); -svgedit.contextmenu.resetCustomMenus = function () { self.contextMenuExtensions = {}; }; -svgedit.contextmenu.add = addContextMenuItem; -svgedit.contextmenu.hasCustomHandler = hasCustomHandler; -svgedit.contextmenu.getCustomHandler = getCustomHandler; -}()); +}; +export const resetCustomMenus = function () { contextMenuExtensions = {}; }; diff --git a/editor/contextmenu/jquery.contextMenu.js b/editor/contextmenu/jquery.contextMenu.js index 7d02ac9d..0e1e72a2 100755 --- a/editor/contextmenu/jquery.contextMenu.js +++ b/editor/contextmenu/jquery.contextMenu.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals jQuery, $, svgedit */ +// Todo: Update to latest version and adapt (and needs jQuery update as well): https://github.com/swisnl/jQuery-contextMenu // jQuery Context Menu Plugin // // Version 1.01 @@ -15,191 +14,190 @@ // This plugin is dual-licensed under the GNU General Public License // and the MIT License and is copyright A Beautiful Site, LLC. // -if (jQuery) { - (function () { - var win = $(window); - var doc = $(document); +import {isMac} from './browser.js'; - $.extend($.fn, { +export default function ($) { + const win = $(window); + const doc = $(document); - contextMenu: function (o, callback) { - // Defaults - if (o.menu === undefined) return false; - if (o.inSpeed === undefined) o.inSpeed = 150; - if (o.outSpeed === undefined) o.outSpeed = 75; - // 0 needs to be -1 for expected results (no fade) - if (o.inSpeed === 0) o.inSpeed = -1; - if (o.outSpeed === 0) o.outSpeed = -1; - // Loop each context menu - $(this).each(function () { - var el = $(this); - var offset = $(el).offset(); + $.extend($.fn, { + contextMenu (o, callback) { + // Defaults + if (o.menu === undefined) return false; + if (o.inSpeed === undefined) o.inSpeed = 150; + if (o.outSpeed === undefined) o.outSpeed = 75; + // 0 needs to be -1 for expected results (no fade) + if (o.inSpeed === 0) o.inSpeed = -1; + if (o.outSpeed === 0) o.outSpeed = -1; + // Loop each context menu + $(this).each(function () { + const el = $(this); + const offset = $(el).offset(); - var menu = $('#' + o.menu); + const menu = $('#' + o.menu); - // Add contextMenu class - menu.addClass('contextMenu'); - // Simulate a true right click - $(this).bind('mousedown', function (e) { - var evt = e; - $(this).mouseup(function (e) { - var srcElement = $(this); - srcElement.unbind('mouseup'); - if (evt.button === 2 || o.allowLeft || - (evt.ctrlKey && svgedit.browser.isMac())) { - e.stopPropagation(); - // Hide context menus that may be showing - $('.contextMenu').hide(); - // Get this context menu + // Add contextMenu class + menu.addClass('contextMenu'); + // Simulate a true right click + $(this).bind('mousedown', function (e) { + const evt = e; + $(this).mouseup(function (e) { + const srcElement = $(this); + srcElement.unbind('mouseup'); + if (evt.button === 2 || o.allowLeft || + (evt.ctrlKey && isMac())) { + e.stopPropagation(); + // Hide context menus that may be showing + $('.contextMenu').hide(); + // Get this context menu - if (el.hasClass('disabled')) return false; + if (el.hasClass('disabled')) return false; - // Detect mouse position - var x = e.pageX, y = e.pageY; + // Detect mouse position + let x = e.pageX, y = e.pageY; - var xOff = win.width() - menu.width(), - yOff = win.height() - menu.height(); + const xOff = win.width() - menu.width(), + yOff = win.height() - menu.height(); - if (x > xOff - 15) x = xOff - 15; - if (y > yOff - 30) y = yOff - 30; // 30 is needed to prevent scrollbars in FF + if (x > xOff - 15) x = xOff - 15; + if (y > yOff - 30) y = yOff - 30; // 30 is needed to prevent scrollbars in FF - // Show the menu - doc.unbind('click'); - menu.css({ top: y, left: x }).fadeIn(o.inSpeed); - // Hover events - menu.find('A').mouseover(function () { - menu.find('LI.hover').removeClass('hover'); - $(this).parent().addClass('hover'); - }).mouseout(function () { - menu.find('LI.hover').removeClass('hover'); - }); + // Show the menu + doc.unbind('click'); + menu.css({ top: y, left: x }).fadeIn(o.inSpeed); + // Hover events + menu.find('A').mouseover(function () { + menu.find('LI.hover').removeClass('hover'); + $(this).parent().addClass('hover'); + }).mouseout(function () { + menu.find('LI.hover').removeClass('hover'); + }); - // Keyboard - doc.keypress(function (e) { - switch (e.keyCode) { - case 38: // up - if (!menu.find('LI.hover').length) { - menu.find('LI:last').addClass('hover'); - } else { - menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover'); - if (!menu.find('LI.hover').length) menu.find('LI:last').addClass('hover'); - } - break; - case 40: // down - if (menu.find('LI.hover').length === 0) { - menu.find('LI:first').addClass('hover'); - } else { - menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover'); - if (!menu.find('LI.hover').length) menu.find('LI:first').addClass('hover'); - } - break; - case 13: // enter - menu.find('LI.hover A').trigger('click'); - break; - case 27: // esc - doc.trigger('click'); - break; + // Keyboard + doc.keypress(function (e) { + switch (e.keyCode) { + case 38: // up + if (!menu.find('LI.hover').length) { + menu.find('LI:last').addClass('hover'); + } else { + menu.find('LI.hover').removeClass('hover').prevAll('LI:not(.disabled)').eq(0).addClass('hover'); + if (!menu.find('LI.hover').length) menu.find('LI:last').addClass('hover'); } - }); + break; + case 40: // down + if (!menu.find('LI.hover').length) { + menu.find('LI:first').addClass('hover'); + } else { + menu.find('LI.hover').removeClass('hover').nextAll('LI:not(.disabled)').eq(0).addClass('hover'); + if (!menu.find('LI.hover').length) menu.find('LI:first').addClass('hover'); + } + break; + case 13: // enter + menu.find('LI.hover A').trigger('click'); + break; + case 27: // esc + doc.trigger('click'); + break; + } + }); - // When items are selected - menu.find('A').unbind('mouseup'); - menu.find('LI:not(.disabled) A').mouseup(function () { + // When items are selected + menu.find('A').unbind('mouseup'); + menu.find('LI:not(.disabled) A').mouseup(function () { + doc.unbind('click').unbind('keypress'); + $('.contextMenu').hide(); + // Callback + if (callback) callback($(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y}); + return false; + }); + + // Hide bindings + setTimeout(function () { // Delay for Mozilla + doc.click(function () { doc.unbind('click').unbind('keypress'); - $('.contextMenu').hide(); - // Callback - if (callback) callback($(this).attr('href').substr(1), $(srcElement), {x: x - offset.left, y: y - offset.top, docX: x, docY: y}); + menu.fadeOut(o.outSpeed); return false; }); - - // Hide bindings - setTimeout(function () { // Delay for Mozilla - doc.click(function () { - doc.unbind('click').unbind('keypress'); - menu.fadeOut(o.outSpeed); - return false; - }); - }, 0); - } - }); + }, 0); + } }); - - // Disable text selection - if ($.browser.mozilla) { - $('#' + o.menu).each(function () { $(this).css({'MozUserSelect': 'none'}); }); - } else if ($.browser.msie) { - $('#' + o.menu).each(function () { $(this).bind('selectstart.disableTextSelect', function () { return false; }); }); - } else { - $('#' + o.menu).each(function () { $(this).bind('mousedown.disableTextSelect', function () { return false; }); }); - } - // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome) - $(el).add($('UL.contextMenu')).bind('contextmenu', function () { return false; }); }); - return $(this); - }, - // Disable context menu items on the fly - disableContextMenuItems: function (o) { - if (o === undefined) { - // Disable all - $(this).find('LI').addClass('disabled'); - return $(this); + // Disable text selection + if ($.browser.mozilla) { + $('#' + o.menu).each(function () { $(this).css({'MozUserSelect': 'none'}); }); + } else if ($.browser.msie) { + $('#' + o.menu).each(function () { $(this).bind('selectstart.disableTextSelect', function () { return false; }); }); + } else { + $('#' + o.menu).each(function () { $(this).bind('mousedown.disableTextSelect', function () { return false; }); }); } - $(this).each(function () { - if (o !== undefined) { - var d = o.split(','); - for (var i = 0; i < d.length; i++) { - $(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled'); - } - } - }); - return $(this); - }, + // Disable browser context menu (requires both selectors to work in IE/Safari + FF/Chrome) + $(el).add($('UL.contextMenu')).bind('contextmenu', function () { return false; }); + }); + return $(this); + }, - // Enable context menu items on the fly - enableContextMenuItems: function (o) { - if (o === undefined) { - // Enable all - $(this).find('LI.disabled').removeClass('disabled'); - return $(this); - } - $(this).each(function () { - if (o !== undefined) { - var d = o.split(','); - for (var i = 0; i < d.length; i++) { - $(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled'); - } - } - }); - return $(this); - }, - - // Disable context menu(s) - disableContextMenu: function () { - $(this).each(function () { - $(this).addClass('disabled'); - }); - return $(this); - }, - - // Enable context menu(s) - enableContextMenu: function () { - $(this).each(function () { - $(this).removeClass('disabled'); - }); - return $(this); - }, - - // Destroy context menu(s) - destroyContextMenu: function () { - // Destroy specified context menus - $(this).each(function () { - // Disable action - $(this).unbind('mousedown').unbind('mouseup'); - }); + // Disable context menu items on the fly + disableContextMenuItems (o) { + if (o === undefined) { + // Disable all + $(this).find('LI').addClass('disabled'); return $(this); } + $(this).each(function () { + if (o !== undefined) { + const d = o.split(','); + for (let i = 0; i < d.length; i++) { + $(this).find('A[href="' + d[i] + '"]').parent().addClass('disabled'); + } + } + }); + return $(this); + }, - }); - })(jQuery); + // Enable context menu items on the fly + enableContextMenuItems (o) { + if (o === undefined) { + // Enable all + $(this).find('LI.disabled').removeClass('disabled'); + return $(this); + } + $(this).each(function () { + if (o !== undefined) { + const d = o.split(','); + for (let i = 0; i < d.length; i++) { + $(this).find('A[href="' + d[i] + '"]').parent().removeClass('disabled'); + } + } + }); + return $(this); + }, + + // Disable context menu(s) + disableContextMenu () { + $(this).each(function () { + $(this).addClass('disabled'); + }); + return $(this); + }, + + // Enable context menu(s) + enableContextMenu () { + $(this).each(function () { + $(this).removeClass('disabled'); + }); + return $(this); + }, + + // Destroy context menu(s) + destroyContextMenu () { + // Destroy specified context menus + $(this).each(function () { + // Disable action + $(this).unbind('mousedown').unbind('mouseup'); + }); + return $(this); + } + }); + return $; } diff --git a/editor/coords.js b/editor/coords.js index 748f1061..00ad3905 100644 --- a/editor/coords.js +++ b/editor/coords.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals $, svgroot */ +/* globals jQuery */ /** * Coords. * @@ -8,25 +7,21 @@ */ // Dependencies: -// 1) jquery.js -// 2) math.js -// 3) pathseg.js -// 4) browser.js -// 5) svgutils.js -// 6) units.js -// 7) svgtransformlist.js +// 1) jquery.min.js -var svgedit = svgedit || {}; // eslint-disable-line no-use-before-define +import './pathseg.js'; +import { + snapToGrid, assignAttributes, getBBox, getRefElem, findDefs +} from './svgutils.js'; +import { + transformPoint, transformListToTransform, matrixMultiply, transformBox +} from './math.js'; +import {getTransformList} from './svgtransformlist.js'; -(function () { -'use strict'; - -if (!svgedit.coords) { - svgedit.coords = {}; -} +const $ = jQuery; // this is how we map paths to our preferred relative segment types -var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', +const pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', 'H', 'h', 'V', 'v', 'S', 's', 'T', 't']; /** @@ -35,12 +30,12 @@ var pathMap = [0, 'z', 'M', 'm', 'L', 'l', 'C', 'c', 'Q', 'q', 'A', 'a', * @property {function} getGridSnapping * @property {function} getDrawing */ -var editorContext_ = null; +let editorContext_ = null; /** * @param {editorContext} editorContext */ -svgedit.coords.init = function (editorContext) { +export const init = function (editorContext) { editorContext_ = editorContext; }; @@ -50,47 +45,45 @@ svgedit.coords.init = function (editorContext) { * @param {Object} changes - Object with changes to be remapped * @param {SVGMatrix} m - Matrix object to use for remapping coordinates */ -svgedit.coords.remapElement = function (selected, changes, m) { - var i, type, - remap = function (x, y) { return svgedit.math.transformPoint(x, y, m); }, +export const remapElement = function (selected, changes, m) { + const remap = function (x, y) { return transformPoint(x, y, m); }, scalew = function (w) { return m.a * w; }, scaleh = function (h) { return m.d * h; }, doSnapping = editorContext_.getGridSnapping() && selected.parentNode.parentNode.localName === 'svg', finishUp = function () { - var o; if (doSnapping) { - for (o in changes) { - changes[o] = svgedit.utilities.snapToGrid(changes[o]); + for (const o in changes) { + changes[o] = snapToGrid(changes[o]); } } - svgedit.utilities.assignAttributes(selected, changes, 1000, true); + assignAttributes(selected, changes, 1000, true); }, - box = svgedit.utilities.getBBox(selected); + box = getBBox(selected); - for (i = 0; i < 2; i++) { - type = i === 0 ? 'fill' : 'stroke'; - var attrVal = selected.getAttribute(type); - if (attrVal && attrVal.indexOf('url(') === 0) { + for (let i = 0; i < 2; i++) { + const type = i === 0 ? 'fill' : 'stroke'; + const attrVal = selected.getAttribute(type); + if (attrVal && attrVal.startsWith('url(')) { if (m.a < 0 || m.d < 0) { - var grad = svgedit.utilities.getRefElem(attrVal); - var newgrad = grad.cloneNode(true); + const grad = getRefElem(attrVal); + const newgrad = grad.cloneNode(true); if (m.a < 0) { // flip x - var x1 = newgrad.getAttribute('x1'); - var x2 = newgrad.getAttribute('x2'); + const x1 = newgrad.getAttribute('x1'); + const x2 = newgrad.getAttribute('x2'); newgrad.setAttribute('x1', -(x1 - 1)); newgrad.setAttribute('x2', -(x2 - 1)); } if (m.d < 0) { // flip y - var y1 = newgrad.getAttribute('y1'); - var y2 = newgrad.getAttribute('y2'); + const y1 = newgrad.getAttribute('y1'); + const y2 = newgrad.getAttribute('y2'); newgrad.setAttribute('y1', -(y1 - 1)); newgrad.setAttribute('y2', -(y2 - 1)); } newgrad.id = editorContext_.getDrawing().getNextId(); - svgedit.utilities.findDefs().appendChild(newgrad); + findDefs().appendChild(newgrad); selected.setAttribute(type, 'url(#' + newgrad.id + ')'); } @@ -101,43 +94,42 @@ svgedit.coords.remapElement = function (selected, changes, m) { } } - var elName = selected.tagName; - var chlist, mt; + const elName = selected.tagName; if (elName === 'g' || elName === 'text' || elName === 'tspan' || elName === 'use') { // if it was a translate, then just update x,y if (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && (m.e !== 0 || m.f !== 0)) { // [T][M] = [M][T'] // therefore [T'] = [M_inv][T][M] - var existing = svgedit.math.transformListToTransform(selected).matrix, - tNew = svgedit.math.matrixMultiply(existing.inverse(), m, existing); + const existing = transformListToTransform(selected).matrix, + tNew = matrixMultiply(existing.inverse(), m, existing); changes.x = parseFloat(changes.x) + tNew.e; changes.y = parseFloat(changes.y) + tNew.f; } else { // we just absorb all matrices into the element and don't do any remapping - chlist = svgedit.transformlist.getTransformList(selected); - mt = svgroot.createSVGTransform(); - mt.setMatrix(svgedit.math.matrixMultiply(svgedit.math.transformListToTransform(chlist).matrix, m)); + const chlist = getTransformList(selected); + const mt = editorContext_.getSVGRoot().createSVGTransform(); + mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m)); chlist.clear(); chlist.appendItem(mt); } } - var c, pt, pt1, pt2, len; + // now we have a set of changes and an applied reduced transform list // we apply the changes directly to the DOM switch (elName) { case 'foreignObject': case 'rect': - case 'image': + case 'image': { // Allow images to be inverted (give them matrix when flipped) if (elName === 'image' && (m.a < 0 || m.d < 0)) { // Convert to matrix - chlist = svgedit.transformlist.getTransformList(selected); - mt = svgroot.createSVGTransform(); - mt.setMatrix(svgedit.math.matrixMultiply(svgedit.math.transformListToTransform(chlist).matrix, m)); + const chlist = getTransformList(selected); + const mt = editorContext_.getSVGRoot().createSVGTransform(); + mt.setMatrix(matrixMultiply(transformListToTransform(chlist).matrix, m)); chlist.clear(); chlist.appendItem(mt); } else { - pt1 = remap(changes.x, changes.y); + const pt1 = remap(changes.x, changes.y); changes.width = scalew(changes.width); changes.height = scaleh(changes.height); changes.x = pt1.x + Math.min(0, changes.width); @@ -147,8 +139,8 @@ svgedit.coords.remapElement = function (selected, changes, m) { } finishUp(); break; - case 'ellipse': - c = remap(changes.cx, changes.cy); + } case 'ellipse': { + const c = remap(changes.cx, changes.cy); changes.cx = c.x; changes.cy = c.y; changes.rx = scalew(changes.rx); @@ -157,62 +149,61 @@ svgedit.coords.remapElement = function (selected, changes, m) { changes.ry = Math.abs(changes.ry); finishUp(); break; - case 'circle': - c = remap(changes.cx, changes.cy); + } case 'circle': { + const c = remap(changes.cx, changes.cy); changes.cx = c.x; changes.cy = c.y; // take the minimum of the new selected box's dimensions for the new circle radius - var tbox = svgedit.math.transformBox(box.x, box.y, box.width, box.height, m); - var w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y; + const tbox = transformBox(box.x, box.y, box.width, box.height, m); + const w = tbox.tr.x - tbox.tl.x, h = tbox.bl.y - tbox.tl.y; changes.r = Math.min(w / 2, h / 2); if (changes.r) { changes.r = Math.abs(changes.r); } finishUp(); break; - case 'line': - pt1 = remap(changes.x1, changes.y1); - pt2 = remap(changes.x2, changes.y2); + } case 'line': { + const pt1 = remap(changes.x1, changes.y1); + const pt2 = remap(changes.x2, changes.y2); changes.x1 = pt1.x; changes.y1 = pt1.y; changes.x2 = pt2.x; changes.y2 = pt2.y; - // deliberately fall through here + } // Fallthrough case 'text': case 'tspan': - case 'use': + case 'use': { finishUp(); break; - case 'g': - var gsvg = $(selected).data('gsvg'); + } case 'g': { + const gsvg = $(selected).data('gsvg'); if (gsvg) { - svgedit.utilities.assignAttributes(gsvg, changes, 1000, true); + assignAttributes(gsvg, changes, 1000, true); } break; - case 'polyline': - case 'polygon': - len = changes.points.length; - for (i = 0; i < len; ++i) { - pt = changes.points[i]; - pt = remap(pt.x, pt.y); - changes.points[i].x = pt.x; - changes.points[i].y = pt.y; + } case 'polyline': + case 'polygon': { + const len = changes.points.length; + for (let i = 0; i < len; ++i) { + const pt = changes.points[i]; + const {x, y} = remap(pt.x, pt.y); + changes.points[i].x = x; + changes.points[i].y = y; } - len = changes.points.length; - var pstr = ''; - for (i = 0; i < len; ++i) { - pt = changes.points[i]; + // const len = changes.points.length; + let pstr = ''; + for (let i = 0; i < len; ++i) { + const pt = changes.points[i]; pstr += pt.x + ',' + pt.y + ' '; } selected.setAttribute('points', pstr); break; - case 'path': - var seg; - var segList = selected.pathSegList; - len = segList.numberOfItems; + } case 'path': { + const segList = selected.pathSegList; + let len = segList.numberOfItems; changes.d = []; - for (i = 0; i < len; ++i) { - seg = segList.getItem(i); + for (let i = 0; i < len; ++i) { + const seg = segList.getItem(i); changes.d[i] = { type: seg.pathSegType, x: seg.x, @@ -230,21 +221,21 @@ svgedit.coords.remapElement = function (selected, changes, m) { } len = changes.d.length; - var firstseg = changes.d[0], + const firstseg = changes.d[0], currentpt = remap(firstseg.x, firstseg.y); changes.d[0].x = currentpt.x; changes.d[0].y = currentpt.y; - for (i = 1; i < len; ++i) { - seg = changes.d[i]; - type = seg.type; + for (let i = 1; i < len; ++i) { + const seg = changes.d[i]; + const {type} = seg; // if absolute or first segment, we want to remap x, y, x1, y1, x2, y2 // if relative, we want to scalew, scaleh if (type % 2 === 0) { // absolute - var thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands + const thisx = (seg.x !== undefined) ? seg.x : currentpt.x, // for V commands thisy = (seg.y !== undefined) ? seg.y : currentpt.y; // for H commands - pt = remap(thisx, thisy); - pt1 = remap(seg.x1, seg.y1); - pt2 = remap(seg.x2, seg.y2); + const pt = remap(thisx, thisy); + const pt1 = remap(seg.x1, seg.y1); + const pt2 = remap(seg.x2, seg.y2); seg.x = pt.x; seg.y = pt.y; seg.x1 = pt1.x; @@ -265,11 +256,11 @@ svgedit.coords.remapElement = function (selected, changes, m) { } } // for each segment - var dstr = ''; + let dstr = ''; len = changes.d.length; - for (i = 0; i < len; ++i) { - seg = changes.d[i]; - type = seg.type; + for (let i = 0; i < len; ++i) { + const seg = changes.d[i]; + const {type} = seg; dstr += pathMap[type]; switch (type) { case 13: // relative horizontal line (h) @@ -312,5 +303,5 @@ svgedit.coords.remapElement = function (selected, changes, m) { selected.setAttribute('d', dstr); break; } + } }; -}()); diff --git a/editor/draw.js b/editor/draw.js index 223a1e2c..f216f4f7 100644 --- a/editor/draw.js +++ b/editor/draw.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals $, svgedit */ +/* globals jQuery */ /** * Package: svgedit.draw * @@ -8,435 +7,21 @@ * Copyright(c) 2011 Jeff Schiller */ -// Dependencies: -// 1) jQuery -// 2) browser.js -// 3) svgutils.js +import {NS} from './svgedit.js'; +import {isOpera} from './browser.js'; +import {copyElem as utilCopyElem} from './svgutils.js'; +import Layer from './layer.js'; -(function () { -'use strict'; +const $ = jQuery; -if (!svgedit.draw) { - svgedit.draw = {}; -} -// alias -var NS = svgedit.NS; +const visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(','); -var visElems = 'a,circle,ellipse,foreignObject,g,image,line,path,polygon,polyline,rect,svg,text,tspan,use'.split(','); - -var RandomizeModes = { +const RandomizeModes = { LET_DOCUMENT_DECIDE: 0, ALWAYS_RANDOMIZE: 1, NEVER_RANDOMIZE: 2 }; -var randomizeIds = RandomizeModes.LET_DOCUMENT_DECIDE; - -/** - * Called to ensure that drawings will or will not have randomized ids. - * The currentDrawing will have its nonce set if it doesn't already. - * @param {boolean} enableRandomization - flag indicating if documents should have randomized ids - * @param {svgedit.draw.Drawing} currentDrawing - */ -svgedit.draw.randomizeIds = function (enableRandomization, currentDrawing) { - randomizeIds = enableRandomization === false - ? RandomizeModes.NEVER_RANDOMIZE - : RandomizeModes.ALWAYS_RANDOMIZE; - - if (randomizeIds === RandomizeModes.ALWAYS_RANDOMIZE && !currentDrawing.getNonce()) { - currentDrawing.setNonce(Math.floor(Math.random() * 100001)); - } else if (randomizeIds === RandomizeModes.NEVER_RANDOMIZE && currentDrawing.getNonce()) { - currentDrawing.clearNonce(); - } -}; - -/** - * This class encapsulates the concept of a SVG-edit drawing - * @param {SVGSVGElement} svgElem - The SVG DOM Element that this JS object - * encapsulates. If the svgElem has a se:nonce attribute on it, then - * IDs will use the nonce as they are generated. - * @param {String=svg_} [optIdPrefix] - The ID prefix to use. - */ -svgedit.draw.Drawing = function (svgElem, optIdPrefix) { - if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI || - svgElem.tagName !== 'svg' || svgElem.namespaceURI !== NS.SVG) { - throw new Error('Error: svgedit.draw.Drawing instance initialized without a element'); - } - - /** - * The SVG DOM Element that represents this drawing. - * @type {SVGSVGElement} - */ - this.svgElem_ = svgElem; - - /** - * The latest object number used in this drawing. - * @type {number} - */ - this.obj_num = 0; - - /** - * The prefix to prepend to each element id in the drawing. - * @type {String} - */ - this.idPrefix = optIdPrefix || 'svg_'; - - /** - * An array of released element ids to immediately reuse. - * @type {Array.} - */ - this.releasedNums = []; - - /** - * The z-ordered array of Layer objects. Each layer has a name - * and group element. - * The first layer is the one at the bottom of the rendering. - * @type {Array.} - */ - this.all_layers = []; - - /** - * Map of all_layers by name. - * - * Note: Layers are ordered, but referenced externally by name; so, we need both container - * types depending on which function is called (i.e. all_layers and layer_map). - * - * @type {Object.} - */ - this.layer_map = {}; - - /** - * The current layer being used. - * @type {Layer} - */ - this.current_layer = null; - - /** - * The nonce to use to uniquely identify elements across drawings. - * @type {!String} - */ - this.nonce_ = ''; - var n = this.svgElem_.getAttributeNS(NS.SE, 'nonce'); - // If already set in the DOM, use the nonce throughout the document - // else, if randomizeIds(true) has been called, create and set the nonce. - if (!!n && randomizeIds !== RandomizeModes.NEVER_RANDOMIZE) { - this.nonce_ = n; - } else if (randomizeIds === RandomizeModes.ALWAYS_RANDOMIZE) { - this.setNonce(Math.floor(Math.random() * 100001)); - } -}; - -/** - * @param {string} id Element ID to retrieve - * @returns {Element} SVG element within the root SVGSVGElement -*/ -svgedit.draw.Drawing.prototype.getElem_ = function (id) { - if (this.svgElem_.querySelector) { - // querySelector lookup - return this.svgElem_.querySelector('#' + id); - } - // jQuery lookup: twice as slow as xpath in FF - return $(this.svgElem_).find('[id=' + id + ']')[0]; -}; - -/** - * @returns {SVGSVGElement} - */ -svgedit.draw.Drawing.prototype.getSvgElem = function () { - return this.svgElem_; -}; - -/** - * @returns {!string|number} The previously set nonce - */ -svgedit.draw.Drawing.prototype.getNonce = function () { - return this.nonce_; -}; - -/** - * @param {!string|number} n The nonce to set - */ -svgedit.draw.Drawing.prototype.setNonce = function (n) { - this.svgElem_.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE); - this.svgElem_.setAttributeNS(NS.SE, 'se:nonce', n); - this.nonce_ = n; -}; - -/** - * Clears any previously set nonce - */ -svgedit.draw.Drawing.prototype.clearNonce = function () { - // We deliberately leave any se:nonce attributes alone, - // we just don't use it to randomize ids. - this.nonce_ = ''; -}; - -/** - * Returns the latest object id as a string. - * @return {String} The latest object Id. - */ -svgedit.draw.Drawing.prototype.getId = function () { - return this.nonce_ - ? this.idPrefix + this.nonce_ + '_' + this.obj_num - : this.idPrefix + this.obj_num; -}; - -/** - * Returns the next object Id as a string. - * @return {String} The next object Id to use. - */ -svgedit.draw.Drawing.prototype.getNextId = function () { - var oldObjNum = this.obj_num; - var restoreOldObjNum = false; - - // If there are any released numbers in the release stack, - // use the last one instead of the next obj_num. - // We need to temporarily use obj_num as that is what getId() depends on. - if (this.releasedNums.length > 0) { - this.obj_num = this.releasedNums.pop(); - restoreOldObjNum = true; - } else { - // If we are not using a released id, then increment the obj_num. - this.obj_num++; - } - - // Ensure the ID does not exist. - var id = this.getId(); - while (this.getElem_(id)) { - if (restoreOldObjNum) { - this.obj_num = oldObjNum; - restoreOldObjNum = false; - } - this.obj_num++; - id = this.getId(); - } - // Restore the old object number if required. - if (restoreOldObjNum) { - this.obj_num = oldObjNum; - } - return id; -}; - -/** - * Releases the object Id, letting it be used as the next id in getNextId(). - * This method DOES NOT remove any elements from the DOM, it is expected - * that client code will do this. - * @param {string} id - The id to release. - * @returns {boolean} True if the id was valid to be released, false otherwise. -*/ -svgedit.draw.Drawing.prototype.releaseId = function (id) { - // confirm if this is a valid id for this Document, else return false - var front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : ''); - if (typeof id !== 'string' || id.indexOf(front) !== 0) { - return false; - } - // extract the obj_num of this id - var num = parseInt(id.substr(front.length), 10); - - // if we didn't get a positive number or we already released this number - // then return false. - if (typeof num !== 'number' || num <= 0 || this.releasedNums.indexOf(num) !== -1) { - return false; - } - - // push the released number into the released queue - this.releasedNums.push(num); - - return true; -}; - -/** - * Returns the number of layers in the current drawing. - * @returns {integer} The number of layers in the current drawing. -*/ -svgedit.draw.Drawing.prototype.getNumLayers = function () { - return this.all_layers.length; -}; - -/** - * Check if layer with given name already exists - * @param {string} name - The layer name to check -*/ -svgedit.draw.Drawing.prototype.hasLayer = function (name) { - return this.layer_map[name] !== undefined; -}; - -/** - * Returns the name of the ith layer. If the index is out of range, an empty string is returned. - * @param {integer} i - The zero-based index of the layer you are querying. - * @returns {string} The name of the ith layer (or the empty string if none found) -*/ -svgedit.draw.Drawing.prototype.getLayerName = function (i) { - return i >= 0 && i < this.getNumLayers() ? this.all_layers[i].getName() : ''; -}; - -/** - * @returns {SVGGElement} The SVGGElement representing the current layer. - */ -svgedit.draw.Drawing.prototype.getCurrentLayer = function () { - return this.current_layer ? this.current_layer.getGroup() : null; -}; - -/** - * Get a layer by name. - * @returns {SVGGElement} The SVGGElement representing the named layer or null. - */ -svgedit.draw.Drawing.prototype.getLayerByName = function (name) { - var layer = this.layer_map[name]; - return layer ? layer.getGroup() : null; -}; - -/** - * Returns the name of the currently selected layer. If an error occurs, an empty string - * is returned. - * @returns {string} The name of the currently active layer (or the empty string if none found). -*/ -svgedit.draw.Drawing.prototype.getCurrentLayerName = function () { - return this.current_layer ? this.current_layer.getName() : ''; -}; - -/** - * Set the current layer's name. - * @param {string} name - The new name. - * @param {svgedit.history.HistoryRecordingService} hrService - History recording service - * @returns {string|null} The new name if changed; otherwise, null. - */ -svgedit.draw.Drawing.prototype.setCurrentLayerName = function (name, hrService) { - var finalName = null; - if (this.current_layer) { - var oldName = this.current_layer.getName(); - finalName = this.current_layer.setName(name, hrService); - if (finalName) { - delete this.layer_map[oldName]; - this.layer_map[finalName] = this.current_layer; - } - } - return finalName; -}; - -/** - * Set the current layer's position. - * @param {number} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1 - * @returns {Object} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null. - */ -svgedit.draw.Drawing.prototype.setCurrentLayerPosition = function (newpos) { - var layerCount = this.getNumLayers(); - if (!this.current_layer || newpos < 0 || newpos >= layerCount) { - return null; - } - - var oldpos; - for (oldpos = 0; oldpos < layerCount; ++oldpos) { - if (this.all_layers[oldpos] === this.current_layer) { break; } - } - // some unknown error condition (current_layer not in all_layers) - if (oldpos === layerCount) { return null; } - - if (oldpos !== newpos) { - // if our new position is below us, we need to insert before the node after newpos - var refGroup = null; - var currentGroup = this.current_layer.getGroup(); - var oldNextSibling = currentGroup.nextSibling; - if (newpos > oldpos) { - if (newpos < layerCount - 1) { - refGroup = this.all_layers[newpos + 1].getGroup(); - } - // if our new position is above us, we need to insert before the node at newpos - } else { - refGroup = this.all_layers[newpos].getGroup(); - } - this.svgElem_.insertBefore(currentGroup, refGroup); - - this.identifyLayers(); - this.setCurrentLayer(this.getLayerName(newpos)); - - return { - currentGroup: currentGroup, - oldNextSibling: oldNextSibling - }; - } - return null; -}; - -svgedit.draw.Drawing.prototype.mergeLayer = function (hrService) { - var currentGroup = this.current_layer.getGroup(); - var prevGroup = $(currentGroup).prev()[0]; - if (!prevGroup) { return; } - - hrService.startBatchCommand('Merge Layer'); - - var layerNextSibling = currentGroup.nextSibling; - hrService.removeElement(currentGroup, layerNextSibling, this.svgElem_); - - while (currentGroup.firstChild) { - var child = currentGroup.firstChild; - if (child.localName === 'title') { - hrService.removeElement(child, child.nextSibling, currentGroup); - currentGroup.removeChild(child); - continue; - } - var oldNextSibling = child.nextSibling; - prevGroup.appendChild(child); - hrService.moveElement(child, oldNextSibling, currentGroup); - } - - // Remove current layer's group - this.current_layer.removeGroup(); - // Remove the current layer and set the previous layer as the new current layer - var index = this.all_layers.indexOf(this.current_layer); - if (index > 0) { - var name = this.current_layer.getName(); - this.current_layer = this.all_layers[index - 1]; - this.all_layers.splice(index, 1); - delete this.layer_map[name]; - } - - hrService.endBatchCommand(); -}; - -svgedit.draw.Drawing.prototype.mergeAllLayers = function (hrService) { - // Set the current layer to the last layer. - this.current_layer = this.all_layers[this.all_layers.length - 1]; - - hrService.startBatchCommand('Merge all Layers'); - while (this.all_layers.length > 1) { - this.mergeLayer(hrService); - } - hrService.endBatchCommand(); -}; - -/** - * Sets the current layer. If the name is not a valid layer name, then this - * function returns false. Otherwise it returns true. This is not an - * undo-able action. - * @param {string} name - The name of the layer you want to switch to. - * @returns {boolean} true if the current layer was switched, otherwise false - */ -svgedit.draw.Drawing.prototype.setCurrentLayer = function (name) { - var layer = this.layer_map[name]; - if (layer) { - if (this.current_layer) { - this.current_layer.deactivate(); - } - this.current_layer = layer; - this.current_layer.activate(); - return true; - } - return false; -}; - -/** - * Deletes the current layer from the drawing and then clears the selection. - * This function then calls the 'changed' handler. This is an undoable action. - * @returns {SVGGElement} The SVGGElement of the layer removed or null. - */ -svgedit.draw.Drawing.prototype.deleteCurrentLayer = function () { - if (this.current_layer && this.getNumLayers() > 1) { - var oldLayerGroup = this.current_layer.removeGroup(); - this.identifyLayers(); - return oldLayerGroup; - } - return null; -}; +let randIds = RandomizeModes.LET_DOCUMENT_DECIDE; /** * Find the layer name in a group element. @@ -444,13 +29,11 @@ svgedit.draw.Drawing.prototype.deleteCurrentLayer = function () { * @returns {string} The layer name or empty string. */ function findLayerNameInGroup (group) { - var name = $('title', group).text(); - - // Hack for Opera 10.60 - if (!name && svgedit.browser.isOpera() && group.querySelectorAll) { - name = $(group.querySelectorAll('title')).text(); - } - return name; + return $('title', group).text() || + (isOpera() && group.querySelectorAll + // Hack for Opera 10.60 + ? $(group.querySelectorAll('title')).text() + : ''); } /** @@ -459,205 +42,613 @@ function findLayerNameInGroup (group) { * @returns {string} - The new name. */ function getNewLayerName (existingLayerNames) { - var i = 1; + let i = 1; // TODO(codedread): What about internationalization of "Layer"? - while (existingLayerNames.indexOf(('Layer ' + i)) >= 0) { i++; } + while (existingLayerNames.includes(('Layer ' + i))) { i++; } return 'Layer ' + i; } /** - * Updates layer system and sets the current layer to the - * top-most layer (last child of this drawing). -*/ -svgedit.draw.Drawing.prototype.identifyLayers = function () { - this.all_layers = []; - this.layer_map = {}; - var numchildren = this.svgElem_.childNodes.length; - // loop through all children of SVG element - var orphans = [], layernames = []; - var layer = null; - var childgroups = false; - for (var i = 0; i < numchildren; ++i) { - var child = this.svgElem_.childNodes.item(i); - // for each g, find its layer name - if (child && child.nodeType === 1) { - if (child.tagName === 'g') { - childgroups = true; - var name = findLayerNameInGroup(child); - if (name) { - layernames.push(name); - layer = new svgedit.draw.Layer(name, child); - this.all_layers.push(layer); - this.layer_map[name] = layer; - } else { - // if group did not have a name, it is an orphan - orphans.push(child); - } - } else if (~visElems.indexOf(child.nodeName)) { - // Child is "visible" (i.e. not a or element), so it is an orphan - orphans.push(child); - } + * This class encapsulates the concept of a SVG-edit drawing + * @param {SVGSVGElement} svgElem - The SVG DOM Element that this JS object + * encapsulates. If the svgElem has a se:nonce attribute on it, then + * IDs will use the nonce as they are generated. + * @param {String=svg_} [optIdPrefix] - The ID prefix to use. + */ +export class Drawing { + constructor (svgElem, optIdPrefix) { + if (!svgElem || !svgElem.tagName || !svgElem.namespaceURI || + svgElem.tagName !== 'svg' || svgElem.namespaceURI !== NS.SVG) { + throw new Error('Error: svgedit.draw.Drawing instance initialized without a element'); + } + + /** + * The SVG DOM Element that represents this drawing. + * @type {SVGSVGElement} + */ + this.svgElem_ = svgElem; + + /** + * The latest object number used in this drawing. + * @type {number} + */ + this.obj_num = 0; + + /** + * The prefix to prepend to each element id in the drawing. + * @type {String} + */ + this.idPrefix = optIdPrefix || 'svg_'; + + /** + * An array of released element ids to immediately reuse. + * @type {Array.} + */ + this.releasedNums = []; + + /** + * The z-ordered array of Layer objects. Each layer has a name + * and group element. + * The first layer is the one at the bottom of the rendering. + * @type {Array.} + */ + this.all_layers = []; + + /** + * Map of all_layers by name. + * + * Note: Layers are ordered, but referenced externally by name; so, we need both container + * types depending on which function is called (i.e. all_layers and layer_map). + * + * @type {Object.} + */ + this.layer_map = {}; + + /** + * The current layer being used. + * @type {Layer} + */ + this.current_layer = null; + + /** + * The nonce to use to uniquely identify elements across drawings. + * @type {!String} + */ + this.nonce_ = ''; + const n = this.svgElem_.getAttributeNS(NS.SE, 'nonce'); + // If already set in the DOM, use the nonce throughout the document + // else, if randomizeIds(true) has been called, create and set the nonce. + if (!!n && randIds !== RandomizeModes.NEVER_RANDOMIZE) { + this.nonce_ = n; + } else if (randIds === RandomizeModes.ALWAYS_RANDOMIZE) { + this.setNonce(Math.floor(Math.random() * 100001)); } } - // If orphans or no layers found, create a new layer and add all the orphans to it - if (orphans.length > 0 || !childgroups) { - layer = new svgedit.draw.Layer(getNewLayerName(layernames), null, this.svgElem_); - layer.appendChildren(orphans); - this.all_layers.push(layer); - this.layer_map[name] = layer; - } else { - layer.activate(); - } - this.current_layer = layer; -}; - -/** - * Creates a new top-level layer in the drawing with the given name and - * makes it the current layer. - * @param {string} name - The given name. If the layer name exists, a new name will be generated. - * @param {svgedit.history.HistoryRecordingService} hrService - History recording service - * @returns {SVGGElement} The SVGGElement of the new layer, which is - * also the current layer of this drawing. -*/ -svgedit.draw.Drawing.prototype.createLayer = function (name, hrService) { - if (this.current_layer) { - this.current_layer.deactivate(); - } - // Check for duplicate name. - if (name === undefined || name === null || name === '' || this.layer_map[name]) { - name = getNewLayerName(Object.keys(this.layer_map)); + /** + * @param {string} id Element ID to retrieve + * @returns {Element} SVG element within the root SVGSVGElement + */ + getElem_ (id) { + if (this.svgElem_.querySelector) { + // querySelector lookup + return this.svgElem_.querySelector('#' + id); + } + // jQuery lookup: twice as slow as xpath in FF + return $(this.svgElem_).find('[id=' + id + ']')[0]; } - // Crate new layer and add to DOM as last layer - var layer = new svgedit.draw.Layer(name, null, this.svgElem_); - // Like to assume hrService exists, but this is backwards compatible with old version of createLayer. - if (hrService) { - hrService.startBatchCommand('Create Layer'); - hrService.insertElement(layer.getGroup()); - hrService.endBatchCommand(); + /** + * @returns {SVGSVGElement} + */ + getSvgElem () { + return this.svgElem_; } - this.all_layers.push(layer); - this.layer_map[name] = layer; - this.current_layer = layer; - return layer.getGroup(); -}; - -/** - * Creates a copy of the current layer with the given name and makes it the current layer. - * @param {string} name - The given name. If the layer name exists, a new name will be generated. - * @param {svgedit.history.HistoryRecordingService} hrService - History recording service - * @returns {SVGGElement} The SVGGElement of the new layer, which is - * also the current layer of this drawing. -*/ -svgedit.draw.Drawing.prototype.cloneLayer = function (name, hrService) { - if (!this.current_layer) { return null; } - this.current_layer.deactivate(); - // Check for duplicate name. - if (name === undefined || name === null || name === '' || this.layer_map[name]) { - name = getNewLayerName(Object.keys(this.layer_map)); + /** + * @returns {!string|number} The previously set nonce + */ + getNonce () { + return this.nonce_; } - // Create new group and add to DOM just after current_layer - var currentGroup = this.current_layer.getGroup(); - var layer = new svgedit.draw.Layer(name, currentGroup, this.svgElem_); - var group = layer.getGroup(); - - // Clone children - var children = currentGroup.childNodes; - var index; - for (index = 0; index < children.length; index++) { - var ch = children[index]; - if (ch.localName === 'title') { continue; } - group.appendChild(this.copyElem(ch)); + /** + * @param {!string|number} n The nonce to set + */ + setNonce (n) { + this.svgElem_.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE); + this.svgElem_.setAttributeNS(NS.SE, 'se:nonce', n); + this.nonce_ = n; } - if (hrService) { - hrService.startBatchCommand('Duplicate Layer'); - hrService.insertElement(group); - hrService.endBatchCommand(); + /** + * Clears any previously set nonce + */ + clearNonce () { + // We deliberately leave any se:nonce attributes alone, + // we just don't use it to randomize ids. + this.nonce_ = ''; } - // Update layer containers and current_layer. - index = this.all_layers.indexOf(this.current_layer); - if (index >= 0) { - this.all_layers.splice(index + 1, 0, layer); - } else { - this.all_layers.push(layer); + /** + * Returns the latest object id as a string. + * @return {String} The latest object Id. + */ + getId () { + return this.nonce_ + ? this.idPrefix + this.nonce_ + '_' + this.obj_num + : this.idPrefix + this.obj_num; } - this.layer_map[name] = layer; - this.current_layer = layer; - return group; -}; -/** - * Returns whether the layer is visible. If the layer name is not valid, - * then this function returns false. - * @param {string} layername - The name of the layer which you want to query. - * @returns {boolean} The visibility state of the layer, or false if the layer name was invalid. -*/ -svgedit.draw.Drawing.prototype.getLayerVisibility = function (layername) { - var layer = this.layer_map[layername]; - return layer ? layer.isVisible() : false; -}; + /** + * Returns the next object Id as a string. + * @return {String} The next object Id to use. + */ + getNextId () { + const oldObjNum = this.obj_num; + let restoreOldObjNum = false; -/** - * Sets the visibility of the layer. If the layer name is not valid, this - * function returns false, otherwise it returns true. This is an - * undo-able action. - * @param {string} layername - The name of the layer to change the visibility - * @param {boolean} bVisible - Whether the layer should be visible - * @returns {?SVGGElement} The SVGGElement representing the layer if the - * layername was valid, otherwise null. -*/ -svgedit.draw.Drawing.prototype.setLayerVisibility = function (layername, bVisible) { - if (typeof bVisible !== 'boolean') { + // If there are any released numbers in the release stack, + // use the last one instead of the next obj_num. + // We need to temporarily use obj_num as that is what getId() depends on. + if (this.releasedNums.length > 0) { + this.obj_num = this.releasedNums.pop(); + restoreOldObjNum = true; + } else { + // If we are not using a released id, then increment the obj_num. + this.obj_num++; + } + + // Ensure the ID does not exist. + let id = this.getId(); + while (this.getElem_(id)) { + if (restoreOldObjNum) { + this.obj_num = oldObjNum; + restoreOldObjNum = false; + } + this.obj_num++; + id = this.getId(); + } + // Restore the old object number if required. + if (restoreOldObjNum) { + this.obj_num = oldObjNum; + } + return id; + } + + /** + * Releases the object Id, letting it be used as the next id in getNextId(). + * This method DOES NOT remove any elements from the DOM, it is expected + * that client code will do this. + * @param {string} id - The id to release. + * @returns {boolean} True if the id was valid to be released, false otherwise. + */ + releaseId (id) { + // confirm if this is a valid id for this Document, else return false + const front = this.idPrefix + (this.nonce_ ? this.nonce_ + '_' : ''); + if (typeof id !== 'string' || !id.startsWith(front)) { + return false; + } + // extract the obj_num of this id + const num = parseInt(id.substr(front.length), 10); + + // if we didn't get a positive number or we already released this number + // then return false. + if (typeof num !== 'number' || num <= 0 || this.releasedNums.includes(num)) { + return false; + } + + // push the released number into the released queue + this.releasedNums.push(num); + + return true; + } + + /** + * Returns the number of layers in the current drawing. + * @returns {integer} The number of layers in the current drawing. + */ + getNumLayers () { + return this.all_layers.length; + } + + /** + * Check if layer with given name already exists + * @param {string} name - The layer name to check + */ + hasLayer (name) { + return this.layer_map[name] !== undefined; + } + + /** + * Returns the name of the ith layer. If the index is out of range, an empty string is returned. + * @param {integer} i - The zero-based index of the layer you are querying. + * @returns {string} The name of the ith layer (or the empty string if none found) + */ + getLayerName (i) { + return i >= 0 && i < this.getNumLayers() ? this.all_layers[i].getName() : ''; + } + + /** + * @returns {SVGGElement} The SVGGElement representing the current layer. + */ + getCurrentLayer () { + return this.current_layer ? this.current_layer.getGroup() : null; + } + + /** + * Get a layer by name. + * @returns {SVGGElement} The SVGGElement representing the named layer or null. + */ + getLayerByName (name) { + const layer = this.layer_map[name]; + return layer ? layer.getGroup() : null; + } + + /** + * Returns the name of the currently selected layer. If an error occurs, an empty string + * is returned. + * @returns {string} The name of the currently active layer (or the empty string if none found). + */ + getCurrentLayerName () { + return this.current_layer ? this.current_layer.getName() : ''; + } + + /** + * Set the current layer's name. + * @param {string} name - The new name. + * @param {svgedit.history.HistoryRecordingService} hrService - History recording service + * @returns {string|null} The new name if changed; otherwise, null. + */ + setCurrentLayerName (name, hrService) { + let finalName = null; + if (this.current_layer) { + const oldName = this.current_layer.getName(); + finalName = this.current_layer.setName(name, hrService); + if (finalName) { + delete this.layer_map[oldName]; + this.layer_map[finalName] = this.current_layer; + } + } + return finalName; + } + + /** + * Set the current layer's position. + * @param {number} newpos - The zero-based index of the new position of the layer. Range should be 0 to layers-1 + * @returns {Object} If the name was changed, returns {title:SVGGElement, previousName:string}; otherwise null. + */ + setCurrentLayerPosition (newpos) { + const layerCount = this.getNumLayers(); + if (!this.current_layer || newpos < 0 || newpos >= layerCount) { + return null; + } + + let oldpos; + for (oldpos = 0; oldpos < layerCount; ++oldpos) { + if (this.all_layers[oldpos] === this.current_layer) { break; } + } + // some unknown error condition (current_layer not in all_layers) + if (oldpos === layerCount) { return null; } + + if (oldpos !== newpos) { + // if our new position is below us, we need to insert before the node after newpos + const currentGroup = this.current_layer.getGroup(); + const oldNextSibling = currentGroup.nextSibling; + + let refGroup = null; + if (newpos > oldpos) { + if (newpos < layerCount - 1) { + refGroup = this.all_layers[newpos + 1].getGroup(); + } + // if our new position is above us, we need to insert before the node at newpos + } else { + refGroup = this.all_layers[newpos].getGroup(); + } + this.svgElem_.insertBefore(currentGroup, refGroup); + + this.identifyLayers(); + this.setCurrentLayer(this.getLayerName(newpos)); + + return { + currentGroup, + oldNextSibling + }; + } return null; } - var layer = this.layer_map[layername]; - if (!layer) { return null; } - layer.setVisible(bVisible); - return layer.getGroup(); -}; -/** - * Returns the opacity of the given layer. If the input name is not a layer, null is returned. - * @param {string} layername - name of the layer on which to get the opacity - * @returns {?number} The opacity value of the given layer. This will be a value between 0.0 and 1.0, or null - * if layername is not a valid layer -*/ -svgedit.draw.Drawing.prototype.getLayerOpacity = function (layername) { - var layer = this.layer_map[layername]; - if (!layer) { return null; } - return layer.getOpacity(); -}; + mergeLayer (hrService) { + const currentGroup = this.current_layer.getGroup(); + const prevGroup = $(currentGroup).prev()[0]; + if (!prevGroup) { return; } -/** - * Sets the opacity of the given layer. If the input name is not a layer, - * nothing happens. If opacity is not a value between 0.0 and 1.0, then - * nothing happens. - * @param {string} layername - Name of the layer on which to set the opacity - * @param {number} opacity - A float value in the range 0.0-1.0 -*/ -svgedit.draw.Drawing.prototype.setLayerOpacity = function (layername, opacity) { - if (typeof opacity !== 'number' || opacity < 0.0 || opacity > 1.0) { - return; + hrService.startBatchCommand('Merge Layer'); + + const layerNextSibling = currentGroup.nextSibling; + hrService.removeElement(currentGroup, layerNextSibling, this.svgElem_); + + while (currentGroup.firstChild) { + const child = currentGroup.firstChild; + if (child.localName === 'title') { + hrService.removeElement(child, child.nextSibling, currentGroup); + currentGroup.removeChild(child); + continue; + } + const oldNextSibling = child.nextSibling; + prevGroup.appendChild(child); + hrService.moveElement(child, oldNextSibling, currentGroup); + } + + // Remove current layer's group + this.current_layer.removeGroup(); + // Remove the current layer and set the previous layer as the new current layer + const index = this.all_layers.indexOf(this.current_layer); + if (index > 0) { + const name = this.current_layer.getName(); + this.current_layer = this.all_layers[index - 1]; + this.all_layers.splice(index, 1); + delete this.layer_map[name]; + } + + hrService.endBatchCommand(); } - var layer = this.layer_map[layername]; - if (layer) { - layer.setOpacity(opacity); + + mergeAllLayers (hrService) { + // Set the current layer to the last layer. + this.current_layer = this.all_layers[this.all_layers.length - 1]; + + hrService.startBatchCommand('Merge all Layers'); + while (this.all_layers.length > 1) { + this.mergeLayer(hrService); + } + hrService.endBatchCommand(); } -}; + + /** + * Sets the current layer. If the name is not a valid layer name, then this + * function returns false. Otherwise it returns true. This is not an + * undo-able action. + * @param {string} name - The name of the layer you want to switch to. + * @returns {boolean} true if the current layer was switched, otherwise false + */ + setCurrentLayer (name) { + const layer = this.layer_map[name]; + if (layer) { + if (this.current_layer) { + this.current_layer.deactivate(); + } + this.current_layer = layer; + this.current_layer.activate(); + return true; + } + return false; + } + + /** + * Deletes the current layer from the drawing and then clears the selection. + * This function then calls the 'changed' handler. This is an undoable action. + * @returns {SVGGElement} The SVGGElement of the layer removed or null. + */ + deleteCurrentLayer () { + if (this.current_layer && this.getNumLayers() > 1) { + const oldLayerGroup = this.current_layer.removeGroup(); + this.identifyLayers(); + return oldLayerGroup; + } + return null; + } + + /** + * Updates layer system and sets the current layer to the + * top-most layer (last child of this drawing). + */ + identifyLayers () { + this.all_layers = []; + this.layer_map = {}; + const numchildren = this.svgElem_.childNodes.length; + // loop through all children of SVG element + const orphans = [], layernames = []; + let layer = null; + let childgroups = false; + for (let i = 0; i < numchildren; ++i) { + const child = this.svgElem_.childNodes.item(i); + // for each g, find its layer name + if (child && child.nodeType === 1) { + if (child.tagName === 'g') { + childgroups = true; + const name = findLayerNameInGroup(child); + if (name) { + layernames.push(name); + layer = new Layer(name, child); + this.all_layers.push(layer); + this.layer_map[name] = layer; + } else { + // if group did not have a name, it is an orphan + orphans.push(child); + } + } else if (visElems.includes(child.nodeName)) { + // Child is "visible" (i.e. not a or element), so it is an orphan + orphans.push(child); + } + } + } + + // If orphans or no layers found, create a new layer and add all the orphans to it + if (orphans.length > 0 || !childgroups) { + layer = new Layer(getNewLayerName(layernames), null, this.svgElem_); + layer.appendChildren(orphans); + this.all_layers.push(layer); + this.layer_map[name] = layer; + } else { + layer.activate(); + } + this.current_layer = layer; + } + + /** + * Creates a new top-level layer in the drawing with the given name and + * makes it the current layer. + * @param {string} name - The given name. If the layer name exists, a new name will be generated. + * @param {svgedit.history.HistoryRecordingService} hrService - History recording service + * @returns {SVGGElement} The SVGGElement of the new layer, which is + * also the current layer of this drawing. + */ + createLayer (name, hrService) { + if (this.current_layer) { + this.current_layer.deactivate(); + } + // Check for duplicate name. + if (name === undefined || name === null || name === '' || this.layer_map[name]) { + name = getNewLayerName(Object.keys(this.layer_map)); + } + + // Crate new layer and add to DOM as last layer + const layer = new Layer(name, null, this.svgElem_); + // Like to assume hrService exists, but this is backwards compatible with old version of createLayer. + if (hrService) { + hrService.startBatchCommand('Create Layer'); + hrService.insertElement(layer.getGroup()); + hrService.endBatchCommand(); + } + + this.all_layers.push(layer); + this.layer_map[name] = layer; + this.current_layer = layer; + return layer.getGroup(); + } + + /** + * Creates a copy of the current layer with the given name and makes it the current layer. + * @param {string} name - The given name. If the layer name exists, a new name will be generated. + * @param {svgedit.history.HistoryRecordingService} hrService - History recording service + * @returns {SVGGElement} The SVGGElement of the new layer, which is + * also the current layer of this drawing. + */ + cloneLayer (name, hrService) { + if (!this.current_layer) { return null; } + this.current_layer.deactivate(); + // Check for duplicate name. + if (name === undefined || name === null || name === '' || this.layer_map[name]) { + name = getNewLayerName(Object.keys(this.layer_map)); + } + + // Create new group and add to DOM just after current_layer + const currentGroup = this.current_layer.getGroup(); + const layer = new Layer(name, currentGroup, this.svgElem_); + const group = layer.getGroup(); + + // Clone children + const children = currentGroup.childNodes; + for (let index = 0; index < children.length; index++) { + const ch = children[index]; + if (ch.localName === 'title') { continue; } + group.appendChild(this.copyElem(ch)); + } + + if (hrService) { + hrService.startBatchCommand('Duplicate Layer'); + hrService.insertElement(group); + hrService.endBatchCommand(); + } + + // Update layer containers and current_layer. + const index = this.all_layers.indexOf(this.current_layer); + if (index >= 0) { + this.all_layers.splice(index + 1, 0, layer); + } else { + this.all_layers.push(layer); + } + this.layer_map[name] = layer; + this.current_layer = layer; + return group; + } + + /** + * Returns whether the layer is visible. If the layer name is not valid, + * then this function returns false. + * @param {string} layername - The name of the layer which you want to query. + * @returns {boolean} The visibility state of the layer, or false if the layer name was invalid. + */ + getLayerVisibility (layername) { + const layer = this.layer_map[layername]; + return layer ? layer.isVisible() : false; + } + + /** + * Sets the visibility of the layer. If the layer name is not valid, this + * function returns false, otherwise it returns true. This is an + * undo-able action. + * @param {string} layername - The name of the layer to change the visibility + * @param {boolean} bVisible - Whether the layer should be visible + * @returns {?SVGGElement} The SVGGElement representing the layer if the + * layername was valid, otherwise null. + */ + setLayerVisibility (layername, bVisible) { + if (typeof bVisible !== 'boolean') { + return null; + } + const layer = this.layer_map[layername]; + if (!layer) { return null; } + layer.setVisible(bVisible); + return layer.getGroup(); + } + + /** + * Returns the opacity of the given layer. If the input name is not a layer, null is returned. + * @param {string} layername - name of the layer on which to get the opacity + * @returns {?number} The opacity value of the given layer. This will be a value between 0.0 and 1.0, or null + * if layername is not a valid layer + */ + getLayerOpacity (layername) { + const layer = this.layer_map[layername]; + if (!layer) { return null; } + return layer.getOpacity(); + } + + /** + * Sets the opacity of the given layer. If the input name is not a layer, + * nothing happens. If opacity is not a value between 0.0 and 1.0, then + * nothing happens. + * @param {string} layername - Name of the layer on which to set the opacity + * @param {number} opacity - A float value in the range 0.0-1.0 + */ + setLayerOpacity (layername, opacity) { + if (typeof opacity !== 'number' || opacity < 0.0 || opacity > 1.0) { + return; + } + const layer = this.layer_map[layername]; + if (layer) { + layer.setOpacity(opacity); + } + } + + /** + * Create a clone of an element, updating its ID and its children's IDs when needed + * @param {Element} el - DOM element to clone + * @returns {Element} + */ + copyElem (el) { + const self = this; + const getNextIdClosure = function () { return self.getNextId(); }; + return utilCopyElem(el, getNextIdClosure); + } +} /** - * Create a clone of an element, updating its ID and its children's IDs when needed - * @param {Element} el - DOM element to clone - * @returns {Element} + * Called to ensure that drawings will or will not have randomized ids. + * The currentDrawing will have its nonce set if it doesn't already. + * @param {boolean} enableRandomization - flag indicating if documents should have randomized ids + * @param {svgedit.draw.Drawing} currentDrawing */ -svgedit.draw.Drawing.prototype.copyElem = function (el) { - var self = this; - var getNextIdClosure = function () { return self.getNextId(); }; - return svgedit.utilities.copyElem(el, getNextIdClosure); +export const randomizeIds = function (enableRandomization, currentDrawing) { + randIds = enableRandomization === false + ? RandomizeModes.NEVER_RANDOMIZE + : RandomizeModes.ALWAYS_RANDOMIZE; + + if (randIds === RandomizeModes.ALWAYS_RANDOMIZE && !currentDrawing.getNonce()) { + currentDrawing.setNonce(Math.floor(Math.random() * 100001)); + } else if (randIds === RandomizeModes.NEVER_RANDOMIZE && currentDrawing.getNonce()) { + currentDrawing.clearNonce(); + } }; -}()); diff --git a/editor/embedapi-dom.js b/editor/embedapi-dom.js index 33c661ff..1baf2b93 100644 --- a/editor/embedapi-dom.js +++ b/editor/embedapi-dom.js @@ -1,21 +1,21 @@ -/* eslint-disable no-var */ -/* globals $, EmbeddedSVGEdit */ -var initEmbed; // eslint-disable-line no-unused-vars +/* globals jQuery */ +import EmbeddedSVGEdit from './embedapi.js'; +const $ = jQuery; -// Todo: Get rid of frame.contentWindow dependencies so can be more easily adjusted to work cross-domain +// Todo: Add iframe load listener +var initEmbed; + +// Todo: Get rid of frame.contentWindow dependencies so can be more +// easily adjusted to work cross-domain $(function () { - 'use strict'; - - var svgCanvas = null; - var frame; + let svgCanvas = null; initEmbed = function () { - var doc, mainButton; svgCanvas = new EmbeddedSVGEdit(frame); // Hide main button, as we will be controlling new, load, save, etc. from the host document - doc = frame.contentDocument || frame.contentWindow.document; - mainButton = doc.getElementById('main_button'); + const doc = frame.contentDocument || frame.contentWindow.document; + const mainButton = doc.getElementById('main_button'); mainButton.style.display = 'none'; }; @@ -28,7 +28,7 @@ $(function () { } function loadSvg () { - var svgexample = 'Layer 1'; + const svgexample = 'Layer 1'; svgCanvas.setSvgString(svgexample); } @@ -37,9 +37,9 @@ $(function () { } function exportPNG () { - var str = frame.contentWindow.svgEditor.uiStrings.notification.loadingImage; + const str = frame.contentWindow.svgEditor.uiStrings.notification.loadingImage; - var exportWindow = window.open( + const exportWindow = window.open( 'data:text/html;charset=utf-8,' + encodeURIComponent('' + str + '

    ' + str + '

    '), 'svg-edit-exportWindow' ); @@ -47,7 +47,7 @@ $(function () { } function exportPDF () { - var str = frame.contentWindow.svgEditor.uiStrings.notification.loadingImage; + const str = frame.contentWindow.svgEditor.uiStrings.notification.loadingImage; /** // If you want to handle the PDF blob yourself, do as follows @@ -58,7 +58,7 @@ $(function () { return; */ - var exportWindow = window.open( + const exportWindow = window.open( 'data:text/html;charset=utf-8,' + encodeURIComponent('' + str + '

    ' + str + '

    '), 'svg-edit-exportWindow' ); @@ -76,5 +76,5 @@ $(function () { '" width="900px" height="600px" id="svgedit" onload="initEmbed();">' ) ); - frame = document.getElementById('svgedit'); + const frame = document.getElementById('svgedit'); }); diff --git a/editor/embedapi.html b/editor/embedapi.html index 7e5cb250..dda45a19 100644 --- a/editor/embedapi.html +++ b/editor/embedapi.html @@ -3,9 +3,9 @@ Embed API - - - + + + diff --git a/editor/embedapi.js b/editor/embedapi.js index 0a20732b..5ad926e5 100644 --- a/editor/embedapi.js +++ b/editor/embedapi.js @@ -1,11 +1,10 @@ -/* eslint-disable no-var */ /* Embedded SVG-edit API General usage: - Have an iframe somewhere pointing to a version of svg-edit > r1000 - Initialize the magic with: -var svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit); +const svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit); - Pass functions in this format: svgCanvas.setSvgString('string') - Or if a callback is needed: @@ -32,18 +31,15 @@ JavaScript methods on the frame itself. The only other difference is when handling returns: the callback notation is used instead. -var blah = new EmbeddedSVGEdit(window.frames.svgedit); +const blah = new EmbeddedSVGEdit(window.frames.svgedit); blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, {a: 'tree',b:6, c: 9})(function(){console.log('GET DATA',arguments)}) */ -(function () { -'use strict'; - -var cbid = 0; +let cbid = 0; function getCallbackSetter (d) { return function () { - var t = this, // New callback + const t = this, // New callback args = [].slice.call(arguments), cbid = t.send(d, args, function () {}); // The callback (currently it's nothing, but will be set later) @@ -59,7 +55,7 @@ function getCallbackSetter (d) { * of same domain control */ function addCallback (t, data) { - var result = data.result || data.error, + const result = data.result || data.error, cbid = data.id; if (t.callbacks[cbid]) { if (data.result) { @@ -76,11 +72,11 @@ function messageListener (e) { if (typeof e.data !== 'string') { return; } - var allowedOrigins = this.allowedOrigins, + const {allowedOrigins} = this, data = e.data && JSON.parse(e.data); if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit' || e.source !== this.frame.contentWindow || - (allowedOrigins.indexOf('*') === -1 && allowedOrigins.indexOf(e.origin) === -1) + (!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin)) ) { return; } @@ -99,80 +95,77 @@ function getMessageListener (t) { * messages will be allowed when same origin is not used; defaults to none. * If supplied, it should probably be the same as svgEditor's allowedOrigins */ -function EmbeddedSVGEdit (frame, allowedOrigins) { - if (!(this instanceof EmbeddedSVGEdit)) { // Allow invocation without 'new' keyword - return new EmbeddedSVGEdit(frame); - } - this.allowedOrigins = allowedOrigins || []; - // Initialize communication - this.frame = frame; - this.callbacks = {}; - // List of functions extracted with this: - // Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html +class EmbeddedSVGEdit { + constructor (frame, allowedOrigins) { + this.allowedOrigins = allowedOrigins || []; + // Initialize communication + this.frame = frame; + this.callbacks = {}; + // List of functions extracted with this: + // Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html - // for (var i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q - // var functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString', - // 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', - // 'moveSelectedToLayer', 'clear']; + // for (const i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q + // const functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString', + // 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', + // 'moveSelectedToLayer', 'clear']; - // Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API - // var svgCanvas = frame.contentWindow.svgCanvas; - // var l = []; for (var i in svgCanvas){ if (typeof svgCanvas[i] == 'function') { l.push(i);} }; - // alert("['" + l.join("', '") + "']"); - // Run in svgedit itself - var i, - functions = [ + // Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API + // const {svgCanvas} = frame.contentWindow; + // const l = []; + // for (const i in svgCanvas) { if (typeof svgCanvas[i] === 'function') { l.push(i);} }; + // alert("['" + l.join("', '") + "']"); + // Run in svgedit itself + const functions = [ 'clearSvgContentElement', 'setIdPrefix', 'getCurrentDrawing', 'addSvgElementFromJson', 'getTransformList', 'matrixMultiply', 'hasMatrixTransform', 'transformListToTransform', 'convertToNum', 'findDefs', 'getUrlFromAttr', 'getHref', 'setHref', 'getBBox', 'getRotationAngle', 'getElem', 'getRefElem', 'assignAttributes', 'cleanupElement', 'remapElement', 'recalculateDimensions', 'sanitizeSvg', 'runExtensions', 'addExtension', 'round', 'getIntersectionList', 'getStrokedBBox', 'getVisibleElements', 'getVisibleElementsAndBBoxes', 'groupSvgElem', 'getId', 'getNextId', 'call', 'bind', 'prepareSvg', 'setRotationAngle', 'recalculateAllSelectedDimensions', 'clearSelection', 'addToSelection', 'selectOnly', 'removeFromSelection', 'selectAllInCurrentLayer', 'getMouseTarget', 'removeUnusedDefElems', 'svgCanvasToString', 'svgToString', 'embedImage', 'setGoodImage', 'open', 'save', 'rasterExport', 'exportPDF', 'getSvgString', 'randomizeIds', 'uniquifyElems', 'setUseData', 'convertGradients', 'convertToGroup', 'setSvgString', 'importSvgString', 'identifyLayers', 'createLayer', 'cloneLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', 'moveSelectedToLayer', 'mergeLayer', 'mergeAllLayers', 'leaveContext', 'setContext', 'clear', 'linkControlPoints', 'getContentElem', 'getRootElem', 'getSelectedElems', 'getResolution', 'getZoom', 'getVersion', 'setUiStrings', 'setConfig', 'getTitle', 'setGroupTitle', 'getDocumentTitle', 'setDocumentTitle', 'getEditorNS', 'setResolution', 'getOffset', 'setBBoxZoom', 'setZoom', 'getMode', 'setMode', 'getColor', 'setColor', 'setGradient', 'setPaint', 'setStrokePaint', 'setFillPaint', 'getStrokeWidth', 'setStrokeWidth', 'setStrokeAttr', 'getStyle', 'getOpacity', 'setOpacity', 'getFillOpacity', 'getStrokeOpacity', 'setPaintOpacity', 'getPaintOpacity', 'getBlur', 'setBlurNoUndo', 'setBlurOffsets', 'setBlur', 'getBold', 'setBold', 'getItalic', 'setItalic', 'getFontFamily', 'setFontFamily', 'setFontColor', 'getFontColor', 'getFontSize', 'setFontSize', 'getText', 'setTextContent', 'setImageURL', 'setLinkURL', 'setRectRadius', 'makeHyperlink', 'removeHyperlink', 'setSegType', 'convertToPath', 'changeSelectedAttribute', 'deleteSelectedElements', 'cutSelectedElements', 'copySelectedElements', 'pasteElements', 'groupSelectedElements', 'pushGroupProperties', 'ungroupSelectedElement', 'moveToTopSelectedElement', 'moveToBottomSelectedElement', 'moveUpDownSelected', 'moveSelectedElements', 'cloneSelectedElements', 'alignSelectedElements', 'updateCanvas', 'setBackground', 'cycleElement', 'getPrivateMethods', 'zoomChanged', 'ready' ]; - // TODO: rewrite the following, it's pretty scary. - for (i = 0; i < functions.length; i++) { - this[functions[i]] = getCallbackSetter(functions[i]); + // TODO: rewrite the following, it's pretty scary. + for (let i = 0; i < functions.length; i++) { + this[functions[i]] = getCallbackSetter(functions[i]); + } + + // Older IE may need a polyfill for addEventListener, but so it would for SVG + window.addEventListener('message', getMessageListener(this), false); } - // Older IE may need a polyfill for addEventListener, but so it would for SVG - window.addEventListener('message', getMessageListener(this), false); + send (name, args, callback) { + const t = this; + cbid++; + + this.callbacks[cbid] = callback; + setTimeout((function (cbid) { + return function () { // Delay for the callback to be set in case its synchronous + /* + * Todo: Handle non-JSON arguments and return values (undefined, + * nonfinite numbers, functions, and built-in objects like Date, + * RegExp), etc.? Allow promises instead of callbacks? Review + * SVG-Edit functions for whether JSON-able parameters can be + * made compatile with all API functionality + */ + // We accept and post strings for the sake of IE9 support + if (window.location.origin === t.frame.contentWindow.location.origin) { + // Although we do not really need this API if we are working same + // domain, it could allow us to write in a way that would work + // cross-domain as well, assuming we stick to the argument limitations + // of the current JSON-based communication API (e.g., not passing + // callbacks). We might be able to address these shortcomings; see + // the todo elsewhere in this file. + const message = {id: cbid}, + {svgCanvas} = t.frame.contentWindow; + try { + message.result = svgCanvas[name].apply(svgCanvas, args); + } catch (err) { + message.error = err.message; + } + addCallback(t, message); + } else { // Requires the ext-xdomain-messaging.js extension + t.frame.contentWindow.postMessage(JSON.stringify({namespace: 'svgCanvas', id: cbid, name, args}), '*'); + } + }; + }(cbid)), 0); + + return cbid; + } } -EmbeddedSVGEdit.prototype.send = function (name, args, callback) { - var t = this; - cbid++; - - this.callbacks[cbid] = callback; - setTimeout((function (cbid) { - return function () { // Delay for the callback to be set in case its synchronous - /* - * Todo: Handle non-JSON arguments and return values (undefined, - * nonfinite numbers, functions, and built-in objects like Date, - * RegExp), etc.? Allow promises instead of callbacks? Review - * SVG-Edit functions for whether JSON-able parameters can be - * made compatile with all API functionality - */ - // We accept and post strings for the sake of IE9 support - if (window.location.origin === t.frame.contentWindow.location.origin) { - // Although we do not really need this API if we are working same - // domain, it could allow us to write in a way that would work - // cross-domain as well, assuming we stick to the argument limitations - // of the current JSON-based communication API (e.g., not passing - // callbacks). We might be able to address these shortcomings; see - // the todo elsewhere in this file. - var message = {id: cbid}, - svgCanvas = t.frame.contentWindow.svgCanvas; - try { - message.result = svgCanvas[name].apply(svgCanvas, args); - } catch (err) { - message.error = err.message; - } - addCallback(t, message); - } else { // Requires the ext-xdomain-messaging.js extension - t.frame.contentWindow.postMessage(JSON.stringify({namespace: 'svgCanvas', id: cbid, name: name, args: args}), '*'); - } - }; - }(cbid)), 0); - - return cbid; -}; - -window.embedded_svg_edit = EmbeddedSVGEdit; // Export old, deprecated API -window.EmbeddedSVGEdit = EmbeddedSVGEdit; // Follows common JS convention of CamelCase and, as enforced in JSLint, of initial caps for constructors -}()); +export default EmbeddedSVGEdit; diff --git a/editor/extensions/ext-arrows.js b/editor/extensions/ext-arrows.js index cb8fb612..8ab9e929 100644 --- a/editor/extensions/ext-arrows.js +++ b/editor/extensions/ext-arrows.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, svgCanvas, $ */ +/* globals jQuery, svgEditor, svgCanvas */ /* * ext-arrows.js * @@ -10,11 +9,10 @@ */ svgEditor.addExtension('Arrows', function (S) { - var // svgcontent = S.svgcontent, + const $ = jQuery; + const // {svgcontent} = S, addElem = S.addSvgElementFromJson, - nonce = S.nonce, - randomizeIds = S.randomize_ids, - selElems, pathdata, + {nonce} = S, langList = { 'en': [ {'id': 'arrow_none', 'textContent': 'No arrow'} @@ -23,9 +21,10 @@ svgEditor.addExtension('Arrows', function (S) { {'id': 'arrow_none', 'textContent': 'Sans flèche'} ] }, - arrowprefix, prefix = 'se_arrow_'; + let selElems, arrowprefix, randomizeIds = S.randomize_ids; + function setArrowNonce (window, n) { randomizeIds = true; arrowprefix = prefix + n + '_'; @@ -49,15 +48,15 @@ svgEditor.addExtension('Arrows', function (S) { arrowprefix = prefix; } - pathdata = { + const pathdata = { fw: {d: 'm0,0l10,5l-10,5l5,-5l-5,-5z', refx: 8, id: arrowprefix + 'fw'}, bk: {d: 'm10,0l-10,5l10,5l-5,-5l5,-5z', refx: 2, id: arrowprefix + 'bk'} }; function getLinked (elem, attr) { - var str = elem.getAttribute(attr); + const str = elem.getAttribute(attr); if (!str) { return null; } - var m = str.match(/\(#(.*)\)/); + const m = str.match(/\(#(.*)\)/); if (!m || m.length !== 2) { return null; } @@ -67,12 +66,11 @@ svgEditor.addExtension('Arrows', function (S) { function showPanel (on) { $('#arrow_panel').toggle(on); if (on) { - var el = selElems[0]; - var end = el.getAttribute('marker-end'); - var start = el.getAttribute('marker-start'); - var mid = el.getAttribute('marker-mid'); - var val; - + const el = selElems[0]; + const end = el.getAttribute('marker-end'); + const start = el.getAttribute('marker-start'); + const mid = el.getAttribute('marker-mid'); + let val; if (end && start) { val = 'both'; } else if (end) { @@ -81,7 +79,7 @@ svgEditor.addExtension('Arrows', function (S) { val = 'start'; } else if (mid) { val = 'mid'; - if (mid.indexOf('bk') !== -1) { + if (mid.includes('bk')) { val = 'mid_bk'; } } @@ -95,7 +93,7 @@ svgEditor.addExtension('Arrows', function (S) { } function resetMarker () { - var el = selElems[0]; + const el = selElems[0]; el.removeAttribute('marker-start'); el.removeAttribute('marker-mid'); el.removeAttribute('marker-end'); @@ -105,32 +103,32 @@ svgEditor.addExtension('Arrows', function (S) { // TODO: Make marker (or use?) per arrow type, since refX can be different id = id || arrowprefix + dir; - var marker = S.getElem(id); - var data = pathdata[dir]; + const data = pathdata[dir]; if (type === 'mid') { data.refx = 5; } + let marker = S.getElem(id); if (!marker) { marker = addElem({ - 'element': 'marker', - 'attr': { - 'viewBox': '0 0 10 10', - 'id': id, - 'refY': 5, - 'markerUnits': 'strokeWidth', - 'markerWidth': 5, - 'markerHeight': 5, - 'orient': 'auto', - 'style': 'pointer-events:none' // Currently needed for Opera + element: 'marker', + attr: { + viewBox: '0 0 10 10', + id, + refY: 5, + markerUnits: 'strokeWidth', + markerWidth: 5, + markerHeight: 5, + orient: 'auto', + style: 'pointer-events:none' // Currently needed for Opera } }); - var arrow = addElem({ - 'element': 'path', - 'attr': { - 'd': data.d, - 'fill': '#000000' + const arrow = addElem({ + element: 'path', + attr: { + d: data.d, + fill: '#000000' } }); marker.appendChild(arrow); @@ -143,15 +141,15 @@ svgEditor.addExtension('Arrows', function (S) { } function setArrow () { - var type = this.value; resetMarker(); + let type = this.value; if (type === 'none') { return; } // Set marker on element - var dir = 'fw'; + let dir = 'fw'; if (type === 'mid_bk') { type = 'mid'; dir = 'bk'; @@ -170,23 +168,23 @@ svgEditor.addExtension('Arrows', function (S) { } function colorChanged (elem) { - var color = elem.getAttribute('stroke'); - var mtypes = ['start', 'mid', 'end']; - var defs = S.findDefs(); + const color = elem.getAttribute('stroke'); + const mtypes = ['start', 'mid', 'end']; + const defs = S.findDefs(); $.each(mtypes, function (i, type) { - var marker = getLinked(elem, 'marker-' + type); + const marker = getLinked(elem, 'marker-' + type); if (!marker) { return; } - var curColor = $(marker).children().attr('fill'); - var curD = $(marker).children().attr('d'); - var newMarker = null; + const curColor = $(marker).children().attr('fill'); + const curD = $(marker).children().attr('d'); if (curColor === color) { return; } - var allMarkers = $(defs).find('marker'); + const allMarkers = $(defs).find('marker'); + let newMarker = null; // Different color, check if already made allMarkers.each(function () { - var attrs = $(this).children().attr(['fill', 'd']); + const attrs = $(this).children().attr(['fill', 'd']); if (attrs.fill === color && attrs.d === curD) { // Found another marker with this color and this path newMarker = this; @@ -195,8 +193,8 @@ svgEditor.addExtension('Arrows', function (S) { if (!newMarker) { // Create a new marker with this color - var lastId = marker.id; - var dir = lastId.indexOf('_fw') !== -1 ? 'fw' : 'bk'; + const lastId = marker.id; + const dir = lastId.includes('_fw') ? 'fw' : 'bk'; newMarker = addMarker(dir, type, arrowprefix + dir + allMarkers.length); @@ -206,9 +204,9 @@ svgEditor.addExtension('Arrows', function (S) { $(elem).attr('marker-' + type, 'url(#' + newMarker.id + ')'); // Check if last marker can be removed - var remove = true; + let remove = true; $(S.svgcontent).find('line, polyline, path, polygon').each(function () { - var elem = this; + const elem = this; $.each(mtypes, function (j, mtype) { if ($(elem).attr('marker-' + mtype) === 'url(#' + marker.id + ')') { remove = false; @@ -245,25 +243,25 @@ svgEditor.addExtension('Arrows', function (S) { change: setArrow } }], - callback: function () { + callback () { $('#arrow_panel').hide(); // Set ID so it can be translated in locale file $('#arrow_list option')[0].id = 'connector_no_arrow'; }, - addLangData: function (lang) { + addLangData (lang) { return { data: langList[lang] }; }, - selectedChanged: function (opts) { + selectedChanged (opts) { // Use this to update the current selected elements selElems = opts.elems; - var i = selElems.length; - var markerElems = ['line', 'path', 'polyline', 'polygon']; + const markerElems = ['line', 'path', 'polyline', 'polygon']; + let i = selElems.length; while (i--) { - var elem = selElems[i]; - if (elem && $.inArray(elem.tagName, markerElems) !== -1) { + const elem = selElems[i]; + if (elem && markerElems.includes(elem.tagName)) { if (opts.selectedElement && !opts.multiselected) { showPanel(true); } else { @@ -274,16 +272,16 @@ svgEditor.addExtension('Arrows', function (S) { } } }, - elementChanged: function (opts) { - var elem = opts.elems[0]; + elementChanged (opts) { + const elem = opts.elems[0]; if (elem && ( elem.getAttribute('marker-start') || elem.getAttribute('marker-mid') || elem.getAttribute('marker-end') )) { - // var start = elem.getAttribute('marker-start'); - // var mid = elem.getAttribute('marker-mid'); - // var end = elem.getAttribute('marker-end'); + // const start = elem.getAttribute('marker-start'); + // const mid = elem.getAttribute('marker-mid'); + // const end = elem.getAttribute('marker-end'); // Has marker, so see if it should match color colorChanged(elem); } diff --git a/editor/extensions/ext-closepath.js b/editor/extensions/ext-closepath.js index c1a5dd7d..bdaf9775 100644 --- a/editor/extensions/ext-closepath.js +++ b/editor/extensions/ext-closepath.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, $ */ +/* globals jQuery, svgEditor */ /* * ext-closepath.js * @@ -8,41 +7,42 @@ * Copyright(c) 2010 Jeff Schiller * */ +// import './pathseg.js'; // This extension adds a simple button to the contextual panel for paths // The button toggles whether the path is open or closed svgEditor.addExtension('ClosePath', function () { - 'use strict'; - var selElems, - updateButton = function (path) { - var seglist = path.pathSegList, - closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1, - showbutton = closed ? '#tool_openpath' : '#tool_closepath', - hidebutton = closed ? '#tool_closepath' : '#tool_openpath'; - $(hidebutton).hide(); - $(showbutton).show(); - }, - showPanel = function (on) { - $('#closepath_panel').toggle(on); - if (on) { - var path = selElems[0]; - if (path) { updateButton(path); } + const $ = jQuery; + let selElems; + const updateButton = function (path) { + const seglist = path.pathSegList, + closed = seglist.getItem(seglist.numberOfItems - 1).pathSegType === 1, + showbutton = closed ? '#tool_openpath' : '#tool_closepath', + hidebutton = closed ? '#tool_closepath' : '#tool_openpath'; + $(hidebutton).hide(); + $(showbutton).show(); + }; + const showPanel = function (on) { + $('#closepath_panel').toggle(on); + if (on) { + const path = selElems[0]; + if (path) { updateButton(path); } + } + }; + const toggleClosed = function () { + const path = selElems[0]; + if (path) { + const seglist = path.pathSegList, + last = seglist.numberOfItems - 1; + // is closed + if (seglist.getItem(last).pathSegType === 1) { + seglist.removeItem(last); + } else { + seglist.appendItem(path.createSVGPathSegClosePath()); } - }, - toggleClosed = function () { - var path = selElems[0]; - if (path) { - var seglist = path.pathSegList, - last = seglist.numberOfItems - 1; - // is closed - if (seglist.getItem(last).pathSegType === 1) { - seglist.removeItem(last); - } else { - seglist.appendItem(path.createSVGPathSegClosePath()); - } - updateButton(path); - } - }; + updateButton(path); + } + }; return { name: 'ClosePath', @@ -53,7 +53,7 @@ svgEditor.addExtension('ClosePath', function () { panel: 'closepath_panel', title: 'Open path', events: { - click: function () { + click () { toggleClosed(); } } @@ -64,19 +64,19 @@ svgEditor.addExtension('ClosePath', function () { panel: 'closepath_panel', title: 'Close path', events: { - click: function () { + click () { toggleClosed(); } } }], - callback: function () { + callback () { $('#closepath_panel').hide(); }, - selectedChanged: function (opts) { + selectedChanged (opts) { selElems = opts.elems; - var i = selElems.length; + let i = selElems.length; while (i--) { - var elem = selElems[i]; + const elem = selElems[i]; if (elem && elem.tagName === 'path') { if (opts.selectedElement && !opts.multiselected) { showPanel(true); diff --git a/editor/extensions/ext-connector.js b/editor/extensions/ext-connector.js index 797fd6e6..7e35248e 100644 --- a/editor/extensions/ext-connector.js +++ b/editor/extensions/ext-connector.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, svgCanvas, $ */ +/* globals jQuery, svgEditor, svgCanvas */ /* * ext-connector.js * @@ -10,27 +9,26 @@ */ svgEditor.addExtension('Connector', function (S) { - var svgcontent = S.svgcontent, - svgroot = S.svgroot, - getNextId = S.getNextId, - getElem = S.getElem, + const $ = jQuery; + const {svgroot, getNextId, getElem, curConfig} = S, addElem = S.addSvgElementFromJson, selManager = S.selectorManager, - curConfig = svgEditor.curConfig, - started = false, - startX, + connSel = '.se_connector', + // connect_str = '-SE_CONNECT-', + elData = $.data; + + let startX, startY, curLine, startElem, endElem, - connections = [], - connSel = '.se_connector', seNs, - // connect_str = '-SE_CONNECT-', - selElems = [], - elData = $.data; + {svgcontent} = S, + started = false, + connections = [], + selElems = []; - var langList = { + const langList = { 'en': [ {'id': 'mode_connect', 'title': 'Connect two objects'} ], @@ -49,15 +47,14 @@ svgEditor.addExtension('Connector', function (S) { bb.y -= offset / 2; } - var midX = bb.x + bb.width / 2; - var midY = bb.y + bb.height / 2; - var lenX = x - midX; - var lenY = y - midY; + const midX = bb.x + bb.width / 2; + const midY = bb.y + bb.height / 2; + const lenX = x - midX; + const lenY = y - midY; - var slope = Math.abs(lenY / lenX); - - var ratio; + const slope = Math.abs(lenY / lenX); + let ratio; if (slope < bb.height / bb.width) { ratio = (bb.width / 2) / Math.abs(lenX); } else { @@ -71,16 +68,16 @@ svgEditor.addExtension('Connector', function (S) { } function getOffset (side, line) { - var giveOffset = !!line.getAttribute('marker-' + side); - // var giveOffset = $(line).data(side+'_off'); + const giveOffset = !!line.getAttribute('marker-' + side); + // const giveOffset = $(line).data(side+'_off'); // TODO: Make this number (5) be based on marker width/height - var size = line.getAttribute('stroke-width') * 5; + const size = line.getAttribute('stroke-width') * 5; return giveOffset ? size : 0; } function showPanel (on) { - var connRules = $('#connector_rules'); + let connRules = $('#connector_rules'); if (!connRules.length) { connRules = $('').appendTo('head'); } @@ -89,8 +86,8 @@ svgEditor.addExtension('Connector', function (S) { } function setPoint (elem, pos, x, y, setMid) { - var i, pts = elem.points; - var pt = svgroot.createSVGPoint(); + const pts = elem.points; + const pt = svgroot.createSVGPoint(); pt.x = x; pt.y = y; if (pos === 'end') { pos = pts.numberOfItems - 1; } @@ -99,8 +96,8 @@ svgEditor.addExtension('Connector', function (S) { pts.replaceItem(pt, pos); } catch (err) { // Should only occur in FF which formats points attr as "n,n n,n", so just split - var ptArr = elem.getAttribute('points').split(' '); - for (i = 0; i < ptArr.length; i++) { + const ptArr = elem.getAttribute('points').split(' '); + for (let i = 0; i < ptArr.length; i++) { if (i === pos) { ptArr[i] = x + ',' + y; } @@ -110,67 +107,69 @@ svgEditor.addExtension('Connector', function (S) { if (setMid) { // Add center point - var ptStart = pts.getItem(0); - var ptEnd = pts.getItem(pts.numberOfItems - 1); + const ptStart = pts.getItem(0); + const ptEnd = pts.getItem(pts.numberOfItems - 1); setPoint(elem, 1, (ptEnd.x + ptStart.x) / 2, (ptEnd.y + ptStart.y) / 2); } } function updateLine (diffX, diffY) { // Update line with element - var i = connections.length; + let i = connections.length; while (i--) { - var conn = connections[i]; - var line = conn.connector; - // var elem = conn.elem; + const conn = connections[i]; + const line = conn.connector; + // const {elem} = conn; - var pre = conn.is_start ? 'start' : 'end'; - // var sw = line.getAttribute('stroke-width') * 5; + const pre = conn.is_start ? 'start' : 'end'; + // const sw = line.getAttribute('stroke-width') * 5; // Update bbox for this element - var bb = elData(line, pre + '_bb'); + const bb = elData(line, pre + '_bb'); bb.x = conn.start_x + diffX; bb.y = conn.start_y + diffY; elData(line, pre + '_bb', bb); - var altPre = conn.is_start ? 'end' : 'start'; + const altPre = conn.is_start ? 'end' : 'start'; // Get center pt of connected element - var bb2 = elData(line, altPre + '_bb'); - var srcX = bb2.x + bb2.width / 2; - var srcY = bb2.y + bb2.height / 2; + const bb2 = elData(line, altPre + '_bb'); + const srcX = bb2.x + bb2.width / 2; + const srcY = bb2.y + bb2.height / 2; // Set point of element being moved - var pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 + const pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); // $(line).data(pre+'_off')?sw:0 setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); // Set point of connected element - var pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); } } - function findConnectors (elems) { - var i; - if (!elems) { elems = selElems; } - var connectors = $(svgcontent).find(connSel); + /** + * + * @param {array} [elem=selElems] Array of elements + */ + function findConnectors (elems = selElems) { + const connectors = $(svgcontent).find(connSel); connections = []; // Loop through connectors to see if one is connected to the element connectors.each(function () { - var addThis; + let addThis; function add () { - if ($.inArray(this, elems) !== -1) { + if (elems.includes(this)) { // Pretend this element is selected addThis = true; } } // Grab the ends - var parts = []; + const parts = []; ['start', 'end'].forEach(function (pos, i) { - var key = 'c_' + pos; - var part = elData(this, key); + const key = 'c_' + pos; + let part = elData(this, key); if (part == null) { part = document.getElementById( this.attributes['se:connector'].value.split(' ')[i] @@ -181,8 +180,8 @@ svgEditor.addExtension('Connector', function (S) { parts.push(part); }.bind(this)); - for (i = 0; i < 2; i++) { - var cElem = parts[i]; + for (let i = 0; i < 2; i++) { + const cElem = parts[i]; addThis = false; // The connected element might be part of a selected group @@ -192,8 +191,8 @@ svgEditor.addExtension('Connector', function (S) { $(this).remove(); continue; } - if ($.inArray(cElem, elems) !== -1 || addThis) { - var bb = svgCanvas.getStrokedBBox([cElem]); + if (elems.includes(cElem) || addThis) { + const bb = svgCanvas.getStrokedBBox([cElem]); connections.push({ elem: cElem, connector: this, @@ -210,47 +209,46 @@ svgEditor.addExtension('Connector', function (S) { // Updates connector lines based on selected elements // Is not used on mousemove, as it runs getStrokedBBox every time, // which isn't necessary there. - var i, j; findConnectors(elems); if (connections.length) { // Update line with element - i = connections.length; + let i = connections.length; while (i--) { - var conn = connections[i]; - var line = conn.connector; - var elem = conn.elem; + const conn = connections[i]; + const line = conn.connector; + const {elem} = conn; - // var sw = line.getAttribute('stroke-width') * 5; - var pre = conn.is_start ? 'start' : 'end'; + // const sw = line.getAttribute('stroke-width') * 5; + const pre = conn.is_start ? 'start' : 'end'; // Update bbox for this element - var bb = svgCanvas.getStrokedBBox([elem]); + const bb = svgCanvas.getStrokedBBox([elem]); bb.x = conn.start_x; bb.y = conn.start_y; elData(line, pre + '_bb', bb); - /* var addOffset = */ elData(line, pre + '_off'); + /* const addOffset = */ elData(line, pre + '_off'); - var altPre = conn.is_start ? 'end' : 'start'; + const altPre = conn.is_start ? 'end' : 'start'; // Get center pt of connected element - var bb2 = elData(line, altPre + '_bb'); - var srcX = bb2.x + bb2.width / 2; - var srcY = bb2.y + bb2.height / 2; + const bb2 = elData(line, altPre + '_bb'); + const srcX = bb2.x + bb2.width / 2; + const srcY = bb2.y + bb2.height / 2; // Set point of element being moved - var pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); + let pt = getBBintersect(srcX, srcY, bb, getOffset(pre, line)); setPoint(line, conn.is_start ? 0 : 'end', pt.x, pt.y, true); // Set point of connected element - var pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); + const pt2 = getBBintersect(pt.x, pt.y, elData(line, altPre + '_bb'), getOffset(altPre, line)); setPoint(line, conn.is_start ? 'end' : 0, pt2.x, pt2.y, true); // Update points attribute manually for webkit - if (navigator.userAgent.indexOf('AppleWebKit') !== -1) { - var pts = line.points; - var len = pts.numberOfItems; - var ptArr = []; - for (j = 0; j < len; j++) { + if (navigator.userAgent.includes('AppleWebKit')) { + const pts = line.points; + const len = pts.numberOfItems; + const ptArr = []; + for (let j = 0; j < len; j++) { pt = pts.getItem(j); ptArr[j] = pt.x + ',' + pt.y; } @@ -262,17 +260,17 @@ svgEditor.addExtension('Connector', function (S) { // Do once (function () { - var gse = svgCanvas.groupSelectedElements; + const gse = svgCanvas.groupSelectedElements; svgCanvas.groupSelectedElements = function () { svgCanvas.removeFromSelection($(connSel).toArray()); return gse.apply(this, arguments); }; - var mse = svgCanvas.moveSelectedElements; + const mse = svgCanvas.moveSelectedElements; svgCanvas.moveSelectedElements = function () { - var cmd = mse.apply(this, arguments); + const cmd = mse.apply(this, arguments); updateConnectors(); return cmd; }; @@ -284,12 +282,12 @@ svgEditor.addExtension('Connector', function (S) { function init () { // Make sure all connectors have data set $(svgcontent).find('*').each(function () { - var conn = this.getAttributeNS(seNs, 'connector'); + const conn = this.getAttributeNS(seNs, 'connector'); if (conn) { this.setAttribute('class', connSel.substr(1)); - var connData = conn.split(' '); - var sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]); - var ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]); + const connData = conn.split(' '); + const sbb = svgCanvas.getStrokedBBox([getElem(connData[0])]); + const ebb = svgCanvas.getStrokedBBox([getElem(connData[1])]); $(this).data('c_start', connData[0]) .data('c_end', connData[1]) .data('start_bb', sbb) @@ -325,40 +323,40 @@ svgEditor.addExtension('Connector', function (S) { position: 1 }, events: { - 'click': function () { + click () { svgCanvas.setMode('connector'); } } }], - addLangData: function (lang) { + addLangData (lang) { return { data: langList[lang] }; }, - mouseDown: function (opts) { - var e = opts.event; + mouseDown (opts) { + const e = opts.event; startX = opts.start_x; startY = opts.start_y; - var mode = svgCanvas.getMode(); + const mode = svgCanvas.getMode(); if (mode === 'connector') { if (started) { return; } - var mouseTarget = e.target; + const mouseTarget = e.target; - var parents = $(mouseTarget).parents(); + const parents = $(mouseTarget).parents(); if ($.inArray(svgcontent, parents) !== -1) { // Connectable element // If child of foreignObject, use parent - var fo = $(mouseTarget).closest('foreignObject'); + const fo = $(mouseTarget).closest('foreignObject'); startElem = fo.length ? fo[0] : mouseTarget; // Get center of source element - var bb = svgCanvas.getStrokedBBox([startElem]); - var x = bb.x + bb.width / 2; - var y = bb.y + bb.height / 2; + const bb = svgCanvas.getStrokedBBox([startElem]); + const x = bb.x + bb.width / 2; + const y = bb.y + bb.height / 2; started = true; curLine = addElem({ @@ -383,21 +381,21 @@ svgEditor.addExtension('Connector', function (S) { findConnectors(); } }, - mouseMove: function (opts) { - var zoom = svgCanvas.getZoom(); - // var e = opts.event; - var x = opts.mouse_x / zoom; - var y = opts.mouse_y / zoom; + mouseMove (opts) { + const zoom = svgCanvas.getZoom(); + // const e = opts.event; + const x = opts.mouse_x / zoom; + const y = opts.mouse_y / zoom; - var diffX = x - startX, + const diffX = x - startX, diffY = y - startY; - var mode = svgCanvas.getMode(); + const mode = svgCanvas.getMode(); if (mode === 'connector' && started) { - // var sw = curLine.getAttribute('stroke-width') * 3; + // const sw = curLine.getAttribute('stroke-width') * 3; // Set start point (adjusts based on bb) - var pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine)); + const pt = getBBintersect(x, y, elData(curLine, 'start_bb'), getOffset('start', curLine)); startX = pt.x; startY = pt.y; @@ -406,10 +404,9 @@ svgEditor.addExtension('Connector', function (S) { // Set end point setPoint(curLine, 'end', x, y, true); } else if (mode === 'select') { - var slen = selElems.length; - + let slen = selElems.length; while (slen--) { - var elem = selElems[slen]; + const elem = selElems[slen]; // Look for selected connector elements if (elem && elData(elem, 'c_start')) { // Remove the "translate" transform given to move @@ -422,18 +419,18 @@ svgEditor.addExtension('Connector', function (S) { } } }, - mouseUp: function (opts) { - // var zoom = svgCanvas.getZoom(); - var e = opts.event, - // x = opts.mouse_x / zoom, - // y = opts.mouse_y / zoom, - mouseTarget = e.target; + mouseUp (opts) { + // const zoom = svgCanvas.getZoom(); + const e = opts.event; + // , x = opts.mouse_x / zoom, + // , y = opts.mouse_y / zoom, + let mouseTarget = e.target; if (svgCanvas.getMode() === 'connector') { - var fo = $(mouseTarget).closest('foreignObject'); + const fo = $(mouseTarget).closest('foreignObject'); if (fo.length) { mouseTarget = fo[0]; } - var parents = $(mouseTarget).parents(); + const parents = $(mouseTarget).parents(); if (mouseTarget === startElem) { // Start line through click @@ -441,7 +438,7 @@ svgEditor.addExtension('Connector', function (S) { return { keep: true, element: null, - started: started + started }; } if ($.inArray(svgcontent, parents) === -1) { @@ -451,18 +448,18 @@ svgEditor.addExtension('Connector', function (S) { return { keep: false, element: null, - started: started + started }; } // Valid end element endElem = mouseTarget; - var startId = startElem.id, endId = endElem.id; - var connStr = startId + ' ' + endId; - var altStr = endId + ' ' + startId; + const startId = startElem.id, endId = endElem.id; + const connStr = startId + ' ' + endId; + const altStr = endId + ' ' + startId; // Don't create connector if one already exists - var dupe = $(svgcontent).find(connSel).filter(function () { - var conn = this.getAttributeNS(seNs, 'connector'); + const dupe = $(svgcontent).find(connSel).filter(function () { + const conn = this.getAttributeNS(seNs, 'connector'); if (conn === connStr || conn === altStr) { return true; } }); if (dupe.length) { @@ -474,9 +471,9 @@ svgEditor.addExtension('Connector', function (S) { }; } - var bb = svgCanvas.getStrokedBBox([endElem]); + const bb = svgCanvas.getStrokedBBox([endElem]); - var pt = getBBintersect(startX, startY, bb, getOffset('start', curLine)); + const pt = getBBintersect(startX, startY, bb, getOffset('start', curLine)); setPoint(curLine, 'end', pt.x, pt.y, true); $(curLine) .data('c_start', startId) @@ -493,11 +490,11 @@ svgEditor.addExtension('Connector', function (S) { return { keep: true, element: curLine, - started: started + started }; } }, - selectedChanged: function (opts) { + selectedChanged (opts) { // TODO: Find better way to skip operations if no connectors are in use if (!$(svgcontent).find(connSel).length) { return; } @@ -508,10 +505,9 @@ svgEditor.addExtension('Connector', function (S) { // Use this to update the current selected elements selElems = opts.elems; - var i = selElems.length; - + let i = selElems.length; while (i--) { - var elem = selElems[i]; + const elem = selElems[i]; if (elem && elData(elem, 'c_start')) { selManager.requestSelector(elem).showGrips(false); if (opts.selectedElement && !opts.multiselected) { @@ -526,8 +522,8 @@ svgEditor.addExtension('Connector', function (S) { } updateConnectors(); }, - elementChanged: function (opts) { - var elem = opts.elems[0]; + elementChanged (opts) { + let elem = opts.elems[0]; if (elem && elem.tagName === 'svg' && elem.id === 'svgcontent') { // Update svgcontent (can change on import) svgcontent = elem; @@ -535,15 +531,14 @@ svgEditor.addExtension('Connector', function (S) { } // Has marker, so change offset - var start; if (elem && ( elem.getAttribute('marker-start') || elem.getAttribute('marker-mid') || elem.getAttribute('marker-end') )) { - start = elem.getAttribute('marker-start'); - var mid = elem.getAttribute('marker-mid'); - var end = elem.getAttribute('marker-end'); + const start = elem.getAttribute('marker-start'); + const mid = elem.getAttribute('marker-mid'); + const end = elem.getAttribute('marker-end'); curLine = elem; $(elem) .data('start_off', !!start) @@ -552,14 +547,14 @@ svgEditor.addExtension('Connector', function (S) { if (elem.tagName === 'line' && mid) { // Convert to polyline to accept mid-arrow - var x1 = Number(elem.getAttribute('x1')); - var x2 = Number(elem.getAttribute('x2')); - var y1 = Number(elem.getAttribute('y1')); - var y2 = Number(elem.getAttribute('y2')); - var id = elem.id; + const x1 = Number(elem.getAttribute('x1')); + const x2 = Number(elem.getAttribute('x2')); + const y1 = Number(elem.getAttribute('y1')); + const y2 = Number(elem.getAttribute('y2')); + const {id} = elem; - var midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' '); - var pline = addElem({ + const midPt = (' ' + ((x1 + x2) / 2) + ',' + ((y1 + y2) / 2) + ' '); + const pline = addElem({ 'element': 'polyline', 'attr': { 'points': (x1 + ',' + y1 + midPt + x2 + ',' + y2), @@ -579,14 +574,14 @@ svgEditor.addExtension('Connector', function (S) { } // Update line if it's a connector if (elem.getAttribute('class') === connSel.substr(1)) { - start = getElem(elData(elem, 'c_start')); + const start = getElem(elData(elem, 'c_start')); updateConnectors([start]); } else { updateConnectors(); } }, - IDsUpdated: function (input) { - var remove = []; + IDsUpdated (input) { + const remove = []; input.elems.forEach(function (elem) { if ('se:connector' in elem.attr) { elem.attr['se:connector'] = elem.attr['se:connector'].split(' ') @@ -599,9 +594,9 @@ svgEditor.addExtension('Connector', function (S) { } } }); - return {remove: remove}; + return {remove}; }, - toolButtonStateUpdate: function (opts) { + toolButtonStateUpdate (opts) { if (opts.nostroke) { if ($('#mode_connect').hasClass('tool_button_current')) { svgEditor.clickSelect(); diff --git a/editor/extensions/ext-eyedropper.js b/editor/extensions/ext-eyedropper.js index 57704f21..eac235dd 100644 --- a/editor/extensions/ext-eyedropper.js +++ b/editor/extensions/ext-eyedropper.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, svgedit, $ */ +/* globals jQuery, svgEditor, svgedit */ /* * ext-eyedropper.js * @@ -16,12 +15,11 @@ // 4) svgcanvas.js svgEditor.addExtension('eyedropper', function (S) { - 'use strict'; - var // NS = svgedit.NS, - // svgcontent = S.svgcontent, + const $ = jQuery; + const // {svgcontent} = S, // svgdoc = S.svgroot.parentNode.ownerDocument, svgCanvas = svgEditor.canvas, - ChangeElementCommand = svgedit.history.ChangeElementCommand, + {ChangeElementCommand} = svgedit.history, addToHistory = function (cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); }, currentStyle = { fillPaint: 'red', fillOpacity: 1.0, @@ -34,14 +32,14 @@ svgEditor.addExtension('eyedropper', function (S) { function getStyle (opts) { // if we are in eyedropper mode, we don't want to disable the eye-dropper tool - var mode = svgCanvas.getMode(); + const mode = svgCanvas.getMode(); if (mode === 'eyedropper') { return; } - var elem = null; - var tool = $('#tool_eyedropper'); + const tool = $('#tool_eyedropper'); // enable-eye-dropper if one element is selected + let elem = null; if (!opts.multiselected && opts.elems[0] && - $.inArray(opts.elems[0].nodeName, ['svg', 'g', 'use']) === -1 + !['svg', 'g', 'use'].includes(opts.elems[0].nodeName) ) { elem = opts.elems[0]; tool.removeClass('disabled'); @@ -70,7 +68,7 @@ svgEditor.addExtension('eyedropper', function (S) { title: 'Eye Dropper Tool', key: 'I', events: { - click: function () { + click () { svgCanvas.setMode('eyedropper'); } } @@ -80,15 +78,15 @@ svgEditor.addExtension('eyedropper', function (S) { selectedChanged: getStyle, elementChanged: getStyle, - mouseDown: function (opts) { - var mode = svgCanvas.getMode(); + mouseDown (opts) { + const mode = svgCanvas.getMode(); if (mode === 'eyedropper') { - var e = opts.event; - var target = e.target; - if ($.inArray(target.nodeName, ['svg', 'g', 'use']) === -1) { - var changes = {}; + const e = opts.event; + const {target} = e; + if (!['svg', 'g', 'use'].includes(target.nodeName)) { + const changes = {}; - var change = function (elem, attrname, newvalue) { + const change = function (elem, attrname, newvalue) { changes[attrname] = elem.getAttribute(attrname); elem.setAttribute(attrname, newvalue); }; diff --git a/editor/extensions/ext-foreignobject.js b/editor/extensions/ext-foreignobject.js index af87e8fd..730dbe41 100644 --- a/editor/extensions/ext-foreignobject.js +++ b/editor/extensions/ext-foreignobject.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, svgedit, svgCanvas, $ */ +/* globals jQuery, svgEditor, svgedit, svgCanvas */ /* * ext-foreignobject.js * @@ -10,25 +9,23 @@ * */ -svgEditor.addExtension('foreignObject', function (S) { - var NS = svgedit.NS, - Utils = svgedit.utilities, - // svgcontent = S.svgcontent, - // addElem = S.addSvgElementFromJson, - selElems, - editingforeign = false, - svgdoc = S.svgroot.parentNode.ownerDocument, - started, - newFO; +import {NS} from './svgedit.js'; - var properlySourceSizeTextArea = function () { +svgEditor.addExtension('foreignObject', function (S) { + const $ = jQuery; + const Utils = svgedit.utilities, + // {svgcontent} = S, + // addElem = S.addSvgElementFromJson, + svgdoc = S.svgroot.parentNode.ownerDocument; + + const properlySourceSizeTextArea = function () { // TODO: remove magic numbers here and get values from CSS - var height = $('#svg_source_container').height() - 80; + const height = $('#svg_source_container').height() - 80; $('#svg_source_textarea').css('height', height); }; function showPanel (on) { - var fcRules = $('#fc_rules'); + let fcRules = $('#fc_rules'); if (!fcRules.length) { fcRules = $('').appendTo('head'); } @@ -41,6 +38,11 @@ svgEditor.addExtension('foreignObject', function (S) { $('#foreign_save, #foreign_cancel').toggle(on); } + let selElems, + started, + newFO, + editingforeign = false; + // Function: setForeignString(xmlString, elt) // This function sets the content of element elt to the input XML. // @@ -51,10 +53,10 @@ svgEditor.addExtension('foreignObject', function (S) { // Returns: // This function returns false if the set was unsuccessful, true otherwise. function setForeignString (xmlString) { - var elt = selElems[0]; + const elt = selElems[0]; try { // convert string into XML document - var newDoc = Utils.text2xml('' + xmlString + ''); + const newDoc = Utils.text2xml('' + xmlString + ''); // run it through our sanitizer to remove anything we do not support S.sanitizeSvg(newDoc.documentElement); elt.parentNode.replaceChild(svgdoc.importNode(newDoc.documentElement.firstChild, true), elt); @@ -69,13 +71,13 @@ svgEditor.addExtension('foreignObject', function (S) { } function showForeignEditor () { - var elt = selElems[0]; + const elt = selElems[0]; if (!elt || editingforeign) { return; } editingforeign = true; toggleSourceButtons(true); elt.removeAttribute('fill'); - var str = S.svgToString(elt, 0); + const str = S.svgToString(elt, 0); $('#svg_source_textarea').val(str); $('#svg_source_editor').fadeIn(); properlySourceSizeTextArea(); @@ -95,7 +97,7 @@ svgEditor.addExtension('foreignObject', function (S) { type: 'mode', title: 'Foreign Object Tool', events: { - 'click': function () { + click () { svgCanvas.setMode('foreign'); } } @@ -105,7 +107,7 @@ svgEditor.addExtension('foreignObject', function (S) { panel: 'foreignObject_panel', title: 'Edit ForeignObject Content', events: { - 'click': function () { + click () { showForeignEditor(); } } @@ -119,7 +121,7 @@ svgEditor.addExtension('foreignObject', function (S) { label: 'w', size: 3, events: { - change: function () { + change () { setAttr('width', this.value); } } @@ -130,7 +132,7 @@ svgEditor.addExtension('foreignObject', function (S) { id: 'foreign_height', label: 'h', events: { - change: function () { + change () { setAttr('height', this.value); } } @@ -143,17 +145,17 @@ svgEditor.addExtension('foreignObject', function (S) { size: 2, defval: 16, events: { - change: function () { + change () { setAttr('font-size', this.value); } } } ], - callback: function () { + callback () { $('#foreignObject_panel').hide(); - var endChanges = function () { + const endChanges = function () { $('#svg_source_editor').hide(); editingforeign = false; $('#svg_source_textarea').blur(); @@ -163,7 +165,7 @@ svgEditor.addExtension('foreignObject', function (S) { // TODO: Needs to be done after orig icon loads setTimeout(function () { // Create source save/cancel buttons - /* var save = */ $('#tool_source_save').clone() + /* const save = */ $('#tool_source_save').clone() .hide().attr('id', 'foreign_save').unbind() .appendTo('#tool_source_back').click(function () { if (!editingforeign) { return; } @@ -179,15 +181,15 @@ svgEditor.addExtension('foreignObject', function (S) { // setSelectMode(); }); - /* var cancel = */ $('#tool_source_cancel').clone() + /* const cancel = */ $('#tool_source_cancel').clone() .hide().attr('id', 'foreign_cancel').unbind() .appendTo('#tool_source_back').click(function () { endChanges(); }); }, 3000); }, - mouseDown: function (opts) { - // var e = opts.event; + mouseDown (opts) { + // const e = opts.event; if (svgCanvas.getMode() === 'foreign') { started = true; @@ -203,15 +205,15 @@ svgEditor.addExtension('foreignObject', function (S) { 'style': 'pointer-events:inherit' } }); - var m = svgdoc.createElementNS(NS.MATH, 'math'); + const m = svgdoc.createElementNS(NS.MATH, 'math'); m.setAttributeNS(NS.XMLNS, 'xmlns', NS.MATH); m.setAttribute('display', 'inline'); - var mi = svgdoc.createElementNS(NS.MATH, 'mi'); + const mi = svgdoc.createElementNS(NS.MATH, 'mi'); mi.setAttribute('mathvariant', 'normal'); mi.textContent = '\u03A6'; - var mo = svgdoc.createElementNS(NS.MATH, 'mo'); + const mo = svgdoc.createElementNS(NS.MATH, 'mo'); mo.textContent = '\u222A'; - var mi2 = svgdoc.createElementNS(NS.MATH, 'mi'); + const mi2 = svgdoc.createElementNS(NS.MATH, 'mi'); mi2.textContent = '\u2133'; m.appendChild(mi); m.appendChild(mo); @@ -222,27 +224,26 @@ svgEditor.addExtension('foreignObject', function (S) { }; } }, - mouseUp: function (opts) { - // var e = opts.event; + mouseUp (opts) { + // const e = opts.event; if (svgCanvas.getMode() === 'foreign' && started) { - var attrs = $(newFO).attr(['width', 'height']); - var keep = (attrs.width !== '0' || attrs.height !== '0'); + const attrs = $(newFO).attr(['width', 'height']); + const keep = (attrs.width !== '0' || attrs.height !== '0'); svgCanvas.addToSelection([newFO], true); return { - keep: keep, + keep, element: newFO }; } }, - selectedChanged: function (opts) { + selectedChanged (opts) { // Use this to update the current selected elements selElems = opts.elems; - var i = selElems.length; - + let i = selElems.length; while (i--) { - var elem = selElems[i]; + const elem = selElems[i]; if (elem && elem.tagName === 'foreignObject') { if (opts.selectedElement && !opts.multiselected) { $('#foreign_font_size').val(elem.getAttribute('font-size')); @@ -257,8 +258,8 @@ svgEditor.addExtension('foreignObject', function (S) { } } }, - elementChanged: function (opts) { - // var elem = opts.elems[0]; + elementChanged (opts) { + // const elem = opts.elems[0]; } }; }); diff --git a/editor/extensions/ext-grid.js b/editor/extensions/ext-grid.js index d352371d..20210471 100644 --- a/editor/extensions/ext-grid.js +++ b/editor/extensions/ext-grid.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, svgedit, svgCanvas, $ */ +/* globals jQuery, svgEditor, svgCanvas */ /* * ext-grid.js * @@ -14,97 +13,97 @@ // 1) units.js // 2) everything else -svgEditor.addExtension('view_grid', function () { - 'use strict'; +import {NS} from './svgedit.js'; +import {getTypeMap} from './units.js'; - var NS = svgedit.NS, - svgdoc = document.getElementById('svgcanvas').ownerDocument, - showGrid = svgEditor.curConfig.showGrid || false, - assignAttributes = svgCanvas.assignAttributes, +svgEditor.addExtension('view_grid', function () { + const $ = jQuery; + const svgdoc = document.getElementById('svgcanvas').ownerDocument, + {assignAttributes} = svgCanvas, hcanvas = document.createElement('canvas'), canvBG = $('#canvasBackground'), - units = svgedit.units.getTypeMap(), + units = getTypeMap(), intervals = [0.01, 0.1, 1, 10, 100, 1000]; + let showGrid = svgEditor.curConfig.showGrid || false; $(hcanvas).hide().appendTo('body'); - var canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg'); + const canvasGrid = svgdoc.createElementNS(NS.SVG, 'svg'); assignAttributes(canvasGrid, { - 'id': 'canvasGrid', - 'width': '100%', - 'height': '100%', - 'x': 0, - 'y': 0, - 'overflow': 'visible', - 'display': 'none' + id: 'canvasGrid', + width: '100%', + height: '100%', + x: 0, + y: 0, + overflow: 'visible', + display: 'none' }); canvBG.append(canvasGrid); // grid-pattern - var gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern'); + const gridPattern = svgdoc.createElementNS(NS.SVG, 'pattern'); assignAttributes(gridPattern, { - 'id': 'gridpattern', - 'patternUnits': 'userSpaceOnUse', - 'x': 0, // -(value.strokeWidth / 2), // position for strokewidth - 'y': 0, // -(value.strokeWidth / 2), // position for strokewidth - 'width': 100, - 'height': 100 + id: 'gridpattern', + patternUnits: 'userSpaceOnUse', + x: 0, // -(value.strokeWidth / 2), // position for strokewidth + y: 0, // -(value.strokeWidth / 2), // position for strokewidth + width: 100, + height: 100 }); - var gridimg = svgdoc.createElementNS(NS.SVG, 'image'); + const gridimg = svgdoc.createElementNS(NS.SVG, 'image'); assignAttributes(gridimg, { - 'x': 0, - 'y': 0, - 'width': 100, - 'height': 100 + x: 0, + y: 0, + width: 100, + height: 100 }); gridPattern.appendChild(gridimg); $('#svgroot defs').append(gridPattern); // grid-box - var gridBox = svgdoc.createElementNS(NS.SVG, 'rect'); + const gridBox = svgdoc.createElementNS(NS.SVG, 'rect'); assignAttributes(gridBox, { - 'width': '100%', - 'height': '100%', - 'x': 0, - 'y': 0, + width: '100%', + height: '100%', + x: 0, + y: 0, 'stroke-width': 0, - 'stroke': 'none', - 'fill': 'url(#gridpattern)', - 'style': 'pointer-events: none; display:visible;' + stroke: 'none', + fill: 'url(#gridpattern)', + style: 'pointer-events: none; display:visible;' }); $('#canvasGrid').append(gridBox); function updateGrid (zoom) { - var i; // TODO: Try this with elements, then compare performance difference - var unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px - var uMulti = unit * zoom; + const unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px + const uMulti = unit * zoom; // Calculate the main number interval - var rawM = 100 / uMulti; - var multi = 1; - for (i = 0; i < intervals.length; i++) { - var num = intervals[i]; + const rawM = 100 / uMulti; + let multi = 1; + for (let i = 0; i < intervals.length; i++) { + const num = intervals[i]; multi = num; if (rawM <= num) { break; } } - var bigInt = multi * uMulti; + const bigInt = multi * uMulti; // Set the canvas size to the width of the container hcanvas.width = bigInt; hcanvas.height = bigInt; - var ctx = hcanvas.getContext('2d'); - var curD = 0.5; - var part = bigInt / 10; + const ctx = hcanvas.getContext('2d'); + const curD = 0.5; + const part = bigInt / 10; ctx.globalAlpha = 0.2; ctx.strokeStyle = svgEditor.curConfig.gridColor; - for (i = 1; i < 10; i++) { - var subD = Math.round(part * i) + 0.5; - // var lineNum = (i % 2)?12:10; - var lineNum = 0; + for (let i = 1; i < 10; i++) { + const subD = Math.round(part * i) + 0.5; + // const lineNum = (i % 2)?12:10; + const lineNum = 0; ctx.moveTo(subD, bigInt); ctx.lineTo(subD, lineNum); ctx.moveTo(bigInt, subD); @@ -120,7 +119,7 @@ svgEditor.addExtension('view_grid', function () { ctx.lineTo(0, curD); ctx.stroke(); - var datauri = hcanvas.toDataURL('image/png'); + const datauri = hcanvas.toDataURL('image/png'); gridimg.setAttribute('width', bigInt); gridimg.setAttribute('height', bigInt); gridimg.parentNode.setAttribute('width', bigInt); @@ -139,10 +138,10 @@ svgEditor.addExtension('view_grid', function () { name: 'view_grid', svgicons: svgEditor.curConfig.extPath + 'grid-icon.xml', - zoomChanged: function (zoom) { + zoomChanged (zoom) { if (showGrid) { updateGrid(zoom); } }, - callback: function () { + callback () { if (showGrid) { gridUpdate(); } @@ -153,7 +152,7 @@ svgEditor.addExtension('view_grid', function () { panel: 'editor_panel', title: 'Show/Hide Grid', events: { - click: function () { + click () { svgEditor.curConfig.showGrid = showGrid = !showGrid; gridUpdate(); } diff --git a/editor/extensions/ext-helloworld.js b/editor/extensions/ext-helloworld.js index e6572932..f49807d2 100644 --- a/editor/extensions/ext-helloworld.js +++ b/editor/extensions/ext-helloworld.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals svgEditor, svgCanvas, $ */ +/* globals jQuery, svgEditor, svgCanvas */ /* * ext-helloworld.js * @@ -16,8 +15,7 @@ */ svgEditor.addExtension('Hello World', function () { - 'use strict'; - + const $ = jQuery; return { name: 'Hello World', // For more notes on how to make an icon file, see the source of @@ -38,7 +36,7 @@ svgEditor.addExtension('Hello World', function () { // Events events: { - 'click': function () { + click () { // The action taken when the button is clicked on. // For "mode" buttons, any other button will // automatically be de-pressed. @@ -48,7 +46,7 @@ svgEditor.addExtension('Hello World', function () { }], // This is triggered when the main mouse button is pressed down // on the editor canvas (not the tool panels) - mouseDown: function () { + mouseDown () { // Check the mode on mousedown if (svgCanvas.getMode() === 'hello_world') { // The returned object must include "started" with @@ -59,16 +57,16 @@ svgEditor.addExtension('Hello World', function () { // This is triggered from anywhere, but "started" must have been set // to true (see above). Note that "opts" is an object with event info - mouseUp: function (opts) { + mouseUp (opts) { // Check the mode on mouseup if (svgCanvas.getMode() === 'hello_world') { - var zoom = svgCanvas.getZoom(); + const zoom = svgCanvas.getZoom(); // Get the actual coordinate by dividing by the zoom value - var x = opts.mouse_x / zoom; - var y = opts.mouse_y / zoom; + const x = opts.mouse_x / zoom; + const y = opts.mouse_y / zoom; - var text = 'Hello World!\n\nYou clicked here: ' + + const text = 'Hello World!\n\nYou clicked here: ' + x + ', ' + y; // Show the text using the custom alert function diff --git a/editor/extensions/ext-imagelib.js b/editor/extensions/ext-imagelib.js index ca723273..3d3d7896 100644 --- a/editor/extensions/ext-imagelib.js +++ b/editor/extensions/ext-imagelib.js @@ -1,5 +1,4 @@ -/* eslint-disable no-var */ -/* globals $, svgEditor, svgedit, svgCanvas, DOMParser */ +/* globals jQuery, svgEditor, svgedit, svgCanvas */ /* * ext-imagelib.js * @@ -10,9 +9,8 @@ */ svgEditor.addExtension('imagelib', function () { - 'use strict'; - - var uiStrings = svgEditor.uiStrings; + const $ = jQuery; + const {uiStrings} = svgEditor; $.extend(uiStrings, { imagelib: { @@ -24,7 +22,7 @@ svgEditor.addExtension('imagelib', function () { } }); - var imgLibs = [ + const imgLibs = [ { name: 'Demo library (local)', url: svgEditor.curConfig.extPath + 'imagelib/index.html', @@ -47,7 +45,7 @@ svgEditor.addExtension('imagelib', function () { } function importImage (url) { - var newImage = svgCanvas.addSvgElementFromJson({ + const newImage = svgCanvas.addSvgElementFromJson({ 'element': 'image', 'attr': { 'x': 0, @@ -63,58 +61,64 @@ svgEditor.addExtension('imagelib', function () { svgCanvas.setImageURL(url); } - var mode = 's'; - var multiArr = []; - var transferStopped = false; - var pending = {}; - var preview, submit; + const pending = {}; + + let mode = 's'; + let multiArr = []; + let transferStopped = false; + let preview, submit; window.addEventListener('message', function (evt) { - // Receive postMessage data - var response = evt.data; + // Receive `postMessage` data + let response = evt.data; - if (!response || typeof response !== 'string') { // Todo: Should namespace postMessage API for this extension and filter out here + if (!response || typeof response !== 'string') { // Do nothing return; } - try { // This block can be removed if embedAPI moves away from a string to an object (if IE9 support not needed) - var res = JSON.parse(response); - if (res.namespace) { // Part of embedAPI communications + try { + // Todo: This block can be removed (and the above check changed to + // insist on an object) if embedAPI moves away from a string to + // an object (if IE9 support not needed) + response = JSON.parse(response); + if (response.namespace !== 'imagelib') { return; } - } catch (e) {} + } catch (e) { + return; + } - var char1 = response.charAt(0); - var id; - var svgStr; - var imgStr; + const hasName = 'name' in response; + const hasHref = 'href' in response; - if (char1 !== '{' && transferStopped) { + if (!hasName && transferStopped) { transferStopped = false; return; } - if (char1 === '|') { - var secondpos = response.indexOf('|', 1); - id = response.substr(1, secondpos - 1); - response = response.substr(secondpos + 1); - char1 = response.charAt(0); + let id; + if (hasHref) { + id = response.href; + response = response.data; } // Hide possible transfer dialog box $('#dialog_box').hide(); - var entry, curMeta; - switch (char1) { - case '{': + let entry, curMeta, svgStr, imgStr; + const type = hasName + ? 'meta' + : response.charAt(0); + switch (type) { + case 'meta': { // Metadata transferStopped = false; - curMeta = JSON.parse(response); + curMeta = response; pending[curMeta.id] = curMeta; - var name = (curMeta.name || 'file'); + const name = (curMeta.name || 'file'); - var message = uiStrings.notification.retrieving.replace('%s', name); + const message = uiStrings.notification.retrieving.replace('%s', name); if (mode !== 'm') { $.process_cancel(message, function () { @@ -130,24 +134,26 @@ svgEditor.addExtension('imagelib', function () { } return; + } case '<': svgStr = true; break; - case 'd': - if (response.indexOf('data:image/svg+xml') === 0) { - var pre = 'data:image/svg+xml;base64,'; - var src = response.substring(pre.length); + case 'd': { + if (response.startsWith('data:image/svg+xml')) { + const pre = 'data:image/svg+xml;base64,'; + const src = response.substring(pre.length); response = svgedit.utilities.decode64(src); svgStr = true; break; - } else if (response.indexOf('data:image/') === 0) { + } else if (response.startsWith('data:image/')) { imgStr = true; break; } - // Else fall through + } + // Else fall through default: // TODO: See if there's a way to base64 encode the binary data stream - // var str = 'data:;base64,' + svgedit.utilities.encode64(response, true); + // const str = 'data:;base64,' + svgedit.utilities.encode64(response, true); // Assume it's raw image data // importImage(str); @@ -181,14 +187,14 @@ svgEditor.addExtension('imagelib', function () { case 'm': // Import multiple multiArr.push([(svgStr ? 'svg' : 'img'), response]); - var title; curMeta = pending[id]; + let title; if (svgStr) { if (curMeta && curMeta.name) { title = curMeta.name; } else { // Try to find a title - var xml = new DOMParser().parseFromString(response, 'text/xml').documentElement; + const xml = new DOMParser().parseFromString(response, 'text/xml').documentElement; title = $(xml).children('title').first().text() || '(SVG #' + response.length + ')'; } if (curMeta) { @@ -260,8 +266,8 @@ svgEditor.addExtension('imagelib', function () { .appendTo('#imgbrowse') .on('click touchend', function () { $.each(multiArr, function (i) { - var type = this[0]; - var data = this[1]; + const type = this[0]; + const data = this[1]; if (type === 'svg') { svgCanvas.importSvgString(data); } else { @@ -284,25 +290,25 @@ svgEditor.addExtension('imagelib', function () { } function showBrowser () { - var browser = $('#imgbrowse'); + let browser = $('#imgbrowse'); if (!browser.length) { $('
    ' + '
    ').insertAfter('#svg_docprops'); browser = $('#imgbrowse'); - var allLibs = uiStrings.imagelib.select_lib; + const allLibs = uiStrings.imagelib.select_lib; - var libOpts = $('
      ').appendTo(browser); - var frame = $('