You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pages/components/header.js

357 lines
10 KiB
JavaScript

export class Header extends HTMLElement {
icons = {
menu: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-list" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
</svg>`,
dot: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
<path d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
3 years ago
</svg>`,
edit: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-pencil-square" viewBox="0 0 16 16">
3 years ago
<path d="M15.502 1.94a.5.5 0 0 1 0 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 0 1 .707 0l1.293 1.293zm-1.75 2.456-2-2L4.939 9.21a.5.5 0 0 0-.121.196l-.805 2.414a.25.25 0 0 0 .316.316l2.414-.805a.5.5 0 0 0 .196-.12l6.813-6.814z"/>
<path fill-rule="evenodd" d="M1 13.5A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-6a.5.5 0 0 0-1 0v6a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-11a.5.5 0 0 1 .5-.5H9a.5.5 0 0 0 0-1H2.5A1.5 1.5 0 0 0 1 2.5v11z"/>
</svg>`,
check: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-lg" viewBox="0 0 16 16">
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"/>
</svg>`,
add: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-plus-lg" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 2a.5.5 0 0 1 .5.5v5h5a.5.5 0 0 1 0 1h-5v5a.5.5 0 0 1-1 0v-5h-5a.5.5 0 0 1 0-1h5v-5A.5.5 0 0 1 8 2Z"/>
</svg>`,
}
textEn = {
download: 'Download',
rename: 'Move/Rename',
delete: 'Delete',
confirmDelete: f => (
`Are you sure you want to delete ${f}?`
),
cancel: 'Cancel',
alreadyExists: 'There is already a page with that name.',
createPage: 'Create Page',
}
textEs = {
download: 'Descargar',
rename: 'Mover/Renombrar',
delete: 'Borrar',
confirmDelete: f => (
`¿Desea borrar ${f}?`
),
cancel: 'Cancelar',
alreadyExists: 'Ya existe una página con ese nombre.',
createPage: 'Crear Página',
}
constructor() {
super()
this.language = navigator.language
3 years ago
this.editing = false
this.attachShadow({mode: 'open'})
this.appBar = document.createElement('div')
this.appBar.classList.add('app-bar')
this.shadowRoot.appendChild(this.appBar)
this.addButton(this.icons.menu, 'nav', () => {
this.menu.pages = this.getPages()
this.menuPanel.classList.add('open')
this.overlay.classList.add('open')
}).classList.add('left-end')
this.addButton(this.icons.add, 'add', () => {
this.addPage()
})
this.addDivider()
3 years ago
this.editBtn = this.addButton(this.editIcon, 'edit', () => {
this.dispatchEvent(new CustomEvent(
'click-edit', {bubbles: true}
))
})
this.pageButton = this.addButton(
this.icons.dot,
'page',
() => {
this.pageMenu.open(this.pageButton)
}
)
this.pageButton.classList.add('right-end')
this.addPageMenu()
this.addMenu()
this.dialogWrap = document.createElement('div')
this.shadowRoot.appendChild(this.dialogWrap)
}
connectedCallback() {
const style = document.createElement('style')
style.textContent = `
:host {
background: #111;
color: #ddd;
display: flex;
flex-direction: column;
align-items: stretch;
}
div.app-bar {
display: flex;
flex-direction: row;
padding: 0;
}
div.divider {
flex-grow: 1;
}
button {
border: none;
background: inherit;
color: inherit;
font-size: 30px;
padding: 0 8px;
}
button.left-end {
padding-left: 12px;
}
button.right-end {
padding-right: 12px;
}
div.menu {
position: fixed;
top: 0;
left: -90vw;
height: 100vh;
width: 90vw;
background-color: #fff;
transition: left .25s ease-in-out;
}
div.menu.open {
left: 0;
}
div.overlay {
position: fixed;
top: 0;
left: -100vw;
height: 100vh;
width: 100vw;
opacity: 0%;
transition: opacity .25s ease-in;
background: #777;
}
div.overlay.closing {
left: 0;
}
div.overlay.open {
left: 0;
opacity: 15%;
}
svg {
width: 20px;
height: 20px;
}
m-dialog::part(footer) {
padding-top: 15px;
}
`
this.shadowRoot.append(style)
}
encodePath(path) {
return path
}
addButton(html, cls, onClick) {
const b = document.createElement('button')
b.innerHTML = html
b.classList.add(cls)
this.appBar.appendChild(b)
if (onClick) {
b.addEventListener('click', onClick)
}
3 years ago
return b
}
addDivider() {
const d = document.createElement('div')
d.classList.add('divider')
this.appBar.appendChild(d)
}
addMenu() {
this.overlay = document.createElement('div')
this.overlay.classList.add('overlay')
this.shadowRoot.appendChild(this.overlay)
this.menuPanel = document.createElement('div')
this.menuPanel.classList.add('menu')
this.menu = document.createElement('m-nav-menu')
this.menuPanel.appendChild(this.menu)
this.shadowRoot.appendChild(this.menuPanel)
this.overlay.addEventListener('click', () => {
this.close()
})
3 years ago
this.menu.addEventListener('close-menu', () => {
this.close()
})
}
addPageMenu() {
this.pageMenu = document.createElement(
'm-menu-dropdown'
)
this.pageMenu.add(this.text.download, () => {
const text = localStorage.getItem(this.path)
const sp = this.path.split('/')
const filename = sp[sp.length - 1]
const el = document.createElement('a')
el.setAttribute(
'href',
'data:text/plain;charset=utf-8,' +
encodeURIComponent(text)
)
el.setAttribute('download', filename)
el.style.display = 'none'
this.shadowRoot.appendChild(el)
el.click()
this.shadowRoot.removeChild(el)
})
this.pageMenu.add(this.text.rename, () => {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const input = document.createElement('input')
input.value = this.path
input.style.minWidth = '300px'
dialog.bodyEl.appendChild(input)
let errorEl
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.rename, () => {
const newPath = input.value
const v = localStorage.getItem(newPath)
if (v !== null || newPath === this.path) {
if (!errorEl) {
errorEl = document.createElement('p')
errorEl.style.color = 'red'
const errText = this.text.alreadyExists
errorEl.innerText = errText
dialog.bodyEl.appendChild(errorEl)
}
return
}
localStorage.setItem(
newPath,
localStorage.getItem(this.path)
)
localStorage.removeItem(this.path)
dialog.close()
location.hash = newPath
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
})
this.pageMenu.add(this.text.delete, () => {
const dialog = document.createElement(
'm-dialog'
)
this.dialogWrap.replaceChildren(dialog)
const p = document.createElement('p')
p.innerText = this.text.confirmDelete(
JSON.stringify(this.path)
)
dialog.bodyEl.appendChild(p)
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.delete, () => {
localStorage.removeItem(this.path)
location.hash = '/'
dialog.close()
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
})
this.shadowRoot.appendChild(this.pageMenu)
}
close() {
this.overlay.classList.add('closing')
this.overlay.classList.remove('open')
this.menuPanel.classList.remove('open')
setTimeout(() => {
this.overlay.classList.remove('closing')
}, 250)
}
getPages() {
return Object.keys(localStorage).slice().sort()
}
3 years ago
set editing(value) {
this._editing = value
if (this.editBtn) {
this.editBtn.innerHTML = this.editIcon
}
}
get editing() {
return this._editing
}
get editIcon() {
return this.editing ? this.icons.check : this.icons.edit
}
addPage() {
const dialog = document.createElement('m-dialog')
this.dialogWrap.replaceChildren(dialog)
const input = document.createElement('input')
input.value = '/'
input.style.minWidth = '300px'
dialog.bodyEl.appendChild(input)
let errorEl
const bGroup = document.createElement(
'm-forms-button-group'
)
bGroup.addPrimary(this.text.createPage, () => {
const newPath = this.encodePath(input.value)
const v = localStorage.getItem(newPath)
if (v !== null || newPath === this.path) {
if (!errorEl) {
errorEl = document.createElement('p')
errorEl.style.color = 'red'
const errText = this.text.alreadyExists
errorEl.innerText = errText
dialog.bodyEl.appendChild(errorEl)
return
}
return
}
localStorage.setItem(newPath, '')
location.hash = newPath
dialog.close()
this.dispatchEvent(new CustomEvent(
'create-page', {bubbles: true}
))
})
bGroup.addCancel(this.text.cancel, () => {
dialog.close()
})
dialog.footerEl.appendChild(bGroup)
dialog.open()
}
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)
}
}