<!--

  # Javascript Sudoku Puzzle Generator

  Generation process is handled in a producer wrapped in
  a worker that is embedded in html.

  API is designed to be pluggable, but needs more
  interface to support this. Currently it responds
  with an array of rows.

  JS code includes:
    - a utility object, 
    - an adapter that acts as the communication layer
      between producer and consumer (app)
    - app singleton that includes the rendering logic

-->

<div id='sudoku-app'></div>

<!-- https://github.com/scriptype/bem -->
<script type='text/javascript'>
  "use strict";!function(a){if("function"==typeof bootstrap)bootstrap("bem",a);else if("object"==typeof exports&&"object"==typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define(a);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeBem=a}else{if("undefined"==typeof window&&"undefined"==typeof self)throw new Error("This environment was not anticipated by bem. Please file a bug.");var b="undefined"!=typeof window?window:self,c=b.bem;b.bem=a(),b.bem.noConflict=function(){return b.bem=c,this}}}(function(){function a(a){"undefined"!=typeof a.modifier&&(c.modifier=a.modifier),"undefined"!=typeof a.element&&(c.element=a.element)}function b(a){if(!d.validate(a))return null;var b=a.block,e=a.element,f=a.modifiers,g=b,h=[];return!!e&&(g+=""+c.element+e),!!f&&Object.keys(f).forEach(function(a){var d=f[a],i="function"==typeof d?d(b,e,f):d;!!i&&h.push(""+g+c.modifier+a+" ")}),(g+" "+h.join("")).slice(0,-1)}var c={element:"__",modifier:"--"},d={messages:{block:"You must specify the name of block.",element:"Element name must be a string.",modifier:"Modifiers must be supplied in the `{name : bool || fn}` style."},blockName:function(a){return"undefined"!=typeof a&&"string"==typeof a&&a.length?!0:(console.warn(this.messages.block),!1)},element:function(a){return"undefined"!=typeof a&&"string"!=typeof a?(console.warn(this.messages.element),!1):!0},modifiers:function(a){return"undefined"==typeof a||"object"==typeof a&&"[object Object]"===toString.call(a)?!0:(console.warn(this.messages.modifier),!1)},validate:function(a){return this.blockName(a.block)&&this.element(a.element)&&this.modifiers(a.modifiers)}};return{setDelimiters:a,makeClassName:b}});
</script>

<!-- Include Babel to transform code in browser -->
<script type='text/javascript'
        src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser-polyfill.min.js'>
</script>
<script type='text/javascript'
        src='https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser.min.js'>
</script>

<!-- worker, sudoku api -->
<script type='text/babel' id='worker'>
  
  self.sudoku = null
  
  // Worker Setup
  self.addEventListener('message', (event) => {
    var options = { method: null }
    try {
      options = JSON.parse(event.data);
    } catch (e) {
      console.warn('event.data is misformed', event)
    }
  
    switch (options.method) {
  
      case 'generate':
        var { hints, limit } = options
        self.sudoku = new Sudoku(hints, limit).generate()

        self.postMessage({
          success: self.sudoku.success,
          board: self.sudoku.getBoard(),
          solution: self.sudoku.getSolution()
        });
        break;
  
      case 'validate':
        var { map, number, index } = options
        self.postMessage({
          result: sudoku.validate(map, number, index)
        });
        break;
  
    }
  }, false);

  // API
  class Sudoku {
    constructor(hints, limit) {
      this.hints = hints
      this.limit = limit || 10000
  
      this._logs = {
        raw: [],
        incidents: {
          limitExceeded: 0,
          notValid: 0,
          noNumbers: 0
        }
      }
  
      this.success = null

      this.numbers = () =>
        new Array(9)
          .join(" ")
          .split(" ")
          .map((num , i) => i + 1)

      /*
        Will be used in initial map. Each row will be
        consisted of randomly ordered numbers
      */
      this.randomRow = () => {
        var row = []
        var numbers = this.numbers()
        while (row.length < 9) {
          var index = Math.floor(Math.random() * numbers.length)
          row.push(numbers[index])
          numbers.splice(index, 1)
        }

        return row
      }

      /*
        This is the dummy placeholder for the
        final results. Will be overridden through the
        backtracking process, and at the and, this will
        be the real results.
      */
      this.result = new Array(9 * 9)
        .join(" ")
        .split(" ")
        .map(entry => null)

      /*
        Will be used as the nodeTree in the
        process of backtracking. Each cell has 9 alternative
        paths (randomly ordered).
      */
      this.map = new Array(9 * 9)
        .join(" ")
        .split(" ")
        .map(path => this.randomRow())

      /*
        Will be used as history in the backtracking
        process for checking if a candidate number is valid.
      */
      this.stack = []

      return this
    }
  
    toRows(arr) {
      var row = 0
      var asRows = new Array(9)
        .join(" ")
        .split(" ")
        .map(row => [])
  
      for (let [index, entry] of arr.entries()) {
        asRows[row].push(entry)

        if ( !((index + 1) % 9) ) {
          row += 1
        }
      }

      return asRows
    }

    no(path, index, msg) {
      var number = path[path.length - 1]
      this._logs.raw.push(`no: @${index} [${number}] ${msg} ${path} `)
    }

    yes(path, index) {
      this._logs.raw.push(`yes: ${index} ${path}`)
    }
  
    finalLog() {
      console.groupCollapsed('Raw Logs')
      console.groupCollapsed(this._logs.raw)
      console.groupEnd()
      console.groupEnd()
      console.groupCollapsed('Incidents')
      console.groupCollapsed(this._logs.incidents)
      console.groupEnd()
      console.groupEnd()
    }

    getBoard() {
      return this.toRows(this.substractCells())
    }

    getSolution() {
      return this.toRows(this.result)
    }

    substractCells() {
      var _getNonEmptyIndex = () => {
        var index = Math.floor(Math.random() * _result.length)
        return _result[index] ? index : _getNonEmptyIndex()
      }

      var _result = this.result.filter(() => true)

      while (
        _result.length - this.hints >
        _result.filter(n => !n).length
      ) {
        _result[_getNonEmptyIndex()] = ''
      }

      return _result
    }
  
    validate(map, number, index) {
      var rowIndex = Math.floor(index / 9)
      var colIndex = index % 9

      var row = map.slice(
        rowIndex * 9, 9 * (rowIndex + 1)
      )

      var col = map.filter((e, i) =>
        i % 9 === colIndex
      )

      var boxRow = Math.floor(rowIndex / 3)
      var boxCol = Math.floor(colIndex / 3)

      var box = map.filter((e, i) =>
        Math.floor(Math.floor(i / 9) / 3) === boxRow &&
        Math.floor((i % 9) / 3) === boxCol
      )

      return {
        row: {
          first: row.indexOf(number),
          last: row.lastIndexOf(number)
        },
        col: {
          first: col.indexOf(number),
          last: col.lastIndexOf(number)
        },
        box: {
          first: box.indexOf(number),
          last: box.lastIndexOf(number)
        }
      }
    }

    _validate(map, index) {
      if (!map[index].length) {
        return false
      }

      this.stack.splice(index, this.stack.length)
  
      var path = map[index]
      var number = path[path.length - 1]
  
      var didFoundNumber = this.validate(this.stack, number, index)
  
      return (
        didFoundNumber.col.first === -1 &&
        didFoundNumber.row.first === -1 &&
        didFoundNumber.box.first === -1
      )
    }

    _generate(map, index) {
      if (index === 9 * 9) {
        return true
      }

      if (--this.limit < 0) {
        this._logs.incidents.limitExceeded++
        this.no(map[index], index, 'limit exceeded')
        return false
      }

      var path = map[index]

      if (!path.length) {
        map[index] = this.numbers()
        map[index - 1].pop()
        this._logs.incidents.noNumbers++
        this.no(path, index, 'no numbers in it')
        return false
      }

      var currentNumber = path[path.length - 1]

      var isValid = this._validate(map, index)
      if (!isValid) {
        map[index].pop()
        map[index + 1] = this.numbers()
        this._logs.incidents.notValid++
        this.no(path, index, 'is not valid')
        return false
      } else {
        this.stack.push(currentNumber)
      }

      for (let number of path.entries()) {
        if (this._generate(map, index + 1)) {
          this.result[index] = currentNumber
          this.yes(path, index)
          return true
        }
      }

      return false
    }

    generate() {
      if (this._generate(this.map, 0)) {
        this.success = true
      }

      this.finalLog()

      return this
    }

  }
</script>
/**********************************\
--------- Sudoku App Styles --------

  Contents:
  - Global definitions
  - Modules

  Each module may have its own
  variables, mixins, animations and
  media queries.

  This is the convention followed
  in structuring rules

  // ==================
  // Foo
  // ==================
  
  // Variables (Variables of Foo)
  [...]

  // Lorem (Lorem of foo)
  [...]
  
  // ==================
  // Bar
  // ==================
  
  [...]

\**********************************/

// ==================
// Global
// ==================

// Fonts
@font-face {
  src: url("http://enes.in/GillSansTr-LightNr.otf");
  font-family: Gill;
  font-weight: 100
}

@font-face {
  src: url("http://enes.in/GillSansTr-Normal.otf");
  font-family: Gill;
  font-weight: 300
}

@font-face {
  src: url("http://enes.in/GillSansTr-Bold.otf");
  font-family: Gill;
  font-weight: 600
}

@font-face {
  src: url("http://enes.in/GillSansTr-ExtraBold.otf");
  font-family: Gill;
  font-weight: 700
}

@font-face {
  src: url("http://enes.in/GillSansTr-UltraBold.otf");
  font-family: Gill;
  font-weight: 900
}

html, body {
  width: 100%;
  height: 100%
}

body {
  margin: 0;
  background: #f0f0f0
}

// Variables
$g-transition-duration: .2s;
$g-breakpoint-xs: 260px;
$g-breakpoint-sm: 420px;
$g-breakpoint-md: 615px;

// Responsive
@media (max-width: $g-breakpoint-xs) {
  .show-on-sm { display: none; }
  .show-on-md { display: none; }
  .show-on-lg { display: none; }
  .show-on-xs { display: block; }
}

@media (max-width: $g-breakpoint-sm) {
  .show-on-xs { display: none; }
  .show-on-md { display: none; }
  .show-on-lg { display: none; }
  .show-on-sm { display: block; }
}

@media (min-width: $g-breakpoint-sm + 1)
  and (max-width: $g-breakpoint-md) {
  .show-on-xs { display: none; }
  .show-on-sm { display: none; }
  .show-on-lg { display: none; }
  .show-on-md { display: block; }
}

@media (min-width: $g-breakpoint-md) {
  .show-on-xs { display: none; }
  .show-on-sm { display: none; }
  .show-on-md { display: none; }
  .show-on-lg { display: block; }
}

// Animations
@keyframes progress {
    0%   {
      box-shadow: none;
    }
    25%  {
      box-shadow: 2px -2px 0 1px;
    }
    50%  {
      box-shadow: 2px -2px 0 1px,
                  7px -2px 0 1px;
    }
    100% {
      box-shadow: 2px -2px 0 1px,
                  7px -2px 0 1px,
                  12px -2px 0 1px;
    }
}

.fr { float: right; }
.fl { float: left; }

// ==================
// Modules
// ==================

// ==================
// CTA Button
// ==================

// Variables
$button-primary-color: lighten(desaturate(blue, 35%), 5%);
$button-secondary-color: lighten(desaturate(red, 35%), 5%);
$button-tertiary-color: #2ECC40;
$button-neutral-color: #333;
$button-disabled-color: #bbb;
$button-border-radius: 3px;

// Mixins
@mixin button-base(
  $font-size,
  $margin,
  $padding,
  $loading-padding-right) {
    .button {
      padding: $padding;
      font-size: $font-size;
      
      &:not(:last-of-type) {
        margin-right: $margin;
      }
      
      &--loading {
        padding-right: $loading-padding-right
      }
    }
}

// Responsive
@media (max-width: $g-breakpoint-xs) {
  @include button-base(
    .6em,
    .15em,
    .25em .5em,
    1.5em);
}

@media (min-width: $g-breakpoint-xs + 1)
  and (max-width: $g-breakpoint-sm) {
  @include button-base(
    .75em,
    .25em,
    .25em .5em .15em,
    1.5em);
}

@media (min-width: $g-breakpoint-sm + 1)
  and (max-width: $g-breakpoint-md) {
  @include button-base(
    .9em,
    .5em,
    .5em .75em .4em,
    1.5em);
}

@media (min-width: $g-breakpoint-md) {
  @include button-base(
    1em,
    .75em,
    .75em 1em .6em,
    1.5em);
}

// Component
.button {
  border: 1px solid;
  font-weight: normal;
  border-radius: $button-border-radius;
  background: none;
  box-shadow: none;
  transition: all $g-transition-duration;
  
  &--primary {
    color: $button-primary-color;
    font-weight: 600;
  
    &:hover,
    &:focus,
    &:active {
      border-color: $button-primary-color;
      background: $button-primary-color;
    }
    
    &:focus {
      box-shadow: 0 0 5px $button-primary-color;
    }
  }
  
  &--secondary {
    color: $button-secondary-color;
  
    &:hover,
    &:focus,
    &:active {
      border-color: $button-secondary-color;
      background: $button-secondary-color;
    }
    
    &:focus {
      box-shadow: 0 0 5px $button-secondary-color;
    }
  }
  
  &--tertiary {
    color: #fff;
    border-color: $button-tertiary-color;
    background: $button-tertiary-color;
  }
  
  &--neutral {
    color: $button-neutral-color;
  
    &:hover,
    &:focus,
    &:active {
      border-color: $button-neutral-color;
      background: $button-neutral-color;
    }
    
    &:focus {
      box-shadow: 0 0 5px $button-neutral-color;
    }
  }
  
  &--compound {
    border-radius: 0;
    border-right: none;
    
    &-first {
      border-bottom-left-radius: $button-border-radius;
      border-top-left-radius: $button-border-radius;
    }
    
    &-last {
      border-bottom-right-radius: $button-border-radius;
      border-top-right-radius: $button-border-radius;
      border-right: 1px solid;
    }
  }
  
  &--muted {
    pointer-events: none;
  }

  &--disabled {
    border-color: $button-disabled-color;
    color: $button-disabled-color;
    pointer-events: none;
  }
  
  &--loading {
    &-text::after {
      display: inline-block;
      width: 1px;
      height: 1px;
      content: '';
      box-shadow: 2px -2px 1px 0;
      animation: progress 1s infinite;
    }
  }
  
  &:hover,
  &:focus,
  &:active {
    color: #fff;
  }
  
  &:focus {
    outline: none;
  }
  
  &:active {
    box-shadow: inset 0 -2px 10px rgba(#000, .4);
  }
}

// ==================
// Feedback Messages
// ==================

// Component
.message {
  font-size: .9em;
  padding: 2em;
  margin: 0;
  border-radius: 3px;
  color: rgba(black, .75);
  
  &--busy {
    background: rgba(blue, .1)
  }
  
  &--fail {
    background: rgba(red, .1)
  }
}

// ==================
// Sudoku Table
// ==================

// Variables
$sudoku-color: #444;

// Mixins
@mixin sudoku-base(
  $thin-border,
  $thick-border,
  $cell-size,
  $font-size,
  $title-size,
  $padding-around,
  $header-padding
  ) {
  .sudoku {
    margin: 0 auto;
    padding-top: $padding-around;
    padding-bottom: $padding-around;
    
    &__header {
      padding-bottom: $header-padding
    }
    
    &__title {
      font-size: $title-size
    }
    
    &__table {
      font-size: $font-size;
      border-top: $thick-border;
      border-left: $thick-border;
      border-collapse: collapse;
  
      &-row {
        border-bottom: $thin-border;
        border-right: $thick-border;

        &--separator {
          border-bottom: $thick-border;
        }
      }

      &-cell {
        width: $cell-size;
        height: $cell-size;
        border-right: $thin-border;

        &--separator {
          border-right: $thick-border;
        }
      }
    }
  }
}

// Responsive
@media (max-width: $g-breakpoint-xs) {
  @include sudoku-base(
    1px solid $sudoku-color,
    2px solid $sudoku-color,
    16px, .9em,  1em, .5em, .6em);
  
  .sudoku {
    max-width: calc(#{$g-breakpoint-xs} / 1.5);
    min-width: calc(#{$g-breakpoint-xs} / 2);
  }
}

@media (min-width: $g-breakpoint-xs + 1)
  and  (max-width: $g-breakpoint-sm) {
  @include sudoku-base(
    1px solid $sudoku-color,
    3px solid $sudoku-color,
    32px, 1.2em, 1.2em, 1em, .9em);
  
  .sudoku {
    width: $g-breakpoint-xs;
  }
}

@media (min-width: $g-breakpoint-sm + 1)
  and (max-width: $g-breakpoint-md) {
  @include sudoku-base(
    1px solid $sudoku-color,
    4px solid $sudoku-color,
    48px, 1.5em, 1.5em, 2em, 1.3em);
  
  .sudoku {
    width: $g-breakpoint-sm
  }
}

@media (min-width: $g-breakpoint-md) {
  @include sudoku-base(
    2px solid $sudoku-color,
    6px solid $sudoku-color,
    64px, 1.75em, 2em, 3em, 1.618em);
  
  .sudoku {
    width: $g-breakpoint-md
  }
}

// Component
.sudoku {
  color: $sudoku-color;
  
  &__header {
    font-family: Gill, sans-serif;
  }
  
  &__title {
    font-weight: 600
  }
  
  &__description {
    max-width: 640px;
    line-height: 1.4;
    font-weight: 100
  }
  
  &__table {
    background: #fff;
    
    &-row {}

    &-cell {
      overflow: hidden;
      text-align: center;
      transition: all .25s;

      &--editable {
        color: desaturate(blue, 25);
        
        &:focus {
          background: rgba(blue, .1);
          outline: none;
        }
      }
      
      &--error {
        color: red;
        background: #fdd;
      }
      
      &--editable-error {
        text-shadow: 0 0 15px;
        
        &:focus {
          color: #eee;
          background: #f45;
        }
      }
    }
    
  }
}
View Compiled
// Utility
var utils = (() => {
  function dom (selector) {
    if (selector[0] === '#') {
      return document.getElementById(selector.slice(1))
    }
    return document.querySelectorAll(selector)
  }
  
  function copyJSON (obj) {
    return JSON.parse(JSON.stringify(obj))
  }
  
  function isTouchDevice () {
    return navigator.userAgent
      .match(/(iPhone|iPod|iPad|Android|BlackBerry)/)
  }
  
  function getWorkerURLFromElement(selector) {
    var element = dom(selector)
    var content = babel.transform(element.innerText).code
    var blob = new Blob([content], {type: 'text/javascript'})
    return URL.createObjectURL(blob)
  }

// Will be used for restoring caret positions on rerenders.
// Taken from:
// http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
  var cursorManager = (function () {
    var cursorManager = {}

      var voidNodeTags = [
        'AREA', 'BASE', 'BR', 'COL', 'EMBED',
        'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK',
        'MENUITEM', 'META', 'PARAM', 'SOURCE',
        'TRACK', 'WBR', 'BASEFONT', 'BGSOUND',
        'FRAME', 'ISINDEX'
      ];

      Array.prototype.contains = function(obj) {
          var i = this.length;
          while (i--) {
              if (this[i] === obj) {
                  return true;
              }
          }
          return false;
      }

      function canContainText(node) {
          if(node.nodeType == 1) {
              return !voidNodeTags.contains(node.nodeName);
          } else {
              return false;
          }
      };

      function getLastChildElement(el){
          var lc = el.lastChild;
          while(lc && lc.nodeType != 1) {
              if(lc.previousSibling)
                  lc = lc.previousSibling;
              else
                  break;
          }
          return lc;
      }
      cursorManager.setEndOfContenteditable = function(contentEditableElement) {

          while(getLastChildElement(contentEditableElement) &&
                canContainText(getLastChildElement(contentEditableElement))) {
              contentEditableElement = getLastChildElement(contentEditableElement);
          }

          var range,selection;
          if(document.createRange) {
              range = document.createRange();
              range.selectNodeContents(contentEditableElement);
              range.collapse(false);
              selection = window.getSelection();
              selection.removeAllRanges();
              selection.addRange(range);
          }
          else if(document.selection)
          { 
              range = document.body.createTextRange();
              range.moveToElementText(contentEditableElement);
              range.collapse(false);
              range.select();
          }
      }

      return cursorManager
  })()
  
  return {
    copyJSON, cursorManager, dom,
    getWorkerURLFromElement, isTouchDevice
  }
})();


// API Adapter
class SudokuAdapter {
  constructor(url) {
    this.worker = new Worker(url)
    return this
  }
  
  _postMessage(options) {
    this.worker.postMessage(JSON.stringify(options))
    return new Promise((resolve, reject) => {
      this.worker.onmessage = event => {
        resolve(event.data)
      }
    })
  }
  
  generate(options) {
    options = Object.assign
      ({}, options, { method: 'generate' })
    
    return this._postMessage(options)
  }
  
  validate(options) {
    options = Object.assign
      ({}, options, { method: 'validate' })
    
    return this._postMessage(options)
  }
}


// Client Side Settings
const SUDOKU_APP_CONFIG = {
  HINTS: 34,
  TRY_LIMIT: 100000,
  WORKER_URL: utils.getWorkerURLFromElement('#worker'),
  DOM_TARGET: utils.dom('#sudoku-app')
}


// Client Side
var SudokuApp = (config => {
  const {
    HINTS, TRY_LIMIT,
    WORKER_URL, DOM_TARGET
  } = config
  
  var sudokuAdapter = new SudokuAdapter(WORKER_URL)
  
  var state = {
    success: null,
    board: null,
    solution: null,
    solved: null,
    errors: []
  };
  Object.observe(state, render)
  
  var history = [state]
  var historyStash = []
  
  
  // Event listeners
  var onClickGenerate = initialize
  
  var onClickSolve = function () {
    setState({
      board: state.solution,
      solved: true,
      errors: []
    })
  }
  
  var onKeyUpCell = function (event) {
    var key = event.keyCode
    if (            // a
      key === 36 || // r
      key === 37 || // r
      key === 38 || // o
      key === 39 || // w
      key === 9  || // tab
      // mod key flags are always false in keyup event
      // keyIdentifier doesn't seem to be implemented
      // in all browsers
      key === 17 || // Control
      key === 16 || // Shift
      key === 91 || // Meta
      key === 19 || // Alt
      event.keyIdentifier === 'Control' ||
      event.keyIdentifier === 'Shift'   ||
      event.keyIdentifier === 'Meta'    ||
      event.keyIdentifier === 'Alt'     
    ) return
    
    var cell = event.target
    var value = cell.innerText
    
    if (value.length > 4) {
      cell.innerText = value.slice(0, 4)
      return false
    }
    
    var cellIndex = cell.getAttribute('data-cell-index')
    cellIndex = parseInt(cellIndex, 10)
    var rowIndex = Math.floor(cellIndex / 9)
    var cellIndexInRow = cellIndex - (rowIndex * 9)
    
    var board = Object.assign([], state.board)
    board[rowIndex].splice(cellIndexInRow, 1, value)
    
    validate(board).then(errors => {
      historyStash = []
      history.push({})
      var solved = null
      if (errors.indexOf(true) === -1) {
        solved = true
        board.forEach(row => {
          row.forEach(value => {
            if (!value || !parseInt(value, 10) || value.length > 1) {
              solved = false
            }
          })
        })
      }
      if (solved) {
        board = Object.assign([], board).map(row => row.map(n => +n))
      }
      setState({ board, errors, solved }, (newState) => {
        history[history.length - 1] = newState
        restoreCaretPosition(cellIndex)
      })
    })
  }
  
  function keyDown (event) {
    var keys = {
      ctrlOrCmd: event.ctrlKey || event.metaKey,
      shift: event.shiftKey,
      z: event.keyCode === 90
    }
    
    if (keys.ctrlOrCmd && keys.z) {
      if (keys.shift && historyStash.length) {
        redo()
      } else if (!keys.shift && history.length > 1) {
        undo()
      }
    }
  }
  
  function undo () {
    historyStash.push(history.pop())
    setState(utils.copyJSON(history[history.length - 1]))
  }
  
  function redo () {
    history.push(historyStash.pop())
    setState(utils.copyJSON(history[history.length - 1]))
  }
  
  
  function initialize () {
    unbindEvents()
    render()
    getSudoku().then(sudoku => {
      setState({
        success: sudoku.success,
        board: sudoku.board,
        solution: sudoku.solution,
        errors: [],
        solved: false
      }, newState => {
        history = [newState]
        historyStash = []
      })
    })
  }
  
  function setState(newState, callback) {
    requestAnimationFrame(() => {
      Object.assign(state, newState)
      if (typeof callback === 'function') {
        var param = utils.copyJSON(state)
        requestAnimationFrame(callback.bind(null, param))
      }
    })
  }
  
  function bindEvents() {
    var generateButton = utils.dom('#generate-button')
    var solveButton = utils.dom('#solve-button')
    var undoButton = utils.dom('#undo-button')
    var redoButton = utils.dom('#redo-button')
    generateButton &&
      generateButton
        .addEventListener('click', onClickGenerate)
    solveButton &&
      solveButton
        .addEventListener('click', onClickSolve)
    undoButton &&
      undoButton
        .addEventListener('click', undo)
    redoButton &&
      redoButton
        .addEventListener('click', redo)
    
    var cells = utils.dom('.sudoku__table-cell')
    ;[].forEach.call(cells, (cell) => {
      cell.addEventListener('keyup', onKeyUpCell)
    })
    
    window.addEventListener('keydown', keyDown)
  }
  
  function unbindEvents() {
    var generateButton = utils.dom('#generate-button')
    var solveButton = utils.dom('#solve-button')
    var undoButton = utils.dom('#undo-button')
    var redoButton = utils.dom('#redo-button')
    generateButton &&
      generateButton
        .removeEventListener('click', onClickGenerate)
    solveButton &&
      solveButton
        .removeEventListener('click', onClickSolve)
    undoButton &&
      undoButton
        .removeEventListener('click', undo)
    redoButton &&
      redoButton
        .removeEventListener('click', redo)
    
    var cells = utils.dom('.sudoku__table-cell')
    ;[].forEach.call(cells, (cell) => {
      cell.removeEventListener('keyup', onKeyUpCell)
    })
    
    window.removeEventListener('keydown', keyDown)
  }
  
  function restoreCaretPosition(cellIndex) {
    utils.cursorManager.setEndOfContenteditable(
      utils.dom(`[data-cell-index="${ cellIndex }"]`)[0]
    )
  }
  
  function getSudoku() {
    return sudokuAdapter.generate({
      hints: HINTS,
      limit: TRY_LIMIT
    })
  }
  
  function validate(board) {
    var map = board.reduce((memo, row) => {
      for (let num of row) {
        memo.push(num)
      }
      return memo
    }, []).map((num) => parseInt(num, 10))
    
    var validations = []
    
    // Will validate one by one
    for (let [index, number] of map.entries()) {
      if (!number) {
        validations.push(
          new Promise(res => {
            res({ result: { box: -1, col: -1, row: -1 } })
          })
        )
      } else {
        let all = Promise.all(validations)
        validations.push(all.then(() => {
          return sudokuAdapter.validate({map, number, index})
        }))
      }
    }
    
    return Promise.all(validations)
      .then(values => {
        var errors = []
        for (let [index, validation] of values.entries()) {
          let { box, col, row } = validation.result
          let errorInBox = box.first !== box.last
          let errorInCol = col.first !== col.last
          let errorInRow = row.first !== row.last

          let indexOfRow = Math.floor(index / 9)
          let indexInRow = index - (indexOfRow * 9)
          
          errors[index] = errorInRow || errorInCol || errorInBox
        }

        return errors
      })
  }
  
  function render() {
    unbindEvents()

    DOM_TARGET.innerHTML = `
      <div class='sudoku'>
        ${ headerComponent() }
        ${ contentComponent() }
      </div>
    `
    
    bindEvents()
  }
  
  function buttonComponent(props) {
    var { id, text, mods, classes } = props
    
    var blockName = 'button'
    var modifiers = {}
    var modType = toString.call(mods)
    if (modType === '[object String]') {
      modifiers[mods] = true
      
    } else if (modType === '[object Array]') {
      for (let modName of mods) {
        modifiers[modName] = true
      }
    }
    
    var blockClasses = bem.makeClassName({
      block: blockName,
      modifiers: modifiers
    });
    
    var buttonTextClass = `${blockName}-text`
    if (Object.keys(modifiers).length) {
      buttonTextClass += (
        Object.keys(modifiers).reduce((memo, curr) => {
          return memo + ` ${blockName}--${curr}-text`
        }, '')
      )
    }
    
    var lgText = typeof text === 'string' ?
        text : text[0]
    var mdText = typeof text === 'string' ?
        text : text[1]
    var smText = typeof text === 'string' ?
        text : text[2]
    
    return (`
      <button
        id='${ id }'
        class='${ blockClasses } ${ classes || "" }'>
        <span class='show-on-sm ${buttonTextClass}'>
          ${ smText }
        </span>
        <span class='show-on-md ${buttonTextClass}'>
          ${ mdText }
        </span>
        <span class='show-on-lg ${buttonTextClass}'>
          ${ lgText }
        </span>
      </button>
    `)
  }
  
  function messageComponent(options) {
    var { state, content } = options
    
    var messageClass = bem.makeClassName({
      block: 'message',
      modifiers: state ? {
        [state]: true
      } : {}
    })
    
    return (`
      <p class='${ messageClass }'>
        ${ content }
      </p>
    `)
  }
  
  function descriptionComponent(options) {
    var { className, infoLevel } = options
    
    var technical = `
      In this demo,
      <a href='https://en.wikipedia.org/wiki/Backtracking'>
        backtracking algorithm
      </a> is used for <em>generating</em>
      the sudoku.`
    
    var description = `
      Difficulty and solvability is
      totally random as I randomly left a certain number of hints
      from a full-filled board.
    `
    
    if (infoLevel === 'full') {
      return (`
        <p class='${ className || '' }'>
          ${ technical } ${ description }
        </p>
      `)
      
    } else if (infoLevel === 'mini') {
      return (`
        <p class='${ className || '' }'>
          ${ description }
        </p>
      `)
    }
  }
  
  function restoreScrollPosComponent() {
    return `<div style='height: 540px'></div>`
  }
  
  function headerComponent() {
    return (`
      <div class='sudoku__header'>

        <h1 class='sudoku__title'>

          <span class='show-on-sm'>
            Sudoku
          </span>

          <span class='show-on-md'>
            Sudoku Puzzle
          </span>

          <span class='show-on-lg'>
            Javascript Sudoku Puzzle Generator
          </span>

        </h1>

        ${descriptionComponent({
          infoLevel: 'mini',
          className: 'sudoku__description show-on-md'
        })}

        ${descriptionComponent({
          infoLevel: 'full',
          className: 'sudoku__description show-on-lg'
        })}

        ${
            state.success ? (`
    
              ${buttonComponent({
                id: 'generate-button',
                text: ['New Board', 'New Board', 'New'],
                mods: 'primary'
              })}
    
              ${ state.solved ?
                  buttonComponent({
                    id: 'solve-button',
                    text: 'Solved',
                    mods: ['tertiary', 'muted']
                  }) :
                  buttonComponent({
                    id: 'solve-button',
                    text: 'Solve',
                    mods: 'secondary'
                  })
              }

            `)
            
            : (`
    
              ${buttonComponent({
                id: 'generate-button',
                text: ['Generating', '', ''],
                mods: ['disabled', 'loading']
              })}
    
              ${buttonComponent({
                id: 'solve-button',
                text: 'Solve',
                mods: 'disabled'
              })}
            `)
            
        }

        ${ utils.isTouchDevice() ? (`

          ${buttonComponent({
            id: 'redo-button',
            text: ['&raquo;', '&raquo;', '&gt;', '&gt;'],
            classes: 'fr',
            mods: [
              'neutral',
              'compound',
              'compound-last',
              `${ !historyStash.length ?
              'disabled' :
              ''
              }`
            ]
          })}
          ${buttonComponent({
            id: 'undo-button',
            text: ['&laquo;', '&laquo;', '&lt;', '&lt;'],
            classes: 'fr',
            mods: [
              'neutral',
              'compound',
              'compound-first',
              `${ history.length > 1 ?
              '' :
              'disabled'
              }`
            ]
          })}

      `) : ''}

      </div>
    `)
  }
  
  function contentComponent() {
    var _isSeparator = (index) =>
      !!index && !((index + 1) % 3)
    
    var resultReady = !!state.board
    var fail = resultReady && !state.success
    
    if (!resultReady) {
      return (`
        ${messageComponent({
          state: 'busy',
          content: `Generating new board...`
        })}
        ${ restoreScrollPosComponent() }
      `)
    }
    
    if (fail) {
      return (`
        ${messageComponent({
          state: 'fail',
          content: `Something went wrong with this board, try generating another one.`
        })}
        ${ restoreScrollPosComponent() }
      `)
    }
    
    var rows = state.board
    
    return (`
      <table class='sudoku__table'>

        ${rows.map((row, index) => {
          let className = bem.makeClassName({
            block: 'sudoku',
            element: 'table-row',
            modifiers: {
              separator: _isSeparator(index)
            }
          });

          return (
            `<tr class='${ className }'>

              ${row.map((num, _index) => {
                let cellIndex = (index * 9) + _index
                let separator = _isSeparator(_index)
                let editable = typeof num !== 'number'
                let error = state.errors[cellIndex]
                let className = bem.makeClassName({
                  block: 'sudoku',
                  element: 'table-cell',
                  modifiers: {
                    separator,
                    editable,
                    error,
                    'editable-error': editable && error
                  }
                });

                return (
                  `\n\t
                  <td class='${ className }'
                      data-cell-index='${ cellIndex }'
                      ${ editable ? 'contenteditable' : ''}>
                        ${ num }
                  </td>`
                )
              }).join('')}

            \n</tr>\n`
          )

        }).join('')}

      </table>
    `)
  }
  
  return { initialize }
  
})( SUDOKU_APP_CONFIG ).initialize()
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdn.rawgit.com/MaxArt2501/object-observe/master/dist/object-observe-lite.min.js