').append(btn.title);\n }\n } else if (btn.list) {\n // Add button to list\n button.addClass('push_button');\n $('#' + btn.list + '_opts').append(button);\n if (btn.isDefault) {\n $('#cur_' + btn.list).append(button.children().clone());\n const svgicon = btn.svgicon || btn.id;\n placementObj['#cur_' + btn.list] = svgicon;\n }\n } else if (btn.includeWith) {\n // Add to flyout menu / make flyout menu\n const opts = btn.includeWith;\n // opts.button, default, position\n refBtn = $(opts.button);\n\n flyoutHolder = refBtn.parent();\n // Create a flyout menu if there isn't one already\n let tlsId;\n if (!refBtn.parent().hasClass('tools_flyout')) {\n // Create flyout placeholder\n tlsId = refBtn[0].id.replace('tool_', 'tools_');\n showBtn = refBtn.clone()\n .attr('id', tlsId + '_show')\n .append($('
', {class: 'flyout_arrow_horiz'}));\n\n refBtn.before(showBtn);\n // Create a flyout div\n flyoutHolder = makeFlyoutHolder(tlsId, refBtn);\n }\n\n refData = Actions.getButtonData(opts.button);\n\n if (opts.isDefault) {\n placementObj['#' + tlsId + '_show'] = btn.id;\n }\n // TODO: Find way to set the current icon using the iconloader if this is not default\n\n // Include data for extension button as well as ref button\n const curH = holders['#' + flyoutHolder[0].id] = [{\n sel: '#' + id,\n fn: btn.events.click,\n icon: btn.id,\n key: btn.key,\n isDefault: Boolean(btn.includeWith && btn.includeWith.isDefault)\n }, refData];\n\n // {sel:'#tool_rect', fn: clickRect, evt: 'mouseup', key: 4, parent: '#tools_rect', icon: 'rect'}\n\n const pos = ('position' in opts) ? opts.position : 'last';\n const len = flyoutHolder.children().length;\n\n // Add at given position or end\n if (!isNaN(pos) && pos >= 0 && pos < len) {\n flyoutHolder.children().eq(pos).before(button);\n } else {\n flyoutHolder.append(button);\n curH.reverse();\n }\n }\n\n if (!svgicons) {\n button.append(icon);\n }\n\n if (!btn.list) {\n // Add given events to button\n $.each(btn.events, function (name, func) {\n if (name === 'click' && btn.type === 'mode') {\n // `touch.js` changes `touchstart` to `mousedown`,\n // so we must map extension click events as well\n if (isTouch() && name === 'click') {\n name = 'mousedown';\n }\n if (btn.includeWith) {\n button.bind(name, func);\n } else {\n button.bind(name, function () {\n if (toolButtonClick(button)) {\n func();\n }\n });\n }\n if (btn.key) {\n $(document).bind('keydown', btn.key, func);\n if (btn.title) {\n button.attr('title', btn.title + ' [' + btn.key + ']');\n }\n }\n } else {\n button.bind(name, func);\n }\n });\n }\n\n setupFlyouts(holders);\n });\n\n $.each(btnSelects, function () {\n addAltDropDown(this.elem, this.list, this.callback, {seticon: true});\n });\n\n if (svgicons) {\n return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new\n $.svgIcons(svgicons, {\n w: 24, h: 24,\n id_match: false,\n no_img: (!isWebkit()),\n fallback: fallbackObj,\n placement: placementObj,\n callback (icons) {\n // Non-ideal hack to make the icon match the current size\n // if (curPrefs.iconsize && curPrefs.iconsize !== 'm') {\n if ($.pref('iconsize') !== 'm') {\n prepResize();\n }\n runCallback();\n resolve();\n }\n });\n });\n }\n }\n return runCallback();\n };\n\n /**\n * @param {string} color\n * @param {Float} opac\n * @param {string} type\n * @returns {module:jGraduate~Paint}\n */\n const getPaint = function (color, opac, type) {\n // update the editor's fill paint\n const opts = {alpha: opac};\n if (color.startsWith('url(#')) {\n let refElem = svgCanvas.getRefElem(color);\n if (refElem) {\n refElem = refElem.cloneNode(true);\n } else {\n refElem = $('#' + type + '_color defs *')[0];\n }\n opts[refElem.tagName] = refElem;\n } else if (color.startsWith('#')) {\n opts.solidColor = color.substr(1);\n } else {\n opts.solidColor = 'none';\n }\n return new $.jGraduate.Paint(opts);\n };\n\n // $('#text').focus(function () { textBeingEntered = true; });\n // $('#text').blur(function () { textBeingEntered = false; });\n\n // bind the selected event to our function that handles updates to the UI\n svgCanvas.bind('selected', selectedChanged);\n svgCanvas.bind('transition', elementTransition);\n svgCanvas.bind('changed', elementChanged);\n svgCanvas.bind('saved', saveHandler);\n svgCanvas.bind('exported', exportHandler);\n svgCanvas.bind('exportedPDF', function (win, data) {\n if (!data.output) { // Ignore Chrome\n return;\n }\n const {exportWindowName} = data;\n if (exportWindowName) {\n exportWindow = window.open('', exportWindowName); // A hack to get the window via JSON-able name without opening a new one\n }\n if (!exportWindow || exportWindow.closed) {\n /* await */ $.alert(uiStrings.notification.popupWindowBlocked);\n return;\n }\n exportWindow.location.href = data.output;\n });\n svgCanvas.bind('zoomed', zoomChanged);\n svgCanvas.bind('zoomDone', zoomDone);\n svgCanvas.bind(\n 'updateCanvas',\n /**\n * @param {external:Window} win\n * @param {PlainObject} centerInfo\n * @param {false} centerInfo.center\n * @param {module:math.XYObject} centerInfo.newCtr\n * @listens module:svgcanvas.SvgCanvas#event:updateCanvas\n * @returns {undefined}\n */\n function (win, {center, newCtr}) {\n updateCanvas(center, newCtr);\n }\n );\n svgCanvas.bind('contextset', contextChanged);\n svgCanvas.bind('extension_added', extAdded);\n svgCanvas.textActions.setInputElem($('#text')[0]);\n\n let str = '
';\n $.each(palette, function (i, item) {\n str += '
';\n });\n $('#palette').append(str);\n\n // Set up editor background functionality\n // TODO add checkerboard as \"pattern\"\n const colorBlocks = ['#FFF', '#888', '#000']; // ,'url(data:image/gif;base64,R0lGODlhEAAQAIAAAP%2F%2F%2F9bW1iH5BAAAAAAALAAAAAAQABAAAAIfjG%2Bgq4jM3IFLJgpswNly%2FXkcBpIiVaInlLJr9FZWAQA7)'];\n str = '';\n $.each(colorBlocks, function () {\n str += '
';\n });\n $('#bg_blocks').append(str);\n const blocks = $('#bg_blocks div');\n const curBg = 'cur_background';\n blocks.each(function () {\n const blk = $(this);\n blk.click(function () {\n blocks.removeClass(curBg);\n $(this).addClass(curBg);\n });\n });\n\n setBackground($.pref('bkgd_color'), $.pref('bkgd_url'));\n\n $('#image_save_opts input').val([$.pref('img_save')]);\n\n /**\n * @implements {module:jQuerySpinButton.ValueCallback}\n */\n const changeRectRadius = function (ctl) {\n svgCanvas.setRectRadius(ctl.value);\n };\n\n /**\n * @implements {module:jQuerySpinButton.ValueCallback}\n */\n const changeFontSize = function (ctl) {\n svgCanvas.setFontSize(ctl.value);\n };\n\n /**\n * @implements {module:jQuerySpinButton.ValueCallback}\n */\n const changeStrokeWidth = function (ctl) {\n let val = ctl.value;\n if (val === 0 && selectedElement && ['line', 'polyline'].includes(selectedElement.nodeName)) {\n val = ctl.value = 1;\n }\n svgCanvas.setStrokeWidth(val);\n };\n\n /**\n * @implements {module:jQuerySpinButton.ValueCallback}\n */\n const changeRotationAngle = function (ctl) {\n svgCanvas.setRotationAngle(ctl.value);\n $('#tool_reorient').toggleClass('disabled', parseInt(ctl.value) === 0);\n };\n\n /**\n * @param {external:jQuery.fn.SpinButton} ctl Spin Button\n * @param {string} [val=ctl.value]\n * @returns {undefined}\n */\n const changeOpacity = function (ctl, val) {\n if (Utils.isNullish(val)) { val = ctl.value; }\n $('#group_opacity').val(val);\n if (!ctl || !ctl.handle) {\n $('#opac_slider').slider('option', 'value', val);\n }\n svgCanvas.setOpacity(val / 100);\n };\n\n /**\n * @param {external:jQuery.fn.SpinButton} ctl Spin Button\n * @param {string} [val=ctl.value]\n * @param {boolean} noUndo\n * @returns {undefined}\n */\n const changeBlur = function (ctl, val, noUndo) {\n if (Utils.isNullish(val)) { val = ctl.value; }\n $('#blur').val(val);\n let complete = false;\n if (!ctl || !ctl.handle) {\n $('#blur_slider').slider('option', 'value', val);\n complete = true;\n }\n if (noUndo) {\n svgCanvas.setBlurNoUndo(val);\n } else {\n svgCanvas.setBlur(val, complete);\n }\n };\n\n $('#stroke_style').change(function () {\n svgCanvas.setStrokeAttr('stroke-dasharray', $(this).val());\n operaRepaint();\n });\n\n $('#stroke_linejoin').change(function () {\n svgCanvas.setStrokeAttr('stroke-linejoin', $(this).val());\n operaRepaint();\n });\n\n // Lose focus for select elements when changed (Allows keyboard shortcuts to work better)\n $('select').change(function () { $(this).blur(); });\n\n // fired when user wants to move elements to another layer\n let promptMoveLayerOnce = false;\n $('#selLayerNames').change(async function () {\n const destLayer = this.options[this.selectedIndex].value;\n const confirmStr = uiStrings.notification.QmoveElemsToLayer.replace('%s', destLayer);\n /**\n * @param {boolean} ok\n * @returns {undefined}\n */\n const moveToLayer = function (ok) {\n if (!ok) { return; }\n promptMoveLayerOnce = true;\n svgCanvas.moveSelectedToLayer(destLayer);\n svgCanvas.clearSelection();\n populateLayers();\n };\n if (destLayer) {\n if (promptMoveLayerOnce) {\n moveToLayer(true);\n } else {\n const ok = await $.confirm(confirmStr);\n if (!ok) {\n return;\n }\n moveToLayer(true);\n }\n }\n });\n\n $('#font_family').change(function () {\n svgCanvas.setFontFamily(this.value);\n });\n\n $('#seg_type').change(function () {\n svgCanvas.setSegType($(this).val());\n });\n\n $('#text').bind('keyup input', function () {\n svgCanvas.setTextContent(this.value);\n });\n\n $('#image_url').change(function () {\n setImageURL(this.value);\n });\n\n $('#link_url').change(function () {\n if (this.value.length) {\n svgCanvas.setLinkURL(this.value);\n } else {\n svgCanvas.removeHyperlink();\n }\n });\n\n $('#g_title').change(function () {\n svgCanvas.setGroupTitle(this.value);\n });\n\n $('.attr_changer').change(function () {\n const attr = this.getAttribute('data-attr');\n let val = this.value;\n const valid = isValidUnit(attr, val, selectedElement);\n\n if (!valid) {\n this.value = selectedElement.getAttribute(attr);\n /* await */ $.alert(uiStrings.notification.invalidAttrValGiven);\n return false;\n }\n\n if (attr !== 'id' && attr !== 'class') {\n if (isNaN(val)) {\n val = svgCanvas.convertToNum(attr, val);\n } else if (curConfig.baseUnit !== 'px') {\n // Convert unitless value to one with given unit\n\n const unitData = getTypeMap();\n\n if (selectedElement[attr] || svgCanvas.getMode() === 'pathedit' || attr === 'x' || attr === 'y') {\n val *= unitData[curConfig.baseUnit];\n }\n }\n }\n\n // if the user is changing the id, then de-select the element first\n // change the ID, then re-select it with the new ID\n if (attr === 'id') {\n const elem = selectedElement;\n svgCanvas.clearSelection();\n elem.id = val;\n svgCanvas.addToSelection([elem], true);\n } else {\n svgCanvas.changeSelectedAttribute(attr, val);\n }\n this.blur();\n return true;\n });\n\n // Prevent selection of elements when shift-clicking\n $('#palette').mouseover(function () {\n const inp = $('
');\n $(this).append(inp);\n inp.focus().remove();\n });\n\n $('.palette_item').mousedown(function (evt) {\n // shift key or right click for stroke\n const picker = evt.shiftKey || evt.button === 2 ? 'stroke' : 'fill';\n let color = $(this).data('rgb');\n let paint;\n\n // Webkit-based browsers returned 'initial' here for no stroke\n if (color === 'none' || color === 'transparent' || color === 'initial') {\n color = 'none';\n paint = new $.jGraduate.Paint();\n } else {\n paint = new $.jGraduate.Paint({alpha: 100, solidColor: color.substr(1)});\n }\n\n paintBox[picker].setPaint(paint);\n svgCanvas.setColor(picker, color);\n\n if (color !== 'none' && svgCanvas.getPaintOpacity(picker) !== 1) {\n svgCanvas.setPaintOpacity(picker, 1.0);\n }\n updateToolButtonState();\n }).bind('contextmenu', function (e) { e.preventDefault(); });\n\n $('#toggle_stroke_tools').on('click', function () {\n $('#tools_bottom').toggleClass('expanded');\n });\n\n (function () {\n const wArea = workarea[0];\n\n let lastX = null, lastY = null,\n panning = false, keypan = false;\n\n $('#svgcanvas').bind('mousemove mouseup', function (evt) {\n if (panning === false) { return true; }\n\n wArea.scrollLeft -= (evt.clientX - lastX);\n wArea.scrollTop -= (evt.clientY - lastY);\n\n lastX = evt.clientX;\n lastY = evt.clientY;\n\n if (evt.type === 'mouseup') { panning = false; }\n return false;\n }).mousedown(function (evt) {\n if (evt.button === 1 || keypan === true) {\n panning = true;\n lastX = evt.clientX;\n lastY = evt.clientY;\n return false;\n }\n return true;\n });\n\n $(window).mouseup(function () {\n panning = false;\n });\n\n $(document).bind('keydown', 'space', function (evt) {\n svgCanvas.spaceKey = keypan = true;\n evt.preventDefault();\n }).bind('keyup', 'space', function (evt) {\n evt.preventDefault();\n svgCanvas.spaceKey = keypan = false;\n }).bind('keydown', 'shift', function (evt) {\n if (svgCanvas.getMode() === 'zoom') {\n workarea.css('cursor', zoomOutIcon);\n }\n }).bind('keyup', 'shift', function (evt) {\n if (svgCanvas.getMode() === 'zoom') {\n workarea.css('cursor', zoomInIcon);\n }\n });\n\n /**\n * @param {boolean} active\n * @returns {undefined}\n */\n editor.setPanning = function (active) {\n svgCanvas.spaceKey = keypan = active;\n };\n }());\n\n (function () {\n const button = $('#main_icon');\n const overlay = $('#main_icon span');\n const list = $('#main_menu');\n\n let onButton = false;\n let height = 0;\n let jsHover = true;\n let setClick = false;\n\n /*\n // Currently unused\n const hideMenu = function () {\n list.fadeOut(200);\n };\n */\n\n $(window).mouseup(function (evt) {\n if (!onButton) {\n button.removeClass('buttondown');\n // do not hide if it was the file input as that input needs to be visible\n // for its change event to fire\n if (evt.target.tagName !== 'INPUT') {\n list.fadeOut(200);\n } else if (!setClick) {\n setClick = true;\n $(evt.target).click(function () {\n list.css('margin-left', '-9999px').show();\n });\n }\n }\n onButton = false;\n }).mousedown(function (evt) {\n // $('.contextMenu').hide();\n const islib = $(evt.target).closest('div.tools_flyout, .contextMenu').length;\n if (!islib) {\n $('.tools_flyout:visible,.contextMenu').fadeOut(250);\n }\n });\n\n overlay.bind('mousedown', function () {\n if (!button.hasClass('buttondown')) {\n // Margin must be reset in case it was changed before;\n list.css('margin-left', 0).show();\n if (!height) {\n height = list.height();\n }\n // Using custom animation as slideDown has annoying 'bounce effect'\n list.css('height', 0).animate({\n height\n }, 200);\n onButton = true;\n } else {\n list.fadeOut(200);\n }\n button.toggleClass('buttondown buttonup');\n }).hover(function () {\n onButton = true;\n }).mouseout(function () {\n onButton = false;\n });\n\n const listItems = $('#main_menu li');\n\n // Check if JS method of hovering needs to be used (Webkit bug)\n listItems.mouseover(function () {\n jsHover = ($(this).css('background-color') === 'rgba(0, 0, 0, 0)');\n\n listItems.unbind('mouseover');\n if (jsHover) {\n listItems.mouseover(function () {\n this.style.backgroundColor = '#FFC';\n }).mouseout(function () {\n this.style.backgroundColor = 'transparent';\n return true;\n });\n }\n });\n }());\n // Made public for UI customization.\n // TODO: Group UI functions into a public editor.ui interface.\n /**\n * See {@link http://api.jquery.com/bind/#bind-eventType-eventData-handler}\n * @callback module:SVGEditor.DropDownCallback\n * @param {external:jQuery.Event} ev See {@link http://api.jquery.com/Types/#Event}\n * @listens external:jQuery.Event\n * @returns {undefined|boolean} Calls `preventDefault()` and `stopPropagation()`\n */\n /**\n * @param {Element|string} elem DOM Element or selector\n * @param {module:SVGEditor.DropDownCallback} callback Mouseup callback\n * @param {boolean} dropUp\n * @returns {undefined}\n */\n editor.addDropDown = function (elem, callback, dropUp) {\n if (!$(elem).length) { return; } // Quit if called on non-existent element\n const button = $(elem).find('button');\n const list = $(elem).find('ul').attr('id', $(elem)[0].id + '-list');\n if (dropUp) {\n $(elem).addClass('dropup');\n } else {\n // Move list to place where it can overflow container\n $('#option_lists').append(list);\n }\n list.find('li').bind('mouseup', callback);\n\n let onButton = false;\n $(window).mouseup(function (evt) {\n if (!onButton) {\n button.removeClass('down');\n list.hide();\n }\n onButton = false;\n });\n\n button.bind('mousedown', function () {\n if (!button.hasClass('down')) {\n if (!dropUp) {\n const pos = $(elem).position();\n list.css({\n top: pos.top + 24,\n left: pos.left - 10\n });\n }\n list.show();\n onButton = true;\n } else {\n list.hide();\n }\n button.toggleClass('down');\n }).hover(function () {\n onButton = true;\n }).mouseout(function () {\n onButton = false;\n });\n };\n\n editor.addDropDown('#font_family_dropdown', function () {\n $('#font_family').val($(this).text()).change();\n });\n\n editor.addDropDown('#opacity_dropdown', function () {\n if ($(this).find('div').length) { return; }\n const perc = parseInt($(this).text().split('%')[0]);\n changeOpacity(false, perc);\n }, true);\n\n // For slider usage, see: http://jqueryui.com/demos/slider/\n $('#opac_slider').slider({\n start () {\n $('#opacity_dropdown li:not(.special)').hide();\n },\n stop () {\n $('#opacity_dropdown li').show();\n $(window).mouseup();\n },\n slide (evt, ui) {\n changeOpacity(ui);\n }\n });\n\n editor.addDropDown('#blur_dropdown', $.noop);\n\n let slideStart = false;\n $('#blur_slider').slider({\n max: 10,\n step: 0.1,\n stop (evt, ui) {\n slideStart = false;\n changeBlur(ui);\n $('#blur_dropdown li').show();\n $(window).mouseup();\n },\n start () {\n slideStart = true;\n },\n slide (evt, ui) {\n changeBlur(ui, null, slideStart);\n }\n });\n\n editor.addDropDown('#zoom_dropdown', function () {\n const item = $(this);\n const val = item.data('val');\n if (val) {\n zoomChanged(window, val);\n } else {\n changeZoom({value: parseFloat(item.text())});\n }\n }, true);\n\n addAltDropDown('#stroke_linecap', '#linecap_opts', function () {\n setStrokeOpt(this, true);\n }, {dropUp: true});\n\n addAltDropDown('#stroke_linejoin', '#linejoin_opts', function () {\n setStrokeOpt(this, true);\n }, {dropUp: true});\n\n addAltDropDown('#tool_position', '#position_opts', function () {\n const letter = this.id.replace('tool_pos', '').charAt(0);\n svgCanvas.alignSelectedElements(letter, 'page');\n }, {multiclick: true});\n\n /*\n\n When a flyout icon is selected\n (if flyout) {\n - Change the icon\n - Make pressing the button run its stuff\n }\n - Run its stuff\n\n When its shortcut key is pressed\n - If not current in list, do as above\n , else:\n - Just run its stuff\n\n */\n\n // Unfocus text input when workarea is mousedowned.\n (function () {\n let inp;\n /**\n *\n * @returns {undefined}\n */\n const unfocus = function () {\n $(inp).blur();\n };\n\n $('#svg_editor').find('button, select, input:not(#text)').focus(function () {\n inp = this; // eslint-disable-line consistent-this\n uiContext = 'toolbars';\n workarea.mousedown(unfocus);\n }).blur(function () {\n uiContext = 'canvas';\n workarea.unbind('mousedown', unfocus);\n // Go back to selecting text if in textedit mode\n if (svgCanvas.getMode() === 'textedit') {\n $('#text').focus();\n }\n });\n }());\n\n /**\n *\n * @returns {undefined}\n */\n const clickFHPath = function () {\n if (toolButtonClick('#tool_fhpath')) {\n svgCanvas.setMode('fhpath');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickLine = function () {\n if (toolButtonClick('#tool_line')) {\n svgCanvas.setMode('line');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickSquare = function () {\n if (toolButtonClick('#tool_square')) {\n svgCanvas.setMode('square');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickRect = function () {\n if (toolButtonClick('#tool_rect')) {\n svgCanvas.setMode('rect');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickFHRect = function () {\n if (toolButtonClick('#tool_fhrect')) {\n svgCanvas.setMode('fhrect');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickCircle = function () {\n if (toolButtonClick('#tool_circle')) {\n svgCanvas.setMode('circle');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickEllipse = function () {\n if (toolButtonClick('#tool_ellipse')) {\n svgCanvas.setMode('ellipse');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickFHEllipse = function () {\n if (toolButtonClick('#tool_fhellipse')) {\n svgCanvas.setMode('fhellipse');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickImage = function () {\n if (toolButtonClick('#tool_image')) {\n svgCanvas.setMode('image');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickZoom = function () {\n if (toolButtonClick('#tool_zoom')) {\n svgCanvas.setMode('zoom');\n workarea.css('cursor', zoomInIcon);\n }\n };\n\n /**\n * @param {Float} multiplier\n * @returns {undefined}\n */\n const zoomImage = function (multiplier) {\n const res = svgCanvas.getResolution();\n multiplier = multiplier ? res.zoom * multiplier : 1;\n // setResolution(res.w * multiplier, res.h * multiplier, true);\n $('#zoom').val(multiplier * 100);\n svgCanvas.setZoom(multiplier);\n zoomDone();\n updateCanvas(true);\n };\n\n /**\n *\n * @returns {undefined}\n */\n const dblclickZoom = function () {\n if (toolButtonClick('#tool_zoom')) {\n zoomImage();\n setSelectMode();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickText = function () {\n if (toolButtonClick('#tool_text')) {\n svgCanvas.setMode('text');\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickPath = function () {\n if (toolButtonClick('#tool_path')) {\n svgCanvas.setMode('path');\n }\n };\n\n /**\n * Delete is a contextual tool that only appears in the ribbon if\n * an element has been selected.\n * @returns {undefined}\n */\n const deleteSelected = function () {\n if (!Utils.isNullish(selectedElement) || multiselected) {\n svgCanvas.deleteSelectedElements();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const cutSelected = function () {\n if (!Utils.isNullish(selectedElement) || multiselected) {\n svgCanvas.cutSelectedElements();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const copySelected = function () {\n if (!Utils.isNullish(selectedElement) || multiselected) {\n svgCanvas.copySelectedElements();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const pasteInCenter = function () {\n const zoom = svgCanvas.getZoom();\n const x = (workarea[0].scrollLeft + workarea.width() / 2) / zoom - svgCanvas.contentW;\n const y = (workarea[0].scrollTop + workarea.height() / 2) / zoom - svgCanvas.contentH;\n svgCanvas.pasteElements('point', x, y);\n };\n\n /**\n *\n * @returns {undefined}\n */\n const moveToTopSelected = function () {\n if (!Utils.isNullish(selectedElement)) {\n svgCanvas.moveToTopSelectedElement();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const moveToBottomSelected = function () {\n if (!Utils.isNullish(selectedElement)) {\n svgCanvas.moveToBottomSelectedElement();\n }\n };\n\n /**\n * @param {\"Up\"|\"Down\"} dir\n * @returns {undefined}\n */\n const moveUpDownSelected = function (dir) {\n if (!Utils.isNullish(selectedElement)) {\n svgCanvas.moveUpDownSelected(dir);\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const convertToPath = function () {\n if (!Utils.isNullish(selectedElement)) {\n svgCanvas.convertToPath();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const reorientPath = function () {\n if (!Utils.isNullish(selectedElement)) {\n path.reorient();\n }\n };\n\n /**\n *\n * @returns {Promise} Resolves to `undefined`\n */\n const makeHyperlink = async function () {\n if (!Utils.isNullish(selectedElement) || multiselected) {\n const url = await $.prompt(uiStrings.notification.enterNewLinkURL, 'http://');\n if (url) {\n svgCanvas.makeHyperlink(url);\n }\n }\n };\n\n /**\n * @param {Float} dx\n * @param {Float} dy\n * @returns {undefined}\n */\n const moveSelected = function (dx, dy) {\n if (!Utils.isNullish(selectedElement) || multiselected) {\n if (curConfig.gridSnapping) {\n // Use grid snap value regardless of zoom level\n const multi = svgCanvas.getZoom() * curConfig.snappingStep;\n dx *= multi;\n dy *= multi;\n }\n svgCanvas.moveSelectedElements(dx, dy);\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const linkControlPoints = function () {\n $('#tool_node_link').toggleClass('push_button_pressed tool_button');\n const linked = $('#tool_node_link').hasClass('push_button_pressed');\n path.linkControlPoints(linked);\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clonePathNode = function () {\n if (path.getNodePoint()) {\n path.clonePathNode();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const deletePathNode = function () {\n if (path.getNodePoint()) {\n path.deletePathNode();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const addSubPath = function () {\n const button = $('#tool_add_subpath');\n const sp = !button.hasClass('push_button_pressed');\n button.toggleClass('push_button_pressed tool_button');\n path.addSubPath(sp);\n };\n\n /**\n *\n * @returns {undefined}\n */\n const opencloseSubPath = function () {\n path.opencloseSubPath();\n };\n\n /**\n *\n * @returns {undefined}\n */\n const selectNext = function () {\n svgCanvas.cycleElement(1);\n };\n\n /**\n *\n * @returns {undefined}\n */\n const selectPrev = function () {\n svgCanvas.cycleElement(0);\n };\n\n /**\n * @param {0|1} cw\n * @param {Integer} step\n * @returns {undefined}\n */\n const rotateSelected = function (cw, step) {\n if (Utils.isNullish(selectedElement) || multiselected) { return; }\n if (!cw) { step *= -1; }\n const angle = parseFloat($('#angle').val()) + step;\n svgCanvas.setRotationAngle(angle);\n updateContextPanel();\n };\n\n /**\n * @fires module:svgcanvas.SvgCanvas#event:ext-onNewDocument\n * @returns {Promise} Resolves to `undefined`\n */\n const clickClear = async function () {\n const [x, y] = curConfig.dimensions;\n const ok = await $.confirm(uiStrings.notification.QwantToClear);\n if (!ok) {\n return;\n }\n setSelectMode();\n svgCanvas.clear();\n svgCanvas.setResolution(x, y);\n updateCanvas(true);\n zoomImage();\n populateLayers();\n updateContextPanel();\n prepPaints();\n svgCanvas.runExtensions('onNewDocument');\n };\n\n /**\n *\n * @returns {false}\n */\n const clickBold = function () {\n svgCanvas.setBold(!svgCanvas.getBold());\n updateContextPanel();\n return false;\n };\n\n /**\n *\n * @returns {false}\n */\n const clickItalic = function () {\n svgCanvas.setItalic(!svgCanvas.getItalic());\n updateContextPanel();\n return false;\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickSave = function () {\n // In the future, more options can be provided here\n const saveOpts = {\n images: $.pref('img_save'),\n round_digits: 6\n };\n svgCanvas.save(saveOpts);\n };\n\n let loadingURL;\n /**\n *\n * @returns {Promise} Resolves to `undefined`\n */\n const clickExport = async function () {\n const imgType = await $.select('Select an image type for export: ', [\n // See http://kangax.github.io/jstests/toDataUrl_mime_type_test/ for a useful list of MIME types and browser support\n // 'ICO', // Todo: Find a way to preserve transparency in SVG-Edit if not working presently and do full packaging for x-icon; then switch back to position after 'PNG'\n 'PNG',\n 'JPEG', 'BMP', 'WEBP', 'PDF'\n ], function () {\n const sel = $(this);\n if (sel.val() === 'JPEG' || sel.val() === 'WEBP') {\n if (!$('#image-slider').length) {\n $(`
`).appendTo(sel.parent());\n }\n } else {\n $('#image-slider').parent().remove();\n }\n }); // todo: replace hard-coded msg with uiStrings.notification.\n if (!imgType) {\n return;\n }\n // Open placeholder window (prevents popup)\n let exportWindowName;\n\n /**\n *\n * @returns {undefined}\n */\n function openExportWindow () {\n const {loadingImage} = uiStrings.notification;\n if (curConfig.exportWindowType === 'new') {\n editor.exportWindowCt++;\n }\n exportWindowName = curConfig.canvasName + editor.exportWindowCt;\n let popHTML, popURL;\n if (loadingURL) {\n popURL = loadingURL;\n } else {\n popHTML = `\n \n
\n
${loadingImage}\n \n
${loadingImage}
\n `;\n if (typeof URL !== 'undefined' && URL.createObjectURL) {\n const blob = new Blob([popHTML], {type: 'text/html'});\n popURL = URL.createObjectURL(blob);\n } else {\n popURL = 'data:text/html;base64;charset=utf-8,' + Utils.encode64(popHTML);\n }\n loadingURL = popURL;\n }\n exportWindow = window.open(popURL, exportWindowName);\n }\n const chrome = isChrome();\n if (imgType === 'PDF') {\n if (!customExportPDF && !chrome) {\n openExportWindow();\n }\n svgCanvas.exportPDF(exportWindowName);\n } else {\n if (!customExportImage) {\n openExportWindow();\n }\n const quality = parseInt($('#image-slider').val()) / 100;\n /* const results = */ await svgCanvas.rasterExport(imgType, quality, exportWindowName);\n }\n };\n\n /**\n * By default, svgCanvas.open() is a no-op. It is up to an extension\n * mechanism (opera widget, etc.) to call `setCustomHandlers()` which\n * will make it do something.\n * @returns {undefined}\n */\n const clickOpen = function () {\n svgCanvas.open();\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickImport = function () {\n /* */\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickUndo = function () {\n if (undoMgr.getUndoStackSize() > 0) {\n undoMgr.undo();\n populateLayers();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickRedo = function () {\n if (undoMgr.getRedoStackSize() > 0) {\n undoMgr.redo();\n populateLayers();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickGroup = function () {\n // group\n if (multiselected) {\n svgCanvas.groupSelectedElements();\n // ungroup\n } else if (selectedElement) {\n svgCanvas.ungroupSelectedElement();\n }\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickClone = function () {\n svgCanvas.cloneSelectedElements(20, 20);\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickAlign = function () {\n const letter = this.id.replace('tool_align', '').charAt(0);\n svgCanvas.alignSelectedElements(letter, $('#align_relative_to').val());\n };\n\n /**\n *\n * @returns {undefined}\n */\n const clickWireframe = function () {\n $('#tool_wireframe').toggleClass('push_button_pressed tool_button');\n workarea.toggleClass('wireframe');\n\n if (supportsNonSS) { return; }\n const wfRules = $('#wireframe_rules');\n if (!wfRules.length) {\n /* wfRules = */ $('').appendTo('head');\n } else {\n wfRules.empty();\n }\n\n updateWireFrame();\n };\n\n $('#svg_docprops_container, #svg_prefs_container').draggable({\n cancel: 'button,fieldset',\n containment: 'window'\n }).css('position', 'absolute');\n\n let docprops = false;\n let preferences = false;\n\n /**\n *\n * @returns {undefined}\n */\n const showDocProperties = function () {\n if (docprops) { return; }\n docprops = true;\n\n // This selects the correct radio button by using the array notation\n $('#image_save_opts input').val([$.pref('img_save')]);\n\n // update resolution option with actual resolution\n const res = svgCanvas.getResolution();\n if (curConfig.baseUnit !== 'px') {\n res.w = convertUnit(res.w) + curConfig.baseUnit;\n res.h = convertUnit(res.h) + curConfig.baseUnit;\n }\n\n $('#canvas_width').val(res.w);\n $('#canvas_height').val(res.h);\n $('#canvas_title').val(svgCanvas.getDocumentTitle());\n\n $('#svg_docprops').show();\n };\n\n /**\n *\n * @returns {undefined}\n */\n const showPreferences = function () {\n if (preferences) { return; }\n preferences = true;\n $('#main_menu').hide();\n\n // Update background color with current one\n const canvasBg = curPrefs.bkgd_color;\n const url = $.pref('bkgd_url');\n blocks.each(function () {\n const blk = $(this);\n const isBg = blk.css('background-color') === canvasBg;\n blk.toggleClass(curBg, isBg);\n if (isBg) { $('#canvas_bg_url').removeClass(curBg); }\n });\n if (!canvasBg) { blocks.eq(0).addClass(curBg); }\n if (url) {\n $('#canvas_bg_url').val(url);\n }\n $('#grid_snapping_on').prop('checked', curConfig.gridSnapping);\n $('#grid_snapping_step').attr('value', curConfig.snappingStep);\n $('#grid_color').attr('value', curConfig.gridColor);\n\n $('#svg_prefs').show();\n };\n\n /**\n *\n * @returns {undefined}\n */\n const hideSourceEditor = function () {\n $('#svg_source_editor').hide();\n editingsource = false;\n $('#svg_source_textarea').blur();\n };\n\n /**\n *\n * @returns {Promise} Resolves to `undefined`\n */\n const saveSourceEditor = async function () {\n if (!editingsource) { return; }\n\n const saveChanges = function () {\n svgCanvas.clearSelection();\n hideSourceEditor();\n zoomImage();\n populateLayers();\n updateTitle();\n prepPaints();\n };\n\n if (!svgCanvas.setSvgString($('#svg_source_textarea').val())) {\n const ok = await $.confirm(uiStrings.notification.QerrorsRevertToSource);\n if (!ok) {\n return;\n }\n saveChanges();\n return;\n }\n saveChanges();\n setSelectMode();\n };\n\n /**\n *\n * @returns {undefined}\n */\n const hideDocProperties = function () {\n $('#svg_docprops').hide();\n $('#canvas_width,#canvas_height').removeAttr('disabled');\n $('#resolution')[0].selectedIndex = 0;\n $('#image_save_opts input').val([$.pref('img_save')]);\n docprops = false;\n };\n\n /**\n *\n * @returns {undefined}\n */\n const hidePreferences = function () {\n $('#svg_prefs').hide();\n preferences = false;\n };\n\n /**\n *\n * @returns {boolean} Whether there were problems saving the document properties\n */\n const saveDocProperties = function () {\n // set title\n const newTitle = $('#canvas_title').val();\n updateTitle(newTitle);\n svgCanvas.setDocumentTitle(newTitle);\n\n // update resolution\n const width = $('#canvas_width'), w = width.val();\n const height = $('#canvas_height'), h = height.val();\n\n if (w !== 'fit' && !isValidUnit('width', w)) {\n width.parent().addClass('error');\n /* await */ $.alert(uiStrings.notification.invalidAttrValGiven);\n return false;\n }\n\n width.parent().removeClass('error');\n\n if (h !== 'fit' && !isValidUnit('height', h)) {\n height.parent().addClass('error');\n /* await */ $.alert(uiStrings.notification.invalidAttrValGiven);\n return false;\n }\n\n height.parent().removeClass('error');\n\n if (!svgCanvas.setResolution(w, h)) {\n /* await */ $.alert(uiStrings.notification.noContentToFitTo);\n return false;\n }\n\n // Set image save option\n $.pref('img_save', $('#image_save_opts :checked').val());\n updateCanvas();\n hideDocProperties();\n return true;\n };\n\n /**\n * Save user preferences based on current values in the UI.\n * @function module:SVGEditor.savePreferences\n * @returns {undefined}\n */\n const savePreferences = editor.savePreferences = async function () {\n // Set background\n const color = $('#bg_blocks div.cur_background').css('background-color') || '#FFF';\n setBackground(color, $('#canvas_bg_url').val());\n\n // set language\n const lang = $('#lang_select').val();\n if (lang !== $.pref('lang')) {\n const {langParam, langData} = await editor.putLocale(lang, goodLangs, curConfig);\n await setLang(langParam, langData);\n }\n\n // set icon size\n setIconSize($('#iconsize').val());\n\n // set grid setting\n curConfig.gridSnapping = $('#grid_snapping_on')[0].checked;\n curConfig.snappingStep = $('#grid_snapping_step').val();\n curConfig.gridColor = $('#grid_color').val();\n curConfig.showRulers = $('#show_rulers')[0].checked;\n\n $('#rulers').toggle(curConfig.showRulers);\n if (curConfig.showRulers) { updateRulers(); }\n curConfig.baseUnit = $('#base_unit').val();\n\n svgCanvas.setConfig(curConfig);\n\n updateCanvas();\n hidePreferences();\n };\n\n let resetScrollPos = $.noop;\n\n /**\n *\n * @returns {Promise} Resolves to `undefined`\n */\n const cancelOverlays = async function () {\n $('#dialog_box').hide();\n if (!editingsource && !docprops && !preferences) {\n if (curContext) {\n svgCanvas.leaveContext();\n }\n return;\n }\n\n if (editingsource) {\n if (origSource !== $('#svg_source_textarea').val()) {\n const ok = await $.confirm(uiStrings.notification.QignoreSourceChanges);\n if (ok) {\n hideSourceEditor();\n }\n } else {\n hideSourceEditor();\n }\n } else if (docprops) {\n hideDocProperties();\n } else if (preferences) {\n hidePreferences();\n }\n resetScrollPos();\n };\n\n const winWh = {width: $(window).width(), height: $(window).height()};\n\n // Fix for Issue 781: Drawing area jumps to top-left corner on window resize (IE9)\n if (isIE()) {\n resetScrollPos = function () {\n if (workarea[0].scrollLeft === 0 && workarea[0].scrollTop === 0) {\n workarea[0].scrollLeft = curScrollPos.left;\n workarea[0].scrollTop = curScrollPos.top;\n }\n };\n\n curScrollPos = {\n left: workarea[0].scrollLeft,\n top: workarea[0].scrollTop\n };\n\n $(window).resize(resetScrollPos);\n editor.ready(function () {\n // TODO: Find better way to detect when to do this to minimize\n // flickering effect\n return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new\n setTimeout(function () {\n resetScrollPos();\n resolve();\n }, 500);\n });\n });\n\n workarea.scroll(function () {\n curScrollPos = {\n left: workarea[0].scrollLeft,\n top: workarea[0].scrollTop\n };\n });\n }\n\n $(window).resize(function (evt) {\n $.each(winWh, function (type, val) {\n const curval = $(window)[type]();\n workarea[0]['scroll' + (type === 'width' ? 'Left' : 'Top')] -= (curval - val) / 2;\n winWh[type] = curval;\n });\n setFlyoutPositions();\n });\n\n workarea.scroll(function () {\n // TODO: jQuery's scrollLeft/Top() wouldn't require a null check\n if ($('#ruler_x').length) {\n $('#ruler_x')[0].scrollLeft = workarea[0].scrollLeft;\n }\n if ($('#ruler_y').length) {\n $('#ruler_y')[0].scrollTop = workarea[0].scrollTop;\n }\n });\n\n $('#url_notice').click(function () {\n /* await */ $.alert(this.title);\n });\n\n $('#change_image_url').click(promptImgURL);\n\n // added these event handlers for all the push buttons so they\n // behave more like buttons being pressed-in and not images\n (function () {\n const toolnames = ['clear', 'open', 'save', 'source', 'delete', 'delete_multi', 'paste', 'clone', 'clone_multi', 'move_top', 'move_bottom'];\n const curClass = 'tool_button_current';\n\n let allTools = '';\n\n $.each(toolnames, function (i, item) {\n allTools += (i ? ',' : '') + '#tool_' + item;\n });\n\n $(allTools).mousedown(function () {\n $(this).addClass(curClass);\n }).bind('mousedown mouseout', function () {\n $(this).removeClass(curClass);\n });\n\n $('#tool_undo, #tool_redo').mousedown(function () {\n if (!$(this).hasClass('disabled')) { $(this).addClass(curClass); }\n }).bind('mousedown mouseout', function () {\n $(this).removeClass(curClass);\n });\n }());\n\n // switch modifier key in tooltips if mac\n // NOTE: This code is not used yet until I can figure out how to successfully bind ctrl/meta\n // in Opera and Chrome\n if (isMac() && !window.opera) {\n const shortcutButtons = [\n 'tool_clear', 'tool_save', 'tool_source',\n 'tool_undo', 'tool_redo', 'tool_clone'\n ];\n let i = shortcutButtons.length;\n while (i--) {\n const button = document.getElementById(shortcutButtons[i]);\n if (button) {\n const {title} = button;\n const index = title.indexOf('Ctrl+');\n button.title = [title.substr(0, index), 'Cmd+', title.substr(index + 5)].join('');\n }\n }\n }\n\n /**\n * @param {external:jQuery} elem\n * @todo Go back to the color boxes having white background-color and then setting\n * background-image to none.png (otherwise partially transparent gradients look weird)\n * @returns {undefined}\n */\n const colorPicker = function (elem) {\n const picker = elem.attr('id') === 'stroke_color' ? 'stroke' : 'fill';\n // const opacity = (picker == 'stroke' ? $('#stroke_opacity') : $('#fill_opacity'));\n const title = picker === 'stroke'\n ? uiStrings.ui.pick_stroke_paint_opacity\n : uiStrings.ui.pick_fill_paint_opacity;\n // let wasNone = false; // Currently unused\n const pos = elem.offset();\n let {paint} = paintBox[picker];\n $('#color_picker')\n .draggable({\n cancel: '.jGraduate_tabs, .jGraduate_colPick, .jGraduate_gradPick, .jPicker',\n containment: 'window'\n })\n .css(curConfig.colorPickerCSS || {left: pos.left - 140, bottom: 40})\n .jGraduate(\n {\n paint,\n window: {pickerTitle: title},\n images: {clientPath: curConfig.jGraduatePath},\n newstop: 'inverse'\n },\n function (p) {\n paint = new $.jGraduate.Paint(p);\n paintBox[picker].setPaint(paint);\n svgCanvas.setPaint(picker, paint);\n $('#color_picker').hide();\n },\n function () {\n $('#color_picker').hide();\n }\n );\n };\n\n class PaintBox {\n constructor (container, type) {\n const cur = curConfig[type === 'fill' ? 'initFill' : 'initStroke'];\n // set up gradients to be used for the buttons\n const svgdocbox = new DOMParser().parseFromString(\n `
`,\n 'text/xml'\n );\n\n let docElem = svgdocbox.documentElement;\n docElem = $(container)[0].appendChild(document.importNode(docElem, true));\n docElem.setAttribute('width', 16.5);\n\n this.rect = docElem.firstElementChild;\n this.defs = docElem.getElementsByTagName('defs')[0];\n this.grad = this.defs.firstElementChild;\n this.paint = new $.jGraduate.Paint({solidColor: cur.color});\n this.type = type;\n }\n setPaint (paint, apply) {\n this.paint = paint;\n\n const ptype = paint.type;\n const opac = paint.alpha / 100;\n\n let fillAttr = 'none';\n switch (ptype) {\n case 'solidColor':\n fillAttr = (paint[ptype] !== 'none') ? '#' + paint[ptype] : paint[ptype];\n break;\n case 'linearGradient':\n case 'radialGradient': {\n this.grad.remove();\n this.grad = this.defs.appendChild(paint[ptype]);\n const id = this.grad.id = 'gradbox_' + this.type;\n fillAttr = 'url(#' + id + ')';\n break;\n }\n }\n\n this.rect.setAttribute('fill', fillAttr);\n this.rect.setAttribute('opacity', opac);\n\n if (apply) {\n svgCanvas.setColor(this.type, this._paintColor, true);\n svgCanvas.setPaintOpacity(this.type, this._paintOpacity, true);\n }\n }\n\n update (apply) {\n if (!selectedElement) { return; }\n\n const {type} = this;\n switch (selectedElement.tagName) {\n case 'use':\n case 'image':\n case 'foreignObject':\n // These elements don't have fill or stroke, so don't change\n // the current value\n return;\n case 'g':\n case 'a': {\n const childs = selectedElement.getElementsByTagName('*');\n\n let gPaint = null;\n for (let i = 0, len = childs.length; i < len; i++) {\n const elem = childs[i];\n const p = elem.getAttribute(type);\n if (i === 0) {\n gPaint = p;\n } else if (gPaint !== p) {\n gPaint = null;\n break;\n }\n }\n\n if (gPaint === null) {\n // No common color, don't update anything\n this._paintColor = null;\n return;\n }\n this._paintColor = gPaint;\n this._paintOpacity = 1;\n break;\n } default: {\n this._paintOpacity = parseFloat(selectedElement.getAttribute(type + '-opacity'));\n if (isNaN(this._paintOpacity)) {\n this._paintOpacity = 1.0;\n }\n\n const defColor = type === 'fill' ? 'black' : 'none';\n this._paintColor = selectedElement.getAttribute(type) || defColor;\n }\n }\n\n if (apply) {\n svgCanvas.setColor(type, this._paintColor, true);\n svgCanvas.setPaintOpacity(type, this._paintOpacity, true);\n }\n\n this._paintOpacity *= 100;\n\n const paint = getPaint(this._paintColor, this._paintOpacity, type);\n // update the rect inside #fill_color/#stroke_color\n this.setPaint(paint);\n }\n\n prep () {\n const ptype = this.paint.type;\n\n switch (ptype) {\n case 'linearGradient':\n case 'radialGradient': {\n const paint = new $.jGraduate.Paint({copy: this.paint});\n svgCanvas.setPaint(this.type, paint);\n break;\n }\n }\n }\n }\n\n paintBox.fill = new PaintBox('#fill_color', 'fill');\n paintBox.stroke = new PaintBox('#stroke_color', 'stroke');\n\n $('#stroke_width').val(curConfig.initStroke.width);\n $('#group_opacity').val(curConfig.initOpacity * 100);\n\n // Use this SVG elem to test vectorEffect support\n const testEl = paintBox.fill.rect.cloneNode(false);\n testEl.setAttribute('style', 'vector-effect:non-scaling-stroke');\n const supportsNonSS = (testEl.style.vectorEffect === 'non-scaling-stroke');\n testEl.removeAttribute('style');\n const svgdocbox = paintBox.fill.rect.ownerDocument;\n // Use this to test support for blur element. Seems to work to test support in Webkit\n const blurTest = svgdocbox.createElementNS(NS.SVG, 'feGaussianBlur');\n if (blurTest.stdDeviationX === undefined) {\n $('#tool_blur').hide();\n }\n $(blurTest).remove();\n\n // Test for zoom icon support\n (function () {\n const pre = '-' + uaPrefix.toLowerCase() + '-zoom-';\n const zoom = pre + 'in';\n workarea.css('cursor', zoom);\n if (workarea.css('cursor') === zoom) {\n zoomInIcon = zoom;\n zoomOutIcon = pre + 'out';\n }\n workarea.css('cursor', 'auto');\n }());\n\n // Test for embedImage support (use timeout to not interfere with page load)\n setTimeout(function () {\n svgCanvas.embedImage('images/logo.png', function (datauri) {\n if (!datauri) {\n // Disable option\n $('#image_save_opts [value=embed]').attr('disabled', 'disabled');\n $('#image_save_opts input').val(['ref']);\n $.pref('img_save', 'ref');\n $('#image_opt_embed').css('color', '#666').attr(\n 'title',\n uiStrings.notification.featNotSupported\n );\n }\n });\n }, 1000);\n\n $('#fill_color, #tool_fill .icon_label').click(function () {\n colorPicker($('#fill_color'));\n updateToolButtonState();\n });\n\n $('#stroke_color, #tool_stroke .icon_label').click(function () {\n colorPicker($('#stroke_color'));\n updateToolButtonState();\n });\n\n $('#group_opacityLabel').click(function () {\n $('#opacity_dropdown button').mousedown();\n $(window).mouseup();\n });\n\n $('#zoomLabel').click(function () {\n $('#zoom_dropdown button').mousedown();\n $(window).mouseup();\n });\n\n $('#tool_move_top').mousedown(function (evt) {\n $('#tools_stacking').show();\n evt.preventDefault();\n });\n\n $('.layer_button').mousedown(function () {\n $(this).addClass('layer_buttonpressed');\n }).mouseout(function () {\n $(this).removeClass('layer_buttonpressed');\n }).mouseup(function () {\n $(this).removeClass('layer_buttonpressed');\n });\n\n $('.push_button').mousedown(function () {\n if (!$(this).hasClass('disabled')) {\n $(this).addClass('push_button_pressed').removeClass('push_button');\n }\n }).mouseout(function () {\n $(this).removeClass('push_button_pressed').addClass('push_button');\n }).mouseup(function () {\n $(this).removeClass('push_button_pressed').addClass('push_button');\n });\n\n // ask for a layer name\n $('#layer_new').click(async function () {\n let uniqName,\n i = svgCanvas.getCurrentDrawing().getNumLayers();\n do {\n uniqName = uiStrings.layers.layer + ' ' + (++i);\n } while (svgCanvas.getCurrentDrawing().hasLayer(uniqName));\n\n const newName = await $.prompt(uiStrings.notification.enterUniqueLayerName, uniqName);\n if (!newName) { return; }\n if (svgCanvas.getCurrentDrawing().hasLayer(newName)) {\n /* await */ $.alert(uiStrings.notification.dupeLayerName);\n return;\n }\n svgCanvas.createLayer(newName);\n updateContextPanel();\n populateLayers();\n });\n\n /**\n *\n * @returns {undefined}\n */\n function deleteLayer () {\n if (svgCanvas.deleteCurrentLayer()) {\n updateContextPanel();\n populateLayers();\n // This matches what SvgCanvas does\n // TODO: make this behavior less brittle (svg-editor should get which\n // layer is selected from the canvas and then select that one in the UI)\n $('#layerlist tr.layer').removeClass('layersel');\n $('#layerlist tr.layer:first').addClass('layersel');\n }\n }\n\n /**\n *\n * @returns {undefined}\n */\n async function cloneLayer () {\n const name = svgCanvas.getCurrentDrawing().getCurrentLayerName() + ' copy';\n\n const newName = await $.prompt(uiStrings.notification.enterUniqueLayerName, name);\n if (!newName) { return; }\n if (svgCanvas.getCurrentDrawing().hasLayer(newName)) {\n /* await */ $.alert(uiStrings.notification.dupeLayerName);\n return;\n }\n svgCanvas.cloneLayer(newName);\n updateContextPanel();\n populateLayers();\n }\n\n /**\n *\n * @returns {undefined}\n */\n function mergeLayer () {\n if ($('#layerlist tr.layersel').index() === svgCanvas.getCurrentDrawing().getNumLayers() - 1) {\n return;\n }\n svgCanvas.mergeLayer();\n updateContextPanel();\n populateLayers();\n }\n\n /**\n * @param {Integer} pos\n * @returns {undefined}\n */\n function moveLayer (pos) {\n const total = svgCanvas.getCurrentDrawing().getNumLayers();\n\n let curIndex = $('#layerlist tr.layersel').index();\n if (curIndex > 0 || curIndex < total - 1) {\n curIndex += pos;\n svgCanvas.setCurrentLayerPosition(total - curIndex - 1);\n populateLayers();\n }\n }\n\n $('#layer_delete').click(deleteLayer);\n\n $('#layer_up').click(() => {\n moveLayer(-1);\n });\n\n $('#layer_down').click(() => {\n moveLayer(1);\n });\n\n $('#layer_rename').click(async function () {\n // const curIndex = $('#layerlist tr.layersel').prevAll().length; // Currently unused\n const oldName = $('#layerlist tr.layersel td.layername').text();\n const newName = await $.prompt(uiStrings.notification.enterNewLayerName, '');\n if (!newName) { return; }\n if (oldName === newName || svgCanvas.getCurrentDrawing().hasLayer(newName)) {\n /* await */ $.alert(uiStrings.notification.layerHasThatName);\n return;\n }\n\n svgCanvas.renameCurrentLayer(newName);\n populateLayers();\n });\n\n const SIDEPANEL_MAXWIDTH = 300;\n const SIDEPANEL_OPENWIDTH = 150;\n let sidedrag = -1, sidedragging = false, allowmove = false;\n\n /**\n * @param {Float} delta\n * @fires module:svgcanvas.SvgCanvas#event:ext-workareaResized\n * @returns {undefined}\n */\n const changeSidePanelWidth = function (delta) {\n const rulerX = $('#ruler_x');\n $('#sidepanels').width('+=' + delta);\n $('#layerpanel').width('+=' + delta);\n rulerX.css('right', parseInt(rulerX.css('right')) + delta);\n workarea.css('right', parseInt(workarea.css('right')) + delta);\n svgCanvas.runExtensions('workareaResized');\n };\n\n /**\n * @param {Event} evt\n * @returns {undefined}\n */\n const resizeSidePanel = function (evt) {\n if (!allowmove) { return; }\n if (sidedrag === -1) { return; }\n sidedragging = true;\n let deltaX = sidedrag - evt.pageX;\n const sideWidth = $('#sidepanels').width();\n if (sideWidth + deltaX > SIDEPANEL_MAXWIDTH) {\n deltaX = SIDEPANEL_MAXWIDTH - sideWidth;\n // sideWidth = SIDEPANEL_MAXWIDTH;\n } else if (sideWidth + deltaX < 2) {\n deltaX = 2 - sideWidth;\n // sideWidth = 2;\n }\n if (deltaX === 0) { return; }\n sidedrag -= deltaX;\n changeSidePanelWidth(deltaX);\n };\n\n /**\n * If width is non-zero, then fully close it; otherwise fully open it.\n * @param {boolean} close Forces the side panel closed\n * @returns {undefined}\n */\n const toggleSidePanel = function (close) {\n const dpr = window.devicePixelRatio || 1;\n const w = $('#sidepanels').width();\n const isOpened = (dpr < 1 ? w : w / dpr) > 2;\n const zoomAdjustedSidepanelWidth = (dpr < 1 ? 1 : dpr) * SIDEPANEL_OPENWIDTH;\n const deltaX = (isOpened || close ? 0 : zoomAdjustedSidepanelWidth) - w;\n changeSidePanelWidth(deltaX);\n };\n\n $('#sidepanel_handle')\n .mousedown(function (evt) {\n sidedrag = evt.pageX;\n $(window).mousemove(resizeSidePanel);\n allowmove = false;\n // Silly hack for Chrome, which always runs mousemove right after mousedown\n setTimeout(function () {\n allowmove = true;\n }, 20);\n })\n .mouseup(function (evt) {\n if (!sidedragging) { toggleSidePanel(); }\n sidedrag = -1;\n sidedragging = false;\n });\n\n $(window).mouseup(function () {\n sidedrag = -1;\n sidedragging = false;\n $('#svg_editor').unbind('mousemove', resizeSidePanel);\n });\n\n populateLayers();\n\n // function changeResolution (x,y) {\n // const {zoom} = svgCanvas.getResolution();\n // setResolution(x * zoom, y * zoom);\n // }\n\n const centerCanvas = () => {\n // this centers the canvas vertically in the workarea (horizontal handled in CSS)\n workarea.css('line-height', workarea.height() + 'px');\n };\n\n $(window).bind('load resize', centerCanvas);\n\n /**\n * @implements {module:jQuerySpinButton.StepCallback}\n */\n function stepFontSize (elem, step) {\n const origVal = Number(elem.value);\n const sugVal = origVal + step;\n const increasing = sugVal >= origVal;\n if (step === 0) { return origVal; }\n\n if (origVal >= 24) {\n if (increasing) {\n return Math.round(origVal * 1.1);\n }\n return Math.round(origVal / 1.1);\n }\n if (origVal <= 1) {\n if (increasing) {\n return origVal * 2;\n }\n return origVal / 2;\n }\n return sugVal;\n }\n\n /**\n * @implements {module:jQuerySpinButton.StepCallback}\n */\n function stepZoom (elem, step) {\n const origVal = Number(elem.value);\n if (origVal === 0) { return 100; }\n const sugVal = origVal + step;\n if (step === 0) { return origVal; }\n\n if (origVal >= 100) {\n return sugVal;\n }\n if (sugVal >= origVal) {\n return origVal * 2;\n }\n return origVal / 2;\n }\n\n // function setResolution (w, h, center) {\n // updateCanvas();\n // // w -= 0; h -= 0;\n // // $('#svgcanvas').css({width: w, height: h});\n // // $('#canvas_width').val(w);\n // // $('#canvas_height').val(h);\n // //\n // // if (center) {\n // // const wArea = workarea;\n // // const scrollY = h/2 - wArea.height()/2;\n // // const scrollX = w/2 - wArea.width()/2;\n // // wArea[0].scrollTop = scrollY;\n // // wArea[0].scrollLeft = scrollX;\n // // }\n // }\n\n $('#resolution').change(function () {\n const wh = $('#canvas_width,#canvas_height');\n if (!this.selectedIndex) {\n if ($('#canvas_width').val() === 'fit') {\n wh.removeAttr('disabled').val(100);\n }\n } else if (this.value === 'content') {\n wh.val('fit').attr('disabled', 'disabled');\n } else {\n const dims = this.value.split('x');\n $('#canvas_width').val(dims[0]);\n $('#canvas_height').val(dims[1]);\n wh.removeAttr('disabled');\n }\n });\n\n // Prevent browser from erroneously repopulating fields\n $('input,select').attr('autocomplete', 'off');\n\n const dialogSelectors = [\n '#tool_source_cancel', '#tool_docprops_cancel',\n '#tool_prefs_cancel', '.overlay'\n ];\n /**\n * Associate all button actions as well as non-button keyboard shortcuts\n * @namespace {PlainObject} module:SVGEditor~Actions\n */\n const Actions = (function () {\n /**\n * @typedef {PlainObject} module:SVGEditor.ToolButton\n * @property {string} sel The CSS selector for the tool\n * @property {external:jQuery.Function} fn A handler to be attached to the `evt`\n * @property {string} evt The event for which the `fn` listener will be added\n * @property {module:SVGEditor.Key} [key] [key, preventDefault, NoDisableInInput]\n * @property {string} [parent] Selector\n * @property {boolean} [hidekey] Whether to show key value in title\n * @property {string} [icon] The button ID\n * @property {boolean} isDefault For flyout holders\n */\n /**\n *\n * @name module:SVGEditor~ToolButtons\n * @type {module:SVGEditor.ToolButton[]}\n */\n const toolButtons = [\n {sel: '#tool_select', fn: clickSelect, evt: 'click', key: ['V', true]},\n {sel: '#tool_fhpath', fn: clickFHPath, evt: 'click', key: ['Q', true]},\n {sel: '#tool_line', fn: clickLine, evt: 'click', key: ['L', true],\n parent: '#tools_line', prepend: true},\n {sel: '#tool_rect', fn: clickRect, evt: 'mouseup',\n key: ['R', true], parent: '#tools_rect', icon: 'rect'},\n {sel: '#tool_square', fn: clickSquare, evt: 'mouseup',\n parent: '#tools_rect', icon: 'square'},\n {sel: '#tool_fhrect', fn: clickFHRect, evt: 'mouseup',\n parent: '#tools_rect', icon: 'fh_rect'},\n {sel: '#tool_ellipse', fn: clickEllipse, evt: 'mouseup',\n key: ['E', true], parent: '#tools_ellipse', icon: 'ellipse'},\n {sel: '#tool_circle', fn: clickCircle, evt: 'mouseup',\n parent: '#tools_ellipse', icon: 'circle'},\n {sel: '#tool_fhellipse', fn: clickFHEllipse, evt: 'mouseup',\n parent: '#tools_ellipse', icon: 'fh_ellipse'},\n {sel: '#tool_path', fn: clickPath, evt: 'click', key: ['P', true]},\n {sel: '#tool_text', fn: clickText, evt: 'click', key: ['T', true]},\n {sel: '#tool_image', fn: clickImage, evt: 'mouseup'},\n {sel: '#tool_zoom', fn: clickZoom, evt: 'mouseup', key: ['Z', true]},\n {sel: '#tool_clear', fn: clickClear, evt: 'mouseup', key: ['N', true]},\n {sel: '#tool_save', fn () {\n if (editingsource) {\n saveSourceEditor();\n } else {\n clickSave();\n }\n }, evt: 'mouseup', key: ['S', true]},\n {sel: '#tool_export', fn: clickExport, evt: 'mouseup'},\n {sel: '#tool_open', fn: clickOpen, evt: 'mouseup', key: ['O', true]},\n {sel: '#tool_import', fn: clickImport, evt: 'mouseup'},\n {sel: '#tool_source', fn: showSourceEditor, evt: 'click', key: ['U', true]},\n {sel: '#tool_wireframe', fn: clickWireframe, evt: 'click', key: ['F', true]},\n {\n key: ['esc', false, false],\n fn () {\n if (dialogSelectors.every((sel) => {\n return $(sel + ':hidden').length;\n })) {\n svgCanvas.clearSelection();\n }\n },\n hidekey: true\n },\n {sel: dialogSelectors.join(','), fn: cancelOverlays, evt: 'click',\n key: ['esc', false, false], hidekey: true},\n {sel: '#tool_source_save', fn: saveSourceEditor, evt: 'click'},\n {sel: '#tool_docprops_save', fn: saveDocProperties, evt: 'click'},\n {sel: '#tool_docprops', fn: showDocProperties, evt: 'mouseup'},\n {sel: '#tool_prefs_save', fn: savePreferences, evt: 'click'},\n {sel: '#tool_prefs_option', fn () { showPreferences(); return false; }, evt: 'mouseup'},\n {sel: '#tool_delete,#tool_delete_multi', fn: deleteSelected,\n evt: 'click', key: ['del/backspace', true]},\n {sel: '#tool_reorient', fn: reorientPath, evt: 'click'},\n {sel: '#tool_node_link', fn: linkControlPoints, evt: 'click'},\n {sel: '#tool_node_clone', fn: clonePathNode, evt: 'click'},\n {sel: '#tool_node_delete', fn: deletePathNode, evt: 'click'},\n {sel: '#tool_openclose_path', fn: opencloseSubPath, evt: 'click'},\n {sel: '#tool_add_subpath', fn: addSubPath, evt: 'click'},\n {sel: '#tool_move_top', fn: moveToTopSelected, evt: 'click', key: 'ctrl+shift+]'},\n {sel: '#tool_move_bottom', fn: moveToBottomSelected, evt: 'click', key: 'ctrl+shift+['},\n {sel: '#tool_topath', fn: convertToPath, evt: 'click'},\n {sel: '#tool_make_link,#tool_make_link_multi', fn: makeHyperlink, evt: 'click'},\n {sel: '#tool_undo', fn: clickUndo, evt: 'click'},\n {sel: '#tool_redo', fn: clickRedo, evt: 'click'},\n {sel: '#tool_clone,#tool_clone_multi', fn: clickClone, evt: 'click', key: ['D', true]},\n {sel: '#tool_group_elements', fn: clickGroup, evt: 'click', key: ['G', true]},\n {sel: '#tool_ungroup', fn: clickGroup, evt: 'click'},\n {sel: '#tool_unlink_use', fn: clickGroup, evt: 'click'},\n {sel: '[id^=tool_align]', fn: clickAlign, evt: 'click'},\n // these two lines are required to make Opera work properly with the flyout mechanism\n // {sel: '#tools_rect_show', fn: clickRect, evt: 'click'},\n // {sel: '#tools_ellipse_show', fn: clickEllipse, evt: 'click'},\n {sel: '#tool_bold', fn: clickBold, evt: 'mousedown'},\n {sel: '#tool_italic', fn: clickItalic, evt: 'mousedown'},\n {sel: '#sidepanel_handle', fn: toggleSidePanel, key: ['X']},\n {sel: '#copy_save_done', fn: cancelOverlays, evt: 'click'},\n\n // Shortcuts not associated with buttons\n\n {key: 'ctrl+left', fn () { rotateSelected(0, 1); }},\n {key: 'ctrl+right', fn () { rotateSelected(1, 1); }},\n {key: 'ctrl+shift+left', fn () { rotateSelected(0, 5); }},\n {key: 'ctrl+shift+right', fn () { rotateSelected(1, 5); }},\n {key: 'shift+O', fn: selectPrev},\n {key: 'shift+P', fn: selectNext},\n {key: [modKey + 'up', true], fn () { zoomImage(2); }},\n {key: [modKey + 'down', true], fn () { zoomImage(0.5); }},\n {key: [modKey + ']', true], fn () { moveUpDownSelected('Up'); }},\n {key: [modKey + '[', true], fn () { moveUpDownSelected('Down'); }},\n {key: ['up', true], fn () { moveSelected(0, -1); }},\n {key: ['down', true], fn () { moveSelected(0, 1); }},\n {key: ['left', true], fn () { moveSelected(-1, 0); }},\n {key: ['right', true], fn () { moveSelected(1, 0); }},\n {key: 'shift+up', fn () { moveSelected(0, -10); }},\n {key: 'shift+down', fn () { moveSelected(0, 10); }},\n {key: 'shift+left', fn () { moveSelected(-10, 0); }},\n {key: 'shift+right', fn () { moveSelected(10, 0); }},\n {key: ['alt+up', true], fn () { svgCanvas.cloneSelectedElements(0, -1); }},\n {key: ['alt+down', true], fn () { svgCanvas.cloneSelectedElements(0, 1); }},\n {key: ['alt+left', true], fn () { svgCanvas.cloneSelectedElements(-1, 0); }},\n {key: ['alt+right', true], fn () { svgCanvas.cloneSelectedElements(1, 0); }},\n {key: ['alt+shift+up', true], fn () { svgCanvas.cloneSelectedElements(0, -10); }},\n {key: ['alt+shift+down', true], fn () { svgCanvas.cloneSelectedElements(0, 10); }},\n {key: ['alt+shift+left', true], fn () { svgCanvas.cloneSelectedElements(-10, 0); }},\n {key: ['alt+shift+right', true], fn () { svgCanvas.cloneSelectedElements(10, 0); }},\n {key: 'a', fn () { svgCanvas.selectAllInCurrentLayer(); }},\n {key: modKey + 'a', fn () { svgCanvas.selectAllInCurrentLayer(); }},\n\n // Standard shortcuts\n {key: modKey + 'z', fn: clickUndo},\n {key: modKey + 'shift+z', fn: clickRedo},\n {key: modKey + 'y', fn: clickRedo},\n\n {key: modKey + 'x', fn: cutSelected},\n {key: modKey + 'c', fn: copySelected},\n {key: modKey + 'v', fn: pasteInCenter}\n ];\n\n // Tooltips not directly associated with a single function\n const keyAssocs = {\n '4/Shift+4': '#tools_rect_show',\n '5/Shift+5': '#tools_ellipse_show'\n };\n\n return { /** @lends module:SVGEditor~Actions */\n /**\n * @returns {undefined}\n */\n setAll () {\n const flyouts = {};\n\n $.each(toolButtons, function (i, opts) {\n // Bind function to button\n let btn;\n if (opts.sel) {\n btn = $(opts.sel);\n if (!btn.length) { return true; } // Skip if markup does not exist\n if (opts.evt) {\n // `touch.js` changes `touchstart` to `mousedown`,\n // so we must map tool button click events as well\n if (isTouch() && opts.evt === 'click') {\n opts.evt = 'mousedown';\n }\n btn[opts.evt](opts.fn);\n }\n\n // Add to parent flyout menu, if able to be displayed\n if (opts.parent && $(opts.parent + '_show').length) {\n let fH = $(opts.parent);\n if (!fH.length) {\n fH = makeFlyoutHolder(opts.parent.substr(1));\n }\n if (opts.prepend) {\n btn[0].style.margin = 'initial';\n }\n fH[opts.prepend ? 'prepend' : 'append'](btn);\n\n if (!Array.isArray(flyouts[opts.parent])) {\n flyouts[opts.parent] = [];\n }\n flyouts[opts.parent].push(opts);\n }\n }\n\n // Bind function to shortcut key\n if (opts.key) {\n // Set shortcut based on options\n let keyval,\n // disInInp = true,\n pd = false;\n if (Array.isArray(opts.key)) {\n keyval = opts.key[0];\n if (opts.key.length > 1) { pd = opts.key[1]; }\n // if (opts.key.length > 2) { disInInp = opts.key[2]; }\n } else {\n keyval = opts.key;\n }\n keyval = String(keyval);\n\n const {fn} = opts;\n $.each(keyval.split('/'), function (j, key) {\n $(document).bind('keydown', key, function (e) {\n fn();\n if (pd) {\n e.preventDefault();\n }\n // Prevent default on ALL keys?\n return false;\n });\n });\n\n // Put shortcut in title\n if (opts.sel && !opts.hidekey && btn.attr('title')) {\n const newTitle = btn.attr('title').split('[')[0] + ' (' + keyval + ')';\n keyAssocs[keyval] = opts.sel;\n // Disregard for menu items\n if (!btn.parents('#main_menu').length) {\n btn.attr('title', newTitle);\n }\n }\n }\n return true;\n });\n\n // Setup flyouts\n setupFlyouts(flyouts);\n\n // Misc additional actions\n\n // Make 'return' keypress trigger the change event\n $('.attr_changer, #image_url').bind(\n 'keydown',\n 'return',\n function (evt) {\n $(this).change();\n evt.preventDefault();\n }\n );\n\n $(window).bind('keydown', 'tab', function (e) {\n if (uiContext === 'canvas') {\n e.preventDefault();\n selectNext();\n }\n }).bind('keydown', 'shift+tab', function (e) {\n if (uiContext === 'canvas') {\n e.preventDefault();\n selectPrev();\n }\n });\n\n $('#tool_zoom').dblclick(dblclickZoom);\n },\n /**\n * @returns {undefined}\n */\n setTitles () {\n $.each(keyAssocs, function (keyval, sel) {\n const menu = ($(sel).parents('#main_menu').length);\n\n $(sel).each(function () {\n let t;\n if (menu) {\n t = $(this).text().split(' [')[0];\n } else {\n t = this.title.split(' [')[0];\n }\n let keyStr = '';\n // Shift+Up\n $.each(keyval.split('/'), function (i, key) {\n const modBits = key.split('+');\n let mod = '';\n if (modBits.length > 1) {\n mod = modBits[0] + '+';\n key = modBits[1];\n }\n keyStr += (i ? '/' : '') + mod + (uiStrings['key_' + key] || key);\n });\n if (menu) {\n this.lastChild.textContent = t + ' [' + keyStr + ']';\n } else {\n this.title = t + ' [' + keyStr + ']';\n }\n });\n });\n },\n /**\n * @param {string} sel Selector to match\n * @returns {module:SVGEditor.ToolButton}\n */\n getButtonData (sel) {\n return Object.values(toolButtons).find((btn) => {\n return btn.sel === sel;\n });\n }\n };\n }());\n\n // Select given tool\n editor.ready(function () {\n let tool;\n const itool = curConfig.initTool,\n container = $('#tools_left, #svg_editor .tools_flyout'),\n preTool = container.find('#tool_' + itool),\n regTool = container.find('#' + itool);\n if (preTool.length) {\n tool = preTool;\n } else if (regTool.length) {\n tool = regTool;\n } else {\n tool = $('#tool_select');\n }\n tool.click().mouseup();\n\n if (curConfig.wireframe) {\n $('#tool_wireframe').click();\n }\n\n if (curConfig.showlayers) {\n toggleSidePanel();\n }\n\n $('#rulers').toggle(Boolean(curConfig.showRulers));\n\n if (curConfig.showRulers) {\n $('#show_rulers')[0].checked = true;\n }\n\n if (curConfig.baseUnit) {\n $('#base_unit').val(curConfig.baseUnit);\n }\n\n if (curConfig.gridSnapping) {\n $('#grid_snapping_on')[0].checked = true;\n }\n\n if (curConfig.snappingStep) {\n $('#grid_snapping_step').val(curConfig.snappingStep);\n }\n\n if (curConfig.gridColor) {\n $('#grid_color').val(curConfig.gridColor);\n }\n });\n\n // init SpinButtons\n $('#rect_rx').SpinButton({\n min: 0, max: 1000, stateObj, callback: changeRectRadius\n });\n $('#stroke_width').SpinButton({\n min: 0, max: 99, smallStep: 0.1, stateObj, callback: changeStrokeWidth\n });\n $('#angle').SpinButton({\n min: -180, max: 180, step: 5, stateObj, callback: changeRotationAngle\n });\n $('#font_size').SpinButton({\n min: 0.001, stepfunc: stepFontSize, stateObj, callback: changeFontSize\n });\n $('#group_opacity').SpinButton({\n min: 0, max: 100, step: 5, stateObj, callback: changeOpacity\n });\n $('#blur').SpinButton({\n min: 0, max: 10, step: 0.1, stateObj, callback: changeBlur\n });\n $('#zoom').SpinButton({\n min: 0.001, max: 10000, step: 50, stepfunc: stepZoom,\n stateObj, callback: changeZoom\n // Set default zoom\n }).val(\n svgCanvas.getZoom() * 100\n );\n\n $('#workarea').contextMenu(\n {\n menu: 'cmenu_canvas',\n inSpeed: 0\n },\n function (action, el, pos) {\n switch (action) {\n case 'delete':\n deleteSelected();\n break;\n case 'cut':\n cutSelected();\n break;\n case 'copy':\n copySelected();\n break;\n case 'paste':\n svgCanvas.pasteElements();\n break;\n case 'paste_in_place':\n svgCanvas.pasteElements('in_place');\n break;\n case 'group':\n case 'group_elements':\n svgCanvas.groupSelectedElements();\n break;\n case 'ungroup':\n svgCanvas.ungroupSelectedElement();\n break;\n case 'move_front':\n moveToTopSelected();\n break;\n case 'move_up':\n moveUpDownSelected('Up');\n break;\n case 'move_down':\n moveUpDownSelected('Down');\n break;\n case 'move_back':\n moveToBottomSelected();\n break;\n default:\n if (hasCustomHandler(action)) {\n getCustomHandler(action).call();\n }\n break;\n }\n }\n );\n\n /**\n * Implements {@see module:jQueryContextMenu.jQueryContextMenuListener}\n * @param {\"dupe\"|\"delete\"|\"merge_down\"|\"merge_all\"} action\n * @param {external:jQuery} el\n * @param {{x: Float, y: Float, docX: Float, docY: Float}} pos\n * @returns {undefined}\n */\n const lmenuFunc = function (action, el, pos) {\n switch (action) {\n case 'dupe':\n /* await */ cloneLayer();\n break;\n case 'delete':\n deleteLayer();\n break;\n case 'merge_down':\n mergeLayer();\n break;\n case 'merge_all':\n svgCanvas.mergeAllLayers();\n updateContextPanel();\n populateLayers();\n break;\n }\n };\n\n $('#layerlist').contextMenu(\n {\n menu: 'cmenu_layers',\n inSpeed: 0\n },\n lmenuFunc\n );\n\n $('#layer_moreopts').contextMenu(\n {\n menu: 'cmenu_layers',\n inSpeed: 0,\n allowLeft: true\n },\n lmenuFunc\n );\n\n $('.contextMenu li').mousedown(function (ev) {\n ev.preventDefault();\n });\n\n $('#cmenu_canvas li').disableContextMenu();\n canvMenu.enableContextMenuItems('#delete,#cut,#copy');\n\n /**\n * @returns {undefined}\n */\n function enableOrDisableClipboard () {\n let svgeditClipboard;\n try {\n svgeditClipboard = localStorage.getItem('svgedit_clipboard');\n } catch (err) {}\n canvMenu[(svgeditClipboard ? 'en' : 'dis') + 'ableContextMenuItems'](\n '#paste,#paste_in_place'\n );\n }\n enableOrDisableClipboard();\n\n window.addEventListener('storage', function (e) {\n if (e.key !== 'svgedit_clipboard') { return; }\n\n enableOrDisableClipboard();\n });\n\n window.addEventListener('beforeunload', function (e) {\n // Suppress warning if page is empty\n if (undoMgr.getUndoStackSize() === 0) {\n editor.showSaveWarning = false;\n }\n\n // showSaveWarning is set to 'false' when the page is saved.\n if (!curConfig.no_save_warning && editor.showSaveWarning) {\n // Browser already asks question about closing the page\n e.returnValue = uiStrings.notification.unsavedChanges; // Firefox needs this when beforeunload set by addEventListener (even though message is not used)\n return uiStrings.notification.unsavedChanges;\n }\n return true;\n });\n\n /**\n * Expose the `uiStrings`.\n * @function module:SVGEditor.canvas.getUIStrings\n * @returns {module:SVGEditor.uiStrings}\n */\n editor.canvas.getUIStrings = function () {\n return uiStrings;\n };\n\n /**\n * @returns {Promise} Resolves to boolean indicating `true` if there were no changes\n * and `false` after the user confirms.\n */\n editor.openPrep = function () {\n $('#main_menu').hide();\n if (undoMgr.getUndoStackSize() === 0) {\n return true;\n }\n return $.confirm(uiStrings.notification.QwantToOpen);\n };\n\n /**\n *\n * @param {Event} e\n * @returns {undefined}\n */\n function onDragEnter (e) {\n e.stopPropagation();\n e.preventDefault();\n // and indicator should be displayed here, such as \"drop files here\"\n }\n\n /**\n *\n * @param {Event} e\n * @returns {undefined}\n */\n function onDragOver (e) {\n e.stopPropagation();\n e.preventDefault();\n }\n\n /**\n *\n * @param {Event} e\n * @returns {undefined}\n */\n function onDragLeave (e) {\n e.stopPropagation();\n e.preventDefault();\n // hypothetical indicator should be removed here\n }\n // Use HTML5 File API: http://www.w3.org/TR/FileAPI/\n // if browser has HTML5 File API support, then we will show the open menu item\n // and provide a file input to click. When that change event fires, it will\n // get the text contents of the file and send it to the canvas\n if (window.FileReader) {\n /**\n * @param {Event} e\n * @returns {undefined}\n */\n const importImage = function (e) {\n $.process_cancel(uiStrings.notification.loadingImage);\n e.stopPropagation();\n e.preventDefault();\n $('#workarea').removeAttr('style');\n $('#main_menu').hide();\n const file = (e.type === 'drop') ? e.dataTransfer.files[0] : this.files[0];\n if (!file) {\n $('#dialog_box').hide();\n return;\n }\n /* if (file.type === 'application/pdf') { // Todo: Handle PDF imports\n\n }\n else */\n if (!file.type.includes('image')) {\n return;\n }\n // Detected an image\n // svg handling\n let reader;\n if (file.type.includes('svg')) {\n reader = new FileReader();\n reader.onloadend = function (ev) {\n const newElement = svgCanvas.importSvgString(ev.target.result, true);\n svgCanvas.ungroupSelectedElement();\n svgCanvas.ungroupSelectedElement();\n svgCanvas.groupSelectedElements();\n svgCanvas.alignSelectedElements('m', 'page');\n svgCanvas.alignSelectedElements('c', 'page');\n // highlight imported element, otherwise we get strange empty selectbox\n svgCanvas.selectOnly([newElement]);\n $('#dialog_box').hide();\n };\n reader.readAsText(file);\n } else {\n // bitmap handling\n reader = new FileReader();\n reader.onloadend = function ({target: {result}}) {\n /**\n * Insert the new image until we know its dimensions\n * @param {Float} width\n * @param {Float} height\n * @returns {undefined}\n */\n const insertNewImage = function (width, height) {\n const newImage = svgCanvas.addSVGElementFromJson({\n element: 'image',\n attr: {\n x: 0,\n y: 0,\n width,\n height,\n id: svgCanvas.getNextId(),\n style: 'pointer-events:inherit'\n }\n });\n svgCanvas.setHref(newImage, result);\n svgCanvas.selectOnly([newImage]);\n svgCanvas.alignSelectedElements('m', 'page');\n svgCanvas.alignSelectedElements('c', 'page');\n updateContextPanel();\n $('#dialog_box').hide();\n };\n // create dummy img so we know the default dimensions\n let imgWidth = 100;\n let imgHeight = 100;\n const img = new Image();\n img.style.opacity = 0;\n img.addEventListener('load', function () {\n imgWidth = img.offsetWidth || img.naturalWidth || img.width;\n imgHeight = img.offsetHeight || img.naturalHeight || img.height;\n insertNewImage(imgWidth, imgHeight);\n });\n img.src = result;\n };\n reader.readAsDataURL(file);\n }\n };\n\n workarea[0].addEventListener('dragenter', onDragEnter);\n workarea[0].addEventListener('dragover', onDragOver);\n workarea[0].addEventListener('dragleave', onDragLeave);\n workarea[0].addEventListener('drop', importImage);\n\n const open = $('
').click(async function () {\n const ok = await editor.openPrep();\n if (!ok) { return; }\n svgCanvas.clear();\n if (this.files.length === 1) {\n $.process_cancel(uiStrings.notification.loadingImage);\n const reader = new FileReader();\n reader.onloadend = async function (e) {\n await loadSvgString(e.target.result);\n updateCanvas();\n };\n reader.readAsText(this.files[0]);\n }\n });\n $('#tool_open').show().prepend(open);\n\n const imgImport = $('
').change(importImage);\n $('#tool_import').show().prepend(imgImport);\n }\n\n updateCanvas(true);\n // const revnums = 'svg-editor.js ($Rev$) ';\n // revnums += svgCanvas.getVersion();\n // $('#copyright')[0].setAttribute('title', revnums);\n\n const loadedExtensionNames = [];\n /**\n * @function module:SVGEditor.setLang\n * @param {string} lang The language code\n * @param {module:locale.LocaleStrings} allStrings See {@tutorial LocaleDocs}\n * @fires module:svgcanvas.SvgCanvas#event:ext-langReady\n * @fires module:svgcanvas.SvgCanvas#event:ext-langChanged\n * @returns {Promise} A Promise which resolves to `undefined`\n */\n const setLang = editor.setLang = async function (lang, allStrings) {\n editor.langChanged = true;\n $.pref('lang', lang);\n $('#lang_select').val(lang);\n if (!allStrings) {\n return;\n }\n $.extend(uiStrings, allStrings);\n\n // const notif = allStrings.notification; // Currently unused\n // $.extend will only replace the given strings\n const oldLayerName = $('#layerlist tr.layersel td.layername').text();\n const renameLayer = (oldLayerName === uiStrings.common.layer + ' 1');\n\n svgCanvas.setUiStrings(allStrings);\n Actions.setTitles();\n\n if (renameLayer) {\n svgCanvas.renameCurrentLayer(uiStrings.common.layer + ' 1');\n populateLayers();\n }\n\n // In case extensions loaded before the locale, now we execute a callback on them\n if (extsPreLang.length) {\n await Promise.all(extsPreLang.map((ext) => {\n loadedExtensionNames.push(ext.name);\n return ext.langReady({\n lang,\n uiStrings,\n importLocale: getImportLocale({defaultLang: lang, defaultName: ext.name})\n });\n }));\n extsPreLang.length = 0;\n } else {\n loadedExtensionNames.forEach((loadedExtensionName) => {\n this.runExtension(\n loadedExtensionName,\n 'langReady',\n /** @type {module:svgcanvas.SvgCanvas#event:ext-langReady} */ {\n lang, uiStrings, importLocale: getImportLocale({defaultLang: lang, defaultName: loadedExtensionName})\n }\n );\n });\n }\n svgCanvas.runExtensions('langChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext-langChanged} */ lang);\n\n // Update flyout tooltips\n setFlyoutTitles();\n\n // Copy title for certain tool elements\n const elems = {\n '#stroke_color': '#tool_stroke .icon_label, #tool_stroke .color_block',\n '#fill_color': '#tool_fill label, #tool_fill .color_block',\n '#linejoin_miter': '#cur_linejoin',\n '#linecap_butt': '#cur_linecap'\n };\n\n $.each(elems, function (source, dest) {\n $(dest).attr('title', $(source)[0].title);\n });\n\n // Copy alignment titles\n $('#multiselected_panel div[id^=tool_align]').each(function () {\n $('#tool_pos' + this.id.substr(10))[0].title = this.title;\n });\n };\n localeInit(\n /**\n * @implements {module:locale.LocaleEditorInit}\n */\n {\n /**\n * Gets an array of results from extensions with a `addLangData` method,\n * returning an object with a `data` property set to its locales (to be\n * merged with regular locales).\n * @param {string} langParam\n * @fires module:svgcanvas.SvgCanvas#event:ext-addLangData\n * @todo Can we forego this in favor of `langReady` (or forego `langReady`)?\n * @returns {module:locale.AddLangExtensionLocaleData[]}\n */\n addLangData (langParam) {\n return svgCanvas.runExtensions(\n 'addLangData',\n /**\n * @function\n * @type {module:svgcanvas.ExtensionVarBuilder}\n * @param {string} name\n * @returns {module:svgcanvas.SvgCanvas#event:ext-addLangData}\n */\n (name) => { // We pass in a function as we don't know the extension name here when defining this `addLangData` method\n return {\n lang: langParam,\n importLocale: getImportLocale({defaultLang: langParam, defaultName: name})\n };\n },\n true\n );\n },\n curConfig\n }\n );\n // Load extensions\n // Bit of a hack to run extensions in local Opera/IE9\n if (document.location.protocol === 'file:') {\n setTimeout(extAndLocaleFunc, 100);\n } else {\n // Returns a promise (if we wanted to fire 'extensions-loaded' event, potentially useful to hide interface as some extension locales are only available after this)\n extAndLocaleFunc();\n }\n};\n\n/**\n* @callback module:SVGEditor.ReadyCallback\n* @returns {Promise|undefined}\n*/\n/**\n* Queues a callback to be invoked when the editor is ready (or\n* to be invoked immediately if it is already ready--i.e.,\n* if `runCallbacks` has been run).\n* @param {module:SVGEditor.ReadyCallback} cb Callback to be queued to invoke\n* @returns {Promise} Resolves when all callbacks, including the supplied have resolved\n*/\neditor.ready = function (cb) { // eslint-disable-line promise/prefer-await-to-callbacks\n return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new\n if (isReady) {\n resolve(cb()); // eslint-disable-line callback-return, promise/prefer-await-to-callbacks\n return;\n }\n callbacks.push([cb, resolve, reject]);\n });\n};\n\n/**\n* Invokes the callbacks previous set by `svgEditor.ready`\n* @returns {Promise} Resolves to `undefined` if all callbacks succeeded and rejects otherwise\n*/\neditor.runCallbacks = async function () {\n try {\n await Promise.all(callbacks.map(([cb]) => {\n return cb(); // eslint-disable-line promise/prefer-await-to-callbacks\n }));\n } catch (err) {\n callbacks.forEach(([, , reject]) => {\n reject();\n });\n throw err;\n }\n callbacks.forEach(([, resolve]) => {\n resolve();\n });\n isReady = true;\n};\n\n/**\n* @param {string} str The SVG string to load\n* @param {PlainObject} [opts={}]\n* @param {boolean} [opts.noAlert=false] Option to avoid alert to user and instead get rejected promise\n* @returns {Promise}\n*/\neditor.loadFromString = function (str, {noAlert} = {}) {\n editor.ready(async function () {\n try {\n await loadSvgString(str, {noAlert});\n } catch (err) {\n if (noAlert) {\n throw err;\n }\n }\n });\n};\n\n/**\n* Not presently in use.\n* @param {PlainObject} featList\n* @returns {undefined}\n*/\neditor.disableUI = function (featList) {\n // $(function () {\n // $('#tool_wireframe, #tool_image, #main_button, #tool_source, #sidepanels').remove();\n // $('#tools_top').css('left', 5);\n // });\n};\n\n/**\n * @callback module:SVGEditor.URLLoadCallback\n * @param {boolean} success\n * @returns {undefined}\n */\n/**\n* @param {string} url URL from which to load an SVG string via Ajax\n* @param {PlainObject} [opts={}] May contain properties: `cache`, `callback`\n* @param {boolean} [opts.cache]\n* @param {boolean} [opts.noAlert]\n* @returns {Promise} Resolves to `undefined` or rejects upon bad loading of\n* the SVG (or upon failure to parse the loaded string) when `noAlert` is\n* enabled\n*/\neditor.loadFromURL = function (url, {cache, noAlert} = {}) {\n return editor.ready(function () {\n return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new\n $.ajax({\n url,\n dataType: 'text',\n cache: Boolean(cache),\n beforeSend () {\n $.process_cancel(uiStrings.notification.loadingImage);\n },\n success (str) {\n resolve(loadSvgString(str, {noAlert}));\n },\n error (xhr, stat, err) {\n if (xhr.status !== 404 && xhr.responseText) {\n resolve(loadSvgString(xhr.responseText, {noAlert}));\n return;\n }\n if (noAlert) {\n reject(new Error('URLLoadFail'));\n return;\n }\n $.alert(uiStrings.notification.URLLoadFail + ': \\n' + err);\n resolve();\n },\n complete () {\n $('#dialog_box').hide();\n }\n });\n });\n });\n};\n\n/**\n* @param {string} str The Data URI to base64-decode (if relevant) and load\n* @param {PlainObject} [opts={}]\n* @param {boolean} [opts.noAlert]\n* @returns {Promise} Resolves to `undefined` and rejects if loading SVG string fails and `noAlert` is enabled\n*/\neditor.loadFromDataURI = function (str, {noAlert} = {}) {\n editor.ready(function () {\n let base64 = false;\n let pre = str.match(/^data:image\\/svg\\+xml;base64,/);\n if (pre) {\n base64 = true;\n } else {\n pre = str.match(/^data:image\\/svg\\+xml(?:;|;utf8)?,/);\n }\n if (pre) {\n pre = pre[0];\n }\n const src = str.slice(pre.length);\n return loadSvgString(base64 ? Utils.decode64(src) : decodeURIComponent(src), {noAlert});\n });\n};\n\n/**\n * @param {string} name Used internally; no need for i18n.\n * @param {module:svgcanvas.ExtensionInitCallback} init Config to be invoked on this module\n * @param {module:svgcanvas.ExtensionInitArgs} initArgs\n * @throws {Error} If called too early\n * @returns {Promise} Resolves to `undefined`\n*/\neditor.addExtension = function (name, init, initArgs) {\n // Note that we don't want this on editor.ready since some extensions\n // may want to run before then (like server_opensave).\n // $(function () {\n if (!svgCanvas) {\n throw new Error('Extension added too early');\n }\n return svgCanvas.addExtension.call(this, name, init, initArgs);\n // });\n};\n\n// Defer injection to wait out initial menu processing. This probably goes\n// away once all context menu behavior is brought to context menu.\neditor.ready(() => {\n injectExtendedContextMenuItemsIntoDom();\n});\n\nlet extensionsAdded = false;\nconst messageQueue = [];\n/**\n * @param {PlainObject} info\n * @param {Any} info.data\n * @param {string} info.origin\n * @fires module:svgcanvas.SvgCanvas#event:message\n * @returns {undefined}\n */\nconst messageListener = ({data, origin}) => { // eslint-disable-line no-shadow\n // console.log('data, origin, extensionsAdded', data, origin, extensionsAdded);\n const messageObj = {data, origin};\n if (!extensionsAdded) {\n messageQueue.push(messageObj);\n } else {\n // Extensions can handle messages at this stage with their own\n // canvas `message` listeners\n svgCanvas.call('message', messageObj);\n }\n};\nwindow.addEventListener('message', messageListener);\n\n// Run init once DOM is loaded\n// jQuery(editor.init);\n\n(async () => {\ntry {\n // We wait a micro-task to let the svgEditor variable be defined for module checks\n await Promise.resolve();\n editor.init();\n} catch (err) {\n console.error(err); // eslint-disable-line no-console\n}\n})();\n\nexport default editor;\n","// Todo: Update: https://github.com/jeresig/jquery.hotkeys\n/*\n * jQuery Hotkeys Plugin\n * Copyright 2010, John Resig\n * Dual licensed under the MIT or GPL Version 2 licenses.\n *\n * http://github.com/jeresig/jquery.hotkeys\n *\n * Based upon the plugin by Tzury Bar Yochay:\n * http://github.com/tzuryby/hotkeys\n *\n * Original idea by:\n * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/\n*/\n\n// We *do* want to allow the escape key within textareas (and possibly tab too), so add the condition `n.which !== 27`\n\nexport default function(b){b.hotkeys={version:\"0.8\",specialKeys:{8:\"backspace\",9:\"tab\",13:\"return\",16:\"shift\",17:\"ctrl\",18:\"alt\",19:\"pause\",20:\"capslock\",27:\"esc\",32:\"space\",33:\"pageup\",34:\"pagedown\",35:\"end\",36:\"home\",37:\"left\",38:\"up\",39:\"right\",40:\"down\",45:\"insert\",46:\"del\",96:\"0\",97:\"1\",98:\"2\",99:\"3\",100:\"4\",101:\"5\",102:\"6\",103:\"7\",104:\"8\",105:\"9\",106:\"*\",107:\"+\",109:\"-\",110:\".\",111:\"/\",112:\"f1\",113:\"f2\",114:\"f3\",115:\"f4\",116:\"f5\",117:\"f6\",118:\"f7\",119:\"f8\",120:\"f9\",121:\"f10\",122:\"f11\",123:\"f12\",144:\"numlock\",145:\"scroll\",191:\"/\",224:\"meta\",219:\"[\",221:\"]\"},shiftNums:{\"`\":\"~\",\"1\":\"!\",\"2\":\"@\",\"3\":\"#\",\"4\":\"$\",\"5\":\"%\",\"6\":\"^\",\"7\":\"&\",\"8\":\"*\",\"9\":\"(\",\"0\":\")\",\"-\":\"_\",\"=\":\"+\",\";\":\": \",\"'\":'\"',\",\":\"<\",\".\":\">\",\"/\":\"?\",\"\\\\\":\"|\"}};function a(d){if(typeof d.data!==\"string\"){return}var c=d.handler,e=d.data.toLowerCase().split(\" \");d.handler=function(n){if(this!==n.target&&(n.which !== 27 && (/textarea|select/i.test(n.target.nodeName)||n.target.type===\"text\"))){return}var h=n.type!==\"keypress\"&&b.hotkeys.specialKeys[n.which],o=String.fromCharCode(n.which).toLowerCase(),k,m=\"\",g={};if(n.altKey&&h!==\"alt\"){m+=\"alt+\"}if(n.ctrlKey&&h!==\"ctrl\"){m+=\"ctrl+\"}if(n.metaKey&&!n.ctrlKey&&h!==\"meta\"){m+=\"meta+\"}if(n.shiftKey&&h!==\"shift\"){m+=\"shift+\"}if(h){g[m+h]=true}else{g[m+o]=true;g[m+b.hotkeys.shiftNums[o]]=true;if(m===\"shift+\"){g[b.hotkeys.shiftNums[o]]=true}}for(var j=0,f=e.length;j
').hide().insertAfter(\"body\")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash=\"#\"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,\"\")+\"#\"+u}}r=setTimeout(s,$[d+\"Delay\"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,window);\n\nreturn jQuery;\n}\n","// Todo: Move to own module (and have it import a modular base64 encoder)\nimport {encode64} from '../utilities.js';\n/**\n * 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 usage #1:\n\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 usage #2:\n\n$(function() {\n $.svgIcons('my_icon_set.svg', { // The SVG file that contains all icons\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 }); //The SVG file that contains all icons\n});\n\n* @example usage #3:\n\n$(function() {\n $.svgIcons('my_icon_set.svgz', { // The SVGZ file that contains all icons\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\n/**\n* @callback module:jQuerySVGIcons.SVGIconsLoadedCallback\n* @param {PlainObject.} svgIcons IDs keyed to jQuery objects of images\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 let fixIDs;\n /**\n * List of raster images with each\n * key being the SVG icon ID to replace, and the value the image file name\n * @typedef {PlainObject.} external:jQuery.svgIcons.Fallback\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 be replaced by,\n rather than include the SVG icon.\n * @param {PlainObject.} [opts.placement] List with selectors for keys and SVG icon ids\n as values. This provides a custom method of adding icons.\n * @param {PlainObject.} [opts.resize] List with selectors for keys and numbers\n as values. This allows an easy way to resize specific icons.\n * @param {module:jQuerySVGIcons.SVGIconsLoadedCallback} [opts.callback] A function to call when all icons have been loaded.\n * @param {boolean} [opts.id_match=true] Automatically attempt to match SVG icon ids with\n corresponding HTML id\n * @param {boolean} [opts.no_img] Prevent attempting to convert the icon into an ``\n element (may be faster, help for browser consistency)\n * @param {boolean} [opts.svgz] Indicate that the file is an SVGZ file, and thus not to\n parse as XML. SVGZ files add compression benefits, but getting data from\n them fails in Firefox 2 and older.\n * @returns {undefined}\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 isOpera = Boolean(window.opera),\n // 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 = $('