TimeScorer

by whelkaholism

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.2/jquery-ui.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.js"></script>
<div class="controls">
    <div class="control style">
        <label>Bar width</label>
        <input type="text" data-bind="textInput: BarWidth" />
    </div>
    <div class="control addbars">
        <label>Add</label>
        <input type="text" data-bind="value: NumberToAdd" />
        <label>bars, </label>
        <input type="text" data-bind="value: BeatsPerBar" />
        <label>beats, </label>
        <input type="text" data-bind="value: TicksPerBeat" />
        <label>ticks</label>
        <button type="button" data-bind="click: addBars">+</button>
        <button type="button" data-bind="click: clearBars">Clear</button>
        <a class="popout" target="_blank" href="http://fiddle.jshell.net/whelkaholism/a6b4uo44/show/">Popout</a>
    </div>
</div>
<div class="score" data-bind="foreach: Bars">
    <table class="bar" data-bind="attr: { style: $parent.BarStyle }">
        <tr class="notes_container" data-bind="foreach: Ticks">
            <td class="notes" data-bind="css: { beat: IsBeat }, attr: { style: TickStyle }"></td>
        </tr>
        <tr class="ticks" data-bind="foreach: Ticks">
            <td data-bind="css: { beat: IsBeat }">
                <span data-bind="text: TickMark"></span>
            </td>
        </tr>
    </table>
</div>

CSS

* {
    font-family: Verdana;
}
.controls {
    margin-bottom: 1em;
}

.controls .popout {
    float:right;
}

.control {
    margin-bottom: 0.5em;
}
.control.addbars input[type=text] {
    width: 2em;
}

.bar {
    float: left;
    width: 100%;
    border-collapse: collapse;
    border: 1px solid #000000;
    margin-bottom: 1em;
    margin-right: -1px;
}

.notes {
    height: 4em;
    border-bottom: 1px solid #909090;
    border-left: 1px dotted #bbbbbb;
}

.notes_container .notes:first-child {
    border-left: none;
}

.ticks .beat {
    font-weight: bold;
    border-left: 1px solid #bbbbbb;
}

.ticks span {
    margin-left: -0.25em;
}

.ticks .beat span {
    margin-left: -0.5em;
}

@media print {
    .controls {
        display: none
    }
    
    .bar {
        page-break-inside: avoid
    }
}

JavaScript

function BarModel(data, mapping)
{
    var _this = this;
    
    if(data)
        ko.mapping.fromJS(data, mapping, _this);
    
    this.BeatsPerBar = this.BeatsPerBar || ko.observable();
    this.TicksPerBeat = this.TicksPerBeat || ko.observable();
    
    this.Ticks = ko.computed(
        function()
        {
            var a = [];
            
            for(var ibeat = 0; ibeat < _this.BeatsPerBar(); ibeat++)
            {
                a.push({ IsBeat: true, TickMark: ibeat + 1 });
                
                for(var itick = 0; itick < _this.TicksPerBeat() - 1; itick++)
                    a.push({ IsBeat: false, TickMark: '-' });                
            }
            
            $.each(a,
                   function()
                   {
                       this.TickStyle = 'width:' + Math.floor(100 / a.length) + '%';
                   }
            );
            
            return a;
        }
    );       
}

function TimeScorerModel(data, mapping)
{
    var _this = this;
    var _mapping = $.extend({}, {}, mapping);
    
    if(data)
        ko.mapping.fromJS(data, _mapping, _this);
    
    this.Bars = this.Bars || ko.observableArray();
  
    this.NumberToAdd = this.NumberToAdd || ko.observable(4);
    this.BeatsPerBar = this.BeatsPerBar || ko.observable(4);
    this.TicksPerBeat = this.TicksPerBeat || ko.observable(2);
    this.BarWidth = this.BarWidth || ko.observable('50%');
    
    //
    this.BarStyle = ko.computed(
        function()
        {
            return 'width:' + _this.BarWidth();
        }
    );
    
    //
    this.addBars = function(model, e)
    {
        var num = !isNaN(parseInt(model)) ? model : _this.NumberToAdd();
        
        for(var i = 0; i < num; i++)
        {
            _this.Bars.push(new BarModel({ 
                BeatsPerBar: _this.BeatsPerBar(),
                TicksPerBeat: _this.TicksPerBeat()
            }, mapping));
        }
    }
    
    this.clearBars = function(model, e)
    {
...