From c496ab89782458930b9e58202dde2abc0d31c6f0 Mon Sep 17 00:00:00 2001 From: JFH <20402845+jfhenon@users.noreply.github.com> Date: Sat, 2 Jan 2021 23:54:08 +0100 Subject: [PATCH] move init (600 lines+) in its own file --- src/editor/EditorStartup.js | 855 ++++++++++++++++++++++++++++++++++++ src/editor/svgedit.js | 844 +---------------------------------- 2 files changed, 862 insertions(+), 837 deletions(-) create mode 100644 src/editor/EditorStartup.js diff --git a/src/editor/EditorStartup.js b/src/editor/EditorStartup.js new file mode 100644 index 00000000..6426e961 --- /dev/null +++ b/src/editor/EditorStartup.js @@ -0,0 +1,855 @@ +/* globals $ seConfirm seAlert */ +import './touch.js'; +import {convertUnit} from '../common/units.js'; +import { + hasCustomHandler, getCustomHandler, injectExtendedContextMenuItemsIntoDom +} from './contextmenu.js'; + +import SvgCanvas from '../svgcanvas/svgcanvas.js'; +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'; + +/** + * @fires module:svgcanvas.SvgCanvas#event:svgEditorReady + * @returns {void} + */ +const readySignal = () => { + // let the opener know SVG Edit is ready (now that config is set up) + const w = window.opener || window.parent; + if (w) { + try { + /** + * Triggered on a containing `document` (of `window.opener` + * or `window.parent`) when the editor is loaded. + * @event module:SVGEditor#event:svgEditorReadyEvent + * @type {Event} + * @property {true} bubbles + * @property {true} cancelable + */ + /** + * @name module:SVGthis.svgEditorReadyEvent + * @type {module:SVGEditor#event:svgEditorReadyEvent} + */ + const svgEditorReadyEvent = new w.CustomEvent('svgEditorReady', { + bubbles: true, + cancelable: true + }); + w.document.documentElement.dispatchEvent(svgEditorReadyEvent); + } catch (e) {} + } +}; + +const {$id} = SvgCanvas; + +/** + * + */ +class EditorStartup { + /** + * + */ + constructor () { + this.extensionsAdded = false; + this.messageQueue = []; + } + /** + * Auto-run after a Promise microtask. + * @function module:SVGthis.init + * @returns {void} + */ + async init () { + try { + // 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); + // alertDialog added to DOM + const alertBox = document.createElement('se-alert-dialog'); + alertBox.setAttribute('id', 'se-alert-dialog'); + document.body.append(alertBox); + // promptDialog added to DOM + const promptBox = document.createElement('se-prompt-dialog'); + promptBox.setAttribute('id', 'se-prompt-dialog'); + document.body.append(promptBox); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + + if ('localStorage' in window) { // && onWeb removed so Webkit works locally + this.storage = window.localStorage; + } + + 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 + 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) { + seAlert(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.configObj.pref('bkgd_color'), this.configObj.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.configObj.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 = seConfirm(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.addEventListener('resize', (evt) => { + Object.entries(winWh).forEach(([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(() => { + seAlert(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} imageWidth + * @param {Float} imageHeight + * @returns {void} + */ + const insertNewImage = function (imageWidth, imageHeight) { + const newImage = this.svgCanvas.addSVGElementFromJson({ + element: 'image', + attr: { + x: 0, + y: 0, + width: imageWidth, + height: imageHeight, + 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(); + }); + // run callbacks stored by this.ready + await this.runCallbacks(); + window.addEventListener('message', this.messageListener.bind(this)); + } + /** + * @fires module:svgcanvas.SvgCanvas#event:ext_addLangData + * @fires module:svgcanvas.SvgCanvas#event:ext_langReady + * @fires module:svgcanvas.SvgCanvas#event:ext_langChanged + * @fires module:svgcanvas.SvgCanvas#event:extensions_added + * @returns {Promise} Resolves to result of {@link module:locale.readLang} + */ + async extAndLocaleFunc () { + const {langParam, langData} = await this.putLocale(this.configObj.pref('lang'), this.goodLangs); + await this.setLang(langParam, langData); + + $id('svg_container').style.visibility = 'visible'; + + try { + // load standard extensions + await Promise.all( + this.configObj.curConfig.extensions.map(async (extname) => { + /** + * @tutorial ExtensionDocs + * @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:SVGthis.ExtensionObject} + */ + const imported = await import(`./extensions/${encodeURIComponent(extname)}/${encodeURIComponent(extname)}.js`); + const {name = extname, init: initfn} = imported.default; + return this.addExtension(name, (initfn && initfn.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 + return undefined; + } + }) + ); + // load user extensions (given as pathNames) + await Promise.all( + this.configObj.curConfig.userExtensions.map(async (extPathName) => { + /** + * @tutorial ExtensionDocs + * @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:SVGthis.ExtensionObject} + */ + const imported = await import(encodeURI(extPathName)); + const {name, init: initfn} = imported.default; + return this.addExtension(name, (initfn && initfn.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 + return undefined; + } + }) + ); + this.svgCanvas.bind( + 'extensions_added', + /** + * @param {external:Window} win + * @param {module:svgcanvas.SvgCanvas#event:extensions_added} data + * @listens module:SvgCanvas#event:extensions_added + * @returns {void} + */ + (win, data) => { + this.extensionsAdded = true; + this.setAll(); + + if (this.storagePromptState === 'ignore') { + this.updateCanvas(true); + } + + this.messageQueue.forEach( + /** + * @param {module:svgcanvas.SvgCanvas#event:message} messageObj + * @fires module:svgcanvas.SvgCanvas#event:message + * @returns {void} + */ + (messageObj) => { + this.svgCanvas.call('message', messageObj); + } + ); + } + ); + this.svgCanvas.call('extensions_added'); + } catch (err) { + // Todo: Report errors through the UI + console.log(err); // eslint-disable-line no-console + } + } + + /** + * @param {PlainObject} info + * @param {any} info.data + * @param {string} info.origin + * @fires module:svgcanvas.SvgCanvas#event:message + * @returns {void} + */ + messageListener ({data, origin}) { // eslint-disable-line no-shadow + // console.log('data, origin, extensionsAdded', data, origin, extensionsAdded); + const messageObj = {data, origin}; + if (!this.extensionsAdded) { + this.messageQueue.push(messageObj); + } else { + // Extensions can handle messages at this stage with their own + // canvas `message` listeners + this.svgCanvas.call('message', messageObj); + } + } +} + +export default EditorStartup; diff --git a/src/editor/svgedit.js b/src/editor/svgedit.js index c7aabab3..36009fae 100644 --- a/src/editor/svgedit.js +++ b/src/editor/svgedit.js @@ -19,24 +19,18 @@ import './touch.js'; import {isChrome, isGecko, isMac} from '../common/browser.js'; import {convertUnit, isValidUnit} from '../common/units.js'; -import { - hasCustomHandler, getCustomHandler, injectExtendedContextMenuItemsIntoDom -} from './contextmenu.js'; import SvgCanvas from '../svgcanvas/svgcanvas.js'; import jQueryPluginJSHotkeys from './js-hotkeys/jquery.hotkeys.min.js'; import ConfigObj from './ConfigObj.js'; -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'; +import EditorStartup from './EditorStartup.js'; + const {$id, $qa, isNullish, encode64, decode64, blankPageObjectURL} = SvgCanvas; // JFH hotkey is used for text input. @@ -45,11 +39,12 @@ const homePage = 'https://github.com/SVG-Edit/svgedit'; /** * */ -class Editor { +class Editor extends EditorStartup { /** * */ constructor () { + super(); /** * @type {Float} */ @@ -341,129 +336,7 @@ class Editor { return btn.sel === sel; }); } - /** - * @fires module:svgcanvas.SvgCanvas#event:ext_addLangData - * @fires module:svgcanvas.SvgCanvas#event:ext_langReady - * @fires module:svgcanvas.SvgCanvas#event:ext_langChanged - * @fires module:svgcanvas.SvgCanvas#event:extensions_added - * @returns {Promise} Resolves to result of {@link module:locale.readLang} - */ - async extAndLocaleFunc () { - const {langParam, langData} = await this.putLocale(this.configObj.pref('lang'), this.goodLangs); - await this.setLang(langParam, langData); - $id('svg_container').style.visibility = 'visible'; - - try { - // load standard extensions - await Promise.all( - this.configObj.curConfig.extensions.map(async (extname) => { - /** - * @tutorial ExtensionDocs - * @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:SVGthis.ExtensionObject} - */ - const imported = await import(`./extensions/${encodeURIComponent(extname)}/${encodeURIComponent(extname)}.js`); - const {name = extname, init} = imported.default; - 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 - return undefined; - } - }) - ); - // load user extensions (given as pathNames) - await Promise.all( - this.configObj.curConfig.userExtensions.map(async (extPathName) => { - /** - * @tutorial ExtensionDocs - * @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:SVGthis.ExtensionObject} - */ - const imported = await import(encodeURI(extPathName)); - const {name, init} = imported.default; - 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 - return undefined; - } - }) - ); - this.svgCanvas.bind( - 'extensions_added', - /** - * @param {external:Window} win - * @param {module:svgcanvas.SvgCanvas#event:extensions_added} data - * @listens module:SvgCanvas#event:extensions_added - * @returns {void} - */ - (win, data) => { - extensionsAdded = true; - this.setAll(); - - if (this.storagePromptState === 'ignore') { - this.updateCanvas(true); - } - - messageQueue.forEach( - /** - * @param {module:svgcanvas.SvgCanvas#event:message} messageObj - * @fires module:svgcanvas.SvgCanvas#event:message - * @returns {void} - */ - (messageObj) => { - this.svgCanvas.call('message', messageObj); - } - ); - } - ); - this.svgCanvas.call('extensions_added'); - } catch (err) { - // Todo: Report errors through the UI - console.log(err); // eslint-disable-line no-console - } - } - /** - * @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) { - try { - /** - * Triggered on a containing `document` (of `window.opener` - * or `window.parent`) when the editor is loaded. - * @event module:SVGEditor#event:svgEditorReadyEvent - * @type {Event} - * @property {true} bubbles - * @property {true} cancelable - */ - /** - * @name module:SVGthis.svgEditorReadyEvent - * @type {module:SVGEditor#event:svgEditorReadyEvent} - */ - const svgEditorReadyEvent = new w.CustomEvent('svgEditorReady', { - bubbles: true, - cancelable: true - }); - w.document.documentElement.dispatchEvent(svgEditorReadyEvent); - } catch (e) {} - } - } /** * Expose the `uiStrings`. * @function module:SVGthis.canvas.getUIStrings @@ -473,688 +346,6 @@ class Editor { return this.uiStrings; } - /** - * Auto-run after a Promise microtask. - * @function module:SVGthis.init - * @returns {void} - */ - async init () { - try { - // 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); - // alertDialog added to DOM - const alertBox = document.createElement('se-alert-dialog'); - alertBox.setAttribute('id', 'se-alert-dialog'); - document.body.append(alertBox); - // promptDialog added to DOM - const promptBox = document.createElement('se-prompt-dialog'); - promptBox.setAttribute('id', 'se-prompt-dialog'); - document.body.append(promptBox); - } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - } - - if ('localStorage' in window) { // && onWeb removed so Webkit works locally - this.storage = window.localStorage; - } - - 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) { - seAlert(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.configObj.pref('bkgd_color'), this.configObj.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.configObj.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 = seConfirm(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.addEventListener('resize', (evt) => { - Object.entries(winWh).forEach(([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(() => { - seAlert(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(); - }); - // run callbacks stored by this.ready - await this.runCallbacks(); - } - /** * @param {boolean} editmode * @param {module:svgcanvas.SvgCanvas#event:selected} elems @@ -2499,43 +1690,22 @@ class Editor { /** * @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.ExtensionInitCallback} initfn Config to be invoked on this module * @param {module:svgcanvas.ExtensionInitArgs} initArgs * @throws {Error} If called too early * @returns {Promise} Resolves to `undefined` */ - addExtension (name, init, initArgs) { + addExtension (name, initfn, initArgs) { // Note that we don't want this on this.ready since some extensions // may want to run before then (like server_opensave). if (!this.svgCanvas) { throw new Error('Extension added too early'); } - return this.svgCanvas.addExtension.call(this, name, init, initArgs); + return this.svgCanvas.addExtension.call(this, name, initfn, initArgs); } } const editor = new Editor(); editor.init(); -let extensionsAdded = false; -const messageQueue = []; -/** - * @param {PlainObject} info - * @param {any} info.data - * @param {string} info.origin - * @fires module:svgcanvas.SvgCanvas#event:message - * @returns {void} - */ -const messageListener = ({data, origin}) => { // eslint-disable-line no-shadow - // console.log('data, origin, extensionsAdded', data, origin, extensionsAdded); - const messageObj = {data, origin}; - if (!extensionsAdded) { - messageQueue.push(messageObj); - } else { - // Extensions can handle messages at this stage with their own - // canvas `message` listeners - this.svgCanvas.call('message', messageObj); - } -}; -window.addEventListener('message', messageListener); export default editor;