/*globals svgEditor:true, globalStorage, widget, svgedit, canvg, jQuery, $, DOMParser, FileReader */ /*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 * Copyright(c) 2014 Brett Zamir * */ // Dependencies: // 1) units.js // 2) browser.js // 3) svgcanvas.js /* TO-DOS 1. JSDoc */ (function() { if (window.svgEditor) { return; } window.svgEditor = (function($) { var editor = {}; // EDITOR PROPERTIES: (defined below) // curPrefs, curConfig, canvas, storage, uiStrings // // STATE MAINTENANCE PROPERTIES editor.tool_scale = 1; // Dependent on icon size, so any use to making configurable instead? Used by JQuerySpinBtn.js editor.langChanged = false; editor.showSaveWarning = false; editor.storagePromptClosed = false; // For use with ext-storage.js var svgCanvas, urldata, Utils = svgedit.utilities, isReady = false, callbacks = [], customHandlers = {}, /** * PREFS AND CONFIG */ // The iteration algorithm for defaultPrefs does not currently support array/objects defaultPrefs = { // EDITOR OPTIONS (DIALOG) lang: '', // Default to "en" if locale.js detection does not detect another language iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise bkgd_color: '#FFF', bkgd_url: '', // DOCUMENT PROPERTIES (DIALOG) img_save: 'embed', // ALERT NOTICES // Only shows in UI as far as alert notices, but useful to remember, so keeping as pref save_notice_done: false, export_notice_done: false }, curPrefs = {}, // Note: The difference between Prefs and Config is that Prefs // can be changed in the UI and are stored in the browser, // while config cannot curConfig = { // We do not put on defaultConfig to simplify object copying // procedures (we obtain instead from defaultExtensions) extensions: [], /** * Can use window.location.origin to indicate the current * origin. Can contain a '*' to allow all domains or 'null' (as * a string) to support all file:// URLs. Cannot be set by * URL for security reasons (not safe, at least for * privacy or data integrity of SVG content). * Might have been fairly safe to allow * `new URL(window.location.href).origin` by default but * avoiding it ensures some more security that even third * party apps on the same domain also cannot communicate * with this app by default. * For use with ext-xdomain-messaging.js * @todo We might instead make as a user-facing preference. */ allowedOrigins: [] }, defaultExtensions = [ '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' ], defaultConfig = { // Todo: svgcanvas.js also sets and checks: show_outside_canvas, selectNew; add here? // Change the following to preferences and add pref controls to the UI (e.g., initTool, wireframe, showlayers)? canvasName: 'default', canvas_expansion: 3, initFill: { color: 'FF0000', // solid red opacity: 1 }, initStroke: { width: 5, color: '000000', // solid black opacity: 1 }, initOpacity: 1, colorPickerCSS: null, initTool: 'select', wireframe: false, showlayers: false, no_save_warning: false, // PATH CONFIGURATION // The following path configuration items are disallowed in the URL (as should any future path configurations) imgPath: 'images/', langPath: 'locale/', extPath: 'extensions/', jGraduatePath: 'jgraduate/images/', // DOCUMENT PROPERTIES // Change the following to a preference (already in the Document Properties dialog)? dimensions: [640, 480], // EDITOR OPTIONS // Change the following to preferences (already in the Editor Options dialog)? gridSnapping: false, gridColor: '#000', baseUnit: 'px', snappingStep: 10, showRulers: true, // URL BEHAVIOR CONFIGURATION preventAllURLConfig: false, preventURLContentLoading: false, // EXTENSION CONFIGURATION (see also preventAllURLConfig) lockExtensions: false, // Disallowed in URL setting noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in config.js or in the URL // EXTENSION-RELATED (GRID) showGrid: false, // Set by ext-grid.js // EXTENSION-RELATED (STORAGE) noStorageOnLoad: false, // Some interaction with ext-storage.js; prevent even the loading of previously saved local storage forceStorage: false, // Some interaction with ext-storage.js; strongly discouraged from modification as it bypasses user privacy by preventing them from choosing whether to keep local storage or not emptyStorageOnDecline: false // Used by ext-storage.js; empty any prior storage if the user declines to store }, /** * LOCALE * @todo Can we remove now that we are always loading even English? (unless locale is set to null) */ 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\' ...' } }; 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); }); } } /** * EXPORTS */ /** * 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. * @todo Can we change setting on the jQuery namespace (onto editor) to avoid conflicts? * @todo Review whether any remaining existing direct references to * getting curPrefs can be changed to use $.pref() getting to ensure * defaultPrefs fallback (also for sake of allowInitialUserOverride); specifically, bkgd_color could be changed so that * the pref dialog has a button to auto-calculate background, but otherwise uses $.pref() to be able to get default prefs * or overridable settings */ $.pref = function (key, val) { if (val) { curPrefs[key] = val; editor.curPrefs = curPrefs; // Update exported value return; } return (key in curPrefs) ? curPrefs[key] : defaultPrefs[key]; }; /** * EDITOR PUBLIC METHODS * locale.js also adds "putLang" and "readLang" as editor methods * @todo Sort these methods per invocation order, ideally with init at the end * @todo Prevent execution until init executes if dependent on it? */ /** * Where permitted, sets canvas and/or defaultPrefs based on previous * storage. This will override URL settings (for security reasons) but * not config.js configuration (unless initial user overriding is explicitly * permitted there via allowInitialUserOverride). * @todo Split allowInitialUserOverride into allowOverrideByURL and * allowOverrideByUserStorage so config.js can disallow some * individual items for URL setting but allow for user storage AND/OR * change URL setting so that it always uses a different namespace, * so it won't affect pre-existing user storage (but then if users saves * that, it will then be subject to tampering */ editor.loadContentAndPrefs = function () { if (!curConfig.forceStorage && (curConfig.noStorageOnLoad || !document.cookie.match(/(?:^|;\s*)store=(?:prefsAndContent|prefsOnly)/))) { return; } // LOAD CONTENT if (editor.storage && // Cookies do not have enough available memory to hold large documents (curConfig.forceStorage || (!curConfig.noStorageOnLoad && document.cookie.match(/(?:^|;\s*)store=prefsAndContent/))) ) { var name = 'svgedit-' + curConfig.canvasName; var cached = editor.storage.getItem(name); if (cached) { editor.loadFromString(cached); } } // LOAD PREFS var key; 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 (editor.storage) { var val = editor.storage.getItem(storeKey); if (val) { defaultPrefs[key] = String(val); // Convert to string for FF (.value fails in Webkit) } } else if (window.widget) { defaultPrefs[key] = widget.preferenceForKey(storeKey); } else { var result = document.cookie.match(new RegExp('(?:^|;\\s*)' + Utils.preg_quote(encodeURIComponent(storeKey)) + '=([^;]+)')); defaultPrefs[key] = result ? decodeURIComponent(result[1]) : ''; } } } }; /** * Allows setting of preferences or configuration (including extensions). * @param {object} opts The preferences or configuration (including extensions) * @param {object} [cfgCfg] Describes configuration which applies to the particular batch of supplied options * @param {boolean} [cfgCfg.allowInitialUserOverride=false] Set to true if you wish * to allow initial overriding of settings by the user via the URL * (if permitted) or previously stored preferences (if permitted); * note that it will be too late if you make such calls in extension * code because the URL or preference storage settings will * have already taken place. * @param {boolean} [cfgCfg.overwrite=true] Set to false if you wish to * prevent the overwriting of prior-set preferences or configuration * (URL settings will always follow this requirement for security * reasons, so config.js settings cannot be overridden unless it * explicitly permits via "allowInitialUserOverride" but extension config * can be overridden as they will run after URL settings). Should * not be needed in config.js. */ editor.setConfig = function (opts, cfgCfg) { cfgCfg = cfgCfg || {}; function extendOrAdd (cfgObj, key, val) { if (cfgObj[key] && typeof cfgObj[key] === 'object') { $.extend(true, cfgObj[key], val); } else { cfgObj[key] = val; } return; } $.each(opts, function(key, val) { if (opts.hasOwnProperty(key)) { // Only allow prefs defined in defaultPrefs if (defaultPrefs.hasOwnProperty(key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || curPrefs.hasOwnProperty(key) )) { return; } if (cfgCfg.allowInitialUserOverride === true) { defaultPrefs[key] = val; } else { $.pref(key, val); } } else if (['extensions', 'allowedOrigins'].indexOf(key) > -1) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || key === 'allowedOrigins' || (key === 'extensions' && curConfig.lockExtensions) ) ) { return; } curConfig[key] = curConfig[key].concat(val); // We will handle any dupes later } // Only allow other curConfig if defined in defaultConfig else if (defaultConfig.hasOwnProperty(key)) { if (cfgCfg.overwrite === false && ( curConfig.preventAllURLConfig || curConfig.hasOwnProperty(key) )) { return; } // Potentially overwriting of previously set config if (curConfig.hasOwnProperty(key)) { if (cfgCfg.overwrite === false) { return; } extendOrAdd(curConfig, key, val); } else { if (cfgCfg.allowInitialUserOverride === true) { extendOrAdd(defaultConfig, key, val); } else { if (defaultConfig[key] && typeof defaultConfig[key] === 'object') { curConfig[key] = {}; $.extend(true, curConfig[key], val); // Merge properties recursively, e.g., on initFill, initStroke objects } else { curConfig[key] = val; } } } } } }); editor.curConfig = curConfig; // Update exported value }; /** * @param {object} opts 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 () { // var host = location.hostname, // onWeb = host && host.indexOf('.') >= 0; // Some FF versions throw security errors here when directly accessing try { if ('localStorage' in window) { // && onWeb removed so Webkit works locally editor.storage = localStorage; } } catch(err) {} // Todo: Avoid var-defined functions and group functions together, etc. where possible var good_langs = []; $('#lang_select option').each(function() { good_langs.push(this.value); }); function setupCurPrefs () { curPrefs = $.extend(true, {}, defaultPrefs, curPrefs); // Now safe to merge with priority for curPrefs in the event any are already set // Export updated prefs editor.curPrefs = curPrefs; } function setupCurConfig () { curConfig = $.extend(true, {}, defaultConfig, curConfig); // Now safe to merge with priority for curConfig in the event any are already set // Now deal with extensions and other array config if (!curConfig.noDefaultExtensions) { curConfig.extensions = curConfig.extensions.concat(defaultExtensions); } // ...and remove any dupes $.each(['extensions', 'allowedOrigins'], function (i, cfg) { curConfig[cfg] = $.grep(curConfig[cfg], function (n, i) { return i === curConfig[cfg].indexOf(n); }); }); // Export updated config editor.curConfig = curConfig; } (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 $.each( [ 'extPath', 'imgPath', 'langPath', 'jGraduatePath' ], function (pathConfig) { if (urldata[pathConfig]) { delete urldata[pathConfig]; } } ); editor.setConfig(urldata, {overwrite: false}); // Note: source, url, and paramurl (as with storagePrompt later) are not set on config but are used below setupCurConfig(); if (!curConfig.preventURLContentLoading) { 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); } return; } if (qstr.indexOf('paramurl=') !== -1) { // Get parameter URL (use full length of remaining location.href) editor.loadFromDataURIRL(qstr.substr(9)); return; } if (urldata.url) { editor.loadFromURL(urldata.url); return; } } if (!urldata.noStorageOnLoad || curConfig.forceStorage) { editor.loadContentAndPrefs(); } setupCurPrefs(); } else { setupCurConfig(); editor.loadContentAndPrefs(); setupCurPrefs(); } }()); // For external openers (function() { // let the opener know SVG Edit is ready (now that config is set up) 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) {} } }()); 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; if (!extname.match(/^ext-.*\.js/)) { // Ensure URL cannot specify some other unintended file in the extPath return; } $.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 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_save': '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'); editor.setIconSize(size || ($(window).height() < min_height ? 's': 'm')); // 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); } } }); editor.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); var supportsNonSS, resize_timer, changeZoom, Actions, curScrollPos, palette = [ // Todo: Make into configuration item? '#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, defaultImageURL = curConfig.imgPath + 'logo.png', workarea = $('#workarea'), canv_menu = $('#cmenu_canvas'), // layer_menu = $('#cmenu_layers'), // Unused exportWindow = null, 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 = $('').appendTo(label); chkbx.val(checkbox.value); if (checkbox.tooltip) { label.attr('title', checkbox.tooltip); } chkbx.prop('checked', !!checkbox.checked); div.append($('
').append(label)); } $.each(opts || [], function (opt, val) { if (typeof val === 'object') { ctrl.append($(''); } if (icon !== undefined) { var copy = icon.clone(); $('td.layervis', layerlist).append(copy); $.resizeSvgIcons({'td.layervis .svg_icon': 14}); } // handle selection of layer $('#layerlist td.layername') .mouseup(function(evt) { $('#layerlist tr.layer').removeClass('layersel'); $(this.parentNode).addClass('layersel'); svgCanvas.setCurrentLayer(this.textContent); evt.preventDefault(); }) .mouseover(function() { toggleHighlightLayer(this.textContent); }) .mouseout(function() { toggleHighlightLayer(); }); $('#layerlist td.layervis').click(function() { var row = $(this.parentNode).prevAll().length; var name = $('#layerlist tr.layer:eq(' + row + ') td.layername').text(); var vis = $(this).hasClass('layerinvis'); svgCanvas.setLayerVisibility(name, vis); $(this).toggleClass('layerinvis'); }); // if there were too few rows, let's add a few to make it not so lonely var num = 5 - $('#layerlist tr.layer').size(); while (num-- > 0) { // FIXME: there must a better way to do this layerlist.append('_'); } }; var showSourceEditor = function(e, forSaving) { if (editingsource) {return;} editingsource = true; origSource = svgCanvas.getSvgString(); $('#save_output_btns').toggle(!!forSaving); $('#tool_source_back').toggle(!forSaving); $('#svg_source_textarea').val(origSource); $('#svg_source_editor').fadeIn(); $('#svg_source_textarea').focus(); }; var togglePathEditMode = function(editmode, elems) { $('#path_node_panel').toggle(editmode); $('#tools_bottom_2,#tools_bottom_3').toggle(!editmode); if (editmode) { // Change select icon $('.tool_button_current').removeClass('tool_button_current').addClass('tool_button'); $('#tool_select').addClass('tool_button_current').removeClass('tool_button'); setIcon('#tool_select', 'select_node'); multiselected = false; if (elems.length) { selectedElement = elems[0]; } } else { setTimeout(function () { setIcon('#tool_select', 'select'); }, 1000); } }; var saveHandler = function(wind, svg) { editor.showSaveWarning = false; // by default, we add the XML prolog back, systems integrating SVG-edit (wikis, CMSs) // can just provide their own custom save handler and might not want the XML prolog svg = '\n' + svg; // IE9 doesn't allow standalone Data URLs // https://connect.microsoft.com/IE/feedback/details/542600/data-uri-images-fail-when-loaded-by-themselves if (svgedit.browser.isIE()) { showSourceEditor(0, true); return; } // Opens the SVG in new window var win = wind.open('data:image/svg+xml;base64,' + Utils.encode64(svg)); // Alert will only appear the first time saved OR the first time the bug is encountered var done = $.pref('save_notice_done'); if (done !== 'all') { var note = uiStrings.notification.saveFromBrowser.replace('%s', 'SVG'); // Check if FF and has if (svgedit.browser.isGecko()) { if (svg.indexOf('', {id: 'export_canvas'}).hide().appendTo('body'); } var c = $('#export_canvas')[0]; c.width = svgCanvas.contentW; c.height = svgCanvas.contentH; canvg(c, data.svg, {renderCallback: function() { var datauri = data.quality ? c.toDataURL('image/' + dataURLType, data.quality) : c.toDataURL('image/' + dataURLType); exportWindow.location.href = datauri; var done = $.pref('export_notice_done'); if (done !== 'all') { var note = uiStrings.notification.saveFromBrowser.replace('%s', type); // Check if there's issues if (issues.length) { var pre = '\n \u2022 '; note += ('\n\n' + uiStrings.notification.noteTheseIssues + pre + issues.join(pre)); } // Note that this will also prevent the notice even though new issues may appear later. // May want to find a way to deal with that without annoying the user $.pref('export_notice_done', 'all'); exportWindow.alert(note); } }}); }; var operaRepaint = function() { // Repaints canvas in Opera. Needed for stroke-dasharray change as well as fill change if (!window.opera) { return; } $('

').hide().appendTo('body').remove(); }; function setStrokeOpt(opt, changeElem) { var id = opt.id; var bits = id.split('_'); var pre = bits[0]; var val = bits[1]; if (changeElem) { svgCanvas.setStrokeAttr('stroke-' + pre, val); } operaRepaint(); setIcon('#cur_' + pre, id, 20); $(opt).addClass('current').siblings().removeClass('current'); } // This is a common function used when a tool has been clicked (chosen) // It does several common things: // - removes the tool_button_current class from whatever tool currently has it // - hides any flyouts // - adds the tool_button_current class to the button passed in var toolButtonClick = editor.toolButtonClick = function(button, noHiding) { if ($(button).hasClass('disabled')) {return false;} if ($(button).parent().hasClass('tools_flyout')) {return true;} var fadeFlyouts = 'normal'; if (!noHiding) { $('.tools_flyout').fadeOut(fadeFlyouts); } $('#styleoverrides').text(''); workarea.css('cursor', 'auto'); $('.tool_button_current').removeClass('tool_button_current').addClass('tool_button'); $(button).addClass('tool_button_current').removeClass('tool_button'); return true; }; var clickSelect = editor.clickSelect = function() { if (toolButtonClick('#tool_select')) { svgCanvas.setMode('select'); $('#styleoverrides').text('#svgcanvas svg *{cursor:move;pointer-events:all}, #svgcanvas svg{cursor:default}'); } }; var setImageURL = editor.setImageURL = function(url) { if (!url) { url = defaultImageURL; } svgCanvas.setImageURL(url); $('#image_url').val(url); if (url.indexOf('data:') === 0) { // data URI found $('#image_url').hide(); $('#change_image_url').show(); } else { // regular URL svgCanvas.embedImage(url, function(dataURI) { // Couldn't embed, so show warning $('#url_notice').toggle(!dataURI); defaultImageURL = url; }); $('#image_url').show(); $('#change_image_url').hide(); } }; function setBackground (color, url) { // if (color == $.pref('bkgd_color') && url == $.pref('bkgd_url')) {return;} $.pref('bkgd_color', color); $.pref('bkgd_url', url); // This should be done in svgcanvas.js for the borderRect fill svgCanvas.setBackground(color, url); } function promptImgURL() { var curhref = svgCanvas.getHref(selectedElement); curhref = curhref.indexOf('data:') === 0 ? '' : curhref; $.prompt(uiStrings.notification.enterNewImgURL, curhref, function(url) { if (url) {setImageURL(url);} }); } var setInputWidth = function(elem) { var w = Math.min(Math.max(12 + elem.value.length * 6, 50), 300); $(elem).width(w); }; function updateRulers(scanvas, zoom) { if (!zoom) {zoom = svgCanvas.getZoom();} if (!scanvas) {scanvas = $('#svgcanvas');} var d, i; var limit = 30000; var contentElem = svgCanvas.getContentElem(); var units = svgedit.units.getTypeMap(); var unit = units[curConfig.baseUnit]; // 1 = 1px // draw x ruler then y ruler for (d = 0; d < 2; d++) { var isX = (d === 0); var dim = isX ? 'x' : 'y'; var lentype = isX ? 'width' : 'height'; var contentDim = Number(contentElem.getAttribute(dim)); var $hcanv_orig = $('#ruler_' + dim + ' canvas:first'); // Bit of a hack to fully clear the canvas in Safari & IE9 var $hcanv = $hcanv_orig.clone(); $hcanv_orig.replaceWith($hcanv); var hcanv = $hcanv[0]; // Set the canvas size to the width of the container var ruler_len = scanvas[lentype](); var total_len = ruler_len; hcanv.parentNode.style[lentype] = total_len + 'px'; var ctx_num = 0; var ctx = hcanv.getContext('2d'); var ctx_arr, num, ctx_arr_num; ctx.fillStyle = 'rgb(200,0,0)'; ctx.fillRect(0, 0, hcanv.width, hcanv.height); // Remove any existing canvasses $hcanv.siblings().remove(); // Create multiple canvases when necessary (due to browser limits) if (ruler_len >= limit) { ctx_arr_num = parseInt(ruler_len / limit, 10) + 1; ctx_arr = []; ctx_arr[0] = ctx; var copy; for (i = 1; i < ctx_arr_num; i++) { hcanv[lentype] = limit; copy = hcanv.cloneNode(true); hcanv.parentNode.appendChild(copy); ctx_arr[i] = copy.getContext('2d'); } copy[lentype] = ruler_len % limit; // set copy width to last ruler_len = limit; } hcanv[lentype] = ruler_len; var u_multi = unit * zoom; // Calculate the main number interval var raw_m = 50 / u_multi; var multi = 1; for (i = 0; i < r_intervals.length; i++) { num = r_intervals[i]; multi = num; if (raw_m <= num) { break; } } var big_int = multi * u_multi; ctx.font = '9px sans-serif'; var ruler_d = ((contentDim / u_multi) % multi) * u_multi; var label_pos = ruler_d - big_int; // draw big intervals while (ruler_d < total_len) { label_pos += big_int; // var real_d = ruler_d - contentDim; // Currently unused var cur_d = Math.round(ruler_d) + 0.5; if (isX) { ctx.moveTo(cur_d, 15); ctx.lineTo(cur_d, 0); } else { ctx.moveTo(15, cur_d); ctx.lineTo(0, cur_d); } num = (label_pos - contentDim) / u_multi; var label; if (multi >= 1) { label = Math.round(num); } else { var decs = String(multi).split('.')[1].length; label = num.toFixed(decs); } // Change 1000s to Ks if (label !== 0 && label !== 1000 && label % 1000 === 0) { label = (label / 1000) + 'K'; } if (isX) { ctx.fillText(label, ruler_d+2, 8); } else { // draw label vertically var str = String(label).split(''); for (i = 0; i < str.length; i++) { ctx.fillText(str[i], 1, (ruler_d+9) + i*9); } } var part = big_int / 10; // draw the small intervals for (i = 1; i < 10; i++) { var sub_d = Math.round(ruler_d + part * i) + 0.5; if (ctx_arr && sub_d > ruler_len) { ctx_num++; ctx.stroke(); if (ctx_num >= ctx_arr_num) { i = 10; ruler_d = total_len; continue; } ctx = ctx_arr[ctx_num]; ruler_d -= limit; sub_d = Math.round(ruler_d + part * i) + 0.5; } // odd lines are slighly longer var line_num = (i % 2) ? 12 : 10; if (isX) { ctx.moveTo(sub_d, 15); ctx.lineTo(sub_d, line_num); } else { ctx.moveTo(15, sub_d); ctx.lineTo(line_num, sub_d); } } ruler_d += big_int; } ctx.strokeStyle = '#000'; ctx.stroke(); } } var updateCanvas = editor.updateCanvas = function(center, new_ctr) { var w = workarea.width(), h = workarea.height(); var w_orig = w, h_orig = h; var zoom = svgCanvas.getZoom(); var w_area = workarea; var cnvs = $('#svgcanvas'); var old_ctr = { x: w_area[0].scrollLeft + w_orig/2, y: w_area[0].scrollTop + h_orig/2 }; var multi = curConfig.canvas_expansion; w = Math.max(w_orig, svgCanvas.contentW * zoom * multi); h = Math.max(h_orig, svgCanvas.contentH * zoom * multi); if (w == w_orig && h == h_orig) { workarea.css('overflow', 'hidden'); } else { workarea.css('overflow', 'scroll'); } var old_can_y = cnvs.height()/2; var old_can_x = cnvs.width()/2; cnvs.width(w).height(h); var new_can_y = h/2; var new_can_x = w/2; var offset = svgCanvas.updateCanvas(w, h); var ratio = new_can_x / old_can_x; var scroll_x = w/2 - w_orig/2; var scroll_y = h/2 - h_orig/2; if (!new_ctr) { var old_dist_x = old_ctr.x - old_can_x; var new_x = new_can_x + old_dist_x * ratio; var old_dist_y = old_ctr.y - old_can_y; var new_y = new_can_y + old_dist_y * ratio; new_ctr = { x: new_x, y: new_y }; } else { new_ctr.x += offset.x; new_ctr.y += offset.y; } if (center) { // Go to top-left for larger documents if (svgCanvas.contentW > w_area.width()) { // Top-left workarea[0].scrollLeft = offset.x - 10; workarea[0].scrollTop = offset.y - 10; } else { // Center w_area[0].scrollLeft = scroll_x; w_area[0].scrollTop = scroll_y; } } else { w_area[0].scrollLeft = new_ctr.x - w_orig/2; w_area[0].scrollTop = new_ctr.y - h_orig/2; } if (curConfig.showRulers) { updateRulers(cnvs, zoom); workarea.scroll(); } if (urldata.storagePrompt !== true && !editor.storagePromptClosed) { $('#dialog_box').hide(); } }; var updateToolButtonState = function() { var index, button; var bNoFill = (svgCanvas.getColor('fill') == 'none'); var bNoStroke = (svgCanvas.getColor('stroke') == 'none'); var buttonsNeedingStroke = [ '#tool_fhpath', '#tool_line' ]; var buttonsNeedingFillAndStroke = [ '#tools_rect .tool_button', '#tools_ellipse .tool_button', '#tool_text', '#tool_path']; if (bNoStroke) { for (index in buttonsNeedingStroke) { button = buttonsNeedingStroke[index]; if ($(button).hasClass('tool_button_current')) { clickSelect(); } $(button).addClass('disabled'); } } else { for (index in buttonsNeedingStroke) { button = buttonsNeedingStroke[index]; $(button).removeClass('disabled'); } } if (bNoStroke && bNoFill) { for (index in buttonsNeedingFillAndStroke) { button = buttonsNeedingFillAndStroke[index]; if ($(button).hasClass('tool_button_current')) { clickSelect(); } $(button).addClass('disabled'); } } else { for (index in buttonsNeedingFillAndStroke) { button = buttonsNeedingFillAndStroke[index]; $(button).removeClass('disabled'); } } svgCanvas.runExtensions('toolButtonStateUpdate', { nofill: bNoFill, nostroke: bNoStroke }); // Disable flyouts if all inside are disabled $('.tools_flyout').each(function() { var shower = $('#' + this.id + '_show'); var has_enabled = false; $(this).children().each(function() { if (!$(this).hasClass('disabled')) { has_enabled = true; } }); shower.toggleClass('disabled', !has_enabled); }); operaRepaint(); }; // Updates the toolbar (colors, opacity, etc) based on the selected element // This function also updates the opacity and id elements that are in the context panel var updateToolbar = function() { var i, len; if (selectedElement != null) { switch (selectedElement.tagName) { case 'use': case 'image': case 'foreignObject': break; case 'g': case 'a': // Look for common styles var gWidth = null; var childs = selectedElement.getElementsByTagName('*'); for (i = 0, len = childs.length; i < len; i++) { var swidth = childs[i].getAttribute('stroke-width'); if (i === 0) { gWidth = swidth; } else if (gWidth !== swidth) { gWidth = null; } } $('#stroke_width').val(gWidth === null ? '' : gWidth); paintBox.fill.update(true); paintBox.stroke.update(true); break; default: paintBox.fill.update(true); paintBox.stroke.update(true); $('#stroke_width').val(selectedElement.getAttribute('stroke-width') || 1); $('#stroke_style').val(selectedElement.getAttribute('stroke-dasharray') || 'none'); var attr = selectedElement.getAttribute('stroke-linejoin') || 'miter'; if ($('#linejoin_' + attr).length != 0) { setStrokeOpt($('#linejoin_' + attr)[0]); } attr = selectedElement.getAttribute('stroke-linecap') || 'butt'; if ($('#linecap_' + attr).length != 0) { setStrokeOpt($('#linecap_' + attr)[0]); } } } // All elements including image and group have opacity if (selectedElement != null) { var opac_perc = ((selectedElement.getAttribute('opacity')||1.0)*100); $('#group_opacity').val(opac_perc); $('#opac_slider').slider('option', 'value', opac_perc); $('#elem_id').val(selectedElement.id); } updateToolButtonState(); }; // updates the context panel tools based on the selected element var updateContextPanel = function() { var elem = selectedElement; // If element has just been deleted, consider it null if (elem != null && !elem.parentNode) {elem = null;} var currentLayerName = svgCanvas.getCurrentDrawing().getCurrentLayerName(); var currentMode = svgCanvas.getMode(); var unit = curConfig.baseUnit !== 'px' ? curConfig.baseUnit : null; var is_node = currentMode == 'pathedit'; //elem ? (elem.id && elem.id.indexOf('pathpointgrip') == 0) : false; var menu_items = $('#cmenu_canvas li'); $('#selected_panel, #multiselected_panel, #g_panel, #rect_panel, #circle_panel,'+ '#ellipse_panel, #line_panel, #text_panel, #image_panel, #container_panel,'+ ' #use_panel, #a_panel').hide(); if (elem != null) { var elname = elem.nodeName; // If this is a link with no transform and one child, pretend // its child is selected // if (elname === 'a') { // && !$(elem).attr('transform')) { // elem = elem.firstChild; // } var angle = svgCanvas.getRotationAngle(elem); $('#angle').val(angle); var blurval = svgCanvas.getBlur(elem); $('#blur').val(blurval); $('#blur_slider').slider('option', 'value', blurval); if (svgCanvas.addedNew) { if (elname === 'image') { // Prompt for URL if not a data URL if (svgCanvas.getHref(elem).indexOf('data:') !== 0) { promptImgURL(); } } /*else if (elname == 'text') { // TODO: Do something here for new text }*/ } if (!is_node && currentMode != 'pathedit') { $('#selected_panel').show(); // Elements in this array already have coord fields if (['line', 'circle', 'ellipse'].indexOf(elname) >= 0) { $('#xy_panel').hide(); } else { var x, y; // Get BBox vals for g, polyline and path if (['g', 'polyline', 'path'].indexOf(elname) >= 0) { var bb = svgCanvas.getStrokedBBox([elem]); if (bb) { x = bb.x; y = bb.y; } } else { x = elem.getAttribute('x'); y = elem.getAttribute('y'); } if (unit) { x = svgedit.units.convertUnit(x); y = svgedit.units.convertUnit(y); } $('#selected_x').val(x || 0); $('#selected_y').val(y || 0); $('#xy_panel').show(); } // Elements in this array cannot be converted to a path var no_path = ['image', 'text', 'path', 'g', 'use'].indexOf(elname) == -1; $('#tool_topath').toggle(no_path); $('#tool_reorient').toggle(elname === 'path'); $('#tool_reorient').toggleClass('disabled', angle === 0); } else { var point = path.getNodePoint(); $('#tool_add_subpath').removeClass('push_button_pressed').addClass('tool_button'); $('#tool_node_delete').toggleClass('disabled', !path.canDeleteNodes); // Show open/close button based on selected point setIcon('#tool_openclose_path', path.closed_subpath ? 'open_path' : 'close_path'); if (point) { var seg_type = $('#seg_type'); if (unit) { point.x = svgedit.units.convertUnit(point.x); point.y = svgedit.units.convertUnit(point.y); } $('#path_node_x').val(point.x); $('#path_node_y').val(point.y); if (point.type) { seg_type.val(point.type).removeAttr('disabled'); } else { seg_type.val(4).attr('disabled', 'disabled'); } } return; } // update contextual tools here var panels = { g: [], a: [], rect: ['rx', 'width', 'height'], image: ['width', 'height'], circle: ['cx', 'cy', 'r'], ellipse: ['cx', 'cy', 'rx', 'ry'], line: ['x1', 'y1', 'x2', 'y2'], text: [], use: [] }; var el_name = elem.tagName; // if ($(elem).data('gsvg')) { // $('#g_panel').show(); // } var link_href = null; if (el_name === 'a') { link_href = svgCanvas.getHref(elem); $('#g_panel').show(); } if (elem.parentNode.tagName === 'a') { if (!$(elem).siblings().length) { $('#a_panel').show(); link_href = svgCanvas.getHref(elem.parentNode); } } // Hide/show the make_link buttons $('#tool_make_link, #tool_make_link').toggle(!link_href); if (link_href) { $('#link_url').val(link_href); } if (panels[el_name]) { var cur_panel = panels[el_name]; $('#' + el_name + '_panel').show(); $.each(cur_panel, function(i, item) { var attrVal = elem.getAttribute(item); if (curConfig.baseUnit !== 'px' && elem[item]) { var bv = elem[item].baseVal.value; attrVal = svgedit.units.convertUnit(bv); } $('#' + el_name + '_' + item).val(attrVal || 0); }); if (el_name == 'text') { $('#text_panel').css('display', 'inline'); if (svgCanvas.getItalic()) { $('#tool_italic').addClass('push_button_pressed').removeClass('tool_button'); } else { $('#tool_italic').removeClass('push_button_pressed').addClass('tool_button'); } if (svgCanvas.getBold()) { $('#tool_bold').addClass('push_button_pressed').removeClass('tool_button'); } else { $('#tool_bold').removeClass('push_button_pressed').addClass('tool_button'); } $('#font_family').val(elem.getAttribute('font-family')); $('#font_size').val(elem.getAttribute('font-size')); $('#text').val(elem.textContent); if (svgCanvas.addedNew) { // Timeout needed for IE9 setTimeout(function() { $('#text').focus().select(); }, 100); } } // text else if (el_name == 'image') { setImageURL(svgCanvas.getHref(elem)); } // image else if (el_name === 'g' || el_name === 'use') { $('#container_panel').show(); var title = svgCanvas.getTitle(); var label = $('#g_title')[0]; label.value = title; setInputWidth(label); $('#g_title').prop('disabled', el_name == 'use'); } } menu_items[(el_name === 'g' ? 'en' : 'dis') + 'ableContextMenuItems']('#ungroup'); menu_items[((el_name === 'g' || !multiselected) ? 'dis' : 'en') + 'ableContextMenuItems']('#group'); } // if (elem != null) else if (multiselected) { $('#multiselected_panel').show(); menu_items .enableContextMenuItems('#group') .disableContextMenuItems('#ungroup'); } else { menu_items.disableContextMenuItems('#delete,#cut,#copy,#group,#ungroup,#move_front,#move_up,#move_down,#move_back'); } // update history buttons $('#tool_undo').toggleClass('disabled', undoMgr.getUndoStackSize() === 0); $('#tool_redo').toggleClass('disabled', undoMgr.getRedoStackSize() === 0); svgCanvas.addedNew = false; if ( (elem && !is_node) || multiselected) { // update the selected elements' layer $('#selLayerNames').removeAttr('disabled').val(currentLayerName); // Enable regular menu options canv_menu.enableContextMenuItems('#delete,#cut,#copy,#move_front,#move_up,#move_down,#move_back'); } else { $('#selLayerNames').attr('disabled', 'disabled'); } }; var updateWireFrame = function() { // Test support if (supportsNonSS) {return;} var rule = '#workarea.wireframe #svgcontent * { stroke-width: ' + 1/svgCanvas.getZoom() + 'px; }'; $('#wireframe_rules').text(workarea.hasClass('wireframe') ? rule : ''); }; var updateTitle = function(title) { title = title || svgCanvas.getDocumentTitle(); var newTitle = origTitle + (title ? ': ' + title : ''); // Remove title update with current context info, isn't really necessary // if (cur_context) { // new_title = new_title + cur_context; // } $('title:first').text(newTitle); }; // called when we've selected a different element var selectedChanged = function(win, elems) { var mode = svgCanvas.getMode(); if (mode === 'select') { setSelectMode(); } var is_node = (mode == "pathedit"); // if elems[1] is present, then we have more than one element selectedElement = (elems.length === 1 || elems[1] == null ? elems[0] : null); multiselected = (elems.length >= 2 && elems[1] != null); if (selectedElement != null) { // unless we're already in always set the mode of the editor to select because // upon creation of a text element the editor is switched into // select mode and this event fires - we need our UI to be in sync if (!is_node) { updateToolbar(); } } // if (elem != null) // Deal with pathedit mode togglePathEditMode(is_node, elems); updateContextPanel(); svgCanvas.runExtensions('selectedChanged', { elems: elems, selectedElement: selectedElement, multiselected: multiselected }); }; // Call when part of element is in process of changing, generally // on mousemove actions like rotate, move, etc. var elementTransition = function(win, elems) { var mode = svgCanvas.getMode(); var elem = elems[0]; if (!elem) { return; } multiselected = (elems.length >= 2 && elems[1] != null); // Only updating fields for single elements for now if (!multiselected) { switch (mode) { case 'rotate': var ang = svgCanvas.getRotationAngle(elem); $('#angle').val(ang); $('#tool_reorient').toggleClass('disabled', ang === 0); break; // TODO: Update values that change on move/resize, etc // case "select": // case "resize": // break; } } svgCanvas.runExtensions('elementTransition', { elems: elems }); }; // called when any element has changed var elementChanged = function(win, elems) { var i, mode = svgCanvas.getMode(); if (mode === 'select') { setSelectMode(); } for (i = 0; i < elems.length; ++i) { var elem = elems[i]; // if the element changed was the svg, then it could be a resolution change if (elem && elem.tagName === 'svg') { populateLayers(); updateCanvas(); } // Update selectedElement if element is no longer part of the image. // This occurs for the text elements in Firefox else if (elem && selectedElement && selectedElement.parentNode == null) { // || elem && elem.tagName == "path" && !multiselected) { // This was added in r1430, but not sure why selectedElement = elem; } } editor.showSaveWarning = true; // we update the contextual panel with potentially new // positional/sizing information (we DON'T want to update the // toolbar here as that creates an infinite loop) // also this updates the history buttons // we tell it to skip focusing the text control if the // text element was previously in focus updateContextPanel(); // In the event a gradient was flipped: if (selectedElement && mode === 'select') { paintBox.fill.update(); paintBox.stroke.update(); } svgCanvas.runExtensions('elementChanged', { elems: elems }); }; var zoomDone = function() { updateWireFrame(); // updateCanvas(); // necessary? }; var zoomChanged = svgCanvas.zoomChanged = function(win, bbox, autoCenter) { var scrbar = 15, // res = svgCanvas.getResolution(), // Currently unused w_area = workarea; // var canvas_pos = $('#svgcanvas').position(); // Currently unused var z_info = svgCanvas.setBBoxZoom(bbox, w_area.width()-scrbar, w_area.height()-scrbar); if (!z_info) {return;} var zoomlevel = z_info.zoom, bb = z_info.bbox; if (zoomlevel < 0.001) { changeZoom({value: 0.1}); return; } $('#zoom').val((zoomlevel*100).toFixed(1)); if (autoCenter) { updateCanvas(); } else { updateCanvas(false, {x: bb.x * zoomlevel + (bb.width * zoomlevel)/2, y: bb.y * zoomlevel + (bb.height * zoomlevel)/2}); } if (svgCanvas.getMode() == 'zoom' && bb.width) { // Go to select if a zoom box was drawn setSelectMode(); } zoomDone(); }; changeZoom = function(ctl) { var zoomlevel = ctl.value / 100; if (zoomlevel < 0.001) { ctl.value = 0.1; return; } var zoom = svgCanvas.getZoom(); var w_area = workarea; zoomChanged(window, { width: 0, height: 0, // center pt of scroll position x: (w_area[0].scrollLeft + w_area.width()/2)/zoom, y: (w_area[0].scrollTop + w_area.height()/2)/zoom, zoom: zoomlevel }, true); }; $('#cur_context_panel').delegate('a', 'click', function() { var link = $(this); if (link.attr('data-root')) { svgCanvas.leaveContext(); } else { svgCanvas.setContext(link.text()); } svgCanvas.clearSelection(); return false; }); var contextChanged = function(win, context) { var link_str = ''; if (context) { var str = ''; link_str = '' + svgCanvas.getCurrentDrawing().getCurrentLayerName() + ''; $(context).parentsUntil('#svgcontent > g').andSelf().each(function() { if (this.id) { str += ' > ' + this.id; if (this !== context) { link_str += ' > ' + this.id + ''; } else { link_str += ' > ' + this.id; } } }); cur_context = str; } else { cur_context = null; } $('#cur_context_panel').toggle(!!context).html(link_str); updateTitle(); }; // Makes sure the current selected paint is available to work with var prepPaints = function() { paintBox.fill.prep(); paintBox.stroke.prep(); }; var flyout_funcs = {}; var setFlyoutTitles = function() { $('.tools_flyout').each(function() { var shower = $('#' + this.id + '_show'); if (shower.data('isLibrary')) { return; } var tooltips = []; $(this).children().each(function() { tooltips.push(this.title); }); shower[0].title = tooltips.join(' / '); }); }; var setFlyoutPositions = function() { $('.tools_flyout').each(function() { var shower = $('#' + this.id + '_show'); var pos = shower.offset(); var w = shower.outerWidth(); $(this).css({left: (pos.left + w) * editor.tool_scale, top: pos.top}); }); }; var setupFlyouts = function(holders) { $.each(holders, function(hold_sel, btn_opts) { var buttons = $(hold_sel).children(); var show_sel = hold_sel + '_show'; var shower = $(show_sel); var def = false; buttons.addClass('tool_button') .unbind('click mousedown mouseup') // may not be necessary .each(function(i) { // Get this buttons options var opts = btn_opts[i]; // Remember the function that goes with this ID flyout_funcs[opts.sel] = opts.fn; if (opts.isDefault) {def = i;} // Clicking the icon in flyout should set this set's icon var func = function(event) { var options = opts; //find the currently selected tool if comes from keystroke if (event.type === 'keydown') { var flyoutIsSelected = $(options.parent + '_show').hasClass('tool_button_current'); var currentOperation = $(options.parent + '_show').attr('data-curopt'); $.each(holders[opts.parent], function(i, tool) { if (tool.sel == currentOperation) { if (!event.shiftKey || !flyoutIsSelected) { options = tool; } else { options = holders[opts.parent][i+1] || holders[opts.parent][0]; } } }); } if ($(this).hasClass('disabled')) {return false;} if (toolButtonClick(show_sel)) { options.fn(); } var icon; if (options.icon) { icon = $.getSvgIcon(options.icon, true); } else { icon = $(options.sel).children().eq(0).clone(); } icon[0].setAttribute('width', shower.width()); icon[0].setAttribute('height', shower.height()); shower.children(':not(.flyout_arrow_horiz)').remove(); shower.append(icon).attr('data-curopt', options.sel); // This sets the current mode }; $(this).mouseup(func); if (opts.key) { $(document).bind('keydown', opts.key[0] + ' shift+' + opts.key[0], func); } }); if (def) { shower.attr('data-curopt', btn_opts[def].sel); } else if (!shower.attr('data-curopt')) { // Set first as default shower.attr('data-curopt', btn_opts[0].sel); } var timer; var pos = $(show_sel).position(); // Clicking the "show" icon should set the current mode shower.mousedown(function(evt) { if (shower.hasClass('disabled')) { return false; } var holder = $(hold_sel); var l = pos.left + 34; var w = holder.width() * -1; var time = holder.data('shown_popop') ? 200 : 0; timer = setTimeout(function() { // Show corresponding menu if (!shower.data('isLibrary')) { holder.css('left', w).show().animate({ left: l }, 150); } else { holder.css('left', l).show(); } holder.data('shown_popop', true); },time); evt.preventDefault(); }).mouseup(function(evt) { clearTimeout(timer); var opt = $(this).attr('data-curopt'); // Is library and popped up, so do nothing if (shower.data('isLibrary') && $(show_sel.replace('_show', '')).is(':visible')) { toolButtonClick(show_sel, true); return; } if (toolButtonClick(show_sel) && flyout_funcs[opt]) { flyout_funcs[opt](); } }); // $('#tools_rect').mouseleave(function(){$('#tools_rect').fadeOut();}); }); setFlyoutTitles(); setFlyoutPositions(); }; var makeFlyoutHolder = function(id, child) { var div = $('

', { 'class': 'tools_flyout', id: id }).appendTo('#svg_editor').append(child); return div; }; var uaPrefix = (function() { var prop; var regex = /^(Moz|Webkit|Khtml|O|ms|Icab)(?=[A-Z])/; var someScript = document.getElementsByTagName('script')[0]; for (prop in someScript.style) { if (regex.test(prop)) { // test is faster than match, so it's better to perform // that on the lot and match only when necessary return prop.match(regex)[0]; } } // Nothing found so far? if ('WebkitOpacity' in someScript.style) {return 'Webkit';} if ('KhtmlOpacity' in someScript.style) {return 'Khtml';} return ''; }()); var scaleElements = function(elems, scale) { // var prefix = '-' + uaPrefix.toLowerCase() + '-'; // Currently unused var sides = ['top', 'left', 'bottom', 'right']; elems.each(function() { // Handled in CSS // this.style[uaPrefix + 'Transform'] = 'scale(' + scale + ')'; var i; var el = $(this); var w = el.outerWidth() * (scale - 1); var h = el.outerHeight() * (scale - 1); // var margins = {}; // Currently unused for (i = 0; i < 4; i++) { var s = sides[i]; var cur = el.data('orig_margin-' + s); if (cur == null) { cur = parseInt(el.css('margin-' + s), 10); // Cache the original margin el.data('orig_margin-' + s, cur); } var val = cur * scale; if (s === 'right') { val += w; } else if (s === 'bottom') { val += h; } el.css('margin-' + s, val); // el.css('outline', '1px solid red'); } }); }; var setIconSize = editor.setIconSize = function (size) { // var elems = $('.tool_button, .push_button, .tool_button_current, .disabled, .icon_label, #url_notice, #tool_open'); var sel_toscale = '#tools_top .toolset, #editor_panel > *, #history_panel > *,'+ ' #main_button, #tools_left > *, #path_node_panel > *, #multiselected_panel > *,'+ ' #g_panel > *, #tool_font_size > *, .tools_flyout'; var elems = $(sel_toscale); var scale = 1; if (typeof size === 'number') { scale = size; } else { var icon_sizes = {s: 0.75, m:1, l: 1.25, xl: 1.5}; scale = icon_sizes[size]; } editor.tool_scale = scale; setFlyoutPositions(); // $('.tools_flyout').each(function() { // var pos = $(this).position(); // console.log($(this), pos.left+(34 * scale)); // $(this).css({'left': pos.left+(34 * scale), 'top': pos.top+(77 * scale)}); // console.log('l', $(this).css('left')); // }); // var scale = .75; var hidden_ps = elems.parents(':hidden'); hidden_ps.css('visibility', 'hidden').show(); scaleElements(elems, scale); hidden_ps.css('visibility', 'visible').hide(); // return; $.pref('iconsize', size); $('#iconsize').val(size); // Change icon size // $('.tool_button, .push_button, .tool_button_current, .disabled, .icon_label, #url_notice, #tool_open') // .find('> svg, > img').each(function() { // this.setAttribute('width',size_num); // this.setAttribute('height',size_num); // }); // // $.resizeSvgIcons({ // '.flyout_arrow_horiz > svg, .flyout_arrow_horiz > img': size_num / 5, // '#logo > svg, #logo > img': size_num * 1.3, // '#tools_bottom .icon_label > *': (size_num === 16 ? 18 : size_num * .75) // }); // if (size != 's') { // $.resizeSvgIcons({'#layerbuttons svg, #layerbuttons img': size_num * .6}); // } // Note that all rules will be prefixed with '#svg_editor' when parsed var cssResizeRules = { // '.tool_button,\ // .push_button,\ // .tool_button_current,\ // .push_button_pressed,\ // .disabled,\ // .icon_label,\ // .tools_flyout .tool_button': { // 'width': {s: '16px', l: '32px', xl: '48px'}, // 'height': {s: '16px', l: '32px', xl: '48px'}, // 'padding': {s: '1px', l: '2px', xl: '3px'} // }, // '.tool_sep': { // 'height': {s: '16px', l: '32px', xl: '48px'}, // 'margin': {s: '2px 2px', l: '2px 5px', xl: '2px 8px'} // }, // '#main_icon': { // 'width': {s: '31px', l: '53px', xl: '75px'}, // 'height': {s: '22px', l: '42px', xl: '64px'} // }, '#tools_top': { 'left': 50, 'height': 72 }, '#tools_left': { 'width': 31, 'top': 74 }, 'div#workarea': { 'left': 38, 'top': 74 } // '#tools_bottom': { // 'left': {s: '27px', l: '46px', xl: '65px'}, // 'height': {s: '58px', l: '98px', xl: '145px'} // }, // '#color_tools': { // 'border-spacing': {s: '0 1px'}, // 'margin-top': {s: '-1px'} // }, // '#color_tools .icon_label': { // 'width': {l:'43px', xl: '60px'} // }, // '.color_tool': { // 'height': {s: '20px'} // }, // '#tool_opacity': { // 'top': {s: '1px'}, // 'height': {s: 'auto', l:'auto', xl:'auto'} // }, // '#tools_top input, #tools_bottom input': { // 'margin-top': {s: '2px', l: '4px', xl: '5px'}, // 'height': {s: 'auto', l: 'auto', xl: 'auto'}, // 'border': {s: '1px solid #555', l: 'auto', xl: 'auto'}, // 'font-size': {s: '.9em', l: '1.2em', xl: '1.4em'} // }, // '#zoom_panel': { // 'margin-top': {s: '3px', l: '4px', xl: '5px'} // }, // '#copyright, #tools_bottom .label': { // 'font-size': {l: '1.5em', xl: '2em'}, // 'line-height': {s: '15px'} // }, // '#tools_bottom_2': { // 'width': {l: '295px', xl: '355px'}, // 'top': {s: '4px'} // }, // '#tools_top > div, #tools_top': { // 'line-height': {s: '17px', l: '34px', xl: '50px'} // }, // '.dropdown button': { // 'height': {s: '18px', l: '34px', xl: '40px'}, // 'line-height': {s: '18px', l: '34px', xl: '40px'}, // 'margin-top': {s: '3px'} // }, // '#tools_top label, #tools_bottom label': { // 'font-size': {s: '1em', l: '1.5em', xl: '2em'}, // 'height': {s: '25px', l: '42px', xl: '64px'} // }, // 'div.toolset': { // 'height': {s: '25px', l: '42px', xl: '64px'} // }, // '#tool_bold, #tool_italic': { // 'font-size': {s: '1.5em', l: '3em', xl: '4.5em'} // }, // '#sidepanels': { // 'top': {s: '50px', l: '88px', xl: '125px'}, // 'bottom': {s: '51px', l: '68px', xl: '65px'} // }, // '#layerbuttons': { // 'width': {l: '130px', xl: '175px'}, // 'height': {l: '24px', xl: '30px'} // }, // '#layerlist': { // 'width': {l: '128px', xl: '150px'} // }, // '.layer_button': { // 'width': {l: '19px', xl: '28px'}, // 'height': {l: '19px', xl: '28px'} // }, // 'input.spin-button': { // 'background-image': {l: 'url('images/spinbtn_updn_big.png')', xl: 'url('images/spinbtn_updn_big.png')'}, // 'background-position': {l: '100% -5px', xl: '100% -2px'}, // 'padding-right': {l: '24px', xl: '24px' } // }, // 'input.spin-button.up': { // 'background-position': {l: '100% -45px', xl: '100% -42px'} // }, // 'input.spin-button.down': { // 'background-position': {l: '100% -85px', xl: '100% -82px'} // }, // '#position_opts': { // 'width': {all: (size_num*4) +'px'} // } }; var rule_elem = $('#tool_size_rules'); if (!rule_elem.length) { rule_elem = $('