2
\$\begingroup\$

After being inspired by some MVC style design patterns, I have been trying to separate data from views in my code and move toward a more sensibly organized object based approach. (please, don't bother telling me that my code doesn't follow strict MVC pattern correctly, that is not my concern at this point!) All the data for a single tutorial is now handled by a data model object (currentTutorial) while currentvideo holds the state and relevant data (such as auto-pause times) for the videos but I have hit a bit of a wall with trying to couple the currentvideo and currentTutorial objects.

I want the currentvideo object to be a property of the currentTutorial object, it makes no sense for it to exist separately as there is exactly one corresponding video for each tutorial and I think it makes my code looser, more confusing and fragile as a result.

Unfortunately every time I try to move the video object into the tutorial object I run into countless scoping problems that just kinda make my brain hurt and I end up reverting back to this point. I know it's a pretty big ask but I would be very grateful to any OOP guru who could untangle this mess for me! :)

P.S. I will of course consider suggestions relating to other issues but this is my main concern right now..

var conjugationsets = [
    ["작다", "놀다", "닦다"], 
    ["웃다", "울다", "멀다"]
];
var section_number = 0;
var tutorial_number = 0;
//firstpause, tryagain, tryagainpause, firstcongrats, firstcongratspause, secondcongrats, secondcongratspause, thirdcongrats
var videos = [{
        url: "conjugation tut.mp4",
        times: [159, 160.5, 163.8, 164.15, 166.8, 167.1, 170, 171.7]
    }, {
        url: "play.mp4",
        times: [159, 160.5, 163.8, 164.15, 166.8, 167.1, 170, 171.7]
    }];
var questionnum = 0;
var currentvideo;
var currentset = {};
var currentTutorial;
function handler() {
    tutorial_number++;
    currentTutorial = new Buildtutorial;

    currentvideo = new Buildvideo(videos[tutorial_number]);

    currentvideo.videoobject.play(0);
}

function Buildtutorial(num) {
    if (num){tutorial_number = num};
    this.sets = [
        {name: "Conjugation",tutorials: ["ㅗ and ㅏ regular", "ㅜ, ㅓ and ㅣ regular", "ㅏ and ㅓ reductive", "ㅗ and ㅜ reductive", "ㅣ reductive"]}, 
        {name: "Sentence Building",tutorials: ["Particles", "Word Order"]}
    ];
    this.tutorial_name = this.sets[section_number].tutorials[tutorial_number];
    this.section_name = this.sets[section_number].name;
    this.tutorial_number = tutorial_number;
    this.section_number = section_number;
    update_menu(this);
}
function Buildvideo(x) {
    if (x === undefined) {
        alert("no new videos");
        return;
    }
    this.firstpause = x.times[0];
    this.url = x.url;
    this.tryagain = x.times[1];
    this.tryagainpause = x.times[2];
    this.firstcongrats = x.times[3];
    this.firstcongratspause = x.times[4];
    this.secondcongrats = x.times[5];
    this.time = $("#video").get(0).currentTime;
    this.secondcongratspause = x.times[6];
    this.thirdcongrats = x.times[7];
    this.videoobject = $("#video").get(0);
    $(this.videoobject).bind('ended', handler);
    this.load = function() {
        $("#video").html("<source src='" + currentvideo.url + "' type='video/ogg'>");
        this.videoobject.load();
    };
    this.start = function(time) {
        this.videoobject.currentTime = time;
        time = time || 0;
        this.videoobject.play();
        this.pause(this.firstpause);
    };
    this.pause = function(time) {
        this.videoobject.addEventListener('timeupdate', function() {
            if (this.currentTime >= time && this.currentTime < (time + 0.3)) {
                console.log("pause at " + this.currentTime);
                this.pause();
            }
        }, true);
    };
}
function getnewset() {
    currentset = {
        conjugations: {},
        baseverb: conjugationsets[tutorial_number][questionnum]
    };
}
var random = function(r) {
    r.children().sort(function() {
        return (Math.round(Math.random()) - 0.5);
    }).appendTo(r);
};
var vowels = "0ㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣ".split(""), 
leads = "0ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ".split(""), 
tails = "0ㄱㄲㄳㄴㄵㄶㄷㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅄㅅㅆㅇㅈ".split("");
function render(x) {
    $("#wrong0").text(x.conjugations.incorrect[0]);
    $("#wrong1").text(x.conjugations.incorrect[1]);
    $("#wrong2").text(x.conjugations.incorrect[2]);
    $("#correct").text(x.conjugations.correct);
    random($(".answers"));
    $(".answer").css("border-radius", "0px");
    $('.answer').first().css('border-radius', '10px 0px 0px 10px');
    $('.answer').last().css('border-radius', '0px 10px 10px 0px');
    $(".answer").css("background-color", "#B7BECC");
    $("#base").text(x.baseverb);
}
function update_menu(y){
    $("#nav,#nav *").css("background-color", "white");
    var query = "#" + y.section_name + ",#" + y.section_name + "" + y.tutorial_number;
    $(query).css("background-color", "rgb(215,215,215)");
}
function render_menu(x) {
    var nav = $("#nav");
    $.each(x.sets, function(ind, val) {
        var text = "";
        $.each(val.tutorials, function(i, v) {
            text += "<li id='" + val.name + i + "'class='tutorial_title'>" + v + "</li>"
        });
        nav.append($("<li id='" + x.sets[ind].name + "' class='section_title'>" + val.name + "</li><ul id='tutorial_titles'>" + text + "</ul>"));
    });
    update_menu(x);
    $(".tutorial_title").click(function(){
        var id = $(this).attr('id');
        var num = id.substring(id.length - 1);
        currentTutorial = new Buildtutorial(num);
    });
}
$(function() {
    //set mouseover colors
    $(".answer").each(function() {
        $(this).mouseover(function() {
            var bg = $(this).css("background-color");
            if (bg === "rgb(183, 190, 204)" || bg === "#b7becc") {
                $(this).css("background-color", "#E5E8EE");
            }
        }).mouseout(function() {
            var bg = $(this).css("background-color");
            if (bg === "rgb(229, 232, 238)" || bg === "#e5e8ee") {
                $(this).css("background-color", "#B7BECC");
            }
        });
    });
    function seperate(x) {
        x = x.charCodeAt();
        var y = {}, 
        z = {};
        y.tail = (x - 44032) % 28;
        y.vowel = 1 + ((x - 44032 - y.tail) % 588 / 28);
        y.lead = 1 + (parseInt((x - 44032) / 588, 10));
        z.vowel = vowels[y.vowel];
        z.tail = tails[y.tail];
        z.lead = leads[y.lead];
        return z;
    }

    function buildhangeul(x) {
        var tail = tails.indexOf(x.tail), 
        vowel = vowels.indexOf(x.vowel), 
        lead = leads.indexOf(x.lead), 
        codepoint = tail + (vowel - 1) * 28 + (lead - 1) * 588 + 44032;
        return String.fromCharCode(codepoint);
    }

    function conjugate(base) {
        var output = {}, 
        each = base.split("");
        output.incorrect = [];
        if (each[each.length - 1] === "다") {
            var stemlast = each[each.length - 2], 
            lastjamo = seperate(stemlast);
            //ㅂ irregular
            if (lastjamo.tail === "ㅂ") {
                lastjamo.tail = "0";
                stemlast = buildhangeul(lastjamo);
                output.correct = each.slice(0, -2).join("") + stemlast + "워";
                output.incorrect[0] = each.slice(0, -1).join("") + buildhangeul({
                    lead: "ㅇ",
                    vowel: lastjamo.vowel,
                    tail: "0"
                });
                output.incorrect[1] = each.slice(0, -1).join("") + "워";
                output.incorrect[2] = each.slice(0, -2).join("") + stemlast + "와";
            } else if (lastjamo.vowel === "ㅏ" && lastjamo.tail !== "0" || lastjamo.vowel === "ㅗ" && lastjamo.tail !== "0") {
                //ㅏ and ㅗ regular
                output.correct = stemlast + "아";
                output.incorrect[0] = buildhangeul({
                    lead: lastjamo.lead,
                    vowel: lastjamo.vowel,
                    tail: "0"
                }) + buildhangeul({
                    lead: lastjamo.tail,
                    vowel: "ㅏ",
                    tail: "0"
                });
                output.incorrect[1] = stemlast + "ㅏ";
                output.incorrect[2] = stemlast;
            } else if (lastjamo.vowel === "ㅓ" && lastjamo.tail !== "0" || lastjamo.vowel === "ㅜ" && lastjamo.tail !== "0" || lastjamo.vowel === "ㅣ" && lastjamo.tail !== "0") {
                //ㅓ, ㅜ, and ㅣ regular
                output.correct = stemlast + "어";
                output.incorrect[0] = buildhangeul({
                    lead: lastjamo.lead,
                    vowel: lastjamo.vowel,
                    tail: "0"
                }) + buildhangeul({
                    lead: lastjamo.tail,
                    vowel: "ㅓ",
                    tail: "0"
                });
                output.incorrect[1] = stemlast + "ㅓ";
                output.incorrect[2] = stemlast;
            } 
            //ㅏ and ㅓ reductive
            else if (lastjamo.vowel === "ㅏ" && lastjamo.tail === "0" || lastjamo.vowel === "ㅓ" && lastjamo.tail === "0") {
                output.correct = stemlast;
                output.incorrect[0] = stemlast + buildhangeul({
                    lead: "ㅇ",
                    vowel: lastjamo.vowel,
                    tail: "0"
                });
                output.incorrect[1] = buildhangeul({
                    lead: lastjamo.lead,
                    vowel: "ㅐ",
                    tail: "0"
                });
                output.incorrect[2] = base;
            } 
            //ㅗ and ㅜ reductive
            else if (lastjamo.vowel === "ㅗ" && lastjamo.tail === "0") {
                lastjamo.vowel = "ㅘ";
                output.correct = buildhangeul(lastjamo);
                output.incorrect[0] = stemlast + "와";
                output.incorrect[1] = stemlast + "아";
                output.incorrect[2] = buildhangeul({
                    lead: lastjamo.lead,
                    vowel: "ㅝ",
                    tail: "0"
                });
            } else if (lastjamo.vowel === "ㅜ" && lastjamo.tail === "0") {
                lastjamo.vowel = "ㅝ";
                output.correct = buildhangeul(lastjamo);
                output.incorrect[0] = stemlast + buildhangeul({
                    lead: "ㅇ",
                    vowel: "ㅝ",
                    tail: "0"
                });
                output.incorrect[1] = buildhangeul({
                    lead: lastjamo.lead,
                    vowel: "ㅜ",
                    tail: "0"
                }) + buildhangeul({
                    lead: "ㅇ",
                    vowel: "ㅓ",
                    tail: "0"
                });
                output.incorrect[2] = buildhangeul({
                    lead: lastjamo.lead,
                    vowel: "ㅘ",
                    tail: "0"
                });
            } 
            //ㅣreductive
            else if (lastjamo.vowel === "ㅣ" && lastjamo.tail === "0") {
                lastjamo.vowel = "ㅕ";
                output.correct = each.slice(0, -2).join("") + buildhangeul(lastjamo);
            } 
            //르 irregular
            else if (stemlast === "르") {
                var secondlastjamo = seperate(each[each.length - 3]);
                secondlastjamo.tail = "ㄹ";
                if (secondlastjamo.vowel === "ㅗ") {
                    stemlast = "라";
                } else if (secondlastjamo.vowel === "ㅜ") {
                    stemlast = "러";
                }
                var secondlast = buildhangeul(secondlastjamo);
                output.correct = each.slice(0, -3).join("") + secondlast + stemlast;
            }
        } else {
            alert("Only Korean verbs in dictionary form please ;)");
        }
        return output;
    }
    $('.answer').click(function() { //click answer
        //wait for tutorial to end
        if (currentvideo.videoobject.currentTime <= currentvideo.firstpause) {
            $("#warning").text("Not yet!").fadeIn(300).delay(1400).fadeOut(300);
            return
        }
        if ($(this).text() === currentset.conjugations.correct) {
            //if correct
            //skip to congratulations
            $(this).css("background-color", "#62F05F");
            questionnum++;
            if (questionnum === 1) {
                currentvideo.start(currentvideo.firstcongrats);
                currentvideo.pause(currentvideo.firstcongratspause);
            } else if (questionnum === 2) {
                currentvideo.start(currentvideo.secondcongrats);
                currentvideo.pause(currentvideo.secondcongratspause);
            } else if (questionnum === 3) {
                currentvideo.start(currentvideo.thirdcongrats);
                questionnum = 0;
            }
            getnewset();
            currentset.conjugations = conjugate(currentset.baseverb);
            setTimeout(function() {
                render(currentset);
            }, 2000);
        //if incorrect
        } else {
            //skip to try again msg
            $(this).css("background-color", "red");
            currentvideo.start(currentvideo.tryagain);
            currentvideo.pause(currentvideo.tryagainpause);
        }
    });
    $("#reset").click(function() {
        currentvideo.videoobject.currentTime = 0;
        currentvideo.start();
    });
    currentTutorial = new Buildtutorial(tutorial_number);
    render_menu(currentTutorial);
    currentvideo = new Buildvideo(videos[tutorial_number]);
    getnewset();
    currentvideo.load();
    currentvideo.videoobject.play();
    currentvideo.pause(currentvideo.firstpause);
    currentset.conjugations = conjugate(currentset.baseverb);

    render(currentset);
});
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I cannot answer your scoping question, though I would not bother putting video under tutorial if it gives you hassles. I can give a review of your code.

  • lowerCamelCasing is good : currentvideo -> currentVideo, currentset -> currentSet, section_number -> sectionNumber etc.
  • Do not skip newlines after if conditions: ( if (num){tutorial_number = num}; )
  • Production code should not have console.log
  • I am not sure how currentTutorial = new Buildtutorial; works, I think you are missing ()
  • I like your videos and conjugationSets objects
  • The parameter x for Buildvideo could use a better name
  • Buildtutorial and Buildvideo are constructors, they ought to have the name of what you build. Tutorial and Video would be better constructor names.
  • questionnum should probably be part of your model
  • handler as a function name is too vague
  • The object that you assign to this.sets in BuildTutorial should probably have been defined on top next to videos and conjugationSets
  • I know it's not your concern, but calling update_menu from a constructor is far away from MVC
  • The times member of videos should probably not be an array but an object instead, it would be easier to maintain videos then
  • the function name random does not give away that it randomizes an array, also the parameter name r is unfortunate
  • You can consider to turn these statements $("#wrong0").text(x.conjugations.incorrect[0]); etc into a loop, this will make your code more flexible
  • In //set mouseover colors I would suggest not to hard code colors in JavaScript but to toggle CSS classes
  • In function seperate(x) and buildhangeul you should document better the magic numbers, I have no idea what they do
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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