/* globals jQuery */ /* * svg-editor.js * * Licensed under the MIT License * * Copyright(c) 2010 Alexis Deveria * Copyright(c) 2010 Pavol Rusnak * Copyright(c) 2010 Jeff Schiller * Copyright(c) 2010 Narendra Sisodiya * Copyright(c) 2014 Brett Zamir * */ import './touch.js'; import {NS} from './svgedit.js'; import {isWebkit, isGecko, isIE, isMac, isTouch} from './browser.js'; import * as Utils from './svgutils.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 jqPluginJSHotkeys from './js-hotkeys/jquery.hotkeys.min.js'; import jqPluginBBQ from './jquerybbq/jquery.bbq.min.js'; import jqPluginSVGIcons from './svgicons/jquery.svgicons.js'; import jqPluginJGraduate from './jgraduate/jquery.jgraduate.js'; import jqPluginSpinBtn from './spinbtn/JQuerySpinBtn.js'; import jqPluginSVG from './jquery-svg.js'; // Needed for SVG attribute setting and array form with `attr` import jqPluginContextMenu from './contextmenu/jquery.contextMenu.js'; import jqPluginJPicker from './jgraduate/jpicker.js'; import { readLang, putLocale, init as localeInit } from './locale/locale.js'; import loadStylesheets from './external/load-stylesheets/index-es.js'; const $ = [ jqPluginJSHotkeys, jqPluginBBQ, jqPluginSVGIcons, jqPluginJGraduate, jqPluginSpinBtn, jqPluginSVG, jqPluginContextMenu, jqPluginJPicker ].reduce(($, cb) => cb($), 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}]); } const editor = {}; // EDITOR PROPERTIES: (defined below) // curPrefs, curConfig, canvas, storage, uiStrings // // STATE MAINTENANCE PROPERTIES editor.tool_scale = 1; // Dependent on icon size, so any use to making configurable instead? Used by JQuerySpinBtn.js editor.exportWindowCt = 0; editor.langChanged = false; editor.showSaveWarning = false; editor.storagePromptClosed = false; // For use with ext-storage.js const callbacks = [], /** * PREFS AND CONFIG */ // The iteration algorithm for defaultPrefs does not currently support array/objects defaultPrefs = { // EDITOR OPTIONS (DIALOG) lang: '', // Default to "en" if locale.js detection does not detect another language iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise 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 }, defaultExtensions = [ 'ext-overview_window.js', 'ext-markers.js', 'ext-connector.js', 'ext-eyedropper.js', 'ext-shapes.js', 'ext-imagelib.js', 'ext-grid.js', 'ext-polygon.js', 'ext-star.js', 'ext-panning.js', 'ext-storage.js' ], defaultConfig = { // Todo: svgcanvas.js also sets and checks: show_outside_canvas, selectNew; add here? // Change the following to preferences and add pref controls to the UI (e.g., initTool, wireframe, showlayers)? 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) imgPath: 'images/', langPath: 'locale/', // Default will be changed if this is a modular load extPath: 'extensions/', // Default will be changed if this is a modular load canvgPath: 'canvg/', // Default will be changed if this is a modular load jspdfPath: 'jspdf/', // Default will be changed if this is a modular load extIconsPath: 'extensions/', jGraduatePath: 'jgraduate/images/', // 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 */ 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 window.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(window.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: [] }; function loadSvgString (str, callback) { const success = svgCanvas.setSvgString(str) !== false; callback = callback || $.noop; if (success) { callback(true); // eslint-disable-line standard/no-callback-literal } else { $.alert(uiStrings.notification.errorLoadingSVG, function () { callback(false); // eslint-disable-line standard/no-callback-literal }); } } /** * 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} 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; editor.curPrefs = curPrefs; // Update exported value return; } 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; /** * 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 */ editor.loadContentAndPrefs = function () { if (!curConfig.forceStorage && (curConfig.noStorageOnLoad || !document.cookie.match(/(?:^|;\s*)store=(?: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*)store=prefsAndContent/)) ) ) { const name = 'svgedit-' + curConfig.canvasName; const cached = editor.storage.getItem(name); if (cached) { editor.loadFromString(cached); } } // LOAD PREFS for (const key in defaultPrefs) { if (defaultPrefs.hasOwnProperty(key)) { // It's our own config, so we don't need to iterate up the prototype chain 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 {Object} opts The preferences or configuration (including extensions) * @param {Object} [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`. */ editor.setConfig = function (opts, cfgCfg) { cfgCfg = cfgCfg || {}; 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 (opts.hasOwnProperty(key)) { // Only allow prefs defined in defaultPrefs if (defaultPrefs.hasOwnProperty(key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || curPrefs.hasOwnProperty(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 (defaultConfig.hasOwnProperty(key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || curConfig.hasOwnProperty(key) )) { return; } // Potentially overwriting of previously set config if (curConfig.hasOwnProperty(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; } } } } } }); editor.curConfig = curConfig; // Update exported value }; /** * @param {Object} opts Extension mechanisms may call setCustomHandlers with three functions: opts.open, opts.save, and opts.exportImage * opts.open's responsibilities are: * - invoke a file chooser dialog in 'open' mode * - let user pick a SVG file * - calls svgCanvas.setSvgString() with the string contents of that file * opts.save's 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 * opts.exportImage's 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. */ 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 } }); }; editor.randomizeIds = function () { svgCanvas.randomizeIds(arguments); }; 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 editor.storage = localStorage; } } catch (err) {} // Todo: Avoid var-defined functions and group functions together, etc. where possible const goodLangs = []; $('#lang_select option').each(function () { goodLangs.push(this.value); }); 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; } 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 $.each(['extensions', 'stylesheets', 'allowedOrigins'], function (i, 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 $.each( [ 'extPath', 'imgPath', 'extIconsPath', 'canvgPath', 'langPath', 'jGraduatePath', 'jspdfPath' ], 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(); } setupCurPrefs(); } else { setupCurConfig(); editor.loadContentAndPrefs(); setupCurPrefs(); } })(); const setIcon = editor.setIcon = function (elem, iconId, forcedSize) { const icon = (typeof iconId === 'string') ? $.getSvgIcon(iconId, true) : iconId.clone(); if (!icon) { console.log('NOTE: Icon image missing: ' + iconId); return; } $(elem).empty().append(icon); }; const extFunc = async function () { 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; } const url = curConfig.extPath + extname; // Todo: Replace this with `return import(url);` when // `import()` widely supported const imported = await importSetGlobalDefault(url, { global: 'svgEditorExtension_' + extName[1].replace(/-/g, '_') }); const {name, init} = imported; return editor.addExtension(name, (init && init.bind(editor)) || imported); }) ); } catch (err) { console.log(err); } // const lang = ('lang' in curPrefs) ? curPrefs.lang : null; return editor.putLocale(null, goodLangs, curConfig); }; const stateObj = {tool_scale: editor.tool_scale}; const setFlyoutPositions = function () { $('.tools_flyout').each(function () { const shower = $('#' + this.id + '_show'); const pos = shower.offset(); const w = shower.outerWidth(); $(this).css({left: (pos.left + w) * editor.tool_scale, top: pos.top}); }); }; 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 ''; }()); 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 (cur == null) { cur = parseInt(el.css('margin-' + s), 10); // 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'); } }); }; 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: { new_image: 'clear.png', save: 'save.png', open: 'open.png', source: 'source.png', docprops: 'document-properties.png', wireframe: 'wireframe.png', undo: 'undo.png', redo: 'redo.png', select: 'select.png', select_node: 'select_node.png', pencil: 'fhpath.png', pen: 'line.png', square: 'square.png', rect: 'rect.png', fh_rect: 'freehand-square.png', circle: 'circle.png', ellipse: 'ellipse.png', fh_ellipse: 'freehand-circle.png', path: 'path.png', text: 'text.png', image: 'image.png', zoom: 'zoom.png', clone: 'clone.png', node_clone: 'node_clone.png', delete: 'delete.png', node_delete: 'node_delete.png', group: 'shape_group_elements.png', ungroup: 'shape_ungroup.png', move_top: 'move_top.png', move_bottom: 'move_bottom.png', to_path: 'to_path.png', link_controls: 'link_controls.png', reorient: 'reorient.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', go_up: 'go-up.png', go_down: 'go-down.png', ok: 'save.png', cancel: 'cancel.png', arrow_right: 'flyouth.png', arrow_down: 'dropdown.gif' }, 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 }, 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); } } }); function getStylesheetPriority (stylesheet) { switch (stylesheet) { case 'jgraduate/css/jPicker.css': return 1; case 'jgraduate/css/jgraduate.css': return 2; case 'svg-editor.css': return 3; case 'spinbtn/JQuerySpinBtn.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); } } loadStylesheets(stylesheets, {acceptErrors: ({stylesheetURL, reject, resolve}) => { if ($.loadingStylesheets.includes(stylesheetURL)) { reject(new Error(`Missing expected stylesheet: ${stylesheetURL}`)); return; } resolve(); }}).then(() => { $('#svg_container')[0].style.visibility = 'visible'; editor.runCallbacks(); setTimeout(function () { $('.flyout_arrow_horiz:empty').each(function () { $(this).append($.getSvgIcon('arrow_right').width(5).height(5)); }); }, 1); }); } }); 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; let svgEditorReadyEvent; if (w) { try { svgEditorReadyEvent = w.document.createEvent('Event'); svgEditorReadyEvent.initEvent('svgEditorReady', true, true); w.document.documentElement.dispatchEvent(svgEditorReadyEvent); } catch (e) {} } }()); // This sets up alternative dialog boxes. They mostly work the same way as // their UI counterparts, expect instead of returning the result, a callback // needs to be included that returns the result as its first parameter. // In the future we may want to add additional types of dialog boxes, since // they should be easy to handle this way. (function () { $('#dialog_container').draggable({ cancel: '#dialog_content, #dialog_buttons *', containment: 'window' }).css('position', 'absolute'); const box = $('#dialog_box'), btnHolder = $('#dialog_buttons'), dialogContent = $('#dialog_content'), dbox = function (type, msg, callback, defaultVal, opts, changeCb, checkbox) { dialogContent.html('
' + msg.replace(/\n/g, '
') + '
') .toggleClass('prompt', (type === 'prompt')); btnHolder.empty(); const ok = $('').appendTo(btnHolder); if (type !== 'alert') { $('') .appendTo(btnHolder) .click(function () { box.hide(); if (callback) { callback(false); // eslint-disable-line standard/no-callback-literal } }); } let ctrl, chkbx; if (type === 'prompt') { ctrl = $('').prependTo(btnHolder); ctrl.val(defaultVal || ''); ctrl.bind('keydown', 'return', function () { ok.click(); }); } else if (type === 'select') { const div = $('