## 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