6
\$\begingroup\$

Turtle Doodle presents several panels which arrange themselves responsively. There is a canvas where the Turtle will move, leaving a paint trail behind him. A control panel allows various commands to be sent to the Turtle such as Forward, Back, Left, Right. An "info" area shows a textual run-length-encoded listing of all the Turtle commands that have been executed. Attached to the "info" area is a listing of the "undo" stack showing what pieces will be added when redo is clicked. Finally, iff the session is logged in to the database (using the "sign in" or "sign up" buttons) another panel lists all the saved doodles which can be loaded by clicking. Turtle Doodle - medium layout

This is my first project using php, and the largest chunk of javascript I've written so far. I'd like to know how I might better organize all the javascript code. I think it isn't horrible at the moment, but I feel that it's right on the verge of becoming unmanageble if I start adding more features.

Sizes (and # description) of all my files:

$ shopt -s extglob
$ wc *([^.]).{php,js,css}
   66   105  1521 index.php   # entry point
  110   327  3684 page.php    # page html
   29    36   252 style.css   # style tweaks
  690  2043 15729 script.js   # page js
  177   542  4280 db.php      # database functions
    4     3    57 create.php  # script - create db tables
    4     3    52 delete.php  # script - delete db tables
    4     3    55 list.php    # script - list all tables
    7    15   133 load.php    # script - simulate ajax request
 1091  3077 25763 total

The directory also has local copies of jQuery and w3.css. I have the local jQuery arranged as a backup to the cdn link, but I haven't quite managed to arrange the same magic for w3.css yet. (see the top of page.php for this part)

index.php

This is the entry point for the project. In response to GET requests, it produces the html content from page.php. In response to POST requests, it checks the $_POST['req'] field for a command name and then calls the appropriate database function defined in db.php.

<?php
session_start();

if( $_SERVER['REQUEST_METHOD'] == 'POST' ){
  handle_post();
} else {
  require "page.php";
}


function set_session_variables(){
  if( isset($_SESSION['user']) ){
?>
     <script>
       sessionStorage.setItem('user', '<?php echo $_SESSION['user']; ?>' );
       sessionStorage.setItem('pass', '<?php echo $_SESSION['pass']; ?>' );
     </script>
<?php
  } else {
?>
     <script>
       sessionStorage.removeItem('user');
       sessionStorage.removeItem('pass');
     </script>
<?php
  }
}


function handle_post(){
  global $pdo;
  include("db.php");

  switch( $_POST['req'] ){
  case 'signin':
                  sign_in($_POST['user'], $_POST['pass']);
                  header("Location: ");
                  break;
  case 'signup':
                  sign_up($_POST['user'], $_POST['mail'], $_POST['pass']);
                  header("Location: ");
                  break;
  case 'signout':
                  sign_out();
                  session_unset();
                  $_SESSION = array();
                  session_destroy();
                  header("Location: ");
                  break;
  case 'save':
                  save_doodle($_POST['user'], $_POST['pass'],
                              $_POST['name'], $_POST['drawing']);
                  break;
  case 'delete':
                  delete_doodle($_POST['user'], $_POST['pass'],
                                $_POST['doodle']);
                  break;

  case 'load':
                  header('Content-Type: application/json');
                  load_doodles($_POST['user'], $_POST['pass']);
                  break;
  }

  $pdo = null;
}

page.php

This file produces the html content for the page. If the session is logged in, it includes a bit of javascript (defined in index.php) to set the user and pass variables in sessionStorage. I've adopted a style here which deliberately omits any markup which isn't absolutely necessary. So, I never close <p> tags. And there isn't even a <body> tag. This seems to really help in making less code overall.

<!doctype html>
<html lang=en>

<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset=utf-8>
<title>Turtle Doodle</title>
<link href=w3css.4.w3.css rel=stylesheet type=text/css>
<!-- https://www.w3schools.com/w3css/4/w3.css -->
<link href=style.css rel=stylesheet type=text/css>
<script src=https://code.jquery.com/jquery-3.4.1.min.js></script>
<script>window.jQuery ||
    document.write('<script src="jquery.3.4.1.min.js"\x3C/script>');</script>
<?php set_session_variables(); ?>
<script src="script.js"></script>


<header class="w3-container w3-green" >
  <div id=brand class="w3-container w3-half">
    <h1 ><nobr >Turtle Doodle</nobr></h1>
  </div>
  <div class="sign w3-container w3-half w3-bar w3-lightgreen" >
    <div class="signedout w3-lightgreen" >
      <button class="w3-bar-item w3-button" id="signin" >Sign In</button>
      <button class="w3-bar-item w3-button" id="signup" >Sign Up</button>
    </div>
    <div class="signin w3-lightgreen" style="display:none" >
      <form method=post >
        <input type=hidden name=req value=signin >
        <input class="w3-bar-item" type=text size=10
               name=user id=userin placeholder="Username">
        <input class="w3-bar-item" type=text size=10
               name=pass placeholder="Password">
        <button class="w3-bar-item w3-button" type=submit >Sign In</button>
      </form>
    </div>
    <div class="signup w3-lightgreen" style="display:none" >
      <form method=post >
        <input type=hidden name=req value=signup>
        <input class="w3-bar-item" type=text size=10
               name=user id=userup placeholder="Username">
        <input class="w3-bar-item" type=text size=10
               name=mail placeholder="Email">
        <input class="w3-bar-item" type=text size=10
               name=pass placeholder="Password">
        <button class="w3-bar-item w3-button" type=submit >Sign Up</button>
    </form>
    </div>
    <div class="signout w3-lightgreen" style="display:none" >
      <form method=post >
        <input type=hidden name=req value=signout>
        <button class="w3-bar-item w3-button" >Signout</button>
      </form>
    </div>
  </div>
</header>


<main class="w3-container w3-row" >
  <section class="canvas w3-container w3-col l6 m9 s12" >
    <canvas>
  </section>
  <section class="controls w3-container w3-col l3 m3 s6" >
    <h2 >controls</h2>
    <nobr >
    <button id=moveForward >F &#x2191;</button>
    <button id=Slink >S</button>
    <input id=step type=text size=1 value=20 onFocus=this.select()>
    <button id=moveBack >B &#x2193;</button>
    </nobr>
    <nobr >
    <button id=turnLeft >L &#x21b6;</button>
    <button id=Alink >A</button>
    <input id=angle type=text size=1 value=90 onFocus=this.select()>
    <button id=turnRight >R &#x21b7;</button>
    </nobr>
    <p>
    <nobr >
    <button id=Tlink style="display:none" >Turtle</button>
    <button id=Nlink >No Turtle</button>
    </nobr>
    <p>
  </section>
  <section class="info w3-container w3-col l3 m3 s6" >
    <input id=doodleName type=text size=10 onFocus=this.select()>
    <button align=right id=save >Save</button>
    <button align=right id=new  >New</button>
    <h2 ><span align=left>info</span>
    <span align=right>
    <button id=undo align=right >Undo</button>
    </span>
    </h2>
    <p><div class=doodle ></div></p>
    <h2 align=right >
    <button id=redo >Redo</button>
    </h2>
    <p align=right><div class=undo align=right></div></p>
  </section>
  <section class="saved w3-container w3-col l6 m12 s12" >
    <h2 >doodles</h2>
    <ol class=list >
    </ol>
  </section>
</main>


<footer class="w3-container" >
  <p >Inspired by the ideas of Seymour Papert
  <br >&copy; M. Joshua Ryan 2019
  <br ><span id=debug ></span>
</footer>

style.css

I'm using w3.css to handle responsive layout and basic styling. So this file just defines a few colors and tweaks.

h1 {
    font: 2em/1.5 sans-serif;
}


#sign {
    padding: .5em;
}



ul li {
    display: inline;
    padding: 0 1em;
}


main {
    font: 11px/1.2 serif;
}


h2 {
    font: 1.5em/1.2 sans-serif;
    background: lightgreen;
    color: darkgreen;
}

script.js

The big pile of javascript. I wonder if there are opportunities to make better use of jQuery constructs to simplify the js? Roughly, the top few functions are involved in attaching event handlers; the next portion implements these handlers and calling appropriate model-transforming functions; the next portion is a grab bag of all the remaining functions.

Would any of my (sets of) functions abstract nicely into separate modules?

var List = [
];

var Doodle = {
    drawing: [],
    undo: []
};

$(function(){ 
    let cx = $('canvas')[0].getContext('2d');
    test_stroke( cx );
    attach_controls();
    load_local_doodle();
    set_default_values();
    if( sessionStorage.getItem('user') ){
        show_signout();
        load_doodles();
    }
    update();
});

function test_stroke( cx ){
    cx.strokeStyle='blue';
    cx.lineWidth=2;
    cx.moveTo( 10, 10 );
    cx.lineTo( 20, 20 );
    cx.stroke();
}


function attach_controls(){
    $("#signin").click(show_signin);
    $("#signup").click(show_signup);
    keys_on();
    $("button.signout").click(destroy_session);
    $("#moveForward").click(moveForward);
    $("#moveBack").click(moveBack);
    $("#turnLeft").click(turnLeft);
    $("#turnRight").click(turnRight);
    $("#Slink").click(focusStep);
    $("#step").keypress(numeric);
    $("#step").change(setStep);
    $("#Alink").click(focusAngle);
    $("#angle").keypress(numeric);
    $("#angle").change(setAngle);
    $("#Tlink").click(showTurtle);
    $("#Nlink").click(hideTurtle);
    $("#save").click(save);
    $("#undo").click(undo);
    $("#redo").click(redo);
    $("#new").click(new_doodle);
    $("#doodleName").focus(()=>keys_off());
    $("#doodleName").change(()=>{Doodle.name = $("#doodleName").val();keys_on()});
}

function keys_on(){
    document.addEventListener("keydown",onKey);
    //$("document").keypress(onKey);
}

function keys_off(){
    document.removeEventListener("keydown",onKey);
    //$("body").off("keypress");
}

function set_default_values(){
    let canvas = $('canvas')[0];

    fix_canvas();
    window.matchMedia( "(min-width: 400px)" ).
        addEventListener( "change", fix_canvas);
    window.matchMedia( "(min-width: 600px)" ).
        addEventListener( "change", fix_canvas);

    $("#step").val(20);
    $("#angle").val(90);
}

function fix_canvas(){
    if( window.matchMedia( "(min-width: 601px)" ).matches ){
        fix_canvas_large();
    } else if( window.matchMedia( "(min-width: 993px)" ).matches ){
        fix_canvas_mid();
    } else {
        fix_canvas_small();
    }
    update();
}

function fix_canvas_small(){
    let canvas = $('canvas')[0];
    if( is_portrait() ){
        canvas.width = -30 + $(window).width() * 12/12;
        canvas.height = $(window).height() * 2/5;
    } else {
        canvas.width = -30 + $(window).width() * 12/12;
        canvas.height = $(window).height() * 2/5;
    }
}

function fix_canvas_mid(){
    let canvas = $('canvas')[0];
    if( is_portrait() ){
        canvas.width = -30 + $(window).width() * 9/12;
        canvas.height = $(window).height() * 2/5;
    } else {
        canvas.width = -30 + $(window).width() * 9/12;
        canvas.height = $(window).height() * 2/5;
    }
}

function fix_canvas_large(){
    let canvas = $('canvas')[0];
    if( is_portrait() ){
        canvas.width = -30 + $(window).width() * 6/12;
        canvas.height = $(window).height() * 3/5;
    } else {
        canvas.width = -30 + $(window).width() * 6/12;
        canvas.height = $(window).height() * 3/5;
    }
}

function is_portrait(){
    return ($(window).height / $(window).width) > 1;
}

function onKey( event ){ 
    //debug( "key: " + event.which );
    if( event.ctrlKey ){
        return true;
    }
    switch( event.which ){
    case A('f'): case A('F'): case 38: return moveForward();
    case A('l'): case A('L'): case 37: return turnLeft();
    case A('r'): case A('R'): case 39: return turnRight();
    case A('b'): case A('B'): case 40: return moveBack();
    case A('a'): case A('A'): return focusAngle();
    case A('s'): case A('S'): return focusStep();
    case A('t'): case A('T'): return showTurtle();
    case A('n'): case A('N'): return hideTurtle();
    }
}
function A( c ){ return c.charCodeAt(0); }

function numeric( event ){
    let whence = event.target;
    let c = event.keyCode || event.which;
    if( A('0') <= c && c <= A('9') ){
        return true;
    } else {
        event.preventDefault();
        event.stopPropagation();
        whence.blur();
        //doodle_add_mod( whence );
        onKey( event );
        return false;
    }
}


function show_signin(){
    $(".signedout").hide();
    $(".signin").show();
    $("#userin").focus();
    keys_off();
    return false;
}

function show_signup(){
    $(".signedout").hide();
    $(".signup").show();
    $("#userup").focus();
    keys_off();
    return false;
}

function show_signout(){
    $(".signedout").hide();
    $(".signout").show();
}

function destroy_session(){
    sessionStorage.removeItem('user');
    sessionStorage.removeItem('pass');
    $("form.signout")[0].submit();
}

function load_doodles(){
    let u = sessionStorage.getItem('user');
    let p = sessionStorage.getItem('pass');
    $.ajax({
        url: 'index.php',
        type: 'POST',
        data: {
            'req': 'load',
            'user': u,
            'pass': p
        },
        dataType: 'json',
        success: function(data){
            //debug("ajax returned "+ JSON.stringify(data));
            List = data;
            update_list();
        },
        error: function(request,error){
            debug("ajax error "+ error);
        }

    });
}

function save(){
    let u = sessionStorage.getItem('user');
    let p = sessionStorage.getItem('pass');
    $.ajax({
        url: 'index.php',
        type: 'POST',
        data: {
            'req': 'save',
            'user': u,
            'pass': p,
            'name': Doodle.name,
            'drawing': Doodle.info
        },
        dataType: 'text',
        success: function(data){
            debug("ajax returned "+ data);
        },
        error: function(request,error){
            debug("ajax error "+ error);
        }
    });
    load_doodles();
    update();
    return false;
}

function new_doodle(){
    Doodle.drawing = [];
    Doodle.name = "doodle" + (List.length + 1);
    update();
    return false;
}

function moveForward(){
    doodle_add_element('F');
    return false;
}

function moveBack(){
    doodle_add_element('B');
    return false;
}

function turnLeft(){
    doodle_add_element('L');
    return false;
}

function turnRight(){
    doodle_add_element('R');
    return false;
}


function focusAngle(){
    $("#angle").focus();
    return false;
}

function setAngle(){
    doodle_add_mod( $("#angle")[0] );
}

function focusStep(){
    $("#step").focus();
    return false;
}

function setStep(){
    doodle_add_mod( $("#step")[0] );
}


function showTurtle(){
    doodle_add_mod( { 'id': 'turtle', 'value': true } );
    $("#Tlink").hide();
    $("#Nlink").show();
    return false;
}

function hideTurtle(){
    doodle_add_mod( { 'id': 'turtle', 'value': false } );
    $("#Tlink").show();
    $("#Nlink").hide();
    return false;
}

function undo(){
    if( Doodle.drawing.length > 0 ){
        Doodle.undo.push(Doodle.drawing.pop());
    }
    update();
    return false;
}

function redo(){
    if( Doodle.undo.length > 0 ){
        Doodle.drawing.push(Doodle.undo.pop());
    }
    update();
    return false;
}

function update(){
    update_doodle_info();
    update_undo_info();
    update_drawing();
    save_doodle_locally();
}

function save_doodle_locally(){
    sessionStorage.setItem('doodle', JSON.stringify( Doodle ) );
}

function load_local_doodle(){
    let x = sessionStorage.getItem('doodle');
    if( x !== null ){
        Doodle = JSON.parse( x );
        debug(" loaded doodle, type: " + typeof( Doodle ) );
    } else {
        new_doodle();
    }
}

function update_list(){
    $(".list").html("");
    List.forEach(function(val, idx){
        $(".list").append(
            "<li><a href=# onClick='return choose("+ idx +")'>"+
                val.name +" "+
                add_breaking_spaces( val.drawing ) +"</a> "+
                "<a href=# onClick='return deletedoodle("+ val.id +")' >delete</a>"
        );
    });
}

function deletedoodle( idx ){
    let u = sessionStorage.getItem('user');
    let p = sessionStorage.getItem('pass');
    $.ajax({
        url: 'index.php',
        type: 'POST',
        data: {
            'req': 'delete',
            'user': u,
            'pass': p,
            'doodle': idx
        },
        dataType: 'text',
        success: function(data){
            debug("ajax returned "+ data);
            load_doodles();
        },
        error: function(request,error){
            debug("ajax error "+ error);
        }
    });
}

function choose( idx ){
    Doodle.name = List[idx].name;
    Doodle.drawing = [];
    for( var i = 0; i < List[idx].drawing.length; i++){
        switch( List[idx].drawing.charAt(i) ){
        case 'F':
            var s = read_number( List[idx].drawing, i+1 );
            i += s.len;
            doodle_add_element( { type: 'F', F: s.num } );
            break;
        case 'B':
            var s = read_number( List[idx].drawing, i+1 );
            i += s.len;
            doodle_add_element( { type: 'B', B: s.num } );
            break;
        case 'L':
            var s = read_number( List[idx].drawing, i+1 );
            i += s.len;
            doodle_add_element( { type: 'L', L: s.num } );
            break;
        case 'R':
            var s = read_number( List[idx].drawing, i+1 );
            i += s.len;
            doodle_add_element( { type: 'R', R: s.num } );
            break;
        case 'T':
            showTurtle();
            break;
        case 'N':
            hideTurtle();
            break;
        case 'A': 
            var s = read_number( List[idx].drawing, i+1 );
            i += s.len;
            doodle_add_mod( { id: 'angle', value: +s.num } );
            break;
        case 'S':
            var s = read_number( List[idx].drawing, i+1 );
            i += s.len;
            doodle_add_mod( { id: 'step', value: +s.num } );
            break;
        }
    }
    update();
    return false;
}

function read_number( str, idx ){
    var n = str.substring( idx ).match(/^\d+/);
    if( n !== null ){
        return { num: Number( n[0] ), len: n[0].length };
    } else {
        return { num: 1, len: 0 };
    }
}

function update_drawing(){
    let cx = $('canvas')[0].getContext('2d');
    let pt = doodle_path_and_final_turtle();
    let p = pt[0];
    let t = pt[1];

    cx.setTransform( 1, 0, 0, 1, 0, 0 );
    cx.clearRect( 0, 0, cx.canvas.width, cx.canvas.height );
    cx.beginPath();
    cx.strokeStyle = 'blue';
    cx.linewidth = 2;
    autoscale( cx, p );
    draw_path( cx, p );
    cx.stroke();
    if( t.show ){
        draw_turtle( cx, t );
    }
}

function autoscale( cx, p ){
    let minx = p.map(
        s => s.map( v=>v[0] ).reduce( (a, b) => Math.min( a, b ) )
    ).reduce( (a, b) => Math.min( a, b ) );
    let maxx = p.map(
        s => s.map( v=>v[0] ).reduce( (a, b) => Math.max( a, b ) )
    ).reduce( (a, b) => Math.max( a, b ) );
    let miny = p.map(
        s => s.map( v=>v[1] ).reduce( (a, b) => Math.min( a, b ) )
    ).reduce( (a, b) => Math.min( a, b ) );
    let maxy = p.map(
        s => s.map( v=>v[1] ).reduce( (a, b) => Math.max( a, b ) )
    ).reduce( (a, b) => Math.max( a, b ) );
    debug( "x["+minx + ","+maxx + "] y["+miny + ","+maxy + "]" );
    minx = Math.min( minx - 20, 0 );
    maxx = Math.max( maxx + 20, cx.canvas.width);
    miny = Math.min( miny - 20, 0 );
    maxy = Math.max( maxy + 20, cx.canvas.height);
    let dx = maxx - minx;
    let dy = maxy - miny;
    let sx = (cx.canvas.width) / dx;
    let sy = (cx.canvas.height) / dy;
    let s = Math.min( sx, sy );
    let ss = Math.min( s, 1 );
    //let ss = s * .5;
    cx.translate( - minx, - miny ); debug( "translate -" + minx + ", -" + miny );
    //cx.translate( minx + dx/2, miny + dy/2 ); debug( "translate " + (minx + dx/2) + ", " + (miny + dy/2) );
    cx.scale( ss, ss ); debug( "scale " + ss + ", " + ss );
    //cx.translate( (- dx/2)*ss, (- dy/2)*ss ); debug( "translate " + ((- dx/2)*ss) + ", " + ((- dy/2)*ss) );
}

function draw_path( cx, path ){
    path.forEach(function(subpath){
        cx.moveTo(...subpath[0]);
        //debug( "moveto (" + subpath[0][0] + ", " + subpath[0][1] + ")" );
        subpath.slice(1).forEach(function(point){
            cx.lineTo(...point);
            //debug( "lineto (" + point[0] + ", " + point[1] + ")" );
        });

    });
}

function doodle_path_and_final_turtle(){
    let T = Turtle();
    let p = [];
    let s = [ [ T.xpos, T.ypos ] ];
    Doodle.drawing.forEach(function(val){
        if( val.type == 'mod') {
            if( 'A' in val ){
                T.adel = val.A;
            }
            if( 'S' in val ){
                T.step = val.S;
            }
            if( 'T' in val ){
                T.show = val.T;
                if( T.show ){
                    p.push( s );
                    s = [ [ T.xpos, T.ypos ] ];
                }
            }
        } else {
            let t = val.type;
            switch( val.type ){
            case 'F':
                T.xpos += val[t] * T.step * Math.cos( rad( T.angle ) );
                T.ypos += val[t] * T.step * Math.sin( rad( T.angle ) );
                if( T.show ){ s.push( [ T.xpos, T.ypos ] ); }
                break;
            case 'B':
                T.xpos -= val[t] * T.step * Math.cos( rad( T.angle ) );
                T.ypos -= val[t] * T.step * Math.sin( rad( T.angle ) );
                if( T.show ){ s.push( [ T.xpos, T.ypos ] ); }
                break;
            case 'L':
                T.angle -= val[t] * T.adel;
                break;
            case 'R':
                T.angle += val[t] * T.adel;
                break;
            }
        }
    });
    if( s ){
        p.push( s );
    }
    return [ p, T ];
}

function draw_turtle( cx, t ){
    cx.translate( t.xpos, t.ypos );
    cx.scale( 1.5, 1.5 );
    cx.rotate( rad( t.angle ) );
    cx.beginPath();
    var p = [ [.5, 0], [1, 1], [2, 3], [4, 4], [6, 4], [8, 3], [9, 1], [9.5, 0],
              [9,-1], [8,-3], [6,-4], [4,-4], [2,-3], [1,-1],
              [2, 0], [3, 3], [3, 0], [3,-3], [4, 2], [4,-2], [6, 2],
              [6,-2], [7, 3], [7, 0], [7,-3], [8, 0] ];
    var v = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 0,
              1, 14, 13, 14, 16,
              18, 15, 2, 15, 3, 15, 18,
              20, 22, 4, 22, 5, 22, 20,
              23, 25, 6, 25, 7, 25, 23,
              21, 24, 9, 24, 10, 24, 21,
              19, 17, 11, 17, 12, 17 ];
    cx.moveTo(...p[v[0]]);
    for( let i = 1; i < v.length; i++ ){
        cx.lineTo(...p[v[i]]);
    }
    cx.strokeStyle = 'darkgreen';
    cx.lineWidth = 1;
    cx.stroke();
}

function rad( deg ){
    return deg * ( Math.PI / 180.0 );
}

function Turtle(){
  return {
      xpos: 10,
      ypos: 10,
      angle: 0,
      step: 20,
      adel: 90,
      show: true,
  };
}

function update_doodle_info(){
    $("#doodleName").val(Doodle.name);
    $(".doodle").html("");
    var x = "";
    Doodle.drawing.forEach(function(val){
        if( val.type == 'mod' ){
            if( 'A' in val ){
                x += 'A' + val.A;
            }
            if( 'S' in val ){
                x += 'S' + val.S;
            }
            if( 'T' in val ){
                x += val.T ? 'T' : 'N';
            }
        } else {
            if( val[val.type] == 1 ){
                x += val.type;
            } else {
                x += val.type + val[val.type];
            }
        }
    });
    Doodle.info = x;
    x = add_breaking_spaces( x );
    //debug( x );
    $(".doodle").html(x);
}

function update_undo_info(){
    $(".undo").html("");
    var x = "";
    Doodle.undo.forEach(function(val){
        if( val.type == 'mod' ){
            if( 'A' in val ){
                x = 'A' + val.A + x;
            }
            if( 'S' in val ){
                x = 'S' + val.S + x;
            }
            if( 'T' in val ){
                x = (val.T ? 'T' : 'N') + x;
            }
        } else {
            if( val[val.type] == 1 ){
                x = val.type + x;
            } else {
                x = val.type + val[val.type] + x;
            }
        }
    });
    Doodle.undoinfo = x;
    x = add_breaking_spaces( x );
    $(".undo").html(x);
}

function add_breaking_spaces( x ){
    let y = x.split("");
    let i = x.length - 1;
    let gap = 4;
    i -= i % gap;
    //debug( i );
    for( ; i > 0; i -= gap ){
        //debug( "splice( " + i + ", '&lt;wbr>' );");
        y.splice( i, 0, '<wbr>' );
    }
    let z = y.join("");
    //debug( z );
    return z;
}

function doodle_add_element( el ){
    if( typeof(el) == 'object' ){
        if( Doodle.drawing.length > 0 ){
            let last = Doodle.drawing[ Doodle.drawing.length - 1 ];
            if( last.type == el.type ){
                if( el.type == 'mod' ){
                    Object.assign( last, el );
                } else {
                    last[el.type] += el[el.type];
                }
            } else {
                Doodle.drawing.push( el );
            }
        } else {
            Doodle.drawing.push( el );
        }
        update();
    } else {
        //debug( "creating move object" );
        let x = { 'type': el };
        x[el] = 1;
        doodle_add_element( x );
    }
}

function doodle_add_mod( whence ){
    switch( whence.id ){
    case 'angle':
        doodle_add_element( { 'type': "mod", 'A': whence.value } );
        break;
    case 'step':
        doodle_add_element( { 'type': "mod", 'S': whence.value } );
        break;
    case 'turtle':
        doodle_add_element( { 'type': "mod", 'T': whence.value } );
        break;
    }
}

function debug(msg){
    let x = $("#debug").html();
    $("#debug").html( x + '<br>' + msg );
}

db.php

Connect to database and define functions for interaction. index.php calls these functions in response to POST requests. The command line scripts create.php, delete.php, list.php, load.php also call these functinos. Prepared statements are used throughout to protect the database from injections.

<?php

$host = '127.0.0.1';
$user = '';
$pass = '';
$db = 'doodle';
$charset = 'utf8mb4';

$options = [
  PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  PDO::ATTR_EMULATE_PREPARES => false,
];
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";

try {
     $pdo = new PDO($dsn, $user, $pass, $options);
} catch (PDOException $e) {
     throw new PDOException($e->getMessage(), (int)$e->getCode());
}


function create_database_tables(){
  global $pdo;

  $stmt = $pdo->query('CREATE TABLE users (
          uid INT NOT NULL AUTO_INCREMENT,
          user VARCHAR(100),
          email VARCHAR(100),
          pass VARCHAR(100),
          PRIMARY KEY(uid) );');
  if( ! $stmt ){ die("Error creating users table"); }

  $stmt = $pdo->query("INSERT INTO users (user, email, pass)
                       VALUES ('m', 'm@m.m', 'm')");
  if( ! $stmt ){ die("Error creating user"); }

  $sql = $pdo->query('CREATE TABLE doodles (
         id INT NOT NULL AUTO_INCREMENT,
         uid INT NOT NULL,
         name VARCHAR(100),
         drawing VARCHAR(512),
         created TIMESTAMP,
         PRIMARY KEY(id) );');
  if( ! $stmt ){ die("Error creating doodles table"); }

  $stmt = $pdo->query("INSERT INTO doodles (uid, name, drawing)
                       VALUES (1, 'doodle1', 'FRFLFRFLF')");
  if( ! $stmt ){ die("Error creating doodle"); }
}

function list_database_tables(){
  global $pdo;

  echo "users:\n";
  $stmt = $pdo->query('SELECT * FROM users');
  while( $row = $stmt->fetch() ){
    print_r($row);
    echo "\n";
  }

  echo "doodles:\n";
  $stmt = $pdo->query('SELECT * FROM doodles');
  while( $row = $stmt->fetch() ){
    print_r($row);
    echo "\n";
  }
}

function delete_database_tables(){
  global $pdo;

  $stmt = $pdo->query('DROP TABLE users;');
  if( ! $stmt ){ die("Error deleting users table"); }

  $stmt = $pdo->query('DROP TABLE doodles');
  if( ! $stmt ){ die("Error deleting doodles table"); }
}


function sign_in( $user, $pass ){
  global $pdo;

  $user = sanitize( $user, 'string' );
  $pass = sanitize( $pass, 'string' );

  $stmt = $pdo->prepare('SELECT * FROM users WHERE
          (user = ? OR email = ?) AND pass = ?;');
  $stmt->execute([$user, $user, $pass]);
  $row = $stmt->fetch();

  if( ! $row ){ die( "No such user or wrong password." ); }
  $_SESSION['user'] = $user;
  $_SESSION['pass'] = $pass;
}

function sign_up( $user, $mail, $pass ){
  global $pdo;

  $user = sanitize( $user, 'string' );
  $pass = sanitize( $pass, 'string' );
  $mail = sanitize( $user, 'email' );

  $stmt = $pdo->prepare('INSERT INTO users (user, email, pass)
          VALUES (?, ?, ?);');
  $status = $stmt->execute([$user, $mail, $pass]);

  if( ! $status ){ die("Failed to add user."); }
  $_SESSION['user'] = $user;
  $_SESSION['pass'] = $pass;
}

function sign_out(){
  global $pdo;
}


function save_doodle( $user, $pass, $name, $drawing ){
  global $pdo;
  $uid = get_uid( $user, $pass );

  $stmt = $pdo->prepare('INSERT INTO doodles (uid, name, drawing)
          VALUES (?, ?, ?);');
  $status = $stmt->execute([$uid, $name, $drawing]);

  if( ! $status ){ die("Failed to save drawing."); }
  echo "Ok.";
}

function delete_doodle( $user, $pass, $doodle ){
  global $pdo;
  $uid = get_uid( $user, $pass );

  $stmt = $pdo->prepare('DELETE FROM doodles WHERE uid=? AND id=?');
  $status = $stmt->execute([$uid, $doodle]);

  if( ! $status ){ die("Failed to delete doodle."); }
  echo "Ok.". $doodle . " " . $status;
}

function load_doodles( $user, $pass ){
  global $pdo;
  $uid = get_uid( $user, $pass );

  echo json_encode( pdo($pdo, 'SELECT * FROM doodles WHERE uid=?',
           [ $uid ] )->fetchAll() );
}


function get_uid( $user, $pass ){
  global $pdo;

  return pdo( $pdo, 'SELECT (uid) FROM users WHERE user=? AND pass=?;',
              [ sanitize($user, 'string'), sanitize($pass, 'string') ]
             )->fetch()['uid'];
}


function sanitize( $var, $type ){
  switch($type){
  case 'string':
    return filter_var( $var, FILTER_SANITIZE_STRING );
  case 'email':
    return filter_var( $var, FILTER_SANITIZE_EMAIL );
  }
}

function pdo($pdo, $sql, $args = NULL)
{
    if (!$args)
    {
         return $pdo->query($sql);
    }
    $stmt = $pdo->prepare($sql);
    $stmt->execute($args);
    return $stmt;
}

create.php

Script to set up database tables, creating default user 'm' and saving one doodle for 'm'.

<?php

include_once("db.php");

create_database_tables();

delete.php

Script to delete all database tables.

<?php

include("db.php");

delete_database_tables();

list.php

Script to list contents of all database tables.

<?php

include_once("db.php");

list_database_tables();

load.php

Script to simulate the ajax request that script.js:load_doodles() performs. Outputs the exact JSON encoded data that the javascript function will receive.

<?php

$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['req'] = 'load';
$_POST['user'] = 'm';
$_POST['pass'] = 'm';

require 'index.php';

Of course I'd love to know if any parts can be improved, but I'm specifically concerned about how to organize all the javascript into manageable pieces.

\$\endgroup\$
6
\$\begingroup\$

I would limit my review to db.php file only.

First of all, I see you made use of some of my advises already, probably visited some of my articles. Sadly, not all recommendations are followed, but that's OK, we'll review them.

Connection

The connection part is simply OK, nothing to review

Structure

Regarding the structure, I would say this file is a bit bloated. And contain a code belongs to different, so to say, layers of responsibility. Such parts as the connection and a pdo() function have a global responsibility. they could be used in any project and even in any part of every project.

Whereas other functions are quite specific and totally useless in any other project part. Also, the connection credentials are project-specific.

It order to make your design modular and reusable, I would

  • leave in db.php only the connection code and general purpose functions such as pdo()
  • move the credentials into a separate file and include it in db.php
  • all specific functions I would move into respective files. Honestly, I don't see a point in having a file consists of a single line that calls a function. Why not to put the function code directly into specific files? At least it will make your code better organized and editing each particular code block would be easier. Honestly, finding a particular function in the current db.php is a challenge.

Global

is frowned upon. it adds magic. You see a function call but have no idea where does it take the database connection from. It is always good to have all resources used by a function to be explicitly set.

Error reporting

Your current approach is simply redundant. In reality, you will never see an error message like "Error creating users table" - PDO will throw an exception and and halt the script execution prior that. So, all such code snippets

if( ! $stmt ){ die("Error creating users table"); }

are rather useless and need to be removed.

Inconsistent use of the helper function.

Somewhere you are using it and somewhere not. It would be a good idea to make it consistent.

Let's rewrite save_doodle() function based on the tree recent recommendations:

function save_doodle($pdo, $user, $pass, $name, $drawing )
{
    $uid = get_uid( $user, $pass );
    $sql = 'INSERT INTO doodles (uid, name, drawing) VALUES (?, ?, ?)');
    pdo($pdo, $sql, [$uid, $name, $drawing]);
}

Much more clean, isn't it?

Sanitization

This is a complex one. There are many things that are get confused in a single word "santitization", so it's better to avoid it at all.

Regarding the process, you need to differentiate two things,

  • data validation
  • data formatting

Sadly, both are misplaced in your sanitize() function.

Data validation is testing the user input according to some rules and telling the user back if some tests failed. Checking email format for example. Just calling "sanitize" on the wrong email will save an empty string in the database which is not what you want. You need to decide, whether you want to validate the user input. If you do, make it vocal: notify a user about a failed validation.

However, for such a simple script you may put it aside for the moment.

Data formatting is make the data usable in the certain medium it is going to be used. Some examples

  • when used inside HTML, the data must be HTML-formatted
  • when used inside SQL, the data must be SQL-formatted (by using prepared statements)
  • when used inside JS, the data must be JS-formatted
  • and so on, you get the idea

But neither is done by the sanitize() function. So you have to get rid of it and make your data formatting destination-specific. In other words you have to format your data right before use and limit such a formatting only to a certain medium. For example, for the DSQL query, all formatting you need is a prepared statement.

Storing user passwords

Is a notorious story. Never ever store passwords in plain text but only in the form of a cryptographic hash. There are specific functions for that. In order to help you with the user login part, I've got a ready made code for the user authorization whose password is properly hashed

Authorization

I see you are asking a user to enter the username and the password every time they perform an action. Although you could leave it as is for the time being, consider using a simple authentication. it is as simple as calling session_start() on every request and storing the authenticated user's id in the $_SESSION array element. Then you could get the user id any time from that variable back.

Separation of concerns

This is a rather important one. but it can be simplified as a rule of thumb: no function should output anything on its own (unless the only purpose of the function is output).

There is a business logic and there is a display logic. They should never interfere. delete_doodle() is a business logic. You'd never know how you would call this function and how you would notify a user of its success. Make this function return only a boolean value whereas output should be done elsewhere.

In practice this one rather contradicts with the earlier suggestion to move the code from function to files, so it's just a generalized advise on using functions in general. A function just calculates some result and returns it, while any output is only done in a designated placeor by a designated agent.

\$\endgroup\$
  • \$\begingroup\$ Much obliged! I'll get to work on these issues. Evidently I did not read your articles closely enough. :) \$\endgroup\$ – luser droog Sep 26 at 10:20
2
\$\begingroup\$

PHP

Constants

The first five variables in db.php could be declared as constants since they shouldn't be changed:

define('DB_HOST', '127.0.0.1');
define('DB_USER', '');
define('DB_PASS, '');
define('DB_NAME, 'doodle');
define('DB_CHARSET', 'utf8mb4');

And notice the prefix DB_ was added to those names to specify that those values pertain to the database. That way variable names like $user don't get re-used between the database connection information and parameters to functions like sign_in(), sign_up(), etc.

However string interpolation wouldn't work with constant, so a line like:

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";

would need to rely on concatenation:

$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;

short echo

In index.php there are script tags with PHP renderings - like:

sessionStorage.setItem('user', '<?php echo $_SESSION['user']; ?>' );

Depending on your version of PHP, you could consider using <?= instead of <?php echo.

For more information, refer to this answer and others to Is it bad practice to use <?= tag in PHP?

JS

Overall I see quite a bit of redundancy in these functions. Perhaps event delegation could be used to simplify all the event handlers setup in attach_controls(). For example, a single click handler could check the id attribute of the element that was clicked and if that corresponds to a defined function then call that function (e.g. turnLeft, turnRight, save, undo, etc.

There is also a lot of repeated code in the fix_canvas_* functions. The only differences appear to be the multipliers on the fractions (e.g. 12/12, 9/12, 6/12 and 2/5, 3/5. Those values could be passed as parameters or a parameter could be made for large, mid or small that leads to those values being used.

Simply anonymous function

The second to last focus handler setup in attach_controls() has an arrow function with one statement:

$("#doodleName").focus(()=>keys_off());

that could be simplified using a function name reference instead:

$("#doodleName").focus(keys_off);

document.write()

In page.php I see

<script>window.jQuery ||
    document.write('<script src="jquery.3.4.1.min.js"\x3C/script>'); 
</script>

Note that document.write() will:

Writing to a document that has already loaded without calling document.open() will automatically call document.open.1

And

The Document.open() method opens a document for writing.

This does come with some side effects. For example:

  • All event listeners currently registered on the document, nodes inside the document, or the document's window are removed.
  • All existing nodes are removed from the document. 2

Which means that you should be aware that the entire HTML could be replaced by <script src="jquery.3.4.1.min.js"\x3C/script> if window.jQuery evaluates to false when that script executes. Perhaps it would be better to wait for either that jQuery script or else the DOM to load and if jQuery isn't loaded then create a script tag and append it to the DOM instead of using document.write().

Prefer const

Use const instead of let for values that shouldn't be re-assigned. For example, in deletedoodle() the variables u and p don't get re-assigned and can be declared with const. This helps avoid accidental re-assignment.

\$\endgroup\$
  • \$\begingroup\$ Having a single click handle is brilliant! I added that very early in my new design. \$\endgroup\$ – luser droog Oct 6 at 18:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.