diff --git a/connection-edit.js b/connection-edit.js new file mode 100644 index 0000000..2fa5b05 --- /dev/null +++ b/connection-edit.js @@ -0,0 +1,123 @@ +export class ConnectionEdit extends HTMLElement { + textEn = { + page: 'Page', + access: 'Access', + read: 'read', + readWrite: 'read and write', + doesntExist: "Error: The page doesn't exist", + samePage: "Error: Cannot connect to same page", + alreadyConnected: "Error: This page is already connected", + } + + textEs = { + page: 'Página', + access: 'Accesso', + read: 'leer', + readWrite: 'leer y escribir', + doesntExist: 'Error: La página no existe', + samePage: "Error: no se puede conectar a la misma página", + alreadyConnected: "Error: esta página ya está conectada", + } + + constructor() { + super() + this.attachShadow({mode: 'open'}) + this.language = navigator.language + const pageLabel = document.createElement('label') + pageLabel.innerText = this.text.page + this.pageInput = document.createElement('input') + const accessLabel = document.createElement('label') + accessLabel.innerText = this.text.access + this.accessSelect = document.createElement('select') + const wrap = document.createElement('div') + wrap.append(this.accessSelect) + const opts = ['read', 'readWrite'].map(value => { + const el = document.createElement('option') + el.value = value + el.innerText = this.text[value] + return el + }) + this.accessSelect.append(...opts) + this.accessSelect.value = 'read' + const fields = document.createElement('div') + fields.classList.add('fields') + fields.append( + pageLabel, + this.pageInput, + accessLabel, + wrap, + ) + this.shadowRoot.append( + fields, + ) + } + + connectedCallback() { + const style = document.createElement('style') + style.textContent = ` + :host { + display: flex; + flex-direction: column; + align-items: stretch; + } + .fields { + display: grid; + grid-template-columns: fit-content(50%) 1fr; + gap: 5px 10px; + } + .error { + color: red; + } + ` + this.shadowRoot.append(style) + } + + get data() { + return { + networkAccess: this.netSelect.value, + } + } + + set data(value) { + this.netText.innerText = JSON.stringify(value) + this.netSelect.value = value.networkAccess ?? 'local' + } + + set error(error) { + this._error = error + if (error === undefined) { + if (this.errorEl) { + this.errorEl.remove() + this.errorEl = undefined + } + } else { + if (!this.errorEl) { + this.errorEl = document.createElement('p') + this.errorEl.classList.add('error') + this.shadowRoot.append(this.errorEl) + } + this.errorEl.innerText = this.text[error] + } + } + + get error() { + return this._error + } + + get language() { + return this._language + } + + set language(language) { + this._language = language + this.text = this.langEs ? this.textEs : this.textEn + } + + get langEs() { + return /^es\b/.test(this.language) + } + + get lang() { + return this.language.split('-')[0] + } +} \ No newline at end of file diff --git a/connections.js b/connections.js new file mode 100644 index 0000000..272cb18 --- /dev/null +++ b/connections.js @@ -0,0 +1,174 @@ +export class Connections extends HTMLElement { + icons = { + del: ` + + + + ` + } + + textEn = { + add: 'Add connection', + cancel: 'Cancel', + read: 'read', + readWrite: 'read and write', + } + + textEs = { + add: 'Añadir conexión', + cancel: 'Cancelar', + read: 'leer', + readWrite: 'leer y escribir', + } + + constructor() { + super() + this.attachShadow({mode: 'open'}) + this.language = navigator.language + this.content = document.createElement('div') + const bGroup = document.createElement( + 'm-forms-button-group' + ) + bGroup.addPrimary(this.text.add, () => { + this.add() + }) + this.shadowRoot.append( + this.content, + bGroup, + ) + } + + connectedCallback() { + const style = document.createElement('style') + style.textContent = ` + :host { + display: flex; + flex-direction: column; + align-items: stretch; + margin-bottom: 5px; + } + m-dialog::part(footer) { + padding-top: 15px; + } + button.icon { + border: none; + background: inherit; + color: #555; + } + button.icon svg { + width: 12px; + height: 12px; + } + .connection { + display: flex; + flex-direction: row; + gap: 8px; + align-items: center; + } + .access { + font-size: 80%; + background: #ccc; + border-radius: 3px; + padding: 5px; + } + ` + this.shadowRoot.append(style) + } + + add() { + const dialog = document.createElement('m-dialog') + dialog.top = 180 + const edit = document.createElement( + 'm-settings-connection-edit' + ) + dialog.bodyEl.append(edit) + const bGroup = document.createElement( + 'm-forms-button-group' + ) + bGroup.addPrimary(this.text.add, () => { + const path = edit.pageInput.value + const access = edit.accessSelect.value + const exists = localStorage.getItem(path) + if (!exists) { + edit.error = 'doesntExist' + return + } else if (path === this.path) { + edit.error = 'samePage' + return + } else if (this.data[path] ?? true !== true) { + edit.error = 'alreadyConnected' + return + } + this.data = { + ...this.data, + [path]: access, + } + dialog.close() + }) + bGroup.addCancel(this.text.cancel, () => { + dialog.close() + }) + dialog.footerEl.appendChild(bGroup) + this.shadowRoot.append(dialog) + dialog.open() + } + + display() { + const entries = Object.entries(this.data).filter( + ([k, v]) => v !== undefined + ) + const content = entries.map(([path, access]) => { + const el = document.createElement('div') + el.classList.add('connection') + const pathEl = document.createElement('span') + pathEl.innerText = path + el.append(pathEl) + const accessEl = document.createElement('span') + accessEl.innerText = this.text[access] + accessEl.classList.add('access') + const delBtn = document.createElement('button') + delBtn.classList.add('delete', 'icon') + delBtn.innerHTML = this.icons.del + delBtn.addEventListener('click', () => { + this.data = {...this.data, [path]: undefined} + }) + el.append(accessEl, delBtn) + return el + }) + this.content.replaceChildren(...content) + } + + set type(value) { + this._type = value + } + + get type() { + return this._type + } + + get data() { + return this._data + } + + set data(value) { + this._data = value + this.display() + } + + get language() { + return this._language + } + + set language(language) { + this._language = language + this.text = this.langEs ? this.textEs : this.textEn + } + + get langEs() { + return /^es\b/.test(this.language) + } + + get lang() { + return this.language.split('-')[0] + } +} \ No newline at end of file diff --git a/network-settings.js b/network-settings.js new file mode 100644 index 0000000..a1ba7c8 --- /dev/null +++ b/network-settings.js @@ -0,0 +1,159 @@ +export class NetworkSettings extends HTMLElement { + textEn = { + csp: 'Content Security Policy', + } + + textEs = { + csp: 'Política de Seguridad de Contenido', + } + + accessOptions = { + local: { + option: { + en: "network access off", + es: "acceso a la red desactivado", + }, + text: { + en: 'direct network access off', + es: 'acceso directo a la red desactivado', + }, + details: { + en: "Direct network access is off and the page can't send data. This prevents data from being exposed even if the code of the page isn't trusted. It is a good option if this page will contain private data.", + es: "El acceso directo a la red está desactivado y la página no puede enviar datos. Esto evita que los datos queden expuestos incluso si el código de la página no es de confianza. Es una buena opción si esta página contendrá datos privados.", + }, + }, + jsCdns: { + option: { + en: 'jsDelivr and UNPKG', + es: 'jsDelivr y UNPKG', + }, + text: { + en: 'access to jsDelivr and UNPKG only', + es: 'acceso a jsDelivr y UNPKG solamente', + }, + details: { + en: "The page can make requests to jsDelivr and UNPKG. This could allow data from your page to be sent to the servers if the code in the page sends it. This means that if you have private data in the page, you should either trust the code on the page not to send it to these servers, or you should trust these servers, or both.", + es: "La página puede realizar solicitudes a jsDelivr y UNPKG. Esto podría permitir que los datos de su página se envíen a los servidores si el código de la página los envía. Esto significa que si tiene datos privados en la página, debe confiar en el código de la página para no enviarlos a estos servidores, o debe confiar en estos servidores, o en ambos.", + }, + }, + open: { + option: { + en: 'network open', + es: 'red abierta', + }, + text: { + en: 'network open (direct access to any site)', + es: 'red abierta (acceso directo a cualquier sitio)', + }, + details: { + en: "Network access is open, so the page can send or receive requests to any site. You should either A) have no private data in the page, or B) trust the code completely. This makes the page similar to major code playgrounds like codesandbox.com, stackblitz.com, jsbin.com, and codepen.io.", + es: "El acceso a la red está abierto, por lo que la página puede enviar o recibir solicitudes a cualquier sitio. Debería A) no tener datos privados en la página, o B) confiar completamente en el código. Esto hace que la página sea similar a los principales juegos de código como codesandbox.com, stackblitz.com, jsbin.com y codepen.io.", + }, + }, + } + + constructor() { + super() + this.attachShadow({mode: 'open'}) + this.language = navigator.language + const netSelectField = document.createElement('div') + netSelectField.classList.add('field') + this.netSelect = document.createElement('select') + this.netSelect.addEventListener( + 'change', () => this.display() + ) + netSelectField.append(this.netSelect) + const netOptions = Object.entries( + this.accessOptions + ).map(([value, {option}]) => { + const el = document.createElement('option') + el.value = value + el.innerText = option[this.lang] + return el + }) + this.netSelect.append(...netOptions) + this.netHeading = document.createElement('h3') + this.netText = document.createElement('p') + this.cspLabel = document.createElement( + 'div' + ) + this.cspLabel.classList.add('csp-label') + this.cspLabel.innerText = ( + this.text.csp + ' (CSP):' + ) + this.netCsp = document.createElement('div') + this.netCsp.classList.add('csp') + this.shadowRoot.append( + netSelectField, + this.netHeading, + this.netText, + this.cspLabel, + this.netCsp, + ) + } + + connectedCallback() { + const style = document.createElement('style') + style.textContent = ` + :host { + display: flex; + flex-direction: column; + align-items: stretch; + } + div.field { + display: flex; + flex-direction: row; + } + h1, h2, h3, p { + margin: 3px 0; + } + .csp-label { + font-weight: bold; + } + .csp { + font-family: monospace; + } + ` + this.shadowRoot.append(style) + } + + display() { + const value = this.netSelect.value + const opt = this.accessOptions[value] + const l = this.lang + this.netHeading.innerText = opt.text[l] + this.netText.innerText = opt.details[l] + this.netCsp.innerText = ( + this.cspProfiles[value] + ) + } + + get data() { + return { + networkAccess: this.netSelect.value, + } + } + + set data(value) { + this.netText.innerText = JSON.stringify(value) + this.netSelect.value = value.networkAccess ?? 'local' + this.display() + } + + get language() { + return this._language + } + + set language(language) { + this._language = language + this.text = this.langEs ? this.textEs : this.textEn + } + + get langEs() { + return /^es\b/.test(this.language) + } + + get lang() { + return this.language.split('-')[0] + } +} \ No newline at end of file diff --git a/page-settings.js b/page-settings.js index d3559cf..50c074c 100644 --- a/page-settings.js +++ b/page-settings.js @@ -1,106 +1,47 @@ export class PageSettings extends HTMLElement { textEn = { - networkAccess: 'Direct network access (CSP)', - csp: 'Content Security Policy', + outbound: 'Outbound connections', + inbound: 'Inbound connections', + netAccess: 'Direct network access (CSP)', } textEs = { - networkAccess: 'Acceso directo a la red (CSP)', - csp: 'Política de Seguridad de Contenido', - } - - networkAccessOptions = { - local: { - option: { - en: "network access off", - es: "acceso a la red desactivado", - }, - text: { - en: 'direct network access off', - es: 'acceso directo a la red desactivado', - }, - details: { - en: "Direct network access is off and the page can't send data. This prevents data from being exposed even if the code of the page isn't trusted. It is a good option if this page will contain private data.", - es: "El acceso directo a la red está desactivado y la página no puede enviar datos. Esto evita que los datos queden expuestos incluso si el código de la página no es de confianza. Es una buena opción si esta página contendrá datos privados.", - }, - }, - unpkg: { - option: { - en: 'UNPKG', - es: 'UNPKG', - }, - text: { - en: 'access to UNPKG only', - es: 'acceso a UNPKG solamente', - }, - details: { - en: "The page can make requests to UNPKG. This could allow data from your page to be sent to the servers if the code in the page sends it. This means that if you have private data in the page, you should either trust the code on the page not to send it to these servers, or you should trust these servers, or both.", - es: "La página puede realizar solicitudes a UNPKG. Esto podría permitir que los datos de su página se envíen a los servidores si el código de la página los envía. Esto significa que si tiene datos privados en la página, debe confiar en el código de la página para no enviarlos a estos servidores, o debe confiar en estos servidores, o en ambos.", - }, - }, - open: { - option: { - en: 'network open', - es: 'red abierta', - }, - text: { - en: 'network open (direct access to any site)', - es: 'red abierta (acceso directo a cualquier sitio)', - }, - details: { - en: "Network access is open, so the page can send or receive requests to any site. You should either A) have no private data in the page, or B) trust the code completely. This makes the page similar to major code playgrounds like codesandbox.com, stackblitz.com, jsbin.com, and codepen.io.", - es: "El acceso a la red está abierto, por lo que la página puede enviar o recibir solicitudes a cualquier sitio. Debería A) no tener datos privados en la página, o B) confiar completamente en el código. Esto hace que la página sea similar a los principales juegos de código como codesandbox.com, stackblitz.com, jsbin.com y codepen.io.", - }, - }, + outbound: 'Conexiones salientes', + inbound: 'Conexiones entrantes', + netAccess: 'Acceso directo a la red (CSP)', } constructor() { super() this.attachShadow({mode: 'open'}) this.language = navigator.language - const netSelectLabel = document.createElement('div') - netSelectLabel.classList.add('label') - netSelectLabel.innerText = this.text.networkAccess - const netSelectField = ( - document.createElement('div') - ) - netSelectField.classList.add('field') - this.netSelect = document.createElement( - 'select' - ) - this.netSelect.addEventListener( - 'change', () => this.display() + const outboundHeading = document.createElement('div') + outboundHeading.classList.add('heading') + outboundHeading.innerText = this.text.outbound + this.outbound = document.createElement( + 'm-settings-connections' ) - netSelectField.append(this.netSelect) - const netOptions = Object.entries( - this.networkAccessOptions - ).map(([value, {option}]) => { - const el = document.createElement('option') - el.value = value - el.innerText = option[this.lang] - return el - }) - this.netSelect.append(...netOptions) - this.netHeading = document.createElement( - 'h3' + this.outbound.type = 'outbound' + const inboundHeading = document.createElement('div') + inboundHeading.classList.add('heading') + inboundHeading.innerText = this.text.inbound + this.inbound = document.createElement( + 'm-settings-connections' ) - this.netText = document.createElement('p') - this.cspLabel = document.createElement( - 'div' + this.inbound.type = 'inbound' + const netSelectHeading = document.createElement('div') + netSelectHeading.classList.add('heading') + netSelectHeading.innerText = this.text.netAccess + this.network = document.createElement( + 'm-settings-network-settings' ) - this.cspLabel.classList.add('csp-label') - this.cspLabel.innerText = ( - this.text.csp + ' (CSP):' - ) - this.netCsp = document.createElement('div') - this.netCsp.classList.add('csp') this.shadowRoot.append( - netSelectLabel, - netSelectField, - this.netHeading, - this.netText, - this.cspLabel, - this.netCsp, + outboundHeading, + this.outbound, + inboundHeading, + this.inbound, + netSelectHeading, + this.network, ) } @@ -114,54 +55,53 @@ export class PageSettings extends HTMLElement { align-items: stretch; overflow-y: auto; } - div.label { - display: flex; - flex-direction: row; - justify-content: flex-start; - background: #ddd; - padding: 5px; - border-radius: 3px; - margin-bottom: 5px; - font-weight: bold; + * { + padding-left: 10px; } - div.field { + div.heading { display: flex; flex-direction: row; - } - h1, h2, h3, p { - margin: 3px 0; - } - .csp-label { + justify-content: flex-start; + background: #f2dbd8; + padding: 3px; + margin-bottom: 10px; + margin-top: 10px; font-weight: bold; } - .csp { - font-family: monospace; - } ` this.shadowRoot.append(style) } - display() { - const value = this.netSelect.value - const opt = this.networkAccessOptions[value] - const l = this.lang - this.netHeading.innerText = opt.text[l] - this.netText.innerText = opt.details[l] - this.netCsp.innerText = ( - this.cspProfiles[value] - ) - } - get data() { return { - networkAccess: this.netSelect.value, + ...this.network.data, + connections: { + outbound: this.outbound.data, + inbound: this.inbound.data, + }, } } set data(value) { - this.netText.innerText = JSON.stringify(value) - this.netSelect.value = value.networkAccess ?? 'local' - this.display() + this.outbound.data = value.connections?.outbound || {} + this.inbound.data = value.connections?.inbound || {} + this.network.data = { + networkAccess: value.networkAccess + } + } + + set path(value) { + this._path = value + this.outbound.path = value + this.inbound.path = value + } + + get path() { + return this._path + } + + set cspProfiles(value) { + this.network.cspProfiles = value } get language() {