-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathbrowser-storage.js
375 lines (346 loc) · 11.8 KB
/
browser-storage.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
//@ts-check
/**
* @author S0AndS0
* @copyright AGPL-3.0
* @example <caption>Quick Usage for Browser Storage</caption>
* // Initialize new class instance
* const storage = new Browser_Storage();
* if (!storage.storage_available) {
* console.error('No browser storage available!');
* } else {
* if (!storage.supports_local_storage) {
* console.warn('Falling back to cookies!');
* }
* // Do stuff with local storage of browser!
* storage.setItem('test__string', 'Spam!', 7);
* console.log("storage.getItem('test__string') -> " + storage.getItem('test__string'));
* }
*/
class Browser_Storage {
/**
* Sets properties used by other methods of this class
* @property {boolean} supports_local_storage - What `this.constructor.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 = Browser_Storage.supportsLocalStorage();
this.supports_cookies = Browser_Storage.supportsCookies();
this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
}
/**
* Copy of `this.constructor` that should not throw `TypeError` when called
* @this Browser_Storage
*/
constructorRefresh() {
this.supports_local_storage = Browser_Storage.supportsLocalStorage();
this.supports_cookies = Browser_Storage.supportsCookies();
this.storage_available = (this.supports_cookies || this.supports_local_storage) ? true : false;
}
/**
* Coerces values into JavaScript object types
* @function coerce
* @param {any} value
* @returns {any}
* @throws {!SyntaxError}
* @example
* coerce('1');
* //> 1
*
* coerce('stringy');
* //> "stringy"
*
* coerce('{"key": "value"}');
* //> {key: "value"}
*/
static coerce(value) {
try {
return JSON.parse(value);
} catch (e) {
/* istanbul ignore next */
if (!(e instanceof SyntaxError)) {
throw e;
}
// @ts-ignore
if (['undefined', undefined].includes(value)) {
return undefined;
}
// @ts-ignore
else if (['NaN', NaN].includes(value)) {
return NaN;
}
return value;
}
}
/**
* Translates `document.cookie` key value strings into dictionary
* @param {boolean?} [coerce_values=false] - When `true` will coerce value types
* @returns {Object<string, string>|Object<string, any>}
*/
static getObjectifiedCookies(coerce_values = false) {
return document.cookie.split(';').reduce((accumulator, data) => {
const key_value = data.split('=');
const key = key_value[0].trim();
let value = decodeURIComponent(key_value[1].trim());
if (coerce_values === true) {
value = Browser_Storage.coerce(value);
}
accumulator[key] = value;
return accumulator;
}, {});
};
/**
* Translates `localStora` key value strings into dictionary
* @param {boolean?} [coerce_values=false] - When `true` will coerce value types
* @returns {Object<string, string>|Object<string, any>}
// * @this Browser_Storage
*/
static getObjectifiedLocalStorage(coerce_values = false) {
// @ts-ignore
return Object.entries(localStorage).reduce((accumulator, [encoded_key, encoded_value]) => {
if (coerce_values === true) {
encoded_value = Browser_Storage.coerce(encoded_value);
}
accumulator[decodeURIComponent(encoded_key)] = encoded_value;
return accumulator;
}, {});
}
/**
* Tests and reports `boolean` if `localStorage` has `setItem` and `removeItem` methods
* @returns {boolean}
*/
static supportsLocalStorage() {
// Because Opera and may be other browsers `setItem`
// is available but with space set to _`0`_
try {
localStorage.setItem('test_key', 'true');
} catch (e) {
/* istanbul ignore next */
if (!(e instanceof ReferenceError)) throw e;
/* istanbul ignore next */
return false;
} finally {
localStorage.removeItem('test_key');
}
return true;
}
/**
* Reports if cookies are enabled. Note, use `this.supports_cookies` instead within tests.
* @returns {boolean}
* @this Browser_Storage
*/
static supportsCookies() {
// Browser support detection must be interactive as some
// may be _full_ or not enabled without updating state!
if (Browser_Storage._setCookieItem('testcookie', '', 7)) {
return Browser_Storage._setCookieItem('testcookie', '', -7);
}
/* istanbul ignore next */
return false;
}
/**
* Use `this.setItem` instead. Attempts to set cookie
* @returns {boolean}
* @param {Object|string|number} key - _variable name_ to store value under
* @param {JSON|Object} value - stored either under localStorage or as a cookie
* @param {Object|number|boolean} [days_to_live=false] - how long a browser is suggested to keep cookies
*/
static _setCookieItem(key, value, days_to_live = false) {
const encoded_key = encodeURIComponent(key);
const encoded_value = encodeURIComponent(JSON.stringify(value));
let expires = '';
if (isNaN(days_to_live) == false) {
const date = new Date();
const now = date.getTime();
if (days_to_live == 0) {
date.setTime(now);
} else {
date.setTime(now + (days_to_live * 24 * 60 * 60 * 1000));
}
expires = `; expires=${date.toUTCString()}`;
}
try {
document.cookie = `${encoded_key}=${encoded_value}${expires}; path=/`;
} catch (e) {
/* istanbul ignore next */
if (!(e instanceof ReferenceError)) throw e;
/* istanbul ignore next */
return false;
}
return true;
}
/**
* Use `this.getItem` instead. Attempts to get cookie by _key_ via `match`
* @returns {JSON|Object}
* @param {Object|string|number} key - Name of key to look up value for.
*/
static _getCookieItem(key) {
const encoded_key = encodeURIComponent(key);
const cookie_data = document.cookie.match(`(^|;) ?${encoded_key}=([^;]*)(;|$)`);
if (cookie_data === null || cookie_data[2] === 'undefined') return undefined;
return JSON.parse(decodeURIComponent(cookie_data[2]));
// return Browser_Storage.coerce(decodeURIComponent(cookie_data[2]));
}
/**
* Gets decoded/JSON value for given key
* @returns {JSON|Object}
* @throws {ReferenceError} When no browser based storage is available
* @param {Object|string|number} key - Name of key to look up value for.
* @this Browser_Storage
*/
getItem(key) {
if (this.supports_local_storage) {
const encoded_key = encodeURIComponent(key);
const raw_value = localStorage.getItem(encoded_key);
if (raw_value === null || raw_value === 'undefined') return undefined;
return JSON.parse(decodeURIComponent(raw_value));
// return Browser_Storage.coerce(decodeURIComponent(raw_value));
} else if (this.supports_cookies) {
return Browser_Storage._getCookieItem(key);
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Removes value by key from browser storage; cookies require page refresh
* @returns {boolean}
* @this Browser_Storage
*/
removeItem(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.
return this.setItem(key, '', -7);
}
return false;
}
/**
* Stores encoded JSON within browser
* @returns {boolean}
* @param {Object|string|number} key - _variable name_ to store value under
* @param {any} value - stored either under localStorage or as a cookie
* @param {number|boolean} [days_to_live=false] - how long a browser is suggested to keep cookies
* @this Browser_Storage
*/
setItem(key, value, days_to_live = false) {
if (this.supports_local_storage) {
localStorage.setItem(
encodeURIComponent(key),
encodeURIComponent(JSON.stringify(value))
);
return true;
} else if (this.supports_cookies) {
return Browser_Storage._setCookieItem(key, value, days_to_live);
}
return false;
}
/**
* Lists keys that may point to values
* @returns {Array}
* @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].trim());
});
return cookie_keys;
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Gets key name by index address
* @returns {string|number}
* @throws {ReferenceError} When no browser based storage is available
* @param {number} index - Key name to return by index
* @this Browser_Storage
*/
key(index) {
if (this.supports_local_storage) {
return localStorage.key(index);
} else if (this.supports_cookies) {
// @ts-ignore
const encoded_value = document.cookie.split(';')[index].split('=')[0].trimStart();
if (encoded_value == undefined) return undefined;
return decodeURIComponent(encoded_value);
}
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
/**
* Clears **all** stored values from either localStorage or cookies
* @returns {boolean}
* @this Browser_Storage
*/
clear() {
if (this.supports_local_storage) {
localStorage.clear();
return true;
} else if (this.supports_cookies) {
document.cookie.split(';').forEach((cookie) => {
const decoded_key = decodeURIComponent(cookie.split('=')[0].trim());
this.removeItem(decoded_key);
});
return true;
}
return false;
}
/**
* Generates `{data.key: data.value}` JSON from localStorage or cookies
* @yields {stored_data}
* @this Browser_Storage
*/
*iterator() {
if (this.supports_local_storage) {
const keys = Object.keys(localStorage);
for (let i = 0; i < keys.length; i++) {
const encoded_key = keys[i];
const decoded_key = decodeURIComponent(encoded_key);
const raw_value = localStorage.getItem(encoded_key);
// Note, `JSON.pars()` has a _finicky appetite_
if (raw_value === null || raw_value === undefined || raw_value === 'undefined') {
yield { key: decoded_key, value: undefined };
} else {
yield { key: decoded_key, value: JSON.parse(decodeURIComponent(raw_value)) };
}
}
} else if (this.supports_cookies) {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const decoded_key = decodeURIComponent(cookies[i].split('=')[0].trim());
yield { key: decoded_key, value: Browser_Storage._getCookieItem(decoded_key) };
}
}
else {
throw new ReferenceError('Browser storage unavailable as of last constructorRefresh()');
}
}
/**
* See `this.iterator()` method
* @returns {stored_data}
* @this Browser_Storage
*/
// @ts-ignore
[Symbol.iterator]() {
// @ts-ignore
return this.iterator();
}
}
/**
* Exports are for Jest who uses Node for Travis-CI tests on gh-pages branch
* https://javascript-utilities.github.io/browser-storage/
*/
if (typeof module !== 'undefined') module.exports = Browser_Storage;
/**
* @typedef stored_data
* @type {Object}
* @property {string|number} key - `data.key` from `localStorage` or cookies
* @property {*} value - `data.value` from `localStorage` or cookies
*/