diff --git a/.eslintignore b/.eslintignore index ef11c935..dcc1f04a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -28,3 +28,5 @@ editor/external/* editor/external/dom-polyfill/* !editor/external/dom-polyfill/dom-polyfill.js !editor/external/dynamic-import-polyfill + +mochawesome-report diff --git a/.eslintrc.js b/.eslintrc.js index 287c0e18..230c50a9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -137,7 +137,7 @@ module.exports = { // Node files files: [ "docs/jsdoc-config.js", - "build-html.js", + "build/build-html.js", "rollup.config.js", "rollup-config.config.js" ], env: { @@ -170,7 +170,10 @@ module.exports = { }, { extends: ['plugin:node/recommended-script'], - files: ['cypress/support/build-coverage-badge.js'] + files: [ + 'cypress/support/build-coverage-badge.js', + 'build/testing-badge.js' + ] }, { files: ["cypress/**"], diff --git a/.gitignore b/.gitignore index b04fcfdb..b20a14a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,17 @@ ignore node_modules -build/ - svgedit-custom.css docs/jsdoc +cypress/results cypress/screenshots cypress/videos cypress.env.json -coverage/** -instrumented/** +coverage +instrumented .nyc_output +mochawesome-report +mochawesome.json diff --git a/.npmignore b/.npmignore index 2d18b13c..4107e78f 100644 --- a/.npmignore +++ b/.npmignore @@ -11,3 +11,5 @@ cypress.env.json coverage/** .nyc_output instrumented/** +mochawesome-report/** +mochawesome.json diff --git a/CHANGES.md b/CHANGES.md index a09b0490..3ae48b06 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,9 +23,14 @@ - Fix: Ensure `setupCurPrefs` is run (including when `source` or `url` is set within the URL) - Optimization: Remove unused `jquery-ui-1.8.custom.min.js` file +- Optimization: Remove old build/tools closure/yuicompressor code - Localization: Add 'SVG-Edit Home Page' to locale files +- Docs: Remove outdated info on jsdoc linting (now just part of eslint config) +- Docs: Add testing badge +- Docs: Reprioritize `docs` in commit lists (prioritize user-facing) - Refactoring: Switch from `$.param.querystring` to `URL` - Refactoring: Ensure file-global jsdoc tags are at beginning of file +- Refactoring: Move `build-html` to `build` directory - Linting (ESLint): Simplify regexes - Linting (ESLint): Replace `innerHTML` with `textContent` from old demo - Linting (ESLint): Update as per latest ash-nazg @@ -37,7 +42,8 @@ setup browser-bug folder and ui issues folder - Testing: Create test utilities for selecting English and visiting and approving storage -- Docs: Remove outdated info on jsdoc linting (now just part of eslint config) +- Testing: Produce mochawesome report +- Testing: Cypress with multiple reporters in case we need - npm: Add `underscore` to copy script - npm: Make `copy`, `compress-images`, `start-embedded`, `build-docs-remove` scripts cross-platform; add `start-allow-origin` script diff --git a/README.md b/README.md index afae412f..2da509e1 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ +[![tests badge](https://raw.githubusercontent.com/SVG-Edit/svgedit/master/badges/tests-badge.svg?sanitize=true)](badges/tests-badge.svg) [![coverage badge](https://raw.githubusercontent.com/SVG-Edit/svgedit/master/badges/coverage-badge.svg?sanitize=true)](badges/coverage-badge.svg) [![Known Vulnerabilities](https://snyk.io/test/github/SVG-Edit/svgedit/badge.svg)](https://snyk.io/test/github/SVG-Edit/svgedit) diff --git a/badges/tests-badge.svg b/badges/tests-badge.svg new file mode 100644 index 00000000..0690604d --- /dev/null +++ b/badges/tests-badge.svg @@ -0,0 +1 @@ +TestsTests124/125124/125 \ No newline at end of file diff --git a/build-html.js b/build/build-html.js similarity index 96% rename from build-html.js rename to build/build-html.js index c84c32da..393f6db1 100644 --- a/build-html.js +++ b/build/build-html.js @@ -19,7 +19,7 @@ const filesAndReplacements = [ [ '', ` -` +` ], [ '', @@ -47,7 +47,7 @@ const filesAndReplacements = [ [ '', ` -` +` ] ] }, @@ -58,7 +58,7 @@ const filesAndReplacements = [ [ '', ` -` +` ], [ '', @@ -85,7 +85,7 @@ const filesAndReplacements = [ [ '', ` -` +` ], [ '', @@ -108,7 +108,7 @@ const filesAndReplacements = [ [ '', ` -` +` ], [ '', diff --git a/build/testing-badge.js b/build/testing-badge.js new file mode 100644 index 00000000..ef4e365d --- /dev/null +++ b/build/testing-badge.js @@ -0,0 +1,29 @@ +'use strict'; + +const EventEmitter = require('events'); +const BadgeGenerator = require('mocha-badge-generator'); + +class MockRunner extends EventEmitter {} + +const {stats: {passes, failures}} = require('../mochawesome.json'); + +const mockRunner = new MockRunner(); + +const options = { + reporterOptions: { + badge_output: 'badges/tests-badge.svg' + } +}; + +(async () => { +const p = BadgeGenerator(mockRunner, options); +mockRunner.emit('start'); +for (let i = 0; i < passes; i++) { + mockRunner.emit('pass'); +} +for (let i = 0; i < failures; i++) { + mockRunner.emit('fail'); +} +mockRunner.emit('end'); +await p; +})(); diff --git a/build/tools/COPYING b/build/tools/COPYING deleted file mode 100644 index d6456956..00000000 --- a/build/tools/COPYING +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/build/tools/README b/build/tools/README deleted file mode 100644 index d3c90e78..00000000 --- a/build/tools/README +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// -// Contents -// - -The Closure Compiler performs checking, instrumentation, and -optimizations on JavaScript code. The purpose of this README is to -explain how to build and run the Closure Compiler. - -The Closure Compiler requires Java 6 or higher. -http://www.java.com/ - - -// -// Building The Closure Compiler -// - -There are three ways to get a Closure Compiler executable. - -1) Use one we built for you. - -Pre-built Closure binaries can be found at -http://code.google.com/p/closure-compiler/downloads/list - - -2) Check out the source and build it with Apache Ant. - -First, check out the full source tree of the Closure Compiler. There -are instructions on how to do this at the project site. -http://code.google.com/p/closure-compiler/source/checkout - -Apache Ant is a cross-platform build tool. -http://ant.apache.org/ - -At the root of the source tree, there is an Ant file named -build.xml. To use it, navigate to the same directory and type the -command - -ant jar - -This will produce a jar file called "build/compiler.jar". - - -3) Check out the source and build it with Eclipse. - -Eclipse is a cross-platform IDE. -http://www.eclipse.org/ - -Under Eclipse's File menu, click "New > Project ..." and create a -"Java Project." You will see an options screen. Give the project a -name, select "Create project from existing source," and choose the -root of the checked-out source tree as the existing directory. Verify -that you are using JRE version 6 or higher. - -Eclipse can use the build.xml file to discover rules. When you -navigate to the build.xml file, you will see all the build rules in -the "Outline" pane. Run the "jar" rule to build the compiler in -build/compiler.jar. - - -// -// Running The Closure Compiler -// - -Once you have the jar binary, running the Closure Compiler is straightforward. - -On the command line, type - -java -jar compiler.jar - -This starts the compiler in interactive mode. Type - -var x = 17 + 25; - -then hit "Enter", then hit "Ctrl-Z" (on Windows) or "Ctrl-D" (on Mac or Linux) -and "Enter" again. The Compiler will respond: - -var x=42; - -The Closure Compiler has many options for reading input from a file, -writing output to a file, checking your code, and running -optimizations. To learn more, type - -java -jar compiler.jar --help - -You can read more detailed documentation about the many flags at -http://code.google.com/closure/compiler/docs/gettingstarted_app.html - - -// -// Compiling Multiple Scripts -// - -If you have multiple scripts, you should compile them all together with -one compile command. - -java -jar compiler.jar --js=in1.js --js=in2.js ... --js_output_file=out.js - -The Closure Compiler will concatenate the files in the order they're -passed at the command line. - -If you need to compile many, many scripts together, you may start to -run into problems with managing dependencies between scripts. You -should check out the Closure Library. It contains functions for -enforcing dependencies between scripts, and a tool called calcdeps.py -that knows how to give scripts to the Closure Compiler in the right -order. - -http://code.google.com/p/closure-library/ - -// -// Licensing -// - -Unless otherwise stated, all source files are licensed under -the Apache License, Version 2.0. - - ------ -Code under: -src/com/google/javascript/rhino -test/com/google/javascript/rhino - -URL: http://www.mozilla.org/rhino -Version: 1.5R3, with heavy modifications -License: Netscape Public License and MPL / GPL dual license - -Description: A partial copy of Mozilla Rhino. Mozilla Rhino is an -implementation of JavaScript for the JVM. The JavaScript parser and -the parse tree data structures were extracted and modified -significantly for use by Google's JavaScript compiler. - -Local Modifications: The packages have been renamespaced. All code not -relavant to parsing has been removed. A JSDoc parser and static typing -system have been added. - - ------ -Code in: -lib/libtrunk_rhino_parser_jarjared.jar - -Rhino -URL: http://www.mozilla.org/rhino -Version: Trunk -License: Netscape Public License and MPL / GPL dual license - -Description: Mozilla Rhino is an implementation of JavaScript for the JVM. - -Local Modifications: None. We've used JarJar to renamespace the code -post-compilation. See: -http://code.google.com/p/jarjar/ - - ------ -Code in: -lib/args4j_deploy.jar - -Args4j -URL: https://args4j.dev.java.net/ -Version: 2.0.9 -License: MIT - -Description: -args4j is a small Java class library that makes it easy to parse command line -options/arguments in your CUI application. - -Local Modifications: None. - - ------ -Code in: -lib/guava-r06.jar - -Guava Libraries -URL: http://code.google.com/p/guava-libraries/ -Version: R6 -License: Apache License 2.0 - -Description: Google's core Java libraries. - -Local Modifications: None. - - ------ -Code in: -lib/hamcrest-core-1.1.jar - -Hamcrest -URL: http://code.google.com/p/hamcrest -License: BSD -License File: LICENSE - -Description: -Provides a library of matcher objects (also known as constraints or -predicates) allowing 'match' rules to be defined declaratively, to be used in -other frameworks. Typical scenarios include testing frameworks, mocking -libraries and UI validation rules. - -Local modifications: -The original jars contained both source code and compiled classes. - -hamcrest-core-1.1.jar just contains the compiled classes. - - - ------ -Code in: -lib/jsr305.jar - -Annotations for software defect detection -URL: http://code.google.com/p/jsr-305/ -Version: svn revision 47 -License: BSD License - -Description: Annotations for software defect detection. - -Local Modifications: None. - - ----- -Code in: -lib/junit.jar - -JUnit -URL: http://sourceforge.net/projects/junit/ -Version: 4.5 -License: Common Public License 1.0 - -Description: A framework for writing and running automated tests in Java. - -Local Modifications: None. - - ---- -Code in: -lib/protobuf-java-2.3.0.jar - -Protocol Buffers -URL: http://code.google.com/p/protobuf/ -Version: 2.3.0 -License: New BSD License - -Description: Supporting libraries for protocol buffers, -an encoding of structured data. - -Local Modifications: None - - ---- -Code in: -lib/ant_deploy.jar - -URL: http://ant.apache.org/bindownload.cgi -Version: 1.6.5 -License: Apache License 2.0 -Description: - Ant is a Java based build tool. In theory it is kind of like "make" - without make's wrinkles and with the full portability of pure java code. - -Local Modifications: - Modified apache-ant-1.6.5/bin/ant to look in the ant.runfiles directory - - ---- -Code in: -lib/json.jar -URL: http://json.org/java/index.html -Version: JSON version 2 -License: MIT license -Description: -JSON is a set of java files for use in transmitting data in JSON format. - -Local Modifications: None - diff --git a/build/tools/closure-compiler.jar b/build/tools/closure-compiler.jar deleted file mode 100644 index 4dfa5ad0..00000000 Binary files a/build/tools/closure-compiler.jar and /dev/null differ diff --git a/build/tools/ship.py b/build/tools/ship.py deleted file mode 100755 index 2cb5df6a..00000000 --- a/build/tools/ship.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -# ship.py -# -# Licensed under the Apache 2 License as is the rest of the project -# Copyright (c) 2011 Jeff Schiller -# -# This script has very little real-world application. It is only used in our pure-client web app -# served on GoogleCode so we can have one HTML file, run a build script and generate a 'release' -# version without having to maintain two separate HTML files. It does this by evaluating -# 'processing comments' that are suspicously similar to IE conditional comments and then outputting -# a new HTML file after evaluating particular variables. -# -# This script takes the following inputs: -# -# * a HTML file (--i=in.html) -# * a series of flag names (--on=Foo --on=Bar) -# -# Example: -# -# in.html: -# -# BAR! -# -# -# $ ship.py --i in.html --on foo -# -# out.html: -# -# FOO! -# -# -# It has the following limitations: -# -# 1) Only if-else-endif are currently supported. -# 2) All processing comments must be on one line with no other non-whitespace characters. -# 3) Comments cannot be nested. - -import optparse -import os - -inside_if = False -last_if_true = False - -_options_parser = optparse.OptionParser( - usage='%prog --i input.html [--on flag1]', - description=('Rewrites an HTML file based on conditional comments and flags')) -_options_parser.add_option('--i', - action='store', dest='input_html_file', help='Input HTML filename') -_options_parser.add_option('--on', - action='append', type='string', dest='enabled_flags', - help='name of flag to enable') - -def parse_args(args=None): - options, rargs = _options_parser.parse_args(args) - return options, (None, None) - -def parseComment(line, line_num, enabled_flags): - global inside_if - global last_if_true - - start = line.find('{') - end = line.find('}') - statement = line[start+1:end].strip() - if statement.startswith('if '): - if inside_if == True: - print 'Fatal Error: Nested {if} found on line ' + str(line_num) - print line - quit() - - # Evaluate whether the expression is true/false. - # only one variable name allowed for now - variable_name = statement[3:].strip() - if variable_name in enabled_flags: - last_if_true = True - line = '' - else: - last_if_true = False - line = '' - - # invert the logic so the endif clause is closed properly - last_if_true = not last_if_true - - # ensure we don't have two else statements in the same if - inside_if = 'else' - - elif statement == 'endif': - if inside_if == False: - print 'Fatal Error: {endif} found without {if} on line ' + str(line_num) - print line - quit() - - if last_if_true: - line = '' - else: - line = '' - - inside_if = False - - return line - -def ship(inFileName, enabled_flags): - # read in HTML file - lines = file(inFileName, 'r').readlines() - out_lines = [] - i = 0 - - # loop for each line of markup - for line in lines: - strline = line.strip() - # if we find a comment, process it and print out - if strline.startswith('');\n break;\n } // switch on node type\n }\n indent--;\n if (!bOneLine) {\n out.push('\\n');\n for (let i = 0; i < indent; i++) { out.push(' '); }\n }\n out.push('');\n } else {\n out.push('/>');\n }\n }\n return out.join('');\n}; // end svgToString()\n\n/**\n * Function to run when image data is found.\n * @callback module:svgcanvas.ImageEmbeddedCallback\n * @param {string|false} result Data URL\n * @returns {void}\n */\n/**\n* Converts a given image file to a data URL when possible, then runs a given callback.\n* @function module:svgcanvas.SvgCanvas#embedImage\n* @param {string} src - The path/URL of the image\n* @returns {Promise} Resolves to a Data URL (string|false)\n*/\nthis.embedImage = function (src) {\n // Todo: Remove this Promise in favor of making an async/await `Image.load` utility\n return new Promise(function (resolve, reject) { // eslint-disable-line promise/avoid-new\n // load in the image and once it's loaded, get the dimensions\n $(new Image()).load(function (response, status, xhr) {\n if (status === 'error') {\n reject(new Error('Error loading image: ' + xhr.status + ' ' + xhr.statusText));\n return;\n }\n // create a canvas the same size as the raster image\n const cvs = document.createElement('canvas');\n cvs.width = this.width;\n cvs.height = this.height;\n // load the raster image into the canvas\n cvs.getContext('2d').drawImage(this, 0, 0);\n // retrieve the data: URL\n try {\n let urldata = ';svgedit_url=' + encodeURIComponent(src);\n urldata = cvs.toDataURL().replace(';base64', urldata + ';base64');\n encodableImages[src] = urldata;\n } catch (e) {\n encodableImages[src] = false;\n }\n lastGoodImgUrl = src;\n resolve(encodableImages[src]);\n }).attr('src', src);\n });\n};\n\n/**\n* Sets a given URL to be a \"last good image\" URL.\n* @function module:svgcanvas.SvgCanvas#setGoodImage\n* @param {string} val\n* @returns {void}\n*/\nthis.setGoodImage = function (val) {\n lastGoodImgUrl = val;\n};\n\n/**\n* Does nothing by default, handled by optional widget/extension.\n* @function module:svgcanvas.SvgCanvas#open\n* @returns {void}\n*/\nthis.open = function () {\n /* */\n};\n\n/**\n* Serializes the current drawing into SVG XML text and passes it to the 'saved' handler.\n* This function also includes the XML prolog. Clients of the `SvgCanvas` bind their save\n* function to the 'saved' event.\n* @function module:svgcanvas.SvgCanvas#save\n* @param {module:svgcanvas.SaveOptions} opts\n* @fires module:svgcanvas.SvgCanvas#event:saved\n* @returns {void}\n*/\nthis.save = function (opts) {\n // remove the selected outline before serializing\n clearSelection();\n // Update save options if provided\n if (opts) { $.extend(saveOptions, opts); }\n saveOptions.apply = true;\n\n // no need for doctype, see https://jwatt.org/svg/authoring/#doctype-declaration\n const str = this.svgCanvasToString();\n call('saved', str);\n};\n\n/**\n* @typedef {PlainObject} module:svgcanvas.IssuesAndCodes\n* @property {string[]} issueCodes The locale-independent code names\n* @property {string[]} issues The localized descriptions\n*/\n\n/**\n* Codes only is useful for locale-independent detection.\n* @returns {module:svgcanvas.IssuesAndCodes}\n*/\nfunction getIssues () {\n // remove the selected outline before serializing\n clearSelection();\n\n // Check for known CanVG issues\n const issues = [];\n const issueCodes = [];\n\n // Selector and notice\n const issueList = {\n feGaussianBlur: uiStrings.exportNoBlur,\n foreignObject: uiStrings.exportNoforeignObject,\n '[stroke-dasharray]': uiStrings.exportNoDashArray\n };\n const content = $(svgcontent);\n\n // Add font/text check if Canvas Text API is not implemented\n if (!('font' in $('')[0].getContext('2d'))) {\n issueList.text = uiStrings.exportNoText;\n }\n\n $.each(issueList, function (sel, descr) {\n if (content.find(sel).length) {\n issueCodes.push(sel);\n issues.push(descr);\n }\n });\n return {issues, issueCodes};\n}\n\nlet canvg;\n/**\n* @typedef {\"feGaussianBlur\"|\"foreignObject\"|\"[stroke-dasharray]\"|\"text\"} module:svgcanvas.IssueCode\n*/\n/**\n* @typedef {PlainObject} module:svgcanvas.ImageExportedResults\n* @property {string} datauri Contents as a Data URL\n* @property {string} bloburl May be the empty string\n* @property {string} svg The SVG contents as a string\n* @property {string[]} issues The localization messages of `issueCodes`\n* @property {module:svgcanvas.IssueCode[]} issueCodes CanVG issues found with the SVG\n* @property {\"PNG\"|\"JPEG\"|\"BMP\"|\"WEBP\"|\"ICO\"} type The chosen image type\n* @property {\"image/png\"|\"image/jpeg\"|\"image/bmp\"|\"image/webp\"} mimeType The image MIME type\n* @property {Float} quality A decimal between 0 and 1 (for use with JPEG or WEBP)\n* @property {string} exportWindowName A convenience for passing along a `window.name` to target a window on which the export could be added\n*/\n\n/**\n* Generates a PNG (or JPG, BMP, WEBP) Data URL based on the current image,\n* then calls \"exported\" with an object including the string, image\n* information, and any issues found.\n* @function module:svgcanvas.SvgCanvas#rasterExport\n* @param {\"PNG\"|\"JPEG\"|\"BMP\"|\"WEBP\"|\"ICO\"} [imgType=\"PNG\"]\n* @param {Float} [quality] Between 0 and 1\n* @param {string} [exportWindowName]\n* @param {PlainObject} [opts]\n* @param {boolean} [opts.avoidEvent]\n* @fires module:svgcanvas.SvgCanvas#event:exported\n* @todo Confirm/fix ICO type\n* @returns {Promise} Resolves to {@link module:svgcanvas.ImageExportedResults}\n*/\nthis.rasterExport = async function (imgType, quality, exportWindowName, opts = {}) {\n const type = imgType === 'ICO' ? 'BMP' : (imgType || 'PNG');\n const mimeType = 'image/' + type.toLowerCase();\n const {issues, issueCodes} = getIssues();\n const svg = this.svgCanvasToString();\n\n if (!canvg) {\n ({canvg} = await importSetGlobal(curConfig.canvgPath + 'canvg.js', {\n global: 'canvg'\n }));\n }\n if (!$('#export_canvas').length) {\n $('', {id: 'export_canvas'}).hide().appendTo('body');\n }\n const c = $('#export_canvas')[0];\n c.width = canvas.contentW;\n c.height = canvas.contentH;\n\n await canvg(c, svg);\n // Todo: Make async/await utility in place of `toBlob`, so we can remove this constructor\n return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new\n const dataURLType = type.toLowerCase();\n const datauri = quality\n ? c.toDataURL('image/' + dataURLType, quality)\n : c.toDataURL('image/' + dataURLType);\n let bloburl;\n /**\n * Called when `bloburl` is available for export.\n * @returns {void}\n */\n function done () {\n const obj = {\n datauri, bloburl, svg, issues, issueCodes, type: imgType,\n mimeType, quality, exportWindowName\n };\n if (!opts.avoidEvent) {\n call('exported', obj);\n }\n resolve(obj);\n }\n if (c.toBlob) {\n c.toBlob((blob) => {\n bloburl = createObjectURL(blob);\n done();\n }, mimeType, quality);\n return;\n }\n bloburl = dataURLToObjectURL(datauri);\n done();\n });\n};\n/**\n * @external jsPDF\n */\n/**\n * @typedef {void|\"save\"|\"arraybuffer\"|\"blob\"|\"datauristring\"|\"dataurlstring\"|\"dataurlnewwindow\"|\"datauri\"|\"dataurl\"} external:jsPDF.OutputType\n * @todo Newer version to add also allows these `outputType` values \"bloburi\"|\"bloburl\" which return strings, so document here and for `outputType` of `module:svgcanvas.PDFExportedResults` below if added\n*/\n/**\n* @typedef {PlainObject} module:svgcanvas.PDFExportedResults\n* @property {string} svg The SVG PDF output\n* @property {string|ArrayBuffer|Blob|window} output The output based on the `outputType`;\n* if `undefined`, \"datauristring\", \"dataurlstring\", \"datauri\",\n* or \"dataurl\", will be a string (`undefined` gives a document, while the others\n* build as Data URLs; \"datauri\" and \"dataurl\" change the location of the current page); if\n* \"arraybuffer\", will return `ArrayBuffer`; if \"blob\", returns a `Blob`;\n* if \"dataurlnewwindow\", will change the current page's location and return a string\n* if in Safari and no window object is found; otherwise opens in, and returns, a new `window`\n* object; if \"save\", will have the same return as \"dataurlnewwindow\" if\n* `navigator.getUserMedia` support is found without `URL.createObjectURL` support; otherwise\n* returns `undefined` but attempts to save\n* @property {external:jsPDF.OutputType} outputType\n* @property {string[]} issues The human-readable localization messages of corresponding `issueCodes`\n* @property {module:svgcanvas.IssueCode[]} issueCodes\n* @property {string} exportWindowName\n*/\n\n/**\n* Generates a PDF based on the current image, then calls \"exportedPDF\" with\n* an object including the string, the data URL, and any issues found.\n* @function module:svgcanvas.SvgCanvas#exportPDF\n* @param {string} [exportWindowName] Will also be used for the download file name here\n* @param {external:jsPDF.OutputType} [outputType=\"dataurlstring\"]\n* @fires module:svgcanvas.SvgCanvas#event:exportedPDF\n* @returns {Promise} Resolves to {@link module:svgcanvas.PDFExportedResults}\n*/\nthis.exportPDF = async function (\n exportWindowName,\n outputType = isChrome() ? 'save' : undefined\n) {\n if (!window.jsPDF) {\n // Todo: Switch to `import()` when widely supported and available (also allow customization of path)\n await importScript([\n // We do not currently have these paths configurable as they are\n // currently global-only, so not Rolled-up\n 'jspdf/underscore-min.js',\n 'jspdf/jspdf.min.js'\n ]);\n\n const modularVersion = !('svgEditor' in window) ||\n !window.svgEditor ||\n window.svgEditor.modules !== false;\n // Todo: Switch to `import()` when widely supported and available (also allow customization of path)\n await importScript(curConfig.jspdfPath + 'jspdf.plugin.svgToPdf.js', {\n type: modularVersion\n ? 'module'\n : 'text/javascript'\n });\n // await importModule('jspdf/jspdf.plugin.svgToPdf.js');\n }\n\n const res = getResolution();\n const orientation = res.w > res.h ? 'landscape' : 'portrait';\n const unit = 'pt'; // curConfig.baseUnit; // We could use baseUnit, but that is presumably not intended for export purposes\n\n // Todo: Give options to use predefined jsPDF formats like \"a4\", etc. from pull-down (with option to keep customizable)\n const doc = jsPDF({\n orientation,\n unit,\n format: [res.w, res.h]\n // , compressPdf: true\n });\n const docTitle = getDocumentTitle();\n doc.setProperties({\n title: docTitle /* ,\n subject: '',\n author: '',\n keywords: '',\n creator: '' */\n });\n const {issues, issueCodes} = getIssues();\n const svg = this.svgCanvasToString();\n doc.addSVG(svg, 0, 0);\n\n // doc.output('save'); // Works to open in a new\n // window; todo: configure this and other export\n // options to optionally work in this manner as\n // opposed to opening a new tab\n outputType = outputType || 'dataurlstring';\n const obj = {svg, issues, issueCodes, exportWindowName, outputType};\n obj.output = doc.output(outputType, outputType === 'save' ? (exportWindowName || 'svg.pdf') : undefined);\n call('exportedPDF', obj);\n return obj;\n};\n\n/**\n* Returns the current drawing as raw SVG XML text.\n* @function module:svgcanvas.SvgCanvas#getSvgString\n* @returns {string} The current drawing as raw SVG XML text.\n*/\nthis.getSvgString = function () {\n saveOptions.apply = false;\n return this.svgCanvasToString();\n};\n\n/**\n* This function determines whether to use a nonce in the prefix, when\n* generating IDs for future documents in SVG-Edit.\n* If you're controlling SVG-Edit externally, and want randomized IDs, call\n* this BEFORE calling `svgCanvas.setSvgString`.\n* @function module:svgcanvas.SvgCanvas#randomizeIds\n* @param {boolean} [enableRandomization] If true, adds a nonce to the prefix. Thus\n* `svgCanvas.randomizeIds() <==> svgCanvas.randomizeIds(true)`\n* @returns {void}\n*/\nthis.randomizeIds = function (enableRandomization) {\n if (arguments.length > 0 && enableRandomization === false) {\n draw.randomizeIds(false, getCurrentDrawing());\n } else {\n draw.randomizeIds(true, getCurrentDrawing());\n }\n};\n\n/**\n* Ensure each element has a unique ID.\n* @function module:svgcanvas.SvgCanvas#uniquifyElems\n* @param {Element} g - The parent element of the tree to give unique IDs\n* @returns {void}\n*/\nconst uniquifyElems = this.uniquifyElems = function (g) {\n const ids = {};\n // TODO: Handle markers and connectors. These are not yet re-identified properly\n // as their referring elements do not get remapped.\n //\n // \n // \n //\n // Problem #1: if svg_1 gets renamed, we do not update the polyline's se:connector attribute\n // Problem #2: if the polyline svg_7 gets renamed, we do not update the marker id nor the polyline's marker-end attribute\n const refElems = ['filter', 'linearGradient', 'pattern', 'radialGradient', 'symbol', 'textPath', 'use'];\n\n walkTree(g, function (n) {\n // if it's an element node\n if (n.nodeType === 1) {\n // and the element has an ID\n if (n.id) {\n // and we haven't tracked this ID yet\n if (!(n.id in ids)) {\n // add this id to our map\n ids[n.id] = {elem: null, attrs: [], hrefs: []};\n }\n ids[n.id].elem = n;\n }\n\n // now search for all attributes on this element that might refer\n // to other elements\n $.each(refAttrs, function (i, attr) {\n const attrnode = n.getAttributeNode(attr);\n if (attrnode) {\n // the incoming file has been sanitized, so we should be able to safely just strip off the leading #\n const url = getUrlFromAttr(attrnode.value),\n refid = url ? url.substr(1) : null;\n if (refid) {\n if (!(refid in ids)) {\n // add this id to our map\n ids[refid] = {elem: null, attrs: [], hrefs: []};\n }\n ids[refid].attrs.push(attrnode);\n }\n }\n });\n\n // check xlink:href now\n const href = getHref(n);\n // TODO: what if an or element refers to an element internally?\n if (href && refElems.includes(n.nodeName)) {\n const refid = href.substr(1);\n if (refid) {\n if (!(refid in ids)) {\n // add this id to our map\n ids[refid] = {elem: null, attrs: [], hrefs: []};\n }\n ids[refid].hrefs.push(n);\n }\n }\n }\n });\n\n // in ids, we now have a map of ids, elements and attributes, let's re-identify\n for (const oldid in ids) {\n if (!oldid) { continue; }\n const {elem} = ids[oldid];\n if (elem) {\n const newid = getNextId();\n\n // assign element its new id\n elem.id = newid;\n\n // remap all url() attributes\n const {attrs} = ids[oldid];\n let j = attrs.length;\n while (j--) {\n const attr = attrs[j];\n attr.ownerElement.setAttribute(attr.name, 'url(#' + newid + ')');\n }\n\n // remap all href attributes\n const hreffers = ids[oldid].hrefs;\n let k = hreffers.length;\n while (k--) {\n const hreffer = hreffers[k];\n setHref(hreffer, '#' + newid);\n }\n }\n }\n};\n\n/**\n* Assigns reference data for each use element.\n* @function module:svgcanvas.SvgCanvas#setUseData\n* @param {Element} parent\n* @returns {void}\n*/\nconst setUseData = this.setUseData = function (parent) {\n let elems = $(parent);\n\n if (parent.tagName !== 'use') {\n elems = elems.find('use');\n }\n\n elems.each(function () {\n const id = getHref(this).substr(1);\n const refElem = getElem(id);\n if (!refElem) { return; }\n $(this).data('ref', refElem);\n if (refElem.tagName === 'symbol' || refElem.tagName === 'svg') {\n $(this).data('symbol', refElem).data('ref', refElem);\n }\n });\n};\n\n/**\n* Converts gradients from userSpaceOnUse to objectBoundingBox.\n* @function module:svgcanvas.SvgCanvas#convertGradients\n* @param {Element} elem\n* @returns {void}\n*/\nconst convertGradients = this.convertGradients = function (elem) {\n let elems = $(elem).find('linearGradient, radialGradient');\n if (!elems.length && isWebkit()) {\n // Bug in webkit prevents regular *Gradient selector search\n elems = $(elem).find('*').filter(function () {\n return (this.tagName.includes('Gradient'));\n });\n }\n\n elems.each(function () {\n const grad = this; // eslint-disable-line consistent-this\n if ($(grad).attr('gradientUnits') === 'userSpaceOnUse') {\n // TODO: Support more than one element with this ref by duplicating parent grad\n const fillStrokeElems = $(svgcontent).find('[fill=\"url(#' + grad.id + ')\"],[stroke=\"url(#' + grad.id + ')\"]');\n if (!fillStrokeElems.length) { return; }\n\n // get object's bounding box\n const bb = utilsGetBBox(fillStrokeElems[0]);\n\n // This will occur if the element is inside a or a ,\n // in which we shouldn't need to convert anyway.\n if (!bb) { return; }\n\n if (grad.tagName === 'linearGradient') {\n const gCoords = $(grad).attr(['x1', 'y1', 'x2', 'y2']);\n\n // If has transform, convert\n const tlist = grad.gradientTransform.baseVal;\n if (tlist && tlist.numberOfItems > 0) {\n const m = transformListToTransform(tlist).matrix;\n const pt1 = transformPoint(gCoords.x1, gCoords.y1, m);\n const pt2 = transformPoint(gCoords.x2, gCoords.y2, m);\n\n gCoords.x1 = pt1.x;\n gCoords.y1 = pt1.y;\n gCoords.x2 = pt2.x;\n gCoords.y2 = pt2.y;\n grad.removeAttribute('gradientTransform');\n }\n\n $(grad).attr({\n x1: (gCoords.x1 - bb.x) / bb.width,\n y1: (gCoords.y1 - bb.y) / bb.height,\n x2: (gCoords.x2 - bb.x) / bb.width,\n y2: (gCoords.y2 - bb.y) / bb.height\n });\n grad.removeAttribute('gradientUnits');\n }\n // else {\n // Note: radialGradient elements cannot be easily converted\n // because userSpaceOnUse will keep circular gradients, while\n // objectBoundingBox will x/y scale the gradient according to\n // its bbox.\n //\n // For now we'll do nothing, though we should probably have\n // the gradient be updated as the element is moved, as\n // inkscape/illustrator do.\n //\n // const gCoords = $(grad).attr(['cx', 'cy', 'r']);\n //\n // $(grad).attr({\n // cx: (gCoords.cx - bb.x) / bb.width,\n // cy: (gCoords.cy - bb.y) / bb.height,\n // r: gCoords.r\n // });\n //\n // grad.removeAttribute('gradientUnits');\n // }\n }\n });\n};\n\n/**\n* Converts selected/given `` or child SVG element to a group.\n* @function module:svgcanvas.SvgCanvas#convertToGroup\n* @param {Element} elem\n* @fires module:svgcanvas.SvgCanvas#event:selected\n* @returns {void}\n*/\nconst convertToGroup = this.convertToGroup = function (elem) {\n if (!elem) {\n elem = selectedElements[0];\n }\n const $elem = $(elem);\n const batchCmd = new BatchCommand();\n let ts;\n\n if ($elem.data('gsvg')) {\n // Use the gsvg as the new group\n const svg = elem.firstChild;\n const pt = $(svg).attr(['x', 'y']);\n\n $(elem.firstChild.firstChild).unwrap();\n $(elem).removeData('gsvg');\n\n const tlist = getTransformList(elem);\n const xform = svgroot.createSVGTransform();\n xform.setTranslate(pt.x, pt.y);\n tlist.appendItem(xform);\n recalculateDimensions(elem);\n call('selected', [elem]);\n } else if ($elem.data('symbol')) {\n elem = $elem.data('symbol');\n\n ts = $elem.attr('transform');\n const pos = $elem.attr(['x', 'y']);\n\n const vb = elem.getAttribute('viewBox');\n\n if (vb) {\n const nums = vb.split(' ');\n pos.x -= Number(nums[0]);\n pos.y -= Number(nums[1]);\n }\n\n // Not ideal, but works\n ts += ' translate(' + (pos.x || 0) + ',' + (pos.y || 0) + ')';\n\n const prev = $elem.prev();\n\n // Remove element\n batchCmd.addSubCommand(new RemoveElementCommand($elem[0], $elem[0].nextSibling, $elem[0].parentNode));\n $elem.remove();\n\n // See if other elements reference this symbol\n const hasMore = $(svgcontent).find('use:data(symbol)').length;\n\n const g = svgdoc.createElementNS(NS.SVG, 'g');\n const childs = elem.childNodes;\n\n let i;\n for (i = 0; i < childs.length; i++) {\n g.append(childs[i].cloneNode(true));\n }\n\n // Duplicate the gradients for Gecko, since they weren't included in the \n if (isGecko()) {\n const dupeGrads = $(findDefs()).children('linearGradient,radialGradient,pattern').clone();\n $(g).append(dupeGrads);\n }\n\n if (ts) {\n g.setAttribute('transform', ts);\n }\n\n const parent = elem.parentNode;\n\n uniquifyElems(g);\n\n // Put the dupe gradients back into (after uniquifying them)\n if (isGecko()) {\n $(findDefs()).append($(g).find('linearGradient,radialGradient,pattern'));\n }\n\n // now give the g itself a new id\n g.id = getNextId();\n\n prev.after(g);\n\n if (parent) {\n if (!hasMore) {\n // remove symbol/svg element\n const {nextSibling} = elem;\n elem.remove();\n batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));\n }\n batchCmd.addSubCommand(new InsertElementCommand(g));\n }\n\n setUseData(g);\n\n if (isGecko()) {\n convertGradients(findDefs());\n } else {\n convertGradients(g);\n }\n\n // recalculate dimensions on the top-level children so that unnecessary transforms\n // are removed\n walkTreePost(g, function (n) {\n try {\n recalculateDimensions(n);\n } catch (e) {\n console.log(e); // eslint-disable-line no-console\n }\n });\n\n // Give ID for any visible element missing one\n $(g).find(visElems).each(function () {\n if (!this.id) { this.id = getNextId(); }\n });\n\n selectOnly([g]);\n\n const cm = pushGroupProperties(g, true);\n if (cm) {\n batchCmd.addSubCommand(cm);\n }\n\n addCommandToHistory(batchCmd);\n } else {\n console.log('Unexpected element to ungroup:', elem); // eslint-disable-line no-console\n }\n};\n\n/**\n* This function sets the current drawing as the input SVG XML.\n* @function module:svgcanvas.SvgCanvas#setSvgString\n* @param {string} xmlString - The SVG as XML text.\n* @param {boolean} [preventUndo=false] - Indicates if we want to do the\n* changes without adding them to the undo stack - e.g. for initializing a\n* drawing on page load.\n* @fires module:svgcanvas.SvgCanvas#event:setnonce\n* @fires module:svgcanvas.SvgCanvas#event:unsetnonce\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {boolean} This function returns `false` if the set was\n* unsuccessful, `true` otherwise.\n*/\nthis.setSvgString = function (xmlString, preventUndo) {\n try {\n // convert string into XML document\n const newDoc = text2xml(xmlString);\n if (newDoc.firstElementChild &&\n newDoc.firstElementChild.namespaceURI !== NS.SVG) {\n return false;\n }\n\n this.prepareSvg(newDoc);\n\n const batchCmd = new BatchCommand('Change Source');\n\n // remove old svg document\n const {nextSibling} = svgcontent;\n const oldzoom = svgroot.removeChild(svgcontent);\n batchCmd.addSubCommand(new RemoveElementCommand(oldzoom, nextSibling, svgroot));\n\n // set new svg document\n // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()\n if (svgdoc.adoptNode) {\n svgcontent = svgdoc.adoptNode(newDoc.documentElement);\n } else {\n svgcontent = svgdoc.importNode(newDoc.documentElement, true);\n }\n\n svgroot.append(svgcontent);\n const content = $(svgcontent);\n\n canvas.current_drawing_ = new draw.Drawing(svgcontent, idprefix);\n\n // retrieve or set the nonce\n const nonce = getCurrentDrawing().getNonce();\n if (nonce) {\n call('setnonce', nonce);\n } else {\n call('unsetnonce');\n }\n\n // change image href vals if possible\n content.find('image').each(function () {\n const image = this; // eslint-disable-line consistent-this\n preventClickDefault(image);\n const val = getHref(this);\n if (val) {\n if (val.startsWith('data:')) {\n // Check if an SVG-edit data URI\n const m = val.match(/svgedit_url=(.*?);/);\n // const m = val.match(/svgedit_url=(?.*?);/);\n if (m) {\n const url = decodeURIComponent(m[1]);\n // const url = decodeURIComponent(m.groups.url);\n $(new Image()).load(function () {\n image.setAttributeNS(NS.XLINK, 'xlink:href', url);\n }).attr('src', url);\n }\n }\n // Add to encodableImages if it loads\n canvas.embedImage(val);\n }\n });\n\n // Wrap child SVGs in group elements\n content.find('svg').each(function () {\n // Skip if it's in a \n if ($(this).closest('defs').length) { return; }\n\n uniquifyElems(this);\n\n // Check if it already has a gsvg group\n const pa = this.parentNode;\n if (pa.childNodes.length === 1 && pa.nodeName === 'g') {\n $(pa).data('gsvg', this);\n pa.id = pa.id || getNextId();\n } else {\n groupSvgElem(this);\n }\n });\n\n // For Firefox: Put all paint elems in defs\n if (isGecko()) {\n content.find('linearGradient, radialGradient, pattern').appendTo(findDefs());\n }\n\n // Set ref element for elements\n\n // TODO: This should also be done if the object is re-added through \"redo\"\n setUseData(content);\n\n convertGradients(content[0]);\n\n const attrs = {\n id: 'svgcontent',\n overflow: curConfig.show_outside_canvas ? 'visible' : 'hidden'\n };\n\n let percs = false;\n\n // determine proper size\n if (content.attr('viewBox')) {\n const vb = content.attr('viewBox').split(' ');\n attrs.width = vb[2];\n attrs.height = vb[3];\n // handle content that doesn't have a viewBox\n } else {\n $.each(['width', 'height'], function (i, dim) {\n // Set to 100 if not given\n const val = content.attr(dim) || '100%';\n\n if (String(val).substr(-1) === '%') {\n // Use user units if percentage given\n percs = true;\n } else {\n attrs[dim] = convertToNum(dim, val);\n }\n });\n }\n\n // identify layers\n draw.identifyLayers();\n\n // Give ID for any visible layer children missing one\n content.children().find(visElems).each(function () {\n if (!this.id) { this.id = getNextId(); }\n });\n\n // Percentage width/height, so let's base it on visible elements\n if (percs) {\n const bb = getStrokedBBoxDefaultVisible();\n attrs.width = bb.width + bb.x;\n attrs.height = bb.height + bb.y;\n }\n\n // Just in case negative numbers are given or\n // result from the percs calculation\n if (attrs.width <= 0) { attrs.width = 100; }\n if (attrs.height <= 0) { attrs.height = 100; }\n\n content.attr(attrs);\n this.contentW = attrs.width;\n this.contentH = attrs.height;\n\n batchCmd.addSubCommand(new InsertElementCommand(svgcontent));\n // update root to the correct size\n const changes = content.attr(['width', 'height']);\n batchCmd.addSubCommand(new ChangeElementCommand(svgroot, changes));\n\n // reset zoom\n currentZoom = 1;\n\n // reset transform lists\n resetListMap();\n clearSelection();\n pathModule.clearData();\n svgroot.append(selectorManager.selectorParentGroup);\n\n if (!preventUndo) addCommandToHistory(batchCmd);\n call('changed', [svgcontent]);\n } catch (e) {\n console.log(e); // eslint-disable-line no-console\n return false;\n }\n\n return true;\n};\n\n/**\n* This function imports the input SVG XML as a `` in the ``, then adds a\n* `` to the current layer.\n* @function module:svgcanvas.SvgCanvas#importSvgString\n* @param {string} xmlString - The SVG as XML text.\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {null|Element} This function returns null if the import was unsuccessful, or the element otherwise.\n* @todo\n* - properly handle if namespace is introduced by imported content (must add to svgcontent\n* and update all prefixes in the imported node)\n* - properly handle recalculating dimensions, `recalculateDimensions()` doesn't handle\n* arbitrary transform lists, but makes some assumptions about how the transform list\n* was obtained\n*/\nthis.importSvgString = function (xmlString) {\n let j, ts, useEl;\n try {\n // Get unique ID\n const uid = encode64(xmlString.length + xmlString).substr(0, 32);\n\n let useExisting = false;\n // Look for symbol and make sure symbol exists in image\n if (importIds[uid]) {\n if ($(importIds[uid].symbol).parents('#svgroot').length) {\n useExisting = true;\n }\n }\n\n const batchCmd = new BatchCommand('Import Image');\n let symbol;\n if (useExisting) {\n ({symbol} = importIds[uid]);\n ts = importIds[uid].xform;\n } else {\n // convert string into XML document\n const newDoc = text2xml(xmlString);\n\n this.prepareSvg(newDoc);\n\n // import new svg document into our document\n let svg;\n // If DOM3 adoptNode() available, use it. Otherwise fall back to DOM2 importNode()\n if (svgdoc.adoptNode) {\n svg = svgdoc.adoptNode(newDoc.documentElement);\n } else {\n svg = svgdoc.importNode(newDoc.documentElement, true);\n }\n\n uniquifyElems(svg);\n\n const innerw = convertToNum('width', svg.getAttribute('width')),\n innerh = convertToNum('height', svg.getAttribute('height')),\n innervb = svg.getAttribute('viewBox'),\n // if no explicit viewbox, create one out of the width and height\n vb = innervb ? innervb.split(' ') : [0, 0, innerw, innerh];\n for (j = 0; j < 4; ++j) {\n vb[j] = Number(vb[j]);\n }\n\n // TODO: properly handle preserveAspectRatio\n const // canvasw = +svgcontent.getAttribute('width'),\n canvash = Number(svgcontent.getAttribute('height'));\n // imported content should be 1/3 of the canvas on its largest dimension\n\n if (innerh > innerw) {\n ts = 'scale(' + (canvash / 3) / vb[3] + ')';\n } else {\n ts = 'scale(' + (canvash / 3) / vb[2] + ')';\n }\n\n // Hack to make recalculateDimensions understand how to scale\n ts = 'translate(0) ' + ts + ' translate(0)';\n\n symbol = svgdoc.createElementNS(NS.SVG, 'symbol');\n const defs = findDefs();\n\n if (isGecko()) {\n // Move all gradients into root for Firefox, workaround for this bug:\n // https://bugzilla.mozilla.org/show_bug.cgi?id=353575\n // TODO: Make this properly undo-able.\n $(svg).find('linearGradient, radialGradient, pattern').appendTo(defs);\n }\n\n while (svg.firstChild) {\n const first = svg.firstChild;\n symbol.append(first);\n }\n const attrs = svg.attributes;\n for (const attr of attrs) { // Ok for `NamedNodeMap`\n symbol.setAttribute(attr.nodeName, attr.value);\n }\n symbol.id = getNextId();\n\n // Store data\n importIds[uid] = {\n symbol,\n xform: ts\n };\n\n findDefs().append(symbol);\n batchCmd.addSubCommand(new InsertElementCommand(symbol));\n }\n\n useEl = svgdoc.createElementNS(NS.SVG, 'use');\n useEl.id = getNextId();\n setHref(useEl, '#' + symbol.id);\n\n (currentGroup || getCurrentDrawing().getCurrentLayer()).append(useEl);\n batchCmd.addSubCommand(new InsertElementCommand(useEl));\n clearSelection();\n\n useEl.setAttribute('transform', ts);\n recalculateDimensions(useEl);\n $(useEl).data('symbol', symbol).data('ref', symbol);\n addToSelection([useEl]);\n\n // TODO: Find way to add this in a recalculateDimensions-parsable way\n // if (vb[0] !== 0 || vb[1] !== 0) {\n // ts = 'translate(' + (-vb[0]) + ',' + (-vb[1]) + ') ' + ts;\n // }\n addCommandToHistory(batchCmd);\n call('changed', [svgcontent]);\n } catch (e) {\n console.log(e); // eslint-disable-line no-console\n return null;\n }\n\n // we want to return the element so we can automatically select it\n return useEl;\n};\n\n// Could deprecate, but besides external uses, their usage makes clear that\n// canvas is a dependency for all of these\nconst dr = {\n identifyLayers, createLayer, cloneLayer, deleteCurrentLayer,\n setCurrentLayer, renameCurrentLayer, setCurrentLayerPosition,\n setLayerVisibility, moveSelectedToLayer, mergeLayer, mergeAllLayers,\n leaveContext, setContext\n};\nObject.entries(dr).forEach(([prop, propVal]) => {\n canvas[prop] = propVal;\n});\ndraw.init(\n /**\n * @implements {module:draw.DrawCanvasInit}\n */\n {\n pathActions,\n getCurrentGroup () {\n return currentGroup;\n },\n setCurrentGroup (cg) {\n currentGroup = cg;\n },\n getSelectedElements,\n getSVGContent,\n undoMgr,\n elData,\n getCurrentDrawing,\n clearSelection,\n call,\n addCommandToHistory,\n /**\n * @fires module:svgcanvas.SvgCanvas#event:changed\n * @returns {void}\n */\n changeSVGContent () {\n call('changed', [svgcontent]);\n }\n }\n);\n\n/**\n* Group: Document functions.\n*/\n\n/**\n* Clears the current document. This is not an undoable action.\n* @function module:svgcanvas.SvgCanvas#clear\n* @fires module:svgcanvas.SvgCanvas#event:cleared\n* @returns {void}\n*/\nthis.clear = function () {\n pathActions.clear();\n\n clearSelection();\n\n // clear the svgcontent node\n canvas.clearSvgContentElement();\n\n // create new document\n canvas.current_drawing_ = new draw.Drawing(svgcontent);\n\n // create empty first layer\n canvas.createLayer('Layer 1');\n\n // clear the undo stack\n canvas.undoMgr.resetUndoStack();\n\n // reset the selector manager\n selectorManager.initGroup();\n\n // reset the rubber band box\n rubberBox = selectorManager.getRubberBandBox();\n\n call('cleared');\n};\n\n// Alias function\nthis.linkControlPoints = pathActions.linkControlPoints;\n\n/**\n* @function module:svgcanvas.SvgCanvas#getContentElem\n* @returns {Element} The content DOM element\n*/\nthis.getContentElem = function () { return svgcontent; };\n\n/**\n* @function module:svgcanvas.SvgCanvas#getRootElem\n* @returns {SVGSVGElement} The root DOM element\n*/\nthis.getRootElem = function () { return svgroot; };\n\n/**\n* @typedef {PlainObject} DimensionsAndZoom\n* @property {Float} w Width\n* @property {Float} h Height\n* @property {Float} zoom Zoom\n*/\n\n/**\n* @function module:svgcanvas.SvgCanvas#getResolution\n* @returns {DimensionsAndZoom} The current dimensions and zoom level in an object\n*/\nconst getResolution = this.getResolution = function () {\n// const vb = svgcontent.getAttribute('viewBox').split(' ');\n// return {w:vb[2], h:vb[3], zoom: currentZoom};\n\n const w = svgcontent.getAttribute('width') / currentZoom;\n const h = svgcontent.getAttribute('height') / currentZoom;\n\n return {\n w,\n h,\n zoom: currentZoom\n };\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getSnapToGrid\n* @returns {boolean} The current snap to grid setting\n*/\nthis.getSnapToGrid = function () { return curConfig.gridSnapping; };\n\n/**\n* @function module:svgcanvas.SvgCanvas#getVersion\n* @returns {string} A string which describes the revision number of SvgCanvas.\n*/\nthis.getVersion = function () {\n return 'svgcanvas.js ($Rev$)';\n};\n\n/**\n* Update interface strings with given values.\n* @function module:svgcanvas.SvgCanvas#setUiStrings\n* @param {module:path.uiStrings} strs - Object with strings (see the [locales API]{@link module:locale.LocaleStrings} and the [tutorial]{@tutorial LocaleDocs})\n* @returns {void}\n*/\nthis.setUiStrings = function (strs) {\n Object.assign(uiStrings, strs.notification);\n $ = jQueryPluginDBox($, strs.common);\n pathModule.setUiStrings(strs);\n};\n\n/**\n* Update configuration options with given values.\n* @function module:svgcanvas.SvgCanvas#setConfig\n* @param {module:SVGEditor.Config} opts - Object with options\n* @returns {void}\n*/\nthis.setConfig = function (opts) {\n Object.assign(curConfig, opts);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getTitle\n* @param {Element} [elem]\n* @returns {string|void} the current group/SVG's title contents or\n* `undefined` if no element is passed nd there are no selected elements.\n*/\nthis.getTitle = function (elem) {\n elem = elem || selectedElements[0];\n if (!elem) { return undefined; }\n elem = $(elem).data('gsvg') || $(elem).data('symbol') || elem;\n const childs = elem.childNodes;\n for (const child of childs) {\n if (child.nodeName === 'title') {\n return child.textContent;\n }\n }\n return '';\n};\n\n/**\n* Sets the group/SVG's title content.\n* @function module:svgcanvas.SvgCanvas#setGroupTitle\n* @param {string} val\n* @todo Combine this with `setDocumentTitle`\n* @returns {void}\n*/\nthis.setGroupTitle = function (val) {\n let elem = selectedElements[0];\n elem = $(elem).data('gsvg') || elem;\n\n const ts = $(elem).children('title');\n\n const batchCmd = new BatchCommand('Set Label');\n\n let title;\n if (!val.length) {\n // Remove title element\n const tsNextSibling = ts.nextSibling;\n batchCmd.addSubCommand(new RemoveElementCommand(ts[0], tsNextSibling, elem));\n ts.remove();\n } else if (ts.length) {\n // Change title contents\n title = ts[0];\n batchCmd.addSubCommand(new ChangeElementCommand(title, {'#text': title.textContent}));\n title.textContent = val;\n } else {\n // Add title element\n title = svgdoc.createElementNS(NS.SVG, 'title');\n title.textContent = val;\n $(elem).prepend(title);\n batchCmd.addSubCommand(new InsertElementCommand(title));\n }\n\n addCommandToHistory(batchCmd);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getDocumentTitle\n* @returns {string|void} The current document title or an empty string if not found\n*/\nconst getDocumentTitle = this.getDocumentTitle = function () {\n return canvas.getTitle(svgcontent);\n};\n\n/**\n* Adds/updates a title element for the document with the given name.\n* This is an undoable action.\n* @function module:svgcanvas.SvgCanvas#setDocumentTitle\n* @param {string} newTitle - String with the new title\n* @returns {void}\n*/\nthis.setDocumentTitle = function (newTitle) {\n const childs = svgcontent.childNodes;\n let docTitle = false, oldTitle = '';\n\n const batchCmd = new BatchCommand('Change Image Title');\n\n for (const child of childs) {\n if (child.nodeName === 'title') {\n docTitle = child;\n oldTitle = docTitle.textContent;\n break;\n }\n }\n if (!docTitle) {\n docTitle = svgdoc.createElementNS(NS.SVG, 'title');\n svgcontent.insertBefore(docTitle, svgcontent.firstChild);\n // svgcontent.firstChild.before(docTitle); // Ok to replace above with this?\n }\n\n if (newTitle.length) {\n docTitle.textContent = newTitle;\n } else {\n // No title given, so element is not necessary\n docTitle.remove();\n }\n batchCmd.addSubCommand(new ChangeElementCommand(docTitle, {'#text': oldTitle}));\n addCommandToHistory(batchCmd);\n};\n\n/**\n* Returns the editor's namespace URL, optionally adding it to the root element.\n* @function module:svgcanvas.SvgCanvas#getEditorNS\n* @param {boolean} [add] - Indicates whether or not to add the namespace value\n* @returns {string} The editor's namespace URL\n*/\nthis.getEditorNS = function (add) {\n if (add) {\n svgcontent.setAttribute('xmlns:se', NS.SE);\n }\n return NS.SE;\n};\n\n/**\n* Changes the document's dimensions to the given size.\n* @function module:svgcanvas.SvgCanvas#setResolution\n* @param {Float|\"fit\"} x - Number with the width of the new dimensions in user units.\n* Can also be the string \"fit\" to indicate \"fit to content\".\n* @param {Float} y - Number with the height of the new dimensions in user units.\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {boolean} Indicates if resolution change was successful.\n* It will fail on \"fit to content\" option with no content to fit to.\n*/\nthis.setResolution = function (x, y) {\n const res = getResolution();\n const {w, h} = res;\n let batchCmd;\n\n if (x === 'fit') {\n // Get bounding box\n const bbox = getStrokedBBoxDefaultVisible();\n\n if (bbox) {\n batchCmd = new BatchCommand('Fit Canvas to Content');\n const visEls = getVisibleElements();\n addToSelection(visEls);\n const dx = [], dy = [];\n $.each(visEls, function (i, item) {\n dx.push(bbox.x * -1);\n dy.push(bbox.y * -1);\n });\n\n const cmd = canvas.moveSelectedElements(dx, dy, true);\n batchCmd.addSubCommand(cmd);\n clearSelection();\n\n x = Math.round(bbox.width);\n y = Math.round(bbox.height);\n } else {\n return false;\n }\n }\n if (x !== w || y !== h) {\n if (!batchCmd) {\n batchCmd = new BatchCommand('Change Image Dimensions');\n }\n\n x = convertToNum('width', x);\n y = convertToNum('height', y);\n\n svgcontent.setAttribute('width', x);\n svgcontent.setAttribute('height', y);\n\n this.contentW = x;\n this.contentH = y;\n batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {width: w, height: h}));\n\n svgcontent.setAttribute('viewBox', [0, 0, x / currentZoom, y / currentZoom].join(' '));\n batchCmd.addSubCommand(new ChangeElementCommand(svgcontent, {viewBox: ['0 0', w, h].join(' ')}));\n\n addCommandToHistory(batchCmd);\n call('changed', [svgcontent]);\n }\n return true;\n};\n\n/**\n* @typedef {module:jQueryAttr.Attributes} module:svgcanvas.ElementPositionInCanvas\n* @property {Float} x\n* @property {Float} y\n*/\n\n/**\n* @function module:svgcanvas.SvgCanvas#getOffset\n* @returns {module:svgcanvas.ElementPositionInCanvas} An object with `x`, `y` values indicating the svgcontent element's\n* position in the editor's canvas.\n*/\nthis.getOffset = function () {\n return $(svgcontent).attr(['x', 'y']);\n};\n\n/**\n * @typedef {PlainObject} module:svgcanvas.ZoomAndBBox\n * @property {Float} zoom\n * @property {module:utilities.BBoxObject} bbox\n */\n/**\n* Sets the zoom level on the canvas-side based on the given value.\n* @function module:svgcanvas.SvgCanvas#setBBoxZoom\n* @param {\"selection\"|\"canvas\"|\"content\"|\"layer\"|module:SVGEditor.BBoxObjectWithFactor} val - Bounding box object to zoom to or string indicating zoom option. Note: the object value type is defined in `svg-editor.js`\n* @param {Integer} editorW - The editor's workarea box's width\n* @param {Integer} editorH - The editor's workarea box's height\n* @returns {module:svgcanvas.ZoomAndBBox|void}\n*/\nthis.setBBoxZoom = function (val, editorW, editorH) {\n let spacer = 0.85;\n let bb;\n const calcZoom = function (bb) { // eslint-disable-line no-shadow\n if (!bb) { return false; }\n const wZoom = Math.round((editorW / bb.width) * 100 * spacer) / 100;\n const hZoom = Math.round((editorH / bb.height) * 100 * spacer) / 100;\n const zoom = Math.min(wZoom, hZoom);\n canvas.setZoom(zoom);\n return {zoom, bbox: bb};\n };\n\n if (typeof val === 'object') {\n bb = val;\n if (bb.width === 0 || bb.height === 0) {\n const newzoom = bb.zoom ? bb.zoom : currentZoom * bb.factor;\n canvas.setZoom(newzoom);\n return {zoom: currentZoom, bbox: bb};\n }\n return calcZoom(bb);\n }\n\n switch (val) {\n case 'selection': {\n if (!selectedElements[0]) { return undefined; }\n const selectedElems = $.map(selectedElements, function (n) {\n if (n) {\n return n;\n }\n return undefined;\n });\n bb = getStrokedBBoxDefaultVisible(selectedElems);\n break;\n } case 'canvas': {\n const res = getResolution();\n spacer = 0.95;\n bb = {width: res.w, height: res.h, x: 0, y: 0};\n break;\n } case 'content':\n bb = getStrokedBBoxDefaultVisible();\n break;\n case 'layer':\n bb = getStrokedBBoxDefaultVisible(getVisibleElements(getCurrentDrawing().getCurrentLayer()));\n break;\n default:\n return undefined;\n }\n return calcZoom(bb);\n};\n\n/**\n* The zoom level has changed. Supplies the new zoom level as a number (not percentage).\n* @event module:svgcanvas.SvgCanvas#event:ext_zoomChanged\n* @type {Float}\n*/\n/**\n* The bottom panel was updated.\n* @event module:svgcanvas.SvgCanvas#event:ext_toolButtonStateUpdate\n* @type {PlainObject}\n* @property {boolean} nofill Indicates fill is disabled\n* @property {boolean} nostroke Indicates stroke is disabled\n*/\n/**\n* The element selection has changed (elements were added/removed from selection).\n* @event module:svgcanvas.SvgCanvas#event:ext_selectedChanged\n* @type {PlainObject}\n* @property {Element[]} elems Array of the newly selected elements\n* @property {Element|null} selectedElement The single selected element\n* @property {boolean} multiselected Indicates whether one or more elements were selected\n*/\n/**\n* Called when part of element is in process of changing, generally on\n* mousemove actions like rotate, move, etc.\n* @event module:svgcanvas.SvgCanvas#event:ext_elementTransition\n* @type {PlainObject}\n* @property {Element[]} elems Array of transitioning elements\n*/\n/**\n* One or more elements were changed.\n* @event module:svgcanvas.SvgCanvas#event:ext_elementChanged\n* @type {PlainObject}\n* @property {Element[]} elems Array of the affected elements\n*/\n/**\n* Invoked as soon as the locale is ready.\n* @event module:svgcanvas.SvgCanvas#event:ext_langReady\n* @type {PlainObject}\n* @property {string} lang The two-letter language code\n* @property {module:SVGEditor.uiStrings} uiStrings\n* @property {module:SVGEditor~ImportLocale} importLocale\n*/\n/**\n* The language was changed. Two-letter code of the new language.\n* @event module:svgcanvas.SvgCanvas#event:ext_langChanged\n* @type {string}\n*/\n/**\n* Means for an extension to add locale data. The two-letter language code.\n* @event module:svgcanvas.SvgCanvas#event:ext_addLangData\n* @type {PlainObject}\n* @property {string} lang\n* @property {module:SVGEditor~ImportLocale} importLocale\n*/\n/**\n * Called when new image is created.\n * @event module:svgcanvas.SvgCanvas#event:ext_onNewDocument\n * @type {void}\n */\n/**\n * Called when sidepanel is resized or toggled.\n * @event module:svgcanvas.SvgCanvas#event:ext_workareaResized\n * @type {void}\n*/\n/**\n * Called upon addition of the extension, or, if svgicons are set,\n * after the icons are ready when extension SVG icons have loaded.\n * @event module:svgcanvas.SvgCanvas#event:ext_callback\n * @type {void}\n*/\n\n/**\n* Sets the zoom to the given level.\n* @function module:svgcanvas.SvgCanvas#setZoom\n* @param {Float} zoomLevel - Float indicating the zoom level to change to\n* @fires module:svgcanvas.SvgCanvas#event:ext_zoomChanged\n* @returns {void}\n*/\nthis.setZoom = function (zoomLevel) {\n const res = getResolution();\n svgcontent.setAttribute('viewBox', '0 0 ' + res.w / zoomLevel + ' ' + res.h / zoomLevel);\n currentZoom = zoomLevel;\n $.each(selectedElements, function (i, elem) {\n if (!elem) { return; }\n selectorManager.requestSelector(elem).resize();\n });\n pathActions.zoomChange();\n runExtensions('zoomChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_zoomChanged} */ zoomLevel);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getMode\n* @returns {string} The current editor mode string\n*/\nthis.getMode = function () {\n return currentMode;\n};\n\n/**\n* Sets the editor's mode to the given string.\n* @function module:svgcanvas.SvgCanvas#setMode\n* @param {string} name - String with the new mode to change to\n* @returns {void}\n*/\nthis.setMode = function (name) {\n pathActions.clear(true);\n textActions.clear();\n curProperties = (selectedElements[0] && selectedElements[0].nodeName === 'text') ? curText : curShape;\n currentMode = name;\n};\n\n/**\n* Group: Element Styling.\n*/\n\n/**\n* @typedef {PlainObject} module:svgcanvas.PaintOptions\n* @property {\"solidColor\"} type\n*/\n\n/**\n* @function module:svgcanvas.SvgCanvas#getColor\n* @param {string} type\n* @returns {string|module:svgcanvas.PaintOptions|Float|module:jGraduate~Paint} The current fill/stroke option\n*/\nthis.getColor = function (type) {\n return curProperties[type];\n};\n\n/**\n* Change the current stroke/fill color/gradient value.\n* @function module:svgcanvas.SvgCanvas#setColor\n* @param {string} type - String indicating fill or stroke\n* @param {string} val - The value to set the stroke attribute to\n* @param {boolean} preventUndo - Boolean indicating whether or not this should be an undoable option\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.setColor = function (type, val, preventUndo) {\n curShape[type] = val;\n curProperties[type + '_paint'] = {type: 'solidColor'};\n const elems = [];\n /**\n *\n * @param {Element} e\n * @returns {void}\n */\n function addNonG (e) {\n if (e.nodeName !== 'g') {\n elems.push(e);\n }\n }\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n if (elem) {\n if (elem.tagName === 'g') {\n walkTree(elem, addNonG);\n } else if (type === 'fill') {\n if (elem.tagName !== 'polyline' && elem.tagName !== 'line') {\n elems.push(elem);\n }\n } else {\n elems.push(elem);\n }\n }\n }\n if (elems.length > 0) {\n if (!preventUndo) {\n changeSelectedAttribute(type, val, elems);\n call('changed', elems);\n } else {\n changeSelectedAttributeNoUndo(type, val, elems);\n }\n }\n};\n\n/**\n* Apply the current gradient to selected element's fill or stroke.\n* @function module:svgcanvas.SvgCanvas#setGradient\n* @param {\"fill\"|\"stroke\"} type - String indicating \"fill\" or \"stroke\" to apply to an element\n* @returns {void}\n*/\nconst setGradient = this.setGradient = function (type) {\n if (!curProperties[type + '_paint'] || curProperties[type + '_paint'].type === 'solidColor') { return; }\n let grad = canvas[type + 'Grad'];\n // find out if there is a duplicate gradient already in the defs\n const duplicateGrad = findDuplicateGradient(grad);\n const defs = findDefs();\n // no duplicate found, so import gradient into defs\n if (!duplicateGrad) {\n // const origGrad = grad;\n grad = defs.appendChild(svgdoc.importNode(grad, true));\n // get next id and set it on the grad\n grad.id = getNextId();\n } else { // use existing gradient\n grad = duplicateGrad;\n }\n canvas.setColor(type, 'url(#' + grad.id + ')');\n};\n\n/**\n* Check if exact gradient already exists.\n* @function module:svgcanvas~findDuplicateGradient\n* @param {SVGGradientElement} grad - The gradient DOM element to compare to others\n* @returns {SVGGradientElement} The existing gradient if found, `null` if not\n*/\nconst findDuplicateGradient = function (grad) {\n const defs = findDefs();\n const existingGrads = $(defs).find('linearGradient, radialGradient');\n let i = existingGrads.length;\n const radAttrs = ['r', 'cx', 'cy', 'fx', 'fy'];\n while (i--) {\n const og = existingGrads[i];\n if (grad.tagName === 'linearGradient') {\n if (grad.getAttribute('x1') !== og.getAttribute('x1') ||\n grad.getAttribute('y1') !== og.getAttribute('y1') ||\n grad.getAttribute('x2') !== og.getAttribute('x2') ||\n grad.getAttribute('y2') !== og.getAttribute('y2')\n ) {\n continue;\n }\n } else {\n const gradAttrs = $(grad).attr(radAttrs);\n const ogAttrs = $(og).attr(radAttrs);\n\n let diff = false;\n $.each(radAttrs, function (j, attr) {\n if (gradAttrs[attr] !== ogAttrs[attr]) { diff = true; }\n });\n\n if (diff) { continue; }\n }\n\n // else could be a duplicate, iterate through stops\n const stops = grad.getElementsByTagNameNS(NS.SVG, 'stop');\n const ostops = og.getElementsByTagNameNS(NS.SVG, 'stop');\n\n if (stops.length !== ostops.length) {\n continue;\n }\n\n let j = stops.length;\n while (j--) {\n const stop = stops[j];\n const ostop = ostops[j];\n\n if (stop.getAttribute('offset') !== ostop.getAttribute('offset') ||\n stop.getAttribute('stop-opacity') !== ostop.getAttribute('stop-opacity') ||\n stop.getAttribute('stop-color') !== ostop.getAttribute('stop-color')) {\n break;\n }\n }\n\n if (j === -1) {\n return og;\n }\n } // for each gradient in defs\n\n return null;\n};\n\n/**\n* Set a color/gradient to a fill/stroke.\n* @function module:svgcanvas.SvgCanvas#setPaint\n* @param {\"fill\"|\"stroke\"} type - String with \"fill\" or \"stroke\"\n* @param {module:jGraduate.jGraduatePaintOptions} paint - The jGraduate paint object to apply\n* @returns {void}\n*/\nthis.setPaint = function (type, paint) {\n // make a copy\n const p = new $.jGraduate.Paint(paint);\n this.setPaintOpacity(type, p.alpha / 100, true);\n\n // now set the current paint object\n curProperties[type + '_paint'] = p;\n switch (p.type) {\n case 'solidColor':\n this.setColor(type, p.solidColor !== 'none' ? '#' + p.solidColor : 'none');\n break;\n case 'linearGradient':\n case 'radialGradient':\n canvas[type + 'Grad'] = p[p.type];\n setGradient(type);\n break;\n }\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#setStrokePaint\n* @param {module:jGraduate~Paint} paint\n* @returns {void}\n*/\nthis.setStrokePaint = function (paint) {\n this.setPaint('stroke', paint);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#setFillPaint\n* @param {module:jGraduate~Paint} paint\n* @returns {void}\n*/\nthis.setFillPaint = function (paint) {\n this.setPaint('fill', paint);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getStrokeWidth\n* @returns {Float|string} The current stroke-width value\n*/\nthis.getStrokeWidth = function () {\n return curProperties.stroke_width;\n};\n\n/**\n* Sets the stroke width for the current selected elements.\n* When attempting to set a line's width to 0, this changes it to 1 instead.\n* @function module:svgcanvas.SvgCanvas#setStrokeWidth\n* @param {Float} val - A Float indicating the new stroke width value\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.setStrokeWidth = function (val) {\n if (val === 0 && ['line', 'path'].includes(currentMode)) {\n canvas.setStrokeWidth(1);\n return;\n }\n curProperties.stroke_width = val;\n\n const elems = [];\n /**\n *\n * @param {Element} e\n * @returns {void}\n */\n function addNonG (e) {\n if (e.nodeName !== 'g') {\n elems.push(e);\n }\n }\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n if (elem) {\n if (elem.tagName === 'g') {\n walkTree(elem, addNonG);\n } else {\n elems.push(elem);\n }\n }\n }\n if (elems.length > 0) {\n changeSelectedAttribute('stroke-width', val, elems);\n call('changed', selectedElements);\n }\n};\n\n/**\n* Set the given stroke-related attribute the given value for selected elements.\n* @function module:svgcanvas.SvgCanvas#setStrokeAttr\n* @param {string} attr - String with the attribute name\n* @param {string|Float} val - String or number with the attribute value\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.setStrokeAttr = function (attr, val) {\n curShape[attr.replace('-', '_')] = val;\n const elems = [];\n\n let i = selectedElements.length;\n while (i--) {\n const elem = selectedElements[i];\n if (elem) {\n if (elem.tagName === 'g') {\n walkTree(elem, function (e) { if (e.nodeName !== 'g') { elems.push(e); } });\n } else {\n elems.push(elem);\n }\n }\n }\n if (elems.length > 0) {\n changeSelectedAttribute(attr, val, elems);\n call('changed', selectedElements);\n }\n};\n\n/**\n* @typedef {PlainObject} module:svgcanvas.StyleOptions\n* @property {string} fill\n* @property {Float} fill_opacity\n* @property {string} stroke\n* @property {Float} stroke_width\n* @property {string} stroke_dasharray\n* @property {string} stroke_linejoin\n* @property {string} stroke_linecap\n* @property {Float} stroke_opacity\n* @property {Float} opacity\n*/\n\n/**\n* @function module:svgcanvas.SvgCanvas#getStyle\n* @returns {module:svgcanvas.StyleOptions} current style options\n*/\nthis.getStyle = function () {\n return curShape;\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getOpacity\n* @returns {Float} the current opacity\n*/\nthis.getOpacity = getOpacity;\n\n/**\n* Sets the given opacity on the current selected elements.\n* @function module:svgcanvas.SvgCanvas#setOpacity\n* @param {string} val\n* @returns {void}\n*/\nthis.setOpacity = function (val) {\n curShape.opacity = val;\n changeSelectedAttribute('opacity', val);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getFillOpacity\n* @returns {Float} the current fill opacity\n*/\nthis.getFillOpacity = function () {\n return curShape.fill_opacity;\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getStrokeOpacity\n* @returns {string} the current stroke opacity\n*/\nthis.getStrokeOpacity = function () {\n return curShape.stroke_opacity;\n};\n\n/**\n* Sets the current fill/stroke opacity.\n* @function module:svgcanvas.SvgCanvas#setPaintOpacity\n* @param {string} type - String with \"fill\" or \"stroke\"\n* @param {Float} val - Float with the new opacity value\n* @param {boolean} preventUndo - Indicates whether or not this should be an undoable action\n* @returns {void}\n*/\nthis.setPaintOpacity = function (type, val, preventUndo) {\n curShape[type + '_opacity'] = val;\n if (!preventUndo) {\n changeSelectedAttribute(type + '-opacity', val);\n } else {\n changeSelectedAttributeNoUndo(type + '-opacity', val);\n }\n};\n\n/**\n* Gets the current fill/stroke opacity.\n* @function module:svgcanvas.SvgCanvas#getPaintOpacity\n* @param {\"fill\"|\"stroke\"} type - String with \"fill\" or \"stroke\"\n* @returns {Float} Fill/stroke opacity\n*/\nthis.getPaintOpacity = function (type) {\n return type === 'fill' ? this.getFillOpacity() : this.getStrokeOpacity();\n};\n\n/**\n* Gets the `stdDeviation` blur value of the given element.\n* @function module:svgcanvas.SvgCanvas#getBlur\n* @param {Element} elem - The element to check the blur value for\n* @returns {string} stdDeviation blur attribute value\n*/\nthis.getBlur = function (elem) {\n let val = 0;\n // const elem = selectedElements[0];\n\n if (elem) {\n const filterUrl = elem.getAttribute('filter');\n if (filterUrl) {\n const blur = getElem(elem.id + '_blur');\n if (blur) {\n val = blur.firstChild.getAttribute('stdDeviation');\n }\n }\n }\n return val;\n};\n\n(function () {\nlet curCommand = null;\nlet filter = null;\nlet filterHidden = false;\n\n/**\n* Sets the `stdDeviation` blur value on the selected element without being undoable.\n* @function module:svgcanvas.SvgCanvas#setBlurNoUndo\n* @param {Float} val - The new `stdDeviation` value\n* @returns {void}\n*/\ncanvas.setBlurNoUndo = function (val) {\n if (!filter) {\n canvas.setBlur(val);\n return;\n }\n if (val === 0) {\n // Don't change the StdDev, as that will hide the element.\n // Instead, just remove the value for \"filter\"\n changeSelectedAttributeNoUndo('filter', '');\n filterHidden = true;\n } else {\n const elem = selectedElements[0];\n if (filterHidden) {\n changeSelectedAttributeNoUndo('filter', 'url(#' + elem.id + '_blur)');\n }\n if (isWebkit()) {\n // console.log('e', elem); // eslint-disable-line no-console\n elem.removeAttribute('filter');\n elem.setAttribute('filter', 'url(#' + elem.id + '_blur)');\n }\n changeSelectedAttributeNoUndo('stdDeviation', val, [filter.firstChild]);\n canvas.setBlurOffsets(filter, val);\n }\n};\n\n/**\n *\n * @returns {void}\n */\nfunction finishChange () {\n const bCmd = canvas.undoMgr.finishUndoableChange();\n curCommand.addSubCommand(bCmd);\n addCommandToHistory(curCommand);\n curCommand = null;\n filter = null;\n}\n\n/**\n* Sets the `x`, `y`, `width`, `height` values of the filter element in order to\n* make the blur not be clipped. Removes them if not neeeded.\n* @function module:svgcanvas.SvgCanvas#setBlurOffsets\n* @param {Element} filterElem - The filter DOM element to update\n* @param {Float} stdDev - The standard deviation value on which to base the offset size\n* @returns {void}\n*/\ncanvas.setBlurOffsets = function (filterElem, stdDev) {\n if (stdDev > 3) {\n // TODO: Create algorithm here where size is based on expected blur\n assignAttributes(filterElem, {\n x: '-50%',\n y: '-50%',\n width: '200%',\n height: '200%'\n }, 100);\n // Removing these attributes hides text in Chrome (see Issue 579)\n } else if (!isWebkit()) {\n filterElem.removeAttribute('x');\n filterElem.removeAttribute('y');\n filterElem.removeAttribute('width');\n filterElem.removeAttribute('height');\n }\n};\n\n/**\n* Adds/updates the blur filter to the selected element.\n* @function module:svgcanvas.SvgCanvas#setBlur\n* @param {Float} val - Float with the new `stdDeviation` blur value\n* @param {boolean} complete - Whether or not the action should be completed (to add to the undo manager)\n* @returns {void}\n*/\ncanvas.setBlur = function (val, complete) {\n if (curCommand) {\n finishChange();\n return;\n }\n\n // Looks for associated blur, creates one if not found\n const elem = selectedElements[0];\n const elemId = elem.id;\n filter = getElem(elemId + '_blur');\n\n val -= 0;\n\n const batchCmd = new BatchCommand();\n\n // Blur found!\n if (filter) {\n if (val === 0) {\n filter = null;\n }\n } else {\n // Not found, so create\n const newblur = addSVGElementFromJson({element: 'feGaussianBlur',\n attr: {\n in: 'SourceGraphic',\n stdDeviation: val\n }\n });\n\n filter = addSVGElementFromJson({element: 'filter',\n attr: {\n id: elemId + '_blur'\n }\n });\n\n filter.append(newblur);\n findDefs().append(filter);\n\n batchCmd.addSubCommand(new InsertElementCommand(filter));\n }\n\n const changes = {filter: elem.getAttribute('filter')};\n\n if (val === 0) {\n elem.removeAttribute('filter');\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n return;\n }\n\n changeSelectedAttribute('filter', 'url(#' + elemId + '_blur)');\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n canvas.setBlurOffsets(filter, val);\n\n curCommand = batchCmd;\n canvas.undoMgr.beginUndoableChange('stdDeviation', [filter ? filter.firstChild : null]);\n if (complete) {\n canvas.setBlurNoUndo(val);\n finishChange();\n }\n};\n}());\n\n/**\n* Check whether selected element is bold or not.\n* @function module:svgcanvas.SvgCanvas#getBold\n* @returns {boolean} Indicates whether or not element is bold\n*/\nthis.getBold = function () {\n // should only have one element selected\n const selected = selectedElements[0];\n if (!isNullish(selected) && selected.tagName === 'text' &&\n isNullish(selectedElements[1])) {\n return (selected.getAttribute('font-weight') === 'bold');\n }\n return false;\n};\n\n/**\n* Make the selected element bold or normal.\n* @function module:svgcanvas.SvgCanvas#setBold\n* @param {boolean} b - Indicates bold (`true`) or normal (`false`)\n* @returns {void}\n*/\nthis.setBold = function (b) {\n const selected = selectedElements[0];\n if (!isNullish(selected) && selected.tagName === 'text' &&\n isNullish(selectedElements[1])) {\n changeSelectedAttribute('font-weight', b ? 'bold' : 'normal');\n }\n if (!selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* Check whether selected element is in italics or not.\n* @function module:svgcanvas.SvgCanvas#getItalic\n* @returns {boolean} Indicates whether or not element is italic\n*/\nthis.getItalic = function () {\n const selected = selectedElements[0];\n if (!isNullish(selected) && selected.tagName === 'text' &&\n isNullish(selectedElements[1])) {\n return (selected.getAttribute('font-style') === 'italic');\n }\n return false;\n};\n\n/**\n* Make the selected element italic or normal.\n* @function module:svgcanvas.SvgCanvas#setItalic\n* @param {boolean} i - Indicates italic (`true`) or normal (`false`)\n* @returns {void}\n*/\nthis.setItalic = function (i) {\n const selected = selectedElements[0];\n if (!isNullish(selected) && selected.tagName === 'text' &&\n isNullish(selectedElements[1])) {\n changeSelectedAttribute('font-style', i ? 'italic' : 'normal');\n }\n if (!selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getFontFamily\n* @returns {string} The current font family\n*/\nthis.getFontFamily = function () {\n return curText.font_family;\n};\n\n/**\n* Set the new font family.\n* @function module:svgcanvas.SvgCanvas#setFontFamily\n* @param {string} val - String with the new font family\n* @returns {void}\n*/\nthis.setFontFamily = function (val) {\n curText.font_family = val;\n changeSelectedAttribute('font-family', val);\n if (selectedElements[0] && !selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* Set the new font color.\n* @function module:svgcanvas.SvgCanvas#setFontColor\n* @param {string} val - String with the new font color\n* @returns {void}\n*/\nthis.setFontColor = function (val) {\n curText.fill = val;\n changeSelectedAttribute('fill', val);\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getFontColor\n* @returns {string} The current font color\n*/\nthis.getFontColor = function () {\n return curText.fill;\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getFontSize\n* @returns {Float} The current font size\n*/\nthis.getFontSize = function () {\n return curText.font_size;\n};\n\n/**\n* Applies the given font size to the selected element.\n* @function module:svgcanvas.SvgCanvas#setFontSize\n* @param {Float} val - Float with the new font size\n* @returns {void}\n*/\nthis.setFontSize = function (val) {\n curText.font_size = val;\n changeSelectedAttribute('font-size', val);\n if (!selectedElements[0].textContent) {\n textActions.setCursor();\n }\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#getText\n* @returns {string} The current text (`textContent`) of the selected element\n*/\nthis.getText = function () {\n const selected = selectedElements[0];\n if (isNullish(selected)) { return ''; }\n return selected.textContent;\n};\n\n/**\n* Updates the text element with the given string.\n* @function module:svgcanvas.SvgCanvas#setTextContent\n* @param {string} val - String with the new text\n* @returns {void}\n*/\nthis.setTextContent = function (val) {\n changeSelectedAttribute('#text', val);\n textActions.init(val);\n textActions.setCursor();\n};\n\n/**\n* Sets the new image URL for the selected image element. Updates its size if\n* a new URL is given.\n* @function module:svgcanvas.SvgCanvas#setImageURL\n* @param {string} val - String with the image URL/path\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.setImageURL = function (val) {\n const elem = selectedElements[0];\n if (!elem) { return; }\n\n const attrs = $(elem).attr(['width', 'height']);\n const setsize = (!attrs.width || !attrs.height);\n\n const curHref = getHref(elem);\n\n // Do nothing if no URL change or size change\n if (curHref === val && !setsize) {\n return;\n }\n\n const batchCmd = new BatchCommand('Change Image URL');\n\n setHref(elem, val);\n batchCmd.addSubCommand(new ChangeElementCommand(elem, {\n '#href': curHref\n }));\n\n $(new Image()).load(function () {\n const changes = $(elem).attr(['width', 'height']);\n\n $(elem).attr({\n width: this.width,\n height: this.height\n });\n\n selectorManager.requestSelector(elem).resize();\n\n batchCmd.addSubCommand(new ChangeElementCommand(elem, changes));\n addCommandToHistory(batchCmd);\n call('changed', [elem]);\n }).attr('src', val);\n};\n\n/**\n* Sets the new link URL for the selected anchor element.\n* @function module:svgcanvas.SvgCanvas#setLinkURL\n* @param {string} val - String with the link URL/path\n* @returns {void}\n*/\nthis.setLinkURL = function (val) {\n let elem = selectedElements[0];\n if (!elem) { return; }\n if (elem.tagName !== 'a') {\n // See if parent is an anchor\n const parentsA = $(elem).parents('a');\n if (parentsA.length) {\n elem = parentsA[0];\n } else {\n return;\n }\n }\n\n const curHref = getHref(elem);\n\n if (curHref === val) { return; }\n\n const batchCmd = new BatchCommand('Change Link URL');\n\n setHref(elem, val);\n batchCmd.addSubCommand(new ChangeElementCommand(elem, {\n '#href': curHref\n }));\n\n addCommandToHistory(batchCmd);\n};\n\n/**\n* Sets the `rx` and `ry` values to the selected `rect` element\n* to change its corner radius.\n* @function module:svgcanvas.SvgCanvas#setRectRadius\n* @param {string|Float} val - The new radius\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.setRectRadius = function (val) {\n const selected = selectedElements[0];\n if (!isNullish(selected) && selected.tagName === 'rect') {\n const r = selected.getAttribute('rx');\n if (r !== String(val)) {\n selected.setAttribute('rx', val);\n selected.setAttribute('ry', val);\n addCommandToHistory(new ChangeElementCommand(selected, {rx: r, ry: r}, 'Radius'));\n call('changed', [selected]);\n }\n }\n};\n\n/**\n* Wraps the selected element(s) in an anchor element or converts group to one.\n* @function module:svgcanvas.SvgCanvas#makeHyperlink\n* @param {string} url\n* @returns {void}\n*/\nthis.makeHyperlink = function (url) {\n canvas.groupSelectedElements('a', url);\n\n // TODO: If element is a single \"g\", convert to \"a\"\n // if (selectedElements.length > 1 && selectedElements[1]) {\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#removeHyperlink\n* @returns {void}\n*/\nthis.removeHyperlink = function () {\n canvas.ungroupSelectedElement();\n};\n\n/**\n* Group: Element manipulation.\n*/\n\n/**\n* Sets the new segment type to the selected segment(s).\n* @function module:svgcanvas.SvgCanvas#setSegType\n* @param {Integer} newType - New segment type. See {@link https://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg} for list\n* @returns {void}\n*/\nthis.setSegType = function (newType) {\n pathActions.setSegType(newType);\n};\n\n/**\n* Convert selected element to a path, or get the BBox of an element-as-path.\n* @function module:svgcanvas.SvgCanvas#convertToPath\n* @todo (codedread): Remove the getBBox argument and split this function into two.\n* @param {Element} elem - The DOM element to be converted\n* @param {boolean} getBBox - Boolean on whether or not to only return the path's BBox\n* @returns {void|DOMRect|false|SVGPathElement|null} If the getBBox flag is true, the resulting path's bounding box object.\n* Otherwise the resulting path element is returned.\n*/\nthis.convertToPath = function (elem, getBBox) {\n if (isNullish(elem)) {\n const elems = selectedElements;\n $.each(elems, function (i, el) {\n if (el) { canvas.convertToPath(el); }\n });\n return undefined;\n }\n if (getBBox) {\n return getBBoxOfElementAsPath(elem, addSVGElementFromJson, pathActions);\n }\n // TODO: Why is this applying attributes from curShape, then inside utilities.convertToPath it's pulling addition attributes from elem?\n // TODO: If convertToPath is called with one elem, curShape and elem are probably the same; but calling with multiple is a bug or cool feature.\n const attrs = {\n fill: curShape.fill,\n 'fill-opacity': curShape.fill_opacity,\n stroke: curShape.stroke,\n 'stroke-width': curShape.stroke_width,\n 'stroke-dasharray': curShape.stroke_dasharray,\n 'stroke-linejoin': curShape.stroke_linejoin,\n 'stroke-linecap': curShape.stroke_linecap,\n 'stroke-opacity': curShape.stroke_opacity,\n opacity: curShape.opacity,\n visibility: 'hidden'\n };\n return convertToPath(elem, attrs, addSVGElementFromJson, pathActions, clearSelection, addToSelection, hstry, addCommandToHistory);\n};\n\n/**\n* This function makes the changes to the elements. It does not add the change\n* to the history stack.\n* @param {string} attr - Attribute name\n* @param {string|Float} newValue - String or number with the new attribute value\n* @param {Element[]} elems - The DOM elements to apply the change to\n* @returns {void}\n*/\nconst changeSelectedAttributeNoUndo = function (attr, newValue, elems) {\n if (currentMode === 'pathedit') {\n // Editing node\n pathActions.moveNode(attr, newValue);\n }\n elems = elems || selectedElements;\n let i = elems.length;\n const noXYElems = ['g', 'polyline', 'path'];\n // const goodGAttrs = ['transform', 'opacity', 'filter'];\n\n while (i--) {\n let elem = elems[i];\n if (isNullish(elem)) { continue; }\n\n // Set x,y vals on elements that don't have them\n if ((attr === 'x' || attr === 'y') && noXYElems.includes(elem.tagName)) {\n const bbox = getStrokedBBoxDefaultVisible([elem]);\n const diffX = attr === 'x' ? newValue - bbox.x : 0;\n const diffY = attr === 'y' ? newValue - bbox.y : 0;\n canvas.moveSelectedElements(diffX * currentZoom, diffY * currentZoom, true);\n continue;\n }\n\n // only allow the transform/opacity/filter attribute to change on elements, slightly hacky\n // TODO: Missing statement body\n // if (elem.tagName === 'g' && goodGAttrs.includes(attr)) {}\n let oldval = attr === '#text' ? elem.textContent : elem.getAttribute(attr);\n if (isNullish(oldval)) { oldval = ''; }\n if (oldval !== String(newValue)) {\n if (attr === '#text') {\n // const oldW = utilsGetBBox(elem).width;\n elem.textContent = newValue;\n\n // FF bug occurs on on rotated elements\n if ((/rotate/).test(elem.getAttribute('transform'))) {\n elem = ffClone(elem);\n }\n // Hoped to solve the issue of moving text with text-anchor=\"start\",\n // but this doesn't actually fix it. Hopefully on the right track, though. -Fyrd\n // const box = getBBox(elem), left = box.x, top = box.y, {width, height} = box,\n // dx = width - oldW, dy = 0;\n // const angle = getRotationAngle(elem, true);\n // if (angle) {\n // const r = Math.sqrt(dx * dx + dy * dy);\n // const theta = Math.atan2(dy, dx) - angle;\n // dx = r * Math.cos(theta);\n // dy = r * Math.sin(theta);\n //\n // elem.setAttribute('x', elem.getAttribute('x') - dx);\n // elem.setAttribute('y', elem.getAttribute('y') - dy);\n // }\n } else if (attr === '#href') {\n setHref(elem, newValue);\n } else { elem.setAttribute(attr, newValue); }\n\n // Go into \"select\" mode for text changes\n // NOTE: Important that this happens AFTER elem.setAttribute() or else attributes like\n // font-size can get reset to their old value, ultimately by svgEditor.updateContextPanel(),\n // after calling textActions.toSelectMode() below\n if (currentMode === 'textedit' && attr !== '#text' && elem.textContent.length) {\n textActions.toSelectMode(elem);\n }\n\n // if (i === 0) {\n // selectedBBoxes[0] = utilsGetBBox(elem);\n // }\n\n // Use the Firefox ffClone hack for text elements with gradients or\n // where other text attributes are changed.\n if (isGecko() && elem.nodeName === 'text' && (/rotate/).test(elem.getAttribute('transform'))) {\n if (String(newValue).startsWith('url') || (['font-size', 'font-family', 'x', 'y'].includes(attr) && elem.textContent)) {\n elem = ffClone(elem);\n }\n }\n // Timeout needed for Opera & Firefox\n // codedread: it is now possible for this function to be called with elements\n // that are not in the selectedElements array, we need to only request a\n // selector if the element is in that array\n if (selectedElements.includes(elem)) {\n setTimeout(function () {\n // Due to element replacement, this element may no longer\n // be part of the DOM\n if (!elem.parentNode) { return; }\n selectorManager.requestSelector(elem).resize();\n }, 0);\n }\n // if this element was rotated, and we changed the position of this element\n // we need to update the rotational transform attribute\n const angle = getRotationAngle(elem);\n if (angle !== 0 && attr !== 'transform') {\n const tlist = getTransformList(elem);\n let n = tlist.numberOfItems;\n while (n--) {\n const xform = tlist.getItem(n);\n if (xform.type === 4) {\n // remove old rotate\n tlist.removeItem(n);\n\n const box = utilsGetBBox(elem);\n const center = transformPoint(box.x + box.width / 2, box.y + box.height / 2, transformListToTransform(tlist).matrix);\n const cx = center.x,\n cy = center.y;\n const newrot = svgroot.createSVGTransform();\n newrot.setRotate(angle, cx, cy);\n tlist.insertItemBefore(newrot, n);\n break;\n }\n }\n }\n } // if oldValue != newValue\n } // for each elem\n};\n\n/**\n* Change the given/selected element and add the original value to the history stack.\n* If you want to change all `selectedElements`, ignore the `elems` argument.\n* If you want to change only a subset of `selectedElements`, then send the\n* subset to this function in the `elems` argument.\n* @function module:svgcanvas.SvgCanvas#changeSelectedAttribute\n* @param {string} attr - String with the attribute name\n* @param {string|Float} val - String or number with the new attribute value\n* @param {Element[]} elems - The DOM elements to apply the change to\n* @returns {void}\n*/\nconst changeSelectedAttribute = this.changeSelectedAttribute = function (attr, val, elems) {\n elems = elems || selectedElements;\n canvas.undoMgr.beginUndoableChange(attr, elems);\n // const i = elems.length;\n\n changeSelectedAttributeNoUndo(attr, val, elems);\n\n const batchCmd = canvas.undoMgr.finishUndoableChange();\n if (!batchCmd.isEmpty()) {\n addCommandToHistory(batchCmd);\n }\n};\n\n/**\n* Removes all selected elements from the DOM and adds the change to the\n* history stack.\n* @function module:svgcanvas.SvgCanvas#deleteSelectedElements\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.deleteSelectedElements = function () {\n const batchCmd = new BatchCommand('Delete Elements');\n const len = selectedElements.length;\n const selectedCopy = []; // selectedElements is being deleted\n\n for (let i = 0; i < len; ++i) {\n const selected = selectedElements[i];\n if (isNullish(selected)) { break; }\n\n let parent = selected.parentNode;\n let t = selected;\n\n // this will unselect the element and remove the selectedOutline\n selectorManager.releaseSelector(t);\n\n // Remove the path if present.\n pathModule.removePath_(t.id);\n\n // Get the parent if it's a single-child anchor\n if (parent.tagName === 'a' && parent.childNodes.length === 1) {\n t = parent;\n parent = parent.parentNode;\n }\n\n const {nextSibling} = t;\n const elem = parent.removeChild(t);\n selectedCopy.push(selected); // for the copy\n batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, parent));\n }\n selectedElements = [];\n\n if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); }\n call('changed', selectedCopy);\n clearSelection();\n};\n\n/**\n* Removes all selected elements from the DOM and adds the change to the\n* history stack. Remembers removed elements on the clipboard.\n* @function module:svgcanvas.SvgCanvas#cutSelectedElements\n* @returns {void}\n*/\nthis.cutSelectedElements = function () {\n canvas.copySelectedElements();\n canvas.deleteSelectedElements();\n};\n\n/**\n* Remembers the current selected elements on the clipboard.\n* @function module:svgcanvas.SvgCanvas#copySelectedElements\n* @returns {void}\n*/\nthis.copySelectedElements = function () {\n localStorage.setItem('svgedit_clipboard', JSON.stringify(\n selectedElements.map(function (x) { return getJsonFromSvgElement(x); })\n ));\n\n $('#cmenu_canvas').enableContextMenuItems('#paste,#paste_in_place');\n};\n\n/**\n* @function module:svgcanvas.SvgCanvas#pasteElements\n* @param {\"in_place\"|\"point\"|void} type\n* @param {Integer|void} x Expected if type is \"point\"\n* @param {Integer|void} y Expected if type is \"point\"\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @fires module:svgcanvas.SvgCanvas#event:ext_IDsUpdated\n* @returns {void}\n*/\nthis.pasteElements = function (type, x, y) {\n let clipb = JSON.parse(localStorage.getItem('svgedit_clipboard'));\n let len = clipb.length;\n if (!len) { return; }\n\n const pasted = [];\n const batchCmd = new BatchCommand('Paste elements');\n // const drawing = getCurrentDrawing();\n /**\n * @typedef {PlainObject} module:svgcanvas.ChangedIDs\n */\n /**\n * @type {module:svgcanvas.ChangedIDs}\n */\n const changedIDs = {};\n\n // Recursively replace IDs and record the changes\n /**\n *\n * @param {module:svgcanvas.SVGAsJSON} elem\n * @returns {void}\n */\n function checkIDs (elem) {\n if (elem.attr && elem.attr.id) {\n changedIDs[elem.attr.id] = getNextId();\n elem.attr.id = changedIDs[elem.attr.id];\n }\n if (elem.children) elem.children.forEach(checkIDs);\n }\n clipb.forEach(checkIDs);\n\n // Give extensions like the connector extension a chance to reflect new IDs and remove invalid elements\n /**\n * Triggered when `pasteElements` is called from a paste action (context menu or key).\n * @event module:svgcanvas.SvgCanvas#event:ext_IDsUpdated\n * @type {PlainObject}\n * @property {module:svgcanvas.SVGAsJSON[]} elems\n * @property {module:svgcanvas.ChangedIDs} changes Maps past ID (on attribute) to current ID\n */\n runExtensions(\n 'IDsUpdated',\n /** @type {module:svgcanvas.SvgCanvas#event:ext_IDsUpdated} */\n {elems: clipb, changes: changedIDs},\n true\n ).forEach(function (extChanges) {\n if (!extChanges || !('remove' in extChanges)) return;\n\n extChanges.remove.forEach(function (removeID) {\n clipb = clipb.filter(function (clipBoardItem) {\n return clipBoardItem.attr.id !== removeID;\n });\n });\n });\n\n // Move elements to lastClickPoint\n while (len--) {\n const elem = clipb[len];\n if (!elem) { continue; }\n\n const copy = addSVGElementFromJson(elem);\n pasted.push(copy);\n batchCmd.addSubCommand(new InsertElementCommand(copy));\n\n restoreRefElems(copy);\n }\n\n selectOnly(pasted);\n\n if (type !== 'in_place') {\n let ctrX, ctrY;\n\n if (!type) {\n ctrX = lastClickPoint.x;\n ctrY = lastClickPoint.y;\n } else if (type === 'point') {\n ctrX = x;\n ctrY = y;\n }\n\n const bbox = getStrokedBBoxDefaultVisible(pasted);\n const cx = ctrX - (bbox.x + bbox.width / 2),\n cy = ctrY - (bbox.y + bbox.height / 2),\n dx = [],\n dy = [];\n\n $.each(pasted, function (i, item) {\n dx.push(cx);\n dy.push(cy);\n });\n\n const cmd = canvas.moveSelectedElements(dx, dy, false);\n if (cmd) batchCmd.addSubCommand(cmd);\n }\n\n addCommandToHistory(batchCmd);\n call('changed', pasted);\n};\n\n/**\n* Wraps all the selected elements in a group (`g`) element.\n* @function module:svgcanvas.SvgCanvas#groupSelectedElements\n* @param {\"a\"|\"g\"} [type=\"g\"] - type of element to group into, defaults to ``\n* @param {string} [urlArg]\n* @returns {void}\n*/\nthis.groupSelectedElements = function (type, urlArg) {\n if (!type) { type = 'g'; }\n let cmdStr = '';\n let url;\n\n switch (type) {\n case 'a': {\n cmdStr = 'Make hyperlink';\n url = urlArg || '';\n break;\n } default: {\n type = 'g';\n cmdStr = 'Group Elements';\n break;\n }\n }\n\n const batchCmd = new BatchCommand(cmdStr);\n\n // create and insert the group element\n const g = addSVGElementFromJson({\n element: type,\n attr: {\n id: getNextId()\n }\n });\n if (type === 'a') {\n setHref(g, url);\n }\n batchCmd.addSubCommand(new InsertElementCommand(g));\n\n // now move all children into the group\n let i = selectedElements.length;\n while (i--) {\n let elem = selectedElements[i];\n if (isNullish(elem)) { continue; }\n\n if (elem.parentNode.tagName === 'a' && elem.parentNode.childNodes.length === 1) {\n elem = elem.parentNode;\n }\n\n const oldNextSibling = elem.nextSibling;\n const oldParent = elem.parentNode;\n g.append(elem);\n batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));\n }\n if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); }\n\n // update selection\n selectOnly([g], true);\n};\n\n/**\n* Pushes all appropriate parent group properties down to its children, then\n* removes them from the group.\n* @function module:svgcanvas.SvgCanvas#pushGroupProperties\n* @param {SVGAElement|SVGGElement} g\n* @param {boolean} undoable\n* @returns {BatchCommand|void}\n*/\nconst pushGroupProperties = this.pushGroupProperties = function (g, undoable) {\n const children = g.childNodes;\n const len = children.length;\n const xform = g.getAttribute('transform');\n\n const glist = getTransformList(g);\n const m = transformListToTransform(glist).matrix;\n\n const batchCmd = new BatchCommand('Push group properties');\n\n // TODO: get all fill/stroke properties from the group that we are about to destroy\n // \"fill\", \"fill-opacity\", \"fill-rule\", \"stroke\", \"stroke-dasharray\", \"stroke-dashoffset\",\n // \"stroke-linecap\", \"stroke-linejoin\", \"stroke-miterlimit\", \"stroke-opacity\",\n // \"stroke-width\"\n // and then for each child, if they do not have the attribute (or the value is 'inherit')\n // then set the child's attribute\n\n const gangle = getRotationAngle(g);\n\n const gattrs = $(g).attr(['filter', 'opacity']);\n let gfilter, gblur, changes;\n const drawing = getCurrentDrawing();\n\n for (let i = 0; i < len; i++) {\n const elem = children[i];\n\n if (elem.nodeType !== 1) { continue; }\n\n if (gattrs.opacity !== null && gattrs.opacity !== 1) {\n // const c_opac = elem.getAttribute('opacity') || 1;\n const newOpac = Math.round((elem.getAttribute('opacity') || 1) * gattrs.opacity * 100) / 100;\n changeSelectedAttribute('opacity', newOpac, [elem]);\n }\n\n if (gattrs.filter) {\n let cblur = this.getBlur(elem);\n const origCblur = cblur;\n if (!gblur) { gblur = this.getBlur(g); }\n if (cblur) {\n // Is this formula correct?\n cblur = Number(gblur) + Number(cblur);\n } else if (cblur === 0) {\n cblur = gblur;\n }\n\n // If child has no current filter, get group's filter or clone it.\n if (!origCblur) {\n // Set group's filter to use first child's ID\n if (!gfilter) {\n gfilter = getRefElem(gattrs.filter);\n } else {\n // Clone the group's filter\n gfilter = drawing.copyElem(gfilter);\n findDefs().append(gfilter);\n }\n } else {\n gfilter = getRefElem(elem.getAttribute('filter'));\n }\n\n // Change this in future for different filters\n const suffix = (gfilter.firstChild.tagName === 'feGaussianBlur') ? 'blur' : 'filter';\n gfilter.id = elem.id + '_' + suffix;\n changeSelectedAttribute('filter', 'url(#' + gfilter.id + ')', [elem]);\n\n // Update blur value\n if (cblur) {\n changeSelectedAttribute('stdDeviation', cblur, [gfilter.firstChild]);\n canvas.setBlurOffsets(gfilter, cblur);\n }\n }\n\n let chtlist = getTransformList(elem);\n\n // Don't process gradient transforms\n if (elem.tagName.includes('Gradient')) { chtlist = null; }\n\n // Hopefully not a problem to add this. Necessary for elements like \n if (!chtlist) { continue; }\n\n // Apparently can get get a transformlist, but we don't want it to have one!\n if (elem.tagName === 'defs') { continue; }\n\n if (glist.numberOfItems) {\n // TODO: if the group's transform is just a rotate, we can always transfer the\n // rotate() down to the children (collapsing consecutive rotates and factoring\n // out any translates)\n if (gangle && glist.numberOfItems === 1) {\n // [Rg] [Rc] [Mc]\n // we want [Tr] [Rc2] [Mc] where:\n // - [Rc2] is at the child's current center but has the\n // sum of the group and child's rotation angles\n // - [Tr] is the equivalent translation that this child\n // undergoes if the group wasn't there\n\n // [Tr] = [Rg] [Rc] [Rc2_inv]\n\n // get group's rotation matrix (Rg)\n const rgm = glist.getItem(0).matrix;\n\n // get child's rotation matrix (Rc)\n let rcm = svgroot.createSVGMatrix();\n const cangle = getRotationAngle(elem);\n if (cangle) {\n rcm = chtlist.getItem(0).matrix;\n }\n\n // get child's old center of rotation\n const cbox = utilsGetBBox(elem);\n const ceqm = transformListToTransform(chtlist).matrix;\n const coldc = transformPoint(cbox.x + cbox.width / 2, cbox.y + cbox.height / 2, ceqm);\n\n // sum group and child's angles\n const sangle = gangle + cangle;\n\n // get child's rotation at the old center (Rc2_inv)\n const r2 = svgroot.createSVGTransform();\n r2.setRotate(sangle, coldc.x, coldc.y);\n\n // calculate equivalent translate\n const trm = matrixMultiply(rgm, rcm, r2.matrix.inverse());\n\n // set up tlist\n if (cangle) {\n chtlist.removeItem(0);\n }\n\n if (sangle) {\n if (chtlist.numberOfItems) {\n chtlist.insertItemBefore(r2, 0);\n } else {\n chtlist.appendItem(r2);\n }\n }\n\n if (trm.e || trm.f) {\n const tr = svgroot.createSVGTransform();\n tr.setTranslate(trm.e, trm.f);\n if (chtlist.numberOfItems) {\n chtlist.insertItemBefore(tr, 0);\n } else {\n chtlist.appendItem(tr);\n }\n }\n } else { // more complicated than just a rotate\n // transfer the group's transform down to each child and then\n // call recalculateDimensions()\n const oldxform = elem.getAttribute('transform');\n changes = {};\n changes.transform = oldxform || '';\n\n const newxform = svgroot.createSVGTransform();\n\n // [ gm ] [ chm ] = [ chm ] [ gm' ]\n // [ gm' ] = [ chmInv ] [ gm ] [ chm ]\n const chm = transformListToTransform(chtlist).matrix,\n chmInv = chm.inverse();\n const gm = matrixMultiply(chmInv, m, chm);\n newxform.setMatrix(gm);\n chtlist.appendItem(newxform);\n }\n const cmd = recalculateDimensions(elem);\n if (cmd) { batchCmd.addSubCommand(cmd); }\n }\n }\n\n // remove transform and make it undo-able\n if (xform) {\n changes = {};\n changes.transform = xform;\n g.setAttribute('transform', '');\n g.removeAttribute('transform');\n batchCmd.addSubCommand(new ChangeElementCommand(g, changes));\n }\n\n if (undoable && !batchCmd.isEmpty()) {\n return batchCmd;\n }\n return undefined;\n};\n\n/**\n* Unwraps all the elements in a selected group (`g`) element. This requires\n* significant recalculations to apply group's transforms, etc. to its children.\n* @function module:svgcanvas.SvgCanvas#ungroupSelectedElement\n* @returns {void}\n*/\nthis.ungroupSelectedElement = function () {\n let g = selectedElements[0];\n if (!g) {\n return;\n }\n if ($(g).data('gsvg') || $(g).data('symbol')) {\n // Is svg, so actually convert to group\n convertToGroup(g);\n return;\n }\n if (g.tagName === 'use') {\n // Somehow doesn't have data set, so retrieve\n const symbol = getElem(getHref(g).substr(1));\n $(g).data('symbol', symbol).data('ref', symbol);\n convertToGroup(g);\n return;\n }\n const parentsA = $(g).parents('a');\n if (parentsA.length) {\n g = parentsA[0];\n }\n\n // Look for parent \"a\"\n if (g.tagName === 'g' || g.tagName === 'a') {\n const batchCmd = new BatchCommand('Ungroup Elements');\n const cmd = pushGroupProperties(g, true);\n if (cmd) { batchCmd.addSubCommand(cmd); }\n\n const parent = g.parentNode;\n const anchor = g.nextSibling;\n const children = new Array(g.childNodes.length);\n\n let i = 0;\n while (g.firstChild) {\n let elem = g.firstChild;\n const oldNextSibling = elem.nextSibling;\n const oldParent = elem.parentNode;\n\n // Remove child title elements\n if (elem.tagName === 'title') {\n const {nextSibling} = elem;\n batchCmd.addSubCommand(new RemoveElementCommand(elem, nextSibling, oldParent));\n elem.remove();\n continue;\n }\n\n children[i++] = elem = anchor.before(elem);\n batchCmd.addSubCommand(new MoveElementCommand(elem, oldNextSibling, oldParent));\n }\n\n // remove the group from the selection\n clearSelection();\n\n // delete the group element (but make undo-able)\n const gNextSibling = g.nextSibling;\n g = parent.removeChild(g);\n batchCmd.addSubCommand(new RemoveElementCommand(g, gNextSibling, parent));\n\n if (!batchCmd.isEmpty()) { addCommandToHistory(batchCmd); }\n\n // update selection\n addToSelection(children);\n }\n};\n\n/**\n* Repositions the selected element to the bottom in the DOM to appear on top of\n* other elements.\n* @function module:svgcanvas.SvgCanvas#moveToTopSelectedElement\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.moveToTopSelectedElement = function () {\n const [selected] = selectedElements;\n if (!isNullish(selected)) {\n let t = selected;\n const oldParent = t.parentNode;\n const oldNextSibling = t.nextSibling;\n t = t.parentNode.appendChild(t);\n // If the element actually moved position, add the command and fire the changed\n // event handler.\n if (oldNextSibling !== t.nextSibling) {\n addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'top'));\n call('changed', [t]);\n }\n }\n};\n\n/**\n* Repositions the selected element to the top in the DOM to appear under\n* other elements.\n* @function module:svgcanvas.SvgCanvas#moveToBottomSelectedElement\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.moveToBottomSelectedElement = function () {\n const [selected] = selectedElements;\n if (!isNullish(selected)) {\n let t = selected;\n const oldParent = t.parentNode;\n const oldNextSibling = t.nextSibling;\n let {firstChild} = t.parentNode;\n if (firstChild.tagName === 'title') {\n firstChild = firstChild.nextSibling;\n }\n // This can probably be removed, as the defs should not ever apppear\n // inside a layer group\n if (firstChild.tagName === 'defs') {\n firstChild = firstChild.nextSibling;\n }\n t = t.parentNode.insertBefore(t, firstChild);\n // If the element actually moved position, add the command and fire the changed\n // event handler.\n if (oldNextSibling !== t.nextSibling) {\n addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'bottom'));\n call('changed', [t]);\n }\n }\n};\n\n/**\n* Moves the select element up or down the stack, based on the visibly\n* intersecting elements.\n* @function module:svgcanvas.SvgCanvas#moveUpDownSelected\n* @param {\"Up\"|\"Down\"} dir - String that's either 'Up' or 'Down'\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {void}\n*/\nthis.moveUpDownSelected = function (dir) {\n const selected = selectedElements[0];\n if (!selected) { return; }\n\n curBBoxes = [];\n let closest, foundCur;\n // jQuery sorts this list\n const list = $(getIntersectionList(getStrokedBBoxDefaultVisible([selected]))).toArray();\n if (dir === 'Down') { list.reverse(); }\n\n $.each(list, function () {\n if (!foundCur) {\n if (this === selected) {\n foundCur = true;\n }\n return true;\n }\n closest = this; // eslint-disable-line consistent-this\n return false;\n });\n if (!closest) { return; }\n\n const t = selected;\n const oldParent = t.parentNode;\n const oldNextSibling = t.nextSibling;\n $(closest)[dir === 'Down' ? 'before' : 'after'](t);\n // If the element actually moved position, add the command and fire the changed\n // event handler.\n if (oldNextSibling !== t.nextSibling) {\n addCommandToHistory(new MoveElementCommand(t, oldNextSibling, oldParent, 'Move ' + dir));\n call('changed', [t]);\n }\n};\n\n/**\n* Moves selected elements on the X/Y axis.\n* @function module:svgcanvas.SvgCanvas#moveSelectedElements\n* @param {Float} dx - Float with the distance to move on the x-axis\n* @param {Float} dy - Float with the distance to move on the y-axis\n* @param {boolean} undoable - Boolean indicating whether or not the action should be undoable\n* @fires module:svgcanvas.SvgCanvas#event:changed\n* @returns {BatchCommand|void} Batch command for the move\n*/\nthis.moveSelectedElements = function (dx, dy, undoable) {\n // if undoable is not sent, default to true\n // if single values, scale them to the zoom\n if (dx.constructor !== Array) {\n dx /= currentZoom;\n dy /= currentZoom;\n }\n undoable = undoable || true;\n const batchCmd = new BatchCommand('position');\n let i = selectedElements.length;\n while (i--) {\n const selected = selectedElements[i];\n if (!isNullish(selected)) {\n // if (i === 0) {\n // selectedBBoxes[0] = utilsGetBBox(selected);\n // }\n // const b = {};\n // for (const j in selectedBBoxes[i]) b[j] = selectedBBoxes[i][j];\n // selectedBBoxes[i] = b;\n\n const xform = svgroot.createSVGTransform();\n const tlist = getTransformList(selected);\n\n // dx and dy could be arrays\n if (dx.constructor === Array) {\n // if (i === 0) {\n // selectedBBoxes[0].x += dx[0];\n // selectedBBoxes[0].y += dy[0];\n // }\n xform.setTranslate(dx[i], dy[i]);\n } else {\n // if (i === 0) {\n // selectedBBoxes[0].x += dx;\n // selectedBBoxes[0].y += dy;\n // }\n xform.setTranslate(dx, dy);\n }\n\n if (tlist.numberOfItems) {\n tlist.insertItemBefore(xform, 0);\n } else {\n tlist.appendItem(xform);\n }\n\n const cmd = recalculateDimensions(selected);\n if (cmd) {\n batchCmd.addSubCommand(cmd);\n }\n\n selectorManager.requestSelector(selected).resize();\n }\n }\n if (!batchCmd.isEmpty()) {\n if (undoable) {\n addCommandToHistory(batchCmd);\n }\n call('changed', selectedElements);\n return batchCmd;\n }\n return undefined;\n};\n\n/**\n* Create deep DOM copies (clones) of all selected elements and move them slightly\n* from their originals.\n* @function module:svgcanvas.SvgCanvas#cloneSelectedElements\n* @param {Float} x Float with the distance to move on the x-axis\n* @param {Float} y Float with the distance to move on the y-axis\n* @returns {void}\n*/\nthis.cloneSelectedElements = function (x, y) {\n let i, elem;\n const batchCmd = new BatchCommand('Clone Elements');\n // find all the elements selected (stop at first null)\n const len = selectedElements.length;\n /**\n * Sorts an array numerically and ascending.\n * @param {Element} a\n * @param {Element} b\n * @returns {Integer}\n */\n function sortfunction (a, b) {\n return ($(b).index() - $(a).index());\n }\n selectedElements.sort(sortfunction);\n for (i = 0; i < len; ++i) {\n elem = selectedElements[i];\n if (isNullish(elem)) { break; }\n }\n // use slice to quickly get the subset of elements we need\n const copiedElements = selectedElements.slice(0, i);\n this.clearSelection(true);\n // note that we loop in the reverse way because of the way elements are added\n // to the selectedElements array (top-first)\n const drawing = getCurrentDrawing();\n i = copiedElements.length;\n while (i--) {\n // clone each element and replace it within copiedElements\n elem = copiedElements[i] = drawing.copyElem(copiedElements[i]);\n (currentGroup || drawing.getCurrentLayer()).append(elem);\n batchCmd.addSubCommand(new InsertElementCommand(elem));\n }\n\n if (!batchCmd.isEmpty()) {\n addToSelection(copiedElements.reverse()); // Need to reverse for correct selection-adding\n this.moveSelectedElements(x, y, false);\n addCommandToHistory(batchCmd);\n }\n};\n\n/**\n* Aligns selected elements.\n* @function module:svgcanvas.SvgCanvas#alignSelectedElements\n* @param {string} type - String with single character indicating the alignment type\n* @param {\"selected\"|\"largest\"|\"smallest\"|\"page\"} relativeTo\n* @returns {void}\n*/\nthis.alignSelectedElements = function (type, relativeTo) {\n const bboxes = []; // angles = [];\n const len = selectedElements.length;\n if (!len) { return; }\n let minx = Number.MAX_VALUE, maxx = Number.MIN_VALUE,\n miny = Number.MAX_VALUE, maxy = Number.MIN_VALUE;\n let curwidth = Number.MIN_VALUE, curheight = Number.MIN_VALUE;\n for (let i = 0; i < len; ++i) {\n if (isNullish(selectedElements[i])) { break; }\n const elem = selectedElements[i];\n bboxes[i] = getStrokedBBoxDefaultVisible([elem]);\n\n // now bbox is axis-aligned and handles rotation\n switch (relativeTo) {\n case 'smallest':\n if (((type === 'l' || type === 'c' || type === 'r') &&\n (curwidth === Number.MIN_VALUE || curwidth > bboxes[i].width)) ||\n ((type === 't' || type === 'm' || type === 'b') &&\n (curheight === Number.MIN_VALUE || curheight > bboxes[i].height))\n ) {\n minx = bboxes[i].x;\n miny = bboxes[i].y;\n maxx = bboxes[i].x + bboxes[i].width;\n maxy = bboxes[i].y + bboxes[i].height;\n curwidth = bboxes[i].width;\n curheight = bboxes[i].height;\n }\n break;\n case 'largest':\n if (((type === 'l' || type === 'c' || type === 'r') &&\n (curwidth === Number.MIN_VALUE || curwidth < bboxes[i].width)) ||\n ((type === 't' || type === 'm' || type === 'b') &&\n (curheight === Number.MIN_VALUE || curheight < bboxes[i].height))\n ) {\n minx = bboxes[i].x;\n miny = bboxes[i].y;\n maxx = bboxes[i].x + bboxes[i].width;\n maxy = bboxes[i].y + bboxes[i].height;\n curwidth = bboxes[i].width;\n curheight = bboxes[i].height;\n }\n break;\n default: // 'selected'\n if (bboxes[i].x < minx) { minx = bboxes[i].x; }\n if (bboxes[i].y < miny) { miny = bboxes[i].y; }\n if (bboxes[i].x + bboxes[i].width > maxx) { maxx = bboxes[i].x + bboxes[i].width; }\n if (bboxes[i].y + bboxes[i].height > maxy) { maxy = bboxes[i].y + bboxes[i].height; }\n break;\n }\n } // loop for each element to find the bbox and adjust min/max\n\n if (relativeTo === 'page') {\n minx = 0;\n miny = 0;\n maxx = canvas.contentW;\n maxy = canvas.contentH;\n }\n\n const dx = new Array(len);\n const dy = new Array(len);\n for (let i = 0; i < len; ++i) {\n if (isNullish(selectedElements[i])) { break; }\n // const elem = selectedElements[i];\n const bbox = bboxes[i];\n dx[i] = 0;\n dy[i] = 0;\n switch (type) {\n case 'l': // left (horizontal)\n dx[i] = minx - bbox.x;\n break;\n case 'c': // center (horizontal)\n dx[i] = (minx + maxx) / 2 - (bbox.x + bbox.width / 2);\n break;\n case 'r': // right (horizontal)\n dx[i] = maxx - (bbox.x + bbox.width);\n break;\n case 't': // top (vertical)\n dy[i] = miny - bbox.y;\n break;\n case 'm': // middle (vertical)\n dy[i] = (miny + maxy) / 2 - (bbox.y + bbox.height / 2);\n break;\n case 'b': // bottom (vertical)\n dy[i] = maxy - (bbox.y + bbox.height);\n break;\n }\n }\n this.moveSelectedElements(dx, dy);\n};\n\n/**\n* Group: Additional editor tools.\n*/\n\n/**\n* @name module:svgcanvas.SvgCanvas#contentW\n* @type {Float}\n*/\nthis.contentW = getResolution().w;\n/**\n* @name module:svgcanvas.SvgCanvas#contentH\n* @type {Float}\n*/\nthis.contentH = getResolution().h;\n\n/**\n* @typedef {PlainObject} module:svgcanvas.CanvasInfo\n* @property {Float} x - The canvas' new x coordinate\n* @property {Float} y - The canvas' new y coordinate\n* @property {string} oldX - The canvas' old x coordinate\n* @property {string} oldY - The canvas' old y coordinate\n* @property {Float} d_x - The x position difference\n* @property {Float} d_y - The y position difference\n*/\n\n/**\n* Updates the editor canvas width/height/position after a zoom has occurred.\n* @function module:svgcanvas.SvgCanvas#updateCanvas\n* @param {Float} w - Float with the new width\n* @param {Float} h - Float with the new height\n* @fires module:svgcanvas.SvgCanvas#event:ext_canvasUpdated\n* @returns {module:svgcanvas.CanvasInfo}\n*/\nthis.updateCanvas = function (w, h) {\n svgroot.setAttribute('width', w);\n svgroot.setAttribute('height', h);\n const bg = $('#canvasBackground')[0];\n const oldX = svgcontent.getAttribute('x');\n const oldY = svgcontent.getAttribute('y');\n const x = ((w - this.contentW * currentZoom) / 2);\n const y = ((h - this.contentH * currentZoom) / 2);\n\n assignAttributes(svgcontent, {\n width: this.contentW * currentZoom,\n height: this.contentH * currentZoom,\n x,\n y,\n viewBox: '0 0 ' + this.contentW + ' ' + this.contentH\n });\n\n assignAttributes(bg, {\n width: svgcontent.getAttribute('width'),\n height: svgcontent.getAttribute('height'),\n x,\n y\n });\n\n const bgImg = getElem('background_image');\n if (bgImg) {\n assignAttributes(bgImg, {\n width: '100%',\n height: '100%'\n });\n }\n\n selectorManager.selectorParentGroup.setAttribute('transform', 'translate(' + x + ',' + y + ')');\n\n /**\n * Invoked upon updates to the canvas.\n * @event module:svgcanvas.SvgCanvas#event:ext_canvasUpdated\n * @type {PlainObject}\n * @property {Integer} new_x\n * @property {Integer} new_y\n * @property {string} old_x (Of Integer)\n * @property {string} old_y (Of Integer)\n * @property {Integer} d_x\n * @property {Integer} d_y\n */\n runExtensions(\n 'canvasUpdated',\n /**\n * @type {module:svgcanvas.SvgCanvas#event:ext_canvasUpdated}\n */\n {new_x: x, new_y: y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY}\n );\n return {x, y, old_x: oldX, old_y: oldY, d_x: x - oldX, d_y: y - oldY};\n};\n\n/**\n* Set the background of the editor (NOT the actual document).\n* @function module:svgcanvas.SvgCanvas#setBackground\n* @param {string} color - String with fill color to apply\n* @param {string} url - URL or path to image to use\n* @returns {void}\n*/\nthis.setBackground = function (color, url) {\n const bg = getElem('canvasBackground');\n const border = $(bg).find('rect')[0];\n let bgImg = getElem('background_image');\n border.setAttribute('fill', color);\n if (url) {\n if (!bgImg) {\n bgImg = svgdoc.createElementNS(NS.SVG, 'image');\n assignAttributes(bgImg, {\n id: 'background_image',\n width: '100%',\n height: '100%',\n preserveAspectRatio: 'xMinYMin',\n style: 'pointer-events:none'\n });\n }\n setHref(bgImg, url);\n bg.append(bgImg);\n } else if (bgImg) {\n bgImg.remove();\n }\n};\n\n/**\n* Select the next/previous element within the current layer.\n* @function module:svgcanvas.SvgCanvas#cycleElement\n* @param {boolean} next - true = next and false = previous element\n* @fires module:svgcanvas.SvgCanvas#event:selected\n* @returns {void}\n*/\nthis.cycleElement = function (next) {\n let num;\n const curElem = selectedElements[0];\n let elem = false;\n const allElems = getVisibleElements(currentGroup || getCurrentDrawing().getCurrentLayer());\n if (!allElems.length) { return; }\n if (isNullish(curElem)) {\n num = next ? allElems.length - 1 : 0;\n elem = allElems[num];\n } else {\n let i = allElems.length;\n while (i--) {\n if (allElems[i] === curElem) {\n num = next ? i - 1 : i + 1;\n if (num >= allElems.length) {\n num = 0;\n } else if (num < 0) {\n num = allElems.length - 1;\n }\n elem = allElems[num];\n break;\n }\n }\n }\n selectOnly([elem], true);\n call('selected', selectedElements);\n};\n\nthis.clear();\n\n/**\n* @interface module:svgcanvas.PrivateMethods\n* @type {PlainObject}\n* @property {module:svgcanvas~addCommandToHistory} addCommandToHistory\n* @property {module:history.HistoryCommand} BatchCommand\n* @property {module:history.HistoryCommand} ChangeElementCommand\n* @property {module:utilities.decode64} decode64\n* @property {module:utilities.dropXMLInternalSubset} dropXMLInternalSubset\n* @property {module:utilities.encode64} encode64\n* @property {module:svgcanvas~ffClone} ffClone\n* @property {module:svgcanvas~findDuplicateGradient} findDuplicateGradient\n* @property {module:utilities.getPathBBox} getPathBBox\n* @property {module:units.getTypeMap} getTypeMap\n* @property {module:draw.identifyLayers} identifyLayers\n* @property {module:history.HistoryCommand} InsertElementCommand\n* @property {module:browser.isChrome} isChrome\n* @property {module:math.isIdentity} isIdentity\n* @property {module:browser.isIE} isIE\n* @property {module:svgcanvas~logMatrix} logMatrix\n* @property {module:history.HistoryCommand} MoveElementCommand\n* @property {module:namespaces.NS} NS\n* @property {module:utilities.preventClickDefault} preventClickDefault\n* @property {module:history.HistoryCommand} RemoveElementCommand\n* @property {module:SVGTransformList.SVGEditTransformList} SVGEditTransformList\n* @property {module:utilities.text2xml} text2xml\n* @property {module:math.transformBox} transformBox\n* @property {module:math.transformPoint} transformPoint\n* @property {module:utilities.walkTree} walkTree\n*/\n/**\n* @deprecated getPrivateMethods\n* Since all methods are/should be public somehow, this function should be removed;\n* we might require `import` in place of this in the future once ES6 Modules\n* widespread\n\n* Being able to access private methods publicly seems wrong somehow,\n* but currently appears to be the best way to allow testing and provide\n* access to them to plugins.\n* @function module:svgcanvas.SvgCanvas#getPrivateMethods\n* @returns {module:svgcanvas.PrivateMethods}\n*/\nthis.getPrivateMethods = function () {\n const obj = {\n addCommandToHistory,\n BatchCommand,\n ChangeElementCommand,\n decode64,\n dropXMLInternalSubset,\n encode64,\n ffClone,\n findDefs,\n findDuplicateGradient,\n getElem,\n getPathBBox,\n getTypeMap,\n getUrlFromAttr,\n identifyLayers: draw.identifyLayers,\n InsertElementCommand,\n isChrome,\n isIdentity,\n isIE,\n logMatrix,\n MoveElementCommand,\n NS,\n preventClickDefault,\n RemoveElementCommand,\n SVGEditTransformList,\n text2xml,\n transformBox,\n transformPoint,\n walkTree\n };\n return obj;\n};\n } // End constructor\n} // End class\n\nexport default SvgCanvas;\n","/**\n * @file SVG Icon Loader 2.0\n *\n * jQuery Plugin for loading SVG icons from a single file\n *\n * Adds {@link external:jQuery.svgIcons}, {@link external:jQuery.getSvgIcon}, {@link external:jQuery.resizeSvgIcons}\n *\n * How to use:\n\n1. Create the SVG master file that includes all icons:\n\nThe master SVG icon-containing file is an SVG file that contains\n`` elements. Each `` element should contain the markup of an SVG\nicon. The `` element has an ID that should\ncorrespond with the ID of the HTML element used on the page that should contain\nor optionally be replaced by the icon. Additionally, one empty element should be\nadded at the end with id \"svg_eof\".\n\n2. Optionally create fallback raster images for each SVG icon.\n\n3. Include the jQuery and the SVG Icon Loader scripts on your page.\n\n4. Run `$.svgIcons()` when the document is ready. See its signature\n\n5. To access an icon at a later point without using the callback, use this:\n `$.getSvgIcon(id (string), uniqueClone (boolean))`;\n\nThis will return the icon (as jQuery object) with a given ID.\n\n6. To resize icons at a later point without using the callback, use this:\n `$.resizeSvgIcons(resizeOptions)` (use the same way as the \"resize\" parameter)\n *\n * @module jQuerySVGIcons\n * @license MIT\n * @copyright (c) 2009 Alexis Deveria\n * {@link http://a.deveria.com}\n * @example\n$(function () {\n $.svgIcons('my_icon_set.svg'); // The SVG file that contains all icons\n // No options have been set, so all icons will automatically be inserted\n // into HTML elements that match the same IDs.\n});\n\n* @example\n$(function () {\n // The SVG file that contains all icons\n $.svgIcons('my_icon_set.svg', {\n callback (icons) { // Custom callback function that sets click\n // events for each icon\n $.each(icons, function (id, icon) {\n icon.click(function () {\n alert('You clicked on the icon with id ' + id);\n });\n });\n }\n });\n});\n\n* @example\n$(function () {\n // The SVGZ file that contains all icons\n $.svgIcons('my_icon_set.svgz', {\n w: 32, // All icons will be 32px wide\n h: 32, // All icons will be 32px high\n fallback_path: 'icons/', // All fallback files can be found here\n fallback: {\n '#open_icon': 'open.png', // The \"open.png\" will be appended to the\n // HTML element with ID \"open_icon\"\n '#close_icon': 'close.png',\n '#save_icon': 'save.png'\n },\n placement: {'.open_icon': 'open'}, // The \"open\" icon will be added\n // to all elements with class \"open_icon\"\n resize: {\n '#save_icon .svg_icon': 64 // The \"save\" icon will be resized to 64 x 64px\n },\n\n callback (icons) { // Sets background color for \"close\" icon\n icons.close.css('background', 'red');\n },\n\n svgz: true // Indicates that an SVGZ file is being used\n });\n});\n*/\n\n// Todo: Move to own module (and have it import a modular base64 encoder)\nimport {encode64} from '../utilities.js';\n\nconst isOpera = Boolean(window.opera);\n\nconst fixIDs = function (svgEl, svgNum, force) {\n const defs = svgEl.find('defs');\n if (!defs.length) return svgEl;\n\n let idElems;\n if (isOpera) {\n idElems = defs.find('*').filter(function () {\n return Boolean(this.id);\n });\n } else {\n idElems = defs.find('[id]');\n }\n\n const allElems = svgEl[0].getElementsByTagName('*'),\n len = allElems.length;\n\n idElems.each(function (i) {\n const {id} = this;\n /*\n const noDupes = ($(svgdoc).find('#' + id).length <= 1);\n if (isOpera) noDupes = false; // Opera didn't clone svgEl, so not reliable\n if(!force && noDupes) return;\n */\n const newId = 'x' + id + svgNum + i;\n this.id = newId;\n\n const oldVal = 'url(#' + id + ')';\n const newVal = 'url(#' + newId + ')';\n\n // Selector method, possibly faster but fails in Opera / jQuery 1.4.3\n // svgEl.find('[fill=\"url(#' + id + ')\"]').each(function() {\n // this.setAttribute('fill', 'url(#' + newId + ')');\n // }).end().find('[stroke=\"url(#' + id + ')\"]').each(function() {\n // this.setAttribute('stroke', 'url(#' + newId + ')');\n // }).end().find('use').each(function() {\n // if(this.getAttribute('xlink:href') == '#' + id) {\n // this.setAttributeNS(xlinkns,'href','#' + newId);\n // }\n // }).end().find('[filter=\"url(#' + id + ')\"]').each(function() {\n // this.setAttribute('filter', 'url(#' + newId + ')');\n // });\n\n for (i = 0; i < len; i++) {\n const elem = allElems[i];\n if (elem.getAttribute('fill') === oldVal) {\n elem.setAttribute('fill', newVal);\n }\n if (elem.getAttribute('stroke') === oldVal) {\n elem.setAttribute('stroke', newVal);\n }\n if (elem.getAttribute('filter') === oldVal) {\n elem.setAttribute('filter', newVal);\n }\n }\n });\n return svgEl;\n};\n\n/**\n* @callback module:jQuerySVGIcons.SVGIconsLoadedCallback\n* @param {PlainObject} svgIcons IDs keyed to jQuery objects of images\n* @returns {void}\n*/\n\n/**\n * @function module:jQuerySVGIcons.jQuerySVGIcons\n * @param {external:jQuery} $ Its keys include all icon IDs and the values, the icon as a jQuery object\n * @returns {external:jQuery} The enhanced jQuery object\n*/\nexport default function jQueryPluginSVGIcons ($) {\n const svgIcons = {};\n\n /**\n * Map of raster images with each key being the SVG icon ID\n * to replace, and the value the image file name.\n * @typedef {PlainObject} external:jQuery.svgIcons.Fallback\n */\n /**\n * Map of raster images with each key being the SVG icon ID\n * whose `alt` will be set, and the value being the `alt` text.\n * @typedef {PlainObject} external:jQuery.svgIcons.Alts\n */\n /**\n * @function external:jQuery.svgIcons\n * @param {string} file The location of a local SVG or SVGz file\n * @param {PlainObject} [opts]\n * @param {Float} [opts.w] The icon widths\n * @param {Float} [opts.h] The icon heights\n * @param {external:jQuery.svgIcons.Fallback} [opts.fallback]\n * @param {string} [opts.fallback_path] The path to use for all images\n * listed under \"fallback\"\n * @param {boolean} [opts.replace] If set to `true`, HTML elements will\n * be replaced by, rather than include the SVG icon.\n * @param {PlainObject} [opts.placement] Map with selectors\n * for keys and SVG icon ids as values. This provides a custom method of\n * adding icons.\n * @param {PlainObject} [opts.resize] Map\n * with selectors for keys and numbers as values. This allows an easy way to\n * resize specific icons.\n * @param {module:jQuerySVGIcons.SVGIconsLoadedCallback} [opts.callback] A\n * function to call when all icons have been loaded.\n * @param {boolean} [opts.id_match=true] Automatically attempt to match\n * SVG icon ids with corresponding HTML id\n * @param {boolean} [opts.no_img] Prevent attempting to convert the icon\n * into an `` element (may be faster, help for browser consistency)\n * @param {boolean} [opts.svgz] Indicate that the file is an SVGZ file, and\n * thus not to parse as XML. SVGZ files add compression benefits, but\n * getting data from them fails in Firefox 2 and older.\n * @param {jQuery.svgIcons.Alts} [opts.alts] Map of images with each key\n * being the SVG icon ID whose `alt` will be set, and the value being\n * the `alt` text\n * @param {string} [opts.testIconAlt=\"icon\"] Alt text for the injected test image.\n * In case wish to ensure have one for accessibility\n * @returns {void}\n */\n $.svgIcons = function (file, opts = {}) {\n const svgns = 'http://www.w3.org/2000/svg',\n xlinkns = 'http://www.w3.org/1999/xlink',\n iconW = opts.w || 24,\n iconH = opts.h || 24;\n let elems, svgdoc, testImg,\n iconsMade = false,\n dataLoaded = false,\n loadAttempts = 0;\n const // ua = navigator.userAgent,\n // isSafari = (ua.includes('Safari/') && !ua.includes('Chrome/')),\n dataPre = 'data:image/svg+xml;charset=utf-8;base64,';\n\n let dataEl;\n if (opts.svgz) {\n dataEl = $('').appendTo('body').hide();\n try {\n svgdoc = dataEl[0].contentDocument;\n dataEl.load(getIcons);\n getIcons(0, true); // Opera will not run \"load\" event if file is already cached\n } catch (err1) {\n useFallback();\n }\n } else {\n const parser = new DOMParser();\n $.ajax({\n url: file,\n dataType: 'string',\n success (data) {\n if (!data) {\n $(useFallback);\n return;\n }\n svgdoc = parser.parseFromString(data, 'text/xml');\n $(function () {\n getIcons('ajax');\n });\n },\n error (err) {\n // TODO: Fix Opera widget icon bug\n if (window.opera) {\n $(function () {\n useFallback();\n });\n } else if (err.responseText) {\n svgdoc = parser.parseFromString(err.responseText, 'text/xml');\n\n if (!svgdoc.childNodes.length) {\n $(useFallback);\n }\n $(function () {\n getIcons('ajax');\n });\n } else {\n $(useFallback);\n }\n }\n });\n }\n\n /**\n *\n * @param {\"ajax\"|0|void} evt\n * @param {boolean} [noWait]\n * @returns {void}\n */\n function getIcons (evt, noWait) {\n if (evt !== 'ajax') {\n if (dataLoaded) return;\n // Webkit sometimes says svgdoc is undefined, other times\n // it fails to load all nodes. Thus we must make sure the \"eof\"\n // element is loaded.\n svgdoc = dataEl[0].contentDocument; // Needed again for Webkit\n const isReady = (svgdoc && svgdoc.getElementById('svg_eof'));\n if (!isReady && !(noWait && isReady)) {\n loadAttempts++;\n if (loadAttempts < 50) {\n setTimeout(getIcons, 20);\n } else {\n useFallback();\n dataLoaded = true;\n }\n return;\n }\n dataLoaded = true;\n }\n\n elems = $(svgdoc.firstChild).children(); // .getElementsByTagName('foreignContent');\n\n if (!opts.no_img) {\n const testSrc = dataPre + 'PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNzUiIGhlaWdodD0iMjc1Ij48L3N2Zz4%3D';\n\n testImg = $(new Image()).attr({\n src: testSrc,\n width: 0,\n height: 0,\n alt: opts.testIconAlt || 'icon'\n }).appendTo('body')\n .load(function () {\n // Safari 4 crashes, Opera and Chrome don't\n makeIcons(true);\n }).error(function () {\n makeIcons();\n });\n } else {\n setTimeout(function () {\n if (!iconsMade) makeIcons();\n }, 500);\n }\n }\n\n /**\n *\n * @param {external:jQuery} target\n * @param {external:jQuery} icon A wrapped `defs` or Image\n * @param {string} id SVG icon ID\n * @param {boolean} setID Whether to set the ID attribute (with `id`)\n * @returns {void}\n */\n function setIcon (target, icon, id, setID) {\n if (isOpera) icon.css('visibility', 'hidden');\n if (opts.replace) {\n if (setID) icon.attr('id', id);\n const cl = target.attr('class');\n if (cl) icon.attr('class', 'svg_icon ' + cl);\n if (!target.alt) {\n let alt = 'icon';\n if (opts.alts) {\n alt = opts.alts[id] || alt;\n }\n icon.attr('alt', alt);\n }\n target.replaceWith(icon);\n } else {\n target.append(icon);\n }\n if (isOpera) {\n setTimeout(function () {\n icon.removeAttr('style');\n }, 1);\n }\n }\n\n let holder;\n /**\n * @param {external:jQuery} icon A wrapped `defs` or Image\n * @param {string} id SVG icon ID\n * @returns {void}\n */\n function addIcon (icon, id) {\n if (opts.id_match === undefined || opts.id_match !== false) {\n setIcon(holder, icon, id, true);\n }\n svgIcons[id] = icon;\n }\n\n /**\n *\n * @param {boolean} [toImage]\n * @param {external:jQuery.svgIcons.Fallback} [fallback=false]\n * @returns {void}\n */\n function makeIcons (toImage = false, fallback = false) {\n if (iconsMade) return;\n if (opts.no_img) toImage = false;\n\n let tempHolder;\n if (toImage) {\n tempHolder = $(document.createElement('div'));\n tempHolder.hide().appendTo('body');\n }\n if (fallback) {\n const path = opts.fallback_path || '';\n $.each(fallback, function (id, imgsrc) {\n holder = $('#' + id);\n let alt = 'icon';\n if (opts.alts) {\n alt = opts.alts[id] || alt;\n }\n const icon = $(new Image())\n .attr({\n class: 'svg_icon',\n src: path + imgsrc,\n width: iconW,\n height: iconH,\n alt\n });\n\n addIcon(icon, id);\n });\n } else {\n const len = elems.length;\n for (let i = 0; i < len; i++) {\n const elem = elems[i];\n const {id} = elem;\n if (id === 'svg_eof') break;\n holder = $('#' + id);\n const svgroot = document.createElementNS(svgns, 'svg');\n // Per https://www.w3.org/TR/xml-names11/#defaulting, the namespace for\n // attributes should have no value.\n svgroot.setAttribute('viewBox', [0, 0, iconW, iconH].join(' '));\n\n let svg = elem.getElementsByTagNameNS(svgns, 'svg')[0];\n\n // Make flexible by converting width/height to viewBox\n const w = svg.getAttribute('width');\n const h = svg.getAttribute('height');\n svg.removeAttribute('width');\n svg.removeAttribute('height');\n\n const vb = svg.getAttribute('viewBox');\n if (!vb) {\n svg.setAttribute('viewBox', [0, 0, w, h].join(' '));\n }\n\n // Not using jQuery to be a bit faster\n svgroot.setAttribute('xmlns', svgns);\n svgroot.setAttribute('width', iconW);\n svgroot.setAttribute('height', iconH);\n svgroot.setAttribute('xmlns:xlink', xlinkns);\n svgroot.setAttribute('class', 'svg_icon');\n\n // Without cloning, Firefox will make another GET request.\n // With cloning, causes issue in Opera/Win/Non-EN\n if (!isOpera) svg = svg.cloneNode(true);\n\n svgroot.append(svg);\n\n let icon;\n if (toImage) {\n tempHolder.empty().append(svgroot);\n const str = dataPre + encode64(unescape(encodeURIComponent(\n new XMLSerializer().serializeToString(svgroot)\n )));\n let alt = 'icon';\n if (opts.alts) {\n alt = opts.alts[id] || alt;\n }\n icon = $(new Image())\n .attr({class: 'svg_icon', src: str, alt});\n } else {\n icon = fixIDs($(svgroot), i);\n }\n addIcon(icon, id);\n }\n }\n\n if (opts.placement) {\n $.each(opts.placement, function (sel, id) {\n if (!svgIcons[id]) return;\n $(sel).each(function (i) {\n let copy = svgIcons[id].clone();\n if (i > 0 && !toImage) copy = fixIDs(copy, i, true);\n setIcon($(this), copy, id);\n });\n });\n }\n if (!fallback) {\n if (toImage) tempHolder.remove();\n if (dataEl) dataEl.remove();\n if (testImg) testImg.remove();\n }\n if (opts.resize) $.resizeSvgIcons(opts.resize);\n iconsMade = true;\n\n if (opts.callback) opts.callback(svgIcons);\n }\n\n /**\n * @returns {void}\n */\n function useFallback () {\n if (file.includes('.svgz')) {\n const regFile = file.replace('.svgz', '.svg');\n if (window.console) {\n console.log('.svgz failed, trying with .svg'); // eslint-disable-line no-console\n }\n $.svgIcons(regFile, opts);\n } else if (opts.fallback) {\n makeIcons(false, opts.fallback);\n }\n }\n };\n\n /**\n * @function external:jQuery.getSvgIcon\n * @param {string} id\n * @param {boolean} uniqueClone Whether to clone\n * @returns {external:jQuery} The icon (optionally cloned)\n */\n $.getSvgIcon = function (id, uniqueClone) {\n let icon = svgIcons[id];\n if (uniqueClone && icon) {\n icon = fixIDs(icon, 0, true).clone(true);\n }\n return icon;\n };\n\n /**\n * @typedef {GenericArray} module:jQuerySVGIcons.Dimensions\n * @property {Integer} length 2\n * @property {Float} 0 Width\n * @property {Float} 1 Height\n */\n\n /**\n * If a Float is used, it will represent width and height. Arrays contain\n * the width and height.\n * @typedef {module:jQuerySVGIcons.Dimensions|Float} module:jQuerySVGIcons.Size\n */\n\n /**\n * @function external:jQuery.resizeSvgIcons\n * @param {PlainObject} obj Object with\n * selectors as keys. The values are sizes.\n * @returns {void}\n */\n $.resizeSvgIcons = function (obj) {\n // FF2 and older don't detect .svg_icon, so we change it detect svg elems instead\n const changeSel = !$('.svg_icon:first').length;\n $.each(obj, function (sel, size) {\n const arr = Array.isArray(size);\n const w = arr ? size[0] : size,\n h = arr ? size[1] : size;\n if (changeSel) {\n sel = sel.replace(/\\.svg_icon/g, 'svg');\n }\n $(sel).each(function () {\n this.setAttribute('width', w);\n this.setAttribute('height', h);\n if (window.opera && window.widget) {\n this.parentNode.style.width = w + 'px';\n this.parentNode.style.height = h + 'px';\n }\n });\n });\n };\n return $;\n}\n","/**\n * @file jGraduate 0.4\n *\n * jQuery Plugin for a gradient picker\n *\n * @module jGraduate\n * @copyright 2010 Jeff Schiller {@link http://blog.codedread.com/}, 2010 Alexis Deveria {@link http://a.deveria.com/}\n *\n * @license Apache-2.0\n * @example\n * // The Paint object is described below.\n * $.jGraduate.Paint(); // constructs a 'none' color\n * @example $.jGraduate.Paint({copy: o}); // creates a copy of the paint o\n * @example $.jGraduate.Paint({hex: '#rrggbb'}); // creates a solid color paint with hex = \"#rrggbb\"\n * @example $.jGraduate.Paint({linearGradient: o, a: 50}); // creates a linear gradient paint with opacity=0.5\n * @example $.jGraduate.Paint({radialGradient: o, a: 7}); // creates a radial gradient paint with opacity=0.07\n * @example $.jGraduate.Paint({hex: '#rrggbb', linearGradient: o}); // throws an exception?\n*/\n\n/* eslint-disable jsdoc/require-property */\n/**\n * The jQuery namespace.\n * @external jQuery\n*/\n/**\n * The jQuery plugin namespace.\n * @namespace {PlainObject} fn\n * @memberof external:jQuery\n * @see {@link http://learn.jquery.com/plugins/|jQuery Plugins}\n */\n/* eslint-enable jsdoc/require-property */\n\nconst ns = {\n svg: 'http://www.w3.org/2000/svg',\n xlink: 'http://www.w3.org/1999/xlink'\n};\n\nif (!window.console) {\n window.console = {\n log (str) { /* */ },\n dir (str) { /* */ }\n };\n}\n\n/**\n* Adds {@link external:jQuery.jGraduate.Paint},\n* {@link external:jQuery.fn.jGraduateDefaults},\n* {@link external:jQuery.fn.jGraduate}.\n* @function module:jGraduate.jGraduate\n* @param {external:jQuery} $ The jQuery instance to wrap\n* @returns {external:jQuery}\n*/\nexport default function jQueryPluginJGraduate ($) {\n if (!$.loadingStylesheets) {\n $.loadingStylesheets = [];\n }\n const stylesheet = 'jgraduate/css/jGraduate.css';\n if (!$.loadingStylesheets.includes(stylesheet)) {\n $.loadingStylesheets.push(stylesheet);\n }\n\n /**\n * @typedef {PlainObject} module:jGraduate.jGraduatePaintOptions\n * @property {Float} [alpha]\n * @property {module:jGraduate~Paint} [copy] Copy paint object\n * @property {SVGLinearGradientElement} [linearGradient]\n * @property {SVGRadialGradientElement} [radialGradient]\n * @property {string} [solidColor]\n */\n\n /**\n * @memberof module:jGraduate~\n */\n class Paint {\n /**\n * @param {module:jGraduate.jGraduatePaintOptions} [opt]\n */\n constructor (opt) {\n const options = opt || {};\n this.alpha = isNaN(options.alpha) ? 100 : options.alpha;\n // copy paint object\n if (options.copy) {\n /**\n * @name module:jGraduate~Paint#type\n * @type {\"none\"|\"solidColor\"|\"linearGradient\"|\"radialGradient\"}\n */\n this.type = options.copy.type;\n /**\n * Represents opacity (0-100).\n * @name module:jGraduate~Paint#alpha\n * @type {Float}\n */\n this.alpha = options.copy.alpha;\n /**\n * Represents #RRGGBB hex of color.\n * @name module:jGraduate~Paint#solidColor\n * @type {string}\n */\n this.solidColor = null;\n /**\n * @name module:jGraduate~Paint#linearGradient\n * @type {SVGLinearGradientElement}\n */\n this.linearGradient = null;\n /**\n * @name module:jGraduate~Paint#radialGradient\n * @type {SVGRadialGradientElement}\n */\n this.radialGradient = null;\n\n switch (this.type) {\n case 'none':\n break;\n case 'solidColor':\n this.solidColor = options.copy.solidColor;\n break;\n case 'linearGradient':\n this.linearGradient = options.copy.linearGradient.cloneNode(true);\n break;\n case 'radialGradient':\n this.radialGradient = options.copy.radialGradient.cloneNode(true);\n break;\n }\n // create linear gradient paint\n } else if (options.linearGradient) {\n this.type = 'linearGradient';\n this.solidColor = null;\n this.radialGradient = null;\n this.linearGradient = options.linearGradient.cloneNode(true);\n // create linear gradient paint\n } else if (options.radialGradient) {\n this.type = 'radialGradient';\n this.solidColor = null;\n this.linearGradient = null;\n this.radialGradient = options.radialGradient.cloneNode(true);\n // create solid color paint\n } else if (options.solidColor) {\n this.type = 'solidColor';\n this.solidColor = options.solidColor;\n // create empty paint\n } else {\n this.type = 'none';\n this.solidColor = null;\n this.linearGradient = null;\n this.radialGradient = null;\n }\n }\n }\n\n /* eslint-disable jsdoc/require-property */\n /**\n * @namespace {PlainObject} jGraduate\n * @memberof external:jQuery\n */\n $.jGraduate = /** @lends external:jQuery.jGraduate */ {\n /* eslint-enable jsdoc/require-property */\n /**\n * @class external:jQuery.jGraduate.Paint\n * @see module:jGraduate~Paint\n */\n Paint\n };\n\n // JSDoc doesn't show this as belonging to our `module:jGraduate.Options` type,\n // so we use `@see`\n /**\n * @namespace {module:jGraduate.Options} jGraduateDefaults\n * @memberof external:jQuery.fn\n */\n $.fn.jGraduateDefaults = /** @lends external:jQuery.fn.jGraduateDefaults */ {\n /**\n * Creates an object with a 'none' color.\n * @type {external:jQuery.jGraduate.Paint}\n * @see module:jGraduate.Options\n */\n paint: new $.jGraduate.Paint(),\n /**\n * @namespace\n */\n window: {\n /**\n * @type {string}\n * @see module:jGraduate.Options\n */\n pickerTitle: 'Drag markers to pick a paint'\n },\n /**\n * @namespace\n */\n images: {\n /**\n * @type {string}\n * @see module:jGraduate.Options\n */\n clientPath: 'images/'\n },\n /**\n * @type {string}\n * @see module:jGraduate.Options\n */\n newstop: 'inverse' // same, inverse, black, white\n };\n\n const isGecko = navigator.userAgent.includes('Gecko/');\n\n /**\n * @typedef {PlainObject} module:jGraduate.Attrs\n */\n /**\n * @param {SVGElement} elem\n * @param {module:jGraduate.Attrs} attrs\n * @returns {void}\n */\n function setAttrs (elem, attrs) {\n if (isGecko) {\n Object.entries(attrs).forEach(([aname, val]) => {\n elem.setAttribute(aname, val);\n });\n } else {\n Object.entries(attrs).forEach(([aname, val]) => {\n const prop = elem[aname];\n if (prop && prop.constructor === 'SVGLength') {\n prop.baseVal.value = val;\n } else {\n elem.setAttribute(aname, val);\n }\n });\n }\n }\n\n /**\n * @param {string} name\n * @param {module:jGraduate.Attrs} attrs\n * @param {Element} newparent\n * @returns {SVGElement}\n */\n function mkElem (name, attrs, newparent) {\n const elem = document.createElementNS(ns.svg, name);\n setAttrs(elem, attrs);\n if (newparent) {\n newparent.append(elem);\n }\n return elem;\n }\n\n /**\n * @typedef {PlainObject} module:jGraduate.ColorOpac Object may have one or both values\n * @property {string} [color] #Hex color\n * @property {Float} [opac] 0-1\n */\n /**\n * @typedef {PlainObject} module:jGraduate.Options\n * @property {module:jGraduate~Paint} [paint] A Paint object object describing the paint to display initially; defaults to a new instance without options (defaults to opaque white)\n * @property {external:Window} [window]\n * @property {string} [window.pickerTitle='Drag markers to pick a paint']\n * @property {PlainObject} [images]\n * @property {string} [images.clientPath='images/']\n * @property {\"same\"|\"inverse\"|\"black\"|\"white\"|module:jGraduate.ColorOpac} [newstop=\"inverse\"]\n */\n\n /**\n * @callback external:jQuery.fn.jGraduate.OkCallback\n * @param {external:jQuery.jGraduate.Paint} paint\n * @returns {void}\n */\n /**\n * @callback external:jQuery.fn.jGraduate.CancelCallback\n * @returns {void}\n */\n\n /**\n * @function external:jQuery.fn.jGraduate\n * @param {module:jGraduate.Options} [options]\n * @param {external:jQuery.fn.jGraduate.OkCallback} [okCallback] Called with a Paint object when Ok is pressed\n * @param {external:jQuery.fn.jGraduate.CancelCallback} [cancelCallback] Called with no arguments when Cancel is pressed\n * @returns {external:jQuery}\n */\n $.fn.jGraduate = function (options, okCallback, cancelCallback) {\n return this.each(function () {\n const $this = $(this),\n $settings = $.extend(true, {}, $.fn.jGraduateDefaults, options || {}),\n id = $this.attr('id'),\n idref = '#' + $this.attr('id') + ' ';\n\n if (!idref) {\n /* await */ $.alert('Container element must have an id attribute to maintain unique id strings for sub-elements.');\n return;\n }\n\n const okClicked = function () {\n switch ($this.paint.type) {\n case 'radialGradient':\n $this.paint.linearGradient = null;\n break;\n case 'linearGradient':\n $this.paint.radialGradient = null;\n break;\n case 'solidColor':\n $this.paint.radialGradient = $this.paint.linearGradient = null;\n break;\n }\n typeof $this.okCallback === 'function' && $this.okCallback($this.paint);\n $this.hide();\n };\n const cancelClicked = function () {\n typeof $this.cancelCallback === 'function' && $this.cancelCallback();\n $this.hide();\n };\n\n $.extend(\n true,\n $this,\n // public properties, methods, and callbacks\n {\n // make a copy of the incoming paint\n paint: new $.jGraduate.Paint({copy: $settings.paint}),\n okCallback: typeof okCallback === 'function' ? okCallback : null,\n cancelCallback: typeof cancelCallback === 'function' ? cancelCallback : null\n }\n );\n\n let // pos = $this.position(),\n color = null;\n const $win = $(window);\n\n if ($this.paint.type === 'none') {\n $this.paint = new $.jGraduate.Paint({solidColor: 'ffffff'});\n }\n\n $this.addClass('jGraduate_Picker');\n $this.html(\n '
    ' +\n '
  • Solid Color
  • ' +\n '
  • Linear Gradient
  • ' +\n '
  • Radial Gradient
  • ' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
'\n );\n const colPicker = $(idref + '> .jGraduate_colPick');\n const gradPicker = $(idref + '> .jGraduate_gradPick');\n\n gradPicker.html(\n '
' +\n '

' + $settings.window.pickerTitle + '

' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '' +\n '' +\n '' +\n '
' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '' +\n '' +\n '' +\n '
' +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '' +\n '' +\n '' +\n '
' +\n '
' +\n '
' +\n '' +\n '
' +\n '
' +\n '' +\n '' +\n '' +\n '' +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '
' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '' +\n '
' +\n '
' +\n '
' +\n '' +\n '' +\n '
'\n );\n\n // --------------\n // Set up all the SVG elements (the gradient, stops and rectangle)\n const MAX = 256,\n MARGINX = 0,\n MARGINY = 0,\n // STOP_RADIUS = 15 / 2,\n SIZEX = MAX - 2 * MARGINX,\n SIZEY = MAX - 2 * MARGINY;\n\n const attrInput = {};\n\n const SLIDERW = 145;\n $('.jGraduate_SliderBar').width(SLIDERW);\n\n const container = $('#' + id + '_jGraduate_GradContainer')[0];\n\n const svg = mkElem('svg', {\n id: id + '_jgraduate_svg',\n width: MAX,\n height: MAX,\n xmlns: ns.svg\n }, container);\n\n // This wasn't working as designed\n // let curType;\n // curType = curType || $this.paint.type;\n\n // if we are sent a gradient, import it\n let curType = $this.paint.type;\n\n let grad = $this.paint[curType];\n let curGradient = grad;\n\n const gradalpha = $this.paint.alpha;\n\n const isSolid = curType === 'solidColor';\n\n // Make any missing gradients\n switch (curType) {\n case 'solidColor':\n // fall through\n case 'linearGradient':\n if (!isSolid) {\n curGradient.id = id + '_lg_jgraduate_grad';\n grad = curGradient = svg.appendChild(curGradient); // .cloneNode(true));\n }\n mkElem('radialGradient', {\n id: id + '_rg_jgraduate_grad'\n }, svg);\n if (curType === 'linearGradient') { break; }\n // fall through\n case 'radialGradient':\n if (!isSolid) {\n curGradient.id = id + '_rg_jgraduate_grad';\n grad = curGradient = svg.appendChild(curGradient); // .cloneNode(true));\n }\n mkElem('linearGradient', {\n id: id + '_lg_jgraduate_grad'\n }, svg);\n }\n\n let stopGroup; // eslint-disable-line prefer-const\n if (isSolid) {\n grad = curGradient = $('#' + id + '_lg_jgraduate_grad')[0];\n color = $this.paint[curType];\n mkStop(0, '#' + color, 1);\n\n const type = typeof $settings.newstop;\n\n if (type === 'string') {\n switch ($settings.newstop) {\n case 'same':\n mkStop(1, '#' + color, 1);\n break;\n\n case 'inverse': {\n // Invert current color for second stop\n let inverted = '';\n for (let i = 0; i < 6; i += 2) {\n // const ch = color.substr(i, 2);\n let inv = (255 - parseInt(color.substr(i, 2), 16)).toString(16);\n if (inv.length < 2) inv = 0 + inv;\n inverted += inv;\n }\n mkStop(1, '#' + inverted, 1);\n break;\n } case 'white':\n mkStop(1, '#ffffff', 1);\n break;\n\n case 'black':\n mkStop(1, '#000000', 1);\n break;\n }\n } else if (type === 'object') {\n const opac = ('opac' in $settings.newstop) ? $settings.newstop.opac : 1;\n mkStop(1, ($settings.newstop.color || '#' + color), opac);\n }\n }\n\n const x1 = parseFloat(grad.getAttribute('x1') || 0.0),\n y1 = parseFloat(grad.getAttribute('y1') || 0.0),\n x2 = parseFloat(grad.getAttribute('x2') || 1.0),\n y2 = parseFloat(grad.getAttribute('y2') || 0.0);\n\n const cx = parseFloat(grad.getAttribute('cx') || 0.5),\n cy = parseFloat(grad.getAttribute('cy') || 0.5),\n fx = parseFloat(grad.getAttribute('fx') || cx),\n fy = parseFloat(grad.getAttribute('fy') || cy);\n\n const previewRect = mkElem('rect', {\n id: id + '_jgraduate_rect',\n x: MARGINX,\n y: MARGINY,\n width: SIZEX,\n height: SIZEY,\n fill: 'url(#' + id + '_jgraduate_grad)',\n 'fill-opacity': gradalpha / 100\n }, svg);\n\n // stop visuals created here\n const beginCoord = $('
').attr({\n class: 'grad_coord jGraduate_lg_field',\n title: 'Begin Stop'\n }).text(1).css({\n top: y1 * MAX,\n left: x1 * MAX\n }).data('coord', 'start').appendTo(container);\n\n const endCoord = beginCoord.clone().text(2).css({\n top: y2 * MAX,\n left: x2 * MAX\n }).attr('title', 'End stop').data('coord', 'end').appendTo(container);\n\n const centerCoord = $('
').attr({\n class: 'grad_coord jGraduate_rg_field',\n title: 'Center stop'\n }).text('C').css({\n top: cy * MAX,\n left: cx * MAX\n }).data('coord', 'center').appendTo(container);\n\n const focusCoord = centerCoord.clone().text('F').css({\n top: fy * MAX,\n left: fx * MAX,\n display: 'none'\n }).attr('title', 'Focus point').data('coord', 'focus').appendTo(container);\n\n focusCoord[0].id = id + '_jGraduate_focusCoord';\n\n // const coords = $(idref + ' .grad_coord');\n\n // $(container).hover(function () {\n // coords.animate({\n // opacity: 1\n // }, 500);\n // }, function () {\n // coords.animate({\n // opacity: .2\n // }, 500);\n // });\n\n let showFocus;\n $.each(['x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy'], function (i, attr) {\n const isRadial = isNaN(attr[1]);\n\n let attrval = curGradient.getAttribute(attr);\n if (!attrval) {\n // Set defaults\n if (isRadial) {\n // For radial points\n attrval = '0.5';\n } else {\n // Only x2 is 1\n attrval = attr === 'x2' ? '1.0' : '0.0';\n }\n }\n\n attrInput[attr] = $('#' + id + '_jGraduate_' + attr)\n .val(attrval)\n .change(function () {\n // TODO: Support values < 0 and > 1 (zoomable preview?)\n if (isNaN(parseFloat(this.value)) || this.value < 0) {\n this.value = 0.0;\n } else if (this.value > 1) {\n this.value = 1.0;\n }\n\n if (!(attr[0] === 'f' && !showFocus)) {\n if ((isRadial && curType === 'radialGradient') || (!isRadial && curType === 'linearGradient')) {\n curGradient.setAttribute(attr, this.value);\n }\n }\n\n const $elem = isRadial\n ? attr[0] === 'c' ? centerCoord : focusCoord\n : attr[1] === '1' ? beginCoord : endCoord;\n\n const cssName = attr.includes('x') ? 'left' : 'top';\n\n $elem.css(cssName, this.value * MAX);\n }).change();\n });\n\n /**\n *\n * @param {Float} n\n * @param {Float|string} colr\n * @param {Float} opac\n * @param {boolean} [sel]\n * @param {SVGStopElement} [stopElem]\n * @returns {SVGStopElement}\n */\n function mkStop (n, colr, opac, sel, stopElem) {\n const stop = stopElem || mkElem('stop', {\n 'stop-color': colr,\n 'stop-opacity': opac,\n offset: n\n }, curGradient);\n if (stopElem) {\n colr = stopElem.getAttribute('stop-color');\n opac = stopElem.getAttribute('stop-opacity');\n n = stopElem.getAttribute('offset');\n } else {\n curGradient.append(stop);\n }\n if (opac === null) opac = 1;\n\n const pickerD = 'M-6.2,0.9c3.6-4,6.7-4.3,6.7-12.4c-0.2,7.9,3.1,8.8,6.5,12.4c3.5,3.8,2.9,9.6,0,12.3c-3.1,2.8-10.4,2.7-13.2,0C-9.6,9.9-9.4,4.4-6.2,0.9z';\n\n const pathbg = mkElem('path', {\n d: pickerD,\n fill: 'url(#jGraduate_trans)',\n transform: 'translate(' + (10 + n * MAX) + ', 26)'\n }, stopGroup);\n\n const path = mkElem('path', {\n d: pickerD,\n fill: colr,\n 'fill-opacity': opac,\n transform: 'translate(' + (10 + n * MAX) + ', 26)',\n stroke: '#000',\n 'stroke-width': 1.5\n }, stopGroup);\n\n $(path).mousedown(function (e) {\n selectStop(this);\n drag = curStop;\n $win.mousemove(dragColor).mouseup(remDrags);\n stopOffset = stopMakerDiv.offset();\n e.preventDefault();\n return false;\n }).data('stop', stop).data('bg', pathbg).dblclick(function () {\n $('div.jGraduate_LightBox').show();\n const colorhandle = this; // eslint-disable-line consistent-this\n let stopOpacity = Number(stop.getAttribute('stop-opacity')) || 1;\n let stopColor = stop.getAttribute('stop-color') || 1;\n let thisAlpha = (parseFloat(stopOpacity) * 255).toString(16);\n while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }\n colr = stopColor.substr(1) + thisAlpha;\n $('#' + id + '_jGraduate_stopPicker').css({left: 100, bottom: 15}).jPicker({\n window: {title: 'Pick the start color and opacity for the gradient'},\n images: {clientPath: $settings.images.clientPath},\n color: {active: colr, alphaSupport: true}\n }, function (clr, arg2) {\n stopColor = clr.val('hex') ? ('#' + clr.val('hex')) : 'none';\n stopOpacity = clr.val('a') !== null ? clr.val('a') / 256 : 1;\n colorhandle.setAttribute('fill', stopColor);\n colorhandle.setAttribute('fill-opacity', stopOpacity);\n stop.setAttribute('stop-color', stopColor);\n stop.setAttribute('stop-opacity', stopOpacity);\n $('div.jGraduate_LightBox').hide();\n $('#' + id + '_jGraduate_stopPicker').hide();\n }, null, function () {\n $('div.jGraduate_LightBox').hide();\n $('#' + id + '_jGraduate_stopPicker').hide();\n });\n });\n\n $(curGradient).find('stop').each(function () {\n const curS = $(this);\n if (Number(this.getAttribute('offset')) > n) {\n if (!colr) {\n const newcolor = this.getAttribute('stop-color');\n const newopac = this.getAttribute('stop-opacity');\n stop.setAttribute('stop-color', newcolor);\n path.setAttribute('fill', newcolor);\n stop.setAttribute('stop-opacity', newopac === null ? 1 : newopac);\n path.setAttribute('fill-opacity', newopac === null ? 1 : newopac);\n }\n curS.before(stop);\n return false;\n }\n return true;\n });\n if (sel) selectStop(path);\n return stop;\n }\n\n /**\n *\n * @returns {void}\n */\n function remStop () {\n delStop.setAttribute('display', 'none');\n const path = $(curStop);\n const stop = path.data('stop');\n const bg = path.data('bg');\n $([curStop, stop, bg]).remove();\n }\n\n const stopMakerDiv = $('#' + id + '_jGraduate_StopSlider');\n\n let stops, curStop, drag;\n\n const delStop = mkElem('path', {\n d: 'm9.75,-6l-19.5,19.5m0,-19.5l19.5,19.5',\n fill: 'none',\n stroke: '#D00',\n 'stroke-width': 5,\n display: 'none'\n }, undefined); // stopMakerSVG);\n\n /**\n * @param {Element} item\n * @returns {void}\n */\n function selectStop (item) {\n if (curStop) curStop.setAttribute('stroke', '#000');\n item.setAttribute('stroke', 'blue');\n curStop = item;\n // stops = $('stop');\n // opac_select.val(curStop.attr('fill-opacity') || 1);\n // root.append(delStop);\n }\n\n let stopOffset;\n\n /**\n *\n * @returns {void}\n */\n function remDrags () {\n $win.unbind('mousemove', dragColor);\n if (delStop.getAttribute('display') !== 'none') {\n remStop();\n }\n drag = null;\n }\n\n let scaleX = 1, scaleY = 1, angle = 0;\n\n let cX = cx;\n let cY = cy;\n /**\n *\n * @returns {void}\n */\n function xform () {\n const rot = angle ? 'rotate(' + angle + ',' + cX + ',' + cY + ') ' : '';\n if (scaleX === 1 && scaleY === 1) {\n curGradient.removeAttribute('gradientTransform');\n // $('#ang').addClass('dis');\n } else {\n const x = -cX * (scaleX - 1);\n const y = -cY * (scaleY - 1);\n curGradient.setAttribute('gradientTransform', rot + 'translate(' + x + ',' + y + ') scale(' + scaleX + ',' + scaleY + ')');\n // $('#ang').removeClass('dis');\n }\n }\n\n /**\n * @param {Event} evt\n * @returns {void}\n */\n function dragColor (evt) {\n let x = evt.pageX - stopOffset.left;\n const y = evt.pageY - stopOffset.top;\n x = x < 10\n ? 10\n : x > MAX + 10\n ? MAX + 10\n : x;\n\n const xfStr = 'translate(' + x + ', 26)';\n if (y < -60 || y > 130) {\n delStop.setAttribute('display', 'block');\n delStop.setAttribute('transform', xfStr);\n } else {\n delStop.setAttribute('display', 'none');\n }\n\n drag.setAttribute('transform', xfStr);\n $.data(drag, 'bg').setAttribute('transform', xfStr);\n const stop = $.data(drag, 'stop');\n const sX = (x - 10) / MAX;\n\n stop.setAttribute('offset', sX);\n\n let last = 0;\n $(curGradient).find('stop').each(function (i) {\n const cur = this.getAttribute('offset');\n const t = $(this);\n if (cur < last) {\n t.prev().before(t);\n stops = $(curGradient).find('stop');\n }\n last = cur;\n });\n }\n\n const stopMakerSVG = mkElem('svg', {\n width: '100%',\n height: 45\n }, stopMakerDiv[0]);\n\n const transPattern = mkElem('pattern', {\n width: 16,\n height: 16,\n patternUnits: 'userSpaceOnUse',\n id: 'jGraduate_trans'\n }, stopMakerSVG);\n\n const transImg = mkElem('image', {\n width: 16,\n height: 16\n }, transPattern);\n\n const bgImage = $settings.images.clientPath + 'map-opacity.png';\n\n transImg.setAttributeNS(ns.xlink, 'xlink:href', bgImage);\n\n $(stopMakerSVG).click(function (evt) {\n stopOffset = stopMakerDiv.offset();\n const {target} = evt;\n if (target.tagName === 'path') return;\n let x = evt.pageX - stopOffset.left - 8;\n x = x < 10 ? 10 : x > MAX + 10 ? MAX + 10 : x;\n mkStop(x / MAX, 0, 0, true);\n evt.stopPropagation();\n });\n\n $(stopMakerSVG).mouseover(function () {\n stopMakerSVG.append(delStop);\n });\n\n stopGroup = mkElem('g', {}, stopMakerSVG);\n\n mkElem('line', {\n x1: 10,\n y1: 15,\n x2: MAX + 10,\n y2: 15,\n 'stroke-width': 2,\n stroke: '#000'\n }, stopMakerSVG);\n\n const spreadMethodOpt = gradPicker.find('.jGraduate_spreadMethod').change(function () {\n curGradient.setAttribute('spreadMethod', $(this).val());\n });\n\n // handle dragging the stop around the swatch\n let draggingCoord = null;\n\n const onCoordDrag = function (evt) {\n let x = evt.pageX - offset.left;\n let y = evt.pageY - offset.top;\n\n // clamp stop to the swatch\n x = x < 0 ? 0 : x > MAX ? MAX : x;\n y = y < 0 ? 0 : y > MAX ? MAX : y;\n\n draggingCoord.css('left', x).css('top', y);\n\n // calculate stop offset\n const fracx = x / SIZEX;\n const fracy = y / SIZEY;\n\n const type = draggingCoord.data('coord');\n const grd = curGradient;\n\n switch (type) {\n case 'start':\n attrInput.x1.val(fracx);\n attrInput.y1.val(fracy);\n grd.setAttribute('x1', fracx);\n grd.setAttribute('y1', fracy);\n break;\n case 'end':\n attrInput.x2.val(fracx);\n attrInput.y2.val(fracy);\n grd.setAttribute('x2', fracx);\n grd.setAttribute('y2', fracy);\n break;\n case 'center':\n attrInput.cx.val(fracx);\n attrInput.cy.val(fracy);\n grd.setAttribute('cx', fracx);\n grd.setAttribute('cy', fracy);\n cX = fracx;\n cY = fracy;\n xform();\n break;\n case 'focus':\n attrInput.fx.val(fracx);\n attrInput.fy.val(fracy);\n grd.setAttribute('fx', fracx);\n grd.setAttribute('fy', fracy);\n xform();\n }\n\n evt.preventDefault();\n };\n\n const onCoordUp = function () {\n draggingCoord = null;\n $win.unbind('mousemove', onCoordDrag).unbind('mouseup', onCoordUp);\n };\n\n // Linear gradient\n // (function () {\n\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n\n let numstops = stops.length;\n // if there are not at least two stops, then\n if (numstops < 2) {\n while (numstops < 2) {\n curGradient.append(document.createElementNS(ns.svg, 'stop'));\n ++numstops;\n }\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n }\n\n for (let i = 0; i < numstops; i++) {\n mkStop(0, 0, 0, 0, stops[i]);\n }\n\n spreadMethodOpt.val(curGradient.getAttribute('spreadMethod') || 'pad');\n\n let offset;\n\n // No match, so show focus point\n showFocus = false;\n\n previewRect.setAttribute('fill-opacity', gradalpha / 100);\n\n $('#' + id + ' div.grad_coord').mousedown(function (evt) {\n evt.preventDefault();\n draggingCoord = $(this);\n // const sPos = draggingCoord.offset();\n offset = draggingCoord.parent().offset();\n $win.mousemove(onCoordDrag).mouseup(onCoordUp);\n });\n\n // bind GUI elements\n $('#' + id + '_jGraduate_Ok').bind('click', function () {\n $this.paint.type = curType;\n $this.paint[curType] = curGradient.cloneNode(true);\n $this.paint.solidColor = null;\n okClicked();\n });\n $('#' + id + '_jGraduate_Cancel').bind('click', function (paint) {\n cancelClicked();\n });\n\n if (curType === 'radialGradient') {\n if (showFocus) {\n focusCoord.show();\n } else {\n focusCoord.hide();\n attrInput.fx.val('');\n attrInput.fy.val('');\n }\n }\n\n $('#' + id + '_jGraduate_match_ctr')[0].checked = !showFocus;\n\n let lastfx, lastfy;\n\n $('#' + id + '_jGraduate_match_ctr').change(function () {\n showFocus = !this.checked;\n focusCoord.toggle(showFocus);\n attrInput.fx.val('');\n attrInput.fy.val('');\n const grd = curGradient;\n if (!showFocus) {\n lastfx = grd.getAttribute('fx');\n lastfy = grd.getAttribute('fy');\n grd.removeAttribute('fx');\n grd.removeAttribute('fy');\n } else {\n const fX = lastfx || 0.5;\n const fY = lastfy || 0.5;\n grd.setAttribute('fx', fX);\n grd.setAttribute('fy', fY);\n attrInput.fx.val(fX);\n attrInput.fy.val(fY);\n }\n });\n\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n numstops = stops.length;\n // if there are not at least two stops, then\n if (numstops < 2) {\n while (numstops < 2) {\n curGradient.append(document.createElementNS(ns.svg, 'stop'));\n ++numstops;\n }\n stops = curGradient.getElementsByTagNameNS(ns.svg, 'stop');\n }\n\n let slider;\n\n const setSlider = function (e) {\n const {offset: {left}} = slider;\n const div = slider.parent;\n let x = (e.pageX - left - parseInt(div.css('border-left-width')));\n if (x > SLIDERW) x = SLIDERW;\n if (x <= 0) x = 0;\n const posx = x - 5;\n x /= SLIDERW;\n\n switch (slider.type) {\n case 'radius':\n x = (x * 2) ** 2.5;\n if (x > 0.98 && x < 1.02) x = 1;\n if (x <= 0.01) x = 0.01;\n curGradient.setAttribute('r', x);\n break;\n case 'opacity':\n $this.paint.alpha = parseInt(x * 100);\n previewRect.setAttribute('fill-opacity', x);\n break;\n case 'ellip':\n scaleX = 1;\n scaleY = 1;\n if (x < 0.5) {\n x /= 0.5; // 0.001\n scaleX = x <= 0 ? 0.01 : x;\n } else if (x > 0.5) {\n x /= 0.5; // 2\n x = 2 - x;\n scaleY = x <= 0 ? 0.01 : x;\n }\n xform();\n x -= 1;\n if (scaleY === x + 1) {\n x = Math.abs(x);\n }\n break;\n case 'angle':\n x -= 0.5;\n angle = x *= 180;\n xform();\n x /= 100;\n break;\n }\n slider.elem.css({'margin-left': posx});\n x = Math.round(x * 100);\n slider.input.val(x);\n };\n\n let ellipVal = 0, angleVal = 0;\n\n if (curType === 'radialGradient') {\n const tlist = curGradient.gradientTransform.baseVal;\n if (tlist.numberOfItems === 2) {\n const t = tlist.getItem(0);\n const s = tlist.getItem(1);\n if (t.type === 2 && s.type === 3) {\n const m = s.matrix;\n if (m.a !== 1) {\n ellipVal = Math.round(-(1 - m.a) * 100);\n } else if (m.d !== 1) {\n ellipVal = Math.round((1 - m.d) * 100);\n }\n }\n } else if (tlist.numberOfItems === 3) {\n // Assume [R][T][S]\n const r = tlist.getItem(0);\n const t = tlist.getItem(1);\n const s = tlist.getItem(2);\n\n if (r.type === 4 &&\n t.type === 2 &&\n s.type === 3\n ) {\n angleVal = Math.round(r.angle);\n const m = s.matrix;\n if (m.a !== 1) {\n ellipVal = Math.round(-(1 - m.a) * 100);\n } else if (m.d !== 1) {\n ellipVal = Math.round((1 - m.d) * 100);\n }\n }\n }\n }\n\n const sliders = {\n radius: {\n handle: '#' + id + '_jGraduate_RadiusArrows',\n input: '#' + id + '_jGraduate_RadiusInput',\n val: (curGradient.getAttribute('r') || 0.5) * 100\n },\n opacity: {\n handle: '#' + id + '_jGraduate_OpacArrows',\n input: '#' + id + '_jGraduate_OpacInput',\n val: $this.paint.alpha || 100\n },\n ellip: {\n handle: '#' + id + '_jGraduate_EllipArrows',\n input: '#' + id + '_jGraduate_EllipInput',\n val: ellipVal\n },\n angle: {\n handle: '#' + id + '_jGraduate_AngleArrows',\n input: '#' + id + '_jGraduate_AngleInput',\n val: angleVal\n }\n };\n\n $.each(sliders, function (type, data) {\n const handle = $(data.handle);\n handle.mousedown(function (evt) {\n const parent = handle.parent();\n slider = {\n type,\n elem: handle,\n input: $(data.input),\n parent,\n offset: parent.offset()\n };\n $win.mousemove(dragSlider).mouseup(stopSlider);\n evt.preventDefault();\n });\n\n $(data.input).val(data.val).change(function () {\n const isRad = curType === 'radialGradient';\n let val = Number(this.value);\n let xpos = 0;\n switch (type) {\n case 'radius':\n if (isRad) curGradient.setAttribute('r', val / 100);\n xpos = (((val / 100) ** (1 / 2.5)) / 2) * SLIDERW;\n break;\n\n case 'opacity':\n $this.paint.alpha = val;\n previewRect.setAttribute('fill-opacity', val / 100);\n xpos = val * (SLIDERW / 100);\n break;\n\n case 'ellip':\n scaleX = scaleY = 1;\n if (val === 0) {\n xpos = SLIDERW * 0.5;\n break;\n }\n if (val > 99.5) val = 99.5;\n if (val > 0) {\n scaleY = 1 - (val / 100);\n } else {\n scaleX = -(val / 100) - 1;\n }\n\n xpos = SLIDERW * ((val + 100) / 2) / 100;\n if (isRad) xform();\n break;\n\n case 'angle':\n angle = val;\n xpos = angle / 180;\n xpos += 0.5;\n xpos *= SLIDERW;\n if (isRad) xform();\n }\n if (xpos > SLIDERW) {\n xpos = SLIDERW;\n } else if (xpos < 0) {\n xpos = 0;\n }\n handle.css({'margin-left': xpos - 5});\n }).change();\n });\n\n const dragSlider = function (evt) {\n setSlider(evt);\n evt.preventDefault();\n };\n\n const stopSlider = function (evt) {\n $win.unbind('mousemove', dragSlider).unbind('mouseup', stopSlider);\n slider = null;\n };\n\n // --------------\n let thisAlpha = ($this.paint.alpha * 255 / 100).toString(16);\n while (thisAlpha.length < 2) { thisAlpha = '0' + thisAlpha; }\n thisAlpha = thisAlpha.split('.')[0];\n color = $this.paint.solidColor === 'none' ? '' : $this.paint.solidColor + thisAlpha;\n\n if (!isSolid) {\n color = stops[0].getAttribute('stop-color');\n }\n\n // This should be done somewhere else, probably\n $.extend($.fn.jPicker.defaults.window, {\n alphaSupport: true, effects: {type: 'show', speed: 0}\n });\n\n colPicker.jPicker(\n {\n window: {title: $settings.window.pickerTitle},\n images: {clientPath: $settings.images.clientPath},\n color: {active: color, alphaSupport: true}\n },\n function (clr) {\n $this.paint.type = 'solidColor';\n $this.paint.alpha = clr.val('ahex') ? Math.round((clr.val('a') / 255) * 100) : 100;\n $this.paint.solidColor = clr.val('hex') ? clr.val('hex') : 'none';\n $this.paint.radialGradient = null;\n okClicked();\n },\n null,\n function () { cancelClicked(); }\n );\n\n const tabs = $(idref + ' .jGraduate_tabs li');\n tabs.click(function () {\n tabs.removeClass('jGraduate_tab_current');\n $(this).addClass('jGraduate_tab_current');\n $(idref + ' > div').hide();\n const type = $(this).attr('data-type');\n /* const container = */ $(idref + ' .jGraduate_gradPick').show();\n if (type === 'rg' || type === 'lg') {\n // Show/hide appropriate fields\n $('.jGraduate_' + type + '_field').show();\n $('.jGraduate_' + (type === 'lg' ? 'rg' : 'lg') + '_field').hide();\n\n $('#' + id + '_jgraduate_rect')[0].setAttribute('fill', 'url(#' + id + '_' + type + '_jgraduate_grad)');\n\n // Copy stops\n\n curType = type === 'lg' ? 'linearGradient' : 'radialGradient';\n\n $('#' + id + '_jGraduate_OpacInput').val($this.paint.alpha).change();\n\n const newGrad = $('#' + id + '_' + type + '_jgraduate_grad')[0];\n\n if (curGradient !== newGrad) {\n const curStops = $(curGradient).find('stop');\n $(newGrad).empty().append(curStops);\n curGradient = newGrad;\n const sm = spreadMethodOpt.val();\n curGradient.setAttribute('spreadMethod', sm);\n }\n showFocus = type === 'rg' && curGradient.getAttribute('fx') !== null && !(cx === fx && cy === fy);\n $('#' + id + '_jGraduate_focusCoord').toggle(showFocus);\n if (showFocus) {\n $('#' + id + '_jGraduate_match_ctr')[0].checked = false;\n }\n } else {\n $(idref + ' .jGraduate_gradPick').hide();\n $(idref + ' .jGraduate_colPick').show();\n }\n });\n $(idref + ' > div').hide();\n tabs.removeClass('jGraduate_tab_current');\n let tab;\n switch ($this.paint.type) {\n case 'linearGradient':\n tab = $(idref + ' .jGraduate_tab_lingrad');\n break;\n case 'radialGradient':\n tab = $(idref + ' .jGraduate_tab_radgrad');\n break;\n default:\n tab = $(idref + ' .jGraduate_tab_color');\n break;\n }\n $this.show();\n\n // jPicker will try to show after a 0ms timeout, so need to fire this after that\n setTimeout(() => {\n tab.addClass('jGraduate_tab_current').click();\n }, 10);\n });\n };\n return $;\n}\n","/* eslint-disable no-bitwise */\n/**\n * @file jPicker (Adapted from version 1.1.6)\n *\n * jQuery Plugin for Photoshop style color picker\n *\n * @module jPicker\n * @copyright (c) 2010 Christopher T. Tillman\n * Digital Magic Productions, Inc. ({@link http://www.digitalmagicpro.com/})\n * FREE to use, alter, copy, sell, and especially ENHANCE\n * @license MIT\n *\n * Painstakingly ported from John Dyers' excellent work on his own color picker based on the Prototype framework.\n *\n * John Dyers' website: {@link http://johndyer.name}\n * Color Picker page: {@link http://johndyer.name/photoshop-like-javascript-color-picker/}\n */\n\n/**\n* @external Math\n*/\n/**\n* @memberof external:Math\n* @param {Float} value\n* @param {Float} precision\n* @returns {Float}\n*/\nfunction toFixedNumeric (value, precision) {\n if (precision === undefined) precision = 0;\n return Math.round(value * (10 ** precision)) / (10 ** precision);\n}\n\n/**\n * Whether a value is `null` or `undefined`.\n * @param {any} val\n * @returns {boolean}\n */\nconst isNullish = (val) => {\n return val === null || val === undefined;\n};\n\n/**\n* @function module:jPicker.jPicker\n* @param {external:jQuery} $ The jQuery object to wrap (with {@link external:jQuery.loadingStylesheets}, {@link external:jQuery.fn.$.fn.jPicker}, {@link external:jQuery.fn.$.fn.jPicker.defaults})\n* @returns {external:jQuery}\n*/\nconst jPicker = function ($) {\n if (!$.loadingStylesheets) {\n /**\n * @name loadingStylesheets\n * @type {string[]}\n * @memberof external:jQuery\n */\n $.loadingStylesheets = [];\n }\n const stylesheet = 'jgraduate/css/jPicker.css';\n if (!$.loadingStylesheets.includes(stylesheet)) {\n $.loadingStylesheets.push(stylesheet);\n }\n /**\n * @typedef {PlainObject} module:jPicker.SliderOptions\n * @property {external:jQuery|PlainObject} arrow\n * @property {string} arrow.image Not in use?\n * @property {Float} arrow.width\n * @property {Float} arrow.height\n * @property {PlainObject} map\n * @property {Float} map.width\n * @property {Float} map.height\n */\n\n /**\n * Encapsulate slider functionality for the ColorMap and ColorBar -\n * could be useful to use a jQuery UI draggable for this with certain extensions.\n * @memberof module:jPicker\n * @param {external:jQuery} bar\n * @param {module:jPicker.SliderOptions} options\n * @returns {void}\n */\n class Slider {\n constructor (bar, options) {\n const that = this;\n /**\n * Fire events on the supplied `context`\n * @param {module:jPicker.JPickerInit} context\n * @returns {void}\n */\n function fireChangeEvents (context) {\n changeEvents.forEach((changeEvent) => {\n changeEvent.call(that, that, context);\n });\n }\n\n /**\n * Bind the mousedown to the bar not the arrow for quick snapping to the clicked location.\n * @param {external:jQuery.Event} e\n * @returns {void}\n */\n function mouseDown (e) {\n const off = bar.offset();\n offset = {l: off.left | 0, t: off.top | 0};\n clearTimeout(timeout);\n // using setTimeout for visual updates - once the style is updated the browser will re-render internally allowing the next Javascript to run\n timeout = setTimeout(function () {\n setValuesFromMousePosition.call(that, e);\n }, 0);\n // Bind mousemove and mouseup event to the document so it responds when dragged of of the bar - we will unbind these when on mouseup to save processing\n $(document).bind('mousemove', mouseMove).bind('mouseup', mouseUp);\n e.preventDefault(); // don't try to select anything or drag the image to the desktop\n }\n /**\n * Set the values as the mouse moves.\n * @param {external:jQuery.Event} e\n * @returns {false}\n */\n function mouseMove (e) {\n clearTimeout(timeout);\n timeout = setTimeout(function () {\n setValuesFromMousePosition.call(that, e);\n }, 0);\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n /**\n * Unbind the document events - they aren't needed when not dragging.\n * @param {external:jQuery.Event} e\n * @returns {false}\n */\n function mouseUp (e) {\n $(document).unbind('mouseup', mouseUp).unbind('mousemove', mouseMove);\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n\n /**\n * Calculate mouse position and set value within the current range.\n * @param {Event} e\n * @returns {void}\n */\n function setValuesFromMousePosition (e) {\n const barW = bar.w, // local copies for YUI compressor\n barH = bar.h;\n let locX = e.pageX - offset.l,\n locY = e.pageY - offset.t;\n // keep the arrow within the bounds of the bar\n if (locX < 0) locX = 0;\n else if (locX > barW) locX = barW;\n if (locY < 0) locY = 0;\n else if (locY > barH) locY = barH;\n val.call(that, 'xy', {\n x: ((locX / barW) * rangeX) + minX,\n y: ((locY / barH) * rangeY) + minY\n });\n }\n /**\n *\n * @returns {void}\n */\n function draw () {\n const\n barW = bar.w,\n barH = bar.h,\n arrowW = arrow.w,\n arrowH = arrow.h;\n let arrowOffsetX = 0,\n arrowOffsetY = 0;\n setTimeout(function () {\n if (rangeX > 0) { // range is greater than zero\n // constrain to bounds\n if (x === maxX) arrowOffsetX = barW;\n else arrowOffsetX = ((x / rangeX) * barW) | 0;\n }\n if (rangeY > 0) { // range is greater than zero\n // constrain to bounds\n if (y === maxY) arrowOffsetY = barH;\n else arrowOffsetY = ((y / rangeY) * barH) | 0;\n }\n // if arrow width is greater than bar width, center arrow and prevent horizontal dragging\n if (arrowW >= barW) arrowOffsetX = (barW >> 1) - (arrowW >> 1); // number >> 1 - superfast bitwise divide by two and truncate (move bits over one bit discarding lowest)\n else arrowOffsetX -= arrowW >> 1;\n // if arrow height is greater than bar height, center arrow and prevent vertical dragging\n if (arrowH >= barH) arrowOffsetY = (barH >> 1) - (arrowH >> 1);\n else arrowOffsetY -= arrowH >> 1;\n // set the arrow position based on these offsets\n arrow.css({left: arrowOffsetX + 'px', top: arrowOffsetY + 'px'});\n });\n }\n\n /**\n * Get or set a value.\n * @param {?(\"xy\"|\"x\"|\"y\")} name\n * @param {module:math.XYObject} value\n * @param {module:jPicker.Slider} context\n * @returns {module:math.XYObject|Float|void}\n */\n function val (name, value, context) {\n const set = value !== undefined;\n if (!set) {\n if (isNullish(name)) name = 'xy';\n switch (name.toLowerCase()) {\n case 'x': return x;\n case 'y': return y;\n case 'xy':\n default: return {x, y};\n }\n }\n if (!isNullish(context) && context === that) return undefined;\n let changed = false;\n\n let newX, newY;\n if (isNullish(name)) name = 'xy';\n switch (name.toLowerCase()) {\n case 'x':\n newX = (value && ((value.x && value.x | 0) || value | 0)) || 0;\n break;\n case 'y':\n newY = (value && ((value.y && value.y | 0) || value | 0)) || 0;\n break;\n case 'xy':\n default:\n newX = (value && value.x && value.x | 0) || 0;\n newY = (value && value.y && value.y | 0) || 0;\n break;\n }\n if (!isNullish(newX)) {\n if (newX < minX) newX = minX;\n else if (newX > maxX) newX = maxX;\n if (x !== newX) {\n x = newX;\n changed = true;\n }\n }\n if (!isNullish(newY)) {\n if (newY < minY) newY = minY;\n else if (newY > maxY) newY = maxY;\n if (y !== newY) {\n y = newY;\n changed = true;\n }\n }\n changed && fireChangeEvents.call(that, context || that);\n return undefined;\n }\n\n /**\n * @typedef {PlainObject} module:jPicker.MinMaxRangeX\n * @property {Float} minX\n * @property {Float} maxX\n * @property {Float} rangeX\n */\n /**\n * @typedef {PlainObject} module:jPicker.MinMaxRangeY\n * @property {Float} minY\n * @property {Float} maxY\n * @property {Float} rangeY\n */\n /**\n * @typedef {module:jPicker.MinMaxRangeY|module:jPicker.MinMaxRangeX} module:jPicker.MinMaxRangeXY\n */\n\n /**\n *\n * @param {\"minx\"|\"maxx\"|\"rangex\"|\"miny\"|\"maxy\"|\"rangey\"|\"all\"} name\n * @param {module:jPicker.MinMaxRangeXY} value\n * @returns {module:jPicker.MinMaxRangeXY|module:jPicker.MinMaxRangeX|module:jPicker.MinMaxRangeY|void}\n */\n function range (name, value) {\n const set = value !== undefined;\n if (!set) {\n if (isNullish(name)) name = 'all';\n switch (name.toLowerCase()) {\n case 'minx': return minX;\n case 'maxx': return maxX;\n case 'rangex': return {minX, maxX, rangeX};\n case 'miny': return minY;\n case 'maxy': return maxY;\n case 'rangey': return {minY, maxY, rangeY};\n case 'all':\n default: return {minX, maxX, rangeX, minY, maxY, rangeY};\n }\n }\n let // changed = false,\n newMinX,\n newMaxX,\n newMinY,\n newMaxY;\n if (isNullish(name)) name = 'all';\n switch (name.toLowerCase()) {\n case 'minx':\n newMinX = (value && ((value.minX && value.minX | 0) || value | 0)) || 0;\n break;\n case 'maxx':\n newMaxX = (value && ((value.maxX && value.maxX | 0) || value | 0)) || 0;\n break;\n case 'rangex':\n newMinX = (value && value.minX && value.minX | 0) || 0;\n newMaxX = (value && value.maxX && value.maxX | 0) || 0;\n break;\n case 'miny':\n newMinY = (value && ((value.minY && value.minY | 0) || value | 0)) || 0;\n break;\n case 'maxy':\n newMaxY = (value && ((value.maxY && value.maxY | 0) || value | 0)) || 0;\n break;\n case 'rangey':\n newMinY = (value && value.minY && value.minY | 0) || 0;\n newMaxY = (value && value.maxY && value.maxY | 0) || 0;\n break;\n case 'all':\n default:\n newMinX = (value && value.minX && value.minX | 0) || 0;\n newMaxX = (value && value.maxX && value.maxX | 0) || 0;\n newMinY = (value && value.minY && value.minY | 0) || 0;\n newMaxY = (value && value.maxY && value.maxY | 0) || 0;\n break;\n }\n\n if (!isNullish(newMinX) && minX !== newMinX) {\n minX = newMinX;\n rangeX = maxX - minX;\n }\n if (!isNullish(newMaxX) && maxX !== newMaxX) {\n maxX = newMaxX;\n rangeX = maxX - minX;\n }\n if (!isNullish(newMinY) && minY !== newMinY) {\n minY = newMinY;\n rangeY = maxY - minY;\n }\n if (!isNullish(newMaxY) && maxY !== newMaxY) {\n maxY = newMaxY;\n rangeY = maxY - minY;\n }\n return undefined;\n }\n /**\n * @param {GenericCallback} callback\n * @returns {void}\n */\n function bind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks\n if (typeof callback === 'function') changeEvents.push(callback);\n }\n /**\n * @param {GenericCallback} callback\n * @returns {void}\n */\n function unbind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks\n if (typeof callback !== 'function') return;\n let i;\n while ((i = changeEvents.includes(callback))) changeEvents.splice(i, 1);\n }\n /**\n *\n * @returns {void}\n */\n function destroy () {\n // unbind all possible events and null objects\n $(document).unbind('mouseup', mouseUp).unbind('mousemove', mouseMove);\n bar.unbind('mousedown', mouseDown);\n bar = null;\n arrow = null;\n changeEvents = null;\n }\n let offset,\n timeout,\n x = 0,\n y = 0,\n minX = 0,\n maxX = 100,\n rangeX = 100,\n minY = 0,\n maxY = 100,\n rangeY = 100,\n arrow = bar.find('img:first'), // the arrow image to drag\n changeEvents = [];\n\n $.extend(\n true,\n // public properties, methods, and event bindings - these we need\n // to access from other controls\n that,\n {\n val,\n range,\n bind,\n unbind,\n destroy\n }\n );\n // initialize this control\n arrow.src = options.arrow && options.arrow.image;\n arrow.w = (options.arrow && options.arrow.width) || arrow.width();\n arrow.h = (options.arrow && options.arrow.height) || arrow.height();\n bar.w = (options.map && options.map.width) || bar.width();\n bar.h = (options.map && options.map.height) || bar.height();\n // bind mousedown event\n bar.bind('mousedown', mouseDown);\n bind.call(that, draw);\n }\n }\n\n /**\n * Controls for all the input elements for the typing in color values.\n * @param {external:jQuery} picker\n * @param {external:jQuery.jPicker.Color} color\n * @param {external:jQuery.fn.$.fn.jPicker} bindedHex\n * @param {Float} alphaPrecision\n */\n class ColorValuePicker {\n constructor (picker, color, bindedHex, alphaPrecision) {\n const that = this; // private properties and methods\n const inputs = picker.find('td.Text input');\n // input box key down - use arrows to alter color\n /**\n *\n * @param {Event} e\n * @returns {Event|false|void}\n */\n function keyDown (e) {\n if (e.target.value === '' && e.target !== hex.get(0) && ((!isNullish(bindedHex) && e.target !== bindedHex.get(0)) || isNullish(bindedHex))) return undefined;\n if (!validateKey(e)) return e;\n switch (e.target) {\n case red.get(0):\n switch (e.keyCode) {\n case 38:\n red.val(setValueInRange.call(that, (red.val() << 0) + 1, 0, 255));\n color.val('r', red.val(), e.target);\n return false;\n case 40:\n red.val(setValueInRange.call(that, (red.val() << 0) - 1, 0, 255));\n color.val('r', red.val(), e.target);\n return false;\n }\n break;\n case green.get(0):\n switch (e.keyCode) {\n case 38:\n green.val(setValueInRange.call(that, (green.val() << 0) + 1, 0, 255));\n color.val('g', green.val(), e.target);\n return false;\n case 40:\n green.val(setValueInRange.call(that, (green.val() << 0) - 1, 0, 255));\n color.val('g', green.val(), e.target);\n return false;\n }\n break;\n case blue.get(0):\n switch (e.keyCode) {\n case 38:\n blue.val(setValueInRange.call(that, (blue.val() << 0) + 1, 0, 255));\n color.val('b', blue.val(), e.target);\n return false;\n case 40:\n blue.val(setValueInRange.call(that, (blue.val() << 0) - 1, 0, 255));\n color.val('b', blue.val(), e.target);\n return false;\n }\n break;\n case alpha && alpha.get(0):\n switch (e.keyCode) {\n case 38:\n alpha.val(setValueInRange.call(that, parseFloat(alpha.val()) + 1, 0, 100));\n color.val('a', toFixedNumeric((alpha.val() * 255) / 100, alphaPrecision), e.target);\n return false;\n case 40:\n alpha.val(setValueInRange.call(that, parseFloat(alpha.val()) - 1, 0, 100));\n color.val('a', toFixedNumeric((alpha.val() * 255) / 100, alphaPrecision), e.target);\n return false;\n }\n break;\n case hue.get(0):\n switch (e.keyCode) {\n case 38:\n hue.val(setValueInRange.call(that, (hue.val() << 0) + 1, 0, 360));\n color.val('h', hue.val(), e.target);\n return false;\n case 40:\n hue.val(setValueInRange.call(that, (hue.val() << 0) - 1, 0, 360));\n color.val('h', hue.val(), e.target);\n return false;\n }\n break;\n case saturation.get(0):\n switch (e.keyCode) {\n case 38:\n saturation.val(setValueInRange.call(that, (saturation.val() << 0) + 1, 0, 100));\n color.val('s', saturation.val(), e.target);\n return false;\n case 40:\n saturation.val(setValueInRange.call(that, (saturation.val() << 0) - 1, 0, 100));\n color.val('s', saturation.val(), e.target);\n return false;\n }\n break;\n case value.get(0):\n switch (e.keyCode) {\n case 38:\n value.val(setValueInRange.call(that, (value.val() << 0) + 1, 0, 100));\n color.val('v', value.val(), e.target);\n return false;\n case 40:\n value.val(setValueInRange.call(that, (value.val() << 0) - 1, 0, 100));\n color.val('v', value.val(), e.target);\n return false;\n }\n break;\n }\n return undefined;\n }\n // input box key up - validate value and set color\n /**\n * @param {Event} e\n * @returns {Event|void}\n * @todo Why is this returning an event?\n */\n function keyUp (e) {\n if (e.target.value === '' && e.target !== hex.get(0) &&\n ((!isNullish(bindedHex) && e.target !== bindedHex.get(0)) ||\n isNullish(bindedHex))) return undefined;\n if (!validateKey(e)) return e;\n switch (e.target) {\n case red.get(0):\n red.val(setValueInRange.call(that, red.val(), 0, 255));\n color.val('r', red.val(), e.target);\n break;\n case green.get(0):\n green.val(setValueInRange.call(that, green.val(), 0, 255));\n color.val('g', green.val(), e.target);\n break;\n case blue.get(0):\n blue.val(setValueInRange.call(that, blue.val(), 0, 255));\n color.val('b', blue.val(), e.target);\n break;\n case alpha && alpha.get(0):\n alpha.val(setValueInRange.call(that, alpha.val(), 0, 100));\n color.val('a', toFixedNumeric((alpha.val() * 255) / 100, alphaPrecision), e.target);\n break;\n case hue.get(0):\n hue.val(setValueInRange.call(that, hue.val(), 0, 360));\n color.val('h', hue.val(), e.target);\n break;\n case saturation.get(0):\n saturation.val(setValueInRange.call(that, saturation.val(), 0, 100));\n color.val('s', saturation.val(), e.target);\n break;\n case value.get(0):\n value.val(setValueInRange.call(that, value.val(), 0, 100));\n color.val('v', value.val(), e.target);\n break;\n case hex.get(0):\n hex.val(hex.val().replace(/[^a-fA-F\\d]/g, '').toLowerCase().substring(0, 6));\n bindedHex && bindedHex.val(hex.val());\n color.val('hex', hex.val() !== '' ? hex.val() : null, e.target);\n break;\n case bindedHex && bindedHex.get(0):\n bindedHex.val(bindedHex.val().replace(/[^a-fA-F\\d]/g, '').toLowerCase().substring(0, 6));\n hex.val(bindedHex.val());\n color.val('hex', bindedHex.val() !== '' ? bindedHex.val() : null, e.target);\n break;\n case ahex && ahex.get(0):\n ahex.val(ahex.val().replace(/[^a-fA-F\\d]/g, '').toLowerCase().substring(0, 2));\n color.val('a', !isNullish(ahex.val()) ? parseInt(ahex.val(), 16) : null, e.target);\n break;\n }\n return undefined;\n }\n // input box blur - reset to original if value empty\n /**\n * @param {Event} e\n * @returns {void}\n */\n function blur (e) {\n if (!isNullish(color.val())) {\n switch (e.target) {\n case red.get(0): red.val(color.val('r')); break;\n case green.get(0): green.val(color.val('g')); break;\n case blue.get(0): blue.val(color.val('b')); break;\n case alpha && alpha.get(0): alpha.val(toFixedNumeric((color.val('a') * 100) / 255, alphaPrecision)); break;\n case hue.get(0): hue.val(color.val('h')); break;\n case saturation.get(0): saturation.val(color.val('s')); break;\n case value.get(0): value.val(color.val('v')); break;\n case hex.get(0):\n case bindedHex && bindedHex.get(0):\n hex.val(color.val('hex'));\n bindedHex && bindedHex.val(color.val('hex'));\n break;\n case ahex && ahex.get(0): ahex.val(color.val('ahex').substring(6)); break;\n }\n }\n }\n /**\n * @param {Event} e\n * @returns {boolean}\n */\n function validateKey (e) {\n switch (e.keyCode) {\n case 9:\n case 16:\n case 29:\n case 37:\n case 39:\n return false;\n case 'c'.charCodeAt():\n case 'v'.charCodeAt():\n if (e.ctrlKey) return false;\n }\n return true;\n }\n\n /**\n * Constrain value within range.\n * @param {Float|string} value\n * @param {Float} min\n * @param {Float} max\n * @returns {Float|string} Returns a number or numeric string\n */\n function setValueInRange (value, min, max) {\n if (value === '' || isNaN(value)) return min;\n if (value > max) return max;\n if (value < min) return min;\n return value;\n }\n /**\n * @param {external:jQuery} ui\n * @param {Element} context\n * @returns {void}\n */\n function colorChanged (ui, context) {\n const all = ui.val('all');\n if (context !== red.get(0)) red.val(!isNullish(all) ? all.r : '');\n if (context !== green.get(0)) green.val(!isNullish(all) ? all.g : '');\n if (context !== blue.get(0)) blue.val(!isNullish(all) ? all.b : '');\n if (alpha && context !== alpha.get(0)) alpha.val(!isNullish(all) ? toFixedNumeric((all.a * 100) / 255, alphaPrecision) : '');\n if (context !== hue.get(0)) hue.val(!isNullish(all) ? all.h : '');\n if (context !== saturation.get(0)) saturation.val(!isNullish(all) ? all.s : '');\n if (context !== value.get(0)) value.val(!isNullish(all) ? all.v : '');\n if (context !== hex.get(0) && ((bindedHex && context !== bindedHex.get(0)) || !bindedHex)) hex.val(!isNullish(all) ? all.hex : '');\n if (bindedHex && context !== bindedHex.get(0) && context !== hex.get(0)) bindedHex.val(!isNullish(all) ? all.hex : '');\n if (ahex && context !== ahex.get(0)) ahex.val(!isNullish(all) ? all.ahex.substring(6) : '');\n }\n /**\n * Unbind all events and null objects.\n * @returns {void}\n */\n function destroy () {\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).add(hex).add(bindedHex).add(ahex).unbind('keyup', keyUp).unbind('blur', blur);\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).unbind('keydown', keyDown);\n color.unbind(colorChanged);\n red = null;\n green = null;\n blue = null;\n alpha = null;\n hue = null;\n saturation = null;\n value = null;\n hex = null;\n ahex = null;\n }\n let\n red = inputs.eq(3),\n green = inputs.eq(4),\n blue = inputs.eq(5),\n alpha = inputs.length > 7 ? inputs.eq(6) : null,\n hue = inputs.eq(0),\n saturation = inputs.eq(1),\n value = inputs.eq(2),\n hex = inputs.eq(inputs.length > 7 ? 7 : 6),\n ahex = inputs.length > 7 ? inputs.eq(8) : null;\n $.extend(true, that, {\n // public properties and methods\n destroy\n });\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).add(hex).add(bindedHex).add(ahex).bind('keyup', keyUp).bind('blur', blur);\n red.add(green).add(blue).add(alpha).add(hue).add(saturation).add(value).bind('keydown', keyDown);\n color.bind(colorChanged);\n }\n }\n\n /**\n * @typedef {PlainObject} module:jPicker.JPickerInit\n * @property {Integer} [a]\n * @property {Integer} [b]\n * @property {Integer} [g]\n * @property {Integer} [h]\n * @property {Integer} [r]\n * @property {Integer} [s]\n * @property {Integer} [v]\n * @property {string} [hex]\n * @property {string} [ahex]\n */\n\n /* eslint-disable jsdoc/require-property */\n /**\n * @namespace {PlainObject} jPicker\n * @memberof external:jQuery\n */\n $.jPicker = /** @lends external:jQuery.jPicker */ {\n /* eslint-enable jsdoc/require-property */\n /**\n * Array holding references to each active instance of the jPicker control.\n * @type {external:jQuery.fn.$.fn.jPicker[]}\n */\n List: [],\n /**\n * Color object - we will be able to assign by any color space type or\n * retrieve any color space info.\n * We want this public so we can optionally assign new color objects to\n * initial values using inputs other than a string hex value (also supported)\n * Note: JSDoc didn't document when expressed here as an ES6 Class.\n * @namespace\n * @class\n * @memberof external:jQuery.jPicker\n * @param {module:jPicker.JPickerInit} init\n * @returns {external:jQuery.jPicker.Color}\n */\n Color: function (init) { // eslint-disable-line object-shorthand\n const that = this;\n /**\n *\n * @param {module:jPicker.Slider} context\n * @returns {void}\n */\n function fireChangeEvents (context) {\n for (let i = 0; i < changeEvents.length; i++) changeEvents[i].call(that, that, context);\n }\n\n /**\n * @param {string|\"ahex\"|\"hex\"|\"all\"|\"\"|null|void} name String composed of letters \"r\", \"g\", \"b\", \"a\", \"h\", \"s\", and/or \"v\"\n * @param {module:jPicker.RGBA|module:jPicker.JPickerInit|string} [value]\n * @param {external:jQuery.jPicker.Color} context\n * @returns {module:jPicker.JPickerInit|string|null|void}\n */\n function val (name, value, context) {\n // Kind of ugly\n const set = Boolean(value);\n if (set && value.ahex === '') value.ahex = '00000000';\n if (!set) {\n let ret;\n if (isNullish(name) || name === '') name = 'all';\n if (isNullish(r)) return null;\n switch (name.toLowerCase()) {\n case 'ahex': return ColorMethods.rgbaToHex({r, g, b, a});\n case 'hex': return val('ahex').substring(0, 6);\n case 'all': return {\n r, g, b, a, h, s, v,\n hex: val.call(that, 'hex'),\n ahex: val.call(that, 'ahex')\n };\n default: {\n ret = {};\n const nameLength = name.length;\n [...name].forEach((ch) => {\n switch (ch) {\n case 'r':\n if (nameLength === 1) ret = r;\n else ret.r = r;\n break;\n case 'g':\n if (nameLength === 1) ret = g;\n else ret.g = g;\n break;\n case 'b':\n if (nameLength === 1) ret = b;\n else ret.b = b;\n break;\n case 'a':\n if (nameLength === 1) ret = a;\n else ret.a = a;\n break;\n case 'h':\n if (nameLength === 1) ret = h;\n else ret.h = h;\n break;\n case 's':\n if (nameLength === 1) ret = s;\n else ret.s = s;\n break;\n case 'v':\n if (nameLength === 1) ret = v;\n else ret.v = v;\n break;\n }\n });\n }\n }\n return typeof ret === 'object' && !Object.keys(ret).length\n ? val.call(that, 'all')\n : ret;\n }\n if (!isNullish(context) && context === that) return undefined;\n if (isNullish(name)) name = '';\n\n let changed = false;\n if (isNullish(value)) {\n if (!isNullish(r)) {\n r = null;\n changed = true;\n }\n if (!isNullish(g)) {\n g = null;\n changed = true;\n }\n if (!isNullish(b)) {\n b = null;\n changed = true;\n }\n if (!isNullish(a)) {\n a = null;\n changed = true;\n }\n if (!isNullish(h)) {\n h = null;\n changed = true;\n }\n if (!isNullish(s)) {\n s = null;\n changed = true;\n }\n if (!isNullish(v)) {\n v = null;\n changed = true;\n }\n changed && fireChangeEvents.call(that, context || that);\n return undefined;\n }\n switch (name.toLowerCase()) {\n case 'ahex':\n case 'hex': {\n const ret = ColorMethods.hexToRgba((value && (value.ahex || value.hex)) || value || 'none');\n val.call(that, 'rgba', {\n r: ret.r,\n g: ret.g,\n b: ret.b,\n a: name === 'ahex'\n ? ret.a\n : !isNullish(a)\n ? a\n : 255\n }, context);\n break;\n } default: {\n if (value && (!isNullish(value.ahex) || !isNullish(value.hex))) {\n val.call(that, 'ahex', value.ahex || value.hex || '00000000', context);\n return undefined;\n }\n const newV = {};\n let rgb = false, hsv = false;\n if (value.r !== undefined && !name.includes('r')) name += 'r';\n if (value.g !== undefined && !name.includes('g')) name += 'g';\n if (value.b !== undefined && !name.includes('b')) name += 'b';\n if (value.a !== undefined && !name.includes('a')) name += 'a';\n if (value.h !== undefined && !name.includes('h')) name += 'h';\n if (value.s !== undefined && !name.includes('s')) name += 's';\n if (value.v !== undefined && !name.includes('v')) name += 'v';\n [...name].forEach((ch) => {\n switch (ch) {\n case 'r':\n if (hsv) return;\n rgb = true;\n newV.r = (value.r && value.r | 0) || (value | 0) || 0;\n if (newV.r < 0) newV.r = 0;\n else if (newV.r > 255) newV.r = 255;\n if (r !== newV.r) {\n ({r} = newV);\n changed = true;\n }\n break;\n case 'g':\n if (hsv) return;\n rgb = true;\n newV.g = (value && value.g && value.g | 0) || (value && value | 0) || 0;\n if (newV.g < 0) newV.g = 0;\n else if (newV.g > 255) newV.g = 255;\n if (g !== newV.g) {\n ({g} = newV);\n changed = true;\n }\n break;\n case 'b':\n if (hsv) return;\n rgb = true;\n newV.b = (value && value.b && value.b | 0) || (value && value | 0) || 0;\n if (newV.b < 0) newV.b = 0;\n else if (newV.b > 255) newV.b = 255;\n if (b !== newV.b) {\n ({b} = newV);\n changed = true;\n }\n break;\n case 'a':\n newV.a = value && !isNullish(value.a) ? value.a | 0 : value | 0;\n if (newV.a < 0) newV.a = 0;\n else if (newV.a > 255) newV.a = 255;\n if (a !== newV.a) {\n ({a} = newV);\n changed = true;\n }\n break;\n case 'h':\n if (rgb) return;\n hsv = true;\n newV.h = (value && value.h && value.h | 0) || (value && value | 0) || 0;\n if (newV.h < 0) newV.h = 0;\n else if (newV.h > 360) newV.h = 360;\n if (h !== newV.h) {\n ({h} = newV);\n changed = true;\n }\n break;\n case 's':\n if (rgb) return;\n hsv = true;\n newV.s = !isNullish(value.s) ? value.s | 0 : value | 0;\n if (newV.s < 0) newV.s = 0;\n else if (newV.s > 100) newV.s = 100;\n if (s !== newV.s) {\n ({s} = newV);\n changed = true;\n }\n break;\n case 'v':\n if (rgb) return;\n hsv = true;\n newV.v = !isNullish(value.v) ? value.v | 0 : value | 0;\n if (newV.v < 0) newV.v = 0;\n else if (newV.v > 100) newV.v = 100;\n if (v !== newV.v) {\n ({v} = newV);\n changed = true;\n }\n break;\n }\n });\n if (changed) {\n if (rgb) {\n r = r || 0;\n g = g || 0;\n b = b || 0;\n const ret = ColorMethods.rgbToHsv({r, g, b});\n ({h, s, v} = ret);\n } else if (hsv) {\n h = h || 0;\n s = !isNullish(s) ? s : 100;\n v = !isNullish(v) ? v : 100;\n const ret = ColorMethods.hsvToRgb({h, s, v});\n ({r, g, b} = ret);\n }\n a = !isNullish(a) ? a : 255;\n fireChangeEvents.call(that, context || that);\n }\n break;\n }\n }\n return undefined;\n }\n /**\n * @param {GenericCallback} callback\n * @returns {void}\n */\n function bind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks\n if (typeof callback === 'function') changeEvents.push(callback);\n }\n /**\n * @param {GenericCallback} callback\n * @returns {void}\n */\n function unbind (callback) { // eslint-disable-line promise/prefer-await-to-callbacks\n if (typeof callback !== 'function') return;\n let i;\n while ((i = changeEvents.includes(callback))) {\n changeEvents.splice(i, 1);\n }\n }\n /**\n * Unset `changeEvents`\n * @returns {void}\n */\n function destroy () {\n changeEvents = null;\n }\n let r, g, b, a, h, s, v, changeEvents = [];\n\n $.extend(true, that, {\n // public properties and methods\n val,\n bind,\n unbind,\n destroy\n });\n if (init) {\n if (!isNullish(init.ahex)) {\n val('ahex', init);\n } else if (!isNullish(init.hex)) {\n val(\n (!isNullish(init.a) ? 'a' : '') + 'hex',\n !isNullish(init.a)\n ? {ahex: init.hex + ColorMethods.intToHex(init.a)}\n : init\n );\n } else if (!isNullish(init.r) && !isNullish(init.g) && !isNullish(init.b)) {\n val('rgb' + (!isNullish(init.a) ? 'a' : ''), init);\n } else if (!isNullish(init.h) && !isNullish(init.s) && !isNullish(init.v)) {\n val('hsv' + (!isNullish(init.a) ? 'a' : ''), init);\n }\n }\n },\n /**\n * Color conversion methods - make public to give use to external scripts.\n * @namespace\n */\n ColorMethods: {\n /**\n * @typedef {PlainObject} module:jPicker.RGBA\n * @property {Integer} r\n * @property {Integer} g\n * @property {Integer} b\n * @property {Integer} a\n */\n /**\n * @typedef {PlainObject} module:jPicker.RGB\n * @property {Integer} r\n * @property {Integer} g\n * @property {Integer} b\n */\n /**\n * @param {string} hex\n * @returns {module:jPicker.RGBA}\n */\n hexToRgba (hex) {\n if (hex === '' || hex === 'none') return {r: null, g: null, b: null, a: null};\n hex = this.validateHex(hex);\n let r = '00', g = '00', b = '00', a = '255';\n if (hex.length === 6) hex += 'ff';\n if (hex.length > 6) {\n r = hex.substring(0, 2);\n g = hex.substring(2, 4);\n b = hex.substring(4, 6);\n a = hex.substring(6, hex.length);\n } else {\n if (hex.length > 4) {\n r = hex.substring(4, hex.length);\n hex = hex.substring(0, 4);\n }\n if (hex.length > 2) {\n g = hex.substring(2, hex.length);\n hex = hex.substring(0, 2);\n }\n if (hex.length > 0) b = hex.substring(0, hex.length);\n }\n return {\n r: this.hexToInt(r), g: this.hexToInt(g), b: this.hexToInt(b), a: this.hexToInt(a)\n };\n },\n /**\n * @param {string} hex\n * @returns {string}\n */\n validateHex (hex) {\n // if (typeof hex === 'object') return '';\n hex = hex.toLowerCase().replace(/[^a-f\\d]/g, '');\n if (hex.length > 8) hex = hex.substring(0, 8);\n return hex;\n },\n /**\n * @param {module:jPicker.RGBA} rgba\n * @returns {string}\n */\n rgbaToHex (rgba) {\n return this.intToHex(rgba.r) + this.intToHex(rgba.g) + this.intToHex(rgba.b) + this.intToHex(rgba.a);\n },\n /**\n * @param {Integer} dec\n * @returns {string}\n */\n intToHex (dec) {\n let result = (dec | 0).toString(16);\n if (result.length === 1) result = ('0' + result);\n return result.toLowerCase();\n },\n /**\n * @param {string} hex\n * @returns {Integer}\n */\n hexToInt (hex) {\n return parseInt(hex, 16);\n },\n /**\n * @typedef {PlainObject} module:jPicker.HSV\n * @property {Integer} h\n * @property {Integer} s\n * @property {Integer} v\n */\n /**\n * @param {module:jPicker.RGB} rgb\n * @returns {module:jPicker.HSV}\n */\n rgbToHsv (rgb) {\n const r = rgb.r / 255, g = rgb.g / 255, b = rgb.b / 255, hsv = {h: 0, s: 0, v: 0};\n let min = 0, max = 0;\n if (r >= g && r >= b) {\n max = r;\n min = g > b ? b : g;\n } else if (g >= b && g >= r) {\n max = g;\n min = r > b ? b : r;\n } else {\n max = b;\n min = g > r ? r : g;\n }\n hsv.v = max;\n hsv.s = max ? (max - min) / max : 0;\n let delta;\n if (!hsv.s) hsv.h = 0;\n else {\n delta = max - min;\n if (r === max) hsv.h = (g - b) / delta;\n else if (g === max) hsv.h = 2 + (b - r) / delta;\n else hsv.h = 4 + (r - g) / delta;\n hsv.h = parseInt(hsv.h * 60);\n if (hsv.h < 0) hsv.h += 360;\n }\n hsv.s = (hsv.s * 100) | 0;\n hsv.v = (hsv.v * 100) | 0;\n return hsv;\n },\n /**\n * @param {module:jPicker.HSV} hsv\n * @returns {module:jPicker.RGB}\n */\n hsvToRgb (hsv) {\n const rgb = {r: 0, g: 0, b: 0, a: 100};\n let {h, s, v} = hsv;\n if (s === 0) {\n if (v === 0) rgb.r = rgb.g = rgb.b = 0;\n else rgb.r = rgb.g = rgb.b = (v * 255 / 100) | 0;\n } else {\n if (h === 360) h = 0;\n h /= 60;\n s /= 100;\n v /= 100;\n const i = h | 0,\n f = h - i,\n p = v * (1 - s),\n q = v * (1 - (s * f)),\n t = v * (1 - (s * (1 - f)));\n switch (i) {\n case 0:\n rgb.r = v;\n rgb.g = t;\n rgb.b = p;\n break;\n case 1:\n rgb.r = q;\n rgb.g = v;\n rgb.b = p;\n break;\n case 2:\n rgb.r = p;\n rgb.g = v;\n rgb.b = t;\n break;\n case 3:\n rgb.r = p;\n rgb.g = q;\n rgb.b = v;\n break;\n case 4:\n rgb.r = t;\n rgb.g = p;\n rgb.b = v;\n break;\n case 5:\n rgb.r = v;\n rgb.g = p;\n rgb.b = q;\n break;\n }\n rgb.r = (rgb.r * 255) | 0;\n rgb.g = (rgb.g * 255) | 0;\n rgb.b = (rgb.b * 255) | 0;\n }\n return rgb;\n }\n }\n };\n const {Color, List, ColorMethods} = $.jPicker; // local copies for YUI compressor\n /* eslint-disable jsdoc/require-returns */\n /**\n * @function external:jQuery.fn.jPicker\n * @see {@link external:jQuery.fn.$.fn.jPicker}\n */\n /* eslint-enable jsdoc/require-returns */\n\n /**\n * Will be bound to active {@link jQuery.jPicker.Color}.\n * @callback module:jPicker.LiveCallback\n * @param {external:jQuery} ui\n * @param {Element} context\n * @returns {void}\n */\n /**\n * @callback module:jPicker.CommitCallback\n * @param {external:jQuery.jPicker.Color} activeColor\n * @param {external:jQuery} okButton\n * @returns {void} Return value not used.\n */\n /**\n * @callback module:jPicker.CancelCallback\n * @param {external:jQuery.jPicker.Color} activeColor\n * @param {external:jQuery} cancelButton\n * @returns {void} Return value not used.\n */\n /**\n * While it would seem this should specify the name `jPicker` for JSDoc, that doesn't\n * get us treated as a function as well as a namespace (even with `@function name`),\n * so we use an approach to add a redundant `$.fn.` in the name.\n * @namespace\n * @memberof external:jQuery.fn\n * @param {external:jQuery.fn.jPickerOptions} options\n * @param {module:jPicker.CommitCallback} [commitCallback]\n * @param {module:jPicker.LiveCallback} [liveCallback]\n * @param {module:jPicker.CancelCallback} [cancelCallback]\n * @returns {external:jQuery}\n */\n $.fn.jPicker = function (options, commitCallback, liveCallback, cancelCallback) {\n return this.each(function () {\n const that = this,\n settings = $.extend(true, {}, $.fn.jPicker.defaults, options); // local copies for YUI compressor\n if ($(that).get(0).nodeName.toLowerCase() === 'input') { // Add color picker icon if binding to an input element and bind the events to the input\n $.extend(true, settings, {\n window: {\n bindToInput: true,\n expandable: true,\n input: $(that)\n }\n });\n if ($(that).val() === '') {\n settings.color.active = new Color({hex: null});\n settings.color.current = new Color({hex: null});\n } else if (ColorMethods.validateHex($(that).val())) {\n settings.color.active = new Color({hex: $(that).val(), a: settings.color.active.val('a')});\n settings.color.current = new Color({hex: $(that).val(), a: settings.color.active.val('a')});\n }\n }\n if (settings.window.expandable) {\n $(that).after('    ');\n } else {\n settings.window.liveUpdate = false; // Basic control binding for inline use - You will need to override the liveCallback or commitCallback function to retrieve results\n }\n const isLessThanIE7 = parseFloat(navigator.appVersion.split('MSIE')[1]) < 7 && document.body.filters; // needed to run the AlphaImageLoader function for IE6\n // set color mode and update visuals for the new color mode\n /**\n *\n * @param {\"h\"|\"s\"|\"v\"|\"r\"|\"g\"|\"b\"|\"a\"} colorMode\n * @throws {Error} Invalid mode\n * @returns {void}\n */\n function setColorMode (colorMode) {\n const {active} = color, // local copies for YUI compressor\n // {clientPath} = images,\n hex = active.val('hex');\n let rgbMap, rgbBar;\n settings.color.mode = colorMode;\n switch (colorMode) {\n case 'h':\n setTimeout(function () {\n setBG.call(that, colorMapDiv, 'transparent');\n setImgLoc.call(that, colorMapL1, 0);\n setAlpha.call(that, colorMapL1, 100);\n setImgLoc.call(that, colorMapL2, 260);\n setAlpha.call(that, colorMapL2, 100);\n setBG.call(that, colorBarDiv, 'transparent');\n setImgLoc.call(that, colorBarL1, 0);\n setAlpha.call(that, colorBarL1, 100);\n setImgLoc.call(that, colorBarL2, 260);\n setAlpha.call(that, colorBarL2, 100);\n setImgLoc.call(that, colorBarL3, 260);\n setAlpha.call(that, colorBarL3, 100);\n setImgLoc.call(that, colorBarL4, 260);\n setAlpha.call(that, colorBarL4, 100);\n setImgLoc.call(that, colorBarL6, 260);\n setAlpha.call(that, colorBarL6, 100);\n }, 0);\n colorMap.range('all', {minX: 0, maxX: 100, minY: 0, maxY: 100});\n colorBar.range('rangeY', {minY: 0, maxY: 360});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('s'), y: 100 - active.val('v')}, colorMap);\n colorBar.val('y', 360 - active.val('h'), colorBar);\n break;\n case 's':\n setTimeout(function () {\n setBG.call(that, colorMapDiv, 'transparent');\n setImgLoc.call(that, colorMapL1, -260);\n setImgLoc.call(that, colorMapL2, -520);\n setImgLoc.call(that, colorBarL1, -260);\n setImgLoc.call(that, colorBarL2, -520);\n setImgLoc.call(that, colorBarL6, 260);\n setAlpha.call(that, colorBarL6, 100);\n }, 0);\n colorMap.range('all', {minX: 0, maxX: 360, minY: 0, maxY: 100});\n colorBar.range('rangeY', {minY: 0, maxY: 100});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('h'), y: 100 - active.val('v')}, colorMap);\n colorBar.val('y', 100 - active.val('s'), colorBar);\n break;\n case 'v':\n setTimeout(function () {\n setBG.call(that, colorMapDiv, '000000');\n setImgLoc.call(that, colorMapL1, -780);\n setImgLoc.call(that, colorMapL2, 260);\n setBG.call(that, colorBarDiv, hex);\n setImgLoc.call(that, colorBarL1, -520);\n setImgLoc.call(that, colorBarL2, 260);\n setAlpha.call(that, colorBarL2, 100);\n setImgLoc.call(that, colorBarL6, 260);\n setAlpha.call(that, colorBarL6, 100);\n }, 0);\n colorMap.range('all', {minX: 0, maxX: 360, minY: 0, maxY: 100});\n colorBar.range('rangeY', {minY: 0, maxY: 100});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('h'), y: 100 - active.val('s')}, colorMap);\n colorBar.val('y', 100 - active.val('v'), colorBar);\n break;\n case 'r':\n rgbMap = -1040;\n rgbBar = -780;\n colorMap.range('all', {minX: 0, maxX: 255, minY: 0, maxY: 255});\n colorBar.range('rangeY', {minY: 0, maxY: 255});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('b'), y: 255 - active.val('g')}, colorMap);\n colorBar.val('y', 255 - active.val('r'), colorBar);\n break;\n case 'g':\n rgbMap = -1560;\n rgbBar = -1820;\n colorMap.range('all', {minX: 0, maxX: 255, minY: 0, maxY: 255});\n colorBar.range('rangeY', {minY: 0, maxY: 255});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('b'), y: 255 - active.val('r')}, colorMap);\n colorBar.val('y', 255 - active.val('g'), colorBar);\n break;\n case 'b':\n rgbMap = -2080;\n rgbBar = -2860;\n colorMap.range('all', {minX: 0, maxX: 255, minY: 0, maxY: 255});\n colorBar.range('rangeY', {minY: 0, maxY: 255});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('r'), y: 255 - active.val('g')}, colorMap);\n colorBar.val('y', 255 - active.val('b'), colorBar);\n break;\n case 'a':\n setTimeout(function () {\n setBG.call(that, colorMapDiv, 'transparent');\n setImgLoc.call(that, colorMapL1, -260);\n setImgLoc.call(that, colorMapL2, -520);\n setImgLoc.call(that, colorBarL1, 260);\n setImgLoc.call(that, colorBarL2, 260);\n setAlpha.call(that, colorBarL2, 100);\n setImgLoc.call(that, colorBarL6, 0);\n setAlpha.call(that, colorBarL6, 100);\n }, 0);\n colorMap.range('all', {minX: 0, maxX: 360, minY: 0, maxY: 100});\n colorBar.range('rangeY', {minY: 0, maxY: 255});\n if (isNullish(active.val('ahex'))) break;\n colorMap.val('xy', {x: active.val('h'), y: 100 - active.val('v')}, colorMap);\n colorBar.val('y', 255 - active.val('a'), colorBar);\n break;\n default:\n throw new Error('Invalid Mode');\n }\n switch (colorMode) {\n case 'h':\n break;\n case 's':\n case 'v':\n case 'a':\n setTimeout(function () {\n setAlpha.call(that, colorMapL1, 100);\n setAlpha.call(that, colorBarL1, 100);\n setImgLoc.call(that, colorBarL3, 260);\n setAlpha.call(that, colorBarL3, 100);\n setImgLoc.call(that, colorBarL4, 260);\n setAlpha.call(that, colorBarL4, 100);\n }, 0);\n break;\n case 'r':\n case 'g':\n case 'b':\n setTimeout(function () {\n setBG.call(that, colorMapDiv, 'transparent');\n setBG.call(that, colorBarDiv, 'transparent');\n setAlpha.call(that, colorBarL1, 100);\n setAlpha.call(that, colorMapL1, 100);\n setImgLoc.call(that, colorMapL1, rgbMap);\n setImgLoc.call(that, colorMapL2, rgbMap - 260);\n setImgLoc.call(that, colorBarL1, rgbBar - 780);\n setImgLoc.call(that, colorBarL2, rgbBar - 520);\n setImgLoc.call(that, colorBarL3, rgbBar);\n setImgLoc.call(that, colorBarL4, rgbBar - 260);\n setImgLoc.call(that, colorBarL6, 260);\n setAlpha.call(that, colorBarL6, 100);\n }, 0);\n break;\n }\n if (isNullish(active.val('ahex'))) return;\n activeColorChanged.call(that, active);\n }\n /**\n * Update color when user changes text values.\n * @param {external:jQuery} ui\n * @param {?module:jPicker.Slider} context\n * @returns {void}\n */\n function activeColorChanged (ui, context) {\n if (isNullish(context) || (context !== colorBar && context !== colorMap)) positionMapAndBarArrows.call(that, ui, context);\n setTimeout(function () {\n updatePreview.call(that, ui);\n updateMapVisuals.call(that, ui);\n updateBarVisuals.call(that, ui);\n }, 0);\n }\n\n /**\n * User has dragged the ColorMap pointer.\n * @param {external:jQuery} ui\n * @param {?module:jPicker.Slider} context\n * @returns {void}\n */\n function mapValueChanged (ui, context) {\n const {active} = color;\n if (context !== colorMap && isNullish(active.val())) return;\n const xy = ui.val('all');\n switch (settings.color.mode) {\n case 'h':\n active.val('sv', {s: xy.x, v: 100 - xy.y}, context);\n break;\n case 's':\n case 'a':\n active.val('hv', {h: xy.x, v: 100 - xy.y}, context);\n break;\n case 'v':\n active.val('hs', {h: xy.x, s: 100 - xy.y}, context);\n break;\n case 'r':\n active.val('gb', {g: 255 - xy.y, b: xy.x}, context);\n break;\n case 'g':\n active.val('rb', {r: 255 - xy.y, b: xy.x}, context);\n break;\n case 'b':\n active.val('rg', {r: xy.x, g: 255 - xy.y}, context);\n break;\n }\n }\n\n /**\n * User has dragged the ColorBar slider.\n * @param {external:jQuery} ui\n * @param {?module:jPicker.Slider} context\n * @returns {void}\n */\n function colorBarValueChanged (ui, context) {\n const {active} = color;\n if (context !== colorBar && isNullish(active.val())) return;\n switch (settings.color.mode) {\n case 'h':\n active.val('h', {h: 360 - ui.val('y')}, context);\n break;\n case 's':\n active.val('s', {s: 100 - ui.val('y')}, context);\n break;\n case 'v':\n active.val('v', {v: 100 - ui.val('y')}, context);\n break;\n case 'r':\n active.val('r', {r: 255 - ui.val('y')}, context);\n break;\n case 'g':\n active.val('g', {g: 255 - ui.val('y')}, context);\n break;\n case 'b':\n active.val('b', {b: 255 - ui.val('y')}, context);\n break;\n case 'a':\n active.val('a', 255 - ui.val('y'), context);\n break;\n }\n }\n\n /**\n * Position map and bar arrows to match current color.\n * @param {external:jQuery} ui\n * @param {?module:jPicker.Slider} context\n * @returns {void}\n */\n function positionMapAndBarArrows (ui, context) {\n if (context !== colorMap) {\n switch (settings.color.mode) {\n case 'h': {\n const sv = ui.val('sv');\n colorMap.val('xy', {x: !isNullish(sv) ? sv.s : 100, y: 100 - (!isNullish(sv) ? sv.v : 100)}, context);\n break;\n } case 's':\n // Fall through\n case 'a': {\n const hv = ui.val('hv');\n colorMap.val('xy', {x: (hv && hv.h) || 0, y: 100 - (!isNullish(hv) ? hv.v : 100)}, context);\n break;\n } case 'v': {\n const hs = ui.val('hs');\n colorMap.val('xy', {x: (hs && hs.h) || 0, y: 100 - (!isNullish(hs) ? hs.s : 100)}, context);\n break;\n } case 'r': {\n const bg = ui.val('bg');\n colorMap.val('xy', {x: (bg && bg.b) || 0, y: 255 - ((bg && bg.g) || 0)}, context);\n break;\n } case 'g': {\n const br = ui.val('br');\n colorMap.val('xy', {x: (br && br.b) || 0, y: 255 - ((br && br.r) || 0)}, context);\n break;\n } case 'b': {\n const rg = ui.val('rg');\n colorMap.val('xy', {x: (rg && rg.r) || 0, y: 255 - ((rg && rg.g) || 0)}, context);\n break;\n }\n }\n }\n if (context !== colorBar) {\n switch (settings.color.mode) {\n case 'h':\n colorBar.val('y', 360 - (ui.val('h') || 0), context);\n break;\n case 's': {\n const s = ui.val('s');\n colorBar.val('y', 100 - (!isNullish(s) ? s : 100), context);\n break;\n } case 'v': {\n const v = ui.val('v');\n colorBar.val('y', 100 - (!isNullish(v) ? v : 100), context);\n break;\n } case 'r':\n colorBar.val('y', 255 - (ui.val('r') || 0), context);\n break;\n case 'g':\n colorBar.val('y', 255 - (ui.val('g') || 0), context);\n break;\n case 'b':\n colorBar.val('y', 255 - (ui.val('b') || 0), context);\n break;\n case 'a': {\n const a = ui.val('a');\n colorBar.val('y', 255 - (!isNullish(a) ? a : 255), context);\n break;\n }\n }\n }\n }\n /**\n * @param {external:jQuery} ui\n * @returns {void}\n */\n function updatePreview (ui) {\n try {\n const all = ui.val('all');\n activePreview.css({backgroundColor: (all && '#' + all.hex) || 'transparent'});\n setAlpha.call(that, activePreview, (all && toFixedNumeric((all.a * 100) / 255, 4)) || 0);\n } catch (e) { }\n }\n /**\n * @param {external:jQuery} ui\n * @returns {void}\n */\n function updateMapVisuals (ui) {\n switch (settings.color.mode) {\n case 'h':\n setBG.call(that, colorMapDiv, new Color({h: ui.val('h') || 0, s: 100, v: 100}).val('hex'));\n break;\n case 's':\n case 'a': {\n const s = ui.val('s');\n setAlpha.call(that, colorMapL2, 100 - (!isNullish(s) ? s : 100));\n break;\n } case 'v': {\n const v = ui.val('v');\n setAlpha.call(that, colorMapL1, !isNullish(v) ? v : 100);\n break;\n } case 'r':\n setAlpha.call(that, colorMapL2, toFixedNumeric((ui.val('r') || 0) / 255 * 100, 4));\n break;\n case 'g':\n setAlpha.call(that, colorMapL2, toFixedNumeric((ui.val('g') || 0) / 255 * 100, 4));\n break;\n case 'b':\n setAlpha.call(that, colorMapL2, toFixedNumeric((ui.val('b') || 0) / 255 * 100));\n break;\n }\n const a = ui.val('a');\n setAlpha.call(that, colorMapL3, toFixedNumeric(((255 - (a || 0)) * 100) / 255, 4));\n }\n /**\n * @param {external:jQuery} ui\n * @returns {void}\n */\n function updateBarVisuals (ui) {\n switch (settings.color.mode) {\n case 'h': {\n const a = ui.val('a');\n setAlpha.call(that, colorBarL5, toFixedNumeric(((255 - (a || 0)) * 100) / 255, 4));\n break;\n } case 's': {\n const hva = ui.val('hva'),\n saturatedColor = new Color({h: (hva && hva.h) || 0, s: 100, v: !isNullish(hva) ? hva.v : 100});\n setBG.call(that, colorBarDiv, saturatedColor.val('hex'));\n setAlpha.call(that, colorBarL2, 100 - (!isNullish(hva) ? hva.v : 100));\n setAlpha.call(that, colorBarL5, toFixedNumeric(((255 - ((hva && hva.a) || 0)) * 100) / 255, 4));\n break;\n } case 'v': {\n const hsa = ui.val('hsa'),\n valueColor = new Color({h: (hsa && hsa.h) || 0, s: !isNullish(hsa) ? hsa.s : 100, v: 100});\n setBG.call(that, colorBarDiv, valueColor.val('hex'));\n setAlpha.call(that, colorBarL5, toFixedNumeric(((255 - ((hsa && hsa.a) || 0)) * 100) / 255, 4));\n break;\n } case 'r':\n case 'g':\n case 'b': {\n const rgba = ui.val('rgba');\n let hValue = 0, vValue = 0;\n if (settings.color.mode === 'r') {\n hValue = (rgba && rgba.b) || 0;\n vValue = (rgba && rgba.g) || 0;\n } else if (settings.color.mode === 'g') {\n hValue = (rgba && rgba.b) || 0;\n vValue = (rgba && rgba.r) || 0;\n } else if (settings.color.mode === 'b') {\n hValue = (rgba && rgba.r) || 0;\n vValue = (rgba && rgba.g) || 0;\n }\n const middle = vValue > hValue ? hValue : vValue;\n setAlpha.call(that, colorBarL2, hValue > vValue ? toFixedNumeric(((hValue - vValue) / (255 - vValue)) * 100, 4) : 0);\n setAlpha.call(that, colorBarL3, vValue > hValue ? toFixedNumeric(((vValue - hValue) / (255 - hValue)) * 100, 4) : 0);\n setAlpha.call(that, colorBarL4, toFixedNumeric((middle / 255) * 100, 4));\n setAlpha.call(that, colorBarL5, toFixedNumeric(((255 - ((rgba && rgba.a) || 0)) * 100) / 255, 4));\n break;\n } case 'a': {\n const a = ui.val('a');\n setBG.call(that, colorBarDiv, ui.val('hex') || '000000');\n setAlpha.call(that, colorBarL5, !isNullish(a) ? 0 : 100);\n setAlpha.call(that, colorBarL6, !isNullish(a) ? 100 : 0);\n break;\n }\n }\n }\n /**\n * @param {external:jQuery} el\n * @param {string} [c=\"transparent\"]\n * @returns {void}\n */\n function setBG (el, c) {\n el.css({backgroundColor: (c && c.length === 6 && '#' + c) || 'transparent'});\n }\n\n /**\n * @param {external:jQuery} img\n * @param {string} src The image source\n * @returns {void}\n */\n function setImg (img, src) {\n if (isLessThanIE7 && (src.includes('AlphaBar.png') || src.includes('Bars.png') || src.includes('Maps.png'))) {\n img.attr('pngSrc', src);\n img.css({backgroundImage: 'none', filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\\'' + src + '\\', sizingMethod=\\'scale\\')'});\n } else img.css({backgroundImage: 'url(\\'' + src + '\\')'});\n }\n /**\n * @param {external:jQuery} img\n * @param {Float} y\n * @returns {void}\n */\n function setImgLoc (img, y) {\n img.css({top: y + 'px'});\n }\n /**\n * @param {external:jQuery} obj\n * @param {Float} alpha\n * @returns {void}\n */\n function setAlpha (obj, alpha) {\n obj.css({visibility: alpha > 0 ? 'visible' : 'hidden'});\n if (alpha > 0 && alpha < 100) {\n if (isLessThanIE7) {\n const src = obj.attr('pngSrc');\n if (!isNullish(src) && (\n src.includes('AlphaBar.png') || src.includes('Bars.png') || src.includes('Maps.png')\n )) {\n obj.css({\n filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\\'' + src +\n '\\', sizingMethod=\\'scale\\') progid:DXImageTransform.Microsoft.Alpha(opacity=' + alpha + ')'\n });\n } else obj.css({opacity: toFixedNumeric(alpha / 100, 4)});\n } else obj.css({opacity: toFixedNumeric(alpha / 100, 4)});\n } else if (alpha === 0 || alpha === 100) {\n if (isLessThanIE7) {\n const src = obj.attr('pngSrc');\n if (!isNullish(src) && (\n src.includes('AlphaBar.png') || src.includes('Bars.png') || src.includes('Maps.png')\n )) {\n obj.css({\n filter: 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src=\\'' + src +\n '\\', sizingMethod=\\'scale\\')'\n });\n } else obj.css({opacity: ''});\n } else obj.css({opacity: ''});\n }\n }\n\n /**\n * Revert color to original color when opened.\n * @returns {void}\n */\n function revertColor () {\n color.active.val('ahex', color.current.val('ahex'));\n }\n /**\n * Commit the color changes.\n * @returns {void}\n */\n function commitColor () {\n color.current.val('ahex', color.active.val('ahex'));\n }\n /**\n * @param {Event} e\n * @returns {void}\n */\n function radioClicked (e) {\n $(this).parents('tbody:first').find('input:radio[value!=\"' + e.target.value + '\"]').removeAttr('checked');\n setColorMode.call(that, e.target.value);\n }\n /**\n *\n * @returns {void}\n */\n function currentClicked () {\n revertColor.call(that);\n }\n /**\n *\n * @returns {void}\n */\n function cancelClicked () {\n revertColor.call(that);\n settings.window.expandable && hide.call(that);\n typeof cancelCallback === 'function' && cancelCallback.call(that, color.active, cancelButton);\n }\n /**\n *\n * @returns {void}\n */\n function okClicked () {\n commitColor.call(that);\n settings.window.expandable && hide.call(that);\n typeof commitCallback === 'function' && commitCallback.call(that, color.active, okButton);\n }\n /**\n *\n * @returns {void}\n */\n function iconImageClicked () {\n show.call(that);\n }\n /**\n * @param {external:jQuery} ui\n * @returns {void}\n */\n function currentColorChanged (ui) {\n const hex = ui.val('hex');\n currentPreview.css({backgroundColor: (hex && '#' + hex) || 'transparent'});\n setAlpha.call(that, currentPreview, toFixedNumeric(((ui.val('a') || 0) * 100) / 255, 4));\n }\n /**\n * @param {external:jQuery} ui\n * @returns {void}\n */\n function expandableColorChanged (ui) {\n const hex = ui.val('hex');\n const va = ui.val('va');\n iconColor.css({backgroundColor: (hex && '#' + hex) || 'transparent'});\n setAlpha.call(that, iconAlpha, toFixedNumeric(((255 - ((va && va.a) || 0)) * 100) / 255, 4));\n if (settings.window.bindToInput && settings.window.updateInputColor) {\n settings.window.input.css({\n backgroundColor: (hex && '#' + hex) || 'transparent',\n color: isNullish(va) || va.v > 75 ? '#000000' : '#ffffff'\n });\n }\n }\n /**\n * @param {Event} e\n * @returns {void}\n */\n function moveBarMouseDown (e) {\n // const {element} = settings.window, // local copies for YUI compressor\n // {page} = settings.window;\n elementStartX = parseInt(container.css('left'));\n elementStartY = parseInt(container.css('top'));\n pageStartX = e.pageX;\n pageStartY = e.pageY;\n // bind events to document to move window - we will unbind these on mouseup\n $(document).bind('mousemove', documentMouseMove).bind('mouseup', documentMouseUp);\n e.preventDefault(); // prevent attempted dragging of the column\n }\n /**\n * @param {Event} e\n * @returns {false}\n */\n function documentMouseMove (e) {\n container.css({\n left: elementStartX - (pageStartX - e.pageX) + 'px',\n top: elementStartY - (pageStartY - e.pageY) + 'px'\n });\n if (settings.window.expandable && !$.support.boxModel) {\n container.prev().css({\n left: container.css('left'),\n top: container.css('top')\n });\n }\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n /**\n * @param {Event} e\n * @returns {false}\n */\n function documentMouseUp (e) {\n $(document).unbind('mousemove', documentMouseMove).unbind('mouseup', documentMouseUp);\n e.stopPropagation();\n e.preventDefault();\n return false;\n }\n /**\n * @param {Event} e\n * @returns {false}\n */\n function quickPickClicked (e) {\n e.preventDefault();\n e.stopPropagation();\n color.active.val('ahex', $(this).attr('title') || null, e.target);\n return false;\n }\n /**\n *\n * @returns {void}\n */\n function show () {\n color.current.val('ahex', color.active.val('ahex'));\n /**\n *\n * @returns {void}\n */\n function attachIFrame () {\n if (!settings.window.expandable || $.support.boxModel) return;\n const table = container.find('table:first');\n container.before('