/*globals globalStorage, widget, svgEditor, svgedit, canvg, DOMParser, FileReader, jQuery, $, SVGEditConfig */
/*jslint vars: true, eqeq: true, todo: true, forin: true, continue: true, regexp: true */
/*
* svg-editor.js
*
* Licensed under the MIT License
*
* Copyright(c) 2010 Alexis Deveria
* Copyright(c) 2010 Pavol Rusnak
* Copyright(c) 2010 Jeff Schiller
* Copyright(c) 2010 Narendra Sisodiya
*
*/
// Dependencies:
// 1) units.js
// 2) browser.js
// 3) svgcanvas.js
(function() {
if (window.svgEditor) {
return;
}
window.svgEditor = (function($) {
var svgCanvas,
Editor = {},
urldata,
isReady = false,
defaultPrefs = {
lang: 'en',
iconsize: 'm',
bkgd_color: '#FFF',
bkgd_url: '',
img_save: 'embed'
},
curPrefs = {},
// Note: Difference between Prefs and Config is that Prefs can be
// changed in the UI and are stored in the browser, config can not
curConfig = {
canvasName: 'default',
canvas_expansion: 3,
dimensions: [640,480],
initFill: {
color: 'FF0000', // solid red
opacity: 1
},
initStroke: {
width: 5,
color: '000000', // solid black
opacity: 1
},
initOpacity: 1,
imgPath: 'images/',
langPath: 'locale/',
extPath: 'extensions/',
jGraduatePath: 'jgraduate/images/',
extensions: [
'ext-overview_window.js',
'ext-markers.js',
'ext-connector.js',
'ext-eyedropper.js',
'ext-shapes.js',
'ext-imagelib.js',
'ext-grid.js',
'ext-polygon.js',
'ext-star.js',
'ext-panning.js',
'ext-storage.js'
],
initTool: 'select',
wireframe: false,
colorPickerCSS: null,
gridSnapping: false,
gridColor: '#000',
baseUnit: 'px',
snappingStep: 10,
showRulers: true,
showlayers: false,
no_save_warning: false,
showGrid: false, // Set by ext-grid.js
noStorageOnLoad: false,
emptyStorageOnDecline: false, // Used by ext-storage.js
prefsLoaded: false,
langChanged: false
},
uiStrings = Editor.uiStrings = {
common: {
ok: 'OK',
cancel: 'Cancel',
key_up: 'Up',
key_down: 'Down',
key_backspace: 'Backspace',
key_del: 'Del'
},
// This is needed if the locale is English, since the locale strings are not read in that instance.
layers: {
layer: 'Layer'
},
notification: {
invalidAttrValGiven: 'Invalid value given',
noContentToFitTo: 'No content to fit to',
dupeLayerName: 'There is already a layer named that!',
enterUniqueLayerName: 'Please enter a unique layer name',
enterNewLayerName: 'Please enter the new layer name',
layerHasThatName: 'Layer already has that name',
QmoveElemsToLayer: 'Move selected elements to layer \'%s\'?',
QwantToClear: 'Do you want to clear the drawing?\nThis will also erase your undo history!',
QwantToOpen: 'Do you want to open a new file?\nThis will also erase your undo history!',
QerrorsRevertToSource: 'There were parsing errors in your SVG source.\nRevert back to original SVG source?',
QignoreSourceChanges: 'Ignore changes made to SVG source?',
featNotSupported: 'Feature not supported',
enterNewImgURL: 'Enter the new image URL',
defsFailOnSave: 'NOTE: Due to a bug in your browser, this image may appear wrong (missing gradients or elements). It will however appear correct once actually saved.',
loadingImage: 'Loading image, please wait...',
saveFromBrowser: 'Select \'Save As...\' in your browser to save this image as a %s file.',
noteTheseIssues: 'Also note the following issues: ',
unsavedChanges: 'There are unsaved changes.',
enterNewLinkURL: 'Enter the new hyperlink URL',
errorLoadingSVG: 'Error: Unable to load SVG data',
URLloadFail: 'Unable to load from URL',
retrieving: 'Retrieving \'%s\' ...'
}
},
customHandlers = {};
function loadSvgString(str, callback) {
var success = svgCanvas.setSvgString(str) !== false;
callback = callback || $.noop;
if (success) {
callback(true);
} else {
$.alert(uiStrings.notification.errorLoadingSVG, function() {
callback(false);
});
}
}
Editor.curPrefs = curPrefs;
Editor.curConfig = curConfig;
Editor.tool_scale = 1;
Editor.loadContentAndPrefs = function () {
if (curConfig.noStorageOnLoad && !document.cookie.match(/store=(?:prefsAndContent|prefsOnly)/)) {
return;
}
// LOAD CONTENT
if (window.localStorage && // Cookies do not have enough available memory to hold large documents
(!curConfig.noStorageOnLoad || document.cookie.match(/store=prefsAndContent/))
) {
var name = 'svgedit-' + curConfig.canvasName;
var cached = window.localStorage && window.localStorage.getItem(name);
if (cached) {
Editor.loadFromString(cached);
}
}
// LOAD PREFS
var key, storage = false;
// var host = location.hostname,
// onWeb = host && host.indexOf('.') >= 0;
// Some FF versions throw security errors here
try {
if (window.localStorage) { // && onWeb removed so Webkit works locally
storage = localStorage;
}
} catch(err) {}
Editor.storage = storage;
for (key in defaultPrefs) {
if (defaultPrefs.hasOwnProperty(key)) { // It's our own config, so we don't need to iterate up the prototype chain
var storeKey = 'svg-edit-' + key;
if (storage) {
var val = storage.getItem(storeKey);
if (val) {
curPrefs[key] = String(val); // Convert to string for FF (.value fails in Webkit)
}
}
else if (window.widget) {
curPrefs[key] = widget.preferenceForKey(storeKey);
}
else {
var result = document.cookie.match(new RegExp(storeKey + '=([^;]+)'));
curPrefs[key] = result ? decodeURIComponent(result[1]) : '';
}
}
}
curConfig.prefsLoaded = true;
};
/**
* Store and retrieve preferences
* @param {string} key The preference name to be retrieved or set
* @param {string} [val] The value. If the value supplied is missing or falsey, no change to the preference will be made.
* @returns {string} If val is missing or falsey, the value of the previously stored preference will be returned.
*/
$.pref = function(key, val) {
if (val) {
curPrefs[key] = val;
return;
}
return curPrefs[key];
};
Editor.setConfig = function(opts, noOverride) {
$.each(opts, function(key, val) {
if (opts.hasOwnProperty(key)) {
// Only allow prefs defined in defaultPrefs
if (defaultPrefs.hasOwnProperty(key)) {
if (!noOverride || ) {
$.pref(key, val);
}
}
else if (curConfig.hasOwnProperty(key)) {
if (!noOverride) {
curConfig[key] = val;
}
}
}
});
var extensions;
if (opts.extensions) {
extensions = opts.extensions;
delete opts.extensions;
}
if (extensions) {
curConfig.extensions = opts.noDefaultExtensions && curConfig. ? extensions : curConfig.extensions.concat(extensions);
// Now remove any dupes
curConfig.extensions = $.grep(curConfig.extensions, function (n, i) {
return i === curConfig.extensions.indexOf(n);
});
}
};
// Extension mechanisms may call setCustomHandlers with three functions: opts.open, opts.save, and opts.exportImage
// opts.open's responsibilities are:
// - invoke a file chooser dialog in 'open' mode
// - let user pick a SVG file
// - calls setCanvas.setSvgString() with the string contents of that file
// opts.save's 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
// opts.exportImage's 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'), "mimeType", and "quality" (for 'JPEG' and 'WEBP'
// types) to determine the proper output.
Editor.setCustomHandlers = function(opts) {
Editor.ready(function() {
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 || opts.pngsave) { // Deprecating pngsave
svgCanvas.bind('exported', opts.exportImage || opts.pngsave);
}
customHandlers = opts;
});
};
Editor.randomizeIds = function() {
svgCanvas.randomizeIds(arguments);
};
Editor.init = function() {
// For external openers
(function() {
// let the opener know SVG Edit is ready
var svgEditorReadyEvent,
w = window.opener;
if (w) {
try {
svgEditorReadyEvent = w.document.createEvent('Event');
svgEditorReadyEvent.initEvent('svgEditorReady', true, true);
w.document.documentElement.dispatchEvent(svgEditorReadyEvent);
}
catch(e) {}
}
}());
(function() {
// Load config/data from URL if given
var src, qstr;
urldata = $.deparam.querystring(true);
if (!$.isEmptyObject(urldata)) {
if (urldata.dimensions) {
urldata.dimensions = urldata.dimensions.split(',');
}
if (urldata.bkgd_color) {
urldata.bkgd_color = '#' + urldata.bkgd_color;
}
if (urldata.extensions) {
// For security reasons, disallow cross-domain or cross-folder extensions via URL
urldata.extensions = urldata.extensions.match(/[:\/\\]/) ? '' : urldata.extensions.split(',');
}
// Disallowing extension paths via URL for
// security reasons, even for same-domain
// ones given potential to interact in undesirable
// ways with other script resources
if (urldata.extPath) {
delete urldata.extPath;
}
if (urldata.jGraduatePath) {
delete urldata.jGraduatePath;
}
if (urldata.imgPath) {
delete urldata.imgPath;
}
if (urldata.langPath) {
delete urldata.langPath;
}
svgEditor.setConfig(urldata, true);
src = urldata.source;
qstr = $.param.querystring();
if (!src) { // urldata.source may have been null if it ended with '='
if (qstr.indexOf('source=data:') >= 0) {
src = qstr.match(/source=(data:[^&]*)/)[1];
}
}
if (src) {
if (src.indexOf('data:') === 0) {
// plusses get replaced by spaces, so re-insert
src = src.replace(/ /g, '+');
Editor.loadFromDataURI(src);
} else {
Editor.loadFromString(src);
}
} else if (qstr.indexOf('paramurl=') !== -1) {
// Get parameter URL (use full length of remaining location.href)
svgEditor.loadFromURL(qstr.substr(9));
} else if (urldata.url) {
svgEditor.loadFromURL(urldata.url);
} else if (!urldata.noStorageOnLoad) {
svgEditor.loadContentAndPrefs();
}
}
else {
svgEditor.loadContentAndPrefs();
}
}());
var setIcon = Editor.setIcon = function(elem, icon_id, forcedSize) {
var icon = (typeof icon_id === 'string') ? $.getSvgIcon(icon_id, true) : icon_id.clone();
if (!icon) {
console.log('NOTE: Icon image missing: ' + icon_id);
return;
}
$(elem).empty().append(icon);
};
var extFunc = function() {
$.each(curConfig.extensions, function() {
var extname = this;
$.getScript(curConfig.extPath + extname, function(d) {
// Fails locally in Chrome 5
if (!d) {
var s = document.createElement('script');
s.src = curConfig.extPath + extname;
document.querySelector('head').appendChild(s);
}
});
});
var good_langs = [];
$('#lang_select option').each(function() {
good_langs.push(this.value);
});
// var lang = ('lang' in curPrefs) ? curPrefs.lang : null;
Editor.putLocale(null, good_langs);
};
// Load extensions
// Bit of a hack to run extensions in local Opera/IE9
if (document.location.protocol === 'file:') {
setTimeout(extFunc, 100);
} else {
extFunc();
}
$.svgIcons(curConfig.imgPath + 'svg_edit_icons.svg', {
w:24, h:24,
id_match: false,
no_img: !svgedit.browser.isWebkit(), // Opera & Firefox 4 gives odd behavior w/images
fallback_path: curConfig.imgPath,
fallback: {
'new_image': 'clear.png',
'save': 'save.png',
'open': 'open.png',
'source': 'source.png',
'docprops': 'document-properties.png',
'wireframe': 'wireframe.png',
'undo': 'undo.png',
'redo': 'redo.png',
'select': 'select.png',
'select_node': 'select_node.png',
'pencil': 'fhpath.png',
'pen': 'line.png',
'square': 'square.png',
'rect': 'rect.png',
'fh_rect': 'freehand-square.png',
'circle': 'circle.png',
'ellipse': 'ellipse.png',
'fh_ellipse': 'freehand-circle.png',
'path': 'path.png',
'text': 'text.png',
'image': 'image.png',
'zoom': 'zoom.png',
'clone': 'clone.png',
'node_clone': 'node_clone.png',
'delete': 'delete.png',
'node_delete': 'node_delete.png',
'group': 'shape_group_elements.png',
'ungroup': 'shape_ungroup.png',
'move_top': 'move_top.png',
'move_bottom': 'move_bottom.png',
'to_path': 'to_path.png',
'link_controls': 'link_controls.png',
'reorient': 'reorient.png',
'align_left': 'align-left.png',
'align_center': 'align-center.png',
'align_right': 'align-right.png',
'align_top': 'align-top.png',
'align_middle': 'align-middle.png',
'align_bottom': 'align-bottom.png',
'go_up': 'go-up.png',
'go_down': 'go-down.png',
'ok': 'save.png',
'cancel': 'cancel.png',
'arrow_right': 'flyouth.png',
'arrow_down': 'dropdown.gif'
},
placement: {
'#logo': 'logo',
'#tool_clear div,#layer_new': 'new_image',
'#tool_save div': 'save',
'#tool_export div': 'export',
'#tool_open div div': 'open',
'#tool_import div div': 'import',
'#tool_source': 'source',
'#tool_docprops > div': 'docprops',
'#tool_wireframe': 'wireframe',
'#tool_undo': 'undo',
'#tool_redo': 'redo',
'#tool_select': 'select',
'#tool_fhpath': 'pencil',
'#tool_line': 'pen',
'#tool_rect,#tools_rect_show': 'rect',
'#tool_square': 'square',
'#tool_fhrect': 'fh_rect',
'#tool_ellipse,#tools_ellipse_show': 'ellipse',
'#tool_circle': 'circle',
'#tool_fhellipse': 'fh_ellipse',
'#tool_path': 'path',
'#tool_text,#layer_rename': 'text',
'#tool_image': 'image',
'#tool_zoom': 'zoom',
'#tool_clone,#tool_clone_multi': 'clone',
'#tool_node_clone': 'node_clone',
'#layer_delete,#tool_delete,#tool_delete_multi': 'delete',
'#tool_node_delete': 'node_delete',
'#tool_add_subpath': 'add_subpath',
'#tool_openclose_path': 'open_path',
'#tool_move_top': 'move_top',
'#tool_move_bottom': 'move_bottom',
'#tool_topath': 'to_path',
'#tool_node_link': 'link_controls',
'#tool_reorient': 'reorient',
'#tool_group_elements': 'group_elements',
'#tool_ungroup': 'ungroup',
'#tool_unlink_use': 'unlink_use',
'#tool_alignleft, #tool_posleft': 'align_left',
'#tool_aligncenter, #tool_poscenter': 'align_center',
'#tool_alignright, #tool_posright': 'align_right',
'#tool_aligntop, #tool_postop': 'align_top',
'#tool_alignmiddle, #tool_posmiddle': 'align_middle',
'#tool_alignbottom, #tool_posbottom': 'align_bottom',
'#cur_position': 'align',
'#linecap_butt,#cur_linecap': 'linecap_butt',
'#linecap_round': 'linecap_round',
'#linecap_square': 'linecap_square',
'#linejoin_miter,#cur_linejoin': 'linejoin_miter',
'#linejoin_round': 'linejoin_round',
'#linejoin_bevel': 'linejoin_bevel',
'#url_notice': 'warning',
'#layer_up': 'go_up',
'#layer_down': 'go_down',
'#layer_moreopts': 'context_menu',
'#layerlist td.layervis': 'eye',
'#tool_source_save,#tool_docprops_save,#tool_prefs_save': 'ok',
'#tool_source_cancel,#tool_docprops_cancel,#tool_prefs_cancel': 'cancel',
'#rwidthLabel, #iwidthLabel': 'width',
'#rheightLabel, #iheightLabel': 'height',
'#cornerRadiusLabel span': 'c_radius',
'#angleLabel': 'angle',
'#linkLabel,#tool_make_link,#tool_make_link_multi': 'globe_link',
'#zoomLabel': 'zoom',
'#tool_fill label': 'fill',
'#tool_stroke .icon_label': 'stroke',
'#group_opacityLabel': 'opacity',
'#blurLabel': 'blur',
'#font_sizeLabel': 'fontsize',
'.flyout_arrow_horiz': 'arrow_right',
'.dropdown button, #main_button .dropdown': 'arrow_down',
'#palette .palette_item:first, #fill_bg, #stroke_bg': 'no_color'
},
resize: {
'#logo .svg_icon': 28,
'.flyout_arrow_horiz .svg_icon': 5,
'.layer_button .svg_icon, #layerlist td.layervis .svg_icon': 14,
'.dropdown button .svg_icon': 7,
'#main_button .dropdown .svg_icon': 9,
'.palette_item:first .svg_icon' : 15,
'#fill_bg .svg_icon, #stroke_bg .svg_icon': 16,
'.toolbar_button button .svg_icon': 16,
'.stroke_tool div div .svg_icon': 20,
'#tools_bottom label .svg_icon': 18
},
callback: function(icons) {
$('.toolbar_button button > svg, .toolbar_button button > img').each(function() {
$(this).parent().prepend(this);
});
var min_height,
tleft = $('#tools_left');
if (tleft.length !== 0) {
min_height = tleft.offset().top + tleft.outerHeight();
}
// var size = $.pref('iconsize');
// if (size && size != 'm') {
// svgEditor.setIconSize(size);
// } else if ($(window).height() < min_height) {
// // Make smaller
// svgEditor.setIconSize('s');
// }
// Look for any missing flyout icons from plugins
$('.tools_flyout').each(function() {
var shower = $('#' + this.id + '_show');
var sel = shower.attr('data-curopt');
// Check if there's an icon here
if (!shower.children('svg, img').length) {
var clone = $(sel).children().clone();
if (clone.length) {
clone[0].removeAttribute('style'); //Needed for Opera
shower.append(clone);
}
}
});
svgEditor.runCallbacks();
setTimeout(function() {
$('.flyout_arrow_horiz:empty').each(function() {
$(this).append($.getSvgIcon('arrow_right').width(5).height(5));
});
}, 1);
}
});
Editor.canvas = svgCanvas = new $.SvgCanvas(document.getElementById('svgcanvas'), curConfig);
Editor.showSaveWarning = false;
var palette = [
'#000000', '#3f3f3f', '#7f7f7f', '#bfbfbf', '#ffffff',
'#ff0000', '#ff7f00', '#ffff00', '#7fff00',
'#00ff00', '#00ff7f', '#00ffff', '#007fff',
'#0000ff', '#7f00ff', '#ff00ff', '#ff007f',
'#7f0000', '#7f3f00', '#7f7f00', '#3f7f00',
'#007f00', '#007f3f', '#007f7f', '#003f7f',
'#00007f', '#3f007f', '#7f007f', '#7f003f',
'#ffaaaa', '#ffd4aa', '#ffffaa', '#d4ffaa',
'#aaffaa', '#aaffd4', '#aaffff', '#aad4ff',
'#aaaaff', '#d4aaff', '#ffaaff', '#ffaad4'
],
modKey = (svgedit.browser.isMac() ? 'meta+' : 'ctrl+'), // ⌘
path = svgCanvas.pathActions,
undoMgr = svgCanvas.undoMgr,
Utils = svgedit.utilities,
defaultImageURL = curConfig.imgPath + 'logo.png',
workarea = $('#workarea'),
canv_menu = $('#cmenu_canvas'),
// layer_menu = $('#cmenu_layers'), // Unused
exportWindow = null,
tool_scale = 1,
zoomInIcon = 'crosshair',
zoomOutIcon = 'crosshair',
ui_context = 'toolbars',
origSource = '',
paintBox = {fill: null, stroke:null};
// This sets up alternative dialog boxes. They mostly work the same way as
// their UI counterparts, expect instead of returning the result, a callback
// needs to be included that returns the result as its first parameter.
// In the future we may want to add additional types of dialog boxes, since
// they should be easy to handle this way.
(function() {
$('#dialog_container').draggable({cancel: '#dialog_content, #dialog_buttons *', containment: 'window'});
var box = $('#dialog_box'),
btn_holder = $('#dialog_buttons'),
dialog_content = $('#dialog_content'),
dbox = function(type, msg, callback, defaultVal, opts, changeCb, checkbox) {
var ok, ctrl, chkbx;
dialog_content.html('
'+msg.replace(/\n/g, '
')+'
')
.toggleClass('prompt', (type == 'prompt'));
btn_holder.empty();
ok = $('').appendTo(btn_holder);
if (type !== 'alert') {
$('')
.appendTo(btn_holder)
.click(function() { box.hide(); callback(false);});
}
if (type === 'prompt') {
ctrl = $('').prependTo(btn_holder);
ctrl.val(defaultVal || '');
ctrl.bind('keydown', 'return', function() {ok.click();});
}
else if (type === 'select') {
var div = $('');
ctrl = $('