Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                main(lang="ja")
  h1 無限ナンプレ
  p 方向キーでマスを移動できます。
  number-place#app
  .ui
    button#reset リセット
    button#removeIncorrects 間違っている場所を空にする
    button#ans 答えを表示
    button#new 新しい問題
              
            
!

CSS

              
                body {
  font-family: sans-serif;
}

h1 {
  margin: 2em 0 1em;
  font-size: 1rem;
  text-align: center;
}

p {
  text-align: center;
  font-size: 0.825rem;
}

number-place {
  display: block;
  width: 304px;
  max-width: 100%;
  margin: 30px auto;
}

.ui {
  text-align: center;
}
              
            
!

JS

              
                class NumberPlace extends HTMLElement {
    constructor () {
        super();

        this.#setProps();
        this.#setMap();
        this.#grouping();
        this.#addEvent();
        this.init();
    }

    #shadowRoot = null;
    #inputs = null;
    #map = null;
    #group = [];
    #vertical = [];
    #horizontal = [];
    #loading = false;

    #getVal = values => {
        const idx = (() => {
            return Math.floor(Math.random() * values.length);
        })();
        const val = values[idx];

        values.splice(idx, 1);

        return val;
    }

    #setProps = () => {
        let src = '';
        const style = document.createElement('style');
        const groupName = [
            '',
            '左上',
            '上',
            '右上',
            '左',
            '中央',
            '右',
            '左下',
            '下',
            '右下',
        ];
        const getLabel = (y, x) => {
            const vertical = {
                '1-1': 'A',
                '1-2': 'B',
                '1-3': 'C',
                '2-1': 'D',
                '2-2': 'E',
                '2-3': 'F',
                '3-1': 'G',
                '3-2': 'H',
                '3-3': 'I',
            };
            const horizontal = {
                '1-1': '1',
                '1-2': '2',
                '1-3': '3',
                '2-1': '4',
                '2-2': '5',
                '2-3': '6',
                '3-1': '7',
                '3-2': '8',
                '3-3': '9',
            };

            return `${vertical[y]}${horizontal[x]}`;
        };

        style.textContent = `
            :host > div * {
                box-sizing: border-box;
                margin: 0;
                padding: 0;
            }
            :host [readOnly] {
                font-weight: bold;
                background: #f1f1f1;
            }
            :host :focus {
                outline: 0;
                box-shadow: 0 0 0 1px #000 inset;
            }
            :host > div {
                display: flex;
                flex-wrap: wrap;
                min-height: 300px;
                border: 1px solid #666;
                border-right: 0;
            }
            :host > div input {
                width: 100%;
                font-size: 1rem;
                text-align: center;
                border: 0 solid #666;
                border-width: 1px 0 0 1px;
                border-radius: 0;
                -webkit-appearance: none;
                -moz-appearance:textfield;
            }
            :host > div input::-webkit-outer-spin-button,
            :host > div input::-webkit-inner-spin-button {
                -webkit-appearance: none;
                margin: 0;
            }
            :host > div > div{
                border: 1px solid #666;
            }
            :host > div > div {
                display: grid;
                grid-template: auto auto auto / auto auto auto;
                flex-basis: 33.3333%;
                max-width: 33.3333%;
                min-width: 33.3333%;
            }
            :host > div > div:nth-child(3n) {
                border-right-width: 2px;
            }
            :host > div > div input:nth-child(1),
            :host > div > div input:nth-child(2),
            :host > div > div input:nth-child(3) {
                grid-row: 1 / 2;
                border-top-width: 0;
            }
            :host > div > div input:nth-child(4),
            :host > div > div input:nth-child(5),
            :host > div > div input:nth-child(6) {
                grid-row: 2 / 3;
            }
            :host > div > div input:nth-child(7),
            :host > div > div input:nth-child(8),
            :host > div > div input:nth-child(9) {
                grid-row: 3 / 4;
            }
            :host > div > div input:nth-child(1),
            :host > div > div input:nth-child(4),
            :host > div > div input:nth-child(7) {
                grid-column: 1 / 2;
                border-left-width: 0;
            }
            :host > div > div input:nth-child(2),
            :host > div > div input:nth-child(5),
            :host > div > div input:nth-child(8) {
                grid-column: 2 / 3;
            }
            :host > div > div input:nth-child(3),
            :host > div > div input:nth-child(6),
            :host > div > div input:nth-child(9) {
                grid-column: 3 / 4;
            }
            :host > div > div input[aria-invalid="true"] {
                background: pink;
            }
        `;

        this.#shadowRoot = this.attachShadow({
            mode: 'closed'
        });
        this.#shadowRoot.append(document.createElement('div'));

        for (let i = 1; i < 10; i++) {
            let vertical = 0;
            let horizontal = 0;

            if (
                i === 1 ||
                i === 4 ||
                i === 7
            ) {
                vertical = 1;
            }

            if (
                i === 2 ||
                i === 5 ||
                i === 8
            ) {
                vertical = 2;
            }

            if (
                i === 3 ||
                i === 6 ||
                i === 9
            ) {
                vertical = 3;
            }

            if (i <= 3) {
                horizontal = 1;
            } else if (i <= 6) {
                horizontal = 2;
            } else {
                horizontal = 3;
            }

            src += `<div role="group" aria-label="${groupName[i]}ブロック">`;

            for (const num of [1, 2, 3, 4, 5, 6, 7, 8, 9]) {
                let innerVertical = 0;
                let innerHorizontal = 0;

                if (
                    num === 1 ||
                    num === 4 ||
                    num === 7
                ) {
                    innerVertical = 1;
                }

                if (
                    num === 2 ||
                    num === 5 ||
                    num === 8
                ) {
                    innerVertical = 2;
                }

                if (
                    num === 3 ||
                    num === 6 ||
                    num === 9
                ) {
                    innerVertical = 3;
                }

                if (num <= 3) {
                    innerHorizontal = 1;
                } else if (num <= 6) {
                    innerHorizontal = 2;
                } else {
                    innerHorizontal = 3;
                }

                src += `<input
                    inputmode="numeric"
                    maxlength="1"
                    pattern="^[0-9]$"
                    title="半角数字1文字を入力してください"
                    tabindex="-1"
                    readOnly
                    data-group-id="${i}"
                    data-vertical="${vertical}-${innerVertical}"
                    data-horizontal="${horizontal}-${innerHorizontal}"
                    aria-label="${getLabel(`${vertical}-${innerVertical}`, `${horizontal}-${innerHorizontal}`)}"
                >`;
            }

            src += '</div>';
        }

        this.isCleared = false;
        this.#shadowRoot.firstElementChild.innerHTML += src;
        this.#shadowRoot.prepend(style);
        this.#inputs = this.#shadowRoot.querySelectorAll('input');
        this.#map = new Map();
        this.#inputs[0].tabIndex = 0;
    }

    #setMap = function () {
        for (const input of this.#inputs) {
            const data = {
                horizontal: null,
                vertical: null,
                group: null,
                ans: ''
            };

            this.#map.set(input, data);
        }
    }

    #grouping = function () {
        for (let i = 1; i <= 3; i++) {
            for (let j = 1; j <= 3; j++) {
                this.#vertical.push(
                    [...this.#shadowRoot.querySelectorAll(`[data-vertical="${i}-${j}"]`)]
                );
                this.#horizontal.push(
                    [...this.#shadowRoot.querySelectorAll(`[data-horizontal="${i}-${j}"]`)]
                );
            }
        }

        for (let i = 1; i <= 9; i++) {
            this.#group.push(
                [...this.#shadowRoot.querySelectorAll(`[data-group-id="${i}"]`)]
            );
        }

        for (const input of this.#inputs) {
            const data = this.#map.get(input);

            for (const group of this.#group) {;
                if (group.indexOf(input) !== -1) {

                    data.group = group;

                    continue;
                }
            }

            for (const group of this.#vertical) {
                if (group.indexOf(input) !== -1) {
                    data.vertical = group;

                    continue;
                }
            }

            for (const group of this.#horizontal) {
                if (group.indexOf(input) !== -1) {
                    data.horizontal = group;

                    continue;
                }
            }
        }
    }

    #addEvent = function () {
        let key = -1;
        const self = this;
        const map = self.#map;
        const inputs = self.#inputs;
        const move = {
            horizontal(e, isRight) {
                const {horizontal} = map.get(this);
                const idx = horizontal.indexOf(this);
                let target = horizontal[idx + (isRight ? 1 : -1)];

                e.preventDefault();

                if (!target) {
                    target = horizontal[(isRight ? 0 : horizontal.length - 1)];
                }

                this.tabIndex = -1;
                target.tabIndex = 0;
                target.focus();
            },
            vertical(e, isDown) {
                const {vertical} = map.get(this);
                const idx = vertical.indexOf(this);
                let target = vertical[idx + (isDown ? 1 : -1)];

                e.preventDefault();

                if (!target) {
                    target = vertical[(isDown ? 0 : vertical.length - 1)];
                }

                this.tabIndex = -1;
                target.tabIndex = 0;
                target.focus();
            },
        };
        const editHandler = function () {
            const check = list => {
                for (const input of list) {
                    if (!input.value) {
                        continue;
                    }

                    list.forEach(item => {
                        if (
                            input !== item &&
                            input.value === item.value
                        ) {
                            item.setAttribute('aria-invalid', 'true');
                            input.setAttribute('aria-invalid', 'true');
                        }
                    });
                }
            };

            if ([1, 2, 3, 4, 5, 6, 7, 8, 9].indexOf(Number(this.value)) === -1) {
                this.value = '';

                return;
            }

            for (const input of inputs) {
                input.removeAttribute('aria-invalid');
            }

            for (const input of inputs) {
                const {group, horizontal, vertical} = map.get(input);

                check(group);
                check(horizontal);
                check(vertical);
            }

            clearTimeout(key);
            key = setTimeout(() => {
                let isClear = true;

                for (const input of inputs) {
                    const {ans} = map.get(input);

                    if (
                        input.value !== ans ||
                        input.disabled
                    ) {
                        isClear = false;
                    }
                }

                if (
                    isClear &&
                    !self.isCleared
                ) {
                    for (const input of inputs) {
                        input.readOnly = true;
                    }

                    self.isCleared = true;
                    self.dispatchEvent(new Event('gameclear'));
                }
            }, 200);
        };
        const focusHandler = function (e) {
            const {key} = e;

            switch (key.replace(/^Arrow/, '')) {
            case 'Right':
                move.horizontal.call(this, e, true);

                break;
            case 'Left':
                move.horizontal.call(this, e, false);

                break;
            case 'Down':
                move.vertical.call(this, e, true);

                break;
            case 'Up':
                move.vertical.call(this, e, false);

                break;

            default:
                break;
            }
        };
        const unloadEvent = () => {
            window.addEventListener('beforeunload', function(e) {
                e.returnValue = '状況は保存されません';
            });

            for (const input of inputs) {
                input.removeEventListener('change', unloadEvent);
            }
        }

        for (const input of inputs) {
            input.addEventListener('keyup', editHandler);
            input.addEventListener('change', editHandler);
            input.addEventListener('keydown', focusHandler);
            input.addEventListener('change', unloadEvent);
        }
    }

    // 負荷がかかる処理のため計算が終わるのをPromiseで待つ
    #setNum = async function () {
        const inputNum = num => new Promise(resolve => {
            const values = [1, 2, 3, 4, 5, 6, 7, 8, 9];
            const duplicateCheck = (inputSet, loop) => {
                let value = [];

                for (const input of inputSet) {
                    if (input.value) {
                        value.push(input.value);
                    }
                }

                value = value.join('');

                for (const input of inputSet) {
                    if (input.value) {
                        const result = value.match(new RegExp(input.value, 'g'));

                        if (result && result.length !== 1) {
                            loop();

                            return;
                        }
                    }
                }
            };
            const loop = () => {
                const input = this.#inputs[num];
                const {vertical, horizontal, group} = this.#map.get(input);
                const result = this.#getVal(values);

                input.value = result;

                // 3x3グループの検査
                duplicateCheck(group, loop);
                // 列の検査
                duplicateCheck(vertical, loop);
                // 行の検査
                duplicateCheck(horizontal, loop);

                // 完了
                resolve(result);
            };

            loop();
        });

        // 数の決定
        await new Promise(async resolve => {
            const loop = async () => {
                const {length} = this.#inputs;

                for (const input of this.#inputs) {
                    input.value = '';
                }

                for (let j = 0; j < length; j++) {
                    const result = await inputNum(j);

                    if (!result) {
                        await loop();

                        return;
                    }
                }
            };

            await loop();
            resolve();
        });
    }

    #saveAnswer = function () {
        const inputs = [...this.#inputs];

        for (const input of inputs) {
            const data = this.#map.get(input);

            data.ans = input.value;
        }

        for (let i = 0; i < 35; i++) {
            const input = this.#getVal(inputs);

            input.readOnly = true;
            input.dataset.isHint = 'true';
        }
    }

    reset() {
        for (const input of this.#inputs) {
            input.removeAttribute('aria-invalid');
            input.disabled = false; // showAnswerの後始末

            if (input.dataset.isHint !== 'true') {
                input.value = '';
                input.readOnly = false; // clear後のリセットのため
            }
        }

        this.isCleared = false;
    }

    async init() {
        return new Promise(async (resolve, reject) => {
            for (const input of this.#inputs) {
                input.readOnly = false;
            }

            if (this.#loading) {
                reject(new TypeError('Now loading'));
                return;
            }

            this.#loading = true;
            await this.#setNum();
            this.#saveAnswer();
            this.reset();
            this.dispatchEvent(new Event('load'));
            this.#loading = false;

            resolve(null);
        });
    }

    removeIncorrects() {
        for (const input of this.#inputs) {
            const {ans} = this.#map.get(input);

            if (
                input.value &&
                input.value !== ans
            ) {
                input.setAttribute('aria-invalid', 'true');
                input.value = '';
            }
        }
    }

    showAnswer() {
        for (const input of this.#inputs) {
            const {ans} = this.#map.get(input);

            input.removeAttribute('aria-invalid');
            input.value = ans;
            input.disabled = true;
        }
    }
}

customElements.define('number-place', NumberPlace);


// その他のUI
{
    const app = document.getElementById('app');

    app.addEventListener('gameclear', () => {
        alert('Game clear!');
    });

    document.getElementById('reset').addEventListener('click', () => {
        if (!window.confirm('リセットしていいですか?')) {
            return;
        }

        app.reset();
    });

    document.getElementById('removeIncorrects').addEventListener('click', () => {
        if (!window.confirm('誤った入力を破棄していいですか?')) {
            return;
        }

        app.removeIncorrects();
    });

    document.getElementById('ans').addEventListener('click', () => {
        if (!window.confirm('現在の状況が破棄されます。解答を表示していいですか?')) {
            return;
        }

        app.showAnswer();
    });

    document.getElementById('new').addEventListener('click', () => {
        if (!window.confirm('新しい問題に切り替えていいですか?')) {
            return;
        }

        app.init();
    });
}

              
            
!
999px

Console