Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html>
- <head>
- <title>Sudoku</title>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=yes">
- <script>
- document.addEventListener('DOMContentLoaded', event => {
- var rootNode = document.querySelector('#sudoku');
- // refference-node not found
- if (!rootNode)
- throw Error('No container with ID sudoku found');
- // create sudoku
- for (var r = 0; r < 9; r++)
- for (var c = 0; c < 9; c++)
- rootNode.appendChild(
- construct('div', {
- class: 'input variable',
- row: r,
- column: c,
- contenteditable: true,
- tabindex: 0
- })
- );
- // create sudoku instance
- var sudoku = window.sudoku = new Sudoku(rootNode);
- // add event listeners to difficulty-buttons
- var ddropdown_item = document.querySelectorAll('.difficulty.dropdown .item');
- if (ddropdown_item && ddropdown_item.length > 0)
- Array.from(ddropdown_item).map((n, i) => n.addEventListener('click', () => initLoad(i + 1)));
- // add event listener to check-buttons
- var btn = {
- check: document.querySelector('.check'),
- checkAndShow: document.querySelector('.checkAndShow'),
- export: document.querySelector('.export'),
- import: document.querySelector('.import'),
- notes: document.querySelector('.notes'),
- autoRemoveNotes: document.querySelector('.autoRemoveNotes'),
- setCandidate: document.querySelector('.setCandidate'),
- setFieldCandidates: document.querySelector('.setFieldCandidates'),
- setAllCandidates: document.querySelector('.setAllCandidates'),
- create: document.querySelector('.create'),
- csExport: document.querySelector('.csExport'),
- csImport: document.querySelector('.csImport'),
- csApply: document.querySelector('.csApply')
- };
- if (btn.check)
- btn.check.addEventListener('click', event => sudoku.notify(sudoku.valid() + 0 ? 'Bis jetzt ist alles richtig!' : 'Nein, irgendwo ist ein Fehler :/'));
- if (btn.checkAndShow)
- btn.checkAndShow.addEventListener('click', sudoku.showError);
- if (btn.export)
- btn.export.addEventListener('click', event => {
- var name = prompt('Dateiname:', 'Sudoku.json');
- if (name === null)
- return;
- sudoku.export(name);
- });
- if (btn.import)
- btn.import.addEventListener('click', event => sudoku.import());
- if (btn.notes)
- btn.notes.addEventListener('click', event => {
- var an = !!btn.notes.innerText.match(/an/);
- sudoku.writeNotes = an ? true : false;
- btn.notes.innerText = btn.notes.innerText.replace(/an|aus/, an ? 'aus' : 'an');
- });
- if (btn.autoRemoveNotes)
- btn.autoRemoveNotes.addEventListener('click', event => {
- var auto = !!btn.autoRemoveNotes.innerText.match(/automatisch/);
- sudoku.autoRemoveNotes = auto ? true : false;
- btn.autoRemoveNotes.innerText = btn.autoRemoveNotes.innerText.replace(/automatisch|manuell/, auto ? 'manuell' : 'automatisch');
- });
- if (btn.setCandidate)
- btn.setCandidate.addEventListener('click', event => sudoku.setCandidate(Number(prompt('Die Notizen für eine Zahl werden eingetragen.\nWelche Zahl soll es sein?'))));
- if (btn.setFieldCandidates)
- btn.setFieldCandidates.addEventListener('click', event => {
- var btnText = btn.setFieldCandidates.innerHTML;
- var selectNode = event => {
- sudoku.resetNotes(event.target.closest('.input'));
- sudoku.setFieldCandidates(event.target.closest('.input'));
- sudoku.rootNode.removeEventListener('click', selectNode);
- btn.setFieldCandidates.innerText = btnText;
- };
- btn.setFieldCandidates.innerText = 'Klicke in ein Feld';
- sudoku.rootNode.addEventListener('click', selectNode);
- setTimeout(() => {
- sudoku.rootNode.removeEventListener('click', selectNode);
- btn.setFieldCandidates.innerText = btnText;
- }, 5000);
- });
- if (btn.setAllCandidates)
- btn.setAllCandidates.addEventListener('click', event =>
- sudoku.children
- .filter(n => !n.className.match(/fixed/) && !Number(n.innerText))
- .map(n => {
- sudoku.resetNotes(n);
- sudoku.setFieldCandidates(n);
- })
- );
- if (btn.create)
- btn.create.addEventListener('click', event => {
- var btnText = btn.create.innerHTML;
- if (btnText.match(/starten/)) {
- btn.create.innerHTML = btnText.replace(/starten/, 'stoppen');
- sudoku.editor.start();
- } else {
- btn.create.innerHTML = btnText.replace(/stoppen/, 'starten');
- sudoku.editor.stop();
- }
- })
- if (btn.csApply)
- btn.csApply.addEventListener('click', event => {
- setColors(getColors());
- });
- if (btn.csImport)
- btn.csImport.addEventListener('click', event => {
- var f = document.head.appendChild(construct('input', {
- type: 'file',
- accept: 'text/json,application/json,text/plain'
- }));
- f.click();
- f.onchange = event => {
- if (f.files.length === 1) {
- var reader = new FileReader;
- reader.readAsText(f.files[0]);
- reader.onloadend = event => onreaderend(event, reader);
- }
- };
- function onreaderend(event, reader) {
- if (reader.result)
- try {
- setColors(colors = JSON.parse(reader.result));
- Array.from(document.querySelectorAll('.themeWindow td input[type="color"]'))
- .map(n => {
- if (Object.keys(colors).includes(n.name))
- n.value = colors[Object.keys(colors)[Object.keys(colors).indexOf(n.name)]];
- })
- }catch(err){
- alert('Keine gültige JSON-Datei ausgewählt!');
- }
- }
- });
- if (btn.csExport)
- btn.csExport.addEventListener('click', event => {
- if ((name = prompt('Dateiname:', 'Farben.json')) === null)
- return;
- document.head.appendChild(
- construct('a', {
- download: name,
- href: 'data:application/json;base64,' + btoa(JSON.stringify(getColors(), null, '\t'))
- })
- )
- .click();
- })
- // add listeners to highlight current units
- Array.from(rootNode.children).map(n => {
- n.addEventListener('blur', event => sudoku.unhighlight(n));
- n.addEventListener('focus', event => sudoku.highlight(n));
- n.addEventListener('click', event => sudoku.highlight(n));
- n.addEventListener('input', event => sudoku.highlight(n));
- n.addEventListener('keydown', sudoku.insertNumber);
- })
- });
- function getColors() {
- var inp = Array.from(document.querySelectorAll('.themeWindow td input[type="color"]')),
- colors = {};
- inp.map(n => colors[n.getAttribute('name')] = n.value);
- return colors;
- }
- function setColors(colors) {
- if (us = document.querySelector('#userStyles'))
- us.innerHTML = `body\n{ background: ${colors.bodyBG} }\nmain\n{\n\t\t/* Hauptteil Hintergrundfarbe */\n\tbackground: ${colors.mainBG};\n\t/* Hauptteil Schriftfarbe */\n\tcolor: ${colors.mainFG};\n\t/* Hauptteil Schatten\n\t\t* X-Verschiebung\n\t\t* Y-Verschiebung\n\t\t* Blur-Breite\n\t\t* Schatten-Breite\n\t\t* Schatten-Farbe\n\t*/\n\tbox-shadow: 0 0 3px 1px ${colors.mainShadowColor};\n}\n.input,\n.input.variable,\n.input.fixed,\n.input:nth-child(3n),\n.input:nth-child(9n+1),\n.input:nth-child(n+19):nth-child(-n+27),\n.input:nth-child(n+46):nth-child(-n+54),\n.input:nth-child(n+73):nth-child(-n+81),\n.input:nth-child(n+1):nth-child(-n+9)\n{\n\t\t/* Sudoku Hintergrundfarbe */\n\tbackground: ${colors.gridBG};\n\t/* Sudoku Rahmenfarbe */\n\tborder-color: ${colors.gridBorderColor};\n}\n.input.fixed\n{\n\t\t/* Feste Zahlen Schriftfarbe */\n\tcolor: ${colors.gridFixedFG} !important;\n}\n.input.variable\n{\n\t\t/* Variable Zahlen Schriftfarbe */\n\tcolor: ${colors.gridVariableFG};\n}\nbutton, .difficulty.dropdown\n{\n\t\t/* Button Schriftfarbe */\n\tcolor: ${colors.btnFG};\n\t/* Button Hintergrundfarbe */\n\tbackground: ${colors.btnBG};\n\t/* Button Rahmenfarbe */\n\tborder-color: ${colors.btnBorderColor}\n}\nbutton:hover\n{\n\t\t/* Button Hover-Schriftfarbe */\n\tcolor: ${colors.btnHFG};\n\t/* Button Hover-Hintergrundfarbe */\n\tbackground: ${colors.btnHBG};\n\t/* Button Hover-Rahmenfarbe */\n\tborder-color: ${colors.btnHoverBorderColor}\n}\na\n{\n\t\t/* Links Schriftfarbe */\n\tcolor: ${colors.linkFG};\n}\na:visited\n{\n\t\t/* besuchter Link Schriftfarbe */\n\tcolor: ${colors.linkVFG};\n}\na:hover\n{\n\t/* Links Hover-Schriftfarbe */\n\tcolor: ${colors.linkHFG};\n}`;
- }
- // init load of sudoku
- function initLoad(difficulty) {
- if (!jsonp)
- throw Error('Function jsonp not defined; Please contact page admin');
- if (difficulty < 1 || difficulty > 4)
- throw Error('Difficulty is not in the range from 1 to 4');
- jsonp('http://nine.websudoku.com/?level=' + difficulty, '`', 'sudoku.load');
- }
- // construct HTML element with attributes
- function construct(tag, attr) {
- if (!tag)
- throw Error("Tag name missing");
- var node = document.createElement(tag);
- for (prop in attr)
- if (attr[prop] instanceof Array)
- for (var i = 0; i < attr[prop].length; i++)
- if (prop.match(/inner/)) {
- if (typeof(attr[prop][i]) === 'string')
- node[prop] = attr[prop][i];
- else if (attr[prop][i] instanceof HTMLElement)
- node.appendChild(attr[prop][i]);
- } else
- node.setAttribute(prop, attr[prop][i]);
- else
- if (prop.match(/inner/)) {
- if (typeof(attr[prop]) === 'string')
- node[prop] = attr[prop];
- else if (attr[prop] instanceof HTMLElement)
- node.appendChild(attr[prop]);
- } else
- node.setAttribute(prop, attr[prop]);
- return node;
- }
- // request a webpage via JSONP
- function jsonp(url, delim, cb) {
- var s = construct("script", {
- type: 'text/javascript',
- src: 'jsonp.php?url=' + url + '&delim=' + delim + '&cb=' + cb
- });
- document.head.appendChild(s);
- setTimeout(() => document.head.removeChild(s), 50);
- }
- </script>
- <script>
- // Sudoku class
- function Sudoku(rootNode) {
- if (!this instanceof Sudoku)
- return new Sudoku();
- if (!rootNode || !(rootNode instanceof HTMLElement))
- throw Error('Argument 1 is not a HTML Element');
- // root node of the sudoku (id="sudoku")
- // children attributes row="x" column="y"
- this.rootNode = rootNode;
- // shorthand to access child elements
- this.children = Array.from(this.rootNode.children);
- // identify if pencil notes should be placed
- this.writeNotes = false;
- // notes should be removed automatically
- this.autoRemoveNotes = true;
- // returns Array of HTML nodes of the specified unit
- this.getNodes = {
- row: x => Array.from(this.rootNode.querySelectorAll(`[row="${x}"]`)) || [],
- column: x => Array.from(this.rootNode.querySelectorAll(`[column="${x}"]`)) || [],
- blockSegment: x => Array.from(this.rootNode.querySelectorAll(`[row]:nth-child(n+${x * 3 + 1}):nth-child(-n+${x * 3 + 3})`)) || [],
- block: x => x < 0 || x > 8 ? [] : [].concat(
- this.getNodes.blockSegment(x + 0 + (x < 6 && x > 2 ? 6 : (x < 9 && x > 5 ? 12 : 0))),
- this.getNodes.blockSegment(x + 3 + (x < 6 && x > 2 ? 6 : (x < 9 && x > 5 ? 12 : 0))),
- this.getNodes.blockSegment(x + 6 + (x < 6 && x > 2 ? 6 : (x < 9 && x > 5 ? 12 : 0)))
- ),
- number: x => this.children.map(n => !!Number(n.innerText) && Number(n.innerText) === x ? n : false).filter(n => n)
- };
- // returns the corresponding values for the specified unit
- this.getValues = {
- row: x => this.getNodes.row(x).map(n => Number(n.innerText)),
- column: x => this.getNodes.column(x).map(n => Number(n.innerText)),
- block: x => this.getNodes.block(x).map(n => Number(n.innerText))
- };
- // same as this.getValues with the exception that it
- // filters out values of notes
- this.getValuesExceptNotes = {
- row: x => this.getNodes.row(x).filter(n => !n.querySelector('table')).map(n => Number(n.innerText)),
- column: x => this.getNodes.column(x).filter(n => !n.querySelector('table')).map(n => Number(n.innerText)),
- block: x => this.getNodes.block(x).filter(n => !n.querySelector('table')).map(n => Number(n.innerText))
- }
- // returns all units the selected node is in
- this.getAllUnits = field => {
- if (!field || !(field instanceof HTMLElement)
- || !field.getAttribute('row') || !field.getAttribute('column'))
- throw Error('Invalid field (need attribute *row* and *column*)');
- // grid with indexes to find the block ID of a node
- var blocks = [
- [ 0, 1, 2, 9, 10, 11, 18, 19, 20],
- [ 3, 4, 5, 12, 13, 14, 21, 22, 23],
- [ 6, 7, 8, 15, 16, 17, 24, 25, 26],
- [27, 28, 29, 36, 37, 38, 45, 46, 47],
- [30, 31, 32, 39, 40, 41, 48, 49, 50],
- [33, 34, 35, 42, 43, 44, 51, 52, 53],
- [54, 55, 56, 63, 64, 65, 72, 73, 74],
- [57, 58, 59, 66, 67, 68, 75, 76, 77],
- [60, 61, 62, 69, 70, 71, 78, 79, 80]
- ];
- return []
- .concat(
- this.getNodes.row(Number(field.getAttribute('row'))),
- this.getNodes.column(Number(field.getAttribute('column'))),
- this.getNodes.block(blocks.map(v => v.indexOf(this.children.indexOf(field)) !== -1).indexOf(true))
- );
- };
- // returns all values of the units the node is in
- this.getAllUnitsValues = field => Array.from(new Set(this.getAllUnits(field).filter(n => n.children.length === 0 && Number(n.innerText) < 10 && Number(n.innerText) > 0).map(n => Number(n.innerText))));
- // return all potential candidates for node x
- this.getCandidateValues = field => {
- var vals = this.getAllUnitsValues(field);
- return [1, 2, 3, 4, 5, 6, 7, 8, 9].filter(v => !vals.includes(v));
- };
- // returns all potential candidate nodes for number x
- this.getCandidateNodes = x => {
- var allUnits = Array.from(new Set([].concat.apply([], this.getNodes.number(x).map(n => this.getAllUnits(n)))));
- return this.children.filter(n => !allUnits.includes(n)).filter(n => !n.className.match(/fixed/));
- };
- // reset all notes of the field
- this.resetNotes = field => this.getAllNote.nodes(field).map(n => n.innerText = '');
- // create all notes for candidate x
- this.setCandidate = x => this.getCandidateNodes(x).map(n => this.insertNumber({target: n}, x, true));
- // create all notes for the field
- this.setFieldCandidates = field => this.getCandidateValues(field).map(v => this.insertNumber({target: field}, v, true));
- // functions for all note-nodes
- this.getAllNote = {
- nodes: field => Array.from(field.querySelectorAll('td')) || [],
- values: field => this.getAllNote.nodes(field).map(n => Number(n.innerText)) || []
- };
- // functions for filled note-nodes
- this.getFilledNote = {
- nodes: field => this.getAllNote.nodes(field).filter(n => !!n.innerText.match(/\d/)) || [],
- values: field => this.getFilledNote.nodes(field).map(n => Number(n.innerText)) || []
- };
- // reset the sudoku
- this.reset = () => {
- for (var r = 0; r < 9; r++)
- for (var c = 0; c < 9; c++){
- var field = this.getNodes.row(r)[c];
- field.className = 'input variable';
- field.innerHTML = '';
- if (!field.hasAttribute('contenteditable'))
- field.setAttribute('contenteditable', 'true');
- }
- }
- // check if the sudoku is valid
- // returns validity, errors
- this.valid = () => {
- var r = {valid: true, error: [], valueOf: () => r.valid};
- for (unit in this.getValuesExceptNotes)
- for (var unitCounter = 0; unitCounter < 9; unitCounter++)
- for (var i = 1; i < 10; i++)
- if(this.getValuesExceptNotes[unit](unitCounter).indexOf(i) !== this.getValuesExceptNotes[unit](unitCounter).lastIndexOf(i)){
- r.valid = false;
- r.error.push({unit: unit, index: unitCounter, value: i});
- }
- return r;
- };
- // extract sudoku from source code
- this.extract = source => {
- var doc = (new DOMParser).parseFromString(source, 'text/html'),
- grid = doc.querySelector('#puzzle_grid');
- if (!grid)
- throw Error('Sudoku not found; Please contact page admin');
- grid = grid.querySelectorAll('tr');
- if (!grid)
- throw Error('Sudoku invalid');
- // return value is a 2-dimensional array with fixed sudoku values
- return Array.from(grid).map(n => Array.from(n.querySelectorAll('input')).map(n => n.value));
- };
- // load sudoku from source-code or 2D Array
- this.load = data => {
- var grid;
- if (typeof(data) === 'string')
- grid = this.extract(data);
- if (typeof(data) === 'object' && data.length &&
- data.length === 9 && data.every(a => a.length === 9))
- grid = data;
- if (!grid)
- this.notify('Sudoku konnte nicht geladen werden.');
- this.reset();
- for (var r = 0; r < 9; r++)
- for (var c = 0; c < 9; c++)
- if (grid[r][c] && grid[r][c] < 10 && grid[r][c] > 0) {
- var n = this.getNodes.row(r)[c];
- n.innerHTML = grid[r][c];
- n.className += ' fixed';
- n.removeAttribute('contenteditable');
- }
- };
- // export sudoku as json
- this.export = (filename, keys) => {
- var data = {
- fixed: new Array(9).fill(0).map(v => new Array(9).fill(0)),
- variable: new Array(9).fill(0).map(v => new Array(9).fill(0)),
- all: new Array(9).fill(0).map(v => new Array(9).fill(0))
- };
- for (var r = 0; r < 9; r++)
- for (var c = 0; c < 9; c++) {
- var val = this.getValues.row(r)[c];
- data.all[r][c] = val;
- if (this.getNodes.row(r)[c].className.match(/fixed/)) {
- data.fixed[r][c] = val;
- } else {
- data.variable[r][c] = val;
- }
- }
- if (keys && keys instanceof Array)
- for (prop in data)
- if (!keys.includes(prop))
- delete data[prop];
- data = JSON.stringify(data, null, '\t').replace(/\n\t{3}/g, '').replace(/(\d)\n\t{2}/g, '$1').replace(/\n/g, '\r\n');
- this.download(data, filename || 'Sudoku.json', 'text/json');
- return data;
- };
- // import sudoku (json)
- this.import = () => {
- var inp = document.createElement('input'),
- reader = new FileReader,
- that = this;
- // ask to load file/paste content
- if (confirm('Klicke OK um eine JSON Datei (von einem exportierten Sudoku) zu laden oder abbrechen, um den Code direkt einzufügen')) {
- inp.type = 'file';
- document.head.appendChild(inp);
- inp.click();
- inp.oninput = inputHandler;
- } else {
- handleJSON(prompt('Hier kannst du das JSON (der Code aus der heruntergeladenen oder selbst erstellten Datei) einfügen:'));
- }
- // read contents of file and call JSON handler
- function inputHandler(event) {
- if (inp.files.length > 0) {
- reader.readAsText(inp.files[0]);
- reader.onloadend = event => handleJSON(reader.result);
- }
- }
- // load fixed values
- function handleJSON(json) {
- if (json === '')
- return that.notify('Wenn du nichts eingibst, kann ich nichts laden');
- try {
- json = JSON.parse(json);
- } catch(e) {
- console.error(Error('Invalid JSON;', e));
- return that.notify('Das ist kein gültiges JSON');
- }
- if (json === null)
- return that.notify('Wenn du immer auf abbrechen klickst, kannst du auch nichts laden');
- if (!json)
- return that.notify('JSON konnte (warum auch immer) nicht geladen werden; Bitte sage mir bescheid!');
- if (!json.fixed)
- return that.notify('Im JSON fehlt der Wert *fixed*');
- // reset sudoku and load fixed values
- that.reset();
- that.load(json.fixed);
- // load variable values (if any)
- if (json.variable) {
- for (var r = 0; r < json.variable.length; r++) {
- for (var c = 0; c < json.variable[r].length; c++) {
- if (json.fixed[r][c] && json.variable[r][c] !== 0)
- that.notify('Du kannst auch über export/import keine festen Felder ändern!\nZeile ' + (r + 1) + ' Spalte ' + (c + 1) + ' wurde nicht geändert.');
- else if(json.variable[r][c])
- that.getNodes.row(r)[c].innerHTML = json.variable[r][c];
- }
- }
- }
- }
- }
- // create and download file
- this.download = (data, filename, type) => {
- var file = new Blob([data], {type: type});
- if (window.navigator.msSaveOrOpenBlob) {
- window.navigator.msSaveOrOpenBlob(file, filename);
- } else {
- var a = document.createElement('a'),
- url = URL.createObjectURL(file);
- a.href = url;
- a.download = filename;
- document.head.appendChild(a);
- a.click();
- setTimeout(() => {
- document.head.removeChild(a);
- URL.revokeObjectURL(url);
- }, 0);
- }
- };
- // table for pencil-notes
- this.insertNotesTable = n => n instanceof HTMLElement ? n.appendChild(new DOMParser().parseFromString('<table><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td></tr></table>', 'text/html').body.firstChild) : false;
- // show error (append error-class)
- this.showError = time => {
- var validity = this.valid();
- time = Number(time) === NaN ? time : 3000;
- // hide all errors if everything's fine
- if (validity.valid)
- return this.hideError();
- // show errors
- var errors = validity.error;
- for (var i = 0; i < errors.length; i++) {
- this.getNodes[errors[i].unit](errors[i].index).map((n, j) => {
- if (this.getValues[errors[i].unit](errors[i].index)[j] === errors[i].value)
- n.className += !!n.className.match(/error/) ? '' : ' error'
- });
- }
- // hide errors after 3s
- setTimeout(this.hideError, time);
- };
- // hide error class after 1/4 seccond
- this.hideError = () => {
- this.children.map(n => n.className += ' out');
- setTimeout(() => this.children.map(n => n.className = n.className.replace(/\serror/, '').replace(/\sout/, '')), 250);
- };
- // highlight all selected units
- this.highlight = elem => {
- elem.className += !!elem.className.match(/selected/) ? '' : ' selected';
- // highlight all units
- this.getAllUnits(elem).map(n => setActive(n));
- // highlight all equal numbers
- this.children.map(n => {
- if (!!elem.innerHTML && n.innerHTML === elem.innerHTML && n.children.length === 0) {
- setActive(n);
- }
- });
- function setActive(n) {
- n.className += !!n.className.match(/active/) ? '' : ' active';
- }
- };
- // remove highlighting of all elements and remove selected class
- this.unhighlight = elem => {
- elem.className = elem.className.replace(/\sselected/g, '');
- this.children.map(n => n.className = n.className.replace(/\sactive/g, ''));
- };
- // function to auto-remove notes from field
- this.autoRemoveNotesFunc = field => Array.from(new Set([].concat.apply([], sudoku.getAllUnits(field).map(n => sudoku.getFilledNote.nodes(n).filter(n => !!n.innerText.match(new RegExp(field.innerText))))))).map(n => n.innerHTML = '')
- // insert a number or note to *this* (for eventlistener)
- // usage: inputField.addEventListener('keydown', sudokuInstance.insertNumber);
- this.insertNumber = (event, key, isNote) => {
- var target = event.target.closest('.input');
- isNote = !!isNote;
- if (!(target instanceof HTMLElement))
- throw Error('target is not a HTMLElement');
- if (target.className.match(/fixed/) && !this.editor.running)
- return console.log('You can\'t change fixed numbers');
- key = key || event.key || String.fromCharCode(event.keyCode);
- // allow jumping with tab thought the sudoku
- if (key === 'Tab' || key === '\t' || event.keyCode === 9)
- return;
- // create dummy event method
- if (!event.preventDefault)
- event = {preventDefault: new Function};
- // pencil-notes used on this field
- if (this.writeNotes || isNote) {
- if (!target.querySelector('td') || target.querySelectorAll('td').length !== 9){
- target.innerHTML = '<div class="overlay"></div>';
- this.insertNotesTable(target);
- }
- if (key < 10 && key > 0) {
- if (!target.querySelector('td') || target.querySelectorAll('td').length !== 9)
- throw Error('Invalid table; Exactly 9 cells are required');
- var node = target.querySelectorAll('td')[key - 1];
- node.innerText = node.innerText === '' ? key : '';
- event.preventDefault();
- return;
- }
- } else {
- target.innerHTML = !!key && key < 10 && key > 0 ? key : (key === ' ' || key === 'Backspace' || key === '\u0008' ? '' : target.innerHTML);
- if (this.autoRemoveNotes && !!Number(key))
- this.autoRemoveNotesFunc(target);
- event.preventDefault();
- }
- };
- // methods to solve a sudoku; return the amount of filled numbers
- this.solve = {
- nakedSingle: unitArr =>
- unitArr/*this.children*/
- .filter(n => !Number(n.innerHTML) && !n.className.match(/fixed/))
- .map(n => ({node: n, notes: this.getCandidateValues(n)}))
- .filter(obj => obj.notes.length === 1)
- .map(obj => this.insertNumber({target: obj.node}, obj.notes[0]))
- .length,
- hiddenSingle: unitArr => {
- var nodes = [].concat.apply([], unitArr.map(this.getFilledNote.nodes)),
- values = nodes.map(n => n.innerHTML);
- nodes
- .map(n => values.indexOf(n.innerHTML) === values.lastIndexOf(n.innerHTML))
- .map((v, i) => v ? values[i] : false)
- .filter(v => v)
- .map(v => this.insertNumber({target: nodes[values.indexOf(v)]}, Number(v)))
- .length
- }
- };
- this.editor = {
- running: false,
- start: () => {
- this.editor.running = true;
- this.reset();
- this.children.map(n => {
- n.setAttribute('contenteditable', 'true');
- n.className = n.className.replace(/variable/g, 'fixed');
- });
- },
- stop: () => {
- if (!this.editor.running)
- return this.notify('Was willst du stoppen, wenn du nichts gestartet hast??');
- if (!this.valid().valid){
- this.showError(5000);
- return this.notify('Sudoku enthält eindeutige Fehler; Bitte verbessern!');
- }
- this.children.map(n => {
- if (!!Number(n.innerText))
- n.setAttribute('contenteditable', 'false');
- else
- n.className = n.className.replace(/fixed/, 'variable');
- })
- if (confirm('Willst du das Sudoku als JSON-Datei herunterladen?')){
- if ((name = prompt('Dateiname:', 'Sudoku.json')) !== null)
- this.export(name, ['fixed']);
- } else {
- this.notify('Schon wieder so ein Abbrechen-Klicker!');
- }
- this.editor.running = false;
- }
- };
- // notify user about something
- this.notify = message => {
- alert(message);
- };
- // return object
- return this;
- }
- </script>
- <style>
- /* allgemeine Konfigurationen */
- :root
- {
- --size: 50px;
- --font-size: 40px;
- --text-color: #00D;
- --outer-border-width: 3px;
- --inner-border-width: 1px;
- }
- body
- {
- padding: 10px;
- margin: 0;
- font-family: Serif;
- }
- /* Hauptteil der Seite */
- main
- {
- width: 900px;
- margin: auto;
- padding: 10px 15px;
- box-shadow: 0 0 3px 1px #BBB;
- }
- /* Flexbox */
- .flex
- {
- display: flex !important;
- margin: 5px 0;
- }
- /* Flexbox 'kinder' */
- .flex *
- {
- flex: 1 1;
- text-align: center;
- }
- button, .difficulty.dropdown
- {
- flex-grow: 1.059;
- background: lightgrey;
- border: solid 1px #999;
- margin: 0 2px;
- height: 30px;
- }
- button:hover
- {
- background: #BBB;
- }
- /* überdeckt bestimmtes Feld */
- .overlay
- {
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 99;
- }
- /* Sudoku Raster */
- #sudoku
- {
- min-width: 480px;
- min-height: 480px;
- max-width: 480px;
- max-height: 480px;
- margin-left: 30px;
- margin-top: 20px;
- float: right;
- }
- /* Regeln für Bleistiftnotizen */
- .input table
- {
- width: 100%;
- height: 100%;
- }
- .input table tr
- {
- height: 33.3333333333%;
- }
- .input table td
- {
- line-height: 12px;
- font-size: 15px;
- color: grey;
- width: 33.3333333333%;
- height: 33.3333333333%;
- }
- /* aussehen eines Feldes */
- .input
- {
- margin: 0;
- width: var(--size);
- height: var(--size);
- line-height: var(--size);
- font-size: var(--font-size);
- text-align: center;
- border: solid var(--inner-border-width) grey;
- display: inline-block;
- position: relative;
- overflow: hidden;
- float: left;
- }
- /*
- * nach 9 Feldern 'umbrechen' (wieder vorne anfangen)
- * dickerer Strich auf der linken Seite
- */
- .input:nth-child(9n+1)
- {
- clear: left;
- border-left: solid var(--outer-border-width) grey;
- }
- /* dickerer Strich nach 3 Feldern (wagerecht) */
- .input:nth-child(n+19):nth-child(-n+27),
- .input:nth-child(n+46):nth-child(-n+54),
- .input:nth-child(n+73):nth-child(-n+81)
- { border-bottom: solid var(--outer-border-width) grey }
- /* dickerer Strich nach drei Feldern (senkrecht) */
- .input:nth-child(3n)
- { border-right: solid var(--outer-border-width) grey }
- /* dickerer Strich oberhalb des Feldes */
- .input:nth-child(n+1):nth-child(-n+9)
- { border-top: solid var(--outer-border-width) grey }
- /* Schriftfarbe von variablen Zahlen */
- .variable.input
- { color: #00D }
- /* Schriftfarbe von fest eingetragenen Zahlen */
- .fixed.input
- { color: black !important }
- /* betroffene Zahlen */
- .active.input
- { background: #DDD !important }
- /* ausgewählte Zahl */
- .selected.input
- { background: #EEE !important }
- /* Fehler anzeigen */
- .error.input
- {
- background: #F00 !important;
- transition: linear 200ms background;
- }
- /* Fehler verbergen */
- .out.error.input
- {
- background: initial !important;
- transition: linear 200ms background;
- }
- /* Dropdown-Menü */
- .dropdown
- {
- /*font-size: 18px;*/
- /*font-family: arial;*/
- height: 30px;/* Schriftgröße + Abstand Oben (5px) + Abstand Unten (5px) + 'Schönheitsabstand' (2px) */
- display: inline-block;
- background: lightgrey;
- overflow: hidden;
- /*border-radius: 3px;*/
- border: solid 1px #999;
- padding-bottom: 0px;
- transition: linear 200ms padding-bottom;
- }
- .dropdown:hover
- {
- padding-bottom: 110px;
- transition: linear 200ms padding-bottom;
- }
- .dropdown *
- { padding: 5px 10px }
- .dropdown .header
- {
- margin: 0;
- font-weight: normal;
- margin-bottom: 3px;
- padding-bottom: 7px;
- border-bottom: solid 1px grey;
- display: block;
- background: inherit;
- font-size: inherit;
- height: auto;
- }
- .dropdown .item
- {
- margin: 0;
- width: 100%;
- display: block;
- border: none;
- background: inherit;
- /*font-size: 16px;*/
- text-align: left;
- height: auto;
- }
- .dropdown .item:hover
- { background: #BBB }
- /* Fenster für Theme anpassungen */
- .themeWindow
- {
- opacity: 0;
- position: fixed;
- width: 400px;
- padding-bottom: 10px;
- transform: translate(-35px);
- border: solid 1px grey;
- border-radius: 10px;
- background: #FFF;
- z-index: -10;
- transition: linear 250ms opacity, linear 300ms z-index;
- }
- /* Fenster zeigen */
- .themeWindow.show
- {
- z-index: 10;
- color: #000;
- opacity: 1;
- transition: linear 250ms opacity;
- }
- /* Fenster Kopfzeile */
- .themeWindow .head
- {
- display: block;
- border-bottom: solid 1px grey;
- padding: 1% 2%;
- font-size: x-large;
- text-align: center;
- cursor: move;
- }
- /* Schließen-Knopf */
- .themeWindow .head .close
- {
- display: inline;
- font-family: Arial;
- position: absolute;
- right: 0;
- margin-right: 1.5%;
- width: 30px;
- height: 30px;
- background: transparent;
- color: #000;
- border-radius: 3px;
- transition: linear 150ms background, linear 150ms color;
- }
- .themeWindow .head .close:hover
- {
- cursor: default;
- background: #F00;
- color: #FFF;
- transition: linear 150ms background, linear 150ms color;
- }
- /* Fenster-Inhalt */
- .themeWindow .content
- {
- display: block;
- margin: 1% 2%;
- padding-right: 1%;
- height: 400px;
- overflow-y: auto;
- }
- /* Farbentabelle */
- .themeWindow table
- {
- width: 100%;
- border-collapse: collapse;
- }
- /* Farb-Input */
- .themeWindow table input
- { width: 100% }
- .themeWindow td
- { padding: 0.5% 1% }
- .themeWindow tr:hover
- { background: #EEE }
- </style>
- <style>
- body
- { background: #EFEFEF }
- main
- {
- /* Hauptteil Hintergrundfarbe */
- background: #FFFFFF;
- /* Hauptteil Schriftfarbe */
- color: #000000;
- /* Hauptteil Schatten
- * X-Verschiebung
- * Y-Verschiebung
- * Blur-Breite
- * Schatten-Breite
- * Schatten-Farbe
- */
- box-shadow: 0 0 3px 1px #BBB;
- }
- .input,
- .input.variable,
- .input.fixed,
- .input:nth-child(3n),
- .input:nth-child(9n+1),
- .input:nth-child(n+19):nth-child(-n+27),
- .input:nth-child(n+46):nth-child(-n+54),
- .input:nth-child(n+73):nth-child(-n+81),
- .input:nth-child(n+1):nth-child(-n+9)
- {
- /* Sudoku Hintergrundfarbe */
- background: #FFFFFF;
- /* Sudoku Rahmenfarbe */
- border-color: #808080;
- }
- .input.fixed
- {
- /* Feste Zahlen Schriftfarbe */
- color: #000;
- /* Feste Zahlen Hintergrundfarbe */
- background: transparent;
- }
- .input.variable
- {
- /* Variable Zahlen Schriftfarbe */
- color: #0000DD;
- /* Variable Zahlen Hintergrundfarbe */
- background: transparent;
- }
- button, .difficulty.dropdown
- {
- /* Button Schriftfarbe */
- color: #000000;
- /* Button Hintergrundfarbe */
- background: #D3D3D3;
- }
- button:hover
- {
- /* Button Hover-Schriftfarbe */
- color: #000000;
- /* Button Hover-Hintergrundfarbe */
- background: #BBBBBB;
- }
- a
- {
- /* Links Schriftfarbe */
- color: #0000EE;
- /* Links Hintergrundfarbe */
- background: transparent;
- }
- a:visited
- {
- /* besuchter Link Schriftfarbe */
- color: #0000EE;
- /* besuchter Link Hintergrundfarbe */
- background: transparent;
- }
- a:hover
- {
- /* Links Hover-Schriftfarbe */
- color: #0000EE;
- /* Links Hover-Hintergrundfarbe */
- background: transparent;
- }
- </style>
- <link rel="stylesheet" type="text/css" href="theme_light.css">
- <style type="text/css" id="userStyles"></style>
- </head>
- <body
- ondragover="
- event.preventDefault()
- "
- ondrop="
- if(event.dataTransfer.getData('class') === 'themeWindow'){
- var tw = document.querySelector('.themeWindow'),
- offset = JSON.parse(event.dataTransfer.getData('offset'));
- tw.style.left = (event.clientX - offset.x) + 'px';
- tw.style.top = (event.clientY - offset.y) + 'px'
- }"
- ondblclick="
- if (event.target !== this)
- return;
- if (n = document.querySelector('#userStyles'))
- n.innerHTML = '';
- if(n = document.querySelector('link[href=\'theme_dark.css\']'))
- n.href='theme_light.css';
- else if(n = document.querySelector('link[href=\'theme_light.css\']'))
- n.href='theme_dark.css';
- event.preventDefault();
- ">
- <main>
- <section class="themeWindow " draggable="true" ondragstart="
- var bbox = this.getBoundingClientRect();
- event.dataTransfer.setData('class', 'themeWindow');
- event.dataTransfer.setData('offset', JSON.stringify({
- x: event.clientX - bbox.x,
- y: event.clientY - bbox.y
- }));
- ">
- <span class="head">
- Farben der Seite anpassen
- <span class="close" onclick="var tw = this.closest('.themeWindow'); tw.className = tw.className.replace(/show/, '')">X</span>
- </span>
- <section class="content">
- <p>Hier kannst du die Farben auf der Seite so anpassen, dass sie dir
- gefallen. Du kannst einfach das <i>light theme</i> oder das <i>dark
- theme</i> benutzen (Doppelklick auf den Hintergrund), oder natürlich
- ein ganz eigenes erstellen.</p>
- <table>
- <tr>
- <td width="50%">Seite Hintergrundfarbe</td>
- <td><input type="color" name="bodyBG" value="#efefef"></td>
- </tr>
- <tr>
- <td>Inhalt Hintergrundfarbe</td>
- <td><input type="color" name="mainBG" value="#ffffff"></td>
- </tr>
- <tr>
- <td>Inhalt Schriftfarbe</td>
- <td><input type="color" name="mainFG" value="#000000"></td>
- </tr>
- <tr>
- <td>Inhalt Schattenfarbe</td>
- <td><input type="color" name="mainShadowColor" value="#bbbbbb"></td>
- </tr>
- <tr>
- <td>Sudoku Hintergrundfarbe</td>
- <td><input type="color" name="gridBG" value="#ffffff"></td>
- </tr>
- <tr>
- <td>Sudoku Rahmenfarbe</td>
- <td><input type="color" name="gridBorderColor" value="#808080"></td>
- </tr>
- <tr>
- <td>Sudoku feste Zahlen</td>
- <td><input type="color" name="gridFixedFG" value="#000000"></td>
- </tr>
- <tr>
- <td>Sudoku variable Zahlen</td>
- <td><input type="color" name="gridVariableFG" value="#0000DD"></td>
- </tr>
- <tr>
- <td>Buttons Schriftfarbe</td>
- <td><input type="color" name="btnFG" value="#000000"></td>
- </tr>
- <tr>
- <td>Buttons Hintergrundfarbe</td>
- <td><input type="color" name="btnBG" value="#d3d3d3"></td>
- </tr>
- <tr>
- <td>Buttons Rahmenfarbe</td>
- <td><input type="color" name="btnBorderColor" value="#999999"></td>
- </tr>
- <tr>
- <td>Buttons Hover-Farbe</td>
- <td><input type="color" name="btnHFG" value="#000000"></td>
- </tr>
- <tr>
- <td>Buttons Hover-Hintergrundfarbe</td>
- <td><input type="color" name="btnHBG" value="#bbbbbb"></td>
- </tr>
- <tr>
- <td>Buttons Hover-Rahmenfarbe</td>
- <td><input type="color" name="btnHoverBorderColor" value="#999999"></td>
- </tr>
- <tr>
- <td>Links Farbe</td>
- <td><input type="color" name="linkFG" value="#0000ee"></td>
- </tr>
- <tr>
- <td>Links (besucht) Farbe</td>
- <td><input type="color" name="linkVFG" value="#0000ee"></td>
- </tr>
- <tr>
- <td>Links Hover-Farbe</td>
- <td><input type="color" name="linkHFG" value="#0000ee"></td>
- </tr>
- </table>
- <section class="flex">
- <button class="csExport">Exportieren</button>
- <button class="csImport">Importieren</button>
- <button class="csApply">Übernehmen</button>
- </section>
- </section>
- </section>
- <section id="sudoku"></section>
- <section>
- <h1>Sudoku</h1>
- <p>Ich denke die Regeln für ein Sudoku sind dir bereits bekannt, wenn
- du auf diese Seite gekommen bist. Nur aus Gründen der Vollständigkeit
- (sie gehören doch wohl dazu ☺) schreibe ich sie hier noch einmal auf.</p>
- <ul>
- <li>In jedem Feld kann eine Zahl von 1 bis 9 stehen</li>
- <li>Jede Zahl muss exakt einmal in jeder Einheit (Zeile, Spalte,
- Block) vorkommen, sonst liegt ein Fehler vor.</li>
- <li>Ist das nicht möglich oder hat das Sudoku keine eindeutige
- Lösung, ist es nicht lösbar</li>
- <li>Falls du ein ungültiges Sudoku hast und es versuchst zu lösen,
- ist das nicht mein Problem. Die Sudokus, die hier geladen werden,
- sollten aber immer nur eine mögliche Lösung haben.</li>
- </ul>
- <p>Das hier ist eine auf JavaScript basierte Version des Spiels Sudoku.
- Es ist also möglich, den kompletten Quellcode auf den PC herunterzuladen
- und es genauso offline zu nutzen. Eine Einschränkung gibt es allerdings:
- Die Funktion um ein Sudoku zu laden greift auf <i>file_get_contents()</i>
- von PHP zurück, um Webseiten mit JSONP zu laden. Das würde also nicht
- funktionieren, falls es einfach kopiert werden würde. Das sollte aber
- kein so großes Problem darstellen, weil man Sudokus ja auch z.B. aus
- einer Zeitschrift abschreiben kann.</p>
- <p>Bis jetzt gbit es schon eine Fehlerüberprüfung, eine Funktion um ein
- Sudoku zu laden, eine um es in das Format JSON zu exportieren und lokal
- auf dem PC zu speichern und natürlich eine um das ganze wieder zu laden.
- <br>Letztere kann auch dazu verwendet werden, irgendein Sudoku hiermit
- zu spielen. Dazu muss man nur eine Textdatei erstellen, in der (in dem
- vorgegebenen Format natürlich) alle Zahlen stehen, die eingetragen
- werden sollen. Um ein leeres Feld zu machen, muss aber eine 0 geschrieben
- werden.</p>
- </section>
- <section>
- <h3>Ein Haufen Buttons ...</h3>
- <p>Einer um Sudokus zu laden, dann noch ein paar um Notizen zu verwenden
- (ein/aus schalten, automatisch ausstreichen, alle für eine bestimmte Zahl
- erstellen oder alle für ein bestimmtes Feld erstellen) und natürlich um
- ein Sudoku zu exportieren oder zu importieren. Weiter unten ist auch noch
- ein Link für eine Vorlage, mit der du ein Sudoku aus einer Zeitschrift
- oder sonstwoher erstellen und hier importieren kannst. Ah und fast hätte
- ich es vergessen: es gibt ja auch noch einen, der dir zeigt, ob du einen
- Fehler gemacht hast und einen, der dir die Fehler anzeigen kann.</p>
- </section>
- <section class="flex">
- <div class="difficulty dropdown">
- <span class="header" title="ein Sudoku laden">Sudoku laden</span>
- <button class="item" title="das bekommt echt jeder hin!">Leicht</button>
- <button class="item" title="das hier ist eher für fortgeschrittene">Mittel</button>
- <button class="item" title="das kannst du oft nur lösen, wenn du ein paar Strategien mehr kennst">Schwer</button>
- <button class="item" title="hierfür musst du echt schon ein Profi sein!">Sehr Schwer</button>
- </div>
- <button onclick="
- var tw = document.querySelector('.themeWindow');
- if (!tw.className.match(/show/))
- tw.className += 'show'"
- >Farben ändern</button>
- </section>
- <section class="flex">
- <button class="check" title="Zeigt dir, ob du bis jetzt alles richtig gemacht hast">Überprüfen</button>
- <button class="checkAndShow" title="markiert deine Fehler für ein paar Sekunden">Fehler zeigen</button>
- </section>
- <section class="flex">
- <button class="notes" title="hiermit kannst du die Bleistiftnotizen ein/aus schalten">Bleistiftnotizen anschalten</button>
- <button class="autoRemoveNotes" title="Willst du Notizen selbst ausstreichen oder soll es automatisch passieren?">Notizen manuell streichen</button>
- <button class="setCandidate">Notizen von Zahl eintragen</button>
- <button class="setFieldCandidates">Notizen für Feld eintragen</button>
- </section>
- <section class="flex">
- <button class="setAllCandidates">Alle Notizen erstellen</button>
- </section>
- <section class="flex">
- <button class="create" title="Sudoku wird resettet und du kannst selbst die festen Zahlen (z.B. aus einer Zeitschrift) eintragen. Wenn du fertig bist, klickst du den Button noch einmal und kannst es herunterladen, spielen oder was auch immer">Editor starten</button>
- </section>
- <section class="flex">
- <button class="export" title="Sudoku als JSON-Datei exportieren, die z.B. offline geladen werden kann">Exportieren</button>
- <button class="import" title="Ein Sudoku laden, dass vorher exportiert wurde. Einzige benötigte Eigenschaft im JSON ist *fixed*">Importieren</button>
- </section>
- <section>
- <p><b><u>Achtung</u>: Bis jetzt können Notizen nicht mit exportiert
- werden. Das werde ich später einmal noch dazumachen. Wenn du ein Spiel
- importiert hast und da drin Notizen waren, gibt es 3 Möglichkeiten:</b></p>
- <ol>
- <li>Du lässt sie automatisch erstellen und machst die logischen
- Ausstreichungen selbst</li>
- <li>Du ignorierst es und versuchst, das Sudoku ohne Notizen zu lösen</li>
- <li>Du zertrümmerst deinen PC, weil er dich so wütend gemacht hat
- und du mit ihm nichts mehr zu tun haben willst</li>
- </ol>
- <p>Ich persönlich würde ja zu Möglichkeit Nr. 1 tendieren aber es gibt
- ja sicher auch Leute, die eine andere wählen würden ...</p>
- </section>
- <section>
- <p>Wenn du ein Sudoku z.B. aus einer Zeitschrift hier einfügen und spielen
- willst, kannst du dir <a onclick="if (name = prompt('Dateiname:', 'Beispielsudoku.txt')) this.download = name; else event.preventDefault();" href="data:application/json;base64,ew0KCSJmaXhlZCI6IFsNCgkJWzAsMCwwLCAgMCwwLDAsICAwLDAsMF0sDQoJCVswLDAsMCwgIDAsMCwwLCAgMCwwLDBdLA0KCQlbMCwwLDAsICAwLDAsMCwgIDAsMCwwXSwNCg0KCQlbMCwwLDAsICAwLDAsMCwgIDAsMCwwXSwNCgkJWzAsMCwwLCAgMCwwLDAsICAwLDAsMF0sDQoJCVswLDAsMCwgIDAsMCwwLCAgMCwwLDBdLA0KDQoJCVswLDAsMCwgIDAsMCwwLCAgMCwwLDBdLA0KCQlbMCwwLDAsICAwLDAsMCwgIDAsMCwwXSwNCgkJWzAsMCwwLCAgMCwwLDAsICAwLDAsMF0NCgldDQp9">hier</a>
- eine Vorlage herunterladen, die nur noch mit den festen Zahlen
- ausgefüllt werden muss und dann importiert werden kann. Eine 0 bedeutet,
- dass nichts in dem Feld steht (der Link funktioniert auch offline).</p>
- <p>Falls du noch Ideen haben solltest, was man noch verbessern könnte,
- was man ändern könnte, noch hinzufügen oder was auch immer, kannst du
- mir ja gerne noch einmal schreiben ☺</p>
- <p>Vor allem für Vorschläge bezüglich Bildern oder Grafiken wäre ich
- dankbar. Das ist nämlich so etwas, wovon ich praktisch überhaupt
- nichts verstehe, wie man sicher sehr schnell merken wird ;-)</p>
- <p>Sooooo jetzt ist die Funktion, für Bleistiftnotizen auch fertig.
- Falls du also auch schon ein fortgeschrittener Sudoku-Spieler bist, der
- zuerst Bleistiftnotizen macht, kannst du das hier jetzt auch machen.
- Einfach den Button <i>Bleistiftnotizen anschalten</i> anklicken und
- dann ganz normal die Zahl eingeben. Sie wird dann in das Feld als
- Bleistiftnotiz eingetragen. Um Die Notiz zu löschen, einfach die
- gleiche Zahl noch einmal eingeben oder noch einmal den Button
- klicken und dann eine feste Zahl eintragen. Dann werden aber alle
- wieder gelöscht.
- </section>
- </main>
- </body>
- </html>
Add Comment
Please, Sign In to add comment