/** * Handles underlying communication between the embedding window and the * editor frame. * @module EmbeddedSVGEdit */ let cbid = 0; /** * @callback module:EmbeddedSVGEdit.CallbackSetter * @param {GenericCallback} newCallback Callback to be stored (signature dependent on function) * @returns {void} */ /** * @callback module:EmbeddedSVGEdit.CallbackSetGetter * @param {...any} args Signature dependent on the function * @returns {module:EmbeddedSVGEdit.CallbackSetter} */ /** * @param {string} funcName * @returns {module:EmbeddedSVGEdit.CallbackSetGetter} */ function getCallbackSetter (funcName) { return function (...args) { const that = this, // New callback callbackID = this.send(funcName, args, function () { /* */ }); // The callback (currently it's nothing, but will be set later) return function (newCallback) { that.callbacks[callbackID] = newCallback; // Set callback }; }; } /** * Having this separate from messageListener allows us to * avoid using JSON parsing (and its limitations) in the case * of same domain control. * @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value * @param {PlainObject} data * @param {JSON} data.result * @param {string} data.error * @param {Integer} data.id * @returns {void} */ function addCallback (t, {result, error, id: callbackID}) { if (typeof callbackID === 'number' && t.callbacks[callbackID]) { // These should be safe both because we check `cbid` is numeric and // because the calls are from trusted origins if (result) { t.callbacks[callbackID](result); // lgtm [js/unvalidated-dynamic-method-call] } else { t.callbacks[callbackID](error, 'error'); // lgtm [js/unvalidated-dynamic-method-call] } } } /** * @param {Event} e * @returns {void} */ function messageListener (e) { // We accept and post strings as opposed to objects for the sake of IE9 support; this // will most likely be changed in the future if (!e.data || !['string', 'object'].includes(typeof e.data)) { return; } const {allowedOrigins} = this, data = typeof e.data === 'object' ? e.data : JSON.parse(e.data); if (!data || typeof data !== 'object' || data.namespace !== 'svg-edit' || e.source !== this.frame.contentWindow || (!allowedOrigins.includes('*') && !allowedOrigins.includes(e.origin)) ) { console.log(`The origin ${e.origin} was not whitelisted as an origin from which responses may be received by this ${window.origin} script.`); // eslint-disable-line no-console return; } addCallback(this, data); } /** * @callback module:EmbeddedSVGEdit.MessageListener * @param {MessageEvent} e * @returns {void} */ /** * @param {module:EmbeddedSVGEdit.EmbeddedSVGEdit} t The `this` value * @returns {module:EmbeddedSVGEdit.MessageListener} Event listener */ function getMessageListener (t) { return function (e) { messageListener.call(t, e); }; } /** * Embedded SVG-edit API. * General usage: * - Have an iframe somewhere pointing to a version of svg-edit > r1000. * @example // Initialize the magic with: const svgCanvas = new EmbeddedSVGEdit(window.frames.svgedit); // Pass functions in this format: svgCanvas.setSvgString('string'); // Or if a callback is needed: svgCanvas.setSvgString('string')(function (data, error) { if (error) { // There was an error } else { // Handle data } }); // Everything is done with the same API as the real svg-edit, // and all documentation is unchanged. // However, this file depends on the postMessage API which // can only support JSON-serializable arguments and // return values, so, for example, arguments whose value is // 'undefined', a function, a non-finite number, or a built-in // object like Date(), RegExp(), etc. will most likely not behave // as expected. In such a case one may need to host // the SVG editor on the same domain and reference the // JavaScript methods on the frame itself. // The only other difference is when handling returns: // the callback notation is used instead. const blah = new EmbeddedSVGEdit(window.frames.svgedit); blah.clearSelection('woot', 'blah', 1337, [1, 2, 3, 4, 5, 'moo'], -42, { a: 'tree', b: 6, c: 9 })(function () { console.log('GET DATA', args); }); * * @memberof module:EmbeddedSVGEdit */ class EmbeddedSVGEdit { /** * @param {HTMLIFrameElement} frame * @param {string[]} [allowedOrigins=[]] Array of origins from which incoming * messages will be allowed when same origin is not used; defaults to none. * If supplied, it should probably be the same as svgEditor's allowedOrigins */ constructor (frame, allowedOrigins) { const that = this; this.allowedOrigins = allowedOrigins || []; // Initialize communication this.frame = frame; this.callbacks = {}; // List of functions extracted with this: // Run in firebug on http://svg-edit.googlecode.com/svn/trunk/docs/files/svgcanvas-js.html // for (const i=0,q=[],f = document.querySelectorAll('div.CFunction h3.CTitle a'); i < f.length; i++) { q.push(f[i].name); }; q // const functions = ['clearSelection', 'addToSelection', 'removeFromSelection', 'open', 'save', 'getSvgString', 'setSvgString', // 'createLayer', 'deleteCurrentLayer', 'setCurrentLayer', 'renameCurrentLayer', 'setCurrentLayerPosition', 'setLayerVisibility', // 'moveSelectedToLayer', 'clear']; // Newer, well, it extracts things that aren't documented as well. All functions accessible through the normal thingy can now be accessed though the API // const {svgCanvas} = frame.contentWindow; // const l = []; // for (const i in svgCanvas) { if (typeof svgCanvas[i] === 'function') { l.push(i);} }; // alert("['" + l.join("', '") + "']"); // Run in svgedit itself const functions = [ 'addExtension', 'addSVGElementFromJson', 'addToSelection', 'alignSelectedElements', 'assignAttributes', 'bind', 'call', 'changeSelectedAttribute', 'cleanupElement', 'clear', 'clearSelection', 'clearSvgContentElement', 'cloneLayer', 'cloneSelectedElements', 'convertGradients', 'convertToGroup', 'convertToNum', 'convertToPath', 'copySelectedElements', 'createLayer', 'cutSelectedElements', 'cycleElement', 'deleteCurrentLayer', 'deleteSelectedElements', 'embedImage', 'exportPDF', 'findDefs', 'getBBox', 'getBlur', 'getBold', 'getColor', 'getContentElem', 'getCurrentDrawing', 'getDocumentTitle', 'getEditorNS', 'getElem', 'getFillOpacity', 'getFontColor', 'getFontFamily', 'getFontSize', 'getHref', 'getId', 'getIntersectionList', 'getItalic', 'getMode', 'getMouseTarget', 'getNextId', 'getOffset', 'getOpacity', 'getPaintOpacity', 'getPrivateMethods', 'getRefElem', 'getResolution', 'getRootElem', 'getRotationAngle', 'getSelectedElems', 'getStrokeOpacity', 'getStrokeWidth', 'getStrokedBBox', 'getStyle', 'getSvgString', 'getText', 'getTitle', 'getTransformList', 'getUIStrings', 'getUrlFromAttr', 'getVersion', 'getVisibleElements', 'getVisibleElementsAndBBoxes', 'getZoom', 'groupSelectedElements', 'groupSvgElem', 'hasMatrixTransform', 'identifyLayers', 'importSvgString', 'leaveContext', 'linkControlPoints', 'makeHyperlink', 'matrixMultiply', 'mergeAllLayers', 'mergeLayer', 'moveSelectedElements', 'moveSelectedToLayer', 'moveToBottomSelectedElement', 'moveToTopSelectedElement', 'moveUpDownSelected', 'open', 'pasteElements', 'prepareSvg', 'pushGroupProperties', 'randomizeIds', 'rasterExport', 'ready', 'recalculateAllSelectedDimensions', 'recalculateDimensions', 'remapElement', 'removeFromSelection', 'removeHyperlink', 'removeUnusedDefElems', 'renameCurrentLayer', 'round', 'runExtensions', 'sanitizeSvg', 'save', 'selectAllInCurrentLayer', 'selectOnly', 'setBBoxZoom', 'setBackground', 'setBlur', 'setBlurNoUndo', 'setBlurOffsets', 'setBold', 'setColor', 'setConfig', 'setContext', 'setCurrentLayer', 'setCurrentLayerPosition', 'setDocumentTitle', 'setFillPaint', 'setFontColor', 'setFontFamily', 'setFontSize', 'setGoodImage', 'setGradient', 'setGroupTitle', 'setHref', 'setIdPrefix', 'setImageURL', 'setItalic', 'setLayerVisibility', 'setLinkURL', 'setMode', 'setOpacity', 'setPaint', 'setPaintOpacity', 'setRectRadius', 'setResolution', 'setRotationAngle', 'setSegType', 'setStrokeAttr', 'setStrokePaint', 'setStrokeWidth', 'setSvgString', 'setTextContent', 'setUiStrings', 'setUseData', 'setZoom', 'svgCanvasToString', 'svgToString', 'transformListToTransform', 'ungroupSelectedElement', 'uniquifyElems', 'updateCanvas', 'zoomChanged' ]; // TODO: rewrite the following, it's pretty scary. for (const func of functions) { this[func] = getCallbackSetter(func); } // Older IE may need a polyfill for addEventListener, but so it would for SVG window.addEventListener('message', getMessageListener(this)); window.addEventListener('keydown', (e) => { const {type, key} = e; if (key === 'Backspace') { e.preventDefault(); const keyboardEvent = new KeyboardEvent(type, {key}); that.frame.contentDocument.dispatchEvent(keyboardEvent); } }); } /** * @param {string} name * @param {ArgumentsArray} args Signature dependent on function * @param {GenericCallback} callback (This may be better than a promise in case adding an event.) * @returns {Integer} */ send (name, args, callback) { // eslint-disable-line promise/prefer-await-to-callbacks const that = this; cbid++; this.callbacks[cbid] = callback; setTimeout((function (callbackID) { return function () { // Delay for the callback to be set in case its synchronous /* * Todo: Handle non-JSON arguments and return values (undefined, * nonfinite numbers, functions, and built-in objects like Date, * RegExp), etc.? Allow promises instead of callbacks? Review * SVG-Edit functions for whether JSON-able parameters can be * made compatile with all API functionality */ // We accept and post strings for the sake of IE9 support let sameOriginWithGlobal = false; try { sameOriginWithGlobal = window.location.origin === that.frame.contentWindow.location.origin && that.frame.contentWindow.svgEditor.canvas; } catch (err) {} if (sameOriginWithGlobal) { // Although we do not really need this API if we are working same // domain, it could allow us to write in a way that would work // cross-domain as well, assuming we stick to the argument limitations // of the current JSON-based communication API (e.g., not passing // callbacks). We might be able to address these shortcomings; see // the todo elsewhere in this file. const message = {id: callbackID}, {svgEditor: {canvas: svgCanvas}} = that.frame.contentWindow; try { message.result = svgCanvas[name](...args); } catch (err) { message.error = err.message; } addCallback(that, message); } else { // Requires the ext-xdomain-messaging.js extension that.frame.contentWindow.postMessage(JSON.stringify({ namespace: 'svgCanvas', id: callbackID, name, args }), '*'); } }; }(cbid)), 0); return cbid; } } export default EmbeddedSVGEdit;