## The Code I've got things operating as desired so far and am hoping fresh eyes can point out any mistakes or bits that can be improved, a [live demo][github__live_demo] is hosted and updated on GitHub Pages, and the [source code][github__source] is just a `fork` or `clone` away.... ### `Browser_Storage.js` ```javascript class Browser_Storage { /** * Sets properties used by other methods of this class * @returns {none} * @property {boolean} supports_local_storage - What `this.supportsLocalStorage()` had to say * @property {boolean} supports_cookies - What `this.supportsCookies()` had to say * @property {boolean} storage_available - If either of the above is `true` * @this Browser_Storage * @class */ constructor() { this.supports_local_storage = this.supportsLocalStorage(); this.supports_cookies = this.supportsCookies(); this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false; } /** * Copy of `this.constructor` that should not throw `TypeError` when called * @returns {none} * @this Browser_Storage */ constructorRefresh() { this.supports_local_storage = this.supportsLocalStorage(); this.supports_cookies = this.supportsCookies(); this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false; } /** * Tests and reports `boolean` if `localStorage` is available * @returns {boolean} */ supportsLocalStorage() { try { localStorage.setItem('test_key', true); } catch (e) { return false; } finally { localStorage.removeItem('test_key'); } return true; } /** * Reports if `navigator.cookieEnabled` or `document.cookie` are available * Note, use `this.supports_cookies` instead within tests. * @returns {boolean} * @this Browser_Storage */ supportsCookies() { if (navigator.cookieEnabled) return true; try { document.cookie = 'testcookie'; } catch (e) { return false; } if (document.cookie.indexOf('testcookie') != -1) { this.remove('testcookie'); return true; } return false; } /** * Gets decoded/JSON value for given key * @returns {?boolean|?number|?string|?undefined} * @throws {ReferenceError} When no browser based storage is available * @param {string|number} key - Name of key to look up value for. * @this Browser_Storage */ get(key) { const encoded_key = encodeURIComponent(key); let decoded_value = undefined; if (this.supports_local_storage) { decoded_value = decodeURIComponent(localStorage.getItem(encoded_key)); } else if (this.supports_cookies) { let cookie_data = document.cookie.match('(^|;) ?' + encoded_key + '=([^;]*)(;|$)'); decoded_value = cookie_data ? decodeURIComponent(cookie_data[2]) : null; } if (this.supports_local_storage || this.supports_cookies) { // Ignore SyntaxError from decoded strings // and if so return decoded value instead try { return JSON.parse(decoded_value); } catch (e) { if (!(e instanceof SyntaxError)) throw e; } return decoded_value; } throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()'); } /** * Removes value by key from browser storage; cookies require page refresh * @returns {boolean} * @throws {ReferenceError} When no browser based storage is available * @this Browser_Storage */ remove(key) { if (this.supports_local_storage) { localStorage.removeItem(key) return true; } else if (this.supports_cookies) { // Note, unsetting and expiring in the past is how // to remove one cookie upon the next page load. this.set(key, '', -7); return true; } throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()'); } /** * Stores encoded JSON within browser * @returns {boolean} * @throws {ReferenceError} When no browser based storage is available * @param {string|number} key - _variable name_ to store value under * @param {boolean|number|string} value - stored either under localStorage or as a cookie * @param {number} days_to_live - how long a browser is suggested to keep cookies * @this Browser_Storage */ set(key, value, days_to_live = false) { const encoded_key = encodeURIComponent(key); const encoded_value = encodeURIComponent(JSON.stringify(value)); if (this.supports_local_storage) { localStorage.setItem(encoded_key, encoded_value); return true; } else if (this.supports_cookies) { let expires = ''; if (days_to_live) { const date = new Date(); if (days_to_live == 0) { date.setTime(date.getTime()); } else { date.setTime(date.getTime() + (days_to_live * 24 * 60 * 60 * 1000)); } expires = '; expires=' + date.toGMTString(); } document.cookie = encoded_key + '=' + encoded_value + expires + '; path=/'; return true; } throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()'); } /** * Lists keys that may point to values * @returns {boolean} * @throws {ReferenceError} When no browser based storage is available * @this Browser_Storage */ keys() { if (this.supports_local_storage) { return Object.keys(localStorage); } else if (this.supports_cookies) { let cookie_keys = []; document.cookie.split(';').forEach((pare) => { cookie_keys.push(pare.split('=')[0]); }); return cookie_keys; } throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()'); } /** * Clears **all** stored values from either localStorage or cookies * @returns {boolean} * @throws {ReferenceError} When no browser based storage is available * @this Browser_Storage */ clear() { if (this.supports_local_storage) { localStorage.clear(); return true; } else if (this.supports_cookies) { document.cookie.split(';').forEach((cookie) => { const key = encodeURIComponent(cookie.split('=')[0].replace(/(^\s+|\s+$)/g, '')); if (key) this.remove(key); }); return true; } throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()'); } } ``` ### `main.css` ```css /** * 1. Center element * 2. Re-justify text */ body { text-align: center; /* 1 */ } .container__main_content { max-width: 80%; max-width: 80vw; display: inline-block; /* 1 */ text-align: left; /* 2 */ } /** * Privacy notice footer stuff */ .container__privacy_notice { position: fixed; bottom: 5px; max-width: 80%; max-width: 80vw; background-color: lightgrey; text-align: left; /* 2 */ } /** * 1. Prevent label text from being selectable */ .privacy_notice__label { background-color: black; color: white; font-size: 1.5rem; padding: 5px 10px; float: right; -webkit-user-select: none; /* 1 */ -moz-user-select: none; /* 1 */ -ms-user-select: none; /* 1 */ user-select: none; /* 1 */ } .privacy_notice__label:hover { color: lightgrey; } /* State of label when checkbox is unchecked */ .privacy_notice__label::after { content: attr(value); } /** * Checkbox hacks */ .hidden__input { display: none; visibility: hidden; opacity: 0; filter: alpha(opacity=0); } /* State updates for when checkbox is checked */ .privacy_notice__checkbox:checked ~ .container__privacy_notice { box-shadow: unset; max-width: 1.5rem; max-height: 1.5rem; } .privacy_notice__checkbox:checked ~ .container__privacy_notice * { visibility: hidden; opacity: 0; filter: alpha(opacity=0); } .privacy_notice__checkbox:checked ~ .container__privacy_notice .privacy_notice__label::after { content: attr(checked); } /* State of label when checkbox is checked */ .privacy_notice__checkbox:checked ~ .container__privacy_notice::after { content: '\1F56D'; position: absolute; bottom: 5px; left: 5px; } /* Combo of checked and hover state updates */ .privacy_notice__checkbox:checked ~ .container__privacy_notice:hover { max-width: 80%; max-width: 80vw; max-height: unset; } .privacy_notice__checkbox:checked ~ .container__privacy_notice:hover::after { display: none; visibility: hidden; opacity: 0; filter: alpha(opacity=0); } .privacy_notice__checkbox:checked ~ .container__privacy_notice:hover * { visibility: visible; opacity: 1; filter: alpha(opacity=100); } ``` ### `index.html` ```html <!DOCTYPE html> <html lang="en" dir="ltr"> <head> <title>Browser_Storage Example</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <script src="assets/javascript-modules/browser-storage/browser-storage.js" type="text/javascript"></script> <link type="text/css" href="assets/css/main.css" rel="stylesheet"/> <script type="text/javascript"> const storage = new Browser_Storage(); if (storage.storage_available != true) throw new Error('We may never have any memory of this...'); function restore_settings() { let privacy_notice_read = storage.get('checkbox__privacy_notice'); if (privacy_notice_read != 'true' && privacy_notice_read != true) return false; document.getElementById('trigger__privacy_notice').checked = true; return true; } function store_settings() { let privacy_notice_read = storage.get('checkbox__privacy_notice'); if (privacy_notice_read != true) return false; if (document.getElementById('trigger__privacy_notice').checked != true) return false; storage.set('checkbox__privacy_notice', true, 365); return true; } function toggle_privacy_notice(input_element) { if (input_element.checked == true) { storage.set('checkbox__privacy_notice', input_element.checked, 365); return true; } storage.remove('checkbox__privacy_notice'); storage.clear(); return false; } </script> </head> <body onload="restore_settings();" onunload="store_settings();"> <div class="container__main_content"> <p> This is a static site that provides the illusion of remembering a client. Test by accepting the <i>privacy notice</i> and then refreshing or closing and re-opening a tab pointing to the current URL. </p> <p> <kbd>Ctrl</kbd> <kbd>R</kbd> or <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>R</kbd> will refresh most desktop browsers. </p> <p> Then reject the privacy notice by clicking on the Acceptable button again to revert the behavior of this site. </p> <p> Use <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>I</kbd> to open debugger on a desktop browser, then navigate to the Storage tab/pane to observe JavaScript doing things for the above mentioned actions. </p> </div> <input type="checkbox" name="checkbox__privacy_notice" class="hidden__input privacy_notice__checkbox" id="trigger__privacy_notice" onchange="toggle_privacy_notice(this);"> <div class="container__privacy_notice"> <p> This site uses JavaScript, and localStorage or cookies of your browser, to enhance your experience here, however, this site currently does <strong>not</strong> store any data about your activity severer-side. </p> <p> Review the <a href="https://github.com/javascript-utilities/browser-storage#privacy">Privacy</a> and <a href="https://github.com/javascript-utilities/browser-storage#security">Security</a> notices regarding the <a href="https://github.com/javascript-utilities/browser-storage/blob/master/browser-storage.js"> browser-storage.js </a>, for more details on how client-side storage and scripting are utilized. </p> <label for="trigger__privacy_notice" class="menu__trigger__label privacy_notice__label" title="Click to toggle." value="false" checked="true" >Acceptable: </label> </div> </body> </html> ``` > Note, I did try to use the web code option when writing this question but that caused various errors. ## Questions My goal is to have near 100% coverage for browsers that have JavaScript and either `localStorage` or `document.cookie` storage options; - in this _day-n-age_ is there any point in using cookies for a local storage fallback? - is there any point in using the old syntax of getting OO behavior from JavaScript or will using the `class` and `function` keywords be sufficient? > if there are any good arguments for not using `class`, please include some feature detection examples for loading an alternate version. - I keep running across `npm` but that seems to be more focused on Node related JavaScript, should I consider issuing _`npm init .`_ or would that _pollute_ their package management system? > If projects like this are a good candidate for `npm`, then some tips on Operating System agnostic ways of issuing `Git` commands would be excellent. - Currently I'm using `decodeURIComponent` to `JSON.parse` when returning browser stored values, however, that leads to requiring a `try`/`catch` for strings which seems messy, so is there a _better_ way of retrieving values? > At this time it almost feels like I'll have to do some pre-checking prior to returning in order to get some consistency in behavior. - Where have I messed up and/or how can code execution be improved without sacrificing readability? - One thing I find bothersome about using _`checkbox hacks`_ is accessibility, are there any _good_ ways of getting <kbd>Tab</kbd> and <kbd>Space</kbd>/<kbd>Enter</kbd> support? I do not expect one person to attempt to answer all of the above so feel free to focus on one, I'll up-vote and link to answers that I believe are valid. And if someone does attempt to answer the majority or remaining questions then I'll mark that as acceptable too. ___ Thanks, and I hope that even if the above is me being a _bad influence_ that we can all learn why together. [github__live_demo]: https://javascript-utilities.github.io/browser-storage/index.html [github__source]: https://github.com/javascript-utilities/browser-storage