web_page_state
Version:
Simple straightforward tool to manage webpage state on the client side and hook state to html tags on page without code
861 lines (718 loc) • 26.1 kB
JavaScript
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
window.wsGlobals = window.wsGlobals || {};
window.wsGlobals.PageState = require("./index").PageState;
},{"./index":2}],2:[function(require,module,exports){
const resolvePath = require('object-resolve-path');
class PageState {
static registerListener(onStateUpdate) {
if (!onStateUpdate) {
return;
}
let index = PageState.listenersAvailableIndex;
PageState.listeners[index] = onStateUpdate;
PageState.listenersAvailableIndex++;
onStateUpdate(PageState.pageState); // Send the first update upon registering.
return index;
}
static unregisterListener(index) {
if (!PageState.listeners.hasOwnProperty(index)) {
return;
}
PageState.listeners[index] = null;
try {
delete PageState.listeners[index];
} catch (e) {}
}
static updatePageStateWithParams(params) {
console.log('updating state with: ', params);
if (!params) {
params = {};
}
let props = Object.getOwnPropertyNames(params);
if (!params.isProcessing) {
PageState.pageState.isProcessing = false;
}
if (props && props.length > 0) {
for (const key of props) {
if (params[key] !== undefined) {
if (key === "epochLicenseExpirationDateInSecs") {
let latestEpoch = Math.max(PageState.pageState[key], params[key]);
PageState.pageState[key] = latestEpoch;
try {
localStorage.setItem("epochLicenseExpirationDateInSecs", PageState.pageState[key]);
if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
// Published extension:
chrome.runtime.sendMessage("nncconplehmbkbhkgkodmnkfaflopkji", {
message: "epochLicenseExpirationDateInSecs",
data: latestEpoch
}, function () {
});
}
} catch (e) {
}
} else {
PageState.pageState[key] = params[key];
}
}
}
}
console.log('new state: ', PageState.pageState);
try {
PageState.updateUiByState(PageState.pageState);
} catch (e) {
}
if (PageState.listeners) {
for (const i of Object.getOwnPropertyNames(PageState.listeners)) {
if (PageState.listeners[i] && PageState.listeners[i] instanceof Function) {
PageState.listeners[i](PageState.pageState);
} else {
delete PageState.listeners[i];
}
}
}
}
static interpretLicenseState(epochToExpirationSecs) {
const ONE_MONTH_IN_SECONDS = 3600 * 24 * 30;
if (!epochToExpirationSecs) {
return PageState.licenseInterpretations.NONE;
} else if (epochToExpirationSecs < 0) {
return PageState.licenseInterpretations.UNKNOWN;
} else if (epochToExpirationSecs >= 3000000000) {
return PageState.licenseInterpretations.LIFETIME;
} else {
let timeInSecsTillExpiration = epochToExpirationSecs - Date.now() / 1000;
if (timeInSecsTillExpiration <= 0) {
return PageState.licenseInterpretations.SUBSCRIPTION_OVER;
} else if (timeInSecsTillExpiration >= ONE_MONTH_IN_SECONDS) {
return PageState.licenseInterpretations.SUBSCRIPTION;
} else {
return PageState.licenseInterpretations.SUBSCRIPTION_SOON_OVER;
}
}
}
static updateUiByStatePropsDirectly(newState) {
console.log('updateUiByStatePropsDirectly got new state: ', newState);
let newStateHelpers = new PageState.pageStateHelpers(newState);
let elements = document.querySelectorAll("[data-ws-show-if]");
console.log(elements);
for (const element of elements) {
let attr = element.getAttribute('data-ws-show-if');
if (!attr) {
continue;
}
let conditions = attr.split(" ");
let shouldShowEl = false;
for (const condition of conditions) {
let value;
try {
value = resolvePath(newState,condition);
} catch (e) {
value = null;
}
if (value) {
shouldShowEl = true;
break;
}
let valueFunction;
try {
valueFunction = resolvePath(newStateHelpers,condition);
} catch (e) {
valueFunction = null;
}
if (valueFunction && (valueFunction instanceof Function)) {
if (valueFunction()) {
shouldShowEl = true;
break;
}
}
}
element.style.display = shouldShowEl ? "" : "none"; // TODO - get/set 'previously' set style, prior to 'none'
}
elements = document.querySelectorAll("[data-ws-show-if-not]");
for (const element of elements) {
let attr = element.getAttribute('data-ws-show-if-not');
if (!attr) {
continue;
}
let conditions = attr.split(" ");
let shouldShowEl = false;
for (const condition of conditions) {
let valueFunction;
try {
valueFunction = resolvePath(newStateHelpers,condition);
} catch (e) {
valueFunction = null;
}
let value;
try {
value = resolvePath(newState,condition);
} catch (e) {
value = null;
}
// We let the function - if exists - override the property for this one, which is inconsistent with the show-if non negated...
if (valueFunction && valueFunction instanceof Function) {
// If there's a function -> no need to check the direct property.
if (!valueFunction()) {
shouldShowEl = true;
break;
}
} else if (!value) {
// There's no function -> check property.
shouldShowEl = true;
break;
}
}
element.style.display = shouldShowEl ? "" : "none";
}
}
static updateUiByState(newState) {
PageState.updateUiByStatePropsDirectly(newState);
let labelEls = document.querySelectorAll("[data-ws-label]");
if (labelEls && labelEls.length > 0) {
for (const el of labelEls) {
if (el.getAttribute("data-ws-label") == "epochAsDate") {
var options = {year: 'numeric', month: 'long', day: 'numeric'};
el.innerText = (new Date(1000 * newState.epochLicenseExpirationDateInSecs)).toLocaleDateString("en-US", options)
} else {
let value;
try {
value = resolvePath(newState,el.getAttribute("data-ws-label"));
} catch (e) {
value = "";
}
if (value===undefined || value===null) {
value = "";
}
el.innerText = value;
}
}
}
// TODO: Move the following to the helper functions, like isAndroid - should be: isLicenseTypeLife... etc.
/*let licenseState = PageState.interpretLicenseState(newState.epochLicenseExpirationDateInSecs);
if (licenseState == PageState.licenseInterpretations.NONE) {
classesToShow.push(PageState.classNamesForUiUpdates.showWhenLicenseNone);
} else if (licenseState == PageState.licenseInterpretations.UNKNOWN) {
classesToShow.push(PageState.classNamesForUiUpdates.showWhenLicenseUnknown);
} else if (licenseState == PageState.licenseInterpretations.LIFETIME) {
classesToShow.push(PageState.classNamesForUiUpdates.showWhenLicenseLife);
} else if (licenseState == PageState.licenseInterpretations.SUBSCRIPTION) {
classesToShow.push(PageState.classNamesForUiUpdates.showWhenLicenseSubscriptionValid);
} else if (licenseState == PageState.licenseInterpretations.SUBSCRIPTION_OVER) {
classesToShow.push(PageState.classNamesForUiUpdates.showWhenLicenseExpired);
} else if (licenseState == PageState.licenseInterpretations.SUBSCRIPTION_SOON_OVER) {
classesToShow.push(PageState.classNamesForUiUpdates.showWhenLicenseSoonToExpire);
}*/
}
static setPremiumLife() {
PageState.updatePageStateWithParams({
epochLicenseExpirationDateInSecs: 3000000000
});
if (PageState.pageState.uid) {
let apiUrl = "https://itranscribe.app:8443/apiLicenses";
var xhr = new XMLHttpRequest();
xhr.open('POST',
apiUrl + '?type=__sn_premium_life__' +
'&uid=' + encodeURIComponent(PageState.pageState.uid));
xhr.onload = function (e) {
if (this.status == 200) {
//console.log('response', this.response); // JSON response
} else {
console.log('error in xhr to: ' + apiUrl);
}
};
xhr.send();
console.log('sending req', xhr);
}
}
static addOneYearToLicense() {
let todayInSecs = (new Date()).getTime() / 1000;
let latestExpiration = PageState.pageState.epochLicenseExpirationDateInSecs || 0;
if (todayInSecs > latestExpiration) {
latestExpiration = todayInSecs;
}
latestExpiration += (3600 * 24 * 365);
PageState.updatePageStateWithParams({
epochLicenseExpirationDateInSecs: latestExpiration
});
}
static onMessageReceivedFromExtension(reply) {
if (reply) {
console.log('snx reply = ', reply);
if (reply.version) {
if (reply.version == 1) {
PageState.hasExtension = true;
console.log('sn extension version == 1 ', reply.version);
PageState.setPremiumLife(); // TODO: register life premium if signed in
} else {
console.log('sn extension version: ', reply.version);
}
} else {
console.log('sn extension reply does not include version: ', reply);
}
if (reply.licenseEpochSecsExpiration) {
console.log('license: ', reply.licenseEpochSecsExpiration);
if (reply.licenseEpochSecsExpiration >= 3000000000) {
PageState.setPremiumLife(); // TODO: register life premium if signed in
} else if (reply.licenseEpochSecsExpiration > PageState.pageState.epochLicenseExpirationDateInSecs) {
PageState.updatePageStateWithParams({
epochLicenseExpirationDateInSecs: reply.licenseEpochSecsExpiration
})
}
} else {
console.log('no license type from extension');
}
}
}
static setUser(user) {
PageState.updatePageStateWithParams({user: user});
}
static toastNotification(title, msg, isError, isSticky) {
let el = document.getElementById('notificationToast');
if (!el) {
el = document.createElement("div");
el.setAttribute("id","notificationToast");
el.setAttribute("data-ws-show-if", "isNotification");
el.setAttribute("style", "position: fixed;bottom: 20px;z-index:9999999;background-color:#aaa");
let msgEl = document.createElement("p");
msgEl.setAttribute("data-ws-label", "notificationMsg");
el.appendChild(msgEl);
document.body.appendChild(el);
}
let titleEl = el.querySelector("[data-ws-label='notificationTitle']");
console.log(titleEl);
if (titleEl) {
titleEl.style.color = isError ? "red" : "blue";
}
PageState.updatePageStateWithParams({
isNotification: true,
notificationTitle: title || "",
notificationMsg: msg || "",
});
if (PageState.notificationTimeout != null) {
clearTimeout(PageState.notificationTimeout);
}
if (!isSticky) {
PageState.notificationTimeout = setTimeout(() => {
PageState.updatePageStateWithParams({isNotification: false});
PageState.notificationTimeout = null;
}, 3000);
}
}
static dismissNotification() {
PageState.updatePageStateWithParams({isNotification: false});
clearTimeout(PageState.notificationTimeout);
PageState.notificationTimeout = null;
}
static getParam(keyPath) {
if (keyPath===undefined || keyPath===null || keyPath.length==0) {
return null;
}
return resolvePath(PageState.pageState, keyPath);
}
static toggleParam(keyPath) {
if (keyPath===undefined || keyPath===null || keyPath.length==0) {
return null;
}
PageState.updatePageStateWithParams({keyPath: !Boolean(PageState.getParam(keyPath))});
}
static init() {
PageState.listeners = {};
PageState.listenersAvailableIndex = 0;
PageState.updatePageStateWithParams(PageState.pageState);
try {
window.addEventListener('online', () =>
PageState.updatePageStateWithParams({isOffline: false})
);
window.addEventListener('offline', () =>
PageState.updatePageStateWithParams({isOffline: true})
);
} catch (e) {
}
}
}
PageState.licenseInterpretations = {
UNKNOWN: "UNKNOWN",
NONE: "NONE", // Queried Google (if prev was 2.0.1) and queried SN and got response none from both
LIFETIME: "LIFETIME", // Queried Google (if prev was 2.0.1) and this was the response
SUBSCRIPTION: "SUBSCRIPTION", // Queried SN - or got notified by SN - and this was the response + there's enough time till renewal
SUBSCRIPTION_SOON_OVER: "SUBSCRIPTION_SOON_OVER", // // Queried SN - or got notified by SN - and the expiration date is soon (± a month)
SUBSCRIPTION_OVER: "SUBSCRIPTION_OVER" // Queried SN - or got notified by SN - and the expiration date is over
}
try {
PageState.storedMemoryEpochLicense = Number.parseFloat("" + localStorage.getItem("epochLicenseExpirationDateInSecs"));
} catch (e) {
}
PageState.memoryEpochLicense = isNaN(PageState.storedMemoryEpochLicense) ? -1 : PageState.storedMemoryEpochLicense;
PageState.notificationTimeout = null;
PageState.pageState = {
platform: getMobileOperatingSystem(), // string may include "mac" / "linux" / etc...
isOffline: typeof window === "undefined" ? true : !window.navigator.onLine,
user: null,
epochLicenseExpirationDateInSecs: PageState.memoryEpochLicense // -1 = unknown, 0 = none, 3000000000 or above = life
}
/**
* Determine the mobile operating system.
* This function returns one of 'iOS', 'Android', 'Windows Phone', or 'unknown'.
*
* @returns {String}
*/
function getMobileOperatingSystem() {
try {
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
// Windows Phone must come first because its UA also contains "Android"
if (/windows phone/i.test(userAgent)) {
return "win";
}
if (/android/i.test(userAgent)) {
return "android";
}
// iOS detection from: http://stackoverflow.com/a/9039885/177710
if (/iPad|iPhone|iPod/.test(userAgent)) {
return "ios";
}
if (navigator.userAgent.includes("Mac") && "ontouchend" in document) {
return "ios";
}
} catch (e) {
}
return "";
}
PageState.pageStateHelpers = function (state) {
this.isSignedIn = function () {
return Boolean(state.user)
}
this.isIOS = function () {
return state.platform=="ios";
}
this.isAndroid = function () {
return state.platform=="android";
}
this.isMobile = function () {
let os = state.platform;
return os=="android" || os=="ios";
}
}
PageState.init();
exports.PageState = PageState;
},{"object-resolve-path":3}],3:[function(require,module,exports){
var Path = require('./path')
/**
*
* @param {Object} o
* @param {String} path
* @returns {*}
*/
module.exports = function (o, path) {
if (typeof path !== 'string') {
throw new TypeError('path must be a string')
}
if (typeof o !== 'object') {
throw new TypeError('object must be passed')
}
var pathObj = Path.get(path)
if (!pathObj.valid) {
throw new Error('path is not a valid object path')
}
return pathObj.getValueFrom(o)
}
},{"./path":4}],4:[function(require,module,exports){
// gutted from https://github.com/Polymer/observe-js/blob/master/src/observe.js
function noop () {}
function detectEval () {
// Don't test for eval if we're running in a Chrome App environment.
// We check for APIs set that only exist in a Chrome App context.
if (typeof chrome !== 'undefined' && chrome.app && chrome.app.runtime) {
return false
}
// Firefox OS Apps do not allow eval. This feature detection is very hacky
// but even if some other platform adds support for this function this code
// will continue to work.
if (typeof navigator != 'undefined' && navigator.getDeviceStorage) {
return false
}
try {
var f = new Function('', 'return true;')
return f()
} catch (ex) {
return false
}
}
var hasEval = detectEval()
function isIndex (s) {
return +s === s >>> 0 && s !== ''
}
function isObject (obj) {
return obj === Object(obj)
}
var createObject = ('__proto__' in {}) ?
function (obj) {
return obj
} :
function (obj) {
var proto = obj.__proto__
if (!proto)
return obj
var newObject = Object.create(proto)
Object.getOwnPropertyNames(obj).forEach(function (name) {
Object.defineProperty(newObject, name,
Object.getOwnPropertyDescriptor(obj, name))
})
return newObject
}
function parsePath (path) {
var keys = []
var index = -1
var c, newChar, key, type, transition, action, typeMap, mode = 'beforePath'
var actions = {
push: function () {
if (key === undefined)
return
keys.push(key)
key = undefined
},
append: function () {
if (key === undefined)
key = newChar
else
key += newChar
}
}
function maybeUnescapeQuote () {
if (index >= path.length)
return
var nextChar = path[index + 1]
if ((mode == 'inSingleQuote' && nextChar == "'") ||
(mode == 'inDoubleQuote' && nextChar == '"')) {
index++
newChar = nextChar
actions.append()
return true
}
}
while (mode) {
index++
c = path[index]
if (c == '\\' && maybeUnescapeQuote(mode))
continue
type = getPathCharType(c)
typeMap = pathStateMachine[mode]
transition = typeMap[type] || typeMap['else'] || 'error'
if (transition == 'error')
return // parse error
mode = transition[0]
action = actions[transition[1]] || noop
newChar = transition[2] === undefined ? c : transition[2]
action()
if (mode === 'afterPath') {
return keys
}
}
return // parse error
}
var identStart = '[\$_a-zA-Z]'
var identPart = '[\$_a-zA-Z0-9]'
var identRegExp = new RegExp('^' + identStart + '+' + identPart + '*' + '$')
function isIdent (s) {
return identRegExp.test(s)
}
var constructorIsPrivate = {}
function Path (parts, privateToken) {
if (privateToken !== constructorIsPrivate)
throw Error('Use Path.get to retrieve path objects')
for (var i = 0; i < parts.length; i++) {
this.push(String(parts[i]))
}
if (hasEval && this.length) {
this.getValueFrom = this.compiledGetValueFromFn()
}
}
var pathCache = {}
function getPath (pathString) {
if (pathString instanceof Path)
return pathString
if (pathString == null || pathString.length == 0)
pathString = ''
if (typeof pathString != 'string') {
if (isIndex(pathString.length)) {
// Constructed with array-like (pre-parsed) keys
return new Path(pathString, constructorIsPrivate)
}
pathString = String(pathString)
}
var path = pathCache[pathString]
if (path)
return path
var parts = parsePath(pathString)
if (!parts)
return invalidPath
var path = new Path(parts, constructorIsPrivate)
pathCache[pathString] = path
return path
}
Path.get = getPath
function formatAccessor (key) {
if (isIndex(key)) {
return '[' + key + ']'
} else {
return '["' + key.replace(/"/g, '\\"') + '"]'
}
}
Path.prototype = createObject({
__proto__: [],
valid: true,
toString: function () {
var pathString = ''
for (var i = 0; i < this.length; i++) {
var key = this[i]
if (isIdent(key)) {
pathString += i ? '.' + key : key
} else {
pathString += formatAccessor(key)
}
}
return pathString
},
getValueFrom: function (obj, directObserver) {
for (var i = 0; i < this.length; i++) {
if (obj == null)
return
obj = obj[this[i]]
}
return obj
},
iterateObjects: function (obj, observe) {
for (var i = 0; i < this.length; i++) {
if (i)
obj = obj[this[i - 1]]
if (!isObject(obj))
return
observe(obj, this[i])
}
},
compiledGetValueFromFn: function () {
var str = ''
var pathString = 'obj'
str += 'if (obj != null'
var i = 0
var key
for (; i < (this.length - 1); i++) {
key = this[i]
pathString += isIdent(key) ? '.' + key : formatAccessor(key)
str += ' &&\n ' + pathString + ' != null'
}
str += ')\n'
var key = this[i]
pathString += isIdent(key) ? '.' + key : formatAccessor(key)
str += ' return ' + pathString + ';\nelse\n return undefined;'
return new Function('obj', str)
},
setValueFrom: function (obj, value) {
if (!this.length)
return false
for (var i = 0; i < this.length - 1; i++) {
if (!isObject(obj))
return false
obj = obj[this[i]]
}
if (!isObject(obj))
return false
obj[this[i]] = value
return true
}
})
function getPathCharType (char) {
if (char === undefined)
return 'eof'
var code = char.charCodeAt(0)
switch (code) {
case 0x5B: // [
case 0x5D: // ]
case 0x2E: // .
case 0x22: // "
case 0x27: // '
case 0x30: // 0
return char
case 0x5F: // _
case 0x24: // $
return 'ident'
case 0x20: // Space
case 0x09: // Tab
case 0x0A: // Newline
case 0x0D: // Return
case 0xA0: // No-break space
case 0xFEFF: // Byte Order Mark
case 0x2028: // Line Separator
case 0x2029: // Paragraph Separator
return 'ws'
}
// a-z, A-Z
if ((0x61 <= code && code <= 0x7A) || (0x41 <= code && code <= 0x5A))
return 'ident'
// 1-9
if (0x31 <= code && code <= 0x39)
return 'number'
return 'else'
}
var pathStateMachine = {
'beforePath': {
'ws': ['beforePath'],
'ident': ['inIdent', 'append'],
'[': ['beforeElement'],
'eof': ['afterPath']
},
'inPath': {
'ws': ['inPath'],
'.': ['beforeIdent'],
'[': ['beforeElement'],
'eof': ['afterPath']
},
'beforeIdent': {
'ws': ['beforeIdent'],
'ident': ['inIdent', 'append']
},
'inIdent': {
'ident': ['inIdent', 'append'],
'0': ['inIdent', 'append'],
'number': ['inIdent', 'append'],
'ws': ['inPath', 'push'],
'.': ['beforeIdent', 'push'],
'[': ['beforeElement', 'push'],
'eof': ['afterPath', 'push']
},
'beforeElement': {
'ws': ['beforeElement'],
'0': ['afterZero', 'append'],
'number': ['inIndex', 'append'],
"'": ['inSingleQuote', 'append', ''],
'"': ['inDoubleQuote', 'append', '']
},
'afterZero': {
'ws': ['afterElement', 'push'],
']': ['inPath', 'push']
},
'inIndex': {
'0': ['inIndex', 'append'],
'number': ['inIndex', 'append'],
'ws': ['afterElement'],
']': ['inPath', 'push']
},
'inSingleQuote': {
"'": ['afterElement'],
'eof': ['error'],
'else': ['inSingleQuote', 'append']
},
'inDoubleQuote': {
'"': ['afterElement'],
'eof': ['error'],
'else': ['inDoubleQuote', 'append']
},
'afterElement': {
'ws': ['afterElement'],
']': ['inPath', 'push']
}
}
var invalidPath = new Path('', constructorIsPrivate)
invalidPath.valid = false
invalidPath.getValueFrom = invalidPath.setValueFrom = function () {}
module.exports = Path
},{}]},{},[1]);