solved lab
This commit is contained in:
parent
59bac579b4
commit
e47f9004f0
|
@ -1,131 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Vier gewinnt</title>
|
||||
|
||||
<style>
|
||||
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.board {
|
||||
width: 84vw;
|
||||
margin: auto;
|
||||
outline: 1px solid black;
|
||||
}
|
||||
|
||||
.board .field {
|
||||
border: 1px solid black;
|
||||
width: 12vw;
|
||||
height: 12vw;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.board .field:first-child {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.board .field .piece {
|
||||
width: 10vw;
|
||||
height: 10vw;
|
||||
border-radius: 50%;
|
||||
margin: 1vw;
|
||||
}
|
||||
.board .field .blue {
|
||||
background-color: blue;
|
||||
}
|
||||
.board .field .red {
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script src="render-sjdon.js"></script>
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<button class="boardclearbutton" style="margin: 10px;">Neues Spiel</button>
|
||||
<p>Spieler <a id="actualPlayer"></a> ist am Zug.</p>
|
||||
<div class="board"></div>
|
||||
|
||||
<script>
|
||||
function showBoard() {
|
||||
let board = document.querySelector(".board")
|
||||
board.innerHTML = ''
|
||||
let element = new Array()
|
||||
for(row = 0; row < rows; row++) {
|
||||
for(column = 0; column < columns; column++) {
|
||||
let attributes = {"id": column.toString() + ":" + row.toString(), "class": "field"}
|
||||
let children = ["div", {"class": state[row][column] + " piece"}]
|
||||
element.append("div", attributes, children)
|
||||
}
|
||||
}
|
||||
//renderSJDON(element, board)
|
||||
}
|
||||
|
||||
function clearBoard() {
|
||||
state = Array(rows).fill('').map(el => Array(columns).fill(''))
|
||||
switchNextColor()
|
||||
showBoard();
|
||||
}
|
||||
|
||||
function switchNextColor() {
|
||||
if(state.nextColor === "red") {
|
||||
state.nextColor = "blue"
|
||||
document.getElementById("actualPlayer").innerHTML = "blau"
|
||||
} else {
|
||||
state.nextColor = "red"
|
||||
document.getElementById("actualPlayer").innerHTML = "rot"
|
||||
}
|
||||
}
|
||||
|
||||
function setField(row, column) {
|
||||
state[row][column] = state.nextColor
|
||||
switchNextColor()
|
||||
showBoard()
|
||||
}
|
||||
|
||||
function setColumn(column) {
|
||||
let row = rows - 1
|
||||
const column_number = column
|
||||
while(row >= 0){
|
||||
if(state[row][column_number] !== ''){
|
||||
row--
|
||||
} else {
|
||||
setField(row, column_number)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columns = 7
|
||||
const rows = 6
|
||||
let state = {}
|
||||
clearBoard()
|
||||
|
||||
const boardClearButton = document.querySelector(".boardclearbutton")
|
||||
boardClearButton.addEventListener("click", event => {
|
||||
clearBoard()
|
||||
})
|
||||
showBoard()
|
||||
document.querySelector(".board").addEventListener("click", event => {
|
||||
let element = event.target
|
||||
if(element.classList.contains("piece")){
|
||||
element = element.parentElement
|
||||
}
|
||||
let column = Number(element.id.split(":")[0])
|
||||
setColumn(column)
|
||||
})
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,53 @@
|
|||
|
||||
import { render } from "./lib/suiweb.js"
|
||||
|
||||
let state = {
|
||||
board: [
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ]
|
||||
],
|
||||
next: 'r'
|
||||
}
|
||||
|
||||
let stateSeq = []
|
||||
|
||||
|
||||
// Components
|
||||
//
|
||||
const App = () => [Board, {board: state.board}]
|
||||
|
||||
const Board = ({board}) => {
|
||||
let flatBoard = [].concat(...board)
|
||||
let fields = flatBoard.map((type) => [Field, {type}])
|
||||
return (
|
||||
["div", {className: "board"}, ...fields]
|
||||
)
|
||||
}
|
||||
|
||||
const Field = ({type}) => {
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
// Show board:
|
||||
// render [App]
|
||||
//
|
||||
function showBoard () {
|
||||
const app = document.querySelector(".app")
|
||||
render([App], app)
|
||||
return app
|
||||
}
|
||||
|
||||
|
||||
|
||||
// document.querySelector("button.undo").addEventListener("click", () => {
|
||||
// if (stateSeq.length > 0) {
|
||||
// state = stateSeq.pop()
|
||||
// showBoard()
|
||||
// }
|
||||
// })
|
||||
|
|
@ -25,7 +25,7 @@ function guidGenerator() {
|
|||
}
|
||||
|
||||
// Statische Dateien im Verzeichnis public
|
||||
app.use(express.static('public'))
|
||||
app.use(express.static(__dirname + "/public"))
|
||||
|
||||
// API-Key überprüfen
|
||||
//
|
||||
|
@ -113,7 +113,15 @@ app.use(function(err, req, res, next){
|
|||
app.use(function(req, res){
|
||||
res.status(404)
|
||||
res.send({ error: "not found" })
|
||||
console.log("sending 404")
|
||||
console.log(req)
|
||||
})
|
||||
|
||||
app.listen(3000)
|
||||
console.log('Express started on port 3000')
|
||||
|
||||
|
||||
const fs = require('fs')
|
||||
const path = __dirname + "/public/styles.css"
|
||||
console.log(fs.existsSync(path))
|
||||
console.log(path)
|
||||
|
|
|
@ -6,213 +6,24 @@
|
|||
<link rel="stylesheet" href="styles.css">
|
||||
<script src="connect4-winner.js"></script>
|
||||
<script src="render-sjdon.js"></script>
|
||||
<script>
|
||||
/*
|
||||
* This solution sould be considered as a proof of concept – the code
|
||||
* definitely needs some cleanup and documentation
|
||||
*/
|
||||
let datakey = ''
|
||||
let state = {
|
||||
board: [
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ]
|
||||
],
|
||||
next: 'blue'
|
||||
}
|
||||
const url = "http://localhost:3000/"
|
||||
const SERVICE = "http://localhost:3000/api/data/c4state?api-key=c4game"
|
||||
|
||||
|
||||
// Initialize game
|
||||
//
|
||||
function initGame () {
|
||||
console.log("initializing game")
|
||||
fetch(url + "api/data/" + datakey + "?api-key=c4game", {
|
||||
method: 'POST',
|
||||
headers: { 'Content-type': 'application/json' },
|
||||
body: JSON.stringify(state)
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
datakey = data.id
|
||||
})
|
||||
let board = showBoard()
|
||||
attachEventHandler(board)
|
||||
}
|
||||
|
||||
|
||||
// Show board
|
||||
//
|
||||
function showBoard () {
|
||||
console.log("showing board")
|
||||
let actualPlayerText = document.getElementById("actualPlayer")
|
||||
if(actualPlayerText){
|
||||
actualPlayerText.innerText = state.next
|
||||
}
|
||||
|
||||
|
||||
let board = document.querySelector(".board")
|
||||
|
||||
// first remove all fields
|
||||
while (board.firstChild) { board.removeChild(board.firstChild) }
|
||||
|
||||
// your implementation
|
||||
let element = []
|
||||
for(row = 0; row < state.board.length; row++) {
|
||||
for(column = 0; column < state.board[0].length; column++) {
|
||||
let attributes = {"id": column.toString() + ":" + row.toString(), "class": "field"}
|
||||
let children = ["div", {"class": state.board[row][column] + " piece"}]
|
||||
element.push(["div", attributes, children])
|
||||
}
|
||||
}
|
||||
renderSJDON(element, board)
|
||||
return board
|
||||
}
|
||||
|
||||
|
||||
// Helper function for DOM manipulation
|
||||
//
|
||||
function elt (type, attrs, ...children) {
|
||||
let node = document.createElement(type)
|
||||
for (a in attrs) {
|
||||
node.setAttribute(a, attrs[a])
|
||||
}
|
||||
for (let child of children) {
|
||||
if (typeof child != "string") node.appendChild(child)
|
||||
else node.appendChild(document.createTextNode(child))
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
|
||||
// Attach event handler to board
|
||||
//
|
||||
function attachEventHandler (board) {
|
||||
console.log("attaching Eventhandler")
|
||||
// your implementation
|
||||
board.addEventListener("click", event => {
|
||||
let field = event.target
|
||||
if(field.classList.contains("piece")){
|
||||
field = field.parentNode
|
||||
}
|
||||
let column = Number(field.id.split(":")[0])
|
||||
console.log("calculated column")
|
||||
setColumn(column)
|
||||
|
||||
showBoard()
|
||||
})
|
||||
}
|
||||
|
||||
function setColumn(column) {
|
||||
console.log("setting Column")
|
||||
let row = state.board.length - 1
|
||||
const column_number = column
|
||||
while(row >= 0){
|
||||
if(state.board[row][column_number] !== ''){
|
||||
row--
|
||||
} else {
|
||||
if(state.next === ''){
|
||||
console.log("not setting Field becuase there is a winner.")
|
||||
} else {
|
||||
setField(row, column_number)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setField(row, column) {
|
||||
state.board[row][column] = state.next
|
||||
showBoard()
|
||||
if(connect4Winner(state.next, state.board)){
|
||||
document.getElementById("state-text").innerText = "Player " + state.next + " won!"
|
||||
state.next = ''
|
||||
} else {
|
||||
switchNextColor()
|
||||
}
|
||||
}
|
||||
|
||||
function switchNextColor() {
|
||||
if(state.next === "red") {
|
||||
state.next = "blue"
|
||||
} else {
|
||||
state.next = "red"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get current state from server and re-draw board
|
||||
//
|
||||
function loadStateFromServer () {
|
||||
// ...
|
||||
// your implementation
|
||||
// ...
|
||||
console.log("loading State from Server")
|
||||
fetch(url + "api/data/" + datakey + "?api-key=c4game", {
|
||||
method: 'GET',
|
||||
headers: { 'Content-type': 'application/json' }
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
state = data
|
||||
showBoard()
|
||||
})
|
||||
}
|
||||
|
||||
// Put current state to server
|
||||
//
|
||||
function saveStateToServer () {
|
||||
console.log("saving state To Server")
|
||||
fetch(url + "api/data/" + datakey + "?api-key=c4game", {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-type': 'application/json' },
|
||||
body: JSON.stringify(state)
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
state = data
|
||||
showBoard()
|
||||
})
|
||||
}
|
||||
|
||||
function loadStateToLocalStorage() {
|
||||
console.log("loading State from LocalStorage")
|
||||
if(localStorage.getItem("state")){
|
||||
state = JSON.parse(localStorage.getItem("state"))
|
||||
} else {
|
||||
console.log("no saved State in Localstorage")
|
||||
}
|
||||
showBoard()
|
||||
}
|
||||
|
||||
function saveStateFromLocalStorage() {
|
||||
console.log("saving State to LocalStorage")
|
||||
localStorage.setItem("state", JSON.stringify(state))
|
||||
}
|
||||
|
||||
function clearLocalStorage() {
|
||||
console.log("clearing localStorage")
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="board"></div>
|
||||
<div class="app"></div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="loadStateFromServer()">Load from Server</button>
|
||||
<button onclick="saveStateToServer()">Save to Server</button>
|
||||
<button onclick="loadStateToLocalStorage()">Load from LocalStorage</button>
|
||||
<button onclick="saveStateFromLocalStorage()">Save to LocalStorage</button>
|
||||
<button onclick="clearLocalStorage()">Clear LocalStorage</button>
|
||||
<button id="loadFromServer">Load from Server</button>
|
||||
<button id="saveToServer">Save to Server</button>
|
||||
<button id="loadFromStorage">Load from LocalStorage</button>
|
||||
<button id="saveToStorage">Save to LocalStorage</button>
|
||||
<button id="clearStorage">Clear LocalStorage</button>
|
||||
<button id ="undo">Undo</button>
|
||||
<p id="state-text"><a id="actualPlayer"></a>'s turn</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script type="module">
|
||||
import { initGame } from "./gamelogic.js"
|
||||
initGame()
|
||||
</script>
|
||||
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
|
||||
/*
|
||||
* This solution sould be considered as a proof of concept – the code
|
||||
* definitely needs some cleanup and documentation
|
||||
*/
|
||||
import { render } from "./lib/suiweb.js"
|
||||
|
||||
let datakey = ''
|
||||
let state = {
|
||||
board: [
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ],
|
||||
[ '', '', '', '', '', '', '' ]
|
||||
],
|
||||
next: 'blue'
|
||||
}
|
||||
let stateSeq = []
|
||||
|
||||
const App = () => [Board, {board: state.board}]
|
||||
|
||||
const Board = ({board}) => {
|
||||
let flatBoard = [].concat(...board)
|
||||
let fields = flatBoard.map((type) => [Field, {type}])
|
||||
return (
|
||||
["div", {className: "board"}, ...fields]
|
||||
)
|
||||
}
|
||||
|
||||
const Field = ({type}) => {
|
||||
return (
|
||||
["div", {className: "field"},
|
||||
["div", {className: ("piece " + type)}]]
|
||||
)
|
||||
}
|
||||
|
||||
const url = "http://localhost:3000/"
|
||||
const SERVICE = "http://localhost:3000/api/data/c4state?api-key=c4game"
|
||||
|
||||
|
||||
// Initialize game
|
||||
//
|
||||
function initGame () {
|
||||
console.log("initializing game")
|
||||
fetch(url + "api/data/" + datakey + "?api-key=c4game", {
|
||||
method: 'POST',
|
||||
headers: { 'Content-type': 'application/json' },
|
||||
body: JSON.stringify(state)
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
datakey = data.id
|
||||
})
|
||||
let board = showBoard()
|
||||
attachEventHandler(board)
|
||||
|
||||
document.getElementById("loadFromServer").addEventListener("click", event => {loadStateFromServer() })
|
||||
document.getElementById("saveToServer").addEventListener("click", event => {saveStateToServer() })
|
||||
document.getElementById("loadFromStorage").addEventListener("click", event => {loadStateFromLocalStorage() })
|
||||
document.getElementById("saveToStorage").addEventListener("click", event => {saveStateToLocalStorage() })
|
||||
document.getElementById("clearStorage").addEventListener("click", event => {clearLocalStorage() })
|
||||
document.getElementById("undo").addEventListener("click", event => {undo() })
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Show board
|
||||
|
||||
function showBoard () {
|
||||
|
||||
console.log("showing board")
|
||||
let actualPlayerText = document.getElementById("actualPlayer")
|
||||
if(actualPlayerText){
|
||||
actualPlayerText.innerText = state.next
|
||||
}
|
||||
const app = document.querySelector(".app")
|
||||
render([App], app)
|
||||
return app
|
||||
}
|
||||
|
||||
|
||||
// Attach event handler to board
|
||||
//
|
||||
function attachEventHandler (board) {
|
||||
console.log("attaching Eventhandler")
|
||||
// your implementation
|
||||
board.addEventListener("click", event => {
|
||||
let field = event.target
|
||||
if(field.classList.contains("piece")){
|
||||
field = field.parentNode
|
||||
}
|
||||
let column = Array.prototype.indexOf.call(field.parentNode.children, field) % state.board[1].length
|
||||
console.log("calculated column", column)
|
||||
setColumn(column)
|
||||
|
||||
showBoard()
|
||||
})
|
||||
}
|
||||
|
||||
function setColumn(column) {
|
||||
console.log("setting Column")
|
||||
let row = state.board.length - 1
|
||||
const column_number = column
|
||||
while(row >= 0){
|
||||
if(state.board[row][column_number] !== ''){
|
||||
row--
|
||||
} else {
|
||||
if(state.next === ''){
|
||||
console.log("not setting Field becuase there is a winner.")
|
||||
} else {
|
||||
setField(row, column_number)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setField(row, column) {
|
||||
stateSeq.push(state)
|
||||
state = setInObj(state, "board", setInList(state.board, row, setInList(state.board[row], column, state.next)))
|
||||
//state.board[row][column] = state.next
|
||||
showBoard()
|
||||
if(connect4Winner(state.next, state.board)){
|
||||
document.getElementById("state-text").innerText = "Player " + state.next + " won!"
|
||||
state = setInObj(state, "next", "")
|
||||
//state.next = ''
|
||||
} else {
|
||||
switchNextColor()
|
||||
}
|
||||
}
|
||||
|
||||
function switchNextColor() {
|
||||
if(state.next === "red") {
|
||||
state = setInObj(state, "next", "blue")
|
||||
//state.next = "blue"
|
||||
} else {
|
||||
state = setInObj(state, "next", "red")
|
||||
//state.next = "red"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Get current state from server and re-draw board
|
||||
//
|
||||
function loadStateFromServer () {
|
||||
// ...
|
||||
// your implementation
|
||||
// ...
|
||||
console.log("loading State from Server")
|
||||
fetch(url + "api/data/" + datakey + "?api-key=c4game", {
|
||||
method: 'GET',
|
||||
headers: { 'Content-type': 'application/json' }
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
state = data
|
||||
stateSeq = []
|
||||
showBoard()
|
||||
})
|
||||
}
|
||||
|
||||
// Put current state to server
|
||||
//
|
||||
function saveStateToServer () {
|
||||
console.log("saving state To Server")
|
||||
fetch(url + "api/data/" + datakey + "?api-key=c4game", {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-type': 'application/json' },
|
||||
body: JSON.stringify(state)
|
||||
}).then(response => response.json())
|
||||
.then(data => {
|
||||
state = data
|
||||
showBoard()
|
||||
})
|
||||
}
|
||||
|
||||
function loadStateFromLocalStorage() {
|
||||
console.log("loading State from LocalStorage")
|
||||
if(localStorage.getItem("state")){
|
||||
state = JSON.parse(localStorage.getItem("state"))
|
||||
stateSeq = []
|
||||
} else {
|
||||
console.log("no saved State in Localstorage")
|
||||
}
|
||||
showBoard()
|
||||
}
|
||||
|
||||
function saveStateToLocalStorage() {
|
||||
console.log("saving State to LocalStorage")
|
||||
localStorage.setItem("state", JSON.stringify(state))
|
||||
}
|
||||
|
||||
function clearLocalStorage() {
|
||||
console.log("clearing localStorage")
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if(stateSeq.length > 0) {
|
||||
console.log("undo")
|
||||
state = stateSeq.pop()
|
||||
showBoard()
|
||||
}
|
||||
}
|
||||
|
||||
function setInList(lst, idx, val) {
|
||||
const newList = []
|
||||
lst.forEach(element => {
|
||||
newList.push(element)
|
||||
});
|
||||
newList[idx] = val
|
||||
return newList
|
||||
}
|
||||
|
||||
function setInObj(obj, attr, val) {
|
||||
const newObject = {}
|
||||
Object.keys(obj).forEach(key => {
|
||||
newObject[key] = obj[key]
|
||||
})
|
||||
newObject[attr] = val
|
||||
return newObject
|
||||
}
|
||||
|
||||
export {initGame}
|
|
@ -0,0 +1,336 @@
|
|||
/**
|
||||
* SuiWeb
|
||||
* Simple User Interface Tool for Web Exercises
|
||||
*
|
||||
* @author bkrt
|
||||
* @version 0.3.4
|
||||
* @date 11.12.2021
|
||||
*
|
||||
* 0.3.4 - only null or undefined qualify as uninitialized state
|
||||
* 0.3.3 - parseSJDON rewritten to use createElement
|
||||
* - flatten children array for props.children in JSX
|
||||
* 0.3.2 - save and restore of active element improved
|
||||
* 0.3.1 - parseSJDON rewritten
|
||||
* 0.3.0 - style property
|
||||
* 0.2.3 - ES6 modules
|
||||
* 0.2.2 - component state and hooks
|
||||
* 0.2.1 - parse SJDON rewritten, function components
|
||||
* 0.2.0 - render single SJDON structures to DOM
|
||||
*
|
||||
* Based on ideas and code from
|
||||
* Rodrigo Pombo: Build your own React
|
||||
* https://pomb.us/build-your-own-react/
|
||||
*
|
||||
* Thanks to Rodrigo Pombo for a great tutorial and for sharing the
|
||||
* code of the Didact library. Didact is a much more sophisticated
|
||||
* re-implementtation of React's basics than the simple SuiWeb.
|
||||
*/
|
||||
|
||||
/* =====================================================================
|
||||
* SJDON - Conversion
|
||||
* =====================================================================
|
||||
*/
|
||||
|
||||
// parseSJDON: convert SJDON to createElement calls
|
||||
//
|
||||
// note: this function can also help to use React with SJDON syntax
|
||||
//
|
||||
// to simplify calls add something like this to your components:
|
||||
// let s = (data) => reactFromSJDON(data, React.createElement)
|
||||
//
|
||||
function parseSJDON ([type, ...rest], create=createElement) {
|
||||
const isObj = (obj) => typeof(obj)==='object' && !Array.isArray(obj)
|
||||
const children = rest.filter(item => !isObj(item))
|
||||
const repr = create(type,
|
||||
Object.assign({}, ...rest.filter(isObj)),
|
||||
...children.map(ch => Array.isArray(ch) ? parseSJDON(ch, create) : ch)
|
||||
)
|
||||
repr.sjdon = children
|
||||
return repr
|
||||
}
|
||||
|
||||
|
||||
// create an element representation
|
||||
//
|
||||
function createElement(type, props, ...children) {
|
||||
return {
|
||||
type,
|
||||
props: {
|
||||
...props,
|
||||
children: children.flat().map(child =>
|
||||
typeof child === "object"
|
||||
? child
|
||||
: createTextElement(child)
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// create a text element representation
|
||||
//
|
||||
function createTextElement(text) {
|
||||
return {
|
||||
type: "TEXT_ELEMENT",
|
||||
props: {
|
||||
nodeValue: text,
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/* =====================================================================
|
||||
* Render node tree to DOM
|
||||
* =====================================================================
|
||||
*/
|
||||
|
||||
// global context
|
||||
let contextStore = createStore();
|
||||
|
||||
|
||||
// remove children and render new subtree
|
||||
//
|
||||
function render(element, container) {
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.lastChild);
|
||||
}
|
||||
renderInit(element, container, 0);
|
||||
}
|
||||
|
||||
|
||||
// render subtree
|
||||
// and call effects after rendering
|
||||
//
|
||||
function renderInit(element, container, n, childIndex) {
|
||||
contextStore("effects", []);
|
||||
|
||||
// save focus and cursor position of active element
|
||||
let [focusSelector, position] = getFocusInput();
|
||||
|
||||
// ** render the element **
|
||||
renderElem(element, container, n, childIndex);
|
||||
|
||||
// restore focus and cursor position of active element
|
||||
setFocusInput(focusSelector, position);
|
||||
|
||||
// run effects
|
||||
contextStore("effects").forEach(fun => fun());
|
||||
contextStore("effects", []);
|
||||
}
|
||||
|
||||
|
||||
// render an element
|
||||
// - if it is in SJDON form: parse first
|
||||
// - render function or host component
|
||||
//
|
||||
function renderElem(element, container, n, childIndex) {
|
||||
if (Array.isArray(element)) {
|
||||
element = parseSJDON(element);
|
||||
}
|
||||
|
||||
if (element.type instanceof Function) {
|
||||
updateFunctionComponent(element, container, n, childIndex);
|
||||
} else {
|
||||
updateHostComponent(element, container, childIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// function component
|
||||
// - run function to get child node
|
||||
// - render child node
|
||||
//
|
||||
function updateFunctionComponent(element, container, n, childIndex) {
|
||||
// save re-render function to context
|
||||
contextStore("re-render", () => renderInit(element, container, n, n));
|
||||
let children = element.sjdon ?? element.props.children;
|
||||
let node = element.type({...element.props, children});
|
||||
renderElem(node, container, n, childIndex);
|
||||
}
|
||||
|
||||
|
||||
// host component
|
||||
// - create dom node
|
||||
// - assign properties
|
||||
// - render child nodes
|
||||
// - add host to dom
|
||||
//
|
||||
function updateHostComponent(element, container, childIndex) {
|
||||
|
||||
// create DOM node
|
||||
const dom =
|
||||
element.type == "TEXT_ELEMENT"
|
||||
? document.createTextNode("")
|
||||
: document.createElement(element.type)
|
||||
|
||||
// assign the element props
|
||||
const isProperty = key => key !== "children"
|
||||
Object.keys(element.props)
|
||||
.filter(isProperty)
|
||||
.forEach(name => {
|
||||
if (name=="style") {
|
||||
updateStyleAttribute(dom, element.props.style);
|
||||
} else {
|
||||
dom[name] = element.props[name];
|
||||
}
|
||||
})
|
||||
|
||||
// render children
|
||||
element.props.children.forEach((child, index) => {
|
||||
renderElem(child, dom, index);
|
||||
});
|
||||
|
||||
if (typeof(childIndex) == 'number') {
|
||||
// re-render: replace node
|
||||
container.replaceChild(dom, container.childNodes[childIndex]);
|
||||
} else {
|
||||
// add node to container
|
||||
container.appendChild(dom);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// update style attribute, value can be:
|
||||
// - a CSS string: set to style attribute
|
||||
// - an object: merged to style attribute
|
||||
// - an array of objects: merged to style attribute
|
||||
//
|
||||
function updateStyleAttribute(dom, styles) {
|
||||
if (typeof(styles)=="string") {
|
||||
dom.style = styles;
|
||||
} else if (Array.isArray(styles)) {
|
||||
Object.assign(dom.style, ...styles);
|
||||
} else if (typeof(styles)=="object") {
|
||||
Object.assign(dom.style, styles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
* Handling state
|
||||
* =====================================================================
|
||||
*/
|
||||
|
||||
// element state
|
||||
let stateHooks = createStore();
|
||||
|
||||
|
||||
// state hook
|
||||
// - access state via id and key
|
||||
// - return state and update function
|
||||
//
|
||||
function useState(id, key, init) {
|
||||
let idKey = "id:" + id + "-key:" + key;
|
||||
|
||||
// prepare render function
|
||||
let renderFunc = contextStore("re-render");
|
||||
|
||||
// define function to update state
|
||||
function updateValue(updateFun, rerender=true) {
|
||||
stateHooks(idKey, updateFun(stateHooks(idKey)));
|
||||
if (rerender) renderFunc();
|
||||
}
|
||||
|
||||
// new state: set initial value
|
||||
if ([undefined, null].includes(stateHooks(idKey))) {
|
||||
stateHooks(idKey, init);
|
||||
}
|
||||
|
||||
return [stateHooks(idKey), updateValue];
|
||||
}
|
||||
|
||||
|
||||
// effect hook
|
||||
// add function to effects array
|
||||
//
|
||||
function useEffect(fun) {
|
||||
contextStore("effects", [...contextStore("effects"), fun]);
|
||||
}
|
||||
|
||||
|
||||
// create a key-value-store
|
||||
// return accessor function
|
||||
//
|
||||
function createStore() {
|
||||
let data = {};
|
||||
function access(key, ...value) {
|
||||
if (value.length === 0) {
|
||||
return data[key];
|
||||
} else {
|
||||
data[key] = value[0];
|
||||
return value[0];
|
||||
}
|
||||
}
|
||||
return access;
|
||||
}
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
* Get and set focus and position in certain elements
|
||||
* Note: this is a quick&dirty solution that probably fails when
|
||||
* elements are added or removed
|
||||
* =====================================================================
|
||||
*/
|
||||
|
||||
// create a CSS selector for a given node
|
||||
//
|
||||
function getSelectorOf(node) {
|
||||
let selector = ""
|
||||
while (node != document.body) {
|
||||
if (selector != "") selector = ">" + selector
|
||||
|
||||
if (node.id) {
|
||||
selector = "#"+node.id + selector
|
||||
break
|
||||
}
|
||||
|
||||
let index = Array.from(node.parentNode.children)
|
||||
.filter(item=>item.tagName==node.tagName)
|
||||
.findIndex(item=>item==node)
|
||||
|
||||
selector = node.tagName + ":nth-of-type(" + (index+1) + ")" + selector
|
||||
node = node.parentNode
|
||||
}
|
||||
return selector
|
||||
}
|
||||
|
||||
// find a selector for the element that has focus and the cursor
|
||||
// position in the element
|
||||
//
|
||||
function getFocusInput() {
|
||||
const active = document.activeElement;
|
||||
let sel = active ? getSelectorOf(active) : undefined
|
||||
let position = active ? active.selectionStart : undefined;
|
||||
return [sel, position];
|
||||
}
|
||||
|
||||
// set focus to an element in a list of elements matching a
|
||||
// selector and position cursor in the element
|
||||
//
|
||||
function setFocusInput(selector, position) {
|
||||
if (selector && typeof(selector) == 'string') {
|
||||
console.log("Sel:"+selector)
|
||||
let el = document.querySelector(selector);
|
||||
if (el) el.focus();
|
||||
if (el && "selectionStart" in el
|
||||
&& "selectionEnd" in el
|
||||
&& position !== undefined) {
|
||||
el.selectionStart = position;
|
||||
el.selectionEnd = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
* Module export
|
||||
* =====================================================================
|
||||
*/
|
||||
|
||||
export { render, createElement, useState, useEffect };
|
||||
|
||||
|
||||
/* =====================================================================
|
||||
* EOF
|
||||
* =====================================================================
|
||||
*/
|
Loading…
Reference in New Issue