Make using web components easier

This commit is contained in:
KernelDeimos 2024-05-02 20:53:48 -04:00
parent 2681a78501
commit c99747d7f2
7 changed files with 233 additions and 1 deletions

View File

@ -0,0 +1,47 @@
import { Component } from "../../util/Component.js";
export default class QRCodeView extends Component {
static PROPERTIES = {
value: {
description: 'The text to encode in the QR code',
}
}
static CSS = /*css*/`
.qr-code {
width: 100%;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
}
`
create_template ({ template }) {
// TODO: The way we handle loading assets doesn't work well
// with web components, so for now it goes in the template.
$(template).html(`
<div class="qr-code opt-qr-code">
</div>
`);
}
on_ready ({ listen }) {
listen('value', value => {
console.log('got value', value);
// $(this.dom_).find('.qr-code').empty();
new QRCode($(this.dom_).find('.qr-code').get(0), {
text: value,
currectLevel: QRCode.CorrectLevel.H,
});
});
}
}
// TODO: This is necessary because files can be loaded from
// both `/src/UI` and `/UI` in the URL; we need to fix that
if ( ! window.__component_qr_code ) {
window.__component_qr_code = true;
customElements.define('qr-code', QRCodeView);
}

View File

@ -59,7 +59,7 @@ export default {
const confirmation = await UIWindowQR({
message_i18n_key: 'scan_qr_2fa',
text: data.url,
text_below: data.secret,
text_alternative: data.secret,
confirmations: [
i18n('confirm_2fa_setup'),
i18n('confirm_2fa_recovery'),

View File

@ -17,7 +17,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import Placeholder from '../util/Placeholder.js';
import TeePromise from '../util/TeePromise.js';
import QRCodeView from './Components/QRCode.js';
import UIWindow from './UIWindow.js'
let checkbox_id_ = 0;
@ -29,6 +31,8 @@ async function UIWindowQR(options){
options = options ?? {};
const placeholder_qr = Placeholder();
let h = '';
// close button containing the multiplication sign
// h += `<div class="qr-code-window-close-btn generic-close-window-button"> &times; </div>`;
@ -38,6 +42,16 @@ async function UIWindowQR(options){
}</h1>`;
h += `</div>`;
h += placeholder_qr.html;
if ( options.text_alternative ) {
h += `<div class="otp-as-text">`;
h += `<p style="text-align: center; font-size: 16px; padding: 10px; font-weight: 400; margin: -10px 10px 20px 10px; -webkit-font-smoothing: antialiased; color: #5f626d;">${
html_encode(options.text_alternative)
}</p>`;
h += `</div>`;
}
if ( options.recovery_codes ) {
h += `<div class="recovery-codes">`;
h += `<h2 style="text-align: center; font-size: 16px; padding: 10px; font-weight: 400; margin: -10px 10px 20px 10px; -webkit-font-smoothing: antialiased; color: #5f626d;">${
@ -112,6 +126,14 @@ async function UIWindowQR(options){
},
})
const component_qr = new QRCodeView({
value: options.text
});
console.log('test', component_qr);
component_qr.attach(placeholder_qr);
// placeholder_qr.replaceWith($(`<h1>test</h1>`).get(0));
if ( false ) {
// generate auth token QR code
new QRCode($(el_window).find('.otp-qr-code').get(0), {
text: options.text,
@ -121,6 +143,7 @@ async function UIWindowQR(options){
colorLight : "#ffffff",
correctLevel : QRCode.CorrectLevel.H
});
}
if ( confirmations.length > 0 ) {
$(el_window).find('.code-confirm-btn').prop('disabled', true);

View File

@ -2613,6 +2613,10 @@ label {
margin-bottom: 20px;
}
.otp-as-text {
margin: 20px 0;
}
.recovery-codes {
border: 1px solid #ccc;
padding: 20px;

63
src/util/Component.js Normal file
View File

@ -0,0 +1,63 @@
import ValueHolder from "./ValueHolder.js";
export class Component extends HTMLElement {
constructor (property_values) {
super();
this.dom_ = this.attachShadow({ mode: 'open' });
this.values_ = {};
if ( this.constructor.template ) {
const template = document.querySelector(this.constructor.template);
this.dom_.appendChild(template.content.cloneNode(true));
}
for ( const key in this.constructor.PROPERTIES ) {
let initial_value;
if ( property_values && key in property_values ) {
initial_value = property_values[key];
}
this.values_[key] = ValueHolder.adapt(initial_value);
}
}
connectedCallback () {
console.log('connectedCallback called')
this.on_ready && this.on_ready(this.get_api_());
}
attach (placeholder) {
const el = this.create_element_();
this.dom_.appendChild(el);
placeholder.replaceWith(this);
}
place (slot_name, child_node) {
child_node.setAttribute('slot', slot_name);
this.appendChild(child_node);
}
create_element_ () {
const template = document.createElement('template');
if ( this.constructor.CSS ) {
const style = document.createElement('style');
style.textContent = this.constructor.CSS;
this.dom_.appendChild(style);
}
if ( this.create_template ) {
this.create_template({ template });
}
const el = template.content.cloneNode(true);
return el;
}
get_api_ () {
return {
listen: (name, callback) => {
this.values_[name].sub(callback);
callback(this.values_[name].get());
}
};
}
}

37
src/util/Placeholder.js Normal file
View File

@ -0,0 +1,37 @@
/**
* @typedef {Object} PlaceholderReturn
* @property {String} html: An html string that represents the placeholder
* @property {String} id: The unique ID of the placeholder
* @property {Function} replaceWith: A function that takes a DOM element
* as an argument and replaces the placeholder with it
*/
/**
* Placeholder creates a simple element with a unique ID
* as an HTML string.
*
* This can be useful where string concatenation is used
* to build element trees.
*
* The `replaceWith` method can be used to replace the
* placeholder with a real element.
*
* @returns {PlaceholderReturn}
*/
const Placeholder = () => {
const id = Placeholder.get_next_id_();
return {
html: `<div id="${id}"></div>`,
id,
replaceWith: (el) => {
const place = document.getElementById(id);
place.replaceWith(el);
}
};
};
const anti_collision = `94d2cb6b85a1`; // Arbitrary random string
Placeholder.next_id_ = 0;
Placeholder.get_next_id_ = () => `${anti_collision}_${Placeholder.next_id_++}`;
export default Placeholder;

58
src/util/ValueHolder.js Normal file
View File

@ -0,0 +1,58 @@
/**
* Holds an observable value.
*/
export default class ValueHolder {
constructor (initial_value) {
this.value_ = null;
this.listeners_ = [];
Object.defineProperty(this, 'value', {
set: this.set_.bind(this),
get: this.get_.bind(this),
});
if (initial_value !== undefined) {
this.set(initial_value);
}
}
static adapt (value) {
if (value instanceof ValueHolder) {
return value;
} else {
return new ValueHolder(value);
}
}
set (value) {
this.value = value;
}
get () {
return this.value;
}
sub (listener) {
this.listeners_.push(listener);
}
set_ (value) {
const old_value = this.value_;
this.value_ = value;
const more = {
holder: this,
old_value,
};
this.listeners_.forEach(listener => listener(value, more));
}
get_ () {
return this.value_;
}
map (fn) {
const holder = new ValueHolder();
this.sub((value, more) => holder.set(fn(value, more)));
return holder;
}
}