3
\$\begingroup\$

JS Bin Clone

This is basically a clone of JSbin that I tried to build from scratch. The Javascript took me a good 20 hours to figure out. I had way too much fun trying to figure out the logic behind the button hover mechanics. Still not sure if it is the optimal way to do it.

Note:

  • You will need to download jquery and have it in the same folder as the html, css, and javascript file.
  • Javascript needs to be saved as jquery-project.js
  • CSS needs to be saved as jquery-project.css

HTML

<!DOCTYPE html>
<html>
<head>
    <title> Code Player </title>

    <script type="text/javascript" src="jquery.min.js"></script>
    <link rel="stylesheet" type="text/css" href="jquery-project.css">
</head>
<body>

    <div id="header">

        <div id="logo">
            CodePlayer
        </div>

        <div id="buttonContainer">

            <div class="toggleButton activeButton" id="html">
                HTML
            </div>

            <div class="toggleButton" id="css">
                CSS
            </div>

            <div class="toggleButton" id="javascript">
                JavaScript
            </div>

            <div class="toggleButton activeButton" id="output">
                Output
            </div>

        </div>

    </div>

    <div id="clearFloat"></div>

    <div id="bodyContainer">

        <textarea id="htmlPanel" class="panel"> <p id="paragraph">Hello World!</p> </textarea>
        <textarea id="cssPanel" class="panel hidden"> p { color:green; } </textarea>
        <textarea id="javascriptPanel" class="panel hidden"> document.getElementById("paragraph").innerHTML = "hello Asaph"; </textarea>

        <iframe id="outputPanel" class="panel"></iframe>

    </div>

<script type="text/javascript" src = jquery-project.js></script>

</body>
</html>

CSS

html {
    margin: 0;
}

body {
    font-family: sans-serif;
}

#header {
    width: 100%;
    margin: 0;
    padding: 0;
    border-top: solid 1px #BFBFBF;
    border-bottom: solid 1px #BFBFBF;
    height: 37px;
    background-color: #EEEEEE;
}

#logo {
    font-weight: bold;
    float: left;
    padding: 6px;
    position: relative;
    top: 3px;
    left: 3px;
}
.toggleButton {
    position: relative;
    top: 2px;
    padding: 6px;
    float: left;
    border: solid 1px #D2D2D2;
    border-right: none;
}

#html {

    border-bottom-left-radius: 5px;
    border-top-left-radius: 5px;
}

#output {
    border-right: solid 1px #D2D2D2;
    border-bottom-right-radius: 5px;
    border-top-right-radius: 5px;

}

#buttonContainer {
    width: 253px;
    margin: 0 auto;
    position: relative;
    top: -1px;
}

#clearFloat {
    clear: both;
}

.activeButton{
    background-color: #E8F2FF;
}

.highlightButton {
    background-color: #E5E5E5 !important;
}

.highlightActiveButton {
    background-color: #F5F9FF !important;
}

.unactiveButton {
    background-color: #EEEEEE;
    position: relative;
    top: 2px;
    padding: 6px;
    float: left;
    border: solid 1px #D2D2D2;
    border-right: none;
}

textarea {
    width:50%;
    resize: none;
    border-top: none;
    /*border-left: none;*/

}

.panel {
    float: left;
    border-left: none;
}

iframe {
    border: none;
}

.hidden {
    display: none;
}

Javascript

// Check if button has a certain class

function checkButton(button, className) {
    var buttonBoolean = $(button).hasClass(className);
    return buttonBoolean;
}   

var toggleActive = checkButton(".toggleButton", "activeButton");

var toggleButtonMethod = toggleActive ? 'activeState' : 'defaultState';

//dynamically choose between 2 states of button

$.fn.buttonHoverState = function(method, hoverState){
    var _this = this;
    var buttonState = {
        defaultState : {
            "on" : function(){
                _this.removeClass("highlightActiveButton");
                _this.addClass("highlightButton");
                // console.log("default hover on " + toggleButtonMethod);
            },
            "off" : function(){
                _this.removeClass("highlightButton");
                // console.log("default hover off " + toggleButtonMethod);
            }
        },
        activeState : {
            "on" : function(){
                _this.removeClass("highlightButton");
                _this.addClass("highlightActiveButton");
                // console.log("active hover on " + toggleButtonMethod);
            },
            "off" : function(){
                _this.removeClass("highlightActiveButton");
                // console.log("active hover off " + toggleButtonMethod);
            }
        }
    };

    buttonState[method][hoverState]();

}

// Determine Hover states of user defined button

function buttonHover(buttonId) {

    $(buttonId).hover(

        function(){
            toggleActive = checkButton( buttonId, "activeButton");
            toggleButtonMethod = toggleActive ? 'activeState' : 'defaultState';
            // console.log("Button is " + toggleButtonMethod);
            $(this).buttonHoverState(toggleButtonMethod, "on");
            // console.log(toggleActive);

        }, function () {
            toggleActive = checkButton( buttonId, "activeButton");
            toggleButtonMethod = toggleActive ? 'activeState' : 'defaultState';
            $(this).buttonHoverState(toggleButtonMethod, "off");
            // console.log(toggleActive);

        }
    );
}

// Determine if button is active or not and change state accordingly

function buttonClick(buttonId) {
    $(buttonId).click(
        function(){
            $(this).toggleClass("activeButton");
            toggleActive = checkButton(buttonId, "activeButton");

            toggleActive ? $(this).removeClass("highlightButton") : $(this).removeClass("highlightActiveButton");

            var panelId = $(this).attr("id") + "Panel";

            $("#" + panelId).toggleClass("hidden");
            // console.log(toggleActive);
            currentPanelCount = $('.panel').length - $('.hidden').length;
            $(".panel").width(($(window).width()/currentPanelCount) - 13);
            console.log(currentPanelCount)
            // console.log("has default highlight" + $(this).hasClass("highlightButton"));

        }
        );      
}

function updateOutput(){
    $("#outputPanel").contents().find("html").html("<html><head><style type='text/css'>" + $("#cssPanel").val() + "</style></head><body>" + $("#htmlPanel").val() + "</body></html>");

    document.getElementById("outputPanel").contentWindow.eval($("#javascriptPanel").val())

    // eval($("#javascriptPanel").val());
}

buttonHover("#html");
buttonHover("#css");
buttonHover("#javascript");
buttonHover("#output");

buttonClick("#html");
buttonClick("#css");
buttonClick("#javascript");
buttonClick("#output");

//set size of text displays

var totalPanelCount = $('.panel').length;

$(".panel").height($(window).height() - $("#header").height() - 15);  


//make iframe display html

$("#outputPanel").contents().find("html").html($("#htmlPanel").val());
updateOutput();
$("textarea").on("change keyup paste" , function() {
    updateOutput();
})
\$\endgroup\$
1
  • \$\begingroup\$ just a tip: you can use Plunker to setup "file environment" \$\endgroup\$
    – Dejan.S
    Sep 10, 2017 at 10:42

1 Answer 1

1
\$\begingroup\$

Very neat idea for a project! I might have to attempt this myself now.

  1. Define variables as close to their use as possible. Currently both toggleActive and toggleButtonMethod are globals. Neither of these variables need to be global and can thus be scoped to the function they are used in.

  2. Avoid using !important in your CSS if possible. It makes styles harder to change in the future without breaking something. Something that can help with this is to avoid using element ids.

  3. Avoid reinventing the wheel. While you can handle hover events on buttons using JavaScript, it would be much less code to manage it using CSS. $.fn.buttonHoverState can be completely removed with a few changes to the CSS. Just remove the highlightButton and highlightActiveButton classes and add the following:

    .toggleButton:hover {
        background-color: #E5E5E5; /* No need for !important */
    }
    .activeButton:hover {
        background-color: #F5F9FF; /* No need for !important */
    }
    
  4. HTML5 has added several new elements that can be used to better define layouts. In particular the <header> element should replace <div id="header">.

  5. The .unactiveButton class isn't used. Remove it.

  6. It is a good idea to define as much of the layout as possible using CSS. By setting the height of the page with jQuery, the page looks odd when resized.

  7. Building layouts using float becomes exponentially more difficult the more complicated the page becomes. I highly recommend looking into using flexbox to define the page layout. In this case it doesn't cut down too much on the CSS length, but I believe it's still worth the change. (Demo at the end)

  8. checkButton isn't a very descriptive name, I would advise using something that immediately tells readers what the function does. hasClass is a better name, but by removing the hover function this function is no longer needed so it can just be removed.

  9. buttonClick has a lot of code that can be removed if the layout is defined with CSS. After removing the code to handle hovering and setting the width it becomes a very clean function.

    function buttonClick(buttonId) {
        $(buttonId).click(
            function () {
                $(this).toggleClass("activeButton");
    
                var panelId = $(this).attr("id") + "Panel";
    
                $("#" + panelId).toggleClass("hidden");
            }
        );
    }
    

    The main downside to this method is that it heavily relies on the HTML to define the behavior. This isn't a great idea as it ties everything very tightly to the HTML. I believe a better way to handle this would be the following:

    function buttonClick(buttonSelector, panelSelector) {
        $(buttonSelector).click(function() {
            $(this).toggleClass("activeButton");
            $(panelSelector).toggleClass("hidden");
        });
    }
    buttonClick("#html", "#htmlPanel");
    
  10. updateOutput's first line is very long. I would recommend splitting this up a bit to make it easier to read. Template literals can also be used to further simplify the code.

    function updateOutput() {
        let html = $("#htmlPanel").val();
        let css = $("#cssPanel").val();
        let js = $("#javascriptPanel").val();
    
        $("#outputPanel").contents().find("html").html(`<html><head><style>${css}</style></head><body>${html}</body></html>`);
    
        document.getElementById("outputPanel").contentWindow.eval(js);
    }
    

With these changes made, I believe the code has become much simpler to read. Here are a few ideas for further improvement.

  1. Avoid calling updateOutput on every input. It would be better to wait until the user has stopped typing and then call the function. This can be done with a simple debounce function.

  2. Consider catching errors thrown when calling eval on the iframe to avoid cluttering up the devtool console. If you are feeling ambitious, adding a pane in the bottom part of the JavaScript (or output) panel to show errors may be helpful to users.


Here's a demo of the JavaScript with most of my suggestions implemented. The HTML and CSS is the same as in the snippet above. The updating won't work due to cross-origin policies, but showing / hiding elements will.

function buttonClick(buttonSelector, panelSelector) {
    $(buttonSelector).click(function () {
        $(this).toggleClass("activeButton");
        $(panelSelector).toggleClass("hidden");
    });
}

function updateOutput() {
    let html = $("#htmlPanel").val();
    let css = $("#cssPanel").val();
    let js = $("#javascriptPanel").val();

    $("#outputPanel").contents().find("html").html(`<html><head><style>${css}</style></head><body>${html}</body></html>`);

    document.getElementById("outputPanel").contentWindow.eval(js);
}

buttonClick("#html", "#htmlPanel");
buttonClick("#css", "#cssPanel");
buttonClick("#javascript", "#javascriptPanel");
buttonClick("#output", "#outputPanel");

updateOutput();
$("textarea").on("change keyup paste", function () {
    updateOutput();
});
body {
    font-family: sans-serif;
    margin: 0;
}
header {
    display: flex;
    /* Center vertically */
    align-items: center;
    justify-content: space-between;

    height: 2.5em;

    /* Unchanged */
    border-top: solid 1px #BFBFBF;
    border-bottom: solid 1px #BFBFBF;
    background-color: #EEEEEE;
}

.nav-left, .nav-right {
    /* Makes the center of the nav stay in the center */
    flex: 1;
}
/* This could technically be left off as it isn't being used, include for completeness */
.nav-right {
    display: flex;
    justify-content: flex-end;
}

#logo {
    font-weight: bold;
    padding: 0.5em;
}


#buttonContainer {
    display: flex;
}
.toggleButton {
    border: solid 1px #D2D2D2;
    border-right: 0;
    padding: 0.3em;
    margin: 0;
}
/* Instead of targeting #html and #output, use pseudo selectors */
.toggleButton:first-child {
    border-top-left-radius: 0.3em;
    border-bottom-left-radius: 0.3em;
}
.toggleButton:last-child {
    border-right: solid 1px #D2D2D2;
    border-top-right-radius: 0.3em;
    border-bottom-right-radius: 0.3em;
}

.activeButton {
    background-color: #E8F2FF;
}
.toggleButton:hover {
    background-color: #E5E5E5;
}
.activeButton:hover {
    background-color: #F5F9FF;
}

/* Again, flexbox removes the need for floats. */
#bodyContainer {
    display: flex;
    /* Children should fill the height */
    align-items: stretch;
    /* full view height - height of header - border of header */
    height: calc(100vh - 2.5em - 2px);
}

.panel {
    /* Automatically take up as much space as possible */
    flex: 1;
    border: 0;
}
.panel:not(:last-child) {
    border-right: solid 1px #BFBFBF;
}

iframe {
    /* Needed to avoid inconsistent sizing when on small screens */
    min-width: 0; 
}

textarea.panel {
    resize: none;
}

.hidden {
    display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<header>
    <div class="nav-left" id="logo">CodePlayer</div>
    <div id="buttonContainer" class="nav-center">
        <div class="toggleButton activeButton" id="html">HTML</div>
        <div class="toggleButton" id="css">CSS</div>
        <div class="toggleButton" id="javascript">JavaScript</div>
        <div class="toggleButton activeButton" id="output">Output</div>
    </div>
    <div class="nav-right">
        <!-- For positioning, if left off the center won't stay in the center. -->
    </div>
</header>
<div id="bodyContainer">
    <textarea class="panel" id="htmlPanel"></textarea>
    <textarea class="panel hidden" id="cssPanel"></textarea>
    <textarea class="panel hidden" id="javascriptPanel"></textarea>
    <iframe class="panel" id="outputPanel"></iframe>
</div>

\$\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.