UNPKG

crypto-slots

Version:

A minimal test server is provided, see server

953 lines (868 loc) 28.4 kB
/** * Add space between camelCase text. */ var unCamelCase = (string) => { string = string.replace(/([a-z\xE0-\xFF])([A-Z\xC0\xDF])/g, '$1 $2'); string = string.toLowerCase(); return string; }; /** * Replaces all accented chars with regular ones */ var replaceAccents = (string) => { // verifies if the String has accents and replace them if (string.search(/[\xC0-\xFF]/g) > -1) { string = string .replace(/[\xC0-\xC5]/g, 'A') .replace(/[\xC6]/g, 'AE') .replace(/[\xC7]/g, 'C') .replace(/[\xC8-\xCB]/g, 'E') .replace(/[\xCC-\xCF]/g, 'I') .replace(/[\xD0]/g, 'D') .replace(/[\xD1]/g, 'N') .replace(/[\xD2-\xD6\xD8]/g, 'O') .replace(/[\xD9-\xDC]/g, 'U') .replace(/[\xDD]/g, 'Y') .replace(/[\xDE]/g, 'P') .replace(/[\xE0-\xE5]/g, 'a') .replace(/[\xE6]/g, 'ae') .replace(/[\xE7]/g, 'c') .replace(/[\xE8-\xEB]/g, 'e') .replace(/[\xEC-\xEF]/g, 'i') .replace(/[\xF1]/g, 'n') .replace(/[\xF2-\xF6\xF8]/g, 'o') .replace(/[\xF9-\xFC]/g, 'u') .replace(/[\xFE]/g, 'p') .replace(/[\xFD\xFF]/g, 'y'); } return string; }; var removeNonWord = (string) => string.replace(/[^0-9a-zA-Z\xC0-\xFF \-]/g, ''); const WHITE_SPACES = [ ' ', '\n', '\r', '\t', '\f', '\v', '\u00A0', '\u1680', '\u180E', '\u2000', '\u2001', '\u2002', '\u2003', '\u2004', '\u2005', '\u2006', '\u2007', '\u2008', '\u2009', '\u200A', '\u2028', '\u2029', '\u202F', '\u205F', '\u3000' ]; /** * Remove chars from beginning of string. */ var ltrim = (string, chars) => { chars = chars || WHITE_SPACES; let start = 0, len = string.length, charLen = chars.length, found = true, i, c; while (found && start < len) { found = false; i = -1; c = string.charAt(start); while (++i < charLen) { if (c === chars[i]) { found = true; start++; break; } } } return (start >= len) ? '' : string.substr(start, len); }; /** * Remove chars from end of string. */ var rtrim = (string, chars) => { chars = chars || WHITE_SPACES; var end = string.length - 1, charLen = chars.length, found = true, i, c; while (found && end >= 0) { found = false; i = -1; c = string.charAt(end); while (++i < charLen) { if (c === chars[i]) { found = true; end--; break; } } } return (end >= 0) ? string.substring(0, end + 1) : ''; }; /** * Remove white-spaces from beginning and end of string. */ var trim = (string, chars) => { chars = chars || WHITE_SPACES; return ltrim(rtrim(string, chars), chars); }; /** * Convert to lower case, remove accents, remove non-word chars and * replace spaces with the specified delimeter. * Does not split camelCase text. */ var slugify = (string, delimeter) => { if (delimeter == null) { delimeter = "-"; } string = replaceAccents(string); string = removeNonWord(string); string = trim(string) //should come after removeNonWord .replace(/ +/g, delimeter) //replace spaces with delimeter .toLowerCase(); return string; }; /** * Replaces spaces with hyphens, split camelCase text, remove non-word chars, remove accents and convert to lower case. */ var hyphenate = string => { string = unCamelCase(string); return slugify(string, "-"); }; const shouldRegister = name => { return customElements.get(name) ? false : true; }; var define = klass => { const name = hyphenate(klass.name); return shouldRegister(name) ? customElements.define(name, klass) : ''; }; /** * @module CSSMixin * @mixin Backed * @param {class} base class to extend from */ const mixins = { 'mixin(--css-row)': `display: flex; flex-direction: row; `, 'mixin(--css-column)': `display: flex; flex-direction: column; `, 'mixin(--css-center)': `align-items: center;`, 'mixin(--css-header)': `height: 128px; width: 100%; background: var(--primary-color); color: var(--text-color); mixin(--css-column)`, 'mixin(--css-flex)': `flex: 1;`, 'mixin(--css-flex-2)': `flex: 2;`, 'mixin(--css-flex-3)': `flex: 3;`, 'mixin(--css-flex-4)': `flex: 4;`, 'mixin(--material-palette)': `--dark-primary-color: #00796B; --light-primary-color: #B2DFDB; --primary-color: #009688; --text-color: #FFF; --primary-text-color: #212121; --secondary-text-color: #757575; --divider-color: #BDBDBD; --accent-color: #4CAF50; --disabled-text-color: #BDBDBD; --primary-background-color: #f9ffff; --dialog-background-color: #FFFFFF;`, 'mixin(--css-hero)': `display: flex; max-width: 600px; max-height: 340px; height: 100%; width: 100%; box-shadow: 3px 2px 4px 2px rgba(0,0,0, 0.15), -2px 0px 4px 2px rgba(0,0,0, 0.15); position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); border-radius: 2px; `, 'mixin(--css-elevation-2dp)': ` box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2);`, 'mixin(--css-elevation-3dp)': ` box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.14), 0 1px 8px 0 rgba(0, 0, 0, 0.12), 0 3px 3px -2px rgba(0, 0, 0, 0.4);`, 'mixin(--css-elevation-4dp)': ` box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.4);`, 'mixin(--css-elevation-6dp)': ` box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.4);`, 'mixin(--css-elevation-8dp)': ` box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.4);`, 'mixin(--css-elevation-12dp)': ` box-shadow: 0 12px 16px 1px rgba(0, 0, 0, 0.14), 0 4px 22px 3px rgba(0, 0, 0, 0.12), 0 6px 7px -4px rgba(0, 0, 0, 0.4);`, 'mixin(--css-elevation-16dp)': ` box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14), 0 6px 30px 5px rgba(0, 0, 0, 0.12), 0 8px 10px -5px rgba(0, 0, 0, 0.4);`, 'mixin(--css-elevation-24dp)': ` box-shadow: 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12), 0 11px 15px -7px rgba(0, 0, 0, 0.4);` }; const classes = { 'apply(--css-row)': `.row { mixin(--css-row) } `, 'apply(--css-column)': `.column { mixin(--css-column) } `, 'apply(--css-flex)': `.flex { mixin(--css-flex) } `, 'apply(--css-flex-2)': `.flex-2 { mixin(--css-flex-2) }`, 'apply(--css-flex-3)': `.flex-3 { mixin(--css-flex-3) }`, 'apply(--css-flex-4)': `.flex-4 { mixin(--css-flex-4) }`, 'apply(--css-center)': `.center { align-items: center; } `, 'apply(--css-center-center)': `.center-center { align-items: center; justify-content: center; } `, 'apply(--css-header)': `header, .header { mixin(--css-header) }`, 'apply(--css-hero)': `.hero { mixin(--css-hero) }`, 'apply(--css-elevation-2dp)': `.elevation-2dp { mixin(--css-elevation-2dp) }`, 'apply(--css-elevation-3dp)': `.elevation-3dp { mixin(--css-elevation-3dp) }`, 'apply(--css-elevation-4dp)': `.elevation-4dp { mixin(--css-elevation-4dp) }`, 'apply(--css-elevation-6dp)': `.elevation-6dp { mixin(--css-elevation-6dp) }`, 'apply(--css-elevation-8dp)': `.elevation-8dp { mixin(--css-elevation-8dp) }`, 'apply(--css-elevation-12dp)': `.elevation-12dp { mixin(--css-elevation-12dp) }`, 'apply(--css-elevation-16dp)': `.elevation-16dp { mixin(--css-elevation-16dp) }`, 'apply(--css-elevation-18dp)': `.elevation-18dp { mixin(--css-elevation-18dp) }` }; var CSSMixin = base => { return class CSSMixin extends base { get __style() { return this.shadowRoot.querySelector('style'); } constructor() { super(); } connectedCallback() { // TODO: test if (super.connectedCallback) super.connectedCallback(); // TODO: Implement better way to check if a renderer is used if (this.render) this.hasRenderer = true; else if(this.template) console.log(`Render method undefined ${this.localname}`); this._init(); } _init() { if (this.hasRenderer) { if (!this.rendered) { return requestAnimationFrame(() => { this._init(); }); } } const styles = this.shadowRoot ? this.shadowRoot.querySelectorAll('style') : this.querySelectorAll('style'); // const matches = style.innerHTML.match(/apply((.*))/g); styles.forEach(style => { this._applyClasses(style.innerHTML).then(innerHTML => { if (innerHTML) this.__style.innerHTML = innerHTML; this._applyMixins(style.innerHTML).then(innerHTML => { if (innerHTML) this.__style.innerHTML = innerHTML; }); }).catch(error => { console.error(error); }); }); // this._applyVariables(matches, style); } _applyMixins(string) { const mixinInMixin = string => { if (!string) return console.warn(`Nothing found for ${string}`); const matches = string.match(/mixin((.*))/g); if (matches) { for (const match of matches) { const mixin = mixins[match]; string = string.replace(match, mixin); } } return string; }; return new Promise((resolve, reject) => { const matches = string.match(/mixin((.*))/g); if (matches) for (const match of matches) { const mixin = mixinInMixin(mixins[match]); string = string.replace(match, mixin); // return [ // match, mixins[match] // ] } resolve(string); }); } _applyClasses(string) { return new Promise((resolve, reject) => { const matches = string.match(/apply((.*))/g); if (matches) for (const match of matches) { // this._applyMixins(classes[match]).then(klass => { string = string.replace(match, classes[match]); // }); } // this.style.innerHTML = string; resolve(string); }); } } }; /** * @param {object} element HTMLElement * @param {function} tagResult custom-renderer-mixin {changes: [], template: ''} */ var render = (element, {changes, template}) => { if (!changes && !template) return console.warn('changes or template expected'); if (element.shadowRoot) element = element.shadowRoot; if (!element.innerHTML) element.innerHTML = template; for (const key of Object.keys(changes)) { const els = Array.from(element.querySelectorAll(`[render-mixin-id="${key}"]`)); for (const el of els) { el.innerHTML = changes[key]; } } return; }; /** * * @example ```js const template = html`<h1>${'name'}</h1>`; let templateResult = template({name: 'Olivia'}); templateResult.values // property values 'Olivia' templateResult.keys // property keys 'name' templateResult.strings // raw template array '["<h1>", "</h1>"]' ``` */ const html$1 = (strings, ...keys) => { return ((...values) => { return {strings, keys, values}; }); }; window.html = window.html || html$1; var RenderMixin = (base = HTMLElement) => class RenderMixin extends base { constructor() { super(); this.set = []; this.renderer = this.renderer.bind(this); this.render = this.renderer; } beforeRender({values, strings, keys}) { const dict = values[values.length - 1] || {}; const changes = {}; let template = null; if (!this.rendered) template = strings[0]; if (values[0] !== undefined) { keys.forEach((key, i) => { const string = strings[i + 1]; let value = Number.isInteger(key) ? values[key] : dict[key]; if (value === undefined && Array.isArray(key)) { value = key.join(''); } else if (value === undefined && !Array.isArray(key) && this.set[i]) { value = this.set[i].value; // set previous value, doesn't require developer to pass all properties } else if (value === undefined && !Array.isArray(key) && !this.set[i]) { value = ''; } if (!this.rendered) { template = template.replace(/(>)[^>]*$/g, ` render-mixin-id="${key}">`); template += `${value}${string}`; } if (this.set[key] && this.set[key] !== value) { changes[key] = value; this.set[key] = value; } else if (!this.set[key]) { this.set[key] = value; changes[key] = value; } }); } else { template += strings[0]; } return { template, changes }; } renderer(properties = this.properties, template = this.template) { if (!properties) properties = {}; else if (!this.isFlat(properties)) { // check if we are dealing with an flat or indexed object // create flat object getting the values from super if there is one // default to given properties set properties[key].value // this implementation is meant to work with 'property-mixin' // checkout https://github.com/vandeurenglenn/backed/src/mixin/property-mixin // while I did not test, I believe it should be compatible with PolymerElements const object = {}; // try getting value from this.property // try getting value from properties.property.value // try getting value from property.property // fallback to property for (const key of Object.keys(properties)) { let value; if (this[key] !== undefined) value = this[key]; else if (properties[key] && properties[key].value !== undefined) { value = properties[key].value; } else { value = ''; } object[key] = value; } properties = object; } render(this, this.beforeRender(template(properties))); } /** * wether or not properties is just an object or indexed object (like {prop: {value: 'value'}}) */ isFlat(object) { const firstObject = object[Object.keys(object)[0]]; if (firstObject) if (firstObject.hasOwnProperty('value') || firstObject.hasOwnProperty('reflect') || firstObject.hasOwnProperty('observer') || firstObject.hasOwnProperty('render')) return false; else return true; } connectedCallback() { if (super.connectedCallback) super.connectedCallback(); if (this.render) { this.render(); this.rendered = true; } } }; window.Backed = window.Backed || {}; // binding does it's magic using the propertyStore ... window.Backed.PropertyStore = window.Backed.PropertyStore || new Map(); // TODO: Create & add global observer var PropertyMixin = base => { return class PropertyMixin extends base { static get observedAttributes() { return Object.entries(this.properties).map(entry => {if (entry[1].reflect) {return entry[0]} else return null}); } get properties() { return customElements.get(this.localName).properties; } constructor() { super(); if (this.properties) { for (const entry of Object.entries(this.properties)) { const { observer, reflect, renderer } = entry[1]; // allways define property even when renderer is not found. this.defineProperty(entry[0], entry[1]); } } } connectedCallback() { if (super.connectedCallback) super.connectedCallback(); if (this.attributes) for (const attribute of this.attributes) { if (String(attribute.name).includes('on-')) { const fn = attribute.value; const name = attribute.name.replace('on-', ''); this.addEventListener(String(name), event => { let target = event.path[0]; while (!target.host) { target = target.parentNode; } if (target.host[fn]) { target.host[fn](event); } }); } } } attributeChangedCallback(name, oldValue, newValue) { this[name] = newValue; } /** * @param {function} options.observer callback function returns {instance, property, value} * @param {boolean} options.reflect when true, reflects value to attribute * @param {function} options.render callback function for renderer (example: usage with lit-html, {render: render(html, shadowRoot)}) */ defineProperty(property = null, {strict = false, observer, reflect = false, renderer, value}) { Object.defineProperty(this, property, { set(value) { if (value === this[`___${property}`]) return; this[`___${property}`] = value; if (reflect) { if (value) this.setAttribute(property, String(value)); else this.removeAttribute(property); } if (observer) { if (observer in this) this[observer](); else console.warn(`observer::${observer} undefined`); } if (renderer) { const obj = {}; obj[property] = value; if (renderer in this) this.render(obj, this[renderer]); else console.warn(`renderer::${renderer} undefined`); } }, get() { return this[`___${property}`]; }, configurable: strict ? false : true }); // check if attribute is defined and update property with it's value // else fallback to it's default value (if any) const attr = this.getAttribute(property); this[property] = attr || this.hasAttribute(property) || value; } } }; /** * @mixin Backed * @module utils * @export merge * * some-prop -> someProp * * @param {object} object The object to merge with * @param {object} source The object to merge * @return {object} merge result */ var merge = (object = {}, source = {}) => { // deep assign for (const key of Object.keys(object)) { if (source[key]) { Object.assign(object[key], source[key]); } } // assign the rest for (const key of Object.keys(source)) { if (!object[key]) { object[key] = source[key]; } } return object; }; var SelectMixin = base => { return class SelectMixin extends PropertyMixin(base) { static get properties() { return merge(super.properties, { selected: { value: 0, observer: '__selectedObserver__' } }); } constructor() { super(); } get slotted() { return this.shadowRoot ? this.shadowRoot.querySelector('slot') : this; } get _assignedNodes() { const nodes = 'assignedNodes' in this.slotted ? this.slotted.assignedNodes() : this.children; const arr = []; for (var i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.nodeType === 1) arr.push(node); } return arr; } /** * @return {String} */ get attrForSelected() { return this.getAttribute('attr-for-selected') || 'name'; } set attrForSelected(value) { this.setAttribute('attr-for-selected', value); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { // check if value is number if (!isNaN(newValue)) { newValue = Number(newValue); } this[name] = newValue; } } /** * @param {string|number|HTMLElement} selected */ select(selected) { if (selected) this.selected = selected; // TODO: fix selectedobservers if (this.multi) this.__selectedObserver__(); } next(string) { const index = this.getIndexFor(this.currentSelected); if (index !== -1 && index >= 0 && this._assignedNodes.length > index && (index + 1) <= this._assignedNodes.length - 1) { this.selected = this._assignedNodes[index + 1]; } } previous() { const index = this.getIndexFor(this.currentSelected); if (index !== -1 && index >= 0 && this._assignedNodes.length > index && (index - 1) >= 0) { this.selected = this._assignedNodes[index - 1]; } } getIndexFor(element) { if (element && element instanceof HTMLElement === false) return console.error(`${element} is not an instanceof HTMLElement`); return this._assignedNodes.indexOf(element || this.selected); } _updateSelected(selected) { selected.classList.add('custom-selected'); if (this.currentSelected && this.currentSelected !== selected) { this.currentSelected.classList.remove('custom-selected'); } this.currentSelected = selected; } /** * @param {string|number|HTMLElement} change.value */ __selectedObserver__(value) { const type = typeof this.selected; if (Array.isArray(this.selected)) { for (const child of this._assignedNodes) { if (child.nodeType === 1) { if (this.selected.indexOf(child.getAttribute(this.attrForSelected)) !== -1) { child.classList.add('custom-selected'); } else { child.classList.remove('custom-selected'); } } } return; } else if (type === 'object') return this._updateSelected(this.selected); else if (type === 'string') { for (const child of this._assignedNodes) { if (child.nodeType === 1) { if (child.getAttribute(this.attrForSelected) === this.selected) { return this._updateSelected(child); } } } } else { // set selected by index const child = this._assignedNodes[this.selected]; if (child && child.nodeType === 1) this._updateSelected(child); // remove selected even when nothing found, better to return nothing } } } }; /** * @extends HTMLElement */ class CustomPages extends SelectMixin(HTMLElement) { constructor() { super(); this.slotchange = this.slotchange.bind(this); this.attachShadow({mode: 'open'}); this.shadowRoot.innerHTML = ` <style> :host { flex: 1; position: relative; --primary-background-color: #ECEFF1; overflow: hidden; } ::slotted(*) { display: flex; position: absolute; opacity: 0; pointer-events: none; top: 0; left: 0; right: 0; bottom: 0; transition: transform ease-out 160ms, opacity ease-out 60ms; /*transform: scale(0.5);*/ transform-origin: left; } ::slotted(.animate-up) { transform: translateY(-120%); } ::slotted(.animate-down) { transform: translateY(120%); } ::slotted(.custom-selected) { opacity: 1; pointer-events: auto; transform: translateY(0); transition: transform ease-in 160ms, opacity ease-in 320ms; max-height: 100%; max-width: 100%; } </style> <!-- TODO: scale animation, ace doesn't resize that well ... --> <div class="wrapper"> <slot></slot> </div> `; } connectedCallback() { super.connectedCallback(); this.shadowRoot.querySelector('slot').addEventListener('slotchange', this.slotchange); } isEvenNumber(number) { return Boolean(number % 2 === 0) } /** * set animation class when slot changes */ slotchange() { let call = 0; for (const child of this.slotted.assignedNodes()) { if (child && child.nodeType === 1) { child.style.zIndex = 99 - call; if (this.isEvenNumber(call++)) { child.classList.add('animate-down'); } else { child.classList.add('animate-up'); } this.dispatchEvent(new CustomEvent('child-change', {detail: child})); } } } }customElements.define('custom-pages', CustomPages); window.slotsAPI = window.slotsAPI || {}; window.slotsAPI.amount = { get: async () => Promise.resolve(localStorage.getItem('amount')), set: async amount => Promise.resolve(localStorage.setItem('amount', amount)) }; var slots = define(class CryptoSlots extends CSSMixin(RenderMixin(HTMLElement)) { get pages() { return this.shadowRoot.querySelector('custom-pages') } constructor() { super(); window.slots = new Map(); this.attachShadow({mode: 'open'}); } connectedCallback() { if (super.connectedCallback) super.connectedCallback(); (async () => { globalThis.loadGame = this.loadGame.bind(this); this.backToSlots(); this.amount = await slotsAPI.amount.get(); this.shadowRoot.querySelector('.amount').innerHTML = `${this.amount} LFC`; document.addEventListener('spin-end', async () => { this.amount = await slotsAPI.amount.get(); this.shadowRoot.querySelector('.amount').innerHTML = `${this.amount} LFC`; }); })(); } async loadGame(game) { const tag = `${game}-slot`; if (!await customElements.get(tag)) { await import(`./slots/${tag}.js`); } const el = document.createElement(tag); el.dataset.route = game; window.slots.set('game', game === 'default' ? 'top-ten' : game); this.pages.appendChild(el); this.pages.select(game); } async backToSlots() { if (!await customElements.get('slots-view')) { await import('./chunk-b16f93d6.js'); } const el = document.createElement('slots-view'); el.dataset.route = 'slots'; this.pages.appendChild(el); this.pages.select('slots'); } get template() { return html` <style> :host { display: flex; flex-direction: column; height: 100%; width: 100%; background: #022e44; color: #fff; font-size: 16px; position: relative; } header { display: flex; height: 56px; width: 100%; align-items: center; background: #042434; padding: 0 12px; box-sizing: border-box; } .hero { mixin(--css-hero) max-height: 500px; overflow: hidden; } a { text-decoration: none; cursor: pointer; color: #c5cae9; } footer { display: flex; flex-direction: row; align-items: center; justify-content: center; height: 40px; font-size: 12px; } apply(--css-flex) </style> <header> <span class="flex"></span> <span class="amount"></span> <span class="flex"></span> </header> <custom-pages attr-for-selected="data-route"> <slots-view></slots-view> <default-slot></default-slot> <onebythree-slot></onebythree-slot> </custom-pages> <footer> <span>&#169; 2019 Glenn Vandeuren. Code licensed under the <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC-BY-NC-SA-4.0</a> License.</span> </footer>`; } }); export default slots;