4 changed files with 266 additions and 244 deletions
@ -0,0 +1,88 @@ |
|||||
|
export class FileGroup extends HTMLElement { |
||||
|
textEn = { |
||||
|
addFile: 'Add File', |
||||
|
} |
||||
|
|
||||
|
textEs = { |
||||
|
addFile: 'Añadir archivo', |
||||
|
} |
||||
|
|
||||
|
constructor() { |
||||
|
super() |
||||
|
this.language = navigator.language |
||||
|
this.attachShadow({mode: 'open'}) |
||||
|
this.headerEl = document.createElement('div') |
||||
|
this.headerEl.classList.add('header') |
||||
|
this.contentEl = document.createElement('div') |
||||
|
this.contentEl.classList.add('content') |
||||
|
this.shadowRoot.appendChild(this.headerEl) |
||||
|
this.shadowRoot.appendChild(this.contentEl) |
||||
|
const bGroup = document.createElement( |
||||
|
'm-forms-button-group' |
||||
|
) |
||||
|
bGroup.addPrimary(this.text.addFile, () => { |
||||
|
this.addFile() |
||||
|
const btn = bGroup.primary |
||||
|
if (btn.scrollIntoViewIfNeeded) { |
||||
|
btn.scrollIntoViewIfNeeded() |
||||
|
} else { |
||||
|
btn.scrollIntoView() |
||||
|
} |
||||
|
}) |
||||
|
this.shadowRoot.appendChild(bGroup) |
||||
|
} |
||||
|
|
||||
|
connectedCallback() { |
||||
|
const style = document.createElement('style') |
||||
|
style.textContent = ` |
||||
|
:host { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
div.header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
} |
||||
|
div.files { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
flex-grow: 1; |
||||
|
overflow-y: auto; |
||||
|
} |
||||
|
` |
||||
|
this.shadowRoot.appendChild(style) |
||||
|
if (this.contentEl.childNodes.length === 0) { |
||||
|
this.addFile() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
addFile({name, data} = {}) { |
||||
|
const el = document.createElement('m-editor-file-view') |
||||
|
if (name !== undefined) { |
||||
|
el.name = name |
||||
|
} |
||||
|
if (data !== undefined) { |
||||
|
el.data = data |
||||
|
} |
||||
|
this.contentEl.appendChild(el) |
||||
|
return el |
||||
|
} |
||||
|
|
||||
|
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 files() { |
||||
|
return [...this.contentEl.children] |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,120 @@ |
|||||
|
export class FileView extends HTMLElement { |
||||
|
icons = { |
||||
|
menu: ` |
||||
|
<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"/> |
||||
|
</svg> |
||||
|
`,
|
||||
|
} |
||||
|
|
||||
|
textEn = { |
||||
|
delete: 'Delete', |
||||
|
} |
||||
|
|
||||
|
textEs = { |
||||
|
delete: 'Borrar', |
||||
|
} |
||||
|
|
||||
|
constructor() { |
||||
|
super() |
||||
|
this.language = navigator.language |
||||
|
this.attachShadow({mode: 'open'}) |
||||
|
this.headerEl = document.createElement('div') |
||||
|
this.headerEl.classList.add('header') |
||||
|
this.contentEl = document.createElement('div') |
||||
|
this.contentEl.classList.add('content') |
||||
|
this.shadowRoot.appendChild(this.headerEl) |
||||
|
this.shadowRoot.appendChild(this.contentEl) |
||||
|
this.nameEl = document.createElement('input') |
||||
|
this.nameEl.classList.add('name') |
||||
|
this.headerEl.appendChild(this.nameEl) |
||||
|
this.editEl = document.createElement('m-editor-text-edit') |
||||
|
this.contentEl.appendChild(this.editEl) |
||||
|
this.menuBtn = document.createElement('button') |
||||
|
this.menuBtn.innerHTML = this.icons.menu |
||||
|
this.menuBtn.addEventListener('click', () => { |
||||
|
this.menu.open(this.menuBtn) |
||||
|
}) |
||||
|
this.headerEl.appendChild(this.menuBtn) |
||||
|
this.menu = document.createElement( |
||||
|
'm-menu-dropdown' |
||||
|
) |
||||
|
this.menu.add('Borrar', () => { |
||||
|
this.remove() |
||||
|
}) |
||||
|
this.shadowRoot.appendChild(this.menu) |
||||
|
} |
||||
|
|
||||
|
connectedCallback() { |
||||
|
const style = document.createElement('style') |
||||
|
style.textContent = ` |
||||
|
:host { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
div.header { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: stretch; |
||||
|
background-color: #111; |
||||
|
color: #ddd; |
||||
|
padding: 3px 0; |
||||
|
} |
||||
|
div.header > * { |
||||
|
background: inherit; |
||||
|
color: inherit; |
||||
|
border: none; |
||||
|
} |
||||
|
.name { |
||||
|
flex-grow: 1; |
||||
|
padding: 0 5px; |
||||
|
font: inherit; |
||||
|
font-family: monospace; |
||||
|
outline: none; |
||||
|
} |
||||
|
div.header button svg { |
||||
|
margin-bottom: -3px; |
||||
|
} |
||||
|
div.content { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
} |
||||
|
svg { |
||||
|
height: 20px; |
||||
|
width: 20px; |
||||
|
} |
||||
|
` |
||||
|
this.shadowRoot.appendChild(style) |
||||
|
} |
||||
|
|
||||
|
set name(name) { |
||||
|
this.nameEl.value = name |
||||
|
} |
||||
|
|
||||
|
get name() { |
||||
|
return this.nameEl.value |
||||
|
} |
||||
|
|
||||
|
set data(data) { |
||||
|
this.editEl.value = data |
||||
|
} |
||||
|
|
||||
|
get data() { |
||||
|
return this.editEl.value |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
} |
||||
|
} |
||||
@ -1,244 +0,0 @@ |
|||||
<style> |
|
||||
html { |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
*, *:before, *:after { |
|
||||
box-sizing: inherit; |
|
||||
} |
|
||||
body { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: stretch; |
|
||||
} |
|
||||
</style> |
|
||||
<script type="module"> |
|
||||
class FileGroup extends HTMLElement { |
|
||||
constructor() { |
|
||||
super() |
|
||||
this.attachShadow({mode: 'open'}) |
|
||||
this.headerEl = document.createElement('div') |
|
||||
this.headerEl.classList.add('header') |
|
||||
this.contentEl = document.createElement('div') |
|
||||
this.contentEl.classList.add('content') |
|
||||
this.shadowRoot.appendChild(this.headerEl) |
|
||||
this.shadowRoot.appendChild(this.contentEl) |
|
||||
} |
|
||||
|
|
||||
connectedCallback() { |
|
||||
const style = document.createElement('style') |
|
||||
style.textContent = ` |
|
||||
:host { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: stretch; |
|
||||
} |
|
||||
div.header { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
} |
|
||||
div.files { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
flex-grow: 1; |
|
||||
overflow-y: auto; |
|
||||
} |
|
||||
` |
|
||||
this.shadowRoot.appendChild(style) |
|
||||
if (this.contentEl.childNodes.length === 0) { |
|
||||
this.addFile() |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
addFile({name, data} = {}) { |
|
||||
const el = document.createElement('m-file-view') |
|
||||
if (name !== undefined) { |
|
||||
el.name = name |
|
||||
} |
|
||||
if (data !== undefined) { |
|
||||
el.data = data |
|
||||
} |
|
||||
this.contentEl.appendChild(el) |
|
||||
return el |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class FileView extends HTMLElement { |
|
||||
icons = { |
|
||||
menu: ` |
|
||||
<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"/> |
|
||||
</svg> |
|
||||
`, |
|
||||
} |
|
||||
|
|
||||
text = { |
|
||||
delete: {en: 'Delete', es: 'Borrar'}, |
|
||||
} |
|
||||
|
|
||||
constructor() { |
|
||||
super() |
|
||||
this.attachShadow({mode: 'open'}) |
|
||||
this.headerEl = document.createElement('div') |
|
||||
this.headerEl.classList.add('header') |
|
||||
this.contentEl = document.createElement('div') |
|
||||
this.contentEl.classList.add('content') |
|
||||
this.shadowRoot.appendChild(this.headerEl) |
|
||||
this.shadowRoot.appendChild(this.contentEl) |
|
||||
this.nameEl = document.createElement('input') |
|
||||
this.nameEl.classList.add('name') |
|
||||
this.headerEl.appendChild(this.nameEl) |
|
||||
this.editEl = document.createElement('m-text-edit') |
|
||||
this.contentEl.appendChild(this.editEl) |
|
||||
this.menuEl = document.createElement('button') |
|
||||
this.menuEl.innerHTML = this.icons.menu |
|
||||
this.headerEl.appendChild(this.menuEl) |
|
||||
} |
|
||||
|
|
||||
connectedCallback() { |
|
||||
const style = document.createElement('style') |
|
||||
style.textContent = ` |
|
||||
:host { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: stretch; |
|
||||
} |
|
||||
div.header { |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
align-items: stretch; |
|
||||
background-color: #111; |
|
||||
color: #ddd; |
|
||||
padding: 3px 0; |
|
||||
} |
|
||||
div.header > * { |
|
||||
background: inherit; |
|
||||
color: inherit; |
|
||||
border: none; |
|
||||
} |
|
||||
.name { |
|
||||
flex-grow: 1; |
|
||||
padding: 0 5px; |
|
||||
font: inherit; |
|
||||
font-family: monospace; |
|
||||
outline: none; |
|
||||
} |
|
||||
div.header button svg { |
|
||||
margin-bottom: -3px; |
|
||||
} |
|
||||
div.content { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: stretch; |
|
||||
} |
|
||||
svg { |
|
||||
height: 20px; |
|
||||
width: 20px; |
|
||||
} |
|
||||
` |
|
||||
this.shadowRoot.appendChild(style) |
|
||||
} |
|
||||
|
|
||||
set name(name) { |
|
||||
this.nameEl.value = name |
|
||||
} |
|
||||
|
|
||||
get name() { |
|
||||
return this.nameEl.value |
|
||||
} |
|
||||
|
|
||||
set data(data) { |
|
||||
this.editEl.value = data |
|
||||
} |
|
||||
|
|
||||
get data() { |
|
||||
return this.editEl.value |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
class TextEdit extends HTMLElement { |
|
||||
constructor() { |
|
||||
super() |
|
||||
this.attachShadow({mode: 'open'}) |
|
||||
const stackEl = document.createElement('div') |
|
||||
stackEl.classList.add('stack') |
|
||||
this.textEl = document.createElement('textarea') |
|
||||
this.textEl.classList.add('text') |
|
||||
this.textEl.rows = 1 |
|
||||
stackEl.appendChild(this.textEl) |
|
||||
this.shadowRoot.appendChild(stackEl) |
|
||||
this.textEl.addEventListener('input', () => { |
|
||||
stackEl.dataset.copy = this.textEl.value |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
connectedCallback() { |
|
||||
// https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/ |
|
||||
const style = document.createElement('style') |
|
||||
style.textContent = ` |
|
||||
:host { |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
align-items: stretch; |
|
||||
margin: 5px 0; |
|
||||
} |
|
||||
div.stack { |
|
||||
display: grid; |
|
||||
} |
|
||||
div.stack::after { |
|
||||
content: attr(data-copy) " "; |
|
||||
visibility: hidden; |
|
||||
overflow: hidden; |
|
||||
} |
|
||||
div.stack::after, div.stack > textarea { |
|
||||
white-space: pre-wrap; |
|
||||
border: 1px solid #888; |
|
||||
padding: 3px; |
|
||||
font: inherit; |
|
||||
font-family: monospace; |
|
||||
grid-area: 1 / 1 / 2 / 2; |
|
||||
min-height: 1em; |
|
||||
border-radius: 2px; |
|
||||
resize: none; |
|
||||
} |
|
||||
` |
|
||||
this.shadowRoot.appendChild(style) |
|
||||
} |
|
||||
|
|
||||
set value(value) { |
|
||||
this.textEl.value = value |
|
||||
} |
|
||||
|
|
||||
get value() { |
|
||||
return this.textEl.value |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
customElements.define('m-file-group', FileGroup) |
|
||||
customElements.define('m-file-view', FileView) |
|
||||
customElements.define('m-text-edit', TextEdit) |
|
||||
|
|
||||
|
|
||||
const fileGroup = document.createElement('m-file-group') |
|
||||
fileGroup.addFile({ |
|
||||
name: 'file-group.js', |
|
||||
data: 'code here' |
|
||||
}) |
|
||||
fileGroup.addFile({ |
|
||||
name: 'file-view.js', |
|
||||
data: 'code here' |
|
||||
}) |
|
||||
|
|
||||
document.body.appendChild(fileGroup) |
|
||||
</script> |
|
||||
|
|
||||
<!-- |
|
||||
|
|
||||
Each file has a header with the filename, an edit icon, and a menu icon. When editing or creating a new one, the filename is a text field. When not editing, tapping on the filename expands and collapses it. |
|
||||
|
|
||||
Tapping on a line selects it and shows icons, tapping on another line selects down to that line. |
|
||||
|
|
||||
At the top is a dropdown for selecting the file and a button for. |
|
||||
|
|
||||
After all the files is a plus skeleton icon bar. |
|
||||
|
|
||||
--> |
|
||||
@ -0,0 +1,58 @@ |
|||||
|
export class TextEdit extends HTMLElement { |
||||
|
constructor() { |
||||
|
super() |
||||
|
this.attachShadow({mode: 'open'}) |
||||
|
this.stackEl = document.createElement('div') |
||||
|
this.stackEl.classList.add('stack') |
||||
|
this.textEl = document.createElement('textarea') |
||||
|
this.textEl.classList.add('text') |
||||
|
this.textEl.rows = 1 |
||||
|
this.stackEl.appendChild(this.textEl) |
||||
|
this.shadowRoot.appendChild(this.stackEl) |
||||
|
this.textEl.addEventListener('input', () => { |
||||
|
this.stackEl.dataset.copy = this.textEl.value |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
connectedCallback() { |
||||
|
// https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
|
||||
|
const style = document.createElement('style') |
||||
|
style.textContent = ` |
||||
|
:host { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
align-items: stretch; |
||||
|
margin: 5px 0; |
||||
|
} |
||||
|
div.stack { |
||||
|
display: grid; |
||||
|
} |
||||
|
div.stack::after { |
||||
|
content: attr(data-copy) " "; |
||||
|
visibility: hidden; |
||||
|
overflow: hidden; |
||||
|
} |
||||
|
div.stack::after, div.stack > textarea { |
||||
|
white-space: pre-wrap; |
||||
|
border: 1px solid #888; |
||||
|
padding: 3px; |
||||
|
font: inherit; |
||||
|
font-family: monospace; |
||||
|
grid-area: 1 / 1 / 2 / 2; |
||||
|
min-height: 1em; |
||||
|
border-radius: 2px; |
||||
|
resize: none; |
||||
|
} |
||||
|
` |
||||
|
this.shadowRoot.appendChild(style) |
||||
|
} |
||||
|
|
||||
|
set value(value) { |
||||
|
this.textEl.value = value |
||||
|
this.stackEl.dataset.copy = this.textEl.value |
||||
|
} |
||||
|
|
||||
|
get value() { |
||||
|
return this.textEl.value |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue