gun/examples/gelf/terminal.html
2024-10-19 08:36:54 -07:00

1006 lines
24 KiB
HTML

<!DOCTYPE html>
<style>html, body, textarea { width: 100%; height: 100%; padding: 0; margin: 0; }</style>
<shared-terminal></shared-terminal>
<script type="module">
import gelf from './gelf.js'
const initial = {
startX: null,
startY: null,
x: null,
y: null,
invertX: false,
invertY: false,
isMouseDown: false,
suggestions: [],
trayZ: 3,
focusedTray: null,
}
const $ = gelf.call({ seed: self.location.href }, 'shared-terminal', initial)
function engine(target) {
const canvas = target.closest($.link).querySelector('canvas')
const rectangle = canvas.getBoundingClientRect()
return { canvas, rectangle }
}
function render(target) {
const container = target.querySelector('.trays')
return function runtime(tray) {
const data = $.learn()[tray]
if(!data) return
const {
suggestions,
suggestIndex,
focusedTray
} = $.learn()
const {
maximized,
minimized,
grabbed,
width,
height,
x,
y,
z,
url,
focused
} = data
const start = Math.max(suggestIndex - 5, 0)
const end = Math.min(suggestIndex + 5, suggestions.length - 1)
let node = container.querySelector(`[data-id="${tray}"]`)
if(!node) {
node = document.createElement('div')
node.classList.add('tray');
node.dataset.id = tray
node.innerHTML = `
<button class="tray-wake" data-tray="${tray}"></button>
<div class="tray-title-bar" data-tray="${tray}" data-url="${url}">
<button class="tray-action tray-toggle" data-tray="${tray}">
<sl-icon name="${minimized ? 'zoom-in' : 'zoom-out' }"></sl-icon>
</button>
<div class="grabber"></div>
<button class="tray-action tray-maxer" data-tray="${tray}">
<sl-icon name="${maximized ? 'fullscreen-exit' : 'fullscreen' }"></sl-icon>
</button>
<form class="search minimizable" method="get">
<div class="input-grid">
<input value="${url}" autocomplete="off" name="browser-${self.crypto.randomUUID()}" class="browser" data-tray="${tray}"/>
<button class="tray-action tray-sync" data-tray="${tray}" tab-index="1" type="submit">
<sl-icon name="telephone"></sl-icon>
</button>
</div>
</form>
<div class="grabber minimizable"></div>
<button class="tray-action tray-close" data-tray="${tray}">
<sl-icon name="x-lg"></sl-icon>
</button>
</div>
<div class="suggestions" data-tray="${tray}"></div>
<div class="tray-body">
<iframe src="${url}" title="${url}"></iframe>
</div>
<div class="resize-actions">
<button aria-label="resize left" data-direction="sw" class="tray-resize minimizable resize-left" data-tray="${tray}">
</button>
<button aria-label="resize right" data-direction="se" class="tray-resize minimizable resize-right" data-tray="${tray}">
</button>
</div>
`
container.appendChild(node)
}
node.style = `--width: ${width}px; --height: ${height}px;--x: ${x}px; --y: ${y}px; --z: ${z}; transform: translate(var(--x), var(--y)); z-index: var(--z);`
if(focusedTray === tray) {
node.dataset.focused = true
} else {
node.dataset.focused = false
}
const fullScreenIcon = node.querySelector('.tray-maxer sl-icon')
if(maximized) {
node.setAttribute('class', 'tray maximized')
fullScreenIcon.name = 'fullscreen-exit'
} else {
node.setAttribute('class', 'tray')
fullScreenIcon.name = 'fullscreen'
}
const miniScreenIcon = node.querySelector('.tray-toggle sl-icon')
if(minimized) {
node.classList.add('minimized')
miniScreenIcon.name = 'zoom-in'
} else {
miniScreenIcon.name = 'zoom-out'
}
if(node.dataset.url !== url) {
node.dataset.url = url
node.querySelector('iframe').src = url
node.querySelector('.browser').value = url
}
node.dataset.grabbed = grabbed
node.persist = true
}
}
$.when('submit', '.search', (event) => {
event.preventDefault()
const { tray } = event.target.dataset
const { buffer } = $.learn()[tray]
const url = buffer.indexOf('://') ? buffer : '/app/giggle-search?query=' + buffer
setState(tray, { url, focused: false })
})
$.when('focus', '.browser', event => {
const { tray } = event.target.dataset
setState(tray, { focused: true })
})
$.when('blur', '.browser', event => {
setTimeout(() => {
const { tray } = event.target.dataset
setState(tray, { focused: false })
}, 250)
})
$.draw((target) => {
if(target.innerHTML) return
const src = target.getAttribute('src')
if(src) {
requestIdleCallback(() => {
const tray = self.crypto.randomUUID()
const { trayZ } = $.learn()
setState(tray, {
width: 300,
height: 150,
x: 0,
y: 0,
z: trayZ+1,
url: src,
maximized: true,
focused: true
})
$.teach({
focusedTray: tray
}, (state, payload) => {
const newState = {...state}
newState.trays ||= []
newState.trays.push(payload)
newState.focusedTray = payload
newState.trayZ += 1
return newState
})
})
}
return `
<div class="trays"></div>
<div class="cursor"></div>
<canvas></canvas>
`
}, { beforeUpdate, afterUpdate })
function beforeUpdate(target) {
saveCursor(target) // first things first
{ // save suggestion box scroll top
const list = target.querySelector('.suggestion-box')
if(list) {
target.dataset.scrollpos = list.scrollTop
}
}
{
const { startX, startY, x, y, invertX, invertY } = $.learn()
const background = target.getAttribute('background')
const color = target.getAttribute('color')
target.style = `--start-x: ${startX}px; --start-y: ${startY}px; --x: ${Math.abs(x)}px; --y: ${Math.abs(y)}px; --transform: translate(${invertX ? '-100' : '0' }%, ${invertY ? '-100' : '0'}%); ${background ? `--background: ${background};` : ``} ${color ? `--color: ${color}` : ``}`
}
{
[...(target.querySelectorAll('.tray') || [])].map(x => {
x.persist = false
})
}
{
const { isMouseDown } = $.learn()
target.dataset.mouse = isMouseDown
}
}
function afterUpdate(target) {
{
const { grabbing } = $.learn()
const trays = target.querySelector('.trays')
trays.dataset.grabbing = !!grabbing
}
{
const { resizing } = $.learn()
const trays = target.querySelector('.trays')
trays.dataset.resizing = !!resizing
}
{ // scroll suggestions
const list = target.querySelector('.suggestion-box')
if(list) {
list.scrollTop = target.dataset.scrollpos
}
}
{ // scroll item into view
const activeItem = target.querySelector('.suggestion-box .active')
if(activeItem) {
activeItem.scrollIntoView({block: "nearest", inline: "nearest"})
}
}
{
const { isMouseDown } = $.learn()
const cursor = target.querySelector('.cursor')
cursor.style = `${isMouseDown ? 'display: block;' : 'display: none;'};`
}
{
const { trays } = $.learn()
if(trays) {
trays.map(render(target))
}
}
{
const { trays } = $.learn()
if(target.matches('.inline') && trays) {
const somethingMaxed = trays.some(x => {
const tray = $.learn()[x]
return tray.maximized
})
if(somethingMaxed) {
target.classList.remove('inline');
target.classList.add('online')
}
}
}
{
[...(target.querySelectorAll('.tray') || [])].filter(x => {
return !x.persist
}).map(x => x.remove())
}
replaceCursor(target) // first things first
}
function preventDefault(e) { e.preventDefault() }
$.when('contextmenu', '.tray-title-bar', preventDefault)
$.when('pointerdown', '.tray-title-bar', grab)
$.when('pointerdown', '.tray-resize', resize)
$.when('pointermove', 'canvas', drag)
$.when('pointermove', '.tray-title-bar', drag)
$.when('pointermove', '.tray-resize', drag)
$.when('dblclick', '.tray-title-bar', toggleMax)
$.when('click', '.tray-maxer', toggleMax)
$.when('pointerup', 'canvas', ungrab)
$.when('pointerup', 'canvas', unresize)
$.when('pointerup', '.tray-title-bar', ungrab)
$.when('pointerup', '.tray-resize', unresize)
$.when('click', '.tray-close', closeTray)
$.when('click', '.tray-sync', syncTray)
$.when('click', '.tray-toggle', toggleMin)
function syncTray(event) {
event.preventDefault()
const { tray } = event.target.dataset
let { buffer, url } = $.learn()[tray]
buffer ||= url
url = buffer.startsWith('/')
? buffer
: buffer.indexOf('://')
? buffer
: '/app/giggle-search?query=' + buffer
event.target.closest('.tray').querySelector('iframe').src = url
setState(tray, { url, focused: false, minimized: false })
}
function toggleMax(event) {
const tray = event.target.closest('.tray').dataset.id
const { maximized } = $.learn()[tray]
maximized ? restoreMax(tray) : maximize(tray)
}
function maximize(tray) {
$.teach(tray, (state, payload) => {
const newState = {...state}
newState[payload].maximized = true
newState[payload].minimized = false
return newState
})
}
// restore a pane
function restoreMax(tray) {
$.teach(tray, (state, payload) => {
const newState = {...state}
newState[payload].maximized = false
return newState
})
}
function toggleMin(event) {
const tray = event.target.closest('.tray').dataset.id
const { minimized } = $.learn()[tray]
minimized ? restoreMin(tray) : minimize(tray)
}
function minimize(tray) {
$.teach(tray, (state, payload) => {
const newState = {...state}
newState[payload].minimized = true
newState[payload].maximized = false
return newState
})
}
// restore a pane
function restoreMin(tray) {
$.teach(tray, (state, payload) => {
const newState = {...state}
newState[payload].minimized = false
return newState
})
}
function closeTray(event) {
const { tray } = event.target.dataset
$.teach(tray, (state, payload) => {
const newState = {...state}
newState.trays ||= []
const trayIndex = newState.trays.indexOf(payload)
if(trayIndex >= 0) {
newState.trays.splice(trayIndex, 1)
delete newState[payload]
}
return newState
})
}
// grab a pane
let grabTimeout
let grabOffsetX, grabOffsetY
function grab(event) {
event.preventDefault()
const { offsetX, offsetY } = event
const { tray } = event.target.dataset
const { trayZ } = $.learn()
const newZ = trayZ + 1
$.teach({ trayZ: newZ, focusedTray: tray })
setState(tray, { z: newZ })
grabTimeout = setTimeout(() => {
setState(tray, { grabbed: true })
$.teach({ grabbing: tray })
grabOffsetX = offsetX
grabOffsetY = offsetY
}, 250)
}
// drag a pane
let lastX, lastY;
function drag(event) {
let { target, clientX, clientY } = event
const { grabbing, resizing } = $.learn()
const tray = grabbing || resizing
if(!tray) return
const { grabbed, resize, x, y, width, height } = $.learn()[tray]
const panX = getComputedStyle(event.target).getPropertyValue("--pan-x") || 0;
const panY = getComputedStyle(event.target).getPropertyValue("--pan-y") || 0;
if (lastX !== undefined && lastY !== undefined) {
const movementX = clientX - lastX;
const movementY = clientY - lastY;
// Use movementX and movementY here
if(grabbed) {
setState(tray, {
x: x + movementX,
y: y + movementY
})
}
if(resize) {
if(resize === 'sw') {
setState(tray, {
x: x + movementX,
height: height + movementY,
width: width - movementX
})
}
if(resize === 'se') {
setState(tray, {
height: height + movementY,
width: width + movementX
})
}
}
} else {
if(grabbed) {
setState(tray, {
x: clientX - grabOffsetX - parseInt(panX, 10),
y: clientY - grabOffsetY - parseInt(panY, 10)
})
}
}
lastX = clientX;
lastY = clientY;
}
// release a pane
function ungrab(event) {
clearTimeout(grabTimeout)
const tray = $.learn().grabbing
if(!tray) return
setState(tray, { grabbed: false })
$.teach({ grabbing: null })
lastX = undefined;
lastY = undefined;
grabOffsetX = undefined
grabOffsetY = undefined
}
// grab a pane
function resize(event) {
event.preventDefault()
const { offsetX, offsetY } = event
const { tray } = event.target.dataset
const { trayZ } = $.learn()
const newZ = trayZ + 1
$.teach({ resizing: tray, trayZ: newZ, focusedTray: tray })
setState(tray, { resize: event.target.dataset.direction, z: newZ })
grabOffsetX = offsetX
grabOffsetY = offsetY
}
function unresize({ target }) {
const tray = $.learn().resizing
if(!tray) return
setState(tray, { resize: null })
$.teach({ resizing: null })
lastX = undefined;
lastY = undefined;
grabOffsetX = undefined
grabOffsetY = undefined
}
function setState(tray, payload) {
$.teach(payload, function merge(state) {
return {
...state,
[tray]: {
...state[tray],
...payload
}
}
})
}
$.style(`
& {
position: relative;
touch-action: none;
overflow: hidden;
}
&.cinema {
--draw-term-bg: #54796d;
--draw-term-fg: #54796d;
}
& .resize-right,
& .resize-left {
position: absolute;
bottom: -.5rem;
width: 1rem;
height: 1rem;
border: 1px solid rgb(0,0,0,.15);
background: transparent;
border-radius: 100%;
cursor: resize;
}
& .resize-right::before,
& .resize-left::before {
content: '';
display: block;
width: 1rem;
height: 1rem;
border: 1px solid rgb(255,255,255,.15);
border-radius: 100%;
bottom: -.5rem;
position: absolute;
}
& .resize-left::before,
& .resize-left {
left: -.5rem;
cursor: sw-resize;
}
& .resize-right::before,
& .resize-right {
right: -.5rem;
cursor: se-resize;
}
&.inline {
display: inline-block;
height: 2.2rem;
}
&.inline .tray:not(.minimized) {
transform: translate(0, 0) !important;
position: absolute;
inset: 0;
width: 100% !important;
height: 100% !important;
}
&.online {
display: block;
position: absolute;
inset: 0;
z-index: 100;
}
& .grabber {
display: block;
width: 100%;
padding: 0 .25rem;
}
& .grabber::before {
content: '';
box-shadow:
0px .2rem 0 .5px var(--red),
0px .7rem 0 .5px var(--orange),
0px 1.2rem 0 .5px var(--yellow);
display: block;
margin: 0;
opacity: .4;
transform: opacity 100ms ease-in-out;
}
&,
& canvas {
display: block;
width: 100%;
height: 100%;
}
& canvas {
background: var(--draw-term-bg, var(--background, lemonchiffon));
touch-action: manipulation;
user-select: none; /* supported by Chrome and Opera */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
}
& .cursor {
position: absolute;
left: var(--start-x);
top: var(--start-y);
width: var(--x);
height: var(--y);
background: var(--draw-term-bg, var(--color, dodgerblue));
transform: var(--transform);
pointer-events: none;
z-index: 9001;
opacity: .65;
}
& .trays[data-resizing="true"],
& .trays[data-grabbing="true"] {
pointer-events: none !important;
}
& .tray {
pointer-events: none;
filter: grayscale(1);
}
&[data-mouse="true"] .tray {
pointer-events: none !important;
}
& .tray[data-focused="true"] {
pointer-events: all;
filter: grayscale(0);
}
& .tray-wake {
background: none;
position: absolute;
inset: 0;
background: 0;
border: 0;
padding: 0;
pointer-events: all;
}
& .tray-wake:hover,
& .tray-wake:focus {
background: rgba(0,0,0,.85);
outline: 2px solid mediumseagreen;
outline-offset: 2px;
}
& .tray[data-focused="true"] .tray-wake {
display: none;
}
& [data-resizing="true"] .tray[data-focused="true"],
& [data-grabbing="true"] .tray[data-focused="true"],
&[data-mouse="true"] .tray[data-focused="true"],
& [data-resizing="true"] .tray-wake,
& [data-grabbing="true"] .tray-wake,
&[data-mouse="true"] .tray-wake {
pointer-events: none !important;
}
& .grabber {
pointer-events: none;
}
& [data-grabbed="true"] {
transform: scale(1.1);
outline: 2px solid var(--green);
outline-offset: 2px;
}
& [data-grabbed="true"] .grabber::before {
box-shadow:
0px .2rem 0 .5px var(--purple),
0px .7rem 0 .5px var(--blue),
0px 1.2rem 0 .5px var(--green);
}
& .trays[data-mousedown="true"] {
pointer-events: none;
}
& .trays:empty::before {
content: 'Draw a rectangle that is not tiny."
position: absolute;
inset: 0;
margin: auto;
}
& .tray {
position: absolute;
width: var(--width, 160px);
height: var(--height, 90px);
background: linear-gradient(25deg, rgba(0,0,0,.65), rgba(0,0,0,.85));
padding: 2px;
display: grid;
grid-template-rows: auto 1px 1fr;
max-width: 100vw;
max-height: 100vh;
}
& .tray iframe {
position: absolute;
inset: 0;
}
& .tray-title-bar {
padding: 5px 4px;
font-size: 1rem;
line-height: 1;
color: white;
position: relative;
display: grid;
grid-template-columns: auto 2rem auto minmax(100px, 1.618fr) 2rem auto;
gap: 5px;
touch-action: manipulation;
user-select: none; /* supported by Chrome and Opera */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
overflow-x: auto;
}
& .tray-title-bar input {
border: none;
border-radius: 0;
background: transparent;
color: rgba(255,255,255,.65);
width: 100%;
padding: 0 4px 0;
height: 100%;
}
& .tray-title-bar input:focus {
color: rgba(255,255,255,.85);
column-span: 2;
}
& .tray-body {
background: white;
color: black;
height: 100%;
position: relative;
}
& .tray-resize {
pointer-events: all;
}
&:not(.infinite) .tray.maximized {
transform: translate(0, 0) !important;
}
& .tray.maximized {
position: absolute;
inset: 0;
width: 100% !important;
height: 100% !important;
}
& .tray.minimized:not(.maximized) {
width: auto;
height: auto;
grid-template-rows: auto 1px 0;
border-radius: 1rem;
}
& .tray.minimized:not(.maximized) .tray-title-bar {
grid-template-columns: auto 2rem auto auto;
}
& .tray.minimized:not(.maximized) .minimizable {
display: none;
}
& .tray [type="color"] {
border: none;
width: 100%;
height: 100%;
padding: 0;
}
& .tray-action {
background: transparent;
border: none;
border-radius: 0;
color: white;
padding: 3px 5px;
opacity: .65;
transition: opacity 100ms;
border-radius: 100%;
display: grid;
place-items: center;
}
& .tray-action:hover,
& .tray-action:focus {
opacity: 1;
}
& .tray-toggle {
}
& .tray-close {
margin-left: auto;
}
& .input-grid {
display: grid;
grid-template-columns: 1fr auto;
text-align: left;
}
& *:focus {
outline: 3px solid var(--underline-color, mediumseagreen);
}
& .suggestions .auto-item {
background: linear-gradient(rgba(0,0,0,.25), rgba(0,0,0,.5));
background-color: var(--button-color, dodgerblue);
border: none;
color: white;
transition: background-color 200ms ease-in-out;
padding: 1rem;
display: block;
}
& .suggestions:not(:empty) {
display: block;
position: relative;
background: var(--green, mediumseagreen);
text-align: left;
}
& .suggestion-box:empty {
pointer-events: none;
}
& .suggestion-box {
position: absolute;
inset: 0;
height: 300px;
overflow: auto;
z-index: 10;
max-height: calc(100vh - 3rem);
display: flex;
flex-direction: column;
}
& .suggestion-box .auto-item {
background: var(--button-color, dodgerblue);
background-image: linear-gradient(rgba(0,0,0,.85), rgba(0,0,0,.85));
color: var(--button-color, dodgerblue);
transition: all 100ms ease-in-out;
padding: .5rem;
width: 100%;
text-align: left;
max-width: 100%;
}
& .suggestion-box .auto-item:focus,
& .suggestion-box .auto-item:hover {
background-color: var(--button-color, dodgerblue);
background-image: linear-gradient(rgba(0,0,0,.35), rgba(0,0,0,.35));
color: white;
}
& .suggestion-box .auto-item.active {
color: white;
background-image: linear-gradient(rgba(0,0,0,.35), rgba(0,0,0,.35));
background-color: var(--button-color, dodgerblue);
}
& .hyper-name {
display: flex;
overflow: hidden;
}
& .file-name {
white-space: nowrap;
padding-right: 2rem;
}
& .hyper-sentence {
white-space: nowrap;
margin-left: auto;
overflow: hidden;
color: rgba(255,255,255,.65);
}
`)
$.when('pointerdown', 'canvas', start)
function start(e) {
const { grabbing } = $.learn()
if(grabbing) return
const { canvas, rectangle } = engine(e.target)
const context = canvas.getContext('2d')
let startX, startY, x, y;
if (e.touches && e.touches[0] && typeof e.touches[0]["force"] !== "undefined") {
startX = e.touches[0].clientX - rectangle.left
startY = e.touches[0].clientY - rectangle.top
} else {
startX = e.clientX - rectangle.left
startY = e.clientY -rectangle.top
}
x = 0
y = 0
$.teach({ startX, startY, isMouseDown: true, x, y })
}
$.when('pointermove', 'canvas', move)
function move (e) {
e.preventDefault()
const { startX, isMouseDown, startY, grabbing } = $.learn()
if(grabbing) return
const { canvas, rectangle } = engine(e.target)
const context = canvas.getContext('2d')
if (!isMouseDown) return
let x, y
if (e.touches && e.touches[0] && typeof e.touches[0]["force"] !== "undefined") {
x = e.touches[0].clientX - startX - rectangle.left
y = e.touches[0].clientY - startY - rectangle.top
} else {
x = e.clientX - startX - rectangle.left
y = e.clientY - startY - rectangle.top
}
$.teach({ x, y, invertX: x < 0, invertY: y < 0 })
}
$.when('click', '.tray-wake', wake)
function wake (e) {
const { trayZ } = $.learn()
const newZ = trayZ + 1
const { tray } = event.target.dataset
$.teach({ trayZ: newZ, focusedTray: tray })
setState(tray, { z: newZ })
}
$.when('pointerup', 'canvas', end)
function end (e) {
const { grabbing } = $.learn()
if(grabbing) return
const { focusedTray, startX, x, y, invertX, invertY, startY } = $.learn()
const { canvas, rectangle } = engine(e.target)
const context = canvas.getContext('2d')
if(Math.abs(x) > 50 && Math.abs(y) > 50) {
const tray = self.crypto.randomUUID()
$.teach(tray, (state, payload) => {
const newState = {...state}
newState.trays ||= []
newState.trays.push(payload)
newState.trayZ += 1
newState.focusedTray = tray
newState[payload] = {
width: Math.abs(x),
height: Math.abs(y),
x: invertX ? startX + x : startX,
y: invertY ? startY + y : startY,
z: newState.trayZ,
url: `/app/hyper-script?src=/private/${$.link}/${new Date().toISOString()}/${self.crypto.randomUUID()}.saga`
}
return newState
})
}
$.teach({ startX: null, startY: null, isMouseDown: false, x: 0, y: 0 })
};
const tags = ['TEXTAREA', 'INPUT']
let sel = []
function saveCursor(target) {
if(target.contains(document.activeElement)) {
target.dataset.paused = document.activeElement.name
if(tags.includes(document.activeElement.tagName)) {
const textarea = document.activeElement
sel = [textarea.selectionStart, textarea.selectionEnd];
}
} else {
target.dataset.paused = null
}
}
function replaceCursor(target) {
const paused = target.querySelector(`[name="${target.dataset.paused}"]`)
if(paused) {
paused.focus()
if(tags.includes(paused.tagName)) {
paused.selectionStart = sel[0];
paused.selectionEnd = sel[1];
}
}
}
</script>