diff --git a/src/editor/Rulers.js b/src/editor/Rulers.js new file mode 100644 index 00000000..25bf17a9 --- /dev/null +++ b/src/editor/Rulers.js @@ -0,0 +1,192 @@ +/* globals $ */ +import {getTypeMap} from '../common/units.js'; + +/** + * + */ +class Rulers { + /** + * @type {Module} + */ + constructor (editor) { + // Make [1,2,5] array + this.rulerIntervals = []; + for (let i = 0.1; i < 1e5; i *= 10) { + this.rulerIntervals.push(i); + this.rulerIntervals.push(2 * i); + this.rulerIntervals.push(5 * i); + } + this.svgCanvas = editor.svgCanvas; + this.curConfig = editor.configObj; + this.editor = editor; + } + /** + * @type {Module} + */ + manageScroll () { + const rulerX = document.getElementById('ruler_x'); + const rulerY = document.getElementById('ruler_x'); + + if (rulerX) rulerX.scrollLeft = this.editor.workarea.scrollLeft; + if (rulerY) rulerY.scrollTop = this.editor.workarea.scrollTop; + } + + /** + * + * @param {HTMLDivElement} [scanvas] + * @param {Float} [zoom] + * @returns {void} + */ + updateRulers (scanvas, zoom) { + if (!zoom) { zoom = this.svgCanvas.getZoom(); } + if (!scanvas) { scanvas = $('#svgcanvas'); } + + let d, i; + const limit = 30000; + const contentElem = this.svgCanvas.getContentElem(); + const units = getTypeMap(); + const unit = units[this.curConfig.baseUnit]; // 1 = 1px + + // draw x ruler then y ruler + for (d = 0; d < 2; d++) { + const isX = (d === 0); + const dim = isX ? 'x' : 'y'; + const lentype = isX ? 'width' : 'height'; + const contentDim = Number(contentElem.getAttribute(dim)); + + const $hcanvOrig = $('#ruler_' + dim + ' canvas:first'); + + // Bit of a hack to fully clear the canvas in Safari & IE9 + const $hcanv = $hcanvOrig.clone(); + $hcanvOrig.replaceWith($hcanv); + + const hcanv = $hcanv[0]; + + // Set the canvas size to the width of the container + let rulerLen = scanvas[lentype](); + const totalLen = rulerLen; + hcanv.parentNode.style[lentype] = totalLen + 'px'; + let ctx = hcanv.getContext('2d'); + let ctxArr, num, ctxArrNum; + + ctx.fillStyle = 'rgb(200,0,0)'; + ctx.fillRect(0, 0, hcanv.width, hcanv.height); + + // Remove any existing canvasses + $hcanv.siblings().remove(); + + // Create multiple canvases when necessary (due to browser limits) + if (rulerLen >= limit) { + ctxArrNum = Number.parseInt(rulerLen / limit) + 1; + ctxArr = []; + ctxArr[0] = ctx; + let copy; + for (i = 1; i < ctxArrNum; i++) { + hcanv[lentype] = limit; + copy = hcanv.cloneNode(true); + hcanv.parentNode.append(copy); + ctxArr[i] = copy.getContext('2d'); + } + + copy[lentype] = rulerLen % limit; + + // set copy width to last + rulerLen = limit; + } + + hcanv[lentype] = rulerLen; + + const uMulti = unit * zoom; + + // Calculate the main number interval + const rawM = 50 / uMulti; + let multi = 1; + for (i = 0; i < this.rulerIntervals.length; i++) { + num = this.rulerIntervals[i]; + multi = num; + if (rawM <= num) { + break; + } + } + + const bigInt = multi * uMulti; + + ctx.font = '9px sans-serif'; + + let rulerD = ((contentDim / uMulti) % multi) * uMulti; + let labelPos = rulerD - bigInt; + // draw big intervals + let ctxNum = 0; + while (rulerD < totalLen) { + labelPos += bigInt; + // const realD = rulerD - contentDim; // Currently unused + + const curD = Math.round(rulerD) + 0.5; + if (isX) { + ctx.moveTo(curD, 15); + ctx.lineTo(curD, 0); + } else { + ctx.moveTo(15, curD); + ctx.lineTo(0, curD); + } + + num = (labelPos - contentDim) / uMulti; + let label; + if (multi >= 1) { + label = Math.round(num); + } else { + const decs = String(multi).split('.')[1].length; + label = num.toFixed(decs); + } + + // Change 1000s to Ks + if (label !== 0 && label !== 1000 && label % 1000 === 0) { + label = (label / 1000) + 'K'; + } + + if (isX) { + ctx.fillText(label, rulerD + 2, 8); + } else { + // draw label vertically + const str = String(label).split(''); + for (i = 0; i < str.length; i++) { + ctx.fillText(str[i], 1, (rulerD + 9) + i * 9); + } + } + + const part = bigInt / 10; + // draw the small intervals + for (i = 1; i < 10; i++) { + let subD = Math.round(rulerD + part * i) + 0.5; + if (ctxArr && subD > rulerLen) { + ctxNum++; + ctx.stroke(); + if (ctxNum >= ctxArrNum) { + i = 10; + rulerD = totalLen; + continue; + } + ctx = ctxArr[ctxNum]; + rulerD -= limit; + subD = Math.round(rulerD + part * i) + 0.5; + } + + // odd lines are slighly longer + const lineNum = (i % 2) ? 12 : 10; + if (isX) { + ctx.moveTo(subD, 15); + ctx.lineTo(subD, lineNum); + } else { + ctx.moveTo(15, subD); + ctx.lineTo(lineNum, subD); + } + } + rulerD += bigInt; + } + ctx.strokeStyle = '#000'; + ctx.stroke(); + } + } +} + +export default Rulers; diff --git a/src/editor/embedapi.js b/src/editor/embedapi.js index 4e4af622..29f0bc1d 100644 --- a/src/editor/embedapi.js +++ b/src/editor/embedapi.js @@ -362,7 +362,7 @@ class EmbeddedSVGEdit { let sameOriginWithGlobal = false; try { sameOriginWithGlobal = window.location.origin === that.frame.contentWindow.location.origin && - that.frame.contentWindow.svgEditor.canvas; + that.frame.contentWindow.svgEditor.svgCanvas; } catch (err) {} if (sameOriginWithGlobal) { diff --git a/src/editor/extensions/ext-arrows/ext-arrows.js b/src/editor/extensions/ext-arrows/ext-arrows.js index fc89ee6e..4bba6c06 100644 --- a/src/editor/extensions/ext-arrows/ext-arrows.js +++ b/src/editor/extensions/ext-arrows/ext-arrows.js @@ -24,7 +24,7 @@ export default { async init (S) { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const addElem = svgCanvas.addSVGElementFromJson, {nonce, $} = S, diff --git a/src/editor/extensions/ext-connector/ext-connector.js b/src/editor/extensions/ext-connector/ext-connector.js index 4e59ead4..3571a785 100644 --- a/src/editor/extensions/ext-connector/ext-connector.js +++ b/src/editor/extensions/ext-connector/ext-connector.js @@ -23,7 +23,7 @@ export default { name: 'connector', async init (S) { const svgEditor = this; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const {getElem} = svgCanvas; const {$, svgroot} = S, addElem = svgCanvas.addSVGElementFromJson, @@ -374,7 +374,7 @@ export default { startX = opts.start_x; startY = opts.start_y; const mode = svgCanvas.getMode(); - const {curConfig: {initStroke}} = svgEditor; + const {curConfig: {initStroke}} = svgEditor.configObj; if (mode === 'connector') { if (started) { return undefined; } diff --git a/src/editor/extensions/ext-eyedropper/ext-eyedropper.js b/src/editor/extensions/ext-eyedropper/ext-eyedropper.js index 3325e65e..d4a03ddd 100644 --- a/src/editor/extensions/ext-eyedropper/ext-eyedropper.js +++ b/src/editor/extensions/ext-eyedropper/ext-eyedropper.js @@ -26,7 +26,7 @@ export default { const strings = await loadExtensionTranslation(svgEditor.pref('lang')); const {$, ChangeElementCommand} = S, // , svgcontent, // svgdoc = S.svgroot.parentNode.ownerDocument, - svgCanvas = svgEditor.canvas, + {svgCanvas} = svgEditor, addToHistory = function (cmd) { svgCanvas.undoMgr.addCommandToHistory(cmd); }, currentStyle = { fillPaint: 'red', fillOpacity: 1.0, diff --git a/src/editor/extensions/ext-foreignobject/ext-foreignobject.js b/src/editor/extensions/ext-foreignobject/ext-foreignobject.js index 60cc0b64..3b0c35d1 100644 --- a/src/editor/extensions/ext-foreignobject/ext-foreignobject.js +++ b/src/editor/extensions/ext-foreignobject/ext-foreignobject.js @@ -24,7 +24,7 @@ export default { async init (S) { const svgEditor = this; const {$, text2xml, NS} = S; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const // {svgcontent} = S, // addElem = svgCanvas.addSVGElementFromJson, diff --git a/src/editor/extensions/ext-grid/ext-grid.js b/src/editor/extensions/ext-grid/ext-grid.js index 2ca766b9..ac01caf4 100644 --- a/src/editor/extensions/ext-grid/ext-grid.js +++ b/src/editor/extensions/ext-grid/ext-grid.js @@ -24,14 +24,14 @@ export default { async init ({$, NS, getTypeMap}) { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const svgdoc = document.getElementById('svgcanvas').ownerDocument, {assignAttributes} = svgCanvas, hcanvas = document.createElement('canvas'), canvBG = $('#canvasBackground'), units = getTypeMap(), // Assumes prior `init()` call on `units.js` module intervals = [0.01, 0.1, 1, 10, 100, 1000]; - let showGrid = svgEditor.curConfig.showGrid || false; + let showGrid = svgEditor.configObj.curConfig.showGrid || false; $(hcanvas).hide().appendTo('body'); @@ -90,7 +90,7 @@ export default { */ function updateGrid (zoom) { // TODO: Try this with elements, then compare performance difference - const unit = units[svgEditor.curConfig.baseUnit]; // 1 = 1px + const unit = units[svgEditor.configObj.curConfig.baseUnit]; // 1 = 1px const uMulti = unit * zoom; // Calculate the main number interval const rawM = 100 / uMulti; @@ -109,7 +109,7 @@ export default { const part = bigInt / 10; ctx.globalAlpha = 0.2; - ctx.strokeStyle = svgEditor.curConfig.gridColor; + ctx.strokeStyle = svgEditor.configObj.curConfig.gridColor; for (let i = 1; i < 10; i++) { const subD = Math.round(part * i) + 0.5; // const lineNum = (i % 2)?12:10; @@ -161,7 +161,7 @@ export default { events: { id: 'view_grid', click () { - svgEditor.curConfig.showGrid = showGrid = !showGrid; + svgEditor.configObj.curConfig.showGrid = showGrid = !showGrid; gridUpdate(); } } diff --git a/src/editor/extensions/ext-helloworld/ext-helloworld.js b/src/editor/extensions/ext-helloworld/ext-helloworld.js index a88c2e95..a4b7d86c 100644 --- a/src/editor/extensions/ext-helloworld/ext-helloworld.js +++ b/src/editor/extensions/ext-helloworld/ext-helloworld.js @@ -30,7 +30,7 @@ export default { async init ({$, importLocale}) { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; return { name: strings.name, events: [{ diff --git a/src/editor/extensions/ext-markers/ext-markers.js b/src/editor/extensions/ext-markers/ext-markers.js index 92e8700c..cf54822b 100644 --- a/src/editor/extensions/ext-markers/ext-markers.js +++ b/src/editor/extensions/ext-markers/ext-markers.js @@ -47,7 +47,7 @@ export default { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); const {$} = S; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const // {svgcontent} = S, addElem = svgCanvas.addSVGElementFromJson; const mtypes = ['start', 'mid', 'end']; diff --git a/src/editor/extensions/ext-mathjax/ext-mathjax.js b/src/editor/extensions/ext-mathjax/ext-mathjax.js index 133c2375..6d5d8e0a 100644 --- a/src/editor/extensions/ext-mathjax/ext-mathjax.js +++ b/src/editor/extensions/ext-mathjax/ext-mathjax.js @@ -25,7 +25,7 @@ export default { async init ({$}) { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; // Configuration of the MathJax extention. diff --git a/src/editor/extensions/ext-panning/ext-panning.js b/src/editor/extensions/ext-panning/ext-panning.js index 33060642..9b96148c 100644 --- a/src/editor/extensions/ext-panning/ext-panning.js +++ b/src/editor/extensions/ext-panning/ext-panning.js @@ -27,7 +27,7 @@ export default { async init ({importLocale}) { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const buttons = [{ id: 'ext-panning', icon: 'panning.png', diff --git a/src/editor/extensions/ext-placemark/ext-placemark.js b/src/editor/extensions/ext-placemark/ext-placemark.js index 5d259611..807634a6 100644 --- a/src/editor/extensions/ext-placemark/ext-placemark.js +++ b/src/editor/extensions/ext-placemark/ext-placemark.js @@ -22,7 +22,7 @@ export default { name: 'placemark', async init (S) { const svgEditor = this; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const addElem = svgCanvas.addSVGElementFromJson; const {$} = S; // {svgcontent}, let diff --git a/src/editor/extensions/ext-polygon/ext-polygon.js b/src/editor/extensions/ext-polygon/ext-polygon.js index 415cb644..56915e48 100644 --- a/src/editor/extensions/ext-polygon/ext-polygon.js +++ b/src/editor/extensions/ext-polygon/ext-polygon.js @@ -22,7 +22,7 @@ export default { name: 'polygon', async init (S) { const svgEditor = this; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const {$} = S, // {svgcontent} // addElem = svgCanvas.addSVGElementFromJson, editingitex = false; diff --git a/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js b/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js index a0bf4b38..88f85a23 100644 --- a/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js +++ b/src/editor/extensions/ext-server_moinsave/ext-server_moinsave.js @@ -26,7 +26,7 @@ export default { async init ({$, encode64, importLocale}) { const svgEditor = this; const strings = await loadExtensionTranslation(svgEditor.pref('lang')); - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const saveSvgAction = '/+modify'; // Create upload target (hidden iframe) diff --git a/src/editor/extensions/ext-shapes/ext-shapes.js b/src/editor/extensions/ext-shapes/ext-shapes.js index 545140ab..366b020d 100644 --- a/src/editor/extensions/ext-shapes/ext-shapes.js +++ b/src/editor/extensions/ext-shapes/ext-shapes.js @@ -11,7 +11,7 @@ export default { name: 'shapes', init ({$}) { const svgEditor = this; - const canv = svgEditor.canvas; + const canv = svgEditor.svgCanvas; const svgroot = canv.getRootElem(); let lastBBox = {}; diff --git a/src/editor/extensions/ext-star/ext-star.js b/src/editor/extensions/ext-star/ext-star.js index 0340d80f..2d0a5292 100644 --- a/src/editor/extensions/ext-star/ext-star.js +++ b/src/editor/extensions/ext-star/ext-star.js @@ -22,7 +22,7 @@ export default { name: 'star', async init (S) { const svgEditor = this; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; const {$} = S; // {svgcontent}, let diff --git a/src/editor/extensions/ext-storage/ext-storage.js b/src/editor/extensions/ext-storage/ext-storage.js index dddadd53..13d80719 100644 --- a/src/editor/extensions/ext-storage/ext-storage.js +++ b/src/editor/extensions/ext-storage/ext-storage.js @@ -36,7 +36,7 @@ export default { name: 'storage', init ({$}) { const svgEditor = this; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; // We could empty any already-set data for users when they decline storage, // but it would be a risk for users who wanted to store but accidentally @@ -56,7 +56,7 @@ export default { // the "noStorageOnLoad" config setting to true in svgedit-config-*.js. noStorageOnLoad, forceStorage - } = svgEditor.curConfig; + } = svgEditor.configObj.curConfig; const {storage, updateCanvas} = svgEditor; /** @@ -90,7 +90,7 @@ export default { */ function setSVGContentStorage (val) { if (storage) { - const name = 'svgedit-' + svgEditor.curConfig.canvasName; + const name = 'svgedit-' + svgEditor.configObj.curConfig.canvasName; if (!val) { storage.removeItem(name); } else { diff --git a/src/editor/extensions/ext-webappfind/ext-webappfind.js b/src/editor/extensions/ext-webappfind/ext-webappfind.js index 9a4cf89a..ac4831a1 100644 --- a/src/editor/extensions/ext-webappfind/ext-webappfind.js +++ b/src/editor/extensions/ext-webappfind/ext-webappfind.js @@ -99,7 +99,7 @@ export default { webappfind: { type: saveMessage, pathID, - content: svgEditor.canvas.getSvgString() + content: svgEditor.svgCanvas.getSvgString() } }, window.location.origin === 'null' // Avoid "null" string error for `file:` protocol (even diff --git a/src/editor/extensions/ext-xdomain-messaging/ext-xdomain-messaging.js b/src/editor/extensions/ext-xdomain-messaging/ext-xdomain-messaging.js index 176c319f..cefd69a0 100644 --- a/src/editor/extensions/ext-xdomain-messaging/ext-xdomain-messaging.js +++ b/src/editor/extensions/ext-xdomain-messaging/ext-xdomain-messaging.js @@ -8,7 +8,7 @@ export default { name: 'xdomain-messaging', init () { const svgEditor = this; - const svgCanvas = svgEditor.canvas; + const {svgCanvas} = svgEditor; try { window.addEventListener('message', function (e) { // We accept and post strings for the sake of IE9 support @@ -22,7 +22,7 @@ export default { // The default is not to allow any origins, including even the same domain or // if run on a `file:///` URL. See `svgedit-config-es.js` for an example of how // to configure - const {allowedOrigins} = svgEditor.curConfig; + const {allowedOrigins} = svgEditor.configObj.curConfig; if (!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin)) { console.log(`Origin ${e.origin} not whitelisted for posting to ${window.origin}`); // eslint-disable-line no-console return; diff --git a/src/editor/panels/BottomPanelHandlers.js b/src/editor/panels/BottomPanelHandlers.js index 4ace5141..dc812f03 100644 --- a/src/editor/panels/BottomPanelHandlers.js +++ b/src/editor/panels/BottomPanelHandlers.js @@ -15,7 +15,7 @@ class BottomPanelHandlers { */ constructor (editor) { this.editor = editor; - this.svgCanvas = editor.canvas; + this.svgCanvas = editor.svgCanvas; } /** * @type {module} @@ -178,7 +178,7 @@ class BottomPanelHandlers { */ init () { // register actions for bottom panel - $id('zoom').addEventListener('change', (e) => this.changeZoom(e.detail.value).bind(this)); + $id('zoom').addEventListener('change', (e) => this.changeZoom.bind(this)(e.detail.value)); $id('stroke_color').addEventListener('change', (evt) => this.handleColorPicker.bind(this)('stroke', evt)); $id('fill_color').addEventListener('change', (evt) => this.handleColorPicker.bind(this)('fill', evt)); $id('stroke_width').addEventListener('change', this.changeStrokeWidth.bind(this)); diff --git a/src/editor/panels/LayersPanel.js b/src/editor/panels/LayersPanel.js index 1b32e4de..1ae435bd 100644 --- a/src/editor/panels/LayersPanel.js +++ b/src/editor/panels/LayersPanel.js @@ -14,7 +14,7 @@ class LayersPanel { * @param {PlainObject} editor */ constructor (editor) { - this.svgCanvas = editor.canvas; + this.svgCanvas = editor.svgCanvas; this.uiStrings = editor.uiStrings; this.updateContextPanel = editor.topPanelHandlers.updateContextPanel; this.sidedrag = -1; @@ -120,7 +120,7 @@ class LayersPanel { this.lmenuFunc.bind(this)(e?.detail?.trigger, e?.detail?.source); }); $id('sidepanel_handle').addEventListener('click', this.toggleSidePanel.bind(this)); - if (this.editor.curConfig.showlayers) { + if (this.editor.configObj.curConfig.showlayers) { this.toggleSidePanel(); } $id('sidepanel_handle').addEventListener('mousedown', (evt) => { diff --git a/src/editor/panels/LeftPanelHandlers.js b/src/editor/panels/LeftPanelHandlers.js index 82c5e247..212a95b3 100644 --- a/src/editor/panels/LeftPanelHandlers.js +++ b/src/editor/panels/LeftPanelHandlers.js @@ -14,7 +14,7 @@ class LeftPanelHandlers { */ constructor (editor) { this.editor = editor; - this.svgCanvas = editor.canvas; + this.svgCanvas = editor.svgCanvas; } /** * This is a common function used when a tool has been clicked (chosen). diff --git a/src/editor/panels/TopPanelHandlers.js b/src/editor/panels/TopPanelHandlers.js index 5134418f..7764d595 100644 --- a/src/editor/panels/TopPanelHandlers.js +++ b/src/editor/panels/TopPanelHandlers.js @@ -16,7 +16,7 @@ class TopPanelHandlers { */ constructor (editor) { this.editor = editor; - this.svgCanvas = editor.canvas; + this.svgCanvas = editor.svgCanvas; this.uiStrings = editor.uiStrings; } /** @@ -198,7 +198,7 @@ class TopPanelHandlers { curPanel.forEach((item) => { let attrVal = elem.getAttribute(item); - if (this.editor.curConfig.baseUnit !== 'px' && elem[item]) { + if (this.editor.configObj.curConfig.baseUnit !== 'px' && elem[item]) { const bv = elem[item].baseVal.value; attrVal = convertUnit(bv); } @@ -265,8 +265,8 @@ class TopPanelHandlers { } // update history buttons - $id('tool_undo').disabled = (this.editor.canvas.undoMgr.getUndoStackSize() === 0); - $id('tool_redo').disabled = (this.editor.canvas.undoMgr.getRedoStackSize() === 0); + $id('tool_undo').disabled = (this.svgCanvas.undoMgr.getUndoStackSize() === 0); + $id('tool_redo').disabled = (this.svgCanvas.undoMgr.getRedoStackSize() === 0); this.svgCanvas.addedNew = false; diff --git a/src/editor/svgedit.js b/src/editor/svgedit.js index c8709ded..32bd3085 100644 --- a/src/editor/svgedit.js +++ b/src/editor/svgedit.js @@ -1,7 +1,7 @@ /* eslint-disable no-alert */ /* globals jQuery seSelect */ /** -* The main module for the visual SVG Editor. +* The main module for the visual SVG this. * * @license MIT * @@ -19,7 +19,7 @@ import './touch.js'; import {isChrome, isGecko, isMac} from '../common/browser.js'; -import {getTypeMap, convertUnit, isValidUnit} from '../common/units.js'; +import {convertUnit, isValidUnit} from '../common/units.js'; import { hasCustomHandler, getCustomHandler, injectExtendedContextMenuItemsIntoDom } from './contextmenu.js'; @@ -31,203 +31,317 @@ import LayersPanel from './panels/LayersPanel.js'; import LeftPanelHandlers from './panels/LeftPanelHandlers.js'; import BottomPanelHandlers from './panels/BottomPanelHandlers.js'; import TopPanelHandlers from './panels/TopPanelHandlers.js'; +import Rulers from './Rulers.js'; import { readLang, putLocale, setStrings } from './locale.js'; -const {$id, isNullish, encode64, decode64, blankPageObjectURL} = SvgCanvas; - -const editor = { - /** - * @type {Float} - */ - tool_scale: 1, - /** - * @type {Integer} - */ - exportWindowCt: 0, - /** - * @type {boolean} - */ - langChanged: false, - /** - * @type {boolean} - */ - showSaveWarning: false, - /** - * Will be set to a boolean by `ext-storage.js` - * @type {"ignore"|"waiting"|"closed"} - */ - storagePromptState: 'ignore', - /* - * EDITOR PUBLIC METHODS - */ - putLocale, - readLang, - setStrings -}; +const {$id, $qa, isNullish, encode64, decode64, blankPageObjectURL} = SvgCanvas; // JFH hotkey is used for text input. const $ = [jQueryPluginJSHotkeys].reduce((jq, func) => func(jq), jQuery); const homePage = 'https://github.com/SVG-Edit/svgedit'; - -const callbacks = []; - -/** -* LOCALE. -* @name module:SVGEditor.uiStrings -* @type {PlainObject} -*/ -const uiStrings = editor.uiStrings = {}; - -let svgCanvas, - isReady = false, - customExportImage = false, - customExportPDF = false; - /** * - * @param {string} str SVG string - * @param {PlainObject} [opts={}] - * @param {boolean} [opts.noAlert] - * @throws {Error} Upon failure to load SVG */ -const loadSvgString = (str, {noAlert} = {}) => { - const success = svgCanvas.setSvgString(str) !== false; - if (success) return; - if (!noAlert) window.alert(uiStrings.notification.errorLoadingSVG); - throw new Error('Error loading SVG'); -}; - -editor.configObj = new ConfigObj(editor); -editor.pref = editor.configObj.pref.bind(editor.configObj); -editor.setConfig = editor.configObj.setConfig.bind(editor.configObj); -editor.curPrefs = editor.configObj.curPrefs; -editor.curConfig = editor.configObj.curConfig; - -/** -* All methods are optional. -* @interface module:SVGEditor.CustomHandler -* @type {PlainObject} -*/ -/** -* Its responsibilities are: -* - invoke a file chooser dialog in 'open' mode -* - let user pick a SVG file -* - calls [svgCanvas.setSvgString()]{@link module:svgcanvas.SvgCanvas#setSvgString} with the string contents of that file. -* Not passed any parameters. -* @function module:SVGEditor.CustomHandler#open -* @returns {void} -*/ -/** -* Its responsibilities are: -* - accept the string contents of the current document -* - invoke a file chooser dialog in 'save' mode -* - save the file to location chosen by the user. -* @function module:SVGEditor.CustomHandler#save -* @param {external:Window} win -* @param {module:svgcanvas.SvgCanvas#event:saved} svgStr A string of the SVG -* @listens module:svgcanvas.SvgCanvas#event:saved -* @returns {void} -*/ -/** -* Its responsibilities (with regard to the object it is supplied in its 2nd argument) are: -* - inform user of any issues supplied via the "issues" property -* - convert the "svg" property SVG string into an image for export; -* utilize the properties "type" (currently 'PNG', 'JPEG', 'BMP', -* 'WEBP', 'PDF'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' -* types) to determine the proper output. -* @function module:SVGEditor.CustomHandler#exportImage -* @param {external:Window} win -* @param {module:svgcanvas.SvgCanvas#event:exported} data -* @listens module:svgcanvas.SvgCanvas#event:exported -* @returns {void} -*/ -/** -* @function module:SVGEditor.CustomHandler#exportPDF -* @param {external:Window} win -* @param {module:svgcanvas.SvgCanvas#event:exportedPDF} data -* @listens module:svgcanvas.SvgCanvas#event:exportedPDF -* @returns {void} -*/ - -/** -* Allows one to override default SVGEdit `open`, `save`, and -* `export` editor behaviors. -* @function module:SVGEditor.setCustomHandlers -* @param {module:SVGEditor.CustomHandler} opts Extension mechanisms may call `setCustomHandlers` with three functions: `opts.open`, `opts.save`, and `opts.exportImage` -* @returns {Promise} -*/ -editor.setCustomHandlers = function (opts) { - return editor.ready(() => { - 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 - } - }); -}; - -/** - * @function module:SVGEditor.randomizeIds - * @param {boolean} arg - * @returns {void} - */ -editor.randomizeIds = (arg) => { - svgCanvas.randomizeIds(arg); -}; - -/** -* Auto-run after a Promise microtask. -* @function module:SVGEditor.init -* @returns {void} -*/ -editor.init = () => { - try { - if ('localStorage' in window) { // && onWeb removed so Webkit works locally - /** - * The built-in interface implemented by `localStorage` - * @external Storage +class Editor { + /** + * + */ + constructor () { + /** + * @type {Float} + */ + this.tool_scale = 1; + /** + * @type {Integer} + */ + this.exportWindowCt = 0; + /** + * @type {boolean} + */ + this.langChanged = false; + /** + * @type {boolean} + */ + this.showSaveWarning = false; + /** + * Will be set to a boolean by `ext-storage.js` + * @type {"ignore"|"waiting"|"closed"} + */ + this.storagePromptState = 'ignore'; + /* + * EDITOR PUBLIC METHODS + */ + this.putLocale = putLocale; + this.readLang = readLang; + this.setStrings = setStrings; + /** + * LOCALE. + * @name module:SVGthis.uiStrings + * @type {PlainObject} */ - /** - * @name storage - * @memberof module:SVGEditor - * @type {external:Storage} - */ - editor.storage = localStorage; - } - // Image props dialog added to DOM - const newSeImgPropDialog = document.createElement('se-img-prop-dialog'); - newSeImgPropDialog.setAttribute('id', 'se-img-prop'); - document.body.append(newSeImgPropDialog); - // editor prefences dialoag added to DOM - const newSeEditPrefsDialog = document.createElement('se-edit-prefs-dialog'); - newSeEditPrefsDialog.setAttribute('id', 'se-edit-prefs'); - document.body.append(newSeEditPrefsDialog); - // canvas menu added to DOM - const dialogBox = document.createElement('se-cmenu_canvas-dialog'); - dialogBox.setAttribute('id', 'se-cmenu_canvas'); - document.body.append(dialogBox); - } catch (err) {} + this.uiStrings = {}; + this.svgCanvas = null; + this.isReady = false; + this.customExportImage = false; + this.customExportPDF = false; + this.configObj = new ConfigObj(this); + this.pref = this.configObj.pref.bind(this.configObj); + this.setConfig = this.configObj.setConfig.bind(this.configObj); + this.callbacks = []; + this.curContext = null; + this.exportWindowName = null; + this.docprops = false; + this.preferences = false; + this.canvMenu = null; + // eslint-disable-next-line max-len + this.goodLangs = ['ar', 'cs', 'de', 'en', 'es', 'fa', 'fr', 'fy', 'hi', 'it', 'ja', 'nl', 'pl', 'pt-BR', 'ro', 'ru', 'sk', 'sl', 'zh-CN', 'zh-TW']; + const modKey = (isMac() ? 'meta+' : 'ctrl+'); + this.toolButtons = [ + // Shortcuts not associated with buttons + {key: 'ctrl+left', fn () { this.rotateSelected(0, 1); }}, + {key: 'ctrl+right', fn () { this.rotateSelected(1, 1); }}, + {key: 'ctrl+shift+left', fn () { this.rotateSelected(0, 5); }}, + {key: 'ctrl+shift+right', fn () { this.rotateSelected(1, 5); }}, + {key: 'shift+O', fn: this.selectPrev}, + {key: 'shift+P', fn: this.selectNext}, + {key: [modKey + 'up', true], fn () { this.zoomImage(2); }}, + {key: [modKey + 'down', true], fn () { this.zoomImage(0.5); }}, + {key: [modKey + ']', true], fn () { this.moveUpDownSelected('Up'); }}, + {key: [modKey + '[', true], fn () { this.moveUpDownSelected('Down'); }}, + {key: ['up', true], fn () { this.moveSelected(0, -1); }}, + {key: ['down', true], fn () { this.moveSelected(0, 1); }}, + {key: ['left', true], fn () { this.moveSelected(-1, 0); }}, + {key: ['right', true], fn () { this.moveSelected(1, 0); }}, + {key: 'shift+up', fn () { this.moveSelected(0, -10); }}, + {key: 'shift+down', fn () { this.moveSelected(0, 10); }}, + {key: 'shift+left', fn () { this.moveSelected(-10, 0); }}, + {key: 'shift+right', fn () { this.moveSelected(10, 0); }}, + {key: ['alt+up', true], fn () { this.svgCanvas.cloneSelectedElements(0, -1); }}, + {key: ['alt+down', true], fn () { this.svgCanvas.cloneSelectedElements(0, 1); }}, + {key: ['alt+left', true], fn () { this.svgCanvas.cloneSelectedElements(-1, 0); }}, + {key: ['alt+right', true], fn () { this.svgCanvas.cloneSelectedElements(1, 0); }}, + {key: ['alt+shift+up', true], fn () { this.svgCanvas.cloneSelectedElements(0, -10); }}, + {key: ['alt+shift+down', true], fn () { this.svgCanvas.cloneSelectedElements(0, 10); }}, + {key: ['alt+shift+left', true], fn () { this.svgCanvas.cloneSelectedElements(-10, 0); }}, + {key: ['alt+shift+right', true], fn () { this.svgCanvas.cloneSelectedElements(10, 0); }}, + {key: 'a', fn () { this.svgCanvas.selectAllInCurrentLayer(); }}, + {key: modKey + 'a', fn () { this.svgCanvas.selectAllInCurrentLayer(); }}, + {key: modKey + 'x', fn: this.cutSelected}, + {key: modKey + 'c', fn: this.copySelected}, + {key: modKey + 'v', fn: this.pasteInCenter} + ]; + } + /** + * + * @param {string} str SVG string + * @param {PlainObject} [opts={}] + * @param {boolean} [opts.noAlert] + * @throws {Error} Upon failure to load SVG + * @returns {void} + */ + loadSvgString (str, {noAlert} = {}) { + const success = this.svgCanvas.setSvgString(str) !== false; + if (success) return; + if (!noAlert) window.alert(this.uiStrings.notification.errorLoadingSVG); + throw new Error('Error loading SVG'); + } - editor.configObj.load(); + /** + * All methods are optional. + * @interface module:SVGthis.CustomHandler + * @type {PlainObject} + */ + /** + * Its responsibilities are: + * - invoke a file chooser dialog in 'open' mode + * - let user pick a SVG file + * - calls [svgCanvas.setSvgString()]{@link module:svgcanvas.SvgCanvas#setSvgString} with the string contents of that file. + * Not passed any parameters. + * @function module:SVGthis.CustomHandler#open + * @returns {void} + */ + /** + * Its responsibilities are: + * - accept the string contents of the current document + * - invoke a file chooser dialog in 'save' mode + * - save the file to location chosen by the user. + * @function module:SVGthis.CustomHandler#save + * @param {external:Window} win + * @param {module:svgcanvas.SvgCanvas#event:saved} svgStr A string of the SVG + * @listens module:svgcanvas.SvgCanvas#event:saved + * @returns {void} + */ + /** + * Its responsibilities (with regard to the object it is supplied in its 2nd argument) are: + * - inform user of any issues supplied via the "issues" property + * - convert the "svg" property SVG string into an image for export; + * utilize the properties "type" (currently 'PNG', 'JPEG', 'BMP', + * 'WEBP', 'PDF'), "mimeType", and "quality" (for 'JPEG' and 'WEBP' + * types) to determine the proper output. + * @function module:SVGthis.CustomHandler#exportImage + * @param {external:Window} win + * @param {module:svgcanvas.SvgCanvas#event:exported} data + * @listens module:svgcanvas.SvgCanvas#event:exported + * @returns {void} + */ + /** + * @function module:SVGthis.CustomHandler#exportPDF + * @param {external:Window} win + * @param {module:svgcanvas.SvgCanvas#event:exportedPDF} data + * @listens module:svgcanvas.SvgCanvas#event:exportedPDF + * @returns {void} + */ - // eslint-disable-next-line max-len - const goodLangs = ['ar', 'cs', 'de', 'en', 'es', 'fa', 'fr', 'fy', 'hi', 'it', 'ja', 'nl', 'pl', 'pt-BR', 'ro', 'ru', 'sk', 'sl', 'zh-CN', 'zh-TW']; + /** + * Allows one to override default SVGEdit `open`, `save`, and + * `export` editor behaviors. + * @function module:SVGthis.setCustomHandlers + * @param {module:SVGthis.CustomHandler} opts Extension mechanisms may call `setCustomHandlers` with three functions: `opts.open`, `opts.save`, and `opts.exportImage` + * @returns {Promise} + */ + + /** + * @param {PlainObject} opts + * @returns {Promise} + */ + setCustomHandlers (opts) { + return this.ready(() => { + if (opts.open) { + $('#tool_open > input[type="file"]').remove(); + $('#tool_open').show(); + this.svgCanvas.open = opts.open; + } + if (opts.save) { + this.showSaveWarning = false; + this.svgCanvas.bind('saved', opts.save); + } + if (opts.exportImage) { + this.customExportImage = opts.exportImage; + this.svgCanvas.bind('exported', this.customExportImage); // canvg and our RGBColor will be available to the method + } + if (opts.exportPDF) { + this.customExportPDF = opts.exportPDF; + this.svgCanvas.bind('exportedPDF', this.customExportPDF); // jsPDF and our RGBColor will be available to the method + } + }); + } + + /** + * @function module:SVGthis.randomizeIds + * @param {boolean} arg + * @returns {void} + */ + randomizeIds (arg) { + this.svgCanvas.randomizeIds(arg); + } + /** @lends module:SVGEditor~Actions */ + /** + * @returns {void} + */ + setAll () { + const keyHandler = {}; // will contain the action for each pressed key + + this.toolButtons.forEach((opts) => { + // Bind function to shortcut key + if (opts.key) { + // Set shortcut based on options + let keyval = opts.key; + let pd = false; + if (Array.isArray(opts.key)) { + keyval = opts.key[0]; + if (opts.key.length > 1) { pd = opts.key[1]; } + } + keyval = String(keyval); + const {fn} = opts; + keyval.split('/').forEach((key) => { keyHandler[key] = {fn, pd}; }); + } + return true; + }); + // register the keydown event + document.addEventListener('keydown', (e) => { + // only track keyboard shortcuts for the body containing the SVG-Editor + if (e.target.nodeName !== 'BODY') return; + // normalize key + const key = `${(e.metaKey) ? 'meta+' : ''}${(e.ctrlKey) ? 'ctrl+' : ''}${e.key.toLowerCase()}`; + // return if no shortcut defined for this key + if (!keyHandler[key]) return; + // launch associated handler and preventDefault if necessary + keyHandler[key].fn(); + if (keyHandler[key].pd) { + e.preventDefault(); + } + }); + + // Misc additional actions + + // Make 'return' keypress trigger the change event + $('.attr_changer, #image_url').bind( + 'keydown', + 'return', + function (evt) { + $(this).change(); + evt.preventDefault(); + } + ); + + $(window).bind('keydown', 'tab', function (e) { + if (this.uiContext === 'canvas') { + e.preventDefault(); + this.selectNext(); + } + }.bind(this)).bind('keydown', 'shift+tab', function (e) { + if (this.uiContext === 'canvas') { + e.preventDefault(); + this.selectPrev(); + } + }.bind(this)); + } + /** + * @returns {void} + */ + setTitles () { + // Tooltips not directly associated with a single function + const keyAssocs = { + '4/Shift+4': '#tools_rect', + '5/Shift+5': '#tools_ellipse' + }; + Object.entries(keyAssocs).forEach(([keyval, sel]) => { + const menu = ($(sel).parents('#main_menu').length); + + $qa(sel).forEach((element) => { + const t = (menu) ? $(element).text().split(' [')[0] : element.title.split(' [')[0]; + let keyStr = ''; + // Shift+Up + keyval.split('/').forEach((key, i) => { + const modBits = key.split('+'); + let mod = ''; + if (modBits.length > 1) { + mod = modBits[0] + '+'; + key = modBits[1]; + } + keyStr += (i ? '/' : '') + mod + (this.uiStrings['key_' + key] || key); + }); + if (menu) { + this.lastChild.textContent = t + ' [' + keyStr + ']'; + } else { + this.title = t + ' [' + keyStr + ']'; + } + }); + }); + } + /** + * @param {string} sel Selector to match + * @returns {module:SVGthis.ToolButton} + */ + getButtonData (sel) { + return Object.values(this.toolButtons).find((btn) => { + return btn.sel === sel; + }); + } /** * @fires module:svgcanvas.SvgCanvas#event:ext_addLangData * @fires module:svgcanvas.SvgCanvas#event:ext_langReady @@ -235,29 +349,29 @@ editor.init = () => { * @fires module:svgcanvas.SvgCanvas#event:extensions_added * @returns {Promise} Resolves to result of {@link module:locale.readLang} */ - const extAndLocaleFunc = async () => { - const {langParam, langData} = await editor.putLocale(editor.pref('lang'), goodLangs); - await setLang(langParam, langData); + async extAndLocaleFunc () { + const {langParam, langData} = await this.putLocale(this.pref('lang'), this.goodLangs); + await this.setLang(langParam, langData); $id('svg_container').style.visibility = 'visible'; try { // load standard extensions await Promise.all( - editor.configObj.curConfig.extensions.map(async (extname) => { + this.configObj.curConfig.extensions.map(async (extname) => { /** * @tutorial ExtensionDocs - * @typedef {PlainObject} module:SVGEditor.ExtensionObject + * @typedef {PlainObject} module:SVGthis.ExtensionObject * @property {string} [name] Name of the extension. Used internally; no need for i18n. Defaults to extension name without beginning "ext-" or ending ".js". * @property {module:svgcanvas.ExtensionInitCallback} [init] */ try { /** - * @type {module:SVGEditor.ExtensionObject} + * @type {module:SVGthis.ExtensionObject} */ const imported = await import(`./extensions/${encodeURIComponent(extname)}/${encodeURIComponent(extname)}.js`); const {name = extname, init} = imported.default; - return editor.addExtension(name, (init && init.bind(editor)), {$, langParam}); + return this.addExtension(name, (init && init.bind(this)), {$, langParam}); } catch (err) { // Todo: Add config to alert any errors console.error('Extension failed to load: ' + extname + '; ', err); // eslint-disable-line no-console @@ -267,20 +381,20 @@ editor.init = () => { ); // load user extensions (given as pathNames) await Promise.all( - editor.configObj.curConfig.userExtensions.map(async (extPathName) => { + this.configObj.curConfig.userExtensions.map(async (extPathName) => { /** * @tutorial ExtensionDocs - * @typedef {PlainObject} module:SVGEditor.ExtensionObject + * @typedef {PlainObject} module:SVGthis.ExtensionObject * @property {string} [name] Name of the extension. Used internally; no need for i18n. Defaults to extension name without beginning "ext-" or ending ".js". * @property {module:svgcanvas.ExtensionInitCallback} [init] */ try { /** - * @type {module:SVGEditor.ExtensionObject} + * @type {module:SVGthis.ExtensionObject} */ const imported = await import(encodeURI(extPathName)); const {name, init} = imported.default; - return editor.addExtension(name, (init && init.bind(editor)), {$, langParam}); + return this.addExtension(name, (init && init.bind(this)), {$, langParam}); } catch (err) { // Todo: Add config to alert any errors console.error('Extension failed to load: ' + extPathName + '; ', err); // eslint-disable-line no-console @@ -288,20 +402,20 @@ editor.init = () => { } }) ); - svgCanvas.bind( + this.svgCanvas.bind( 'extensions_added', /** * @param {external:Window} win * @param {module:svgcanvas.SvgCanvas#event:extensions_added} data - * @listens module:svgcanvas.SvgCanvas#event:extensions_added + * @listens module:SvgCanvas#event:extensions_added * @returns {void} */ (win, data) => { extensionsAdded = true; - Actions.setAll(); + this.setAll(); - if (editor.storagePromptState === 'ignore') { - updateCanvas(true); + if (this.storagePromptState === 'ignore') { + this.updateCanvas(true); } messageQueue.forEach( @@ -311,45 +425,22 @@ editor.init = () => { * @returns {void} */ (messageObj) => { - svgCanvas.call('message', messageObj); + this.svgCanvas.call('message', messageObj); } ); } ); - svgCanvas.call('extensions_added'); + this.svgCanvas.call('extensions_added'); } catch (err) { // Todo: Report errors through the UI console.log(err); // eslint-disable-line no-console } - }; - + } /** - * @name module:SVGEditor.canvas - * @type {module:svgcanvas.SvgCanvas} - */ - editor.canvas = svgCanvas = new SvgCanvas( - $id('svgcanvas'), - editor.configObj.curConfig - ); - - editor.leftPanelHandlers = new LeftPanelHandlers(editor); - editor.bottomPanelHandlers = new BottomPanelHandlers(editor); - editor.topPanelHandlers = new TopPanelHandlers(editor); - editor.layersPanel = new LayersPanel(editor); - - const modKey = (isMac() ? 'meta+' : 'ctrl+'); - const {undoMgr} = svgCanvas; - const workarea = $('#workarea'); - editor.workarea = workarea; - const canvMenu = document.getElementById('se-cmenu_canvas'); - let exportWindow = null; - let defaultImageURL = editor.configObj.curConfig.imgPath + 'logo.svg'; - const zoomInIcon = 'crosshair'; - const zoomOutIcon = 'crosshair'; - let uiContext = 'toolbars'; - - // For external openers - (function () { + * @fires module:svgcanvas.SvgCanvas#event:svgEditorReady + * @returns {void} + */ + static readySignal () { // let the opener know SVG Edit is ready (now that config is set up) const w = window.opener || window.parent; if (w) { @@ -363,7 +454,7 @@ editor.init = () => { * @property {true} cancelable */ /** - * @name module:SVGEditor.svgEditorReadyEvent + * @name module:SVGthis.svgEditorReadyEvent * @type {module:SVGEditor#event:svgEditorReadyEvent} */ const svgEditorReadyEvent = new w.CustomEvent('svgEditorReady', { @@ -373,41 +464,714 @@ editor.init = () => { w.document.documentElement.dispatchEvent(svgEditorReadyEvent); } catch (e) {} } - }()); - - // Make [1,2,5] array - const rIntervals = []; - for (let i = 0.1; i < 1e5; i *= 10) { - rIntervals.push(i); - rIntervals.push(2 * i); - rIntervals.push(5 * i); + } + /** + * Expose the `uiStrings`. + * @function module:SVGthis.canvas.getUIStrings + * @returns {module:SVGthis.uiStrings} + */ + getUIStrings () { + return this.uiStrings; } - editor.layersPanel.populateLayers(); - editor.selectedElement = null; - editor.multiselected = false; + /** + * Auto-run after a Promise microtask. + * @function module:SVGthis.init + * @returns {void} + */ + init () { + try { + if ('localStorage' in window) { // && onWeb removed so Webkit works locally + /** + * The built-in interface implemented by `localStorage` + * @external Storage + */ + /** + * @name storage + * @memberof module:SVGEditor + * @type {external:Storage} + */ + this.storage = this.localStorage; + } + // Image props dialog added to DOM + const newSeImgPropDialog = document.createElement('se-img-prop-dialog'); + newSeImgPropDialog.setAttribute('id', 'se-img-prop'); + document.body.append(newSeImgPropDialog); + // editor prefences dialoag added to DOM + const newSeEditPrefsDialog = document.createElement('se-edit-prefs-dialog'); + newSeEditPrefsDialog.setAttribute('id', 'se-edit-prefs'); + document.body.append(newSeEditPrefsDialog); + // canvas menu added to DOM + const dialogBox = document.createElement('se-cmenu_canvas-dialog'); + dialogBox.setAttribute('id', 'se-cmenu_canvas'); + document.body.append(dialogBox); + } catch (err) {} + + this.configObj.load(); + + /** + * @name module:SVGthis.canvas + * @type {module:svgcanvas.SvgCanvas} + */ + this.svgCanvas = new SvgCanvas( + $id('svgcanvas'), + this.configObj.curConfig + ); + + this.leftPanelHandlers = new LeftPanelHandlers(this); + this.bottomPanelHandlers = new BottomPanelHandlers(this); + this.topPanelHandlers = new TopPanelHandlers(this); + this.layersPanel = new LayersPanel(this); + + const {undoMgr} = this.svgCanvas; + this.workarea = $('#workarea'); + this.canvMenu = document.getElementById('se-cmenu_canvas'); + this.exportWindow = null; + this.defaultImageURL = this.configObj.curConfig.imgPath + 'logo.svg'; + const zoomInIcon = 'crosshair'; + const zoomOutIcon = 'crosshair'; + this.uiContext = 'toolbars'; + + // For external openers + Editor.readySignal(); + + this.rulers = new Rulers(this); + + this.layersPanel.populateLayers(); + this.selectedElement = null; + this.multiselected = false; + + $('#cur_context_panel').delegate('a', 'click', (evt) => { + const link = $(evt.currentTarget); + if (link.attr('data-root')) { + this.svgCanvas.leaveContext(); + } else { + this.svgCanvas.setContext(link.text()); + } + this.svgCanvas.clearSelection(); + return false; + }); + // bind the selected event to our function that handles updates to the UI + this.svgCanvas.bind('selected', this.selectedChanged.bind(this)); + this.svgCanvas.bind('transition', this.elementTransition.bind(this)); + this.svgCanvas.bind('changed', this.elementChanged.bind(this)); + this.svgCanvas.bind('saved', this.saveHandler.bind(this)); + this.svgCanvas.bind('exported', this.exportHandler.bind(this)); + this.svgCanvas.bind('exportedPDF', function (win, data) { + if (!data.output) { // Ignore Chrome + return; + } + const {exportWindowName} = data; + if (exportWindowName) { + this.exportWindow = window.open('', this.exportWindowName); // A hack to get the window via JSON-able name without opening a new one + } + if (!this.exportWindow || this.exportWindow.closed) { + alert(this.uiStrings.notification.popupWindowBlocked); + return; + } + this.exportWindow.location.href = data.output; + }.bind(this)); + this.svgCanvas.bind('zoomed', this.zoomChanged.bind(this)); + this.svgCanvas.bind('zoomDone', this.zoomDone.bind(this)); + this.svgCanvas.bind( + 'updateCanvas', + /** + * @param {external:Window} win + * @param {PlainObject} centerInfo + * @param {false} centerInfo.center + * @param {module:math.XYObject} centerInfo.newCtr + * @listens module:svgcanvas.SvgCanvas#event:updateCanvas + * @returns {void} + */ + function (win, {center, newCtr}) { + this.updateCanvas(center, newCtr); + }.bind(this) + ); + this.svgCanvas.bind('contextset', this.contextChanged.bind(this)); + this.svgCanvas.bind('extension_added', this.extAdded.bind(this)); + this.svgCanvas.textActions.setInputElem($('#text')[0]); + + this.setBackground(this.pref('bkgd_color'), this.pref('bkgd_url')); + + // update resolution option with actual resolution + const res = this.svgCanvas.getResolution(); + if (this.configObj.curConfig.baseUnit !== 'px') { + res.w = convertUnit(res.w) + this.configObj.curConfig.baseUnit; + res.h = convertUnit(res.h) + this.configObj.curConfig.baseUnit; + } + $('#se-img-prop').attr('dialog', 'close'); + $('#se-img-prop').attr('title', this.svgCanvas.getDocumentTitle()); + $('#se-img-prop').attr('width', res.w); + $('#se-img-prop').attr('height', res.h); + $('#se-img-prop').attr('save', this.pref('img_save')); + + // Lose focus for select elements when changed (Allows keyboard shortcuts to work better) + $('select').change((evt) => { $(evt.currentTarget).blur(); }); + + // fired when user wants to move elements to another layer + let promptMoveLayerOnce = false; + $('#selLayerNames').change((evt) => { + const destLayer = evt.currentTarget.options[evt.currentTarget.selectedIndex].value; + const confirmStr = this.uiStrings.notification.Qmovethis.elemsToLayer.replace('%s', destLayer); + /** + * @param {boolean} ok + * @returns {void} + */ + const moveToLayer = (ok) => { + if (!ok) { return; } + promptMoveLayerOnce = true; + this.svgCanvas.moveSelectedToLayer(destLayer); + this.svgCanvas.clearSelection(); + this.layersPanel.populateLayers(); + }; + if (destLayer) { + if (promptMoveLayerOnce) { + moveToLayer(true); + } else { + const ok = confirm(confirmStr); + if (!ok) { + return; + } + moveToLayer(true); + } + } + }); + + $('#font_family').change((evt) => { + this.svgCanvas.setFontFamily(evt.currentTarget.value); + }); + + $('#seg_type').change((evt) => { + this.svgCanvas.setSegType($(evt.currentTarget).val()); + }); + + $('#text').bind('keyup input', (evt) => { + this.svgCanvas.setTextContent(evt.currentTarget.value); + }); + + $('#image_url').change((evt) => { + this.setImageURL(evt.currentTarget.value); + }); + + $('#link_url').change((evt) => { + if (evt.currentTarget.value.length) { + this.svgCanvas.setLinkURL(evt.currentTarget.value); + } else { + this.svgCanvas.removeHyperlink(); + } + }); + + $('#g_title').change((evt) => { + this.svgCanvas.setGroupTitle(evt.currentTarget.value); + }); + + const wArea = this.workarea[0]; + + let lastX = null, lastY = null, + panning = false, keypan = false; + + $('#svgcanvas').bind('mousemove mouseup', function (evt) { + if (panning === false) { return true; } + + wArea.scrollLeft -= (evt.clientX - lastX); + wArea.scrollTop -= (evt.clientY - lastY); + + lastX = evt.clientX; + lastY = evt.clientY; + + if (evt.type === 'mouseup') { panning = false; } + return false; + }).mousedown(function (evt) { + if (evt.button === 1 || keypan === true) { + panning = true; + lastX = evt.clientX; + lastY = evt.clientY; + return false; + } + return true; + }); + + $(window).mouseup(() => { + panning = false; + }); + + $(document).bind('keydown', 'space', function (evt) { + this.svgCanvas.spaceKey = keypan = true; + evt.preventDefault(); + }.bind(this)).bind('keyup', 'space', function (evt) { + evt.preventDefault(); + this.svgCanvas.spaceKey = keypan = false; + }.bind(this)).bind('keydown', 'shift', function (evt) { + if (this.svgCanvas.getMode() === 'zoom') { + this.workarea.css('cursor', zoomOutIcon); + } + }.bind(this)).bind('keyup', 'shift', function (evt) { + if (this.svgCanvas.getMode() === 'zoom') { + this.workarea.css('cursor', zoomInIcon); + } + }.bind(this)); + + /** + * @function module:SVGthis.setPanning + * @param {boolean} active + * @returns {void} + */ + this.setPanning = (active) => { + this.svgCanvas.spaceKey = keypan = active; + }; + + const button = $('#main_icon'); + const overlay = $('#main_icon span'); + const list = $('#main_menu'); + + let onButton = false; + let height = 0; + let jsHover = true; + let setClick = false; + + $(window).mouseup(function (evt) { + if (!onButton) { + button.removeClass('buttondown'); + // do not hide if it was the file input as that input needs to be visible + // for its change event to fire + if (evt.target.tagName !== 'INPUT') { + list.fadeOut(200); + } else if (!setClick) { + setClick = true; + $(evt.target).click(() => { + list.css('margin-left', '-9999px').show(); + }); + } + } + onButton = false; + }).mousedown(function (evt) { + // $('.contextMenu').hide(); + const islib = $(evt.target).closest('.contextMenu').length; + if (!islib) { + $('.contextMenu').fadeOut(250); + } + }); + + overlay.bind('mousedown', () => { + if (!button.hasClass('buttondown')) { + // Margin must be reset in case it was changed before; + list.css('margin-left', 0).show(); + if (!height) { + height = list.height(); + } + // Using custom animation as slideDown has annoying 'bounce effect' + list.css('height', 0).animate({ + height + }, 200); + onButton = true; + } else { + list.fadeOut(200); + } + button.toggleClass('buttondown buttonup'); + }).hover(() => { + onButton = true; + }).mouseout(() => { + onButton = false; + }); + + const listItems = $('#main_menu li'); + + // Check if JS method of hovering needs to be used (Webkit bug) + listItems.mouseover(function () { + jsHover = ($(this).css('background-color') === 'rgba(0, 0, 0, 0)'); + + listItems.unbind('mouseover'); + if (jsHover) { + listItems.mouseover(() => { + this.style.backgroundColor = '#FFC'; + }).mouseout((evt) => { + evt.currentTarget.style.backgroundColor = 'transparent'; + return true; + }); + } + }); + // Unfocus text input when this.workarea is mousedowned. + let inp; + /** + * + * @returns {void} + */ + const unfocus = () => { + $(inp).blur(); + }; + + $('#svg_editor').find('button, select, input:not(#text)').focus(() => { + inp = this; + this.uiContext = 'toolbars'; + this.workarea.mousedown(unfocus); + }).blur(() => { + this.uiContext = 'canvas'; + this.workarea.unbind('mousedown', unfocus); + // Go back to selecting text if in textedit mode + if (this.svgCanvas.getMode() === 'textedit') { + $('#text').focus(); + } + }); + const winWh = {width: $(window).width(), height: $(window).height()}; + + $(window).resize(function (evt) { + $.each(winWh, function (type, val) { + const curval = $(window)[type](); + this.workarea[0]['scroll' + (type === 'width' ? 'Left' : 'Top')] -= (curval - val) / 2; + winWh[type] = curval; + }); + }); + + this.workarea.scroll(() => { + // TODO: jQuery's scrollLeft/Top() wouldn't require a null check + this.rulers.manageScroll(); + }); + + $('#url_notice').click(() => { + alert(this.title); + }); + + $('#stroke_width').val(this.configObj.curConfig.initStroke.width); + $('#group_opacity').val(this.configObj.curConfig.initOpacity * 100); + + $('#group_opacityLabel').click(() => { + $('#opacity_dropdown button').mousedown(); + $(window).mouseup(); + }); + + $('.push_button').mousedown(() => { + if (!$(this).hasClass('disabled')) { + $(this).addClass('push_button_pressed').removeClass('push_button'); + } + }).mouseout(() => { + $(this).removeClass('push_button_pressed').addClass('push_button'); + }).mouseup(() => { + $(this).removeClass('push_button_pressed').addClass('push_button'); + }); + + this.layersPanel.populateLayers(); + + const centerCanvas = () => { + // this centers the canvas vertically in the this.workarea (horizontal handled in CSS) + this.workarea.css('line-height', this.workarea.height() + 'px'); + }; + + $(window).bind('load resize', centerCanvas); + + // Prevent browser from erroneously repopulating fields + $('input,select').attr('autocomplete', 'off'); + + /** + * Associate all button actions as well as non-button keyboard shortcuts. + */ + this.leftPanelHandlers.init(); + this.bottomPanelHandlers.init(); + this.topPanelHandlers.init(); + this.layersPanel.init(); + + $id('tool_clear').addEventListener('click', this.clickClear.bind(this)); + $id('tool_open').addEventListener('click', function (e) { + this.clickOpen(); + window.dispatchEvent(new CustomEvent('openImage')); + }.bind(this)); + $id('tool_import').addEventListener('click', (e) => { + this.clickImport(); + window.dispatchEvent(new CustomEvent('importImage')); + }); + $id('tool_save').addEventListener('click', function (e) { + const $editorDialog = document.getElementById('se-svg-editor-dialog'); + const editingsource = $editorDialog.getAttribute('dialog') === 'open'; + if (editingsource) { + this.saveSourceEditor(); + } else { + this.clickSave(); + } + }.bind(this)); + $id('tool_export').addEventListener('click', this.clickExport.bind(this)); + $id('tool_docprops').addEventListener('click', this.showDocProperties.bind(this)); + $id('tool_editor_prefs').addEventListener('click', this.showPreferences.bind(this)); + $id('tool_editor_homepage').addEventListener('click', this.openHomePage.bind(this)); + $id('se-img-prop').addEventListener('change', function (e) { + if (e.detail.dialog === 'closed') { + this.hideDocProperties(); + } else { + this.saveDocProperties(e); + } + }.bind(this)); + $id('se-edit-prefs').addEventListener('change', function (e) { + if (e.detail.dialog === 'closed') { + this.hidePreferences(); + } else { + this.savePreferences(e); + } + }.bind(this)); + $id('se-svg-editor-dialog').addEventListener('change', function (e) { + if (e?.detail?.copy === 'click') { + this.cancelOverlays(e); + } else if (e?.detail?.dialog === 'closed') { + this.hideSourceEditor(); + } else { + this.saveSourceEditor(e); + } + }.bind(this)); + $id('se-cmenu_canvas').addEventListener('change', function (e) { + const action = e?.detail?.trigger; + switch (action) { + case 'delete': + this.svgCanvas.deleteSelectedElements(); + break; + case 'cut': + this.cutSelected(); + break; + case 'copy': + this.copySelected(); + break; + case 'paste': + this.svgCanvas.pasteElements(); + break; + case 'paste_in_place': + this.svgCanvas.pasteElements('in_place'); + break; + case 'group': + case 'group_elements': + this.svgCanvas.groupSelectedElements(); + break; + case 'ungroup': + this.svgCanvas.ungroupSelectedElement(); + break; + case 'move_front': + this.svgCanvas.moveToTopSelectedElement(); + break; + case 'move_up': + this.moveUpDownSelected('Up'); + break; + case 'move_down': + this.moveUpDownSelected('Down'); + break; + case 'move_back': + this.svgCanvas.moveToBottomSelected(); + break; + default: + if (hasCustomHandler(action)) { + getCustomHandler(action).call(); + } + break; + } + }.bind(this)); + + // Select given tool + this.ready(function () { + const preTool = $id(`tool_${this.configObj.curConfig.initTool}`); + const regTool = $id(this.configObj.curConfig.initTool); + const selectTool = $id('tool_select'); + const $editDialog = $id('se-edit-prefs'); + + if (preTool) { + preTool.click(); + } else if (regTool) { + regTool.click(); + } else { + selectTool.click(); + } + + if (this.configObj.curConfig.wireframe) { + $id('tool_wireframe').click(); + } + + $('#rulers').toggle(Boolean(this.configObj.curConfig.showRulers)); + + if (this.configObj.curConfig.showRulers) { + $editDialog.setAttribute('showrulers', true); + } + + if (this.configObj.curConfig.baseUnit) { + $editDialog.setAttribute('baseunit', this.configObj.curConfig.baseUnit); + } + + if (this.configObj.curConfig.gridSnapping) { + $editDialog.setAttribute('gridsnappingon', true); + } + + if (this.configObj.curConfig.snappingStep) { + $editDialog.setAttribute('gridsnappingstep', this.configObj.curConfig.snappingStep); + } + + if (this.configObj.curConfig.gridColor) { + $editDialog.setAttribute('gridcolor', this.configObj.curConfig.gridColor); + } + }.bind(this)); + + // zoom + $id('zoom').value = (this.svgCanvas.getZoom() * 100).toFixed(1); + this.canvMenu.setAttribute('disableallmenu', true); + this.canvMenu.setAttribute('enablemenuitems', '#delete,#cut,#copy'); + + this.enableOrDisableClipboard(); + + window.addEventListener('storage', function (e) { + if (e.key !== 'svgedit_clipboard') { return; } + + this.enableOrDisableClipboard(); + }.bind(this)); + + window.addEventListener('beforeunload', function (e) { + // Suppress warning if page is empty + if (undoMgr.getUndoStackSize() === 0) { + this.showSaveWarning = false; + } + + // showSaveWarning is set to 'false' when the page is saved. + if (!this.configObj.curConfig.no_save_warning && this.showSaveWarning) { + // Browser already asks question about closing the page + e.returnValue = this.uiStrings.notification.unsavedChanges; // Firefox needs this when beforeunload set by addEventListener (even though message is not used) + return this.uiStrings.notification.unsavedChanges; + } + return true; + }.bind(this)); + + // Use HTML5 File API: http://www.w3.org/TR/FileAPI/ + // if browser has HTML5 File API support, then we will show the open menu item + // and provide a file input to click. When that change event fires, it will + // get the text contents of the file and send it to the canvas + if (window.FileReader) { + /** + * @param {Event} e + * @returns {void} + */ + const importImage = function (e) { + $.process_cancel(this.uiStrings.notification.loadingImage); + e.stopPropagation(); + e.preventDefault(); + $('#main_menu').hide(); + const file = (e.type === 'drop') ? e.dataTransfer.files[0] : this.files[0]; + if (!file) { + $('#dialog_box').hide(); + return; + } + + if (!file.type.includes('image')) { + return; + } + // Detected an image + // svg handling + let reader; + if (file.type.includes('svg')) { + reader = new FileReader(); + reader.onloadend = function (ev) { + const newElement = this.svgCanvas.importSvgString(ev.target.result, true); + this.svgCanvas.ungroupSelectedElement(); + this.svgCanvas.ungroupSelectedElement(); + this.svgCanvas.groupSelectedElements(); + this.svgCanvas.alignSelectedElements('m', 'page'); + this.svgCanvas.alignSelectedElements('c', 'page'); + // highlight imported element, otherwise we get strange empty selectbox + this.svgCanvas.selectOnly([newElement]); + $('#dialog_box').hide(); + }; + reader.readAsText(file); + } else { + // bitmap handling + reader = new FileReader(); + reader.onloadend = function ({target: {result}}) { + /** + * Insert the new image until we know its dimensions. + * @param {Float} width + * @param {Float} height + * @returns {void} + */ + const insertNewImage = function (width, height) { + const newImage = this.svgCanvas.addSVGElementFromJson({ + element: 'image', + attr: { + x: 0, + y: 0, + width, + height, + id: this.svgCanvas.getNextId(), + style: 'pointer-events:inherit' + } + }); + this.svgCanvas.setHref(newImage, result); + this.svgCanvas.selectOnly([newImage]); + this.svgCanvas.alignSelectedElements('m', 'page'); + this.svgCanvas.alignSelectedElements('c', 'page'); + this.topPanelHandlers.updateContextPanel(); + $('#dialog_box').hide(); + }; + // create dummy img so we know the default dimensions + let imgWidth = 100; + let imgHeight = 100; + const img = new Image(); + img.style.opacity = 0; + img.addEventListener('load', () => { + imgWidth = img.offsetWidth || img.naturalWidth || img.width; + imgHeight = img.offsetHeight || img.naturalHeight || img.height; + insertNewImage(imgWidth, imgHeight); + }); + img.src = result; + }; + reader.readAsDataURL(file); + } + }; + + this.workarea[0].addEventListener('dragenter', this.onDragEnter); + this.workarea[0].addEventListener('dragover', this.onDragOver); + this.workarea[0].addEventListener('dragleave', this.onDragLeave); + this.workarea[0].addEventListener('drop', importImage); + + const open = $('').change(async function (e) { + const ok = await this.openPrep(); + if (!ok) { return; } + this.svgCanvas.clear(); + if (this.files.length === 1) { + $.process_cancel(this.uiStrings.notification.loadingImage); + const reader = new FileReader(); + reader.onloadend = async function ({target}) { + await this.loadSvgString(target.result); + this.updateCanvas(); + }; + reader.readAsText(this.files[0]); + } + }); + $('#tool_open').show(); + $(window).on('openImage', () => open.click()); + + const imgImport = $('').change(importImage); + $('#tool_import').show(); + $(window).on('importImage', () => imgImport.click()); + } + + this.updateCanvas(true); + // Load extensions + this.extAndLocaleFunc(); + // Defer injection to wait out initial menu processing. This probably goes + // away once all context menu behavior is brought to context menu. + this.ready(() => { + injectExtendedContextMenuItemsIntoDom(); + }); + } /** * @param {boolean} editmode * @param {module:svgcanvas.SvgCanvas#event:selected} elems * @returns {void} */ - const togglePathEditMode = function (editmode, elems) { + togglePathEditMode (editmode, elems) { $('#path_node_panel').toggle(editmode); if (editmode) { // Change select icon $('.tool_button_current').removeClass('tool_button_current').addClass('tool_button'); $('#tool_select').addClass('tool_button_current').removeClass('tool_button'); - editor.multiselected = false; + this.multiselected = false; if (elems.length) { - editor.selectedElement = elems[0]; + this.selectedElement = this.elems[0]; } } else { setTimeout(() => { // setIcon('#tool_select', 'select'); }, 1000); } - }; + } /** * @type {module:svgcanvas.EventHandler} @@ -416,8 +1180,8 @@ editor.init = () => { * @listens module:svgcanvas.SvgCanvas#event:saved * @returns {void} */ - const saveHandler = function (wind, svg) { - editor.showSaveWarning = false; + saveHandler (wind, svg) { + this.showSaveWarning = false; // by default, we add the XML prolog back, systems integrating SVG-edit (wikis, CMSs) // can just provide their own custom save handler and might not want the XML prolog @@ -435,28 +1199,28 @@ editor.init = () => { // Alert will only appear the first time saved OR the // first time the bug is encountered - let done = editor.pref('save_notice_done'); + let done = this.pref('save_notice_done'); if (done !== 'all') { - let note = uiStrings.notification.saveFromBrowser.replace('%s', 'SVG'); + let note = this.uiStrings.notification.saveFromBrowser.replace('%s', 'SVG'); // Check if FF and has if (isGecko()) { if (svg.includes(' { * @listens module:svgcanvas.SvgCanvas#event:exported * @returns {void} */ - const exportHandler = function (win, data) { + exportHandler (win, data) { const {issues, exportWindowName} = data; - exportWindow = window.open(blankPageObjectURL || '', exportWindowName); // A hack to get the window via JSON-able name without opening a new one + this.exportWindow = window.open(blankPageObjectURL || '', exportWindowName); // A hack to get the window via JSON-able name without opening a new one - if (!exportWindow || exportWindow.closed) { - alert(uiStrings.notification.popupWindowBlocked); + if (!this.exportWindow || this.exportWindow.closed) { + alert(this.uiStrings.notification.popupWindowBlocked); return; } - exportWindow.location.href = data.bloburl || data.datauri; - const done = editor.pref('export_notice_done'); + this.exportWindow.location.href = data.bloburl || data.datauri; + const done = this.pref('export_notice_done'); if (done !== 'all') { - let note = uiStrings.notification.saveFromBrowser.replace('%s', data.type); + let note = this.uiStrings.notification.saveFromBrowser.replace('%s', data.type); // Check if there are issues if (issues.length) { const pre = '\n \u2022 '; - note += ('\n\n' + uiStrings.notification.noteTheseIssues + pre + issues.join(pre)); + note += ('\n\n' + this.uiStrings.notification.noteTheseIssues + pre + issues.join(pre)); } // Note that this will also prevent the notice even though new issues may appear later. // May want to find a way to deal with that without annoying the user - editor.pref('export_notice_done', 'all'); - exportWindow.alert(note); + this.pref('export_notice_done', 'all'); + this.exportWindow.alert(note); } - }; + } /** * @@ -498,28 +1262,28 @@ editor.init = () => { * @param {boolean} changeElem * @returns {void} */ - function setStrokeOpt (opt, changeElem) { + setStrokeOpt (opt, changeElem) { const {id} = opt; const bits = id.split('_'); const [pre, val] = bits; if (changeElem) { - svgCanvas.setStrokeAttr('stroke-' + pre, val); + this.svgCanvas.setStrokeAttr('stroke-' + pre, val); } $(opt).addClass('current').siblings().removeClass('current'); } /** * Set a selected image's URL. - * @function module:SVGEditor.setImageURL + * @function module:SVGthis.setImageURL * @param {string} url * @returns {void} */ - const setImageURL = editor.setImageURL = function (url) { + setImageURL (url) { if (!url) { - url = defaultImageURL; + url = this.defaultImageURL; } - svgCanvas.setImageURL(url); + this.svgCanvas.setImageURL(url); $('#image_url').val(url); if (url.startsWith('data:')) { @@ -528,15 +1292,15 @@ editor.init = () => { $('#change_image_url').show(); } else { // regular URL - svgCanvas.embedImage(url, function (dataURI) { + this.svgCanvas.embedImage(url, function (dataURI) { // Couldn't embed, so show warning $('#url_notice').toggle(!dataURI); - defaultImageURL = url; + this.defaultImageURL = url; }); $('#image_url').show(); $('#change_image_url').hide(); } - }; + } /** * @@ -544,197 +1308,40 @@ editor.init = () => { * @param {string} url * @returns {void} */ - function setBackground (color, url) { - // if (color == editor.pref('bkgd_color') && url == editor.pref('bkgd_url')) { return; } - editor.pref('bkgd_color', color); - editor.pref('bkgd_url', url, true); + setBackground (color, url) { + // if (color == this.pref('bkgd_color') && url == this.pref('bkgd_url')) { return; } + this.pref('bkgd_color', color); + this.pref('bkgd_url', url, true); - // This should be done in svgcanvas.js for the borderRect fill - svgCanvas.setBackground(color, url); + // This should be done in this.svgCanvas.js for the borderRect fill + this.svgCanvas.setBackground(color, url); } /** - * - * @param {HTMLDivElement} [scanvas] - * @param {Float} [zoom] - * @returns {void} - */ - function updateRulers (scanvas, zoom) { - if (!zoom) { zoom = svgCanvas.getZoom(); } - if (!scanvas) { scanvas = $('#svgcanvas'); } - - let d, i; - const limit = 30000; - const contentElem = svgCanvas.getContentElem(); - const units = getTypeMap(); - const unit = units[editor.configObj.curConfig.baseUnit]; // 1 = 1px - - // draw x ruler then y ruler - for (d = 0; d < 2; d++) { - const isX = (d === 0); - const dim = isX ? 'x' : 'y'; - const lentype = isX ? 'width' : 'height'; - const contentDim = Number(contentElem.getAttribute(dim)); - - const $hcanvOrig = $('#ruler_' + dim + ' canvas:first'); - - // Bit of a hack to fully clear the canvas in Safari & IE9 - const $hcanv = $hcanvOrig.clone(); - $hcanvOrig.replaceWith($hcanv); - - const hcanv = $hcanv[0]; - - // Set the canvas size to the width of the container - let rulerLen = scanvas[lentype](); - const totalLen = rulerLen; - hcanv.parentNode.style[lentype] = totalLen + 'px'; - let ctx = hcanv.getContext('2d'); - let ctxArr, num, ctxArrNum; - - ctx.fillStyle = 'rgb(200,0,0)'; - ctx.fillRect(0, 0, hcanv.width, hcanv.height); - - // Remove any existing canvasses - $hcanv.siblings().remove(); - - // Create multiple canvases when necessary (due to browser limits) - if (rulerLen >= limit) { - ctxArrNum = Number.parseInt(rulerLen / limit) + 1; - ctxArr = []; - ctxArr[0] = ctx; - let copy; - for (i = 1; i < ctxArrNum; i++) { - hcanv[lentype] = limit; - copy = hcanv.cloneNode(true); - hcanv.parentNode.append(copy); - ctxArr[i] = copy.getContext('2d'); - } - - copy[lentype] = rulerLen % limit; - - // set copy width to last - rulerLen = limit; - } - - hcanv[lentype] = rulerLen; - - const uMulti = unit * zoom; - - // Calculate the main number interval - const rawM = 50 / uMulti; - let multi = 1; - for (i = 0; i < rIntervals.length; i++) { - num = rIntervals[i]; - multi = num; - if (rawM <= num) { - break; - } - } - - const bigInt = multi * uMulti; - - ctx.font = '9px sans-serif'; - - let rulerD = ((contentDim / uMulti) % multi) * uMulti; - let labelPos = rulerD - bigInt; - // draw big intervals - let ctxNum = 0; - while (rulerD < totalLen) { - labelPos += bigInt; - // const realD = rulerD - contentDim; // Currently unused - - const curD = Math.round(rulerD) + 0.5; - if (isX) { - ctx.moveTo(curD, 15); - ctx.lineTo(curD, 0); - } else { - ctx.moveTo(15, curD); - ctx.lineTo(0, curD); - } - - num = (labelPos - contentDim) / uMulti; - let label; - if (multi >= 1) { - label = Math.round(num); - } else { - const decs = String(multi).split('.')[1].length; - label = num.toFixed(decs); - } - - // Change 1000s to Ks - if (label !== 0 && label !== 1000 && label % 1000 === 0) { - label = (label / 1000) + 'K'; - } - - if (isX) { - ctx.fillText(label, rulerD + 2, 8); - } else { - // draw label vertically - const str = String(label).split(''); - for (i = 0; i < str.length; i++) { - ctx.fillText(str[i], 1, (rulerD + 9) + i * 9); - } - } - - const part = bigInt / 10; - // draw the small intervals - for (i = 1; i < 10; i++) { - let subD = Math.round(rulerD + part * i) + 0.5; - if (ctxArr && subD > rulerLen) { - ctxNum++; - ctx.stroke(); - if (ctxNum >= ctxArrNum) { - i = 10; - rulerD = totalLen; - continue; - } - ctx = ctxArr[ctxNum]; - rulerD -= limit; - subD = Math.round(rulerD + part * i) + 0.5; - } - - // odd lines are slighly longer - const lineNum = (i % 2) ? 12 : 10; - if (isX) { - ctx.moveTo(subD, 15); - ctx.lineTo(subD, lineNum); - } else { - ctx.moveTo(15, subD); - ctx.lineTo(lineNum, subD); - } - } - rulerD += bigInt; - } - ctx.strokeStyle = '#000'; - ctx.stroke(); - } - } - - /** - * @function module:SVGEditor.updateCanvas + * @function module:SVGthis.updateCanvas * @param {boolean} center * @param {module:math.XYObject} newCtr * @returns {void} */ - const updateCanvas = editor.updateCanvas = function (center, newCtr) { - const zoom = svgCanvas.getZoom(); - const wArea = workarea; + updateCanvas (center, newCtr) { + const zoom = this.svgCanvas.getZoom(); + const wArea = this.workarea; const cnvs = $('#svgcanvas'); - let w = workarea.width(), h = workarea.height(); + let w = this.workarea.width(), h = this.workarea.height(); const wOrig = w, hOrig = h; const oldCtr = { x: wArea[0].scrollLeft + wOrig / 2, y: wArea[0].scrollTop + hOrig / 2 }; - const multi = editor.configObj.curConfig.canvas_expansion; - w = Math.max(wOrig, svgCanvas.contentW * zoom * multi); - h = Math.max(hOrig, svgCanvas.contentH * zoom * multi); + const multi = this.configObj.curConfig.canvas_expansion; + w = Math.max(wOrig, this.svgCanvas.contentW * zoom * multi); + h = Math.max(hOrig, this.svgCanvas.contentH * zoom * multi); if (w === wOrig && h === hOrig) { - workarea.css('overflow', 'hidden'); + this.workarea.css('overflow', 'hidden'); } else { - workarea.css('overflow', 'scroll'); + this.workarea.css('overflow', 'scroll'); } const oldCanY = cnvs.height() / 2; @@ -742,7 +1349,7 @@ editor.init = () => { cnvs.width(w).height(h); const newCanY = h / 2; const newCanX = w / 2; - const offset = svgCanvas.updateCanvas(w, h); + const offset = this.svgCanvas.updateCanvas(w, h); const ratio = newCanX / oldCanX; @@ -767,10 +1374,10 @@ editor.init = () => { if (center) { // Go to top-left for larger documents - if (svgCanvas.contentW > wArea.width()) { + if (this.svgCanvas.contentW > wArea.width()) { // Top-left - workarea[0].scrollLeft = offset.x - 10; - workarea[0].scrollTop = offset.y - 10; + this.workarea[0].scrollLeft = offset.x - 10; + this.workarea[0].scrollTop = offset.y - 10; } else { // Center wArea[0].scrollLeft = scrollX; @@ -780,15 +1387,15 @@ editor.init = () => { wArea[0].scrollLeft = newCtr.x - wOrig / 2; wArea[0].scrollTop = newCtr.y - hOrig / 2; } - if (editor.configObj.curConfig.showRulers) { - updateRulers(cnvs, zoom); - workarea.scroll(); + if (this.configObj.curConfig.showRulers) { + this.rulers.updateRulers(cnvs, zoom); + this.workarea.scroll(); } - if (editor.configObj.urldata.storagePrompt !== true && editor.storagePromptState === 'ignore') { + if (this.configObj.urldata.storagePrompt !== true && this.storagePromptState === 'ignore') { $('#dialog_box').hide(); } - }; + } /** * Updates the toolbar (colors, opacity, etc) based on the selected element. @@ -796,10 +1403,10 @@ editor.init = () => { * context panel. * @returns {void} */ - const updateToolbar = () => { + updateToolbar () { let i, len; - if (!isNullish(editor.selectedElement)) { - switch (editor.selectedElement.tagName) { + if (!isNullish(this.selectedElement)) { + switch (this.selectedElement.tagName) { case 'use': case 'image': case 'foreignObject': @@ -807,7 +1414,7 @@ editor.init = () => { case 'g': case 'a': { // Look for common styles - const childs = editor.selectedElement.getElementsByTagName('*'); + const childs = this.selectedElement.getElementsByTagName('*'); let gWidth = null; for (i = 0, len = childs.length; i < len; i++) { const swidth = childs[i].getAttribute('stroke-width'); @@ -820,71 +1427,69 @@ editor.init = () => { } $('#stroke_width').val(gWidth === null ? '' : gWidth); - editor.bottomPanelHandlers.updateColorpickers(true); + this.bottomPanelHandlers.updateColorpickers(true); break; } default: { - editor.bottomPanelHandlers.updateColorpickers(true); + this.bottomPanelHandlers.updateColorpickers(true); - $('#stroke_width').val(editor.selectedElement.getAttribute('stroke-width') || 1); - $('#stroke_style').val(editor.selectedElement.getAttribute('stroke-dasharray') || 'none'); + $('#stroke_width').val(this.selectedElement.getAttribute('stroke-width') || 1); + $('#stroke_style').val(this.selectedElement.getAttribute('stroke-dasharray') || 'none'); - let attr = editor.selectedElement.getAttribute('stroke-linejoin') || 'miter'; + let attr = this.selectedElement.getAttribute('stroke-linejoin') || 'miter'; if ($('#linejoin_' + attr).length) { - setStrokeOpt($('#linejoin_' + attr)[0]); + this.setStrokeOpt($('#linejoin_' + attr)[0]); } - attr = editor.selectedElement.getAttribute('stroke-linecap') || 'butt'; + attr = this.selectedElement.getAttribute('stroke-linecap') || 'butt'; if ($('#linecap_' + attr).length) { - setStrokeOpt($('#linecap_' + attr)[0]); + this.setStrokeOpt($('#linecap_' + attr)[0]); } } } } // All elements including image and group have opacity - if (!isNullish(editor.selectedElement)) { - const opacPerc = (editor.selectedElement.getAttribute('opacity') || 1.0) * 100; + if (!isNullish(this.selectedElement)) { + const opacPerc = (this.selectedElement.getAttribute('opacity') || 1.0) * 100; $('#group_opacity').val(opacPerc); $('#opac_slider').slider('option', 'value', opacPerc); - $id('elem_id').value = editor.selectedElement.id; + $id('elem_id').value = this.selectedElement.id; $id('elem_class').value = - (editor.selectedElement.getAttribute('class') !== null) ? editor.selectedElement.getAttribute('class') : ''; + (this.selectedElement.getAttribute('class') !== null) ? this.selectedElement.getAttribute('class') : ''; } - editor.bottomPanelHandlers.updateToolButtonState(); - }; + this.bottomPanelHandlers.updateToolButtonState(); + } /** * * @returns {void} */ - const updateWireFrame = () => { + updateWireFrame () { const rule = ` #workarea.wireframe #svgcontent * { - stroke-width: ${1 / svgCanvas.getZoom()}px; + stroke-width: ${1 / this.svgCanvas.getZoom()}px; } `; - $('#wireframe_rules').text(workarea.hasClass('wireframe') ? rule : ''); - }; - - let curContext = ''; + $('#wireframe_rules').text(this.workarea.hasClass('wireframe') ? rule : ''); + } /** * @param {string} [title=svgCanvas.getDocumentTitle()] * @returns {void} */ - const updateTitle = function (title) { - title = title || svgCanvas.getDocumentTitle(); + updateTitle (title) { + title = title || this.svgCanvas.getDocumentTitle(); const newTitle = document.querySelector('title').text + (title ? ': ' + title : ''); // Remove title update with current context info, isn't really necessary - // if (curContext) { - // new_title = new_title + curContext; + // if (this.curContext) { + // new_title = new_title + this.curContext; // } $('title:first').text(newTitle); - }; + } // called when we've selected a different element /** @@ -895,28 +1500,28 @@ editor.init = () => { * @fires module:svgcanvas.SvgCanvas#event:ext_selectedChanged * @returns {void} */ - const selectedChanged = function (win, elems) { - const mode = svgCanvas.getMode(); + selectedChanged (win, elems) { + const mode = this.svgCanvas.getMode(); if (mode === 'select') { - editor.leftPanelHandlers.clickSelect(); + this.leftPanelHandlers.clickSelect(); } const isNode = mode === 'pathedit'; - // if elems[1] is present, then we have more than one element - editor.selectedElement = (elems.length === 1 || isNullish(elems[1]) ? elems[0] : null); - editor.multiselected = (elems.length >= 2 && !isNullish(elems[1])); - if (!isNullish(editor.selectedElement) && !isNode) { - updateToolbar(); + // if this.elems[1] is present, then we have more than one element + this.selectedElement = (elems.length === 1 || isNullish(elems[1]) ? elems[0] : null); + this.multiselected = (elems.length >= 2 && !isNullish(elems[1])); + if (!isNullish(this.selectedElement) && !isNode) { + this.updateToolbar(); } // if (!isNullish(elem)) // Deal with pathedit mode - togglePathEditMode(isNode, elems); - editor.topPanelHandlers.updateContextPanel(); - svgCanvas.runExtensions('selectedChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_selectedChanged} */ { + this.togglePathEditMode(isNode, elems); + this.topPanelHandlers.updateContextPanel(); + this.svgCanvas.runExtensions('selectedChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_selectedChanged} */ { elems, - selectedElement: editor.selectedElement, - multiselected: editor.multiselected + selectedElement: this.selectedElement, + multiselected: this.multiselected }); - }; + } // Call when part of element is in process of changing, generally // on mousemove actions like rotate, move, etc. @@ -927,68 +1532,62 @@ editor.init = () => { * @fires module:svgcanvas.SvgCanvas#event:ext_elementTransition * @returns {void} */ - const elementTransition = function (win, elems) { - const mode = svgCanvas.getMode(); + elementTransition (win, elems) { + const mode = this.svgCanvas.getMode(); const elem = elems[0]; if (!elem) { return; } - editor.multiselected = (elems.length >= 2 && !isNullish(elems[1])); + this.multiselected = (elems.length >= 2 && !isNullish(elems[1])); // Only updating fields for single elements for now - if (!editor.multiselected) { + if (!this.multiselected) { switch (mode) { case 'rotate': { - const ang = svgCanvas.getRotationAngle(elem); + const ang = this.svgCanvas.getRotationAngle(elem); $('#angle').val(ang); $('#tool_reorient').toggleClass('disabled', ang === 0); break; - - // TODO: Update values that change on move/resize, etc - // } case 'select': { - // } case 'resize': { - // break; - // } } } } - svgCanvas.runExtensions('elementTransition', /** @type {module:svgcanvas.SvgCanvas#event:ext_elementTransition} */ { + this.svgCanvas.runExtensions('elementTransition', /** @type {module:svgcanvas.SvgCanvas#event:ext_elementTransition} */ { elems }); - }; + } // called when any element has changed /** * @param {external:Window} win - * @param {module:svgcanvas.SvgCanvas#event:changed} elems + * @param {Array} elems * @listens module:svgcanvas.SvgCanvas#event:changed * @fires module:svgcanvas.SvgCanvas#event:ext_elementChanged * @returns {void} */ - const elementChanged = function (win, elems) { - const mode = svgCanvas.getMode(); + elementChanged (win, elems) { + const mode = this.svgCanvas.getMode(); if (mode === 'select') { - editor.leftPanelHandlers.clickSelect(); + this.leftPanelHandlers.clickSelect(); } elems.forEach((elem) => { const isSvgElem = (elem && elem.tagName === 'svg'); - if (isSvgElem || svgCanvas.isLayer(elem)) { - editor.layersPanel.populateLayers(); + if (isSvgElem || this.svgCanvas.isLayer(elem)) { + this.layersPanel.populateLayers(); // if the element changed was the svg, then it could be a resolution change if (isSvgElem) { - updateCanvas(); + this.updateCanvas(); } // Update selectedElement if element is no longer part of the image. // This occurs for the text elements in Firefox - } else if (elem && editor.selectedElement && isNullish(editor.selectedElement.parentNode)) { + } else if (elem && this.selectedElement && isNullish(this.selectedElement.parentNode)) { // || elem && elem.tagName == "path" && !multiselected) { // This was added in r1430, but not sure why - editor.selectedElement = elem; + this.selectedElement = elem; } }); - editor.showSaveWarning = true; + this.showSaveWarning = true; // we update the contextual panel with potentially new // positional/sizing information (we DON'T want to update the @@ -997,28 +1596,27 @@ editor.init = () => { // we tell it to skip focusing the text control if the // text element was previously in focus - editor.topPanelHandlers.updateContextPanel(); + this.topPanelHandlers.updateContextPanel(); // In the event a gradient was flipped: - if (editor.selectedElement && mode === 'select') { - editor.bottomPanelHandlers.updateColorpickers(); + if (this.selectedElement && mode === 'select') { + this.bottomPanelHandlers.updateColorpickers(); } - svgCanvas.runExtensions('elementChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_elementChanged} */ { + this.svgCanvas.runExtensions('elementChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_elementChanged} */ { elems }); - }; + } /** * @returns {void} */ - const zoomDone = () => { - updateWireFrame(); - // updateCanvas(); // necessary? - }; + zoomDone () { + this.updateWireFrame(); + } /** - * @typedef {PlainObject} module:SVGEditor.BBoxObjectWithFactor (like `DOMRect`) + * @typedef {PlainObject} module:SVGthis.BBoxObjectWithFactor (like `DOMRect`) * @property {Float} x * @property {Float} y * @property {Float} width @@ -1036,47 +1634,39 @@ editor.init = () => { * @listens module:svgcanvas.SvgCanvas#event:zoomed * @returns {void} */ - const zoomChanged = svgCanvas.zoomChanged = function (win, bbox, autoCenter) { + zoomChanged (win, bbox, autoCenter) { const scrbar = 15, - // res = svgCanvas.getResolution(), // Currently unused - wArea = workarea; + // res = this.svgCanvas.getResolution(), // Currently unused + wArea = this.workarea; // const canvasPos = $('#svgcanvas').position(); // Currently unused - const zInfo = svgCanvas.setBBoxZoom(bbox, wArea.width() - scrbar, wArea.height() - scrbar); + const zInfo = this.svgCanvas.setBBoxZoom(bbox, wArea.width() - scrbar, wArea.height() - scrbar); if (!zInfo) { return; } const zoomlevel = zInfo.zoom, bb = zInfo.bbox; if (zoomlevel < 0.001) { - editor.changeZoom(0.1); + this.changeZoom(0.1); return; } - $id('zoom').value = (svgCanvas.getZoom() * 100).toFixed(1); + $id('zoom').value = (this.svgCanvas.getZoom() * 100).toFixed(1); if (autoCenter) { - updateCanvas(); + this.updateCanvas(); } else { - updateCanvas(false, {x: bb.x * zoomlevel + (bb.width * zoomlevel) / 2, y: bb.y * zoomlevel + (bb.height * zoomlevel) / 2}); + this.updateCanvas( + false, + {x: bb.x * zoomlevel + (bb.width * zoomlevel) / 2, y: bb.y * zoomlevel + (bb.height * zoomlevel) / 2} + ); } - if (svgCanvas.getMode() === 'zoom' && bb.width) { + if (this.svgCanvas.getMode() === 'zoom' && bb.width) { // Go to select if a zoom box was drawn - editor.leftPanelHandlers.clickSelect(); + this.leftPanelHandlers.clickSelect(); } - zoomDone(); - }; - - $('#cur_context_panel').delegate('a', 'click', (evt) => { - const link = $(evt.currentTarget); - if (link.attr('data-root')) { - svgCanvas.leaveContext(); - } else { - svgCanvas.setContext(link.text()); - } - svgCanvas.clearSelection(); - return false; - }); + this.zoomDone(); + } /** * @param {external:Window} win @@ -1084,11 +1674,11 @@ editor.init = () => { * @listens module:svgcanvas.SvgCanvas#event:contextset * @returns {void} */ - const contextChanged = function (win, context) { + contextChanged (win, context) { let linkStr = ''; if (context) { let str = ''; - linkStr = '' + svgCanvas.getCurrentDrawing().getCurrentLayerName() + ''; + linkStr = '' + this.svgCanvas.getCurrentDrawing().getCurrentLayerName() + ''; $(context).parentsUntil('#svgcontent > g').andSelf().each(() => { if (this.id) { @@ -1097,14 +1687,14 @@ editor.init = () => { } }); - curContext = str; + this.curContext = str; } else { - curContext = null; + this.curContext = null; } $('#cur_context_panel').toggle(Boolean(context)).html(linkStr); - updateTitle(); - }; + this.updateTitle(); + } /** * @param {external:Window} win @@ -1112,14 +1702,14 @@ editor.init = () => { * @listens module:svgcanvas.SvgCanvas#event:extension_added * @returns {Promise|void} Resolves to `undefined` */ - const extAdded = async (win, ext) => { + async extAdded (win, ext) { if (!ext) { return undefined; } let cbCalled = false; - if (ext.langReady && editor.langChanged) { // We check for this since the "lang" pref could have been set by storage - const lang = editor.pref('lang'); + if (ext.langReady && this.langChanged) { // We check for this since the "lang" pref could have been set by storage + const lang = this.pref('lang'); await ext.langReady({lang}); } @@ -1135,7 +1725,7 @@ editor.init = () => { }; /** - * @typedef {PlainObject} module:SVGEditor.ContextTool + * @typedef {PlainObject} module:SVGthis.ContextTool * @property {string} panel The ID of the existing panel to which the tool is being added. Required. * @property {string} id The ID of the actual tool element. Required. * @property {PlainObject|PlainObject<"change", external:jQuery.Function>} events DOM event names keyed to associated functions. Example: `{change () { alert('Option was changed') } }`. "change" event is one specifically handled for the "button-select" type. Required. @@ -1225,482 +1815,155 @@ editor.init = () => { } if (ext.events) { - editor.leftPanelHandlers.add(ext.events.id, ext.events.click); + this.leftPanelHandlers.add(ext.events.id, ext.events.click); } return runCallback(); - }; - - // bind the selected event to our function that handles updates to the UI - svgCanvas.bind('selected', selectedChanged); - svgCanvas.bind('transition', elementTransition); - svgCanvas.bind('changed', elementChanged); - svgCanvas.bind('saved', saveHandler); - svgCanvas.bind('exported', exportHandler); - svgCanvas.bind('exportedPDF', function (win, data) { - if (!data.output) { // Ignore Chrome - return; - } - const {exportWindowName} = data; - if (exportWindowName) { - exportWindow = window.open('', exportWindowName); // A hack to get the window via JSON-able name without opening a new one - } - if (!exportWindow || exportWindow.closed) { - alert(uiStrings.notification.popupWindowBlocked); - return; - } - exportWindow.location.href = data.output; - }); - svgCanvas.bind('zoomed', zoomChanged); - svgCanvas.bind('zoomDone', zoomDone); - svgCanvas.bind( - 'updateCanvas', - /** - * @param {external:Window} win - * @param {PlainObject} centerInfo - * @param {false} centerInfo.center - * @param {module:math.XYObject} centerInfo.newCtr - * @listens module:svgcanvas.SvgCanvas#event:updateCanvas - * @returns {void} - */ - function (win, {center, newCtr}) { - updateCanvas(center, newCtr); - } - ); - svgCanvas.bind('contextset', contextChanged); - svgCanvas.bind('extension_added', extAdded); - svgCanvas.textActions.setInputElem($('#text')[0]); - - setBackground(editor.pref('bkgd_color'), editor.pref('bkgd_url')); - - // update resolution option with actual resolution - const res = svgCanvas.getResolution(); - if (editor.configObj.curConfig.baseUnit !== 'px') { - res.w = convertUnit(res.w) + editor.configObj.curConfig.baseUnit; - res.h = convertUnit(res.h) + editor.configObj.curConfig.baseUnit; } - $('#se-img-prop').attr('dialog', 'close'); - $('#se-img-prop').attr('title', svgCanvas.getDocumentTitle()); - $('#se-img-prop').attr('width', res.w); - $('#se-img-prop').attr('height', res.h); - $('#se-img-prop').attr('save', editor.pref('img_save')); - // Lose focus for select elements when changed (Allows keyboard shortcuts to work better) - $('select').change((evt) => { $(evt.currentTarget).blur(); }); - - // fired when user wants to move elements to another layer - let promptMoveLayerOnce = false; - $('#selLayerNames').change((evt) => { - const destLayer = evt.currentTarget.options[evt.currentTarget.selectedIndex].value; - const confirmStr = uiStrings.notification.QmoveElemsToLayer.replace('%s', destLayer); - /** - * @param {boolean} ok - * @returns {void} - */ - const moveToLayer = function (ok) { - if (!ok) { return; } - promptMoveLayerOnce = true; - svgCanvas.moveSelectedToLayer(destLayer); - svgCanvas.clearSelection(); - editor.layersPanel.populateLayers(); - }; - if (destLayer) { - if (promptMoveLayerOnce) { - moveToLayer(true); - } else { - const ok = confirm(confirmStr); - if (!ok) { - return; - } - moveToLayer(true); - } - } - }); - - $('#font_family').change((evt) => { - svgCanvas.setFontFamily(evt.currentTarget.value); - }); - - $('#seg_type').change((evt) => { - svgCanvas.setSegType($(evt.currentTarget).val()); - }); - - $('#text').bind('keyup input', (evt) => { - svgCanvas.setTextContent(evt.currentTarget.value); - }); - - $('#image_url').change((evt) => { - setImageURL(evt.currentTarget.value); - }); - - $('#link_url').change((evt) => { - if (evt.currentTarget.value.length) { - svgCanvas.setLinkURL(evt.currentTarget.value); - } else { - svgCanvas.removeHyperlink(); - } - }); - - $('#g_title').change((evt) => { - svgCanvas.setGroupTitle(evt.currentTarget.value); - }); - - (function () { - const wArea = workarea[0]; - - let lastX = null, lastY = null, - panning = false, keypan = false; - - $('#svgcanvas').bind('mousemove mouseup', function (evt) { - if (panning === false) { return true; } - - wArea.scrollLeft -= (evt.clientX - lastX); - wArea.scrollTop -= (evt.clientY - lastY); - - lastX = evt.clientX; - lastY = evt.clientY; - - if (evt.type === 'mouseup') { panning = false; } - return false; - }).mousedown(function (evt) { - if (evt.button === 1 || keypan === true) { - panning = true; - lastX = evt.clientX; - lastY = evt.clientY; - return false; - } - return true; + /* + this.addDropDown('#font_family_dropdown', () => { + $('#font_family').val($(this).text()).change(); }); - - $(window).mouseup(() => { - panning = false; - }); - - $(document).bind('keydown', 'space', function (evt) { - svgCanvas.spaceKey = keypan = true; - evt.preventDefault(); - }).bind('keyup', 'space', function (evt) { - evt.preventDefault(); - svgCanvas.spaceKey = keypan = false; - }).bind('keydown', 'shift', function (evt) { - if (svgCanvas.getMode() === 'zoom') { - workarea.css('cursor', zoomOutIcon); - } - }).bind('keyup', 'shift', function (evt) { - if (svgCanvas.getMode() === 'zoom') { - workarea.css('cursor', zoomInIcon); - } - }); - - /** - * @function module:SVGEditor.setPanning - * @param {boolean} active - * @returns {void} - */ - editor.setPanning = function (active) { - svgCanvas.spaceKey = keypan = active; - }; - }()); - - (function () { - const button = $('#main_icon'); - const overlay = $('#main_icon span'); - const list = $('#main_menu'); - - let onButton = false; - let height = 0; - let jsHover = true; - let setClick = false; - - $(window).mouseup(function (evt) { - if (!onButton) { - button.removeClass('buttondown'); - // do not hide if it was the file input as that input needs to be visible - // for its change event to fire - if (evt.target.tagName !== 'INPUT') { - list.fadeOut(200); - } else if (!setClick) { - setClick = true; - $(evt.target).click(() => { - list.css('margin-left', '-9999px').show(); - }); - } - } - onButton = false; - }).mousedown(function (evt) { - // $('.contextMenu').hide(); - const islib = $(evt.target).closest('.contextMenu').length; - if (!islib) { - $('.contextMenu').fadeOut(250); - } - }); - - overlay.bind('mousedown', () => { - if (!button.hasClass('buttondown')) { - // Margin must be reset in case it was changed before; - list.css('margin-left', 0).show(); - if (!height) { - height = list.height(); - } - // Using custom animation as slideDown has annoying 'bounce effect' - list.css('height', 0).animate({ - height - }, 200); - onButton = true; - } else { - list.fadeOut(200); - } - button.toggleClass('buttondown buttonup'); - }).hover(() => { - onButton = true; - }).mouseout(() => { - onButton = false; - }); - - const listItems = $('#main_menu li'); - - // Check if JS method of hovering needs to be used (Webkit bug) - listItems.mouseover(function () { - jsHover = ($(this).css('background-color') === 'rgba(0, 0, 0, 0)'); - - listItems.unbind('mouseover'); - if (jsHover) { - listItems.mouseover(() => { - this.style.backgroundColor = '#FFC'; - }).mouseout((evt) => { - evt.currentTarget.style.backgroundColor = 'transparent'; - return true; - }); - } - }); - }()); - // Made public for UI customization. - // TODO: Group UI functions into a public editor.ui interface. - /** - * See {@link http://api.jquery.com/bind/#bind-eventType-eventData-handler}. - * @callback module:SVGEditor.DropDownCallback - * @param {external:jQuery.Event} ev See {@link http://api.jquery.com/Types/#Event} - * @listens external:jQuery.Event - * @returns {void|boolean} Calls `preventDefault()` and `stopPropagation()` */ - /** - * @function module:SVGEditor.addDropDown - * @param {Element|string} elem DOM Element or selector - * @param {module:SVGEditor.DropDownCallback} callback Mouseup callback - * @param {boolean} dropUp - * @returns {void} - */ - editor.addDropDown = function (elem, callback, dropUp) { - if (!$(elem).length) { return; } // Quit if called on non-existent element - const button = $(elem).find('button'); - const list = $(elem).find('ul').attr('id', $(elem)[0].id + '-list'); - if (dropUp) { - $(elem).addClass('dropup'); - } else { - // Move list to place where it can overflow container - $('#option_lists').append(list); - } - list.find('li').bind('mouseup', callback); - - let onButton = false; - $(window).mouseup(function (evt) { - if (!onButton) { - button.removeClass('down'); - list.hide(); - } - onButton = false; - }); - - button.bind('mousedown', () => { - if (!button.hasClass('down')) { - if (!dropUp) { - const pos = $(elem).position(); - list.css({ - top: pos.top + 24, - left: pos.left - 10 - }); - } - list.show(); - onButton = true; - } else { - list.hide(); - } - button.toggleClass('down'); - }).hover(() => { - onButton = true; - }).mouseout(() => { - onButton = false; - }); - }; - - editor.addDropDown('#font_family_dropdown', () => { - $('#font_family').val($(this).text()).change(); - }); - /** * @param {Float} multiplier * @returns {void} */ - editor.zoomImage = function (multiplier) { - const resolution = svgCanvas.getResolution(); + zoomImage (multiplier) { + const resolution = this.svgCanvas.getResolution(); multiplier = multiplier ? resolution.zoom * multiplier : 1; // setResolution(res.w * multiplier, res.h * multiplier, true); $id('zoom').value = (multiplier * 100).toFixed(1); - svgCanvas.setZoom(multiplier); - zoomDone(); - updateCanvas(true); - }; - - // Unfocus text input when workarea is mousedowned. - (function () { - let inp; - /** - * - * @returns {void} - */ - const unfocus = () => { - $(inp).blur(); - }; - - $('#svg_editor').find('button, select, input:not(#text)').focus(() => { - inp = this; - uiContext = 'toolbars'; - workarea.mousedown(unfocus); - }).blur(() => { - uiContext = 'canvas'; - workarea.unbind('mousedown', unfocus); - // Go back to selecting text if in textedit mode - if (svgCanvas.getMode() === 'textedit') { - $('#text').focus(); - } - }); - }()); + this.svgCanvas.setZoom(multiplier); + this.zoomDone(); + this.updateCanvas(true); + } /** * * @returns {void} */ - const cutSelected = () => { - if (!isNullish(editor.selectedElement) || editor.multiselected) { - svgCanvas.cutSelectedElements(); + cutSelected () { + if (!isNullish(this.selectedElement) || this.multiselected) { + this.svgCanvas.cutSelectedElements(); } - }; + } /** * * @returns {void} */ - const copySelected = () => { - if (!isNullish(editor.selectedElement) || editor.multiselected) { - svgCanvas.copySelectedElements(); + copySelected () { + if (!isNullish(this.selectedElement) || this.multiselected) { + this.svgCanvas.copySelectedElements(); } - }; + } /** * * @returns {void} */ - const pasteInCenter = () => { - const zoom = svgCanvas.getZoom(); - const x = (workarea[0].scrollLeft + workarea.width() / 2) / zoom - svgCanvas.contentW; - const y = (workarea[0].scrollTop + workarea.height() / 2) / zoom - svgCanvas.contentH; - svgCanvas.pasteElements('point', x, y); - }; + pasteInCenter () { + const zoom = this.svgCanvas.getZoom(); + const x = (this.workarea[0].scrollLeft + this.workarea.width() / 2) / zoom - this.svgCanvas.contentW; + const y = (this.workarea[0].scrollTop + this.workarea.height() / 2) / zoom - this.svgCanvas.contentH; + this.svgCanvas.pasteElements('point', x, y); + } /** * @param {"Up"|"Down"} dir * @returns {void} */ - const moveUpDownSelected = function (dir) { - if (!isNullish(editor.selectedElement)) { - svgCanvas.moveUpDownSelected(dir); + moveUpDownSelected (dir) { + if (!isNullish(this.selectedElement)) { + this.svgCanvas.moveUpDownSelected(dir); } - }; + } /** * @param {Float} dx * @param {Float} dy * @returns {void} */ - const moveSelected = function (dx, dy) { - if (!isNullish(editor.selectedElement) || editor.multiselected) { - if (editor.configObj.curConfig.gridSnapping) { + moveSelected (dx, dy) { + if (!isNullish(this.selectedElement) || this.multiselected) { + if (this.configObj.curConfig.gridSnapping) { // Use grid snap value regardless of zoom level - const multi = svgCanvas.getZoom() * editor.configObj.curConfig.snappingStep; + const multi = this.svgCanvas.getZoom() * this.configObj.curConfig.snappingStep; dx *= multi; dy *= multi; } - svgCanvas.moveSelectedElements(dx, dy); + this.svgCanvas.moveSelectedElements(dx, dy); } - }; + } /** * * @returns {void} */ - const selectNext = () => { - svgCanvas.cycleElement(1); - }; + selectNext () { + this.svgCanvas.cycleElement(1); + } /** * * @returns {void} */ - const selectPrev = () => { - svgCanvas.cycleElement(0); - }; + selectPrev () { + this.svgCanvas.cycleElement(0); + } /** * @param {0|1} cw * @param {Integer} step * @returns {void} */ - const rotateSelected = function (cw, step) { - if (isNullish(editor.selectedElement) || editor.multiselected) { return; } + rotateSelected (cw, step) { + if (isNullish(this.selectedElement) || this.multiselected) { return; } if (!cw) { step *= -1; } const angle = Number.parseFloat($('#angle').val()) + step; - svgCanvas.setRotationAngle(angle); - editor.topPanelHandlers.updateContextPanel(); - }; + this.svgCanvas.setRotationAngle(angle); + this.topPanelHandlers.updateContextPanel(); + } /** * @fires module:svgcanvas.SvgCanvas#event:ext_onNewDocument * @returns {void} */ - const clickClear = () => { - const [x, y] = editor.configObj.curConfig.dimensions; - const ok = confirm(uiStrings.notification.QwantToClear); + clickClear () { + const [x, y] = this.configObj.curConfig.dimensions; + const ok = confirm(this.uiStrings.notification.QwantToClear); if (!ok) { return; } - editor.leftPanelHandlers.clickSelect(); - svgCanvas.clear(); - svgCanvas.setResolution(x, y); - updateCanvas(true); - editor.zoomImage(); - editor.layersPanel.populateLayers(); - editor.topPanelHandlers.updateContextPanel(); - svgCanvas.runExtensions('onNewDocument'); - }; + this.leftPanelHandlers.clickSelect(); + this.svgCanvas.clear(); + this.svgCanvas.setResolution(x, y); + this.updateCanvas(true); + this.zoomImage(); + this.layersPanel.populateLayers(); + this.topPanelHandlers.updateContextPanel(); + this.svgCanvas.runExtensions('onNewDocument'); + } /** * * @returns {void} */ - const clickSave = () => { + clickSave () { // In the future, more options can be provided here const saveOpts = { - images: editor.pref('img_save'), + images: this.pref('img_save'), round_digits: 6 }; - svgCanvas.save(saveOpts); - }; + this.svgCanvas.save(saveOpts); + } - let loadingURL; /** * * @returns {Promise} Resolves to `undefined` */ - const clickExport = async () => { + async clickExport () { const imgType = await seSelect('Select an image type for export: ', [ // See http://kangax.github.io/jstests/toDataUrl_mime_type_test/ for a useful list of MIME types and browser support // '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' @@ -1718,15 +1981,15 @@ editor.init = () => { * * @returns {void} */ - function openExportWindow () { - const {loadingImage} = uiStrings.notification; - if (editor.configObj.curConfig.exportWindowType === 'new') { - editor.exportWindowCt++; + const openExportWindow = () => { + const {loadingImage} = this.uiStrings.notification; + if (this.configObj.curConfig.exportWindowType === 'new') { + this.exportWindowCt++; } - exportWindowName = editor.configObj.curConfig.canvasName + editor.exportWindowCt; + this.exportWindowName = this.configObj.curConfig.canvasName + this.exportWindowCt; let popHTML, popURL; - if (loadingURL) { - popURL = loadingURL; + if (this.loadingURL) { + popURL = this.loadingURL; } else { popHTML = ` @@ -1741,124 +2004,124 @@ editor.init = () => { } else { popURL = 'data:text/html;base64;charset=utf-8,' + encode64(popHTML); } - loadingURL = popURL; + this.loadingURL = popURL; } - exportWindow = window.open(popURL, exportWindowName); - } + this.exportWindow = window.open(popURL, this.exportWindowName); + }; const chrome = isChrome(); if (imgType === 'PDF') { - if (!customExportPDF && !chrome) { + if (!this.customExportPDF && !chrome) { openExportWindow(); } - svgCanvas.exportPDF(exportWindowName); + this.svgCanvas.exportPDF(exportWindowName); } else { - if (!customExportImage) { + if (!this.customExportImage) { openExportWindow(); } const quality = 1; // JFH !!! Number.parseInt($('#image-slider').val()) / 100; - /* const results = */ await svgCanvas.rasterExport(imgType, quality, exportWindowName); + /* const results = */ await this.svgCanvas.rasterExport(imgType, quality, this.exportWindowName); } - }; + } /** - * By default, svgCanvas.open() is a no-op. It is up to an extension + * By default, this.svgCanvas.open() is a no-op. It is up to an extension * mechanism (opera widget, etc.) to call `setCustomHandlers()` which * will make it do something. * @returns {void} */ - const clickOpen = () => { - svgCanvas.open(); - }; + clickOpen () { + this.svgCanvas.open(); + } /** * * @returns {void} */ - const clickImport = () => { + // eslint-disable-next-line class-methods-use-this + clickImport () { /* empty fn */ - }; - - let docprops = false; - let preferences = false; + } /** * * @returns {void} */ - const showDocProperties = () => { - if (docprops) { return; } - docprops = true; + showDocProperties () { + if (this.docprops) { return; } + this.docprops = true; const $imgDialog = document.getElementById('se-img-prop'); // update resolution option with actual resolution - const resolution = svgCanvas.getResolution(); - if (editor.configObj.curConfig.baseUnit !== 'px') { - resolution.w = convertUnit(resolution.w) + editor.configObj.curConfig.baseUnit; - resolution.h = convertUnit(resolution.h) + editor.configObj.curConfig.baseUnit; + const resolution = this.svgCanvas.getResolution(); + if (this.configObj.curConfig.baseUnit !== 'px') { + resolution.w = convertUnit(resolution.w) + this.configObj.curConfig.baseUnit; + resolution.h = convertUnit(resolution.h) + this.configObj.curConfig.baseUnit; } - $imgDialog.setAttribute('save', editor.pref('img_save')); + $imgDialog.setAttribute('save', this.pref('img_save')); $imgDialog.setAttribute('width', resolution.w); $imgDialog.setAttribute('height', resolution.h); - $imgDialog.setAttribute('title', svgCanvas.getDocumentTitle()); + $imgDialog.setAttribute('title', this.svgCanvas.getDocumentTitle()); $imgDialog.setAttribute('dialog', 'open'); - }; + } /** * * @returns {void} */ - const showPreferences = () => { - if (preferences) { return; } - preferences = true; + showPreferences () { + if (this.preferences) { return; } + this.preferences = true; const $editDialog = document.getElementById('se-edit-prefs'); $('#main_menu').hide(); // Update background color with current one - const canvasBg = editor.configObj.curPrefs.bkgd_color; - const url = editor.pref('bkgd_url'); + const canvasBg = this.configObj.curPrefs.bkgd_color; + const url = this.pref('bkgd_url'); if (url) { $editDialog.setAttribute('bgurl', url); } - $editDialog.setAttribute('gridsnappingon', editor.configObj.curConfig.gridSnapping); - $editDialog.setAttribute('gridsnappingstep', editor.configObj.curConfig.snappingStep); - $editDialog.setAttribute('gridcolor', editor.configObj.curConfig.gridColor); + $editDialog.setAttribute('gridsnappingon', this.configObj.curConfig.gridSnapping); + $editDialog.setAttribute('gridsnappingstep', this.configObj.curConfig.snappingStep); + $editDialog.setAttribute('gridcolor', this.configObj.curConfig.gridColor); $editDialog.setAttribute('canvasbg', canvasBg); $editDialog.setAttribute('dialog', 'open'); - }; + } /** * * @returns {void} */ - const openHomePage = () => { + // eslint-disable-next-line class-methods-use-this + openHomePage () { window.open(homePage, '_blank'); - }; + } /** * * @returns {void} */ - const hideSourceEditor = () => { + // eslint-disable-next-line class-methods-use-this + hideSourceEditor () { const $editorDialog = document.getElementById('se-svg-editor-dialog'); $editorDialog.setAttribute('dialog', 'closed'); - }; + } /** * @param {Event} e * @returns {void} Resolves to `undefined` */ - const saveSourceEditor = (e) => { + saveSourceEditor (e) { const $editorDialog = document.getElementById('se-svg-editor-dialog'); if ($editorDialog.getAttribute('dialog') !== 'open') return; const saveChanges = () => { - svgCanvas.clearSelection(); - hideSourceEditor(); - editor.zoomImage(); - editor.layersPanel.populateLayers(); - updateTitle(); + this.svgCanvas.clearSelection(); + this.hideSourceEditor(); + this.zoomImage(); + this.layersPanel.populateLayers(); + this.updateTitle(); }; - if (!svgCanvas.setSvgString(e.detail.value)) { - const ok = confirm(uiStrings.notification.QerrorsRevertToSource); + if (!this.svgCanvas.setSvgString(e.detail.value)) { + const ok = confirm(this.uiStrings.notification.QerrorsRevertToSource); if (!ok) { return; } @@ -1866,542 +2129,149 @@ editor.init = () => { return; } saveChanges(); - editor.leftPanelHandlers.clickSelect(); - }; + this.leftPanelHandlers.clickSelect(); + } /** * * @returns {void} */ - const hideDocProperties = () => { + hideDocProperties () { const $imgDialog = document.getElementById('se-img-prop'); $imgDialog.setAttribute('dialog', 'close'); - $imgDialog.setAttribute('save', editor.pref('img_save')); - docprops = false; - }; + $imgDialog.setAttribute('save', this.pref('img_save')); + this.docprops = false; + } /** * * @returns {void} */ - const hidePreferences = () => { + hidePreferences () { const $editDialog = document.getElementById('se-edit-prefs'); $editDialog.setAttribute('dialog', 'close'); - preferences = false; - }; + this.preferences = false; + } /** * @param {Event} e * @returns {boolean} Whether there were problems saving the document properties */ - const saveDocProperties = function (e) { + saveDocProperties (e) { // set title const {title, w, h, save} = e.detail; // set document title - svgCanvas.setDocumentTitle(title); + this.svgCanvas.setDocumentTitle(title); if (w !== 'fit' && !isValidUnit('width', w)) { - alert(uiStrings.notification.invalidAttrValGiven); + alert(this.uiStrings.notification.invalidAttrValGiven); return false; } if (h !== 'fit' && !isValidUnit('height', h)) { - alert(uiStrings.notification.invalidAttrValGiven); + alert(this.uiStrings.notification.invalidAttrValGiven); return false; } - if (!svgCanvas.setResolution(w, h)) { - alert(uiStrings.notification.noContentToFitTo); + if (!this.svgCanvas.setResolution(w, h)) { + alert(this.uiStrings.notification.noContentToFitTo); return false; } // Set image save option - editor.pref('img_save', save); - updateCanvas(); - hideDocProperties(); + this.pref('img_save', save); + this.updateCanvas(); + this.hideDocProperties(); return true; - }; + } /** * Save user preferences based on current values in the UI. * @param {Event} e - * @function module:SVGEditor.savePreferences + * @function module:SVGthis.savePreferences * @returns {Promise} */ - const savePreferences = editor.savePreferences = async function (e) { + async savePreferences (e) { const {lang, bgcolor, bgurl, gridsnappingon, gridsnappingstep, gridcolor, showrulers, baseunit} = e.detail; // Set background - setBackground(bgcolor, bgurl); + this.setBackground(bgcolor, bgurl); // set language - if (lang && lang !== editor.pref('lang')) { - const {langParam, langData} = await editor.putLocale(lang, goodLangs); - await setLang(langParam, langData); + if (lang && lang !== this.pref('lang')) { + const {langParam, langData} = await this.putLocale(lang, this.goodLangs); + await this.setLang(langParam, langData); } // set grid setting - editor.configObj.curConfig.gridSnapping = gridsnappingon; - editor.configObj.curConfig.snappingStep = gridsnappingstep; - editor.configObj.curConfig.gridColor = gridcolor; - editor.configObj.curConfig.showRulers = showrulers; + this.configObj.curConfig.gridSnapping = gridsnappingon; + this.configObj.curConfig.snappingStep = gridsnappingstep; + this.configObj.curConfig.gridColor = gridcolor; + this.configObj.curConfig.showRulers = showrulers; - $('#rulers').toggle(editor.configObj.curConfig.showRulers); - if (editor.configObj.curConfig.showRulers) { updateRulers(); } - editor.configObj.curConfig.baseUnit = baseunit; - - svgCanvas.setConfig(editor.configObj.curConfig); - updateCanvas(); - hidePreferences(); - }; + $('#rulers').toggle(this.configObj.curConfig.showRulers); + if (this.configObj.curConfig.showRulers) { this.rulers.updateRulers(); } + this.configObj.curConfig.baseUnit = baseunit; + this.svgCanvas.setConfig(this.configObj.curConfig); + this.updateCanvas(); + this.hidePreferences(); + } /** * @param {Event} e * @returns {void} Resolves to `undefined` */ - const cancelOverlays = (e) => { + cancelOverlays (e) { $('#dialog_box').hide(); const $editorDialog = document.getElementById('se-svg-editor-dialog'); const editingsource = $editorDialog.getAttribute('dialog') === 'open'; - if (!editingsource && !docprops && !preferences) { - if (curContext) { - svgCanvas.leaveContext(); + if (!editingsource && !this.docprops && !this.preferences) { + if (this.curContext) { + this.svgCanvas.leaveContext(); } return; } if (editingsource) { - const origSource = svgCanvas.getSvgString(); + const origSource = this.svgCanvas.getSvgString(); if (origSource !== e.detail.value) { - const ok = confirm(uiStrings.notification.QignoreSourceChanges); + const ok = confirm(this.uiStrings.notification.QignoreSourceChanges); if (ok) { - hideSourceEditor(); + this.hideSourceEditor(); } } else { - hideSourceEditor(); + this.hideSourceEditor(); } } - }; + } - const winWh = {width: $(window).width(), height: $(window).height()}; - - $(window).resize(function (evt) { - $.each(winWh, function (type, val) { - const curval = $(window)[type](); - workarea[0]['scroll' + (type === 'width' ? 'Left' : 'Top')] -= (curval - val) / 2; - winWh[type] = curval; - }); - }); - - workarea.scroll(() => { - // TODO: jQuery's scrollLeft/Top() wouldn't require a null check - if ($('#ruler_x').length) { - $('#ruler_x')[0].scrollLeft = workarea[0].scrollLeft; - } - if ($('#ruler_y').length) { - $('#ruler_y')[0].scrollTop = workarea[0].scrollTop; - } - }); - - $('#url_notice').click(() => { - alert(this.title); - }); - - $('#stroke_width').val(editor.configObj.curConfig.initStroke.width); - $('#group_opacity').val(editor.configObj.curConfig.initOpacity * 100); - - $('#group_opacityLabel').click(() => { - $('#opacity_dropdown button').mousedown(); - $(window).mouseup(); - }); - - $('.push_button').mousedown(() => { - if (!$(this).hasClass('disabled')) { - $(this).addClass('push_button_pressed').removeClass('push_button'); - } - }).mouseout(() => { - $(this).removeClass('push_button_pressed').addClass('push_button'); - }).mouseup(() => { - $(this).removeClass('push_button_pressed').addClass('push_button'); - }); - - editor.layersPanel.populateLayers(); - - const centerCanvas = () => { - // this centers the canvas vertically in the workarea (horizontal handled in CSS) - workarea.css('line-height', workarea.height() + 'px'); - }; - - $(window).bind('load resize', centerCanvas); - - // Prevent browser from erroneously repopulating fields - $('input,select').attr('autocomplete', 'off'); - - /* eslint-disable jsdoc/require-property */ - /** - * Associate all button actions as well as non-button keyboard shortcuts. - * @namespace {PlainObject} module:SVGEditor~Actions - */ - const Actions = (function () { - /* eslint-enable jsdoc/require-property */ - /** - * @typedef {PlainObject} module:SVGEditor.ToolButton - * @property {string} sel The CSS selector for the tool - * @property {external:jQuery.Function} fn A handler to be attached to the `evt` - * @property {string} evt The event for which the `fn` listener will be added - * @property {module:SVGEditor.Key} [key] [key, preventDefault, NoDisableInInput] - * @property {string} [parent] Selector - * @property {boolean} [hidekey] Whether to show key value in title - * @property {string} [icon] The button ID - */ - /** - * - * @name module:SVGEditor~ToolButtons - * @type {module:SVGEditor.ToolButton[]} - */ - - editor.leftPanelHandlers.init(); - editor.bottomPanelHandlers.init(); - editor.topPanelHandlers.init(); - editor.layersPanel.init(); - - $id('tool_clear').addEventListener('click', clickClear); - $id('tool_open').addEventListener('click', function (e) { - clickOpen(); - window.dispatchEvent(new CustomEvent('openImage')); - }); - $id('tool_import').addEventListener('click', function (e) { - clickImport(); - window.dispatchEvent(new CustomEvent('importImage')); - }); - $id('tool_save').addEventListener('click', function (e) { - const $editorDialog = document.getElementById('se-svg-editor-dialog'); - const editingsource = $editorDialog.getAttribute('dialog') === 'open'; - if (editingsource) { - saveSourceEditor(); - } else { - clickSave(); - } - }); - $id('tool_export').addEventListener('click', clickExport); - $id('tool_docprops').addEventListener('click', showDocProperties); - $id('tool_editor_prefs').addEventListener('click', showPreferences); - $id('tool_editor_homepage').addEventListener('click', openHomePage); - $id('se-img-prop').addEventListener('change', function (e) { - if (e.detail.dialog === 'closed') { - hideDocProperties(); - } else { - saveDocProperties(e); - } - }); - $id('se-edit-prefs').addEventListener('change', function (e) { - if (e.detail.dialog === 'closed') { - hidePreferences(); - } else { - savePreferences(e); - } - }); - $id('se-svg-editor-dialog').addEventListener('change', function (e) { - if (e?.detail?.copy === 'click') { - cancelOverlays(e); - } else if (e?.detail?.dialog === 'closed') { - hideSourceEditor(); - } else { - saveSourceEditor(e); - } - }); - $id('se-cmenu_canvas').addEventListener('change', function (e) { - const action = e?.detail?.trigger; - switch (action) { - case 'delete': - svgCanvas.deleteSelectedElements(); - break; - case 'cut': - cutSelected(); - break; - case 'copy': - copySelected(); - break; - case 'paste': - svgCanvas.pasteElements(); - break; - case 'paste_in_place': - svgCanvas.pasteElements('in_place'); - break; - case 'group': - case 'group_elements': - svgCanvas.groupSelectedElements(); - break; - case 'ungroup': - svgCanvas.ungroupSelectedElement(); - break; - case 'move_front': - svgCanvas.moveToTopSelectedElement(); - break; - case 'move_up': - moveUpDownSelected('Up'); - break; - case 'move_down': - moveUpDownSelected('Down'); - break; - case 'move_back': - svgCanvas.moveToBottomSelected(); - break; - default: - if (hasCustomHandler(action)) { - getCustomHandler(action).call(); - } - break; - } - }); - - const toolButtons = [ - // Shortcuts not associated with buttons - {key: 'ctrl+left', fn () { rotateSelected(0, 1); }}, - {key: 'ctrl+right', fn () { rotateSelected(1, 1); }}, - {key: 'ctrl+shift+left', fn () { rotateSelected(0, 5); }}, - {key: 'ctrl+shift+right', fn () { rotateSelected(1, 5); }}, - {key: 'shift+O', fn: selectPrev}, - {key: 'shift+P', fn: selectNext}, - {key: [modKey + 'up', true], fn () { editor.zoomImage(2); }}, - {key: [modKey + 'down', true], fn () { editor.zoomImage(0.5); }}, - {key: [modKey + ']', true], fn () { moveUpDownSelected('Up'); }}, - {key: [modKey + '[', true], fn () { moveUpDownSelected('Down'); }}, - {key: ['up', true], fn () { moveSelected(0, -1); }}, - {key: ['down', true], fn () { moveSelected(0, 1); }}, - {key: ['left', true], fn () { moveSelected(-1, 0); }}, - {key: ['right', true], fn () { moveSelected(1, 0); }}, - {key: 'shift+up', fn () { moveSelected(0, -10); }}, - {key: 'shift+down', fn () { moveSelected(0, 10); }}, - {key: 'shift+left', fn () { moveSelected(-10, 0); }}, - {key: 'shift+right', fn () { moveSelected(10, 0); }}, - {key: ['alt+up', true], fn () { svgCanvas.cloneSelectedElements(0, -1); }}, - {key: ['alt+down', true], fn () { svgCanvas.cloneSelectedElements(0, 1); }}, - {key: ['alt+left', true], fn () { svgCanvas.cloneSelectedElements(-1, 0); }}, - {key: ['alt+right', true], fn () { svgCanvas.cloneSelectedElements(1, 0); }}, - {key: ['alt+shift+up', true], fn () { svgCanvas.cloneSelectedElements(0, -10); }}, - {key: ['alt+shift+down', true], fn () { svgCanvas.cloneSelectedElements(0, 10); }}, - {key: ['alt+shift+left', true], fn () { svgCanvas.cloneSelectedElements(-10, 0); }}, - {key: ['alt+shift+right', true], fn () { svgCanvas.cloneSelectedElements(10, 0); }}, - {key: 'a', fn () { svgCanvas.selectAllInCurrentLayer(); }}, - {key: modKey + 'a', fn () { svgCanvas.selectAllInCurrentLayer(); }}, - {key: modKey + 'x', fn: cutSelected}, - {key: modKey + 'c', fn: copySelected}, - {key: modKey + 'v', fn: pasteInCenter} - ]; - - // Tooltips not directly associated with a single function - const keyAssocs = { - '4/Shift+4': '#tools_rect', - '5/Shift+5': '#tools_ellipse' - }; - - return { - /** @lends module:SVGEditor~Actions */ - /** - * @returns {void} - */ - setAll () { - const keyHandler = {}; // will contain the action for each pressed key - - toolButtons.forEach((opts) => { - // Bind function to shortcut key - if (opts.key) { - // Set shortcut based on options - let keyval = opts.key; - let pd = false; - if (Array.isArray(opts.key)) { - keyval = opts.key[0]; - if (opts.key.length > 1) { pd = opts.key[1]; } - } - keyval = String(keyval); - const {fn} = opts; - keyval.split('/').forEach((key) => { keyHandler[key] = {fn, pd}; }); - } - return true; - }); - // register the keydown event - document.addEventListener('keydown', (e) => { - // only track keyboard shortcuts for the body containing the SVG-Editor - if (e.target.nodeName !== 'BODY') return; - // normalize key - const key = `${(e.metaKey) ? 'meta+' : ''}${(e.ctrlKey) ? 'ctrl+' : ''}${e.key.toLowerCase()}`; - // return if no shortcut defined for this key - if (!keyHandler[key]) return; - // launch associated handler and preventDefault if necessary - keyHandler[key].fn(); - if (keyHandler[key].pd) { - e.preventDefault(); - } - }); - - // Misc additional actions - - // Make 'return' keypress trigger the change event - $('.attr_changer, #image_url').bind( - 'keydown', - 'return', - function (evt) { - $(this).change(); - evt.preventDefault(); - } - ); - - $(window).bind('keydown', 'tab', function (e) { - if (uiContext === 'canvas') { - e.preventDefault(); - selectNext(); - } - }).bind('keydown', 'shift+tab', function (e) { - if (uiContext === 'canvas') { - e.preventDefault(); - selectPrev(); - } - }); - }, - /** - * @returns {void} - */ - setTitles () { - $.each(keyAssocs, function (keyval, sel) { - const menu = ($(sel).parents('#main_menu').length); - - $(sel).each(function () { - const t = (menu) ? $(this).text().split(' [')[0] : this.title.split(' [')[0]; - let keyStr = ''; - // Shift+Up - $.each(keyval.split('/'), function (i, key) { - const modBits = key.split('+'); - let mod = ''; - if (modBits.length > 1) { - mod = modBits[0] + '+'; - key = modBits[1]; - } - keyStr += (i ? '/' : '') + mod + (uiStrings['key_' + key] || key); - }); - if (menu) { - this.lastChild.textContent = t + ' [' + keyStr + ']'; - } else { - this.title = t + ' [' + keyStr + ']'; - } - }); - }); - }, - /** - * @param {string} sel Selector to match - * @returns {module:SVGEditor.ToolButton} - */ - getButtonData (sel) { - return Object.values(toolButtons).find((btn) => { - return btn.sel === sel; - }); - } - }; - }()); - - // Select given tool - editor.ready(function () { - const preTool = $id(`tool_${editor.configObj.curConfig.initTool}`); - const regTool = $id(editor.configObj.curConfig.initTool); - const selectTool = $id('tool_select'); - const $editDialog = $id('se-edit-prefs'); - - if (preTool) { - preTool.click(); - } else if (regTool) { - regTool.click(); - } else { - selectTool.click(); - } - - if (editor.configObj.curConfig.wireframe) { - $id('tool_wireframe').click(); - } - - $('#rulers').toggle(Boolean(editor.configObj.curConfig.showRulers)); - - if (editor.configObj.curConfig.showRulers) { - $editDialog.setAttribute('showrulers', true); - } - - if (editor.configObj.curConfig.baseUnit) { - $editDialog.setAttribute('baseunit', editor.configObj.curConfig.baseUnit); - } - - if (editor.configObj.curConfig.gridSnapping) { - $editDialog.setAttribute('gridsnappingon', true); - } - - if (editor.configObj.curConfig.snappingStep) { - $editDialog.setAttribute('gridsnappingstep', editor.configObj.curConfig.snappingStep); - } - - if (editor.configObj.curConfig.gridColor) { - $editDialog.setAttribute('gridcolor', editor.configObj.curConfig.gridColor); - } - }); - - // zoom - $id('zoom').value = (svgCanvas.getZoom() * 100).toFixed(1); - canvMenu.setAttribute('disableallmenu', true); - canvMenu.setAttribute('enablemenuitems', '#delete,#cut,#copy'); /** * @returns {void} */ - function enableOrDisableClipboard () { + enableOrDisableClipboard () { let svgeditClipboard; try { - svgeditClipboard = localStorage.getItem('svgedit_clipboard'); + svgeditClipboard = this.localStorage.getItem('svgedit_clipboard'); } catch (err) {} - canvMenu.setAttribute((svgeditClipboard ? 'en' : 'dis') + 'ablemenuitems', '#paste,#paste_in_place'); + this.canvMenu.setAttribute((svgeditClipboard ? 'en' : 'dis') + 'ablemenuitems', '#paste,#paste_in_place'); } - enableOrDisableClipboard(); - - window.addEventListener('storage', function (e) { - if (e.key !== 'svgedit_clipboard') { return; } - - enableOrDisableClipboard(); - }); - - window.addEventListener('beforeunload', function (e) { - // Suppress warning if page is empty - if (undoMgr.getUndoStackSize() === 0) { - editor.showSaveWarning = false; - } - - // showSaveWarning is set to 'false' when the page is saved. - if (!editor.configObj.curConfig.no_save_warning && editor.showSaveWarning) { - // Browser already asks question about closing the page - e.returnValue = uiStrings.notification.unsavedChanges; // Firefox needs this when beforeunload set by addEventListener (even though message is not used) - return uiStrings.notification.unsavedChanges; - } - return true; - }); /** - * Expose the `uiStrings`. - * @function module:SVGEditor.canvas.getUIStrings - * @returns {module:SVGEditor.uiStrings} - */ - editor.canvas.getUIStrings = () => { - return uiStrings; - }; - - /** - * @function module:SVGEditor.openPrep + * @function module:SVGthis.openPrep * @returns {boolean|Promise} Resolves to boolean indicating `true` if there were no changes * and `false` after the user confirms. */ - editor.openPrep = () => { + openPrep () { $('#main_menu').hide(); - if (undoMgr.getUndoStackSize() === 0) { + if (this.undoMgr.getUndoStackSize() === 0) { return true; } - return confirm(uiStrings.notification.QwantToOpen); - }; + return confirm(this.uiStrings.notification.QwantToOpen); + } /** * * @param {Event} e * @returns {void} */ - function onDragEnter (e) { + // eslint-disable-next-line class-methods-use-this + onDragEnter (e) { e.stopPropagation(); e.preventDefault(); // and indicator should be displayed here, such as "drop files here" @@ -2412,7 +2282,8 @@ editor.init = () => { * @param {Event} e * @returns {void} */ - function onDragOver (e) { + // eslint-disable-next-line class-methods-use-this + onDragOver (e) { e.stopPropagation(); e.preventDefault(); } @@ -2422,136 +2293,24 @@ editor.init = () => { * @param {Event} e * @returns {void} */ - function onDragLeave (e) { + // eslint-disable-next-line class-methods-use-this + onDragLeave (e) { e.stopPropagation(); e.preventDefault(); // hypothetical indicator should be removed here } - // Use HTML5 File API: http://www.w3.org/TR/FileAPI/ - // if browser has HTML5 File API support, then we will show the open menu item - // and provide a file input to click. When that change event fires, it will - // get the text contents of the file and send it to the canvas - if (window.FileReader) { - /** - * @param {Event} e - * @returns {void} - */ - const importImage = function (e) { - $.process_cancel(uiStrings.notification.loadingImage); - e.stopPropagation(); - e.preventDefault(); - $('#main_menu').hide(); - const file = (e.type === 'drop') ? e.dataTransfer.files[0] : this.files[0]; - if (!file) { - $('#dialog_box').hide(); - return; - } - - if (!file.type.includes('image')) { - return; - } - // Detected an image - // svg handling - let reader; - if (file.type.includes('svg')) { - reader = new FileReader(); - reader.onloadend = function (ev) { - const newElement = svgCanvas.importSvgString(ev.target.result, true); - svgCanvas.ungroupSelectedElement(); - svgCanvas.ungroupSelectedElement(); - svgCanvas.groupSelectedElements(); - svgCanvas.alignSelectedElements('m', 'page'); - svgCanvas.alignSelectedElements('c', 'page'); - // highlight imported element, otherwise we get strange empty selectbox - svgCanvas.selectOnly([newElement]); - $('#dialog_box').hide(); - }; - reader.readAsText(file); - } else { - // bitmap handling - reader = new FileReader(); - reader.onloadend = function ({target: {result}}) { - /** - * Insert the new image until we know its dimensions. - * @param {Float} width - * @param {Float} height - * @returns {void} - */ - const insertNewImage = function (width, height) { - const newImage = svgCanvas.addSVGElementFromJson({ - element: 'image', - attr: { - x: 0, - y: 0, - width, - height, - id: svgCanvas.getNextId(), - style: 'pointer-events:inherit' - } - }); - svgCanvas.setHref(newImage, result); - svgCanvas.selectOnly([newImage]); - svgCanvas.alignSelectedElements('m', 'page'); - svgCanvas.alignSelectedElements('c', 'page'); - editor.topPanelHandlers.updateContextPanel(); - $('#dialog_box').hide(); - }; - // create dummy img so we know the default dimensions - let imgWidth = 100; - let imgHeight = 100; - const img = new Image(); - img.style.opacity = 0; - img.addEventListener('load', () => { - imgWidth = img.offsetWidth || img.naturalWidth || img.width; - imgHeight = img.offsetHeight || img.naturalHeight || img.height; - insertNewImage(imgWidth, imgHeight); - }); - img.src = result; - }; - reader.readAsDataURL(file); - } - }; - - workarea[0].addEventListener('dragenter', onDragEnter); - workarea[0].addEventListener('dragover', onDragOver); - workarea[0].addEventListener('dragleave', onDragLeave); - workarea[0].addEventListener('drop', importImage); - - const open = $('').change(async function (e) { - const ok = await editor.openPrep(); - if (!ok) { return; } - svgCanvas.clear(); - if (this.files.length === 1) { - $.process_cancel(uiStrings.notification.loadingImage); - const reader = new FileReader(); - reader.onloadend = async function ({target}) { - await loadSvgString(target.result); - updateCanvas(); - }; - reader.readAsText(this.files[0]); - } - }); - $('#tool_open').show(); - $(window).on('openImage', () => open.click()); - - const imgImport = $('').change(importImage); - $('#tool_import').show(); - $(window).on('importImage', () => imgImport.click()); - } - - updateCanvas(true); /** - * @function module:SVGEditor.setLang + * @function module:SVGthis.setLang * @param {string} lang The language code * @param {module:locale.LocaleStrings} allStrings See {@tutorial LocaleDocs} * @fires module:svgcanvas.SvgCanvas#event:ext_langReady * @fires module:svgcanvas.SvgCanvas#event:ext_langChanged * @returns {void} A Promise which resolves to `undefined` */ - const setLang = editor.setLang = function (lang, allStrings) { - editor.langChanged = true; - editor.pref('lang', lang); + setLang (lang, allStrings) { + this.langChanged = true; + this.pref('lang', lang); const $editDialog = document.getElementById('se-edit-prefs'); $editDialog.setAttribute('lang', lang); if (!allStrings) { @@ -2563,32 +2322,32 @@ editor.init = () => { // Todo: Add proper RTL Support! // Todo: Use RTL detection instead and take out of locales? // document.documentElement.dir = allStrings.dir; - $.extend(uiStrings, allStrings); + $.extend(this.uiStrings, allStrings); // const notif = allStrings.notification; // Currently unused // $.extend will only replace the given strings const oldLayerName = $('#layerlist tr.layersel td.layername').text(); - const renameLayer = (oldLayerName === uiStrings.common.layer + ' 1'); + const renameLayer = (oldLayerName === this.uiStrings.common.layer + ' 1'); - svgCanvas.setUiStrings(allStrings); - Actions.setTitles(); + this.svgCanvas.setUiStrings(allStrings); + this.setTitles(); if (renameLayer) { - svgCanvas.renameCurrentLayer(uiStrings.common.layer + ' 1'); - editor.layersPanel.populateLayers(); + this.svgCanvas.renameCurrentLayer(this.uiStrings.common.layer + ' 1'); + this.layersPanel.populateLayers(); } - svgCanvas.runExtensions('langChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_langChanged} */ lang); + this.svgCanvas.runExtensions('langChanged', /** @type {module:svgcanvas.SvgCanvas#event:ext_langChanged} */ lang); // Copy title for certain tool elements - const elems = { + this.elems = { '#stroke_color': '#tool_stroke .icon_label, #tool_stroke .color_block', '#fill_color': '#tool_fill label, #tool_fill .color_block', '#linejoin_miter': '#cur_linejoin', '#linecap_butt': '#cur_linecap' }; - $.each(elems, function (source, dest) { + $.each(this.elems, function (source, dest) { $(dest).attr('title', $(source)[0].title); }); @@ -2596,82 +2355,78 @@ editor.init = () => { $('#multiselected_panel div[id^=tool_align]').each(() => { $('#tool_pos' + this.id.substr(10))[0].title = this.title; }); - }; + } - // Load extensions - extAndLocaleFunc(); -}; - -/** -* @callback module:SVGEditor.ReadyCallback + /** +* @callback module:SVGthis.ReadyCallback * @returns {Promise|void} */ -/** + /** * Queues a callback to be invoked when the editor is ready (or * to be invoked immediately if it is already ready--i.e., * if `runCallbacks` has been run). -* @function module:SVGEditor.ready -* @param {module:SVGEditor.ReadyCallback} cb Callback to be queued to invoke +* @function module:SVGthis.ready +* @param {module:SVGthis.ReadyCallback} cb Callback to be queued to invoke * @returns {Promise} Resolves when all callbacks, including the supplied have resolved */ -editor.ready = function (cb) { // eslint-disable-line promise/prefer-await-to-callbacks - return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new - if (isReady) { - resolve(cb()); // eslint-disable-line node/callback-return, promise/prefer-await-to-callbacks - return; - } - callbacks.push([cb, resolve, reject]); - }); -}; + ready (cb) { // eslint-disable-line promise/prefer-await-to-callbacks + return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new + if (this.isReady) { + resolve(cb()); // eslint-disable-line node/callback-return, promise/prefer-await-to-callbacks + return; + } + this.callbacks.push([cb, resolve, reject]); + }); + } -/** -* Invokes the callbacks previous set by `svgEditor.ready` -* @function module:SVGEditor.runCallbacks + /** +* Invokes the callbacks previous set by `svgthis.ready` +* @function module:SVGthis.runCallbacks * @returns {Promise} Resolves to `undefined` if all callbacks succeeded and rejects otherwise */ -editor.runCallbacks = async () => { - try { - await Promise.all(callbacks.map(([cb]) => { - return cb(); // eslint-disable-line promise/prefer-await-to-callbacks - })); - } catch (err) { - callbacks.forEach(([, , reject]) => { - reject(); + async runCallbacks () { + try { + await Promise.all(this.callbacks.map(([cb]) => { + return cb(); // eslint-disable-line promise/prefer-await-to-callbacks + })); + } catch (err) { + this.callbacks.forEach(([, , reject]) => { + reject(); + }); + throw err; + } + this.callbacks.forEach(([, resolve]) => { + resolve(); }); - throw err; + this.isReady = true; } - callbacks.forEach(([, resolve]) => { - resolve(); - }); - isReady = true; -}; -/** - * @function module:SVGEditor.loadFromString + /** + * @function module:SVGthis.loadFromString * @param {string} str The SVG string to load * @param {PlainObject} [opts={}] * @param {boolean} [opts.noAlert=false] Option to avoid alert to user and instead get rejected promise * @returns {Promise} */ -editor.loadFromString = function (str, {noAlert} = {}) { - return editor.ready(async () => { - try { - await loadSvgString(str, {noAlert}); - } catch (err) { - if (noAlert) { - throw err; + loadFromString (str, {noAlert} = {}) { + return this.ready(async () => { + try { + await this.loadSvgString(str, {noAlert}); + } catch (err) { + if (noAlert) { + throw err; + } } - } - }); -}; + }); + } -/** - * @callback module:SVGEditor.URLLoadCallback + /** + * @callback module:SVGthis.URLLoadCallback * @param {boolean} success * @returns {void} */ -/** - * @function module:SVGEditor.loadFromURL + /** + * @function module:SVGthis.loadFromURL * @param {string} url URL from which to load an SVG string via Ajax * @param {PlainObject} [opts={}] May contain properties: `cache`, `callback` * @param {boolean} [opts.cache] @@ -2680,85 +2435,83 @@ editor.loadFromString = function (str, {noAlert} = {}) { * the SVG (or upon failure to parse the loaded string) when `noAlert` is * enabled */ -editor.loadFromURL = function (url, {cache, noAlert} = {}) { - return editor.ready(() => { - return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new - $.ajax({ - url, - dataType: 'text', - cache: Boolean(cache), - beforeSend () { - $.process_cancel(uiStrings.notification.loadingImage); - }, - success (str) { - loadSvgString(str, {noAlert}); - }, - error (xhr, stat, err) { - if (xhr.status !== 404 && xhr.responseText) { - loadSvgString(xhr.responseText, {noAlert}); - return; + loadFromURL (url, {cache, noAlert} = {}) { + return this.ready(() => { + return new Promise((resolve, reject) => { // eslint-disable-line promise/avoid-new + $.ajax({ + url, + dataType: 'text', + cache: Boolean(cache), + beforeSend () { + $.process_cancel(this.uiStrings.notification.loadingImage); + }, + success (str) { + this.loadSvgString(str, {noAlert}); + }, + error (xhr, stat, err) { + if (xhr.status !== 404 && xhr.responseText) { + this.loadSvgString(xhr.responseText, {noAlert}); + return; + } + if (noAlert) { + reject(new Error('URLLoadFail')); + return; + } + alert(this.uiStrings.notification.URLLoadFail + ': \n' + err); + resolve(); + }, + complete () { + $('#dialog_box').hide(); } - if (noAlert) { - reject(new Error('URLLoadFail')); - return; - } - alert(uiStrings.notification.URLLoadFail + ': \n' + err); - resolve(); - }, - complete () { - $('#dialog_box').hide(); - } + }); }); }); - }); -}; + } -/** -* @function module:SVGEditor.loadFromDataURI + /** +* @function module:SVGthis.loadFromDataURI * @param {string} str The Data URI to base64-decode (if relevant) and load * @param {PlainObject} [opts={}] * @param {boolean} [opts.noAlert] * @returns {Promise} Resolves to `undefined` and rejects if loading SVG string fails and `noAlert` is enabled */ -editor.loadFromDataURI = function (str, {noAlert} = {}) { - return editor.ready(() => { - let base64 = false; - let pre = str.match(/^data:image\/svg\+xml;base64,/); - if (pre) { - base64 = true; - } else { - pre = str.match(/^data:image\/svg\+xml(?:;|;utf8)?,/); - } - if (pre) { - pre = pre[0]; - } - const src = str.slice(pre.length); - return loadSvgString(base64 ? decode64(src) : decodeURIComponent(src), {noAlert}); - }); -}; + loadFromDataURI (str, {noAlert} = {}) { + return this.ready(() => { + let base64 = false; + let pre = str.match(/^data:image\/svg\+xml;base64,/); + if (pre) { + base64 = true; + } else { + pre = str.match(/^data:image\/svg\+xml(?:;|;utf8)?,/); + } + if (pre) { + pre = pre[0]; + } + const src = str.slice(pre.length); + return this.loadSvgString(base64 ? decode64(src) : decodeURIComponent(src), {noAlert}); + }); + } -/** - * @function module:SVGEditor.addExtension + /** + * @function module:SVGthis.addExtension * @param {string} name Used internally; no need for i18n. * @param {module:svgcanvas.ExtensionInitCallback} init Config to be invoked on this module * @param {module:svgcanvas.ExtensionInitArgs} initArgs * @throws {Error} If called too early * @returns {Promise} Resolves to `undefined` */ -editor.addExtension = (name, init, initArgs) => { - // Note that we don't want this on editor.ready since some extensions + addExtension (name, init, initArgs) { + // Note that we don't want this on this.ready since some extensions // may want to run before then (like server_opensave). - if (!svgCanvas) { - throw new Error('Extension added too early'); + if (!this.svgCanvas) { + throw new Error('Extension added too early'); + } + return this.svgCanvas.addExtension.call(this, name, init, initArgs); } - return svgCanvas.addExtension.call(editor, name, init, initArgs); -}; +} -// Defer injection to wait out initial menu processing. This probably goes -// away once all context menu behavior is brought to context menu. -editor.ready(() => { - injectExtendedContextMenuItemsIntoDom(); -}); +const editor = new Editor(); +editor.init(); let extensionsAdded = false; const messageQueue = []; @@ -2777,22 +2530,8 @@ const messageListener = ({data, origin}) => { // eslint-disable-line no-shadow } else { // Extensions can handle messages at this stage with their own // canvas `message` listeners - svgCanvas.call('message', messageObj); + this.svgCanvas.call('message', messageObj); } }; window.addEventListener('message', messageListener); - -// Run init once DOM is loaded -// jQuery(editor.init); - -(async () => { -try { - // We wait a micro-task to let the svgEditor variable be defined for module checks - await Promise.resolve(); - editor.init(); -} catch (err) { - console.error(err); // eslint-disable-line no-console -} -})(); - export default editor;