JSFiddle - React, Tailwind, and code Playground

by Gwyn Milcote

HTML

<h3>NQ Editor</h3>

<div id='editor-tab-buttons'>
    <div class='tab-button' data-tab='maps'>Maps</div>
    <div class='tab-button' data-tab='doors'>Doors</div>
    <div class='tab-button' data-tab='npcs'>NPCs</div>
    <div class='tab-button' data-tab='enemies'>Enemies</div>
    <div class='tab-button' data-tab='items'>Items</div>
</div>

<p>Click on a tile to 'paint' it with the currently-selected terrain.</p>

Width: <input type='number' id='select-width' min='10' max='100' value='100'> 
Height: <input type='number' id='select-height' min='10' max='100' value='100'> 
<button id='new-map'>Generate</button> &nbsp; &nbsp; &nbsp; 
Tile size: <input type='number' id='select-tileSize' min='10' max='40' value='20'>

<div id='terrain-container'></div>



<div id='map-container'></div>

<p><button id='import-button'>Import</button> - paste your JSON map object here</p>
<textarea id='import-map'></textarea>

<p><button id='export-button'>Export</button> - copy this</p>
<textarea id='export-map'></textarea>

CSS

div, img, p, a, button, input {
    box-sizing: border-box;
}

input {
    width: 60px;
}
textarea {
    width: 90%;
    height: 100px;
}

#map-container {
    width: 100%;
    padding: 20px;
    min-height: 150px;
    max-height: 400px;
    overflow: auto;
    background-color: silver;
    text-align: center;
}
#map-border {
    border: 2px solid black;
    line-height: 0px;
    margin: auto;
}

#map-border .tile {
    display: inline-block;
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);
    background-size: cover;
}
#map-border .tile:hover {
    cursor: pointer;
    box-shadow: inset 0 0 0 1px #000;
}
#map-border .selected-tile {
    box-shadow: inset 0 0 0 1px #fff;
}

#terrain-container {
    width: 100%;
    height: 100px;
    overflow: auto;
    background-color: silver;
    margin: 20px auto;
}
#terrain-container .terrain {
    width: 40px;
    height: 40px;
    display: inline-block;
    margin: 5px;
        box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.3);

}
#terrain-container .terrain:hover {
    cursor: pointer;
    box-shadow: inset 0 0 0 1px #000;
}
#terrain-container .selected-terrain {
    box-shadow: inset 0 0 0 1px #fff;
}

JavaScript

const path = "http://images.neopets.com/nq/tp/",
      urls = [
          "grassland",
          "water_iso",
          "forest",
          "mountain",
          "hills",
          "jungle",
          "desert",
          "dirt",
          "city",
          "castle",
          "ruins",
          "cave",
          "stone",
          "swamp",
          "snow",
          "cave_ent",
          "cave_exit",
          "cave_up",
          "cave_down",
          "dungeon",
          "dungeon_barrel",
          "dungeon_crate",
          "dungeon_chair",
          "dungeon_table",
          "dungeon_pillar",
          "dungeon_crate",
          "dungeon_bed",
          "dungeon_portal",
          "dungeon_door",
          "dungeon_up",
          "dungeon_down"
      ];

class MapEditor {

    constructor(){
        this.rows = 10;
        this.cols = 10;
        this.tileSize = 20;
        this.terrain = 1;
        this.selectedTile = {x: 0, y: 0};
        
        this.terrainOptions();
        this.blankMap();
    }
    
    setSize(w,h){
        this.rows = h;
        this.cols = w;
    }
    
    setTileSize(px){
        this.tileSize = px;
        let c = this.getContainerSize();
        $("#map-border").css("height", c.h + "px").css("width", c.w + "px");
        $("#map-border .tile").css("height", px + "px").css("width", px + "px");
    }
    
    // Add 4px to account for the 2px-thick borders
    getContainerSize(){
        return {
            w: (this.cols * this.tileSize) + 4,
            h: (this.rows * this.tileSize) + 4
        };
    }
    
    blankMap(terrain = this.terrain){
        this.map = [];
        for(let y = 0; y < this.rows; y++){
            let row = [];
            for(let x = 0; x < this.cols; x++){
                row.push(terrain);
            }
            this.map.push(row);
        }
        this.renderMap();
    }

    renderMap(){
        let c = this.getContainerSize();
        let html = "<div id='map-border' style='width:" + c.w...