diff --git a/editor/config-sample.js b/editor/config-sample.js new file mode 100644 index 00000000..39118a2a --- /dev/null +++ b/editor/config-sample.js @@ -0,0 +1,138 @@ +// DO NOT EDIT THIS FILE! +// THIS FILE IS JUST A SAMPLE; TO APPLY, YOU MUST +// CREATE A NEW FILE config.js AND ADD CONTENTS +// SUCH AS SHOWN BELOW INTO THAT FILE. + +/*globals svgEditor*/ +/* +The config.js file is intended for the setting of configuration or + preferences which must run early on; if this is not needed, it is + recommended that you create an extension instead (for greater + reusability and modularity). +*/ + +// CONFIG AND EXTENSION SETTING +/* +See defaultConfig and defaultExtensions in svg-editor.js for a list + of possible configuration settings. + +See svg-editor.js for documentation on using setConfig(). +*/ + +// URL OVERRIDE CONFIG +svgEditor.setConfig({ + /** + To override the ability for URLs to set URL-based SVG content, + uncomment the following: + */ + // preventURLContentLoading: true, + /** + To override the ability for URLs to set other configuration (including + extensions), uncomment the following: + */ + // preventAllURLConfig: true, + /** + To override the ability for URLs to set their own extensions, + uncomment the following (note that if setConfig() is used in + extension code, it will still be additive to extensions, + however): + */ + // lockExtensions: true, +}); + +svgEditor.setConfig({ + /* + Provide default values here which differ from that of the editor but + which the URL can override + */ +}, {allowInitialUserOverride: true}); + +// EXTENSION CONFIG +svgEditor.setConfig({ + extensions: [ + // 'ext-overview_window.js', 'ext-markers.js', 'ext-connector.js', 'ext-eyedropper.js', 'ext-shapes.js', 'ext-imagelib.js', 'ext-grid.js', 'ext-polygon.js', 'ext-star.js', 'ext-panning.js', 'ext-storage.js' + ] + // , noDefaultExtensions: false, // noDefaultExtensions can only be meaningfully used in config.js or in the URL +}); + +// OTHER CONFIG +svgEditor.setConfig({ + // 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 + // imgPath: 'images/', + // langPath: 'locale/', + // extPath: 'extensions/', + // jGraduatePath: 'jgraduate/images/', + // DOCUMENT PROPERTIES + // dimensions: [640, 480], + // EDITOR OPTIONS + // gridSnapping: false, + // gridColor: '#000', + // baseUnit: 'px', + // snappingStep: 10, + // showRulers: true, + // 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: true, // Used by ext-storage.js; empty any prior storage if the user declines to store +}); + +// PREF CHANGES +/** +setConfig() can also be used to set preferences in addition to + configuration (see defaultPrefs in svg-editor.js for a list of + possible settings), but at least if you are using ext-storage.js + to store preferences, it will probably be better to let your + users control these. +As with configuration, one may use allowInitialUserOverride, but + in the case of preferences, any previously stored preferences + will also thereby be enabled to override this setting (and at a + higher priority than any URL preference setting overrides). + Failing to use allowInitialUserOverride will ensure preferences + are hard-coded here regardless of URL or prior user storage setting. +*/ +svgEditor.setConfig( + { + // lang: '', // Set dynamically within locale.js if not previously set + // iconsize: '', // Will default to 's' if the window height is smaller than the minimum height and 'm' otherwise + /** + * When showing the preferences dialog, svg-editor.js currently relies + * on curPrefs instead of $.pref, so allowing an override for bkgd_color + * means that this value won't have priority over block auto-detection as + * far as determining which color shows initially in the preferences + * dialog (though it can be changed and saved). + */ + // bkgd_color: '#FFF', + // bkgd_url: '', + // img_save: 'embed', + // Only shows in UI as far as alert notices + // save_notice_done: false, + // export_notice_done: false + } +); +svgEditor.setConfig( + { + // Indicate pref settings here if you wish to allow user storage or URL settings + // to be able to override your default preferences (unless other config options + // have already explicitly prevented one or the other) + }, + {allowInitialUserOverride: true} +); diff --git a/editor/extensions/ext-mathjax.js b/editor/extensions/ext-mathjax.js index db88704a..6d6e9944 100644 --- a/editor/extensions/ext-mathjax.js +++ b/editor/extensions/ext-mathjax.js @@ -43,7 +43,7 @@ svgEditor.addExtension("mathjax", function() {'use strict'; mathjaxLoaded = false, uiStrings = svgEditor.uiStrings; - // TODO: Implement language support. + // TODO: Implement language support. Move these uiStrings to the locale files and the code to the langReady callback. $.extend(uiStrings, { mathjax: { embed_svg: 'Save as mathematics', diff --git a/editor/extensions/ext-storage.js b/editor/extensions/ext-storage.js new file mode 100644 index 00000000..709933cb --- /dev/null +++ b/editor/extensions/ext-storage.js @@ -0,0 +1,281 @@ +/*globals svgEditor, svgCanvas, $, widget*/ +/*jslint vars: true, eqeq: true, regexp: true, continue: true*/ +/* + * ext-storage.js + * + * Licensed under the MIT License + * + * Copyright(c) 2010 Brett Zamir + * + */ +/** +* This extension allows automatic saving of the SVG canvas contents upon +* page unload (which can later be automatically retrieved upon future +* editor loads). +* +* The functionality was originally part of the SVG Editor, but moved to a +* separate extension to make the setting behavior optional, and adapted +* to inform the user of its setting of local data. +*/ + +/* +TODOS +1. Revisit on whether to use $.pref over directly setting curConfig in all + extensions for a more public API (not only for extPath and imagePath, + but other currently used config in the extensions) +2. We might provide control of storage settings through the UI besides the + initial (or URL-forced) dialog. +*/ +svgEditor.addExtension('storage', function() { + // We could empty any already-set data for users when they decline storage, + // but it would be a risk for users who wanted to store but accidentally + // said "no"; instead, we'll let those who already set it, delete it themselves; + // to change, set the "emptyStorageOnDecline" config setting to true + // in config.js. + var emptyStorageOnDecline = svgEditor.curConfig.emptyStorageOnDecline, + // When the code in svg-editor.js prevents local storage on load per + // user request, we also prevent storing on unload here so as to + // avoid third-party sites making XSRF requests or providing links + // which would cause the user's local storage not to load and then + // upon page unload (such as the user closing the window), the storage + // would thereby be set with an empty value, erasing any of the + // user's prior work. To change this behavior so that no use of storage + // or adding of new storage takes place regardless of settings, set + // the "noStorageOnLoad" config setting to true in config.js. + noStorageOnLoad = svgEditor.curConfig.noStorageOnLoad, + forceStorage = svgEditor.curConfig.forceStorage; + + function replaceStoragePrompt (val) { + val = val ? 'storagePrompt=' + val : ''; + if (window.location.href.indexOf('storagePrompt=') > -1) { + window.location.href = window.location.href.replace(/([&?])storagePrompt=[^&]*(&?)/, function (n0, n1, amp) { + return (val ? n1 : '') + val + (!val && amp ? n1 : (amp || '')); + }); + } + else { + window.location.href += (window.location.href.indexOf('?') > -1 ? '&' : '?') + val; + } + } + function setSVGContentStorage (val) { + if ('localStorage' in window) { + var name = 'svgedit-' + svgEditor.curConfig.canvasName; + if (!val) { + window.localStorage.removeItem(name); + } + else { + window.localStorage.setItem(name, val); + } + } + } + function removeStoragePrefCookie () { + document.cookie = 'store=; expires=Thu, 01 Jan 1970 00:00:00 GMT'; + } + function emptyLocalStorage() { + setSVGContentStorage(''); + var name; + if ('localStorage' in window) { + for (name in svgEditor.curPrefs) { + if (svgEditor.curPrefs.hasOwnProperty(name)) { + window.localStorage.removeItem(name); + } + } + } + } + +// emptyLocalStorage(); + + /** + * Listen for unloading: If and only if opted in by the user, set the content + * document and preferences into storage: + * 1. Prevent save warnings (since we're automatically saving unsaved + * content into storage) + * 2. Use localStorage to set SVG contents (potentially too large to allow in cookies) + * 3. Use localStorage (where available) or cookies to set preferences. + */ + function setupBeforeUnloadListener () { + window.addEventListener('beforeunload', function(e) { + // Don't save anything unless the user opted in to storage + if (!document.cookie.match(/(?:^|;\s*)store=(?:prefsAndContent|prefsOnly)/)) { + return; + } + var key; + if (document.cookie.match(/(?:^|;\s*)store=prefsAndContent/)) { + setSVGContentStorage(svgCanvas.getSvgString()); + } + + svgEditor.setConfig({no_save_warning: true}); // No need for explicit saving at all once storage is on + // svgEditor.showSaveWarning = false; + + var curPrefs = svgEditor.curPrefs; + for (key in curPrefs) { + if (curPrefs.hasOwnProperty(key)) { // It's our own config, so we don't need to iterate up the prototype chain + var storage = svgEditor.storage, + val = curPrefs[key], + store = (val != undefined); + key = 'svg-edit-' + key; + if (!store) { + continue; + } + if (storage) { + storage.setItem(key, val); + } + else if (window.widget) { + widget.setPreferenceForKey(val, key); + } + else { + val = encodeURIComponent(val); + document.cookie = key + '=' + val + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; + } + } + } + }, false); + } + + /* + // We could add locales here instead (and also thereby avoid the need + // to keep our content within "langReady"), but this would be less + // convenient for translators. + $.extend(uiStrings, {confirmSetStorage: { + message: "By default and where supported, SVG-Edit can store your editor "+ + "preferences and SVG content locally on your machine so you do not "+ + "need to add these back each time you load SVG-Edit. If, for privacy "+ + "reasons, you do not wish to store this information on your machine, "+ + "you can change away from the default option below.", + storagePrefsAndContent: "Store preferences and SVG content locally", + storagePrefsOnly: "Only store preferences locally", + storagePrefs: "Store preferences locally", + storageNoPrefsOrContent: "Do not store my preferences or SVG content locally", + storageNoPrefs: "Do not store my preferences locally", + rememberLabel: "Remember this choice?", + rememberTooltip: "If you choose to opt out of storage while remembering this choice, the URL will change so as to avoid asking again." + }}); + */ + var loaded = false; + return { + name: 'storage', + langReady: function (data) { + var // lang = data.lang, + uiStrings = data.uiStrings, // No need to store as dialog should only run once + storagePrompt = $.deparam.querystring(true).storagePrompt; + + // No need to run this one-time dialog again just because the user + // changes the language + if (loaded) { + return; + } + loaded = true; + + // Note that the following can load even if "noStorageOnLoad" is + // set to false; to avoid any chance of storage, avoid this + // extension! (and to avoid using any prior storage, set the + // config option "noStorageOnLoad" to true). + if (!forceStorage && ( + // If the URL has been explicitly set to always prompt the + // user (e.g., so one can be pointed to a URL where one + // can alter one's settings, say to prevent future storage)... + storagePrompt === true || + ( + // ...or...if the URL at least doesn't explicitly prevent a + // storage prompt (as we use for users who + // don't want to set cookies at all but who don't want + // continual prompts about it)... + storagePrompt !== false && + // ...and this user hasn't previously indicated a desire for storage + !document.cookie.match(/(?:^|;\s*)store=(?:prefsAndContent|prefsOnly)/) + ) + // ...then show the storage prompt. + )) { + + var options = []; + if ('localStorage' in window) { + options.unshift( + {value: 'prefsAndContent', text: uiStrings.confirmSetStorage.storagePrefsAndContent}, + {value: 'prefsOnly', text: uiStrings.confirmSetStorage.storagePrefsOnly}, + {value: 'noPrefsOrContent', text: uiStrings.confirmSetStorage.storageNoPrefsOrContent} + ); + } + else { + options.unshift( + {value: 'prefsOnly', text: uiStrings.confirmSetStorage.storagePrefs}, + {value: 'noPrefsOrContent', text: uiStrings.confirmSetStorage.storageNoPrefs} + ); + } + + // Hack to temporarily provide a wide and high enough dialog + var oldContainerWidth = $('#dialog_container')[0].style.width, + oldContainerMarginLeft = $('#dialog_container')[0].style.marginLeft, + oldContentHeight = $('#dialog_content')[0].style.height, + oldContainerHeight = $('#dialog_container')[0].style.height; + $('#dialog_content')[0].style.height = '120px'; + $('#dialog_container')[0].style.height = '170px'; + $('#dialog_container')[0].style.width = '800px'; + $('#dialog_container')[0].style.marginLeft = '-400px'; + + // Open select-with-checkbox dialog + $.select( + uiStrings.confirmSetStorage.message, + options, + function (pref, checked) { + if (pref && pref !== 'noPrefsOrContent') { + // Regardless of whether the user opted + // to remember the choice (and move to a URL which won't + // ask them again), we have to assume the user + // doesn't even want to remember their not wanting + // storage, so we don't set the cookie or continue on with + // setting storage on beforeunload + document.cookie = 'store=' + pref + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; // 'prefsAndContent' | 'prefsOnly' + // If the URL was configured to always insist on a prompt, if + // the user does indicate a wish to store their info, we + // don't want ask them again upon page refresh so move + // them instead to a URL which does not always prompt + if (storagePrompt === true && checked) { + replaceStoragePrompt(); + return; + } + } + else { // The user does not wish storage (or cancelled, which we treat equivalently) + removeStoragePrefCookie(); + if (emptyStorageOnDecline) { + emptyLocalStorage(); + } + if (pref && checked) { + // Open a URL which won't set storage and won't prompt user about storage + replaceStoragePrompt('false'); + return; + } + } + + // Reset width/height of dialog (e.g., for use by Export) + $('#dialog_container')[0].style.width = oldContainerWidth; + $('#dialog_container')[0].style.marginLeft = oldContainerMarginLeft; + $('#dialog_content')[0].style.height = oldContentHeight; + $('#dialog_container')[0].style.height = oldContainerHeight; + + // It should be enough to (conditionally) add to storage on + // beforeunload, but if we wished to update immediately, + // we might wish to try setting: + // svgEditor.setConfig({noStorageOnLoad: true}); + // and then call: + // svgEditor.loadContentAndPrefs(); + + // We don't check for noStorageOnLoad here because + // the prompt gives the user the option to store data + setupBeforeUnloadListener(); + + svgEditor.storagePromptClosed = true; + }, + null, + null, + { + label: uiStrings.confirmSetStorage.rememberLabel, + checked: false, + tooltip: uiStrings.confirmSetStorage.rememberTooltip + } + ); + } + else if (!noStorageOnLoad || forceStorage) { + setupBeforeUnloadListener(); + } + } + }; +}); diff --git a/editor/locale/lang.en.js b/editor/locale/lang.en.js index c5c2ff31..e670d276 100644 --- a/editor/locale/lang.en.js +++ b/editor/locale/lang.en.js @@ -1,5 +1,19 @@ /*globals svgEditor */ svgEditor.readLang({ + confirmSetStorage: { + message: "By default and where supported, SVG-Edit can store your editor "+ + "preferences and SVG content locally on your machine so you do not "+ + "need to add these back each time you load SVG-Edit. If, for privacy "+ + "reasons, you do not wish to store this information on your machine, "+ + "you can change away from the default option below.", + storagePrefsAndContent: "Store preferences and SVG content locally", + storagePrefsOnly: "Only store preferences locally", + storagePrefs: "Store preferences locally", + storageNoPrefsOrContent: "Do not store my preferences or SVG content locally", + storageNoPrefs: "Do not store my preferences locally", + rememberLabel: "Remember this choice?", + rememberTooltip: "If you choose to opt out of storage while remembering this choice, the URL will change so as to avoid asking again." + }, lang: "en", dir : "ltr", common: { diff --git a/editor/locale/locale.js b/editor/locale/locale.js index aca3d0fb..d592e70c 100644 --- a/editor/locale/locale.js +++ b/editor/locale/locale.js @@ -15,7 +15,7 @@ // 2) svgcanvas.js // 3) svg-editor.js -var svgEditor = (function($, Editor) {'use strict'; +var svgEditor = (function($, editor) {'use strict'; var lang_param; @@ -54,10 +54,10 @@ var svgEditor = (function($, Editor) {'use strict'; } } - Editor.readLang = function(langData) { - var more = Editor.canvas.runExtensions("addlangData", lang_param, true); + editor.readLang = function(langData) { + var more = editor.canvas.runExtensions("addlangData", lang_param, true); $.each(more, function(i, m) { - if(m.data) { + if (m.data) { langData = $.merge(langData, m.data); } }); @@ -272,10 +272,10 @@ var svgEditor = (function($, Editor) {'use strict'; }, true); - Editor.setLang(lang_param, langData); + editor.setLang(lang_param, langData); }; - Editor.putLocale = function (given_param, good_langs) { + editor.putLocale = function (given_param, good_langs) { if (given_param) { lang_param = given_param; @@ -286,10 +286,10 @@ var svgEditor = (function($, Editor) {'use strict'; if (navigator.userLanguage) { // Explorer lang_param = navigator.userLanguage; } - else if (navigator.language) {// FF, Opera, ... + else if (navigator.language) { // FF, Opera, ... lang_param = navigator.language; } - if (lang_param == '') { + if (lang_param == null) { // Todo: Would cause problems if uiStrings removed; remove this? return; } } @@ -302,11 +302,14 @@ var svgEditor = (function($, Editor) {'use strict'; } // don't bother on first run if language is English - if (lang_param.indexOf("en") === 0) {return;} + // The following line prevents setLang from running + // extensions which depend on updated uiStrings, + // so commenting it out. + // if (lang_param.indexOf("en") === 0) {return;} } - var conf = Editor.curConfig; + var conf = editor.curConfig; var url = conf.langPath + "lang." + lang_param + ".js"; @@ -321,5 +324,5 @@ var svgEditor = (function($, Editor) {'use strict'; }; - return Editor; + return editor; }(jQuery, svgEditor)); diff --git a/editor/svg-editor.html b/editor/svg-editor.html index 90f5dd8e..b115f2a0 100644 --- a/editor/svg-editor.html +++ b/editor/svg-editor.html @@ -49,13 +49,13 @@ - - + + - diff --git a/editor/svg-editor.js b/editor/svg-editor.js index b93a5264..5c5a2581 100644 --- a/editor/svg-editor.js +++ b/editor/svg-editor.js @@ -17,30 +17,75 @@ // 2) browser.js // 3) svgcanvas.js -(function() {'use strict'; +/* +TO-DOS +1. JSDoc +*/ + +(function() { if (window.svgEditor) { return; } window.svgEditor = (function($) { - var svgCanvas, - Editor = {}, + var editor = {}; + // EDITOR PROPERTIES: (defined below) + // curPrefs, curConfig, canvas, storage, uiStrings + // + // STATE MAINTENANCE PROPERTIES + editor.tool_scale = 1; // Dependent on icon size, so no need to make configurable? + editor.langChanged = false; + editor.showSaveWarning = false; + editor.storagePromptClosed = false; // For use with ext-storage.js + + var svgCanvas, urldata, isReady = false, + callbacks = [], + customHandlers = {}, + /** + * PREFS AND CONFIG + */ + // The iteration algorithm for defaultPrefs does not currently support array/objects defaultPrefs = { - lang: 'en', - iconsize: 'm', + // 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: '', - img_save: 'embed' + // 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: Difference between Prefs and Config is that Prefs can be - // changed in the UI and are stored in the browser, config can not + // 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-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, - dimensions: [640,480], initFill: { color: 'FF0000', // solid red opacity: 1 @@ -51,31 +96,45 @@ 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/', - extensions: [ - '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' - ], - initTool: 'select', - wireframe: false, - colorPickerCSS: null, + // 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 + 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 }, - uiStrings = Editor.uiStrings = { + /** + * 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', @@ -112,10 +171,9 @@ URLloadFail: 'Unable to load from URL', retrieving: 'Retrieving \'%s\' ...' } - }, - customHandlers = {}; + }; - function loadSvgString(str, callback) { + function loadSvgString (str, callback) { var success = svgCanvas.setSvgString(str) !== false; callback = callback || $.noop; if (success) { @@ -127,99 +185,213 @@ } } - Editor.curConfig = curConfig; - Editor.tool_scale = 1; - + /** + * 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 null or undefined, no change to the preference will be made. - * @returns {string} If val is not present (or is null or undefined), the value of the previously stored preference will be returned. + * @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) { + $.pref = function (key, val) { if (val) { curPrefs[key] = val; + editor.curPrefs = curPrefs; // Update exported value + return; } - key = 'svg-edit-' + key; - var d, result, - host = location.hostname, - onWeb = host && host.indexOf('.') >= 0, - store = (val != undefined), - storage = false; + return (key in curPrefs) ? curPrefs[key] : defaultPrefs[key]; + }; + + /** + * EDITOR PUBLIC 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 ('localStorage' in window && // 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 = window.localStorage.getItem(name); + if (cached) { + editor.loadFromString(cached); + } + } + + // LOAD PREFS + var key, storage = false; + // var host = location.hostname, + // onWeb = host && host.indexOf('.') >= 0; + // Some FF versions throw security errors here try { if (window.localStorage) { // && onWeb removed so Webkit works locally storage = localStorage; } - } catch(e) {} - try { - if (window.globalStorage && onWeb) { - storage = globalStorage[host]; - } - } catch(e2) {} + } catch(err) {} + editor.storage = storage; - if (storage) { - if (store) { - storage.setItem(key, val); - } else if (storage.getItem(key)) { - return String(storage.getItem(key)); // Convert to string for FF (.value fails in Webkit) - } - } else if (window.widget) { - if (store) { - widget.setPreferenceForKey(val, key); - } else { - return widget.preferenceForKey(key); - } - } else { - if (store) { - d = new Date(); - d.setTime(d.getTime() + 31536000000); - val = encodeURIComponent(val); - document.cookie = key+'='+val+'; expires='+d.toUTCString(); - } else { - result = document.cookie.match(new RegExp(key + '=([^;]+)')); - return result ? decodeURIComponent(result[1]) : ''; + for (key in defaultPrefs) { + if (defaultPrefs.hasOwnProperty(key)) { // It's our own config, so we don't need to iterate up the prototype chain + var storeKey = 'svg-edit-' + key; + if (storage) { + var val = storage.getItem(storeKey); + if (val) { + 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*)' + storeKey + '=([^;]+)')); + defaultPrefs[key] = result ? decodeURIComponent(result[1]) : ''; + } } } }; - Editor.setConfig = function(opts) { + /** + * 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) { - // Only allow prefs defined in defaultPrefs - if (defaultPrefs[key]) { - $.pref(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; + } + } + } + } } }); - $.extend(true, curConfig, opts); - if (opts.extensions) { - curConfig.extensions = opts.extensions; - } + editor.curConfig = curConfig; // Update exported value }; - // 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() { + /** + * @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; + editor.showSaveWarning = false; svgCanvas.bind('saved', opts.save); } if (opts.exportImage || opts.pngsave) { // Deprecating pngsave @@ -229,30 +401,40 @@ }); }; - Editor.randomizeIds = function() { + editor.randomizeIds = function () { svgCanvas.randomizeIds(arguments); }; - Editor.init = function() { - // For external openers - (function() { - // let the opener know SVG Edit is ready - var svgEditorReadyEvent, - w = window.opener; - if (w) { - try { - svgEditorReadyEvent = w.document.createEvent('Event'); - svgEditorReadyEvent.initEvent('svgEditorReady', true, true); - w.document.documentElement.dispatchEvent(svgEditorReadyEvent); - } - catch(e) {} - } - }()); + editor.init = function () { + // 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); + var src, qstr; + urldata = $.deparam.querystring(true); if (!$.isEmptyObject(urldata)) { if (urldata.dimensions) { urldata.dimensions = urldata.dimensions.split(','); @@ -271,45 +453,78 @@ // security reasons, even for same-domain // ones given potential to interact in undesirable // ways with other script resources - if (urldata.extPath) { - delete urldata.extPath; - } + $.each( + [ + 'extPath', 'imgPath', + 'langPath', 'jGraduatePath' + ], + function (pathConfig) { + if (urldata[pathConfig]) { + delete urldata[pathConfig]; + } + } + ); - svgEditor.setConfig(urldata); + svgEditor.setConfig(urldata, {overwrite: false}); // Note: source, url, and paramurl (as with storagePrompt later) are not set on config but are used below + + setupCurConfig(); - 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 (!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) + svgEditor.loadFromURL(qstr.substr(9)); + return; + } + if (urldata.url) { + svgEditor.loadFromURL(urldata.url); + return; } } - - if (src) { - if (src.indexOf('data:') === 0) { - // plusses get replaced by spaces, so re-insert - src = src.replace(/ /g, '+'); - Editor.loadFromDataURI(src); - } else { - Editor.loadFromString(src); - } - } else if (qstr.indexOf('paramurl=') !== -1) { - // Get parameter URL (use full length of remaining location.href) - svgEditor.loadFromURL(qstr.substr(9)); - } else if (urldata.url) { - svgEditor.loadFromURL(urldata.url); - } - } else { - var name = 'svgedit-' + Editor.curConfig.canvasName; - var cached = window.localStorage && window.localStorage.getItem(name); - if (cached) { - Editor.loadFromString(cached); + if (!urldata.noStorageOnLoad || curConfig.forceStorage) { + svgEditor.loadContentAndPrefs(); } + setupCurPrefs(); + } + else { + setupCurConfig(); + svgEditor.loadContentAndPrefs(); + setupCurPrefs(); } }()); - var setIcon = Editor.setIcon = function(elem, icon_id, forcedSize) { + // 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); @@ -331,14 +546,8 @@ }); }); - var good_langs = []; - - $('#lang_select option').each(function() { - good_langs.push(this.value); - }); - // var lang = ('lang' in curPrefs) ? curPrefs.lang : null; - Editor.putLocale(null, good_langs); + editor.putLocale(null, good_langs); }; // Load extensions @@ -515,13 +724,9 @@ if (tleft.length !== 0) { min_height = tleft.offset().top + tleft.outerHeight(); } -// var size = $.pref('iconsize'); -// if (size && size != 'm') { -// svgEditor.setIconSize(size); -// } else if ($(window).height() < min_height) { -// // Make smaller -// svgEditor.setIconSize('s'); -// } + + var size = $.pref('iconsize'); + svgEditor.setIconSize(size || ($(window).height() < min_height ? 's': 'm')); // Look for any missing flyout icons from plugins $('.tools_flyout').each(function() { @@ -547,19 +752,19 @@ } }); - Editor.canvas = svgCanvas = new $.SvgCanvas(document.getElementById('svgcanvas'), curConfig); - Editor.showSaveWarning = false; - var palette = [ - '#000000', '#3f3f3f', '#7f7f7f', '#bfbfbf', '#ffffff', - '#ff0000', '#ff7f00', '#ffff00', '#7fff00', - '#00ff00', '#00ff7f', '#00ffff', '#007fff', - '#0000ff', '#7f00ff', '#ff00ff', '#ff007f', - '#7f0000', '#7f3f00', '#7f7f00', '#3f7f00', - '#007f00', '#007f3f', '#007f7f', '#003f7f', - '#00007f', '#3f007f', '#7f007f', '#7f003f', - '#ffaaaa', '#ffd4aa', '#ffffaa', '#d4ffaa', - '#aaffaa', '#aaffd4', '#aaffff', '#aad4ff', - '#aaaaff', '#d4aaff', '#ffaaff', '#ffaad4' + 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, @@ -568,15 +773,14 @@ defaultImageURL = curConfig.imgPath + 'logo.png', workarea = $('#workarea'), canv_menu = $('#cmenu_canvas'), - layer_menu = $('#cmenu_layers'), + // layer_menu = $('#cmenu_layers'), // Unused exportWindow = null, - tool_scale = 1, zoomInIcon = 'crosshair', zoomOutIcon = 'crosshair', ui_context = 'toolbars', origSource = '', paintBox = {fill: null, stroke:null}; - + // This sets up alternative dialog boxes. They mostly work the same way as // their UI counterparts, expect instead of returning the result, a callback // needs to be included that returns the result as its first parameter. @@ -587,8 +791,8 @@ var box = $('#dialog_box'), btn_holder = $('#dialog_buttons'), dialog_content = $('#dialog_content'), - dbox = function(type, msg, callback, defaultVal, opts, changeCb) { - var ok, ctrl; + 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(); @@ -609,8 +813,23 @@ else if (type === 'select') { var div = $('