From e463b43220ffda927cd41bb6d80a73e998e0627c Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Tue, 18 Feb 2014 15:06:27 +0000 Subject: [PATCH] 1. Reference config.js in the editor (and remove encouragement for adding extensions to HTML) but ignore config.js in SVN (let user configure) but supply config-sample.js to indicate config/pref/extension possibilities; 2. Move ext-overview_window.js to default but overridable list of extensions (as with other extensions); 3. Allow extensions to avoid problems if failing to return an object (in svgcanvas.js); 4. Support new langReady callback to ensure extension always called when locale info is ready (and always load locale, even English); 5. Move localStorage storing to a new (i18n-ized and available-by-default) storage extension which adds a dialog asking user for whether to store prefs and/or SVG content; $.pref() now falls back to checking defaultPrefs (which may have been expanded at runtime to include URL or storage settings); use new config "forceStorage" to get old (bad) behavior 6. Remove initial cap from "Editor" to reflect singleton nature of object (as compared to JSLint conventions for initial cap constructors); 7. Begin a little JSDoc, clearer grouping of properties/methods; JSLint/clean-up 8. Omit values for lang and iconsize to be successfully auto-detected; 9. Document "save_notice_done" and "export_notice_done" within list of prefs; document "showlayers" and "no_save_warning" as config 10. Add "preventAllURLConfig" and "preventURLContentLoading" config for URL security; 11. Add "lockExtensions" and "noDefaultExtensions" config for URL behavior re: extension loading 12. Document "showGrid", and new "noStorageOnLoad" and "emptyStorageOnDecline" extension-related config 13. Change setConfig to allow a second object with "overwrite" and "allowInitialUserOverride" properties and to behave accordingly (with URL config acting with overwrite=false to act under lower priority given security concern), along with checking "preventAllURLConfig" and "lockExtensions" config. 14. Remove any dupe extensions 15. Strip all path config from URL setting in addition to extPath (imgPath, langPath, jGraduatePath) 16. Support select+checkbox type dialog (used for storage ext.) 17. Ensure clickSelect is public so can be properly used by ext-connector.js 18. Reinstate 'in' checks just to be safe 19. Fix broken linkControlPoints() and addSubPath() functions 20. Fix problem when position returned by extension object was too high (e.g., if too few other extensions were included). git-svn-id: http://svg-edit.googlecode.com/svn/trunk@2705 eee81c28-f429-11dd-99c0-75d572ba1ddd --- editor/config-sample.js | 138 +++++ editor/extensions/ext-mathjax.js | 2 +- editor/extensions/ext-storage.js | 281 ++++++++++ editor/locale/lang.en.js | 14 + editor/locale/locale.js | 25 +- editor/svg-editor.html | 6 +- editor/svg-editor.js | 864 ++++++++++++++++++++----------- editor/svgcanvas.js | 2 +- 8 files changed, 1004 insertions(+), 328 deletions(-) create mode 100644 editor/config-sample.js create mode 100644 editor/extensions/ext-storage.js 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 = $('
'); 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) { - ctrl.append($('