396 lines
12 KiB
JavaScript
396 lines
12 KiB
JavaScript
/**
|
|
* 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 () { /* empty */ }); // 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))
|
|
) {
|
|
// eslint-disable-next-line no-console -- Info for developers
|
|
console.error(
|
|
`The origin ${e.origin} was not whitelisted as an origin from ` +
|
|
`which responses may be received by this ${window.origin} script.`
|
|
);
|
|
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:
|
|
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.
|
|
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;
|