/* globals jQuery */ import './touch.js'; import {NS} from './namespaces.js'; import {isWebkit, isChrome, isGecko, isIE, isMac, isTouch} from './browser.js'; import * as Utils from './utilities.js'; import {getTypeMap, convertUnit, isValidUnit} from './units.js'; import { hasCustomHandler, getCustomHandler, injectExtendedContextMenuItemsIntoDom } from './contextmenu.js'; import {importSetGlobalDefault} from './external/dynamic-import-polyfill/importModule.js'; import SvgCanvas from './svgcanvas.js'; import Layer from './layer.js'; import jQueryPluginJSHotkeys from './js-hotkeys/jquery.hotkeys.min.js'; import jQueryPluginBBQ from './jquerybbq/jquery.bbq.min.js'; import jQueryPluginSVGIcons from './svgicons/jQuery.svgIcons.js'; import jQueryPluginJGraduate from './jgraduate/jQuery.jGraduate.js'; import jQueryPluginSpinButton from './spinbtn/jQuery.SpinButton.js'; import jQueryPluginSVG from './jQuery.attr.js'; // Needed for SVG attribute setting and array form with `attr` import jQueryPluginContextMenu from './contextmenu/jQuery.contextMenu.js'; import jQueryPluginJPicker from './jgraduate/jQuery.jPicker.js'; import jQueryPluginDBox from './dbox.js'; import { readLang, putLocale, setStrings, init as localeInit } from './locale/locale.js'; import loadStylesheets from './external/load-stylesheets/index-es.js'; /** * The main module for the visual SVG Editor * * @license MIT * * @copyright 2010 Alexis Deveria * 2010 Pavol Rusnak * 2010 Jeff Schiller * 2010 Narendra Sisodiya * 2014 Brett Zamir * @exports module:SVGEditor * @borrows module:locale.putLocale as putLocale * @borrows module:locale.readLang as readLang * @borrows module:locale.setStrings as setStrings */ const editor = {}; const $ = [ jQueryPluginJSHotkeys, jQueryPluginBBQ, jQueryPluginSVGIcons, jQueryPluginJGraduate, jQueryPluginSpinButton, jQueryPluginSVG, jQueryPluginContextMenu, jQueryPluginJPicker ].reduce((jq, func) => func(jq), jQuery); /* if (!$.loadingStylesheets) { $.loadingStylesheets = []; } */ const stylesheet = 'svg-editor.css'; if (!$.loadingStylesheets.includes(stylesheet)) { $.loadingStylesheets.push(stylesheet); } const favicon = 'images/logo.png'; if ($.loadingStylesheets.some((item) => { return !Array.isArray(item) || item[0] !== favicon; })) { $.loadingStylesheets.push([favicon, {favicon: true}]); } // EDITOR PROPERTIES: (defined below) // curPrefs, curConfig, canvas, storage, uiStrings // // STATE MAINTENANCE PROPERTIES /** * @type {Float} */ editor.tool_scale = 1; // Dependent on icon size, so any use to making configurable instead? Used by `jQuery.SpinButton.js` /** * @type {Integer} */ editor.exportWindowCt = 0; /** * @type {boolean} */ editor.langChanged = false; /** * @type {boolean} */ editor.showSaveWarning = false; /** * Will be set to a boolean by `ext-storage.js` * @type {"ignore"|"waiting"|"closed"} */ editor.storagePromptState = 'ignore'; const callbacks = [], /** * @typedef {"s"|"m"|"l"|"xl"|Float} module:SVGEditor.IconSize */ /** * Preferences * @interface module:SVGEditor.Prefs * @property {string} [lang="en"] Two-letter language code. The language must exist in the Editor Preferences language list. Defaults to "en" if `locale.js` detection does not detect another language. * @property {module:SVGEditor.IconSize} [iconsize="s"|"m"] Size of the toolbar icons. Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise. * @property {string} [bkgd_color="#FFF"] Color hex for canvas background color. Defaults to white. * @property {string} [bkgd_url=""] Background raster image URL. This image will fill the background of the document; useful for tracing purposes. * @property {"embed"|"ref"} [img_save="embed"] Defines whether included raster images should be saved as Data URIs when possible, or as URL references. Settable in the Document Properties dialog. * @property {boolean} [save_notice_done=false] Used to track alert status * @property {boolean} [export_notice_done=false] Used to track alert status * @todo `save_notice_done` and `export_notice_done` should be changed to flags rather than preferences */ /** * @namespace {module:SVGEditor.Prefs} defaultPrefs * @memberof module:SVGEditor~ * @implements {module:SVGEditor.Prefs} */ // The iteration algorithm for defaultPrefs does not currently support array/objects defaultPrefs = /** @lends module:SVGEditor~defaultPrefs */ { // EDITOR OPTIONS (DIALOG) /** * Default to "en" if locale.js detection does not detect another language */ lang: '', /** * Will default to 's' if the window height is smaller than the minimum height and * 'm' otherwise */ iconsize: '', bkgd_color: '#FFF', bkgd_url: '', // DOCUMENT PROPERTIES (DIALOG) img_save: 'embed', // ALERT NOTICES // Only shows in UI as far as alert notices, but useful to remember, so keeping as pref save_notice_done: false, export_notice_done: false }, /** * @name module:SVGEditor~defaultExtensions * @type {string[]} */ defaultExtensions = [ 'ext-connector.js', 'ext-eyedropper.js', 'ext-grid.js', 'ext-imagelib.js', 'ext-markers.js', 'ext-overview_window.js', 'ext-panning.js', 'ext-polygon.js', 'ext-shapes.js', 'ext-star.js', 'ext-storage.js' ], /** * @typedef {"@default"|string} module:SVGEditor.Stylesheet `@default` will automatically load all of the default CSS paths for SVGEditor */ /** * @typedef {GenericArray} module:SVGEditor.XYDimensions * @property {Integer} length 2 * @property {Float} 0 * @property {Float} 1 */ /** * @tutorial ConfigOptions * @interface module:SVGEditor.Config * @property {string} [canvasName="default"] Used to namespace storage provided via `ext-storage.js`; you can use this if you wish to have multiple independent instances of SVG Edit on the same domain * @property {boolean} [no_save_warning=false] If `true`, prevents the warning dialog box from appearing when closing/reloading the page. Mostly useful for testing. * @property {string} [imgPath="images/"] The path where the SVG icons are located, with trailing slash. Note that as of version 2.7, this is not configurable by URL for security reasons. * @property {string} [langPath="locale/"] The path where the language files are located, with trailing slash. Default will be changed to `../dist/locale/` if this is a modular load. Note that as of version 2.7, this is not configurable by URL for security reasons. * @property {string} [extPath="extensions/"] The path used for extension files, with trailing slash. Default will be changed to `../dist/extensions/` if this is a modular load. Note that as of version 2.7, this is not configurable by URL for security reasons. * @property {string} [canvgPath="canvg/"] The path used for `canvg` files, with trailing slash. Default will be changed to `../dist/` if this is a modular load. * @property {string} [jspdfPath="jspdf/"] The path used for `jsPDF` files, with trailing slash. Default will be changed to `../dist/` if this is a modular load. * @property {string} [extIconsPath="extensions/"] The path used for extension icons, with trailing slash. * @property {string} [jGraduatePath="jgraduate/images/"] The path where jGraduate images are located. Note that as of version 2.7, this is not configurable by URL for security reasons. * @property {boolean} [preventAllURLConfig=false] Set to `true` to override the ability for URLs to set non-content configuration (including extension config). Must be set early, i.e., in `svgedit-config-iife.js`; extension loading is too late! * @property {boolean} [preventURLContentLoading=false] Set to `true` to override the ability for URLs to set URL-based SVG content. Must be set early, i.e., in `svgedit-config-iife.js`; extension loading is too late! * @property {boolean} [lockExtensions=false] Set to `true` to override the ability for URLs to set their own extensions; disallowed in URL setting. There is no need for this when `preventAllURLConfig` is used. Must be set early, i.e., in `svgedit-config-iife.js`; extension loading is too late! * @property {boolean} [noDefaultExtensions=false] If set to `true`, prohibits automatic inclusion of default extensions (though "extensions" can still be used to add back any desired default extensions along with any other extensions). This can only be meaningfully used in `svgedit-config-iife.js` or in the URL * @property {boolean} [noStorageOnLoad=false] Some interaction with `ext-storage.js`; prevent even the loading of previously saved local storage. * @property {boolean} [forceStorage=false] Some interaction with `ext-storage.js`; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not (and may be required by law in some regions) * @property {boolean} [emptyStorageOnDecline=false] Used by `ext-storage.js`; empty any prior storage if the user declines to store * @property {string[]} [extensions=module:SVGEditor~defaultExtensions] Extensions to load on startup. Use an array in `setConfig` and comma separated file names in the URL. Extension names must begin with "ext-". Note that as of version 2.7, paths containing "/", "\", or ":", are disallowed for security reasons. Although previous versions of this list would entirely override the default list, as of version 2.7, the defaults will always be added to this explicit list unless the configuration `noDefaultExtensions` is included. * @property {module:SVGEditor.Stylesheet[]} [stylesheets=["@default"]] An array of required stylesheets to load in parallel; include the value `"@default"` within this array to ensure all default stylesheets are loaded. * @property {string[]} [allowedOrigins=[]] Used by `ext-xdomain-messaging.js` to indicate which origins are permitted for cross-domain messaging (e.g., between the embedded editor and main editor code). Besides explicit domains, one might add '*' to allow all domains (not recommended for privacy/data integrity of your user's content!), `window.location.origin` for allowing the same origin (should be safe if you trust all apps on your domain), 'null' to allow `file:///` URL usage * @property {null|PlainObject} [colorPickerCSS=null] Object of CSS properties mapped to values (for jQuery) to apply to the color picker. See {@link http://api.jquery.com/css/#css-properties}. A `null` value (the default) will cause the CSS to default to `left` with a position equal to that of the `fill_color` or `stroke_color` element minus 140, and a `bottom` equal to 40 * @property {string} [paramurl] This was available via URL only. Allowed an un-encoded URL within the query string (use "url" or "source" with a data: URI instead) * @property {Float} [canvas_expansion=3] The minimum area visible outside the canvas, as a multiple of the image dimensions. The larger the number, the more one can scroll outside the canvas. * @property {PlainObject} [initFill] Init fill properties * @property {string} [initFill.color="FF0000"] The initial fill color. Must be a hex code string. Defaults to solid red. * @property {Float} [initFill.opacity=1] The initial fill opacity. Must be a number between 0 and 1 * @property {PlainObject} [initStroke] Init stroke properties * @property {Float} [initStroke.width=5] The initial stroke width. Must be a positive number. * @property {string} [initStroke.color="000000"] The initial stroke color. Must be a hex code. Defaults to solid black. * @property {Float} [initStroke.opacity=1] The initial stroke opacity. Must be a number between 0 and 1. * @property {PlainObject} text Text style properties * @property {Float} [text.stroke_width=0] Text stroke width * @property {Float} [text.font_size=24] Text font size * @property {string} [text.font_family="serif"] Text font family * @property {Float} [initOpacity=1] Initial opacity (multiplied by 100) * @property {module:SVGEditor.XYDimensions} [dimensions=[640, 480]] The default width/height of a new document. Use an array in `setConfig` (e.g., `[800, 600]`) and comma separated numbers in the URL. * @property {boolean} [gridSnapping=false] Enable snap to grid by default. Set in Editor Options. * @property {string} [gridColor="#000"] Accepts hex, e.g., '#000'. Set in Editor Options. Defaults to black. * @property {string} [baseUnit="px"] Set in Editor Options. * @property {Float} [snappingStep=10] Set the default grid snapping value. Set in Editor Options. * @property {boolean} [showRulers=true] Initial state of ruler display (v2.6). Set in Editor Options. * @property {string} [initTool="select"] The initially selected tool. Must be either the ID of the button for the tool, or the ID without `tool_` prefix (e.g., "select"). * @property {boolean} [wireframe=false] Start in wireframe mode * @property {boolean} [showlayers=false] Open the layers side-panel by default. * @property {"new"|"same"} [exportWindowType="new"] Can be "new" or "same" to indicate whether new windows will be generated for each export; the `window.name` of the export window is namespaced based on the `canvasName` (and incremented if "new" is selected as the type). Introduced 2.8. * @property {boolean} [showGrid=false] Set by `ext-grid.js`; determines whether or not to show the grid by default * @property {boolean} [show_outside_canvas=true] Defines whether or not elements outside the canvas should be visible. Set and used in `svgcanvas.js`. * @property {boolean} [selectNew=true] If true, will replace the selection with the current element and automatically select element objects (when not in "path" mode) after they are created, showing their grips (v2.6). Set and used in `svgcanvas.js` (`mouseUp`). * @todo Some others could be preferences as well (e.g., preventing URL changing of extensions, defaultExtensions, stylesheets, colorPickerCSS); Change the following to preferences and add pref controls where missing to the UI (e.g., `canvas_expansion`, `initFill`, `initStroke`, `text`, `initOpacity`, `dimensions`, `initTool`, `wireframe`, `showlayers`, `gridSnapping`, `gridColor`, `baseUnit`, `snappingStep`, `showRulers`, `exportWindowType`, `showGrid`, `show_outside_canvas`, `selectNew`)? */ /** * @namespace {module:SVGEditor.Config} defaultConfig * @memberof module:SVGEditor~ * @implements {module:SVGEditor.Config} */ defaultConfig = { canvasName: 'default', canvas_expansion: 3, initFill: { color: 'FF0000', // solid red opacity: 1 }, initStroke: { width: 5, color: '000000', // solid black opacity: 1 }, text: { stroke_width: 0, font_size: 24, font_family: 'serif' }, initOpacity: 1, colorPickerCSS: null, // Defaults to 'left' with a position equal to that of the fill_color or stroke_color element minus 140, and a 'bottom' equal to 40 initTool: 'select', exportWindowType: 'new', // 'same' (todo: also support 'download') wireframe: false, showlayers: false, no_save_warning: false, // PATH CONFIGURATION // The following path configuration items are disallowed in the URL (as should any future path configurations) langPath: 'locale/', // Default will be changed if this is a non-modular load extPath: 'extensions/', // Default will be changed if this is a non-modular load canvgPath: 'canvg/', // Default will be changed if this is a non-modular load jspdfPath: 'jspdf/', // Default will be changed if this is a non-modular load imgPath: 'images/', jGraduatePath: 'jgraduate/images/', extIconsPath: 'extensions/', // DOCUMENT PROPERTIES // Change the following to a preference (already in the Document Properties dialog)? dimensions: [640, 480], // EDITOR OPTIONS // Change the following to preferences (already in the Editor Options dialog)? gridSnapping: false, gridColor: '#000', baseUnit: 'px', snappingStep: 10, showRulers: true, // URL BEHAVIOR CONFIGURATION preventAllURLConfig: false, preventURLContentLoading: false, // EXTENSION CONFIGURATION (see also preventAllURLConfig) lockExtensions: false, // Disallowed in URL setting noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in `svgedit-config-iife.js` or in the URL // EXTENSION-RELATED (GRID) showGrid: false, // Set by ext-grid.js // EXTENSION-RELATED (STORAGE) noStorageOnLoad: false, // Some interaction with ext-storage.js; prevent even the loading of previously saved local storage forceStorage: false, // Some interaction with ext-storage.js; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not emptyStorageOnDecline: false // Used by ext-storage.js; empty any prior storage if the user declines to store }, /** * LOCALE * @name module:SVGEditor.uiStrings * @type {PlainObject} */ uiStrings = editor.uiStrings = {}; let svgCanvas, urldata, isReady = false, customExportImage = false, customExportPDF = false, curPrefs = {}, // Note: The difference between Prefs and Config is that Prefs // can be changed in the UI and are stored in the browser, // while config cannot curConfig = { // We do not put on defaultConfig to simplify object copying // procedures (we obtain instead from defaultExtensions) extensions: [], stylesheets: [], /** * Can use `location.origin` to indicate the current * origin. Can contain a '*' to allow all domains or 'null' (as * a string) to support all `file:///` URLs. Cannot be set by * URL for security reasons (not safe, at least for * privacy or data integrity of SVG content). * Might have been fairly safe to allow * `new URL(location.href).origin` by default but * avoiding it ensures some more security that even third * party apps on the same domain also cannot communicate * with this app by default. * For use with `ext-xdomain-messaging.js` * @todo We might instead make as a user-facing preference. */ allowedOrigins: [] }; /** * * @param {string} str SVG string * @param {PlainObject} [opts={}] * @param {boolean} [opts.noAlert] * @throws {Error} Upon failure to load SVG * @returns {Promise} Resolves to undefined upon success (or if `noAlert` is * falsey, though only until after the `alert` is closed); rejects if SVG * loading fails and `noAlert` is truthy. */ async function loadSvgString (str, {noAlert} = {}) { const success = svgCanvas.setSvgString(str) !== false; if (success) { return; } if (!noAlert) { await $.alert(uiStrings.notification.errorLoadingSVG); return; } throw new Error('Error loading SVG'); } /** * @function module:SVGEditor~getImportLocale * @param {PlainObject} defaults * @param {string} defaults.defaultLang * @param {string} defaults.defaultName * @returns {module:SVGEditor~ImportLocale} */ function getImportLocale ({defaultLang, defaultName}) { /** * @function module:SVGEditor~ImportLocale * @param {PlainObject} localeInfo * @param {string} [localeInfo.name] Defaults to `defaultName` of {@link module:SVGEditor~getImportLocale} * @param {string} [localeInfo.lang=defaultLang] Defaults to `defaultLang` of {@link module:SVGEditor~getImportLocale} * @returns {Promise} Resolves to {@link module:locale.LocaleStrings} */ return async function importLocaleDefaulting ({name = defaultName, lang = defaultLang} = {}) { /** * * @param {string} language * @returns {Promise} Resolves to {@link module:locale.LocaleStrings} */ function importLocale (language) { const url = `${curConfig.extPath}ext-locale/${name}/${language}.js`; return importSetGlobalDefault(url, { global: `svgEditorExtensionLocale_${name}_${language.replace(/-/g, '_')}` }); } try { return await importLocale(lang); } catch (err) { return importLocale('en'); } }; } /** * EXPORTS */ /** * Store and retrieve preferences. * @param {string} key The preference name to be retrieved or set * @param {string} [val] The value. If the value supplied is missing or falsey, no change to the preference will be made. * @returns {string|undefined} If val is missing or falsey, the value of the previously stored preference will be returned. * @todo Can we change setting on the jQuery namespace (onto editor) to avoid conflicts? * @todo Review whether any remaining existing direct references to * getting `curPrefs` can be changed to use `$.pref()` getting to ensure * `defaultPrefs` fallback (also for sake of `allowInitialUserOverride`); specifically, `bkgd_color` could be changed so that * the pref dialog has a button to auto-calculate background, but otherwise uses `$.pref()` to be able to get default prefs * or overridable settings */ $.pref = function (key, val) { if (val) { curPrefs[key] = val; /** * @name curPrefs * @memberof module:SVGEditor * @implements {module:SVGEditor.Prefs} */ editor.curPrefs = curPrefs; // Update exported value return undefined; } return (key in curPrefs) ? curPrefs[key] : defaultPrefs[key]; }; /* * EDITOR PUBLIC METHODS // Todo: Sort these methods per invocation order, ideally with init at the end // Todo: Prevent execution until init executes if dependent on it? */ editor.putLocale = putLocale; editor.readLang = readLang; editor.setStrings = setStrings; /** * Where permitted, sets canvas and/or `defaultPrefs` based on previous * storage. This will override URL settings (for security reasons) but * not `svgedit-config-iife.js` configuration (unless initial user * overriding is explicitly permitted there via `allowInitialUserOverride`). * @todo Split `allowInitialUserOverride` into `allowOverrideByURL` and * `allowOverrideByUserStorage` so `svgedit-config-iife.js` can disallow some * individual items for URL setting but allow for user storage AND/OR * change URL setting so that it always uses a different namespace, * so it won't affect pre-existing user storage (but then if users saves * that, it will then be subject to tampering * @returns {undefined} */ editor.loadContentAndPrefs = function () { if (!curConfig.forceStorage && (curConfig.noStorageOnLoad || !document.cookie.match(/(?:^|;\s*)svgeditstore=(?:prefsAndContent|prefsOnly)/) ) ) { return; } // LOAD CONTENT if (editor.storage && // Cookies do not have enough available memory to hold large documents (curConfig.forceStorage || (!curConfig.noStorageOnLoad && document.cookie.match(/(?:^|;\s*)svgeditstore=prefsAndContent/)) ) ) { const name = 'svgedit-' + curConfig.canvasName; const cached = editor.storage.getItem(name); if (cached) { editor.loadFromString(cached); } } // LOAD PREFS Object.keys(defaultPrefs).forEach((key) => { const storeKey = 'svg-edit-' + key; if (editor.storage) { const val = editor.storage.getItem(storeKey); if (val) { defaultPrefs[key] = String(val); // Convert to string for FF (.value fails in Webkit) } } else if (window.widget) { defaultPrefs[key] = window.widget.preferenceForKey(storeKey); } else { const result = document.cookie.match(new RegExp('(?:^|;\\s*)' + Utils.regexEscape(encodeURIComponent(storeKey)) + '=([^;]+)')); defaultPrefs[key] = result ? decodeURIComponent(result[1]) : ''; } }); }; /** * Allows setting of preferences or configuration (including extensions). * @param {module:SVGEditor.Config|module:SVGEditor.Prefs} opts The preferences or configuration (including extensions). See the tutorial on {@tutorial ConfigOptions} for info on config and preferences. * @param {PlainObject} [cfgCfg] Describes configuration which applies to the * particular batch of supplied options * @param {boolean} [cfgCfg.allowInitialUserOverride=false] Set to true if you wish * to allow initial overriding of settings by the user via the URL * (if permitted) or previously stored preferences (if permitted); * note that it will be too late if you make such calls in extension * code because the URL or preference storage settings will * have already taken place. * @param {boolean} [cfgCfg.overwrite=true] Set to false if you wish to * prevent the overwriting of prior-set preferences or configuration * (URL settings will always follow this requirement for security * reasons, so `svgedit-config-iife.js` settings cannot be overridden unless it * explicitly permits via `allowInitialUserOverride` but extension config * can be overridden as they will run after URL settings). Should * not be needed in `svgedit-config-iife.js`. * @returns {undefined} */ editor.setConfig = function (opts, cfgCfg) { cfgCfg = cfgCfg || {}; /** * * @param {module:SVGEditor.Config|module:SVGEditor.Prefs} cfgObj * @param {string} key * @param {Any} val See {@link module:SVGEditor.Config} or {@link module:SVGEditor.Prefs} * @returns {undefined} */ function extendOrAdd (cfgObj, key, val) { if (cfgObj[key] && typeof cfgObj[key] === 'object') { $.extend(true, cfgObj[key], val); } else { cfgObj[key] = val; } } $.each(opts, function (key, val) { if ({}.hasOwnProperty.call(opts, key)) { // Only allow prefs defined in defaultPrefs if ({}.hasOwnProperty.call(defaultPrefs, key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || {}.hasOwnProperty.call(curPrefs, key) )) { return; } if (cfgCfg.allowInitialUserOverride === true) { defaultPrefs[key] = val; } else { $.pref(key, val); } } else if (['extensions', 'stylesheets', 'allowedOrigins'].includes(key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || ['allowedOrigins', 'stylesheets'].includes(key) || (key === 'extensions' && curConfig.lockExtensions) ) ) { return; } curConfig[key] = curConfig[key].concat(val); // We will handle any dupes later // Only allow other curConfig if defined in defaultConfig } else if ({}.hasOwnProperty.call(defaultConfig, key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || {}.hasOwnProperty.call(curConfig, key) )) { return; } // Potentially overwriting of previously set config if ({}.hasOwnProperty.call(curConfig, key)) { if (cfgCfg.overwrite === false) { return; } extendOrAdd(curConfig, key, val); } else if (cfgCfg.allowInitialUserOverride === true) { extendOrAdd(defaultConfig, key, val); } else if (defaultConfig[key] && typeof defaultConfig[key] === 'object') { curConfig[key] = {}; $.extend(true, curConfig[key], val); // Merge properties recursively, e.g., on initFill, initStroke objects } else { curConfig[key] = val; } } } }); /** * @name curConfig * @memberof module:SVGEditor * @implements {module:SVGEditor.Config} */ editor.curConfig = curConfig; // Update exported value }; /** * All methods are optional * @interface module:SVGEditor.CustomHandler * @type {PlainObject} */ /** * Its responsibilities are: * - invoke a file chooser dialog in 'open' mode * - let user pick a SVG file * - calls [svgCanvas.setSvgString()]{@link module:svgcanvas.SvgCanvas#setSvgString} with the string contents of that file. * Not passed any parameters. * @function module:SVGEditor.CustomHandler#open * @returns {undefined} */ /** * Its responsibilities are: * - accept the string contents of the current document * - invoke a file chooser dialog in 'save' mode * - save the file to location chosen by the user * @function module:SVGEditor.CustomHandler#save * @param {external:Window} win * @param {module:svgcanvas.SvgCanvas#event:saved} svgStr A string of the SVG * @listens module:svgcanvas.SvgCanvas#event:saved * @returns {undefined} */ /** * Its responsibilities (with regard to the object it is supplied in its 2nd argument) are: * - inform user of any issues supplied via the "issues" property * - convert the "svg" property SVG string into an image for export; * utilize the properties "type" (currently 'PNG', 'JPEG', 'BMP', * 'WEBP', 'PDF'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' * types) to determine the proper output. * @function module:SVGEditor.CustomHandler#exportImage * @param {external:Window} win * @param {module:svgcanvas.SvgCanvas#event:exported} data * @listens module:svgcanvas.SvgCanvas#event:exported * @returns {undefined} */ /** * @function module:SVGEditor.CustomHandler#exportPDF * @param {external:Window} win * @param {module:svgcanvas.SvgCanvas#event:exportedPDF} data * @listens module:svgcanvas.SvgCanvas#event:exportedPDF * @returns {undefined} */ /** * Allows one to override default SVGEdit `open`, `save`, and * `export` editor behaviors. * @param {module:SVGEditor.CustomHandler} opts Extension mechanisms may call `setCustomHandlers` with three functions: `opts.open`, `opts.save`, and `opts.exportImage` * @returns {undefined} */ editor.setCustomHandlers = function (opts) { editor.ready(function () { if (opts.open) { $('#tool_open > input[type="file"]').remove(); $('#tool_open').show(); svgCanvas.open = opts.open; } if (opts.save) { editor.showSaveWarning = false; svgCanvas.bind('saved', opts.save); } if (opts.exportImage) { customExportImage = opts.exportImage; svgCanvas.bind('exported', customExportImage); // canvg and our RGBColor will be available to the method } if (opts.exportPDF) { customExportPDF = opts.exportPDF; svgCanvas.bind('exportedPDF', customExportPDF); // jsPDF and our RGBColor will be available to the method } }); }; /** * @param {boolean} arg * @returns {undefined} */ editor.randomizeIds = function (arg) { return svgCanvas.randomizeIds(arg); }; /** * Auto-run after a Promise microtask. * @returns {undefined} */ editor.init = function () { const modularVersion = !('svgEditor' in window) || !window.svgEditor || window.svgEditor.modules !== false; if (!modularVersion) { Object.assign(defaultConfig, { langPath: '../dist/locale/', extPath: '../dist/extensions/', canvgPath: '../dist/', jspdfPath: '../dist/' }); } // const host = location.hostname, // onWeb = host && host.includes('.'); // Some FF versions throw security errors here when directly accessing try { if ('localStorage' in window) { // && onWeb removed so Webkit works locally /** * The built-in interface implemented by `localStorage` * @external Storage */ /** * @name storage * @memberof module:SVGEditor * @type {external:Storage} */ editor.storage = localStorage; } } catch (err) {} // Todo: Avoid const-defined functions and group functions together, etc. where possible const goodLangs = []; $('#lang_select option').each(function () { goodLangs.push(this.value); }); /** * Sets up current preferences based on defaults. * @returns {undefined} */ function setupCurPrefs () { curPrefs = $.extend(true, {}, defaultPrefs, curPrefs); // Now safe to merge with priority for curPrefs in the event any are already set // Export updated prefs editor.curPrefs = curPrefs; } /** * Sets up current config based on defaults. * @returns {undefined} */ function setupCurConfig () { curConfig = $.extend(true, {}, defaultConfig, curConfig); // Now safe to merge with priority for curConfig in the event any are already set // Now deal with extensions and other array config if (!curConfig.noDefaultExtensions) { curConfig.extensions = curConfig.extensions.concat(defaultExtensions); } // ...and remove any dupes ['extensions', 'stylesheets', 'allowedOrigins'].forEach(function (cfg) { curConfig[cfg] = $.grep(curConfig[cfg], function (n, i) { // Supposedly faster than filter per http://amandeep1986.blogspot.hk/2015/02/jquery-grep-vs-js-filter.html return i === curConfig[cfg].indexOf(n); }); }); // Export updated config editor.curConfig = curConfig; } (() => { // Load config/data from URL if given let src, qstr; urldata = $.deparam.querystring(true); if (!$.isEmptyObject(urldata)) { if (urldata.dimensions) { urldata.dimensions = urldata.dimensions.split(','); } if (urldata.bkgd_color) { urldata.bkgd_color = '#' + urldata.bkgd_color; } if (urldata.extensions) { // For security reasons, disallow cross-domain or cross-folder extensions via URL urldata.extensions = urldata.extensions.match(/[:/\\]/) ? '' : urldata.extensions.split(','); } // Disallowing extension paths via URL for // security reasons, even for same-domain // ones given potential to interact in undesirable // ways with other script resources [ 'langPath', 'extPath', 'canvgPath', 'jspdfPath', 'imgPath', 'jGraduatePath', 'extIconsPath' ].forEach(function (pathConfig) { if (urldata[pathConfig]) { delete urldata[pathConfig]; } }); editor.setConfig(urldata, {overwrite: false}); // Note: source and url (as with storagePrompt later) are not set on config but are used below setupCurConfig(); if (!curConfig.preventURLContentLoading) { src = urldata.source; qstr = $.param.querystring(); if (!src) { // urldata.source may have been null if it ended with '=' if (qstr.includes('source=data:')) { src = qstr.match(/source=(data:[^&]*)/)[1]; } } if (src) { if (src.startsWith('data:')) { editor.loadFromDataURI(src); } else { editor.loadFromString(src); } return; } if (urldata.url) { editor.loadFromURL(urldata.url); return; } } if (!urldata.noStorageOnLoad || curConfig.forceStorage) { editor.loadContentAndPrefs(); } } else { setupCurConfig(); editor.loadContentAndPrefs(); } setupCurPrefs(); })(); /** * Called internally. * @param {string|Element|external:jQuery} elem * @param {string|external:jQuery} iconId * @param {Float} forcedSize Not in use * @returns {undefined} */ const setIcon = editor.setIcon = function (elem, iconId, forcedSize) { const icon = (typeof iconId === 'string') ? $.getSvgIcon(iconId, true) : iconId.clone(); if (!icon) { // Todo: Investigate why this still occurs in some cases console.log('NOTE: Icon image missing: ' + iconId); // eslint-disable-line no-console return; } $(elem).empty().append(icon); }; /** * @fires module:svgcanvas.SvgCanvas#event:ext-addLangData * @fires module:svgcanvas.SvgCanvas#event:ext-langReady * @fires module:svgcanvas.SvgCanvas#event:ext-langChanged * @fires module:svgcanvas.SvgCanvas#event:extensions_added * @returns {Promise} Resolves to result of {@link module:locale.readLang} */ const extAndLocaleFunc = async function () { // const lang = ('lang' in curPrefs) ? curPrefs.lang : null; const {langParam, langData} = await editor.putLocale(null, goodLangs, curConfig); await setLang(langParam, langData); const {ok, cancel} = uiStrings.common; jQueryPluginDBox($, {ok, cancel}); try { await Promise.all( curConfig.extensions.map(async (extname) => { const extName = extname.match(/^ext-(.+)\.js/); if (!extName) { // Ensure URL cannot specify some other unintended file in the extPath return undefined; } const url = curConfig.extPath + extname; // Todo: Replace this with `return import(url);` when // `import()` widely supported /** * @tutorial ExtensionDocs * @typedef {PlainObject} module:SVGEditor.ExtensionObject * @property {string} [name] Name of the extension. Used internally; no need for i18n. Defaults to extension name without beginning "ext-" or ending ".js". * @property {module:svgcanvas.ExtensionInitCallback} [init] */ try { /** * @type {module:SVGEditor.ExtensionObject} */ const imported = await importSetGlobalDefault(url, { global: 'svgEditorExtension_' + extName[1].replace(/-/g, '_') }); const {name = extName[1], init} = imported; const importLocale = getImportLocale({defaultLang: langParam, defaultName: name}); return editor.addExtension(name, (init && init.bind(editor)), {$, importLocale}); } catch (err) { // Todo: Add config to alert any errors console.log(err); // eslint-disable-line no-console console.error('Extension failed to load: ' + extname + '; ' + err); // eslint-disable-line no-console return undefined; } }) ); svgCanvas.bind( 'extensions_added', /** * @param {external:Window} win * @param {module:svgcanvas.SvgCanvas#event:extensions_added} data * @listens module:svgcanvas.SvgCanvas#event:extensions_added * @returns {undefined} */ (win, data) => { extensionsAdded = true; Actions.setAll(); $('.flyout_arrow_horiz:empty').each(function () { $(this).append($.getSvgIcon('arrow_right', true).width(5).height(5)); }); if (editor.storagePromptState === 'ignore') { updateCanvas(true); } messageQueue.forEach( /** * @param {module:svgcanvas.SvgCanvas#event:message} messageObj * @fires module:svgcanvas.SvgCanvas#event:message * @returns {undefined} */ (messageObj) => { svgCanvas.call('message', messageObj); } ); } ); svgCanvas.call('extensions_added'); } catch (err) { // Todo: Report errors through the UI console.log(err); // eslint-disable-line no-console } }; const stateObj = {tool_scale: editor.tool_scale}; /** * * @returns {undefined} */ const setFlyoutPositions = function () { $('.tools_flyout').each(function () { const shower = $('#' + this.id + '_show'); const {left, top} = shower.offset(); const w = shower.outerWidth(); $(this).css({left: (left + w) * editor.tool_scale, top}); }); }; /** * @type {string} */ const uaPrefix = (function () { const regex = /^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/; const someScript = document.getElementsByTagName('script')[0]; for (const prop in someScript.style) { if (regex.test(prop)) { // test is faster than match, so it's better to perform // that on the lot and match only when necessary return prop.match(regex)[0]; } } // Nothing found so far? if ('WebkitOpacity' in someScript.style) { return 'Webkit'; } if ('KhtmlOpacity' in someScript.style) { return 'Khtml'; } return ''; }()); /** * @param {external:jQuery} elems * @param {Float} scale * @returns {undefined} */ const scaleElements = function (elems, scale) { // const prefix = '-' + uaPrefix.toLowerCase() + '-'; // Currently unused const sides = ['top', 'left', 'bottom', 'right']; elems.each(function () { // Handled in CSS // this.style[uaPrefix + 'Transform'] = 'scale(' + scale + ')'; const el = $(this); const w = el.outerWidth() * (scale - 1); const h = el.outerHeight() * (scale - 1); // const margins = {}; // Currently unused for (let i = 0; i < 4; i++) { const s = sides[i]; let cur = el.data('orig_margin-' + s); if (Utils.isNullish(cur)) { cur = parseInt(el.css('margin-' + s)); // Cache the original margin el.data('orig_margin-' + s, cur); } let val = cur * scale; if (s === 'right') { val += w; } else if (s === 'bottom') { val += h; } el.css('margin-' + s, val); // el.css('outline', '1px solid red'); } }); }; /** * Called internally. * @param {module:SVGEditor.IconSize} size * @returns {undefined} */ const setIconSize = editor.setIconSize = function (size) { // const elems = $('.tool_button, .push_button, .tool_button_current, .disabled, .icon_label, #url_notice, #tool_open'); const selToscale = '#tools_top .toolset, #editor_panel > *, #history_panel > *,' + ' #main_button, #tools_left > *, #path_node_panel > *, #multiselected_panel > *,' + ' #g_panel > *, #tool_font_size > *, .tools_flyout'; const elems = $(selToscale); let scale = 1; if (typeof size === 'number') { scale = size; } else { const iconSizes = {s: 0.75, m: 1, l: 1.25, xl: 1.5}; scale = iconSizes[size]; } stateObj.tool_scale = editor.tool_scale = scale; setFlyoutPositions(); // $('.tools_flyout').each(function () { // const pos = $(this).position(); // console.log($(this), pos.left+(34 * scale)); // $(this).css({left: pos.left+(34 * scale), top: pos.top+(77 * scale)}); // console.log('l', $(this).css('left')); // }); // // const scale = .75; const hiddenPs = elems.parents(':hidden'); hiddenPs.css('visibility', 'hidden').show(); scaleElements(elems, scale); hiddenPs.css('visibility', 'visible').hide(); // return; $.pref('iconsize', size); $('#iconsize').val(size); // Change icon size // $('.tool_button, .push_button, .tool_button_current, .disabled, .icon_label, #url_notice, #tool_open') // .find('> svg, > img').each(function () { // this.setAttribute('width',size_num); // this.setAttribute('height',size_num); // }); // // $.resizeSvgIcons({ // '.flyout_arrow_horiz > svg, .flyout_arrow_horiz > img': size_num / 5, // '#logo > svg, #logo > img': size_num * 1.3, // '#tools_bottom .icon_label > *': (size_num === 16 ? 18 : size_num * .75) // }); // if (size != 's') { // $.resizeSvgIcons({'#layerbuttons svg, #layerbuttons img': size_num * .6}); // } // Note that all rules will be prefixed with '#svg_editor' when parsed const cssResizeRules = { // '.tool_button,\ // .push_button,\ // .tool_button_current,\ // .push_button_pressed,\ // .disabled,\ // .icon_label,\ // .tools_flyout .tool_button': { // width: {s: '16px', l: '32px', xl: '48px'}, // height: {s: '16px', l: '32px', xl: '48px'}, // padding: {s: '1px', l: '2px', xl: '3px'} // }, // '.tool_sep': { // height: {s: '16px', l: '32px', xl: '48px'}, // margin: {s: '2px 2px', l: '2px 5px', xl: '2px 8px'} // }, // '#main_icon': { // width: {s: '31px', l: '53px', xl: '75px'}, // height: {s: '22px', l: '42px', xl: '64px'} // }, '#tools_top': { left: 50 + $('#main_button').width(), height: 72 }, '#tools_left': { width: 31, top: 74 }, 'div#workarea': { left: 38, top: 74 } // '#tools_bottom': { // left: {s: '27px', l: '46px', xl: '65px'}, // height: {s: '58px', l: '98px', xl: '145px'} // }, // '#color_tools': { // 'border-spacing': {s: '0 1px'}, // 'margin-top': {s: '-1px'} // }, // '#color_tools .icon_label': { // width: {l:'43px', xl: '60px'} // }, // '.color_tool': { // height: {s: '20px'} // }, // '#tool_opacity': { // top: {s: '1px'}, // height: {s: 'auto', l:'auto', xl:'auto'} // }, // '#tools_top input, #tools_bottom input': { // 'margin-top': {s: '2px', l: '4px', xl: '5px'}, // height: {s: 'auto', l: 'auto', xl: 'auto'}, // border: {s: '1px solid #555', l: 'auto', xl: 'auto'}, // 'font-size': {s: '.9em', l: '1.2em', xl: '1.4em'} // }, // '#zoom_panel': { // 'margin-top': {s: '3px', l: '4px', xl: '5px'} // }, // '#copyright, #tools_bottom .label': { // 'font-size': {l: '1.5em', xl: '2em'}, // 'line-height': {s: '15px'} // }, // '#tools_bottom_2': { // width: {l: '295px', xl: '355px'}, // top: {s: '4px'} // }, // '#tools_top > div, #tools_top': { // 'line-height': {s: '17px', l: '34px', xl: '50px'} // }, // '.dropdown button': { // height: {s: '18px', l: '34px', xl: '40px'}, // 'line-height': {s: '18px', l: '34px', xl: '40px'}, // 'margin-top': {s: '3px'} // }, // '#tools_top label, #tools_bottom label': { // 'font-size': {s: '1em', l: '1.5em', xl: '2em'}, // height: {s: '25px', l: '42px', xl: '64px'} // }, // 'div.toolset': { // height: {s: '25px', l: '42px', xl: '64px'} // }, // '#tool_bold, #tool_italic': { // 'font-size': {s: '1.5em', l: '3em', xl: '4.5em'} // }, // '#sidepanels': { // top: {s: '50px', l: '88px', xl: '125px'}, // bottom: {s: '51px', l: '68px', xl: '65px'} // }, // '#layerbuttons': { // width: {l: '130px', xl: '175px'}, // height: {l: '24px', xl: '30px'} // }, // '#layerlist': { // width: {l: '128px', xl: '150px'} // }, // '.layer_button': { // width: {l: '19px', xl: '28px'}, // height: {l: '19px', xl: '28px'} // }, // 'input.spin-button': { // 'background-image': {l: 'url('images/spinbtn_updn_big.png')', xl: 'url('images/spinbtn_updn_big.png')'}, // 'background-position': {l: '100% -5px', xl: '100% -2px'}, // 'padding-right': {l: '24px', xl: '24px' } // }, // 'input.spin-button.up': { // 'background-position': {l: '100% -45px', xl: '100% -42px'} // }, // 'input.spin-button.down': { // 'background-position': {l: '100% -85px', xl: '100% -82px'} // }, // '#position_opts': { // width: {all: (size_num*4) +'px'} // } }; let ruleElem = $('#tool_size_rules'); if (!ruleElem.length) { ruleElem = $('').appendTo('head'); } else { ruleElem.empty(); } if (size !== 'm') { let styleStr = ''; $.each(cssResizeRules, function (selector, rules) { selector = '#svg_editor ' + selector.replace(/,/g, ', #svg_editor'); styleStr += selector + '{'; $.each(rules, function (prop, values) { let val; if (typeof values === 'number') { val = (values * scale) + 'px'; } else if (values[size] || values.all) { val = (values[size] || values.all); } styleStr += (prop + ':' + val + ';'); }); styleStr += '}'; }); // this.style[uaPrefix + 'Transform'] = 'scale(' + scale + ')'; const prefix = '-' + uaPrefix.toLowerCase() + '-'; styleStr += (selToscale + '{' + prefix + 'transform: scale(' + scale + ');}' + ' #svg_editor div.toolset .toolset {' + prefix + 'transform: scale(1); margin: 1px !important;}' + // Hack for markers ' #svg_editor .ui-slider {' + prefix + 'transform: scale(' + (1 / scale) + ');}' // Hack for sliders ); ruleElem.text(styleStr); } setFlyoutPositions(); }; $.svgIcons(curConfig.imgPath + 'svg_edit_icons.svg', { w: 24, h: 24, id_match: false, no_img: !isWebkit(), // Opera & Firefox 4 gives odd behavior w/images fallback_path: curConfig.imgPath, fallback: { logo: 'logo.png', select: 'select.png', select_node: 'select_node.png', square: 'square.png', rect: 'rect.png', fh_rect: 'freehand-square.png', circle: 'circle.png', ellipse: 'ellipse.png', fh_ellipse: 'freehand-circle.png', pencil: 'fhpath.png', pen: 'line.png', text: 'text.png', path: 'path.png', add_subpath: 'add_subpath.png', close_path: 'closepath.png', open_path: 'openpath.png', image: 'image.png', zoom: 'zoom.png', arrow_right: 'flyouth.png', arrow_right_big: 'arrow_right_big.png', arrow_down: 'dropdown.gif', fill: 'fill.png', stroke: 'stroke.png', opacity: 'opacity.png', new_image: 'clear.png', save: 'save.png', export: 'export.png', open: 'open.png', import: 'import.png', docprops: 'document-properties.png', source: 'source.png', wireframe: 'wireframe.png', undo: 'undo.png', redo: 'redo.png', clone: 'clone.png', delete: 'delete.png', go_up: 'go-up.png', go_down: 'go-down.png', context_menu: 'context_menu.png', move_bottom: 'move_bottom.png', move_top: 'move_top.png', to_path: 'to_path.png', link_controls: 'link_controls.png', reorient: 'reorient.png', group_elements: 'shape_group_elements.png', ungroup: 'shape_ungroup.png', unlink_use: 'unlink_use.png', width: 'width.png', height: 'height.png', c_radius: 'c_radius.png', angle: 'angle.png', blur: 'blur.png', fontsize: 'fontsize.png', align: 'align.png', align_left: 'align-left.png', align_center: 'align-center.png', align_right: 'align-right.png', align_top: 'align-top.png', align_middle: 'align-middle.png', align_bottom: 'align-bottom.png', linecap_butt: 'linecap_butt.png', linecap_square: 'linecap_square.png', linecap_round: 'linecap_round.png', linejoin_miter: 'linejoin_miter.png', linejoin_bevel: 'linejoin_bevel.png', linejoin_round: 'linejoin_round.png', eye: 'eye.png', no_color: 'no_color.png', ok: 'save.png', cancel: 'cancel.png', warning: 'warning.png', node_delete: 'node_delete.png', node_clone: 'node_clone.png', globe_link: 'globe_link.png' }, placement: { '#logo': 'logo', '#tool_clear div,#layer_new': 'new_image', '#tool_save div': 'save', '#tool_export div': 'export', '#tool_open div div': 'open', '#tool_import div div': 'import', '#tool_source': 'source', '#tool_docprops > div': 'docprops', '#tool_wireframe': 'wireframe', '#tool_undo': 'undo', '#tool_redo': 'redo', '#tool_select': 'select', '#tool_fhpath': 'pencil', '#tool_line': 'pen', '#tool_rect,#tools_rect_show': 'rect', '#tool_square': 'square', '#tool_fhrect': 'fh_rect', '#tool_ellipse,#tools_ellipse_show': 'ellipse', '#tool_circle': 'circle', '#tool_fhellipse': 'fh_ellipse', '#tool_path': 'path', '#tool_text,#layer_rename': 'text', '#tool_image': 'image', '#tool_zoom': 'zoom', '#tool_clone,#tool_clone_multi': 'clone', '#tool_node_clone': 'node_clone', '#layer_delete,#tool_delete,#tool_delete_multi': 'delete', '#tool_node_delete': 'node_delete', '#tool_add_subpath': 'add_subpath', '#tool_openclose_path': 'open_path', '#tool_move_top': 'move_top', '#tool_move_bottom': 'move_bottom', '#tool_topath': 'to_path', '#tool_node_link': 'link_controls', '#tool_reorient': 'reorient', '#tool_group_elements': 'group_elements', '#tool_ungroup': 'ungroup', '#tool_unlink_use': 'unlink_use', '#tool_alignleft, #tool_posleft': 'align_left', '#tool_aligncenter, #tool_poscenter': 'align_center', '#tool_alignright, #tool_posright': 'align_right', '#tool_aligntop, #tool_postop': 'align_top', '#tool_alignmiddle, #tool_posmiddle': 'align_middle', '#tool_alignbottom, #tool_posbottom': 'align_bottom', '#cur_position': 'align', '#linecap_butt,#cur_linecap': 'linecap_butt', '#linecap_round': 'linecap_round', '#linecap_square': 'linecap_square', '#linejoin_miter,#cur_linejoin': 'linejoin_miter', '#linejoin_round': 'linejoin_round', '#linejoin_bevel': 'linejoin_bevel', '#url_notice': 'warning', '#layer_up': 'go_up', '#layer_down': 'go_down', '#layer_moreopts': 'context_menu', '#layerlist td.layervis': 'eye', '#tool_source_save,#tool_docprops_save,#tool_prefs_save': 'ok', '#tool_source_cancel,#tool_docprops_cancel,#tool_prefs_cancel': 'cancel', '#rwidthLabel, #iwidthLabel': 'width', '#rheightLabel, #iheightLabel': 'height', '#cornerRadiusLabel span': 'c_radius', '#angleLabel': 'angle', '#linkLabel,#tool_make_link,#tool_make_link_multi': 'globe_link', '#zoomLabel': 'zoom', '#tool_fill label': 'fill', '#tool_stroke .icon_label': 'stroke', '#group_opacityLabel': 'opacity', '#blurLabel': 'blur', '#font_sizeLabel': 'fontsize', '.flyout_arrow_horiz': 'arrow_right', '.dropdown button, #main_button .dropdown': 'arrow_down', '#palette .palette_item:first, #fill_bg, #stroke_bg': 'no_color' }, resize: { '#logo .svg_icon': 28, '.flyout_arrow_horiz .svg_icon': 5, '.layer_button .svg_icon, #layerlist td.layervis .svg_icon': 14, '.dropdown button .svg_icon': 7, '#main_button .dropdown .svg_icon': 9, '.palette_item:first .svg_icon': 15, '#fill_bg .svg_icon, #stroke_bg .svg_icon': 16, '.toolbar_button button .svg_icon': 16, '.stroke_tool div div .svg_icon': 20, '#tools_bottom label .svg_icon': 18 }, async callback (icons) { $('.toolbar_button button > svg, .toolbar_button button > img').each(function () { $(this).parent().prepend(this); }); const tleft = $('#tools_left'); let minHeight; if (tleft.length) { minHeight = tleft.offset().top + tleft.outerHeight(); } const size = $.pref('iconsize'); editor.setIconSize(size || ($(window).height() < minHeight ? 's' : 'm')); // Look for any missing flyout icons from plugins $('.tools_flyout').each(function () { const shower = $('#' + this.id + '_show'); const sel = shower.attr('data-curopt'); // Check if there's an icon here if (!shower.children('svg, img').length) { const clone = $(sel).children().clone(); if (clone.length) { clone[0].removeAttribute('style'); // Needed for Opera shower.append(clone); } } }); /** * Since stylesheets may be added out of order, we indicate the desired order * for defaults and others after them (in an indeterminate order). * @param {string} stylesheetFile * @returns {Integer|PositiveInfinity} */ function getStylesheetPriority (stylesheetFile) { switch (stylesheetFile) { case 'jgraduate/css/jPicker.css': return 1; case 'jgraduate/css/jGraduate.css': return 2; case 'svg-editor.css': return 3; case 'spinbtn/jQuery.SpinButton.css': return 4; default: return Infinity; } } let stylesheets = $.loadingStylesheets.sort((a, b) => { const priorityA = getStylesheetPriority(a); const priorityB = getStylesheetPriority(b); if (priorityA === priorityB) { return 0; } return priorityA > priorityB; }); if (curConfig.stylesheets.length) { // Ensure a copy with unique items stylesheets = [...new Set(curConfig.stylesheets)]; const idx = stylesheets.indexOf('@default'); if (idx > -1) { stylesheets.splice(idx, 1, ...$.loadingStylesheets); } } await loadStylesheets(stylesheets, { acceptErrors ({stylesheetURL, reject, resolve}) { if ($.loadingStylesheets.includes(stylesheetURL)) { reject(new Error(`Missing expected stylesheet: ${stylesheetURL}`)); return; } resolve(); } }); $('#svg_container')[0].style.visibility = 'visible'; await editor.runCallbacks(); } }); /** * @name module:SVGEditor.canvas * @type {module:svgcanvas.SvgCanvas} */ editor.canvas = svgCanvas = new SvgCanvas( document.getElementById('svgcanvas'), curConfig ); const palette = [ // Todo: Make into configuration item? '#000000', '#3f3f3f', '#7f7f7f', '#bfbfbf', '#ffffff', '#ff0000', '#ff7f00', '#ffff00', '#7fff00', '#00ff00', '#00ff7f', '#00ffff', '#007fff', '#0000ff', '#7f00ff', '#ff00ff', '#ff007f', '#7f0000', '#7f3f00', '#7f7f00', '#3f7f00', '#007f00', '#007f3f', '#007f7f', '#003f7f', '#00007f', '#3f007f', '#7f007f', '#7f003f', '#ffaaaa', '#ffd4aa', '#ffffaa', '#d4ffaa', '#aaffaa', '#aaffd4', '#aaffff', '#aad4ff', '#aaaaff', '#d4aaff', '#ffaaff', '#ffaad4' ], modKey = (isMac() ? 'meta+' : 'ctrl+'), // ⌘ path = svgCanvas.pathActions, {undoMgr} = svgCanvas, workarea = $('#workarea'), canvMenu = $('#cmenu_canvas'), // layerMenu = $('#cmenu_layers'), // Unused paintBox = {fill: null, stroke: null}; let resizeTimer, curScrollPos; let exportWindow = null, defaultImageURL = curConfig.imgPath + 'logo.png', zoomInIcon = 'crosshair', zoomOutIcon = 'crosshair', uiContext = 'toolbars'; // For external openers (function () { // let the opener know SVG Edit is ready (now that config is set up) const w = window.opener || window.parent; if (w) { try { /** * Triggered on a containing `document` (of `window.opener` * or `window.parent`) when the editor is loaded. * @event module:SVGEditor#event:svgEditorReadyEvent * @type {Event} * @property {true} bubbles * @property {true} cancelable */ /** * @name module:SVGEditor.svgEditorReadyEvent * @type {module:SVGEditor#event:svgEditorReadyEvent} */ const svgEditorReadyEvent = new w.CustomEvent('svgEditorReady', { bubbles: true, cancelable: true }); w.document.documentElement.dispatchEvent(svgEditorReadyEvent); } catch (e) {} } }()); /** * * @returns {undefined} */ const setSelectMode = function () { const curr = $('.tool_button_current'); if (curr.length && curr[0].id !== 'tool_select') { curr.removeClass('tool_button_current').addClass('tool_button'); $('#tool_select').addClass('tool_button_current').removeClass('tool_button'); $('#styleoverrides').text(` #svgcanvas svg * { cursor: move; pointer-events: all; } #svgcanvas svg { cursor: default; } `); } svgCanvas.setMode('select'); workarea.css('cursor', 'auto'); }; // used to make the flyouts stay on the screen longer the very first time // const flyoutspeed = 1250; // Currently unused // let textBeingEntered = false; // Currently unused const origTitle = $('title:first').text(); // Make [1,2,5] array const rIntervals = []; for (let i = 0.1; i < 1E5; i *= 10) { rIntervals.push(i); rIntervals.push(2 * i); rIntervals.push(5 * i); } /** * This function highlights the layer passed in (by fading out the other layers). * If no layer is passed in, this function restores the other layers * @param {string} [layerNameToHighlight] * @returns {undefined} */ const toggleHighlightLayer = function (layerNameToHighlight) { let i; const curNames = [], numLayers = svgCanvas.getCurrentDrawing().getNumLayers(); for (i = 0; i < numLayers; i++) { curNames[i] = svgCanvas.getCurrentDrawing().getLayerName(i); } if (layerNameToHighlight) { curNames.forEach((curName) => { if (curName !== layerNameToHighlight) { svgCanvas.getCurrentDrawing().setLayerOpacity(curName, 0.5); } }); } else { curNames.forEach((curName) => { svgCanvas.getCurrentDrawing().setLayerOpacity(curName, 1.0); }); } }; /** * * @returns {undefined} */ const populateLayers = function () { svgCanvas.clearSelection(); const layerlist = $('#layerlist tbody').empty(); const selLayerNames = $('#selLayerNames').empty(); const drawing = svgCanvas.getCurrentDrawing(); const currentLayerName = drawing.getCurrentLayerName(); const icon = $.getSvgIcon('eye'); let layer = svgCanvas.getCurrentDrawing().getNumLayers(); // we get the layers in the reverse z-order (the layer rendered on top is listed first) while (layer--) { const name = drawing.getLayerName(layer); const layerTr = $('