/*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: [], }, defaultExtensions = [ //'ext-overview_window.js', //'ext-markers.js', //'ext-connector.js', //'ext-eyedropper.js', //'ext-shapes.js', //'ext-imagelib.js', 'ext-server_opensave.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: 'mm', 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\' ...' } }; //alert('QQ'); 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 (key === 'extensions') { if (cfgCfg.overwrite === false && (curConfig.preventAllURLConfig || curConfig.lockExtensions) ) { return; } curConfig.extensions = curConfig.extensions.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 //alert('359:unit='+editor.curConfig.baseUnit); }; /** * @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 if (!curConfig.noDefaultExtensions) { curConfig.extensions = curConfig.extensions.concat(defaultExtensions); } // ...and remove any dupes curConfig.extensions = $.grep(curConfig.extensions, function (n, i) { return i === curConfig.extensions.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.loadFromURL(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_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); var applyLayerEyeVisibility = 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); }; $('#layerlist td.layervis').each(applyLayerEyeVisibility); svgCanvas.setLayerVisibility(this.textContent, true); 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 initPopulatedLayers = function() { $('#layerlist td.layername').first().mouseup(); }; 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, useImageData) { if (!url) { url = defaultImageURL; } useImageData = 0; var setImageUrlOrData = function(url){ 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(); } }; //all solution ideas are from https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Sending_and_Receiving_Binary_Data //It is more reliable not to store original link and get image content on stage od saving to DB or even generation, as backend and moreover generation script may doesn't have access to the image url (which can be accessed using cookies and so on) if(useImageData){ var loadBinary = function(url,callback) { var xmlHttpReq = false; //alert(q); if (window.XMLHttpRequest) { xmlHttpReq = new XMLHttpRequest(); } else if (window.ActiveXObject) { xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP"); } xmlHttpReq.open('GET', url, true); //XHR binary charset opt by Marcus Granado 2006 [http://mgran.blogspot.com] //this doesn't work - data are broken anyway //if(xmlHttpReq.overrideMimeType){ // xmlHttpReq.overrideMimeType('text\/plain; charset=x-user-defined'); //} xmlHttpReq.responseType = "arraybuffer"; if(callback){ xmlHttpReq.onreadystatechange = function(){ if (xmlHttpReq.readyState == 4) { if(typeof callback == 'function'){ callback(xmlHttpReq); }else{ eval(callback); } } } } xmlHttpReq.send(null); }; //two functions from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Base64_encoding_and_decoding#Solution_.232_.E2.80.93_rewriting_atob()_and_btoa()_using_TypedArrays_and_UTF-8 function uint6ToB64 (nUint6) { return nUint6 < 26 ? nUint6 + 65 : nUint6 < 52 ? nUint6 + 71 : nUint6 < 62 ? nUint6 - 4 : nUint6 === 62 ? 43 : nUint6 === 63 ? 47 : 65; }; function base64EncArr (aBytes) { var nMod3 = 2, sB64Enc = ""; for (var nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { nMod3 = nIdx % 3; if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); if (nMod3 === 2 || aBytes.length - nIdx === 1) { sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); nUint24 = 0; } } return sB64Enc.substr(0, sB64Enc.length - 2 + nMod3) + (nMod3 === 2 ? '' : nMod3 === 1 ? '=' : '=='); }; var imageDataCallback = function(request){ var contentType = ''+request.getResponseHeader('Content-Type'); contentType = contentType.replace(/image\/x\-/i,'image/'); var content = ''; //if(request.responseBody){//ie // content = request.responseBody; // url = 'data:'+contentType+';base64,'+svgedit.utilities.encode64(content); //}else if(request.response){ content = request.response; var byteArray = new Uint8Array(content); url = 'data:'+contentType+';base64,'+base64EncArr(byteArray); //}else{ // content = request.responseText; // url = 'data:'+contentType+';base64,'+svgedit.utilities.encode64(content); //} setImageUrlOrData(url); }; loadBinary(url,imageDataCallback); }else{ setImageUrlOrData(url); } }; 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, 1);}//useImageData=1 }); } 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(); initPopulatedLayers(); 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; //alert('zoomlevel='+zoomlevel+'; bb.width='+bb.width+';bb.height='+bb.height); 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; //alert('zoomLevel='+zoomlevel); 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); }; //changeZoom({value: 200}); $('#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 = $('