@eric-skaftason/web-components 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +339 -0
- package/README.md +2 -0
- package/index.js +0 -0
- package/package.json +19 -0
- package/src/card/card.html +7 -0
- package/src/card/card.js +102 -0
- package/src/card/card_container.html +16 -0
- package/src/card/styles/default.css +12 -0
- package/src/card/styles/flat.css +7 -0
- package/src/card/styles/universal.css +65 -0
- package/src/dropdown/dropdown.js +120 -0
- package/src/dropdown/dropdown_element.html +27 -0
- package/src/dropdown/dropdown_menu.html +58 -0
- package/src/modal/list_add_element/list_add_element.html +44 -0
- package/src/modal/list_add_element/list_add_element.js +53 -0
- package/src/modal/list_element/list_element.html +42 -0
- package/src/modal/list_element/list_element.js +28 -0
- package/src/modal/list_input/list_input.html +28 -0
- package/src/modal/list_input/list_input.js +174 -0
- package/src/modal/menu_body/menu_body.html +38 -0
- package/src/modal/menu_body/menu_body.js +22 -0
- package/src/modal/menu_button/menu_button.html +32 -0
- package/src/modal/menu_button/menu_button.js +15 -0
- package/src/modal/menu_controller/menu_controller.html +22 -0
- package/src/modal/menu_controller/menu_controller.js +38 -0
- package/src/modal/menu_controls/menu_controls.html +19 -0
- package/src/modal/menu_controls/menu_controls.js +15 -0
- package/src/modal/menu_header/menu_header.html +16 -0
- package/src/modal/menu_header/menu_header.js +15 -0
- package/src/modal/menu_text/menu_text.html +18 -0
- package/src/modal/menu_text/menu_text.js +15 -0
- package/src/modal/menu_title/menu_title.html +21 -0
- package/src/modal/menu_title/menu_title.js +15 -0
- package/src/modal/modal_menu/modal_menu.html +39 -0
- package/src/modal/modal_menu/modal_menu.js +42 -0
- package/src/modal/modal_menu.js +15 -0
- package/src/modal-lite/modal_menu.css +122 -0
- package/src/nav_bar/nav_bar.html +178 -0
- package/src/nav_bar/nav_bar.js +50 -0
- package/src/progress_bar/progress_bar.html +21 -0
- package/src/progress_bar/progress_bar.js +58 -0
- package/tests/card.html +17 -0
- package/tests/dropdown.html +0 -0
- package/tests/modal.html +0 -0
- package/tests/nav_bar.html +0 -0
- package/tests/progress_bar.html +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
class DropdownMenu extends HTMLElement {
|
|
2
|
+
constructor() {
|
|
3
|
+
super();
|
|
4
|
+
|
|
5
|
+
this.isOpen = false; // menu open/closed
|
|
6
|
+
this.subOpen = false; // sub menu open/closed
|
|
7
|
+
this.attachShadow({ mode: 'open' });
|
|
8
|
+
}
|
|
9
|
+
subMenuOpen() {
|
|
10
|
+
this.shadowRoot.querySelector('')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getMenuPos() {
|
|
14
|
+
const controller = this.shadowRoot.querySelector('.dropdown_menu_controller');
|
|
15
|
+
const openFrom = this.getAttribute('openFrom');
|
|
16
|
+
|
|
17
|
+
switch (openFrom) {
|
|
18
|
+
case 'right':
|
|
19
|
+
return {
|
|
20
|
+
x: controller.getBoundingClientRect().right,
|
|
21
|
+
y: controller.getBoundingClientRect().top
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Default / bottom
|
|
25
|
+
default:
|
|
26
|
+
return {
|
|
27
|
+
x: controller.getBoundingClientRect().left,
|
|
28
|
+
y: controller.getBoundingClientRect().bottom
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
open() {
|
|
34
|
+
const controller = this.shadowRoot.querySelector('.dropdown_menu_controller');
|
|
35
|
+
const dropdown_menu = this.shadowRoot.querySelector('.dropdown_menu');
|
|
36
|
+
dropdown_menu.style.display = 'flex';
|
|
37
|
+
|
|
38
|
+
const {x: menuX, y: menuY} = this.getMenuPos();
|
|
39
|
+
|
|
40
|
+
dropdown_menu.style.top = `${menuY}px`;
|
|
41
|
+
dropdown_menu.style.left = `${menuX}px`;
|
|
42
|
+
|
|
43
|
+
dropdown_menu.style.maxWidth = `${controller.getBoundingClientRect().width}px`;
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
// Change controller text
|
|
47
|
+
controller.innerText = `▼ ${this.desc}`;
|
|
48
|
+
|
|
49
|
+
this.isOpen = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
close() {
|
|
53
|
+
const controller = this.shadowRoot.querySelector('.dropdown_menu_controller');
|
|
54
|
+
const dropdown_menu = this.shadowRoot.querySelector('.dropdown_menu');
|
|
55
|
+
dropdown_menu.style.display = 'none';
|
|
56
|
+
controller.innerText = `▶ ${this.desc}`;
|
|
57
|
+
|
|
58
|
+
this.isOpen = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async connectedCallback() {
|
|
62
|
+
// Set shadow DOM HTML
|
|
63
|
+
const res = await fetch('/components/dropdown/dropdown_menu.html');
|
|
64
|
+
const html = await res.text();
|
|
65
|
+
|
|
66
|
+
this.shadowRoot.innerHTML = html;
|
|
67
|
+
|
|
68
|
+
// set text
|
|
69
|
+
const controller = this.shadowRoot.querySelector('.dropdown_menu_controller');
|
|
70
|
+
|
|
71
|
+
this.desc = this.getAttribute('desc') || 'dropdown menu';
|
|
72
|
+
controller.innerText = `▶ ${this.desc}`;
|
|
73
|
+
|
|
74
|
+
// Add event listeners
|
|
75
|
+
controller.addEventListener('click', (event) => {
|
|
76
|
+
this.isOpen ? this.close() : this.open();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
document.addEventListener('click', (event) => {
|
|
80
|
+
// composedPath returns array ordered from deepest element
|
|
81
|
+
// Thus, it works with shadow DOM
|
|
82
|
+
// event.target doesn't work because shadow DOM hides the nodes
|
|
83
|
+
const path = event.composedPath();
|
|
84
|
+
|
|
85
|
+
// some method checks if any element matches the condition
|
|
86
|
+
const clickedDropdownElement = path.some(node =>
|
|
87
|
+
node.tagName === 'DROPDOWN-ELEMENT'
|
|
88
|
+
); // tag name property is always in upercase
|
|
89
|
+
|
|
90
|
+
// Run close method if the path includes 'this' HTML element or if a dropdown element was clicked
|
|
91
|
+
if (!path.includes(this) || clickedDropdownElement) this.close();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
customElements.define('dropdown-menu', DropdownMenu);
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class DropdownElement extends HTMLElement {
|
|
100
|
+
constructor() {
|
|
101
|
+
super();
|
|
102
|
+
this.attachShadow({ mode: 'open' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async connectedCallback() {
|
|
106
|
+
const res = await fetch('/components/dropdown/dropdown_element.html');
|
|
107
|
+
const html = await res.text();
|
|
108
|
+
|
|
109
|
+
this.shadowRoot.innerHTML = html;
|
|
110
|
+
|
|
111
|
+
this.addEventListener('click', () => {
|
|
112
|
+
const link = this.getAttribute('link') || null;
|
|
113
|
+
if (!link) return;
|
|
114
|
+
|
|
115
|
+
document.location = link;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
customElements.define('dropdown-element', DropdownElement);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.dropdown_element {
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
align-items: center;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
|
|
10
|
+
cursor: pointer;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.dropdown_element:hover {
|
|
14
|
+
background-color: rgba(255, 255, 255, 0.95);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
::slotted(*) {
|
|
18
|
+
font-family: 'Courier New', Courier, monospace;
|
|
19
|
+
font-size: 16px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
</style>
|
|
23
|
+
<div class="dropdown_element">
|
|
24
|
+
<slot>
|
|
25
|
+
|
|
26
|
+
</slot>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.dropdown_menu_controller {
|
|
3
|
+
display: flex;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 50px;
|
|
7
|
+
|
|
8
|
+
font-family: 'Courier New', Courier, monospace;
|
|
9
|
+
font-size: 16px;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
align-items: center;
|
|
12
|
+
|
|
13
|
+
user-select: none;
|
|
14
|
+
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.dropdown_menu_controller:hover {
|
|
19
|
+
text-decoration: underline;
|
|
20
|
+
background-color: rgba(255, 255, 255, 0.95);
|
|
21
|
+
border: 1px solid #00469c;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/* Styling for the menu itself */
|
|
26
|
+
.dropdown_menu {
|
|
27
|
+
/* Initially not visible */
|
|
28
|
+
display: none;
|
|
29
|
+
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
position: fixed;
|
|
32
|
+
|
|
33
|
+
top: 0;
|
|
34
|
+
left: 0;
|
|
35
|
+
|
|
36
|
+
box-sizing: border-box;
|
|
37
|
+
width: 250px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* styling for elements within the menu */
|
|
41
|
+
::slotted(*) {
|
|
42
|
+
box-sizing: border-box;
|
|
43
|
+
user-select: none;
|
|
44
|
+
|
|
45
|
+
flex: 0 0 50px;
|
|
46
|
+
width: 100%;
|
|
47
|
+
|
|
48
|
+
background-color: rgba(255, 255, 255, 0.9);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
</style>
|
|
52
|
+
<div class="dropdown_menu_controller"></div>
|
|
53
|
+
<div class="dropdown_menu">
|
|
54
|
+
<!-- use slots so HTML elements can be passed into the custom dropdown -->
|
|
55
|
+
<slot>
|
|
56
|
+
|
|
57
|
+
</slot>
|
|
58
|
+
</div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script src="/components/modal/menu_button/menu_button.js"></script>
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
:host {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
width: 100%;
|
|
7
|
+
color: #f5f5f5;
|
|
8
|
+
|
|
9
|
+
padding-left: 10px;
|
|
10
|
+
margin-bottom: 10px;
|
|
11
|
+
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
background-color: #3a3a3a;
|
|
17
|
+
|
|
18
|
+
border: 1px solid #1a1a1a;
|
|
19
|
+
|
|
20
|
+
cursor: text;
|
|
21
|
+
transition: background-color 0.2s ease;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:host(:hover) {
|
|
25
|
+
background-color: #292929;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.text_input {
|
|
29
|
+
all: unset;
|
|
30
|
+
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
</style>
|
|
37
|
+
|
|
38
|
+
<slot hidden></slot>
|
|
39
|
+
<input type="text" class="text_input" id="text_input_add">
|
|
40
|
+
|
|
41
|
+
</input>
|
|
42
|
+
<div class="element_functions">
|
|
43
|
+
<menu-button id="add">+</menu-button>
|
|
44
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// List element
|
|
2
|
+
export class ListAddElement extends HTMLElement {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.attachShadow({ mode: 'open' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
getTextFromSlot() {
|
|
9
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
10
|
+
const text = slot.assignedNodes({flatten: true}).filter(n => n.nodeType === Node.TEXT_NODE).map(n => n.textContent.trim()).join('');
|
|
11
|
+
|
|
12
|
+
return text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async connectedCallback() {
|
|
16
|
+
const res = await fetch('/components/modal/list_add_element/list_add_element.html');
|
|
17
|
+
const html = await res.text();
|
|
18
|
+
|
|
19
|
+
this.shadowRoot.innerHTML = html;
|
|
20
|
+
|
|
21
|
+
// Get text from slot
|
|
22
|
+
const text = this.getTextFromSlot();
|
|
23
|
+
|
|
24
|
+
const text_input = this.shadowRoot.querySelector('.text_input');
|
|
25
|
+
text_input.placeholder = text;
|
|
26
|
+
|
|
27
|
+
// running obj.method in an event listener doesn't work becasue it calls method instead of obj.method
|
|
28
|
+
// method must be bound to the object manually
|
|
29
|
+
this.shadowRoot.querySelector('#add').addEventListener('click', this.addElement.bind(this));
|
|
30
|
+
this.shadowRoot.querySelector('#text_input_add').addEventListener('keydown', (event) => {
|
|
31
|
+
if (event.key === 'Enter') this.addElement();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async addElement() {
|
|
36
|
+
// Add element
|
|
37
|
+
const newElementText = this.shadowRoot.querySelector('.text_input').value;
|
|
38
|
+
const newElement = await this.shadowRoot.host.closest('list-input').appendElement(newElementText);
|
|
39
|
+
|
|
40
|
+
// if new element was created
|
|
41
|
+
if (newElement) {
|
|
42
|
+
// Scroll to bottom of body
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
// this.shadowRoot.host.closest('menu-body').scrollToEnd();
|
|
45
|
+
newElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
46
|
+
}, 50);
|
|
47
|
+
|
|
48
|
+
// clear input box
|
|
49
|
+
this.shadowRoot.querySelector('#text_input_add').value = '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
customElements.define('list-add-element', ListAddElement);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script src="/components/modal/menu_button/menu_button.js"></script>
|
|
2
|
+
|
|
3
|
+
<style>
|
|
4
|
+
:host {
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
width: 100%;
|
|
7
|
+
color: #f5f5f5;
|
|
8
|
+
|
|
9
|
+
padding-left: 10px;
|
|
10
|
+
margin-bottom: 10px;
|
|
11
|
+
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
background-color: #3a3a3a;
|
|
17
|
+
|
|
18
|
+
cursor: text;
|
|
19
|
+
transition: background-color 0.2s ease;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:host(:hover) {
|
|
23
|
+
background-color: #292929;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.text_input {
|
|
27
|
+
all: unset;
|
|
28
|
+
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: 100%;
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
</style>
|
|
35
|
+
|
|
36
|
+
<slot hidden></slot>
|
|
37
|
+
<input type="text" class="text_input">
|
|
38
|
+
|
|
39
|
+
</input>
|
|
40
|
+
<div class="element_functions">
|
|
41
|
+
<menu-button id="del">🗑</menu-button>
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// List element
|
|
2
|
+
export class ListElement extends HTMLElement {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.attachShadow({ mode: 'open' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async connectedCallback() {
|
|
9
|
+
const res = await fetch('/components/modal/list_element/list_element.html');
|
|
10
|
+
const html = await res.text();
|
|
11
|
+
|
|
12
|
+
this.shadowRoot.innerHTML = html;
|
|
13
|
+
|
|
14
|
+
// Get text
|
|
15
|
+
const slot = this.shadowRoot.querySelector('slot');
|
|
16
|
+
const text = slot.assignedNodes({flatten: true}).filter(n => n.nodeType === Node.TEXT_NODE).map(n => n.textContent.trim()).join('');
|
|
17
|
+
|
|
18
|
+
const text_input = this.shadowRoot.querySelector('.text_input');
|
|
19
|
+
text_input.value = text;
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
this.shadowRoot.querySelector('#del').addEventListener('click', async (event) => {
|
|
23
|
+
const listElement = this.shadowRoot.host;
|
|
24
|
+
await this.shadowRoot.host.closest('list-input').removeElement(listElement);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
customElements.define('list-element', ListElement);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script src="/components/modal/menu_button/menu_button.js"></script>
|
|
2
|
+
<script src="/components/modal/modal_menu/modal_menu.js"></script>
|
|
3
|
+
|
|
4
|
+
<style>
|
|
5
|
+
:host {
|
|
6
|
+
color: #f5f5f5;
|
|
7
|
+
|
|
8
|
+
padding: 10px;
|
|
9
|
+
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
justify-content: center;
|
|
13
|
+
align-items: center;
|
|
14
|
+
|
|
15
|
+
background-color: #464646;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.function_row {
|
|
19
|
+
width: 100%;
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: row;
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
|
|
25
|
+
<slot hidden></slot>
|
|
26
|
+
<!-- <div class="function_row">
|
|
27
|
+
<menu-button>Expand/shrink</menu-button>
|
|
28
|
+
</div> -->
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// List input
|
|
2
|
+
export class ListInput extends HTMLElement {
|
|
3
|
+
#elements = []; // should be an array of strings
|
|
4
|
+
#maxStringLen = 64;
|
|
5
|
+
|
|
6
|
+
#isValidStringExternal = () => {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#isValidStringExternalAsync = async () => {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.attachShadow({ mode: 'open' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
modalMessage(message) {
|
|
20
|
+
const modal_menu = this.shadowRoot.host.closest('modal-menu');
|
|
21
|
+
modal_menu.insertAdjacentHTML('afterend',
|
|
22
|
+
`
|
|
23
|
+
<modal-menu width="200px" height="200px" opacity="0" center>
|
|
24
|
+
<menu-controls>
|
|
25
|
+
<close-menu></close-menu>
|
|
26
|
+
</menu-controls>
|
|
27
|
+
<menu-body>
|
|
28
|
+
<menu-text>${message}</menu-text>
|
|
29
|
+
<close-menu>OK</close-menu>
|
|
30
|
+
</menu-body>
|
|
31
|
+
</modal-menu>
|
|
32
|
+
`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async connectedCallback() {
|
|
37
|
+
const res = await fetch('/components/modal/list_input/list_input.html');
|
|
38
|
+
const html = await res.text();
|
|
39
|
+
|
|
40
|
+
this.shadowRoot.innerHTML = html;
|
|
41
|
+
|
|
42
|
+
// initialise #elements
|
|
43
|
+
const list_elements = this.shadowRoot.host.querySelectorAll('list-element');
|
|
44
|
+
|
|
45
|
+
for (let i = list_elements.length - 1; i >= 0; i--) {
|
|
46
|
+
const list_element = list_elements[i];
|
|
47
|
+
let element_text = list_element.innerText;
|
|
48
|
+
|
|
49
|
+
// ENFORCES MAX STRING LENGTH
|
|
50
|
+
// DOES NOT ENFORCE STRING VALIDITY FOR SLOTTED STRINGS
|
|
51
|
+
if (element_text.length > this.#maxStringLen) {
|
|
52
|
+
element_text = element_text.slice(0, this.#maxStringLen);
|
|
53
|
+
list_elements[i].innerText = element_text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.#elements.unshift(element_text);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// slot is visible to this.shadowRoot but not this.shadowRoot.host
|
|
60
|
+
// slotted elements are projected to the shadow dom, and slot is removed
|
|
61
|
+
this.shadowRoot.querySelector('slot').hidden = false;
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// Connect isValidString to src
|
|
65
|
+
const verificationScriptSrc = this.shadowRoot.host.getAttribute('verify-str-src');
|
|
66
|
+
if (verificationScriptSrc) {
|
|
67
|
+
try {
|
|
68
|
+
const module = await import(verificationScriptSrc);
|
|
69
|
+
if (typeof module.isValidString !== 'function') throw new Error('Missing isValidString function.');
|
|
70
|
+
|
|
71
|
+
if (module.isValidString.constructor.name === "AsyncFunction") {
|
|
72
|
+
this.#isValidStringExternalAsync = module.isValidString;
|
|
73
|
+
} else {
|
|
74
|
+
this.#isValidStringExternal = module.isValidString;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('Module failed to load:', err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async isValidString(string) {
|
|
84
|
+
if (typeof string !== 'string') return false;
|
|
85
|
+
|
|
86
|
+
if (string.length > this.#maxStringLen) return false;
|
|
87
|
+
|
|
88
|
+
const additional_args = this.shadowRoot.host.getAttribute('verify-str-args');
|
|
89
|
+
const additional_args_parsed = additional_args !== null ? JSON.parse(additional_args): [];
|
|
90
|
+
|
|
91
|
+
const validSync = this.#isValidStringExternal(string, ...additional_args_parsed);
|
|
92
|
+
const validAsync = await this.#isValidStringExternalAsync(string, ...additional_args_parsed);
|
|
93
|
+
if (validSync !== true || validAsync !== true) {
|
|
94
|
+
|
|
95
|
+
const errorMsg = (() => {
|
|
96
|
+
if (typeof validSync === 'string') return validSync;
|
|
97
|
+
if (typeof validAsync === 'string') return validAsync;
|
|
98
|
+
return null;
|
|
99
|
+
})();
|
|
100
|
+
if (errorMsg) {
|
|
101
|
+
this.modalMessage(errorMsg);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getNewElement(text) {
|
|
111
|
+
const list_element = document.createElement('list-element');
|
|
112
|
+
list_element.innerText = text;
|
|
113
|
+
|
|
114
|
+
return list_element;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getElements() {
|
|
118
|
+
return this.#elements;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getElementIndex(element) {
|
|
122
|
+
const allElements = Array.from(this.shadowRoot.host.querySelectorAll('list-element'));
|
|
123
|
+
return allElements.indexOf(element);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async prependElement(text) {
|
|
127
|
+
if (!await this.isValidString(text)) return;
|
|
128
|
+
|
|
129
|
+
const newElement = this.getNewElement(text);
|
|
130
|
+
|
|
131
|
+
this.#elements.unshift(text);
|
|
132
|
+
this.shadowRoot.host.prepend(newElement);
|
|
133
|
+
|
|
134
|
+
return newElement;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async addElement(index, text) {
|
|
138
|
+
if (!await this.isValidString(text)) return;
|
|
139
|
+
if (index < 0 || index >= this.#elements.length) return;
|
|
140
|
+
|
|
141
|
+
if (index === this.#elements.length - 1) return this.appendElement(text);
|
|
142
|
+
if (index === 0) return this.prependElement(text);
|
|
143
|
+
|
|
144
|
+
// insert at index, remove 0, insert element
|
|
145
|
+
this.#elements.splice(index, 0, text);
|
|
146
|
+
|
|
147
|
+
const newElement = this.getNewElement(text);
|
|
148
|
+
|
|
149
|
+
this.shadowRoot.host.insertBefore(newElement, this.shadowRoot.host.querySelectorAll('list-element')[index]);
|
|
150
|
+
|
|
151
|
+
return newElement;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async appendElement(text) {
|
|
155
|
+
if (!await this.isValidString(text)) return;
|
|
156
|
+
|
|
157
|
+
this.#elements.push(text);
|
|
158
|
+
|
|
159
|
+
const newElement = this.getNewElement(text);
|
|
160
|
+
|
|
161
|
+
this.shadowRoot.host.insertBefore(newElement, this.shadowRoot.host.querySelector('list-add-element'));
|
|
162
|
+
|
|
163
|
+
return newElement;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async removeElement(element) {
|
|
167
|
+
const index = this.getElementIndex(element);
|
|
168
|
+
if (index == null || index < 0 || index >= this.#elements.length) return;
|
|
169
|
+
|
|
170
|
+
this.#elements.splice(index, 1);
|
|
171
|
+
element.remove();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
customElements.define('list-input', ListInput);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
:host {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
width: 100%;
|
|
5
|
+
|
|
6
|
+
padding: 10px;
|
|
7
|
+
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
|
|
11
|
+
border-top: 1px solid #dfdfdf;
|
|
12
|
+
/* border-bottom: 1px solid #dfdfdf; */
|
|
13
|
+
|
|
14
|
+
user-select: none;
|
|
15
|
+
|
|
16
|
+
overflow-y: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:host::-webkit-scrollbar {
|
|
20
|
+
width: 8px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
:host::-webkit-scrollbar-thumb {
|
|
24
|
+
background-color: #1880ffbd;
|
|
25
|
+
border-radius: 4px;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:host::-webkit-scrollbar-track {
|
|
29
|
+
background-color: #00295a;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
host::-webkit-scrollbar-button {
|
|
33
|
+
display: none;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
</style>
|
|
37
|
+
|
|
38
|
+
<slot></slot>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Menu body (container)
|
|
2
|
+
export class MenuBody extends HTMLElement {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.attachShadow({ mode: 'open' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async connectedCallback() {
|
|
9
|
+
const res = await fetch('/components/modal/menu_body/menu_body.html');
|
|
10
|
+
const html = await res.text();
|
|
11
|
+
|
|
12
|
+
this.shadowRoot.innerHTML = html;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
scrollToEnd() {
|
|
16
|
+
this.shadowRoot.host.scrollTo({
|
|
17
|
+
top: this.shadowRoot.host.scrollHeight,
|
|
18
|
+
behavior: 'smooth'
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
customElements.define('menu-body', MenuBody);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
:host {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
width: 100%;
|
|
5
|
+
min-height: 50px;
|
|
6
|
+
height: 50px;
|
|
7
|
+
min-width: 50px;
|
|
8
|
+
padding: 0 5px 0 5px;
|
|
9
|
+
|
|
10
|
+
color: #f5f5f5;
|
|
11
|
+
|
|
12
|
+
display: flex;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
background-color: #292929;
|
|
17
|
+
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
transition: background-color 0.2s ease, transform 0.1s ease;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:host(:hover) {
|
|
23
|
+
background-color: #1a1a1a;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host(:active) {
|
|
27
|
+
transform: scale(0.98);
|
|
28
|
+
}
|
|
29
|
+
</style>
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
<slot></slot>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Menu button
|
|
2
|
+
export class MenuButton extends HTMLElement {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.attachShadow({ mode: 'open' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async connectedCallback() {
|
|
9
|
+
const res = await fetch('/components/modal/menu_button/menu_button.html');
|
|
10
|
+
const html = await res.text();
|
|
11
|
+
|
|
12
|
+
this.shadowRoot.innerHTML = html;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
customElements.define('menu-button', MenuButton);
|