<commit_before>var realWidth = 1000;
var realHeight = 1000;
var paper = new Raphael('drawboard');
var svg = paper.canvas;

svg.setAttribute('viewBox', '0 0 ' + realWidth + ' ' + realHeight);
svg.removeAttribute('width');
svg.removeAttribute('height');

function drawSpacer(x1, y1, x2, y2, size, width, height) {
    size = typeof size !== 'undefined' ? size : 4;
    width = typeof width !== 'undefined' ? width : 2.5*size;
    height = typeof height !== 'undefined' ? height : 3*size;

    var angle = Raphael.angle(x1, y1, x2, y2);
    var angle_diff_1 = Raphael.deg(Math.tan(width/2/height));
    var angle_diff_2 = Raphael.deg(Math.tan(size/2/height));
    var length_1 = Math.hypot(width/2, height);
    var length_2 = Math.hypot(size/2, height);

    var a_outer = Raphael.rad(angle-angle_diff_1);
    var a_outer_m = Raphael.rad(angle+angle_diff_1);
    var a_inner = Raphael.rad(angle-angle_diff_2);
    var a_inner_m = Raphael.rad(angle+angle_diff_2);
    var a_outer_r = Raphael.rad(angle-(180-angle_diff_1));
    var a_outer_r_m = Raphael.rad(angle+(180-angle_diff_1));
    var a_inner_r = Raphael.rad(angle-(180-angle_diff_2));
    var a_inner_r_m = Raphael.rad(angle+(180-angle_diff_2));

    var x_a = x1;
    var y_a = y1;
    var x_b = x1 + Math.cos(a_outer_r) * length_1;
    var y_b = y1 + Math.sin(a_outer_r) * length_1;
    var x_c = x1 + Math.cos(a_inner_r) * length_2;
    var y_c = y1 + Math.sin(a_inner_r) * length_2;
    var x_d = x2 + Math.cos(a_inner) * length_2;
    var y_d = y2 + Math.sin(a_inner) * length_2;
    var x_e = x2 + Math.cos(a_outer) * length_1;
    var y_e = y2 + Math.sin(a_outer) * length_1;
    var x_f = x2;
    var y_f = y2;
    var x_g = x2 + Math.cos(a_outer_m) * length_1;
    var y_g = y2 + Math.sin(a_outer_m) * length_1;
    var x_h = x2 + Math.cos(a_inner_m) * length_2;
    var y_h = y2 + Math.sin(a_inner_m) * length_2;
    var x_i = x1 + Math.cos(a_inner_r_m) * length_2;
    var y_i = y1 + Math.sin(a_inner_r_m) * length_2;
    var x_j = x1 + Math.cos(a_outer_r_m) * length_1;
    var y_j = y1 + Math.sin(a_outer_r_m) * length_1;
    return paper.path(
        "M"+x_a+" "+y_a+
        "L"+x_b+" "+y_b+
        "L"+x_c+" "+y_c+
        "L"+x_d+" "+y_d+
        "L"+x_e+" "+y_e+
        "L"+x_f+" "+y_f+
        "L"+x_g+" "+y_g+
        "L"+x_h+" "+y_h+
        "L"+x_i+" "+y_i+
        "L"+x_j+" "+y_j+
        "z"
    ).attr({'stroke': '0'});
}

function drawSpacerText(text, x, y) {
    return paper.text(x, y, text).attr({
        'font-size': 50
    });
}

function drawA(width, height, a, b) {
    paper.clear();

    var ratio = realWidth/Math.max(width, height);
    var alpha = Math.atan(2*height/width);
    // big shape
    var x1_a = realWidth/2 - (width/2*ratio);
    var y1_a = realHeight/2 + (height/2*ratio);
    var x1_b = x1_a + a*ratio;
    var y1_b = y1_a;
    var x1_c = x1_b + b/Math.tan(alpha)*ratio;
    var y1_c = y1_b - b*ratio;
    var x1_d = realWidth/2 + (width/2 - a - b/Math.tan(alpha))*ratio;
    var y1_d = y1_c;
    var x1_e = x1_d + b/Math.tan(alpha)*ratio;
    var y1_e = y1_b;

    var d = `M ${realWidth/2-(width/2*ratio)},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+a*ratio},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+a*ratio+b*ratio*Math.cos(alpha)},${realHeight/2+(height/2*ratio)-b*ratio*Math.sin(alpha)}`;
    d += ` L ${realWidth/2+(width/2*ratio)-a*ratio-b*ratio*Math.cos(alpha)},${realHeight/2+(height/2*ratio)-b*ratio*Math.sin(alpha)}`;

    d += ` L ${realWidth/2+(width/2*ratio)-a*ratio},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2},${realHeight/2-(height/2*ratio)}`;
    d += `z`;

    // small hole
    d += `M ${realWidth/2},${realHeight/2-(height/2*ratio)+a*ratio*Math.tan(alpha)}`
    d +=` L ${realWidth/2+(width/2*ratio)-a*ratio-((b*Math.sin(alpha)+a*Math.sin(alpha))/Math.tan(alpha)*ratio)},${realHeight/2+(height/2*ratio)-b*ratio*Math.sin(alpha)-a*ratio*Math.sin(alpha)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+a*ratio+((b*Math.sin(alpha)+a*Math.sin(alpha))/Math.tan(alpha)*ratio)},${realHeight/2+(height/2*ratio)-b*ratio*Math.sin(alpha)-a*ratio*Math.sin(alpha)}`;
    d += `z`;
    p = paper.path(d);
    p.node.id = 'raphael-letter';
    p.attr({
        'stroke': 0
    });

    var t = paper.text(realWidth/2-(width/2*ratio)+a*ratio/2, realHeight/2+(height/2*ratio)-30, "a").attr({
        'font-size': 50
    });
    t.node.id = 'raphael-text-light';
    t = paper.text(realWidth/2+(width/2*ratio)-a*ratio-b*ratio*Math.cos(alpha)/2+30, realHeight/2+(height/2*ratio)-b*ratio*Math.sin(alpha)/2, "b").attr({
        'font-size': 50
    });
    t.node.id = 'raphael-text-light';
}

function calculatePerimeterA(width, height, a, b) {
    var alpha = Math.atan(2*height/width);
    var c = Math.sqrt(height*height + width*width);
    var d = width-2*(a+Math.cos(alpha)*b)
    var f = width-2*(a+(b*Math.sin(alpha)+a*Math.sin(alpha))/Math.tan(alpha))
    var e = Math.sqrt(Math.pow(height-b*Math.sin(alpha)-a*Math.sin(alpha)-a*Math.tan(alpha), 2) + Math.pow(f/2, 2));
    var perimeter1 = (2*(a+b+c)+d).toFixed(1);
    var perimeter2 = (2*e+f).toFixed(1);
    $('#perimeter').html('Umfang 1: ' + perimeter1 + 'cm
Umfang 2: ' + perimeter2 + 'cm');
}

function drawE(width, height, a, b, c) {
    paper.clear();

    var ratio = realWidth/Math.max(width, height);
    // big shape
    var d = `M ${realWidth/2-(width/2*ratio)},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2+(height/2*ratio)-(b*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2+(height/2*ratio)-(b*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2+(b/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)-(c*ratio)},${realHeight/2+(b/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)-(c*ratio)},${realHeight/2-(b/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2-(b/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2-(height/2*ratio)+(b*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2-(height/2*ratio)+(b*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2-(height/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)},${realHeight/2-(height/2*ratio)}`;
    d += `z`
    p = paper.path(d);
    p.node.id = 'raphael-letter';
    p.attr({
        'stroke': 0
    });

    // a
    p = drawSpacer(realWidth/2-(width/2*ratio), realHeight/2 - ((height/2*ratio)-(1.5*b*ratio))/2 - (b/2*ratio), realWidth/2-(width/2*ratio)+(a*ratio), realHeight/2 - ((height/2*ratio)-(1.5*b*ratio))/2 - (b/2*ratio));
    p.node.id = 'raphael-spacer-light';
    var t = drawSpacerText('a', realWidth/2-(width/2*ratio)+(a/2*ratio), realHeight/2 - ((height/2*ratio)-(1.5*b*ratio))/2 - (b/2*ratio) - 30);
    t.node.id = 'raphael-text-light';

    // b
    p = drawSpacer(realWidth/2+(width/2*ratio)-((width-a)/2*ratio), realHeight/2 - (height/2*ratio), realWidth/2+(width/2*ratio)-((width-a)/2*ratio), realHeight/2 - (height/2*ratio) + (b*ratio));
    p.node.id = 'raphael-spacer-light';
    t = drawSpacerText('b', realWidth/2+(width/2*ratio)-((width-a)/2*ratio)+30, realHeight/2 - (height/2*ratio) + (b/2*ratio));
    t.node.id = 'raphael-text-light';

    // c
    p = drawSpacer(realWidth/2+(width/2*ratio)-(c*ratio), realHeight/2, realWidth/2+(width/2*ratio),realHeight/2);
    p.node.id = 'raphael-spacer-dark';
    t = drawSpacerText('c', realWidth/2+(width/2*ratio)-(c/2*ratio), realHeight/2-30);
    t.node.id = 'raphael-text-dark';
}

function calculatePerimeterE(width, height, a, b, c) {
    var perimeter = (2*(width+height)+2*(width-a)+2*(width-a-c)).toFixed(1);
    $('#perimeter').html('Umfang: ' + perimeter + 'cm');
}

function recalculateA(width, height) {
    var a = parseFloat($('#a-side-a').val());
    var b = parseFloat($('#a-side-b').val());
    drawA(width, height, a, b);
    calculatePerimeterA(width, height, a, b);
}

function recalculateE(width, height) {
    var a = parseFloat($('#e-side-a').val());
    var b = parseFloat($('#e-side-b').val());
    var c = parseFloat($('#e-side-c').val());
    drawE(width, height, a, b, c);
    calculatePerimeterE(width, height, a, b, c);
}

function recalculate() {
    var selectedLetter = $('#letter-select').val();
    var height = parseFloat($('#height').val());
    var width = parseFloat($('#width').val());
    if (selectedLetter == 'a') {
        recalculateA(width, height);
    } else if (selectedLetter == 'e') {
        recalculateE(width, height);
    }
}

function resizeDrawboard() {
    var $wrapper = $('#drawboard-wrapper');
    var $drawboard = $('#drawboard');
    var width = $wrapper.get(0).getBoundingClientRect().width;
    var height = $wrapper.get(0).getBoundingClientRect().height - 32;
    var widthToSet = Math.min(width, height);
    $drawboard.css('width', widthToSet + 'px');
}

function changeInputs() {
    var selectedLetter = $('#letter-select').val();
    var tabId = '#inputs-' + selectedLetter;
    $('.input-tab').removeClass('active');
    $(tabId).addClass('active');
    recalculate();
    window.location.hash = '#' + selectedLetter;
}

window.onresize = function(event) {
    resizeDrawboard();
};
resizeDrawboard();

$('#input-change').on('change', recalculate);
recalculate();

$('#letter-select').on('change', changeInputs);

if (window.location.hash && window.location.hash.length == 2) {
    $('#letter-select').val(window.location.hash[1]);
    changeInputs()
}<commit_msg>Rework letter A.<commit_after>var realWidth = 1000;
var realHeight = 1000;
var paper = new Raphael('drawboard');
var svg = paper.canvas;

svg.setAttribute('viewBox', '0 0 ' + realWidth + ' ' + realHeight);
svg.removeAttribute('width');
svg.removeAttribute('height');

function drawSpacer(x1, y1, x2, y2, size, width, height) {
    size = typeof size !== 'undefined' ? size : 4;
    width = typeof width !== 'undefined' ? width : 2.5*size;
    height = typeof height !== 'undefined' ? height : 3*size;

    var angle = Raphael.angle(x1, y1, x2, y2);
    var angle_diff_1 = Raphael.deg(Math.tan(width/2/height));
    var angle_diff_2 = Raphael.deg(Math.tan(size/2/height));
    var length_1 = Math.hypot(width/2, height);
    var length_2 = Math.hypot(size/2, height);

    var a_outer = Raphael.rad(angle-angle_diff_1);
    var a_outer_m = Raphael.rad(angle+angle_diff_1);
    var a_inner = Raphael.rad(angle-angle_diff_2);
    var a_inner_m = Raphael.rad(angle+angle_diff_2);
    var a_outer_r = Raphael.rad(angle-(180-angle_diff_1));
    var a_outer_r_m = Raphael.rad(angle+(180-angle_diff_1));
    var a_inner_r = Raphael.rad(angle-(180-angle_diff_2));
    var a_inner_r_m = Raphael.rad(angle+(180-angle_diff_2));

    var x_a = x1;
    var y_a = y1;
    var x_b = x1 + Math.cos(a_outer_r) * length_1;
    var y_b = y1 + Math.sin(a_outer_r) * length_1;
    var x_c = x1 + Math.cos(a_inner_r) * length_2;
    var y_c = y1 + Math.sin(a_inner_r) * length_2;
    var x_d = x2 + Math.cos(a_inner) * length_2;
    var y_d = y2 + Math.sin(a_inner) * length_2;
    var x_e = x2 + Math.cos(a_outer) * length_1;
    var y_e = y2 + Math.sin(a_outer) * length_1;
    var x_f = x2;
    var y_f = y2;
    var x_g = x2 + Math.cos(a_outer_m) * length_1;
    var y_g = y2 + Math.sin(a_outer_m) * length_1;
    var x_h = x2 + Math.cos(a_inner_m) * length_2;
    var y_h = y2 + Math.sin(a_inner_m) * length_2;
    var x_i = x1 + Math.cos(a_inner_r_m) * length_2;
    var y_i = y1 + Math.sin(a_inner_r_m) * length_2;
    var x_j = x1 + Math.cos(a_outer_r_m) * length_1;
    var y_j = y1 + Math.sin(a_outer_r_m) * length_1;
    return paper.path(
        "M"+x_a+" "+y_a+
        "L"+x_b+" "+y_b+
        "L"+x_c+" "+y_c+
        "L"+x_d+" "+y_d+
        "L"+x_e+" "+y_e+
        "L"+x_f+" "+y_f+
        "L"+x_g+" "+y_g+
        "L"+x_h+" "+y_h+
        "L"+x_i+" "+y_i+
        "L"+x_j+" "+y_j+
        "z"
    ).attr({'stroke': '0'});
}

function drawSpacerText(text, x, y) {
    return paper.text(x, y, text).attr({
        'font-size': 50
    });
}

function drawA(width, height, a, b) {
    paper.clear();

    var ratio = realWidth/Math.max(width, height);
    var alpha = Math.atan(2*height/width);
    // big shape
    var widthOffset1 = (width/2)*ratio;
    var widthOffset2 = (width/2-a)*ratio;
    var widthOffset3 = (width/2-a-b/Math.tan(alpha))*ratio;
    var heightOffset1 = (height/2)*ratio;
    var heightOffset2 = (height/2-b)*ratio;
    var dataPointsBig = new Array();
    dataPointsBig.push({'x': realWidth/2 - widthOffset1, 'y': realHeight/2 + heightOffset1});  // a
    dataPointsBig.push({'x': realWidth/2 - widthOffset2, 'y': realHeight/2 + heightOffset1});  // b
    dataPointsBig.push({'x': realWidth/2 - widthOffset3, 'y': realHeight/2 + heightOffset2}); // c
    dataPointsBig.push({'x': realWidth/2 + widthOffset3, 'y': realHeight/2 + heightOffset2}); // d
    dataPointsBig.push({'x': realWidth/2 + widthOffset2, 'y': realHeight/2 + heightOffset1}); // e
    dataPointsBig.push({'x': realWidth/2 + widthOffset1, 'y': realHeight/2 + heightOffset1}); // f
    dataPointsBig.push({'x': realWidth/2, 'y': realHeight/2 - heightOffset1}); // g

    // small hole
    var dataPointsSmall = new Array();
    var widthOffset4 = widthOffset3 - a*Math.sin(alpha)/Math.tan(alpha)*ratio;
    var heightOffset3 = height/2 - a/Math.tan(alpha)*ratio;
    var heightOffset4 = heightOffset2 - a*Math.sin(alpha)*ratio;
    dataPointsSmall.push({'x': realWidth/2, 'y': realHeight/2 + heightOffset3})
    dataPointsSmall.push({'x': realWidth/2 + widthOffset4, 'y': realHeight/2 + heightOffset4})
    dataPointsSmall.push({'x': realWidth/2 - widthOffset4, 'y': realHeight/2 + heightOffset4})

    var d = 'M' + $.map(dataPointsBig, function(el) { return el.x + ' ' + el.y}).join('L') + 'z';
    d += 'M' + $.map(dataPointsSmall, function(el) { return el.x + ' ' + el.y}).join('L') + 'z';

    p = paper.path(d);
    p.node.id = 'raphael-letter';
    p.attr({
        'stroke': 0
    });

    // a
    p = drawSpacer(
        realWidth/2 - (width/2-b/2/Math.tan(alpha))*ratio,
        realHeight/2 + (height/2-b/2)*ratio,
        realWidth/2 - (width/2-b/2/Math.tan(alpha)-a)*ratio,
        realHeight/2 + (height/2-b/2)*ratio);
    p.node.id = 'raphael-spacer-light';
    var t = drawSpacerText('a',
        realWidth/2 - (width/2-b/2/Math.tan(alpha)-a/2)*ratio,
        realHeight/2 + (height/2-b/2)*ratio - 30);
    t.node.id = 'raphael-text-light';

    // b
    p = drawSpacer(
        realWidth/2,
        realHeight/2 + (height/2-b)*ratio,
        realWidth/2,
        realHeight/2 + (height/2)*ratio);
    p.node.id = 'raphael-spacer-dark';
    t = drawSpacerText('b', realWidth/2 + 30, realHeight/2 + (height/2-b/2)*ratio);
    t.node.id = 'raphael-text-dark';
}

function calculatePerimeterA(width, height, a, b) {
    var alpha = Math.atan(2*height/width);
    var b1 = b/Math.sin(alpha)
    var c = Math.sqrt(height*height + width*width);
    var d = width-2*(a+Math.cos(alpha)*b)
    var f = width-2*(a+(b1*Math.sin(alpha)+a*Math.sin(alpha))/Math.tan(alpha))
    var e = Math.sqrt(Math.pow(height-b1*Math.sin(alpha)-a*Math.sin(alpha)-a*Math.tan(alpha), 2) + Math.pow(f/2, 2));
    var perimeter1 = (2*(a+b1+c)+d).toFixed(1);
    var perimeter2 = (2*e+f).toFixed(1);
    $('#perimeter').html('Umfang 1: ' + perimeter1 + 'cm
Umfang 2: ' + perimeter2 + 'cm');
}

function drawE(width, height, a, b, c) {
    paper.clear();

    var ratio = realWidth/Math.max(width, height);
    // big shape
    var d = `M ${realWidth/2-(width/2*ratio)},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2+(height/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2+(height/2*ratio)-(b*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2+(height/2*ratio)-(b*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2+(b/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)-(c*ratio)},${realHeight/2+(b/2*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)-(c*ratio)},${realHeight/2-(b/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2-(b/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)+(a*ratio)},${realHeight/2-(height/2*ratio)+(b*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2-(height/2*ratio)+(b*ratio)}`;
    d += ` L ${realWidth/2+(width/2*ratio)},${realHeight/2-(height/2*ratio)}`;
    d += ` L ${realWidth/2-(width/2*ratio)},${realHeight/2-(height/2*ratio)}`;
    d += `z`
    p = paper.path(d);
    p.node.id = 'raphael-letter';
    p.attr({
        'stroke': 0
    });

    // a
    p = drawSpacer(realWidth/2-(width/2*ratio), realHeight/2 - ((height/2*ratio)-(1.5*b*ratio))/2 - (b/2*ratio), realWidth/2-(width/2*ratio)+(a*ratio), realHeight/2 - ((height/2*ratio)-(1.5*b*ratio))/2 - (b/2*ratio));
    p.node.id = 'raphael-spacer-light';
    var t = drawSpacerText('a', realWidth/2-(width/2*ratio)+(a/2*ratio), realHeight/2 - ((height/2*ratio)-(1.5*b*ratio))/2 - (b/2*ratio) - 30);
    t.node.id = 'raphael-text-light';

    // b
    p = drawSpacer(realWidth/2+(width/2*ratio)-((width-a)/2*ratio), realHeight/2 - (height/2*ratio), realWidth/2+(width/2*ratio)-((width-a)/2*ratio), realHeight/2 - (height/2*ratio) + (b*ratio));
    p.node.id = 'raphael-spacer-light';
    t = drawSpacerText('b', realWidth/2+(width/2*ratio)-((width-a)/2*ratio)+30, realHeight/2 - (height/2*ratio) + (b/2*ratio));
    t.node.id = 'raphael-text-light';

    // c
    p = drawSpacer(realWidth/2+(width/2*ratio)-(c*ratio), realHeight/2, realWidth/2+(width/2*ratio),realHeight/2);
    p.node.id = 'raphael-spacer-dark';
    t = drawSpacerText('c', realWidth/2+(width/2*ratio)-(c/2*ratio), realHeight/2-30);
    t.node.id = 'raphael-text-dark';
}

function calculatePerimeterE(width, height, a, b, c) {
    var perimeter = (2*(width+height)+2*(width-a)+2*(width-a-c)).toFixed(1);
    $('#perimeter').html('Umfang: ' + perimeter + 'cm');
}

function recalculateA(width, height) {
    var a = parseFloat($('#a-side-a').val());
    var b = parseFloat($('#a-side-b').val());
    drawA(width, height, a, b);
    calculatePerimeterA(width, height, a, b);
}

function recalculateE(width, height) {
    var a = parseFloat($('#e-side-a').val());
    var b = parseFloat($('#e-side-b').val());
    var c = parseFloat($('#e-side-c').val());
    drawE(width, height, a, b, c);
    calculatePerimeterE(width, height, a, b, c);
}

function recalculate() {
    var selectedLetter = $('#letter-select').val();
    var height = parseFloat($('#height').val());
    var width = parseFloat($('#width').val());
    if (selectedLetter == 'a') {
        recalculateA(width, height);
    } else if (selectedLetter == 'e') {
        recalculateE(width, height);
    }
}

function resizeDrawboard() {
    var $wrapper = $('#drawboard-wrapper');
    var $drawboard = $('#drawboard');
    var width = $wrapper.get(0).getBoundingClientRect().width;
    var height = $wrapper.get(0).getBoundingClientRect().height - 32;
    var widthToSet = Math.min(width, height);
    $drawboard.css('width', widthToSet + 'px');
}

function changeInputs() {
    var selectedLetter = $('#letter-select').val();
    var tabId = '#inputs-' + selectedLetter;
    $('.input-tab').removeClass('active');
    $(tabId).addClass('active');
    recalculate();
    window.location.hash = '#' + selectedLetter;
}

window.onresize = function(event) {
    resizeDrawboard();
};
resizeDrawboard();

$('#input-change').on('change', recalculate);
recalculate();

$('#letter-select').on('change', changeInputs);

if (window.location.hash && window.location.hash.length == 2) {
    $('#letter-select').val(window.location.hash[1]);
    changeInputs()
}<|endoftext|>


<commit_before>import React from "react"
import { shallow, mount } from "enzyme"
import sinon from "sinon"
import "../../setupTests"
import { InfoPageView } from "./InfoPageView"

describe("InfoPage/InfoPageView", () => {
  let props
  let mountedInfoPageView
  const infoPage = () => {
    if (!mountedInfoPageView) {
      mountedInfoPageView = mount(<InfoPageView {...props} />)
    }
    return mountedInfoPageView
  }

  beforeEach(() => {
    props = {
      page: undefined,
      match: undefined,
      editPage: undefined,
      fetchUserInfo: undefined,
      fetchedUserData: undefined,
      loggedInUser: undefined,
      isAuthenticated: undefined,
    }
    mountedInfoPageView = undefined
  })

  // insert tests here
})
<commit_msg>add more unit tests for InfoPageView<commit_after>import React from "react"
import { shallow, mount } from "enzyme"
import sinon from "sinon"
import "../../setupTests"
import { InfoPageView } from "./InfoPageView"
import { Container } from "styles"
import { Editor } from "draft-js"

describe("InfoPage/InfoPageView", () => {
  let props
  let mountedInfoPageView
  const infoPageView = () => {
    if (!mountedInfoPageView) {
      mountedInfoPageView = shallow(<InfoPageView {...props} />)
    }
    return mountedInfoPageView
  }

  beforeEach(() => {
    props = {
      page: undefined,
      match: undefined,
      editPage: undefined,
      fetchUserInfo: undefined,
      fetchedUserData: undefined,
      loggedInUser: undefined,
      isAuthenticated: undefined,
    }
    mountedInfoPageView = undefined
  })

  describe("initial render", () => {
    beforeEach(() => {
      props = {
        page: {
          data: {
            attributes: {
              content: "page content",
              updated_at: "999",
            },
          },
        },
        match: {
          params: {
            name: "order",
          },
        },
        fetchUserInfo: () => {},
      }
    })

    it("always renders a Container", () => {
      expect(infoPageView().find(Container).length).toBeGreaterThan(0)
    })
    it("always renders an Editor", () => {
      expect(infoPageView().find(Editor).length).toBeGreaterThan(0)
    })
    it("Editor is readOnly", () => {
      const editor = infoPageView().find(Editor)
      expect(editor)
        .props()
        .readOnly.toBe(true)
    })
  })

  describe("user is authenticated with no other permissions", () => {
    beforeEach(() => {
      props = {
        page: {
          data: {
            attributes: {
              content: "page content",
              updated_at: "999",
            },
          },
        },
        match: {
          params: {
            name: "order",
          },
        },
        fetchUserInfo: () => {},
        isAuthenticated: true,
      }
    })
    it("calls componentDidMount", () => {
      sinon.spy(InfoPageView.prototype, "componentDidMount")
      infoPageView()
      expect(InfoPageView.prototype.componentDidMount.calledOnce).toEqual(true)
    })
  })

  describe("user is authenticated with editing privileges and valid JWT", () => {
    // add tests here
  })

  describe("user is authenticated with editing privileges but JWT is invalid", () => {
    // add tests here
  })
})
<|endoftext|>


<commit_before>require.config({
    paths: {
        jquery: 'libs/jquery-min',
        underscore: 'libs/underscore-min',
        backbone: 'libs/backbone-min',
        mustache: 'libs/mustache',
    },
    shim: {
        jquery: {
            exports: '$'
        },
        underscore: {
            exports: '_'
        },
        backbone: {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone'
        }
    },

    // add dummy params to break caching
    urlArgs: '__nocache__=' +  (new Date()).getTime()
});

require([
    'jquery',
    'underscore',
    'backbone',
    'mustache',
    'text!/static/templates/search_bar.html.mustache',
    'text!/static/templates/ac_suggestion.html.mustache'
],
function ($, _, Backbone, Mustache, tmplSearchBar, tmplAcSuggestion) {

// the search bar
var SearchBar = Backbone.Model.extend({
    defaults: {
        query: '',

        // minimum amount of time b/w AJAX calls
        minUpdateIntervalMs: 200,
        updateTimeout: null // id for the last update timeout
    },

    initialize: function () {
        this.view = new SearchBarView({model: this});
        this.acSuggestionList = new AutocompleteSuggestionList();
    },

    updateQuery: function (query) {
        if (query !== this.get('query')) {
            this.set({'query': query});

            // clear any existing update timeout
            if (this.get('updateTimeout') !== null) {
                clearTimeout(this.get('updateTimeout'));
            }

            var self = this;
            var timeoutId = setTimeout(function () {
                self.acSuggestionList.update(query);

                // clear the timeout id once done
                self.set({'updateTimeout': null});

            }, this.get('minUpdateIntervalMs'));

            // store the timeout id for the next update call
            this.set({'updateTimeout': timeoutId});
        }
    }
});

var SearchBarView = Backbone.View.extend({
    template: Mustache.compile(tmplSearchBar),

    events: {
        'keyup input': 'updateQuery',
        'keydown input': 'updateQuery'
    },

    initialize: function () {
        // create the element and add it to the document
        this.setElement($(this.template({placeholder: 'search'})));
        this.$el.appendTo($('body'));

        // cache a ref to the input and focus it
        this.$input = this.$el.find('input');
        this.$input.focus();
    },

    updateQuery: function (e) {
        this.model.updateQuery(this.$input.val());
    }
});

// a single suggestion below the search bar
var AutocompleteSuggestion = Backbone.Model.extend({
    defaults: {
        provider: null,
        suggestion: ''
    }
});

// the suggestions collection below the search bar
var AutocompleteSuggestionList = Backbone.Collection.extend({
    model: AutocompleteSuggestion,
    url: '/search/autocomplete',

    initialize: function () {
        this.view = new AutocompleteSuggestionListView({
            el: $('#search-bar ul'),
            collection: this
        });
    },

    update: function (query) {
        var xhr = $.getJSON(this.url, {'query': query});

        // update the collection on reset
        var collection = this;
        xhr.success(function (data) {
            collection.reset(data.results);
        });
    }
});

var AutocompleteSuggestionListView = Backbone.View.extend({
    template: Mustache.compile(tmplAcSuggestion),

    initialize: function () {
        this.collection.on('reset', this.render, this);
    },

    render: function () {
        // get the suggestion list from the collection
        var suggestions = this.collection.toJSON();

        // clear out the old suggestions
        this.$el.children().remove();

        // add all the new suggestions
        _.each(suggestions, function (suggestion) {
            this.$el.append(this.template(suggestion));
        }, this);

        return this;
    },
});

// ENTRY POINT
$(function () {
    var searchBar = new SearchBar();
});

});
<commit_msg>Rename 'updateTimeout' to 'timeoutId'<commit_after>require.config({
    paths: {
        jquery: 'libs/jquery-min',
        underscore: 'libs/underscore-min',
        backbone: 'libs/backbone-min',
        mustache: 'libs/mustache',
    },
    shim: {
        jquery: {
            exports: '$'
        },
        underscore: {
            exports: '_'
        },
        backbone: {
            deps: ['underscore', 'jquery'],
            exports: 'Backbone'
        }
    },

    // add dummy params to break caching
    urlArgs: '__nocache__=' +  (new Date()).getTime()
});

require([
    'jquery',
    'underscore',
    'backbone',
    'mustache',
    'text!/static/templates/search_bar.html.mustache',
    'text!/static/templates/ac_suggestion.html.mustache'
],
function ($, _, Backbone, Mustache, tmplSearchBar, tmplAcSuggestion) {

// the search bar
var SearchBar = Backbone.Model.extend({
    defaults: {
        query: '',

        // minimum amount of time b/w AJAX calls
        minUpdateIntervalMs: 200,
        timeoutId: null // id for the last update timeout
    },

    initialize: function () {
        this.view = new SearchBarView({model: this});
        this.acSuggestionList = new AutocompleteSuggestionList();
    },

    updateQuery: function (query) {
        if (query !== this.get('query')) {
            this.set({'query': query});

            // clear any existing update timeout
            if (this.get('timeoutId') !== null) {
                clearTimeout(this.get('timeoutId'));
            }

            // set a timeout to update once input is done coming for a bit
            var self = this;
            var timeoutId = setTimeout(function () {
                // update suggesion list and clear the timeout id
                self.acSuggestionList.update(query);
                self.set({'timeoutId': null});

            }, this.get('minUpdateIntervalMs'));

            // store the timeout id for the next update call
            this.set({'timeoutId': timeoutId});
        }
    }
});

var SearchBarView = Backbone.View.extend({
    template: Mustache.compile(tmplSearchBar),

    events: {
        'keyup input': 'updateQuery',
        'keydown input': 'updateQuery'
    },

    initialize: function () {
        // create the element and add it to the document
        this.setElement($(this.template({placeholder: 'search'})));
        this.$el.appendTo($('body'));

        // cache a ref to the input and focus it
        this.$input = this.$el.find('input');
        this.$input.focus();
    },

    updateQuery: function (e) {
        this.model.updateQuery(this.$input.val());
    }
});

// a single suggestion below the search bar
var AutocompleteSuggestion = Backbone.Model.extend({
    defaults: {
        provider: null,
        suggestion: ''
    }
});

// the suggestions collection below the search bar
var AutocompleteSuggestionList = Backbone.Collection.extend({
    model: AutocompleteSuggestion,
    url: '/search/autocomplete',

    initialize: function () {
        this.view = new AutocompleteSuggestionListView({
            el: $('#search-bar ul'),
            collection: this
        });
    },

    update: function (query) {
        var xhr = $.getJSON(this.url, {'query': query});

        // update the collection on reset
        var collection = this;
        xhr.success(function (data) {
            collection.reset(data.results);
        });
    }
});

var AutocompleteSuggestionListView = Backbone.View.extend({
    template: Mustache.compile(tmplAcSuggestion),

    initialize: function () {
        this.collection.on('reset', this.render, this);
    },

    render: function () {
        // get the suggestion list from the collection
        var suggestions = this.collection.toJSON();

        // clear out the old suggestions
        this.$el.children().remove();

        // add all the new suggestions
        _.each(suggestions, function (suggestion) {
            this.$el.append(this.template(suggestion));
        }, this);

        return this;
    },
});

// ENTRY POINT
$(function () {
    var searchBar = new SearchBar();
});

});
<|endoftext|>


<commit_before>import React, { PureComponent } from 'react';
import { graphql, compose } from 'react-apollo';
import styled from 'styled-components';
import Button from './styled/Button';
import Dialog from './Dialog';
import ScheduleTransfer from './ScheduleTransfer';
import { getLocaleAppointment } from '../common/utils';
import ErrorHandler from '../common/ErrorHandler';
import { IMAGE_ENDPOINT } from '../common/constants';
import { UPDATE_ITEM_TRANSFER } from '../mutations/item_transfer';
import { UPDATE_ITEM_REQUEST_STATUS } from '../mutations/item_request';

const Styled = styled.div`
  display: flex;

  > div {
    flex: 1;
    text-align: center;
  }

  .actions {
    padding-top: 6px;
  }
`;

class MessageItemRequestTransfer extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      dialogActive: false,
    };
    this.closeDialog = this.closeDialog.bind(this);
    this.confirmItemTransfer = this.confirmItemTransfer.bind(this);
    this.rescheduleItemTransfer = this.rescheduleItemTransfer.bind(this);
    this.cancelItemTransfer = this.cancelItemTransfer.bind(this);
    this.onScheduleTransfer = this.onScheduleTransfer.bind(this);
  }

  cancelItemTransfer() {
    const {
      itemRequest,
      user,
      refetchItemRequestsTransfer,
      updateItemRequestStatus,
    } = this.props;
    const isOwner = user.id === itemRequest.owner.id;
    let variables = {
      id: itemRequest.id,
    };
    // If the owner cancels the transfer, the item request will move on
    // to a different state than if the requester cancels the transfer.
    // The idea is to have another state for the requester to confirm
    // the cancellation.
    variables.status = (isOwner) ? 'CANCEL' : 'CANCEL_COMPLETE';

    updateItemRequestStatus({ variables })
      .then(refetchItemRequestsTransfer)
      .catch(e => ErrorHandler(e));
  }

  closeDialog() {
    this.setState({ dialogActive: false });
  }

  confirmItemTransfer() {
    const {
      itemRequest: { transfer },
      refetchItemRequestsTransfer,
      updateItemTransfer,
    } = this.props;

    updateItemTransfer({
      variables: {
        id: transfer.id,
        date: transfer.date,
        method: transfer.method,
        requesterApproved: true,
        ownerApproved: true,
      },
    })
      .then(() => refetchItemRequestsTransfer())
      .catch(e => ErrorHandler(e));
  }

  getItemTransferMessage(user, itemRequest) {
    const { transfer } = itemRequest;
    const transferAppointment = getLocaleAppointment(transfer.date);
    const isOwner = user.id === itemRequest.owner.id;
    return isOwner
      ? `${itemRequest.requester
          .name} would like to schedule the transfer of your ${itemRequest.item
          .title} for ${transferAppointment}`
      : `${itemRequest.owner.name} would like to schedule the
          transfer of the ${itemRequest.item.title} for ${transferAppointment}`;
  }

  onScheduleTransfer() {
    this.props.refetchItemRequestsTransfer();
    this.closeDialog();
  }

  rescheduleItemTransfer() {
    this.setState({ dialogActive: true });
  }

  render() {
    const { itemRequest, user, className } = this.props;
    const { dialogActive } = this.state;
    const message = this.getItemTransferMessage(user, itemRequest);

    return (

    );
  }
}

export default compose(
  graphql(UPDATE_ITEM_TRANSFER, { name: 'updateItemTransfer' }),
  graphql(UPDATE_ITEM_REQUEST_STATUS, { name: 'updateItemRequestStatus' }),
)(MessageItemRequestTransfer);
<commit_msg>Confirming an item request should refetch item transfer notifications<commit_after>import React, { PureComponent } from 'react';
import { graphql, compose } from 'react-apollo';
import styled from 'styled-components';
import Button from './styled/Button';
import Dialog from './Dialog';
import ScheduleTransfer from './ScheduleTransfer';
import { getLocaleAppointment } from '../common/utils';
import ErrorHandler from '../common/ErrorHandler';
import { IMAGE_ENDPOINT } from '../common/constants';
import { UPDATE_ITEM_TRANSFER } from '../mutations/item_transfer';
import { UPDATE_ITEM_REQUEST_STATUS } from '../mutations/item_request';
import { ITEM_REQUESTS_CONFIRMED } from '../queries/item_request';

const Styled = styled.div`
  display: flex;

  > div {
    flex: 1;
    text-align: center;
  }

  .actions {
    padding-top: 6px;
  }
`;

class MessageItemRequestTransfer extends PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      dialogActive: false,
    };
    this.closeDialog = this.closeDialog.bind(this);
    this.confirmItemTransfer = this.confirmItemTransfer.bind(this);
    this.rescheduleItemTransfer = this.rescheduleItemTransfer.bind(this);
    this.cancelItemTransfer = this.cancelItemTransfer.bind(this);
    this.onScheduleTransfer = this.onScheduleTransfer.bind(this);
  }

  cancelItemTransfer() {
    const {
      itemRequest,
      user,
      refetchItemRequestsTransfer,
      updateItemRequestStatus,
    } = this.props;
    const isOwner = user.id === itemRequest.owner.id;
    let variables = {
      id: itemRequest.id,
    };
    // If the owner cancels the transfer, the item request will move on
    // to a different state than if the requester cancels the transfer.
    // The idea is to have another state for the requester to confirm
    // the cancellation.
    variables.status = isOwner ? 'CANCEL' : 'CANCEL_COMPLETE';

    updateItemRequestStatus({ variables })
      .then(refetchItemRequestsTransfer)
      .catch(e => ErrorHandler(e));
  }

  closeDialog() {
    this.setState({ dialogActive: false });
  }

  confirmItemTransfer() {
    const {
      itemRequest: { transfer },
      refetchItemRequestsTransfer,
      updateItemTransfer,
    } = this.props;

    updateItemTransfer({
      variables: {
        id: transfer.id,
        date: transfer.date,
        method: transfer.method,
        requesterApproved: true,
        ownerApproved: true,
      },
    })
      .then(refetchItemRequestsTransfer)
      .catch(e => ErrorHandler(e));
  }

  getItemTransferMessage(user, itemRequest) {
    const { transfer } = itemRequest;
    const transferAppointment = getLocaleAppointment(transfer.date);
    const isOwner = user.id === itemRequest.owner.id;
    return isOwner
      ? `${itemRequest.requester
          .name} would like to schedule the transfer of your ${itemRequest.item
          .title} for ${transferAppointment}`
      : `${itemRequest.owner.name} would like to schedule the
          transfer of the ${itemRequest.item.title} for ${transferAppointment}`;
  }

  onScheduleTransfer() {
    this.props.refetchItemRequestsTransfer();
    this.closeDialog();
  }

  rescheduleItemTransfer() {
    this.setState({ dialogActive: true });
  }

  render() {
    const { itemRequest, user, className } = this.props;
    const { dialogActive } = this.state;
    const message = this.getItemTransferMessage(user, itemRequest);

    return (

    );
  }
}

export default compose(
  graphql(UPDATE_ITEM_TRANSFER, {
    name: 'updateItemTransfer',
    options: ({ user }) => ({
      refetchQueries: [
        {
          query: ITEM_REQUESTS_CONFIRMED,
          variables: {
            uid: user.id,
          },
        },
      ],
    }),
  }),
  graphql(UPDATE_ITEM_REQUEST_STATUS, { name: 'updateItemRequestStatus' }),
)(MessageItemRequestTransfer);
<|endoftext|>


<commit_before>"use strict";

function shortUID() {
  return ('0000' + (Math.random() * Math.pow(36,4) << 0).toString(36)).substr(-4);
}

var app = angular.module('twitto-feels', ['ngRoute', 'ui.bootstrap', 'google-maps']);

app.config(function($routeProvider) {
  $routeProvider.when('/', {
    templateUrl: '/partials/home.html',
  }).when('/create_topic', {
    templateUrl: '/partials/create_topic.html',
    controller: 'CreateTopicCtrl'
  }).when('/view_topic/:topicId', {
    templateUrl: '/partials/view_topic.html',
    controller: 'ViewTopicCtrl'
  }).otherwise({
    redirectTo: '/'
  });
});

app.directive('ngEnter', function() {
  return function($scope, elem, attrs) {
    elem.bind("keydown keypress", function(evt) {
      if (evt.which === 13) {
        $scope.$apply(function (){
          $scope.$eval(attrs.ngEnter);
        });
        evt.preventDefault();
      }
    });
  };
});

app.factory('ApiService', ['$q', '$http', function($q, $http) {
  function Service(baseURL) {
    this.baseURL = '/api' + baseURL;
  }

  Service.prototype.get = function(id) {
    var deferred = $q.defer();
    $http.get((id !== undefined) ? this.baseURL + '/' + id : this.baseURL)
      .success(function(obj) { deferred.resolve(obj); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  Service.prototype.post = function(obj) {
    var deferred = $q.defer();
    $http.post(this.baseURL, obj)
      .success(function(obj) { deferred.resolve(obj); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  Service.prototype.put = function(obj) {
    var deferred = $q.defer();
    $http.put(this.baseURL + '/' + obj._id.$oid, obj)
      .success(function(obj) { deferred.resolve(obj); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  Service.prototype.delete = function(obj) {
    var deferred = $q.defer();
    $http.delete(this.baseURL + '/' + obj._id.$oid)
      .success(function() { deferred.resolve(); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  return Service;
}]);

function makeBasicApiService(name, url) {
  return app.factory(name, ['ApiService', function(ApiService) {
    return new ApiService(url);
  }]);
}

makeBasicApiService('TopicsService', '/topics');
makeBasicApiService('TweetsService', '/tweets');

app.factory('FlashService', ['$timeout', function($timeout) {
  var flash = {};

  // Messages container
  flash.messages = [];

  // Possible types for flash messages
  flash.possibleTypes = ['success', 'danger', 'warning', 'info'];
  // and "none" type
  flash.noneType = 'none';

  // Timeout for message dismissal, in seconds
  flash.timeout = 1;

  flash.add = function(msg, type) {
    var self = this;

    // Default arguments
    var obj = {};
    if (type === undefined) {
      obj = (typeof(msg) == 'string') ? { text: msg } : msg;
    } else {
      obj.text = msg;
      obj.type = type;
    }

    // Check "type" member
    if (!obj.type || self.possibleTypes.indexOf(obj.type) == -1) {
      obj.type = self.noneType;
    }

    // Add UID
    obj.uid = shortUID();

    // Add "dismiss" method for flash messages
    obj.dismiss = function() {
      for (var i = 0; i < self.messages.length; i++) {
        if (self.messages[i].uid == obj.uid) {
          self.messages.splice(i, 1);
          break;
        }
      }
    };
    self.messages.push(obj);

    // Finally, schedule dismissal
    if (self.timeout > 0) {
      $timeout(function() { obj.dismiss(); }, self.timeout * 1000);
    }
  };

  return flash;
}]);

app.controller('MainCtrl', function($scope, $http, TopicsService, TweetsService, FlashService) {
  $scope.topics = [];
  $scope.tweets = [];
  $scope.flashMessages = FlashService.messages;

  TopicsService.get().then(function(topics) {
    $scope.topics = topics;
  }, function() {
    FlashService.add('Topics loading failed', 'danger');
  });

  TweetsService.get().then(function(tweets) {
    $scope.tweets = tweets;
  }, function() {
    FlashService.add('Tweets loading failed', 'danger');
  });
});

app.controller('ViewTopicCtrl', function($scope, $routeParams, $location,
      $modal, TopicsService, FlashService) {

  // Find "current" topic
  angular.forEach($scope.topics, function(topic, index) {
    if (topic._id.$oid == $routeParams.topicId) {
      $scope.topicIndex = index;
      $scope.topic = topic;

      // Add "tweets" member for topic
      if (topic.tweets) { return; }
      topic.tweets = [];
      angular.forEach($scope.tweets, function(tweet) {
        if (tweet.topic.$oid == topic._id.$oid) {
          topic.tweets.push(tweet);
        }
      });
    }
  });

  $scope.requestDelete = function() {
    var modalInstance = $modal.open({
      templateUrl: 'confirm_topic_deletion.html',
      controller: function($scope, $modalInstance) {
        $scope.confirm = function () {
          $modalInstance.close(true);
        };

        $scope.cancel = function () {
          $modalInstance.dismiss('cancel');
        };
      }
    });

    modalInstance.result.then(function(confirmed) {
      if (!confirmed) { return; }
      TopicsService.delete($scope.topic).then(function() {
        $scope.topics.splice($scope.topicIndex, 1);
        $location.path('/');
      }, function() {
        FlashService.add('An error occurred while deleting the given topic', 'danger');
      });
    });
  };
});

app.controller('CreateTopicCtrl', function($scope, $location,
      TopicsService, FlashService) {
  $scope.name = '';

  // Tags
  $scope.tags = [];

  $scope.currentTag = '';
  $scope.addTag = function(tag) {
    tag = tag.toLowerCase().replace(/[^\w]/gi, '');
    if (!tag) {
      FlashService.add('Cannot add empty tag', 'warning');
    } else if ($scope.tags.indexOf(tag) != -1) {
      FlashService.add('Tag "' + tag + '" already added', 'warning');
    } else {
      $scope.tags.push(tag);
      $scope.currentTag = '';
    }
  };
  $scope.removeTag = function(index) {
    $scope.tags.splice(index, 1);
  };

  // Locations
  $scope.map = {
    center: { latitude: 33.678176, longitude: -116.242568 },
    zoom: 11,
    events: {
      tilesloaded: function(map) {
        //$scope.$apply(function() {});
        $scope.map.instance = map;
      }
    }, options: {
      mapTypeId: google.maps.MapTypeId.TERRAIN
    }
  };

  $scope.locations = [];

  $scope.addLocation = function() {
    if (!$scope.map.instance) {
      FlashService.add('Please wait for map to load !', 'warning');
    }

    var rectangle = new google.maps.Rectangle({
      map: $scope.map.instance,
      strokeColor: '#428bca', strokeOpacity: 0.8, strokeWeight: 2,
      fillColor: '#428bca', fillOpacity: 0.35,
      bounds: new google.maps.LatLngBounds(
        new google.maps.LatLng(33.671068, -116.25128),
        new google.maps.LatLng(33.685282, -116.233942)),
      editable: true,
      draggable: true
    });

    var loc = {
      uid: shortUID(),
      rectangle: rectangle,
      sw: {
        lat: rectangle.bounds.getSouthWest().lat(),
        lng: rectangle.bounds.getSouthWest().lng()
      }, ne: {
        lat: rectangle.bounds.getNorthEast().lat(),
        lng: rectangle.bounds.getNorthEast().lng()
      }
    };

    google.maps.event.addListener(rectangle, 'bounds_changed', function() {
      $scope.$apply(function() {
        loc.sw.lat = rectangle.bounds.getSouthWest().lat();
        loc.sw.lng = rectangle.bounds.getSouthWest().lng();
        loc.ne.lat = rectangle.bounds.getNorthEast().lat();
        loc.ne.lng = rectangle.bounds.getNorthEast().lng();
      });
    });

    google.maps.event.addListener(rectangle, 'click', function() {
      $scope.$apply(function() {
        $scope.selectLocation(loc);
      });
    });

    $scope.locations.push(loc);
  };

  $scope.selectedLocation = null;
  $scope.selectLocation = function(loc) {
    $scope.selectedLocation = loc;
  };
  $scope.removeSelectedLocation = function() {
    if (!$scope.selectedLocation) { return; }
    $scope.removeLocation($scope.selectedLocation);
    $scope.selectedLocation = null;
  };

  $scope.removeLocation = function(loc) {
    for (var i = 0; i < $scope.locations.length; i++) {
      if ($scope.locations[i].uid == loc.uid) {
        loc.rectangle.setMap(null);
        $scope.locations.splice(i,  1);
        break;
      }
    }
  };

  // Form validation
  $scope.submit = function() {
    var valid = true;

    // Validate name
    if (!$scope.name) {
      FlashService.add('No name provided for the topic', 'danger');
      valid = false;
    }

    // Validate tags
    if (!$scope.tags.length) {
      FlashService.add('No tags provided for the topic', 'danger');
      valid = false;
    }

    if (valid) { $scope.$submit(); }
  };
  // form submission
  $scope.$submit = function() {
    TopicsService.post({ name: $scope.name, tags: $scope.tags }).then(function(topic) {
      $scope.topics.push(topic);
      $location.path('/view_topic/' + topic._id.$oid);
    }, function() {
      FlashService.add('An error occurred while creating the given topic', 'danger');
    });
  };
});
<commit_msg>Locations -> added<commit_after>"use strict";

function shortUID() {
  return ('0000' + (Math.random() * Math.pow(36,4) << 0).toString(36)).substr(-4);
}

var app = angular.module('twitto-feels', ['ngRoute', 'ui.bootstrap', 'google-maps']);

app.config(function($routeProvider) {
  $routeProvider.when('/', {
    templateUrl: '/partials/home.html',
  }).when('/create_topic', {
    templateUrl: '/partials/create_topic.html',
    controller: 'CreateTopicCtrl'
  }).when('/view_topic/:topicId', {
    templateUrl: '/partials/view_topic.html',
    controller: 'ViewTopicCtrl'
  }).otherwise({
    redirectTo: '/'
  });
});

app.directive('ngEnter', function() {
  return function($scope, elem, attrs) {
    elem.bind("keydown keypress", function(evt) {
      if (evt.which === 13) {
        $scope.$apply(function (){
          $scope.$eval(attrs.ngEnter);
        });
        evt.preventDefault();
      }
    });
  };
});

app.factory('ApiService', ['$q', '$http', function($q, $http) {
  function Service(baseURL) {
    this.baseURL = '/api' + baseURL;
  }

  Service.prototype.get = function(id) {
    var deferred = $q.defer();
    $http.get((id !== undefined) ? this.baseURL + '/' + id : this.baseURL)
      .success(function(obj) { deferred.resolve(obj); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  Service.prototype.post = function(obj) {
    var deferred = $q.defer();
    $http.post(this.baseURL, obj)
      .success(function(obj) { deferred.resolve(obj); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  Service.prototype.put = function(obj) {
    var deferred = $q.defer();
    $http.put(this.baseURL + '/' + obj._id.$oid, obj)
      .success(function(obj) { deferred.resolve(obj); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  Service.prototype.delete = function(obj) {
    var deferred = $q.defer();
    $http.delete(this.baseURL + '/' + obj._id.$oid)
      .success(function() { deferred.resolve(); })
      .error(function() { deferred.reject(); })
    ;
    return deferred.promise;
  };

  return Service;
}]);

function makeBasicApiService(name, url) {
  return app.factory(name, ['ApiService', function(ApiService) {
    return new ApiService(url);
  }]);
}

makeBasicApiService('TopicsService', '/topics');
makeBasicApiService('TweetsService', '/tweets');

app.factory('FlashService', ['$timeout', function($timeout) {
  var flash = {};

  // Messages container
  flash.messages = [];

  // Possible types for flash messages
  flash.possibleTypes = ['success', 'danger', 'warning', 'info'];
  // and "none" type
  flash.noneType = 'none';

  // Timeout for message dismissal, in seconds
  flash.timeout = 1;

  flash.add = function(msg, type) {
    var self = this;

    // Default arguments
    var obj = {};
    if (type === undefined) {
      obj = (typeof(msg) == 'string') ? { text: msg } : msg;
    } else {
      obj.text = msg;
      obj.type = type;
    }

    // Check "type" member
    if (!obj.type || self.possibleTypes.indexOf(obj.type) == -1) {
      obj.type = self.noneType;
    }

    // Add UID
    obj.uid = shortUID();

    // Add "dismiss" method for flash messages
    obj.dismiss = function() {
      for (var i = 0; i < self.messages.length; i++) {
        if (self.messages[i].uid == obj.uid) {
          self.messages.splice(i, 1);
          break;
        }
      }
    };
    self.messages.push(obj);

    // Finally, schedule dismissal
    if (self.timeout > 0) {
      $timeout(function() { obj.dismiss(); }, self.timeout * 1000);
    }
  };

  return flash;
}]);

app.controller('MainCtrl', function($scope, $http, TopicsService, TweetsService, FlashService) {
  $scope.topics = [];
  $scope.tweets = [];
  $scope.flashMessages = FlashService.messages;

  TopicsService.get().then(function(topics) {
    $scope.topics = topics;
  }, function() {
    FlashService.add('Topics loading failed', 'danger');
  });

  TweetsService.get().then(function(tweets) {
    $scope.tweets = tweets;
  }, function() {
    FlashService.add('Tweets loading failed', 'danger');
  });
});

app.controller('ViewTopicCtrl', function($scope, $routeParams, $location,
      $modal, TopicsService, FlashService) {

  // Find "current" topic
  angular.forEach($scope.topics, function(topic, index) {
    if (topic._id.$oid == $routeParams.topicId) {
      $scope.topicIndex = index;
      $scope.topic = topic;

      // Add "tweets" member for topic
      if (topic.tweets) { return; }
      topic.tweets = [];
      angular.forEach($scope.tweets, function(tweet) {
        if (tweet.topic.$oid == topic._id.$oid) {
          topic.tweets.push(tweet);
        }
      });
    }
  });

  $scope.requestDelete = function() {
    var modalInstance = $modal.open({
      templateUrl: 'confirm_topic_deletion.html',
      controller: function($scope, $modalInstance) {
        $scope.confirm = function () {
          $modalInstance.close(true);
        };

        $scope.cancel = function () {
          $modalInstance.dismiss('cancel');
        };
      }
    });

    modalInstance.result.then(function(confirmed) {
      if (!confirmed) { return; }
      TopicsService.delete($scope.topic).then(function() {
        $scope.topics.splice($scope.topicIndex, 1);
        $location.path('/');
      }, function() {
        FlashService.add('An error occurred while deleting the given topic', 'danger');
      });
    });
  };
});

app.controller('CreateTopicCtrl', function($scope, $location,
      TopicsService, FlashService) {
  $scope.name = '';

  // Tags
  $scope.tags = [];

  $scope.currentTag = '';
  $scope.addTag = function(tag) {
    tag = tag.toLowerCase().replace(/[^\w]/gi, '');
    if (!tag) {
      FlashService.add('Cannot add empty tag', 'warning');
    } else if ($scope.tags.indexOf(tag) != -1) {
      FlashService.add('Tag "' + tag + '" already added', 'warning');
    } else {
      $scope.tags.push(tag);
      $scope.currentTag = '';
    }
  };
  $scope.removeTag = function(index) {
    $scope.tags.splice(index, 1);
  };

  // Locations
  $scope.map = {
    center: { latitude: 33.678176, longitude: -116.242568 },
    zoom: 11,
    events: {
      tilesloaded: function(map) {
        //$scope.$apply(function() {});
        $scope.map.instance = map;
      }
    }, options: {
      mapTypeId: google.maps.MapTypeId.TERRAIN
    }
  };

  $scope.locations = [];

  $scope.addLocation = function() {
    if (!$scope.map.instance) {
      FlashService.add('Please wait for map to load !', 'warning');
    }

    var rectangle = new google.maps.Rectangle({
      map: $scope.map.instance,
      strokeColor: '#428bca', strokeOpacity: 0.8, strokeWeight: 2,
      fillColor: '#428bca', fillOpacity: 0.35,
      bounds: $scope.map.instance.getBounds(),
      editable: true,
      draggable: true
    });

    var loc = {
      uid: shortUID(),
      rectangle: rectangle,
      sw: {
        lat: rectangle.bounds.getSouthWest().lat(),
        lng: rectangle.bounds.getSouthWest().lng()
      }, ne: {
        lat: rectangle.bounds.getNorthEast().lat(),
        lng: rectangle.bounds.getNorthEast().lng()
      }
    };

    google.maps.event.addListener(rectangle, 'bounds_changed', function() {
      $scope.$apply(function() {
        loc.sw.lat = rectangle.bounds.getSouthWest().lat();
        loc.sw.lng = rectangle.bounds.getSouthWest().lng();
        loc.ne.lat = rectangle.bounds.getNorthEast().lat();
        loc.ne.lng = rectangle.bounds.getNorthEast().lng();
      });
    });

    google.maps.event.addListener(rectangle, 'click', function() {
      $scope.$apply(function() {
        $scope.selectLocation(loc);
      });
    });

    $scope.locations.push(loc);
  };

  $scope.selectedLocation = null;
  $scope.selectLocation = function(loc) {
    $scope.selectedLocation = loc;
  };
  $scope.removeSelectedLocation = function() {
    if (!$scope.selectedLocation) { return; }
    $scope.removeLocation($scope.selectedLocation);
    $scope.selectedLocation = null;
  };

  $scope.removeLocation = function(loc) {
    for (var i = 0; i < $scope.locations.length; i++) {
      if ($scope.locations[i].uid == loc.uid) {
        loc.rectangle.setMap(null);
        $scope.locations.splice(i,  1);
        break;
      }
    }
  };

  // Form validation
  $scope.submit = function() {
    var valid = true;

    // Validate name
    if (!$scope.name) {
      FlashService.add('No name provided for the topic', 'danger');
      valid = false;
    }

    // Validate tags
    if (!$scope.tags.length) {
      FlashService.add('No tags provided for the topic', 'danger');
      valid = false;
    }

    if (valid) { $scope.$submit(); }
  };
  // form submission
  $scope.$submit = function() {
    TopicsService.post({
      name: $scope.name,
      tags: $scope.tags,
      locations: (function() {
        var locations = [];
        angular.forEach($scope.locations, function(loc) {
          locations.push(loc.sw.lat, loc.sw.lng, loc.ne.lat, loc.ne.lng);
        });
        console.log(locations);
        return locations;
      })()
    }).then(function(topic) {
      $scope.topics.push(topic);
      $location.path('/view_topic/' + topic._id.$oid);
    }, function() {
      FlashService.add('An error occurred while creating the given topic', 'danger');
    });
  };
});
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

// @flow

import React, { Component } from "react";
import classnames from "classnames";
import { connect } from "react-redux";
import * as I from "immutable";
import { createSelector } from "reselect";
import { groupBy, sortBy } from "lodash";

import Breakpoint from "./Breakpoint";

import actions from "../../actions";
import { getFilenameFromURL, getRawSourceURL } from "../../utils/source";
import {
  getSources,
  getSourceInSources,
  getBreakpoints,
  getPauseReason,
  getSelectedSource,
  getTopFrame
} from "../../selectors";
import { isInterrupted } from "../../utils/pause";
import { makeLocationId } from "../../utils/breakpoint";
import showContextMenu from "./BreakpointsContextMenu";

import type {
  Breakpoint as BreakpointType,
  Location,
  Source,
  Frame,
  Why
} from "../../types";

import type { SourcesMap, SourceMetaDataMap } from "../../reducers/types";

import "./Breakpoints.css";

export type LocalBreakpoint = BreakpointType & {
  location: Location,
  isCurrentlyPaused: boolean,
  locationId: string,
  source: Source
};

type BreakpointsMap = I.Map<string, LocalBreakpoint>;

type Props = {
  breakpoints: BreakpointsMap,
  sources: SourcesMap,
  selectedSource: Source,
  selectSource: String => void,
  sourcesMetaData: SourceMetaDataMap,
  enableBreakpoint: Location => void,
  disableBreakpoint: Location => void,
  selectLocation: Object => void,
  removeBreakpoint: string => void,
  removeAllBreakpoints: () => void,
  removeBreakpoints: BreakpointsMap => void,
  toggleBreakpoints: (boolean, BreakpointsMap) => void,
  toggleAllBreakpoints: boolean => void,
  toggleDisabledBreakpoint: number => void,
  setBreakpointCondition: Location => void,
  openConditionalPanel: number => void,
  shouldPauseOnExceptions: boolean,
  shouldPauseOnCaughtExceptions: boolean,
  pauseOnExceptions: Function
};

function isCurrentlyPausedAtBreakpoint(
  frame: Frame,
  why: Why,
  breakpoint: LocalBreakpoint
) {
  if (!frame || !isInterrupted(why)) {
    return false;
  }

  const bpId = makeLocationId(breakpoint.location);
  const pausedId = makeLocationId(frame.location);
  return bpId === pausedId;
}

function createExceptionOption(
  label: string,
  value: boolean,
  onChange: Function,
  className: string
) {
  return (

      <input
        type="checkbox"
        checked={value ? "checked" : ""}
        onChange={e => e.stopPropagation() && onChange()}
      />

{label}

  );
}

function sortFilenames(urlA, urlB) {
  const filenameA = getFilenameFromURL(urlA);
  const filenameB = getFilenameFromURL(urlB);

  if (filenameA > filenameB) {
    return 1;
  }
  if (filenameA < filenameB) {
    return -1;
  }

  return 0;
}

class Breakpoints extends Component<Props> {
  handleBreakpointCheckbox(breakpoint) {
    if (breakpoint.loading) {
      return;
    }

    if (breakpoint.disabled) {
      this.props.enableBreakpoint(breakpoint.location);
    } else {
      this.props.disableBreakpoint(breakpoint.location);
    }
  }

  selectBreakpoint(breakpoint) {
    this.props.selectLocation(breakpoint.location);
  }

  removeBreakpoint(event, breakpoint) {
    event.stopPropagation();
    this.props.removeBreakpoint(breakpoint.location);
  }

  renderBreakpoint(breakpoint) {
    const { selectedSource } = this.props;

    return (
      <Breakpoint
        key={breakpoint.locationId}
        breakpoint={breakpoint}
        selectedSource={selectedSource}
        onClick={() => this.selectBreakpoint(breakpoint)}
        onContextMenu={e =>
          showContextMenu({ ...this.props, breakpoint, contextMenuEvent: e })
        }
        onChange={() => this.handleBreakpointCheckbox(breakpoint)}
        onCloseClick={ev => this.removeBreakpoint(ev, breakpoint)}
      />
    );
  }

  renderExceptionsOptions() {
    const {
      breakpoints,
      shouldPauseOnExceptions,
      shouldPauseOnCaughtExceptions,
      pauseOnExceptions
    } = this.props;

    const isEmpty = breakpoints.size == 0;

    const exceptionsBox = createExceptionOption(
      L10N.getStr("pauseOnExceptionsItem"),
      shouldPauseOnExceptions,
      () => pauseOnExceptions(!shouldPauseOnExceptions, false),
      "breakpoints-exceptions"
    );

    const ignoreCaughtBox = createExceptionOption(
      L10N.getStr("pauseOnCaughtExceptionsItem"),
      shouldPauseOnCaughtExceptions,
      () => pauseOnExceptions(true, !shouldPauseOnCaughtExceptions),
      "breakpoints-exceptions-caught"
    );

    return (

        {exceptionsBox}
        {shouldPauseOnExceptions ? ignoreCaughtBox : null}

    );
  }

  renderBreakpoints() {
    const { breakpoints } = this.props;
    if (breakpoints.size == 0) {
      return;
    }

    const groupedBreakpoints = groupBy(
      sortBy([...breakpoints.valueSeq()], bp => bp.location.line),
      bp => getRawSourceURL(bp.source.url)
    );

    return [
      ...Object.keys(groupedBreakpoints)
        .sort(sortFilenames)
        .map(url => {
          const file = getFilenameFromURL(url);
          const groupBreakpoints = groupedBreakpoints[url].filter(
            bp => !bp.hidden && (bp.text || bp.originalText)
          );

          if (!groupBreakpoints.length) {
            return null;
          }

          return [

                this.props.selectSource(groupBreakpoints[0].source.id)
              }
            >
              {file}

,
            ...groupBreakpoints.map(bp => this.renderBreakpoint(bp))
          ];
        })
    ];
  }

  render() {
    return (

        {this.renderExceptionsOptions()}
        {this.renderBreakpoints()}

    );
  }
}

function updateLocation(sources, frame, why, bp): LocalBreakpoint {
  const source = getSourceInSources(sources, bp.location.sourceId);
  const isCurrentlyPaused = isCurrentlyPausedAtBreakpoint(frame, why, bp);
  const locationId = makeLocationId(bp.location);
  const localBP = { ...bp, locationId, isCurrentlyPaused, source };

  return localBP;
}

const _getBreakpoints = createSelector(
  getBreakpoints,
  getSources,
  getTopFrame,
  getPauseReason,
  (breakpoints, sources, frame, why) =>
    breakpoints
      .map(bp => updateLocation(sources, frame, why, bp))
      .filter(bp => bp.source && !bp.source.isBlackBoxed)
);

const mapStateToProps = state => ({
  breakpoints: _getBreakpoints(state),
  selectedSource: getSelectedSource(state)
});

export default connect(mapStateToProps, actions)(Breakpoints);
<commit_msg>Prevent querystring from displaying in breakpoints header (#6314)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

// @flow

import React, { Component } from "react";
import classnames from "classnames";
import { connect } from "react-redux";
import * as I from "immutable";
import { createSelector } from "reselect";
import { groupBy, sortBy } from "lodash";

import Breakpoint from "./Breakpoint";

import actions from "../../actions";
import {
  getFilename,
  getFilenameFromURL,
  getRawSourceURL
} from "../../utils/source";
import {
  getSources,
  getSourceInSources,
  getBreakpoints,
  getPauseReason,
  getSelectedSource,
  getTopFrame
} from "../../selectors";
import { isInterrupted } from "../../utils/pause";
import { makeLocationId } from "../../utils/breakpoint";
import showContextMenu from "./BreakpointsContextMenu";

import type {
  Breakpoint as BreakpointType,
  Location,
  Source,
  Frame,
  Why
} from "../../types";

import type { SourcesMap, SourceMetaDataMap } from "../../reducers/types";

import "./Breakpoints.css";

export type LocalBreakpoint = BreakpointType & {
  location: Location,
  isCurrentlyPaused: boolean,
  locationId: string,
  source: Source
};

type BreakpointsMap = I.Map<string, LocalBreakpoint>;

type Props = {
  breakpoints: BreakpointsMap,
  sources: SourcesMap,
  selectedSource: Source,
  selectSource: String => void,
  sourcesMetaData: SourceMetaDataMap,
  enableBreakpoint: Location => void,
  disableBreakpoint: Location => void,
  selectLocation: Object => void,
  removeBreakpoint: string => void,
  removeAllBreakpoints: () => void,
  removeBreakpoints: BreakpointsMap => void,
  toggleBreakpoints: (boolean, BreakpointsMap) => void,
  toggleAllBreakpoints: boolean => void,
  toggleDisabledBreakpoint: number => void,
  setBreakpointCondition: Location => void,
  openConditionalPanel: number => void,
  shouldPauseOnExceptions: boolean,
  shouldPauseOnCaughtExceptions: boolean,
  pauseOnExceptions: Function
};

function isCurrentlyPausedAtBreakpoint(
  frame: Frame,
  why: Why,
  breakpoint: LocalBreakpoint
) {
  if (!frame || !isInterrupted(why)) {
    return false;
  }

  const bpId = makeLocationId(breakpoint.location);
  const pausedId = makeLocationId(frame.location);
  return bpId === pausedId;
}

function createExceptionOption(
  label: string,
  value: boolean,
  onChange: Function,
  className: string
) {
  return (

      <input
        type="checkbox"
        checked={value ? "checked" : ""}
        onChange={e => e.stopPropagation() && onChange()}
      />

{label}

  );
}

function sortFilenames(urlA, urlB) {
  const filenameA = getFilenameFromURL(urlA);
  const filenameB = getFilenameFromURL(urlB);

  if (filenameA > filenameB) {
    return 1;
  }
  if (filenameA < filenameB) {
    return -1;
  }

  return 0;
}

class Breakpoints extends Component<Props> {
  handleBreakpointCheckbox(breakpoint) {
    if (breakpoint.loading) {
      return;
    }

    if (breakpoint.disabled) {
      this.props.enableBreakpoint(breakpoint.location);
    } else {
      this.props.disableBreakpoint(breakpoint.location);
    }
  }

  selectBreakpoint(breakpoint) {
    this.props.selectLocation(breakpoint.location);
  }

  removeBreakpoint(event, breakpoint) {
    event.stopPropagation();
    this.props.removeBreakpoint(breakpoint.location);
  }

  renderBreakpoint(breakpoint) {
    const { selectedSource } = this.props;

    return (
      <Breakpoint
        key={breakpoint.locationId}
        breakpoint={breakpoint}
        selectedSource={selectedSource}
        onClick={() => this.selectBreakpoint(breakpoint)}
        onContextMenu={e =>
          showContextMenu({ ...this.props, breakpoint, contextMenuEvent: e })
        }
        onChange={() => this.handleBreakpointCheckbox(breakpoint)}
        onCloseClick={ev => this.removeBreakpoint(ev, breakpoint)}
      />
    );
  }

  renderExceptionsOptions() {
    const {
      breakpoints,
      shouldPauseOnExceptions,
      shouldPauseOnCaughtExceptions,
      pauseOnExceptions
    } = this.props;

    const isEmpty = breakpoints.size == 0;

    const exceptionsBox = createExceptionOption(
      L10N.getStr("pauseOnExceptionsItem"),
      shouldPauseOnExceptions,
      () => pauseOnExceptions(!shouldPauseOnExceptions, false),
      "breakpoints-exceptions"
    );

    const ignoreCaughtBox = createExceptionOption(
      L10N.getStr("pauseOnCaughtExceptionsItem"),
      shouldPauseOnCaughtExceptions,
      () => pauseOnExceptions(true, !shouldPauseOnCaughtExceptions),
      "breakpoints-exceptions-caught"
    );

    return (

        {exceptionsBox}
        {shouldPauseOnExceptions ? ignoreCaughtBox : null}

    );
  }

  renderBreakpoints() {
    const { breakpoints } = this.props;
    if (breakpoints.size == 0) {
      return;
    }

    const groupedBreakpoints = groupBy(
      sortBy([...breakpoints.valueSeq()], bp => bp.location.line),
      bp => getRawSourceURL(bp.source.url)
    );

    return [
      ...Object.keys(groupedBreakpoints)
        .sort(sortFilenames)
        .map(url => {
          const groupBreakpoints = groupedBreakpoints[url].filter(
            bp => !bp.hidden && (bp.text || bp.originalText)
          );

          if (!groupBreakpoints.length) {
            return null;
          }

          const { source } = groupBreakpoints[0];

          return [

 this.props.selectSource(source.id)}
            >
              {getFilename(source)}

,
            ...groupBreakpoints.map(bp => this.renderBreakpoint(bp))
          ];
        })
    ];
  }

  render() {
    return (

        {this.renderExceptionsOptions()}
        {this.renderBreakpoints()}

    );
  }
}

function updateLocation(sources, frame, why, bp): LocalBreakpoint {
  const source = getSourceInSources(sources, bp.location.sourceId);
  const isCurrentlyPaused = isCurrentlyPausedAtBreakpoint(frame, why, bp);
  const locationId = makeLocationId(bp.location);
  const localBP = { ...bp, locationId, isCurrentlyPaused, source };

  return localBP;
}

const _getBreakpoints = createSelector(
  getBreakpoints,
  getSources,
  getTopFrame,
  getPauseReason,
  (breakpoints, sources, frame, why) =>
    breakpoints
      .map(bp => updateLocation(sources, frame, why, bp))
      .filter(bp => bp.source && !bp.source.isBlackBoxed)
);

const mapStateToProps = state => ({
  breakpoints: _getBreakpoints(state),
  selectedSource: getSelectedSource(state)
});

export default connect(mapStateToProps, actions)(Breakpoints);
<|endoftext|>


<commit_before>/**
 * 页面ready方法
 */
$(document).ready(function() {
    generateContent();
    share();
    disqus();
});

/**
 * 侧边目录
 */
function generateContent() {
    var $mt = $('.toc');
    var $toc;
    $mt.each(function(i,o){
        $toc = $(o);
        $toc.toc({ listType: 'ul', headers: 'h1, h2, h3' });
    });
}

function share(){
    window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"1","bdSize":"24"},"share":{}};
    with(document)0[getElementsByTagName("script")[0].parentNode.appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];
}

function disqus(){
    /* * * CONFIGURATION VARIABLES * * */
    var disqus_shortname = 'songzuotao';

    var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
    dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
    document.getElementsByTagName("script")[0].parentNode.appendChild(dsq);
}
<commit_msg>Update post.js<commit_after>/**
 * 页面ready方法
 */
$(document).ready(function() {
    generateContent();
    share();
    disqus();
});

/**
 * 侧边目录
 */
function generateContent() {
    var $mt = $('.toc');
    var $toc;
    $mt.each(function(i,o){
        $toc = $(o);
        $toc.toc({ listType: 'ul', headers: 'h1, h2, h3' });
    });
}

function share(){
    window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"1","bdSize":"24"},"share":{}};
    with(document)0[getElementsByTagName("script")[0].parentNode.appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];
}

function disqus(){
    /* * * CONFIGURATION VARIABLES * * */
    var disqus_shortname = 'songzuotao.github.io';

    var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
    dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
    document.getElementsByTagName("script")[0].parentNode.appendChild(dsq);
}
<|endoftext|>


<commit_before>
function escape(str) {
  return str.replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''')
}
function unescape(str) {
    return str.replace('&', '&', 'g')
    .replace('<','<','g')
    .replace('>','>','g')
    .replace('"', '"', 'g')
    .replace(''', "'", 'g')
}

function wikify(str) {
    return escape(str)
    .replace(/@(\w+)/g, "$1");
}

var el = React.createElement;

var Wiki = React.createClass({

    getInitialState: function() {
        return {
            editing: (this.props.initial_text ? false : true),
            text: this.props.initial_text
        }
    },

    save: function() {
        var t = unescape(this.state.text);
        var url = window.location.href;
        var that = this;
        fetch(url, {
            method: 'post',
            headers: { 'content-type':'application/json'},
            body: JSON.stringify({content:t})
        })
        .then(function(data){
            console.log('got ', data)
            that.setState({ text: t, editing: false })
        })
        .catch(function(err) {
            console.log('error ' + err);
        })
    },

    handleChange: function(e) {
        this.setState({ text: e.target.value } );
    },

    editMode: function(e) {
        this.setState({ text: this.state.text, editing: true } );
    },
    render: function () {
        return (
            el('div', {},
                el('row', {},
                    el('div', { className: 'text-right' },
                        this.state.editing ?
                        el('a', { className: 'small-4 small-centered columns success button', onClick: this.save },'save' ) :
                        el('a', { className: 'small-4 small-centered columns hollow button', onClick: this.editMode },'edit' )
                      )
                   ),
                   this.state.editing ?
                   el('textarea', { id: 'note', onChange: this.handleChange,
                       placeholder: 'New Page (use @link to make links)',
                       rows: 19, value: this.state.text })
                   :
                   el('pre', {
                       className: 'secondary callout',
                       dangerouslySetInnerHTML:
                       { __html: (wikify(this.state.text)) }
                   })
              )
        )
    }
});

<commit_msg>clever js binding<commit_after>
function escape(str) {
  return str.replace(/&/g, '&')
    .replace(/</g, '<')
    .replace(/>/g, '>')
    .replace(/"/g, '"')
    .replace(/'/g, ''')
}
function unescape(str) {
    return str.replace('&', '&', 'g')
    .replace('<','<','g')
    .replace('>','>','g')
    .replace('"', '"', 'g')
    .replace(''', "'", 'g')
}

function wikify(str) {
    return escape(str)
    .replace(/@(\w+)/g, "$1");
}

var el = React.createElement;
var div = React.createElement.bind(this, 'div');
var row = React.createElement.bind(this, 'row');
var textarea = React.createElement.bind(this, 'textarea');
var pre = React.createElement.bind(this, 'pre');
var a = React.createElement.bind(this, 'a');

var Wiki = React.createClass({

    getInitialState: function() {
        return {
            editing: (this.props.initial_text ? false : true),
            text: this.props.initial_text
        }
    },

    save: function() {
        var t = unescape(this.state.text);
        var url = window.location.href;
        var that = this;
        fetch(url, {
            method: 'post',
            headers: { 'content-type':'application/json'},
            body: JSON.stringify({content:t})
        })
        .then(function(data){
            console.log('got ', data)
            that.setState({ text: t, editing: false })
        })
        .catch(function(err) {
            console.log('error ' + err);
        })
    },

    handleChange: function(e) {
        this.setState({ text: e.target.value } );
    },

    editMode: function(e) {
        this.setState({ text: this.state.text, editing: true } );
    },
    render: function () {
        return (
            div( {},
                row( {},
                    div( { className: 'text-right' },
                        this.state.editing ?
                        a( { className: 'small-4 small-centered columns success button', onClick: this.save },'save' ) :
                        a( { className: 'small-4 small-centered columns hollow button', onClick: this.editMode },'edit' )
                      )
                   ),
                   this.state.editing ?
                   textarea( { id: 'note', onChange: this.handleChange,
                       placeholder: 'New Page (use @link to make links)',
                       rows: 19, value: this.state.text })
                   :
                   pre( {
                       className: 'secondary callout',
                       dangerouslySetInnerHTML:
                       { __html: (wikify(this.state.text)) }
                   })
              )
        )
    }
});

<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
  Animated,
  StyleSheet,
  PanResponder,
} from 'react-native';

export class PinchZoomResponder extends Component {
  static propTypes = {
    maxScale: PropTypes.number,
    onScaleChange: PropTypes.func,
    onOffsetChange: PropTypes.func,

    style: PropTypes.node,
    children: PropTypes.node.isRequired,
  };
  static defaultProps = {
    maxScale: 2.0,
    onScaleChange: (() => null),
    onOffsetChange: (() => null),

    style: null,
  };
  state = {
    scale: new Animated.Value(1.0),
    offset: {
      x: new Animated.Value(0.0),
      y: new Animated.Value(0.0),
    },
  };
  prevState = {
    scale: 1,
    offset: {
      x: 0.0,
      y: 0.0,
    },
    isPinch: false,
    pinchDistance: 150,
  };

  componentWillMount() {
    this.gestureHandlers = PanResponder.create({
      onStartShouldSetPanResponder: this.onStartShouldSetPanResponder,
      onMoveShouldSetPanResponder: this.onMoveShouldSetPanResponder,
      onPanResponderGrant: this.onPanResponderGrant,
      onPanResponderMove: this.onPanResponderMove,
      onPanResponderRelease: this.onPanResponderRelease,
      onPanResponderTerminationRequest: this.onPanResponderTerminationRequest,
      onShouldBlockNativeResponder: this.onShouldBlockNativeResponder,
    });
  }

  zoomTo = (scale) => {
    const offset = scale === 1.0 ? { x: 0.0, y: 0.0 } : this.getOffsetValue();
    this.getZoomAnimation(scale, offset).start(this.onScaleChanged);
  };

  // Pan Responder callbacks

  /**
   * Does this view want to become responder on the start of a touch?
   * @returns {@code false} to block single touches
   */
  onStartShouldSetPanResponder = () => this.getScaleValue() >= 1;

  /**
   * Called for every touch move on the View when it is not the responder
   * does this view want to "claim" touch responsiveness?
   * @returns {@code true} if pan or pinch gesture recognized.
   */
  onMoveShouldSetPanResponder = (event, state) => {
    if (this.isPanGesture(event, state)) {
      // start pan gesture handling predicate
      return this.getScaleValue() > 1.0;
    }
    if (this.isPinchGesture(event, state)) {
      // start pinch gesture handling predicate
      return true;
    }
    return false;
  };

  /**
   * The View is now responding for touch events.
   */
  onPanResponderGrant = (event, state) => {
    if (this.isPinchGesture(event, state)) {
      const distance = {
        dx: Math.abs(event.nativeEvent.touches[0].pageX - event.nativeEvent.touches[1].pageX),
        dy: Math.abs(event.nativeEvent.touches[0].pageY - event.nativeEvent.touches[1].pageY),
      };
      const powDistance = (distance.dx * distance.dx) + (distance.dy * distance.dy);
      this.prevState.pinchDistance = Math.sqrt(powDistance);
    }
  };

  /**
   * Finger movement
   */
  onPanResponderMove = (event, state) => {
    if (state.numberActiveTouches === 2) {
      this.onPanResponderPinch(event, state);
    } else if (state.numberActiveTouches === 1) {
      this.onPanResponderPan(event, state);
    }
  };

  /**
   * End of the touch
   */
  onPanResponderRelease = () => {
    this.prevState = {
      scale: this.getScaleValue(),
      offset: this.getOffsetValue(),
    };
  };

  /**
   * Something else wants to become responder. Should this view release the responder?
   * @returns {@code true} to allow release.
   */
  onPanResponderTerminationRequest = () => true;

  onShouldBlockNativeResponder = () => false;

  // Supporting functions

  isPanGesture = (event, state) => Math.abs(state.dx) > 2 || Math.abs(state.dy) > 2;

  isPinchGesture = (event, state) => state.numberActiveTouches === 2;

  onPanResponderPan = (event, state) => {
    const distance = {
      dx: this.prevState.isPinch ? 0 : state.dx,
      dy: this.prevState.isPinch ? 0 : state.dy,
    };
    const offset = {
      x: this.prevState.offset.x + (distance.dx / this.getScaleValue()),
      y: this.prevState.offset.y + (distance.dy / this.getScaleValue()),
    };
    const boundedOffsetValue = this.getBoundedOffsetValue(offset);
    this.setState({
      offset: {
        x: new Animated.Value(boundedOffsetValue.x),
        y: new Animated.Value(boundedOffsetValue.y),
      },
    }, this.onOffsetChanged);
  };

  onPanResponderPinch = (event) => {
    const touchDiff = {
      x: Math.abs(event.nativeEvent.touches[0].pageX - event.nativeEvent.touches[1].pageX),
      y: Math.abs(event.nativeEvent.touches[0].pageY - event.nativeEvent.touches[1].pageY),
    };
    const pinchDistance = Math.sqrt((touchDiff.x * touchDiff.x) + (touchDiff.y * touchDiff.y));
    const scale = (pinchDistance / this.prevState.pinchDistance) * this.prevState.scale;

    const isBackZoom = scale < this.prevState.scale;
    const offsetValue = this.getOffsetValue();
    const offset = {
      x: isBackZoom ? offsetValue.x * (scale / this.prevState.scale) : offsetValue.x,
      y: isBackZoom ? offsetValue.y * (scale / this.prevState.scale) : offsetValue.y,
    };
    const boundedValue = {
      scale: this.getBoundedScaleValue(scale),
      offset: this.getBoundedOffsetValue(offset),
    };
    this.setState({
      scale: new Animated.Value(boundedValue.scale),
      offset: {
        x: new Animated.Value(boundedValue.offset.x),
        y: new Animated.Value(boundedValue.offset.y),
      },
    }, this.onScaleChanged);
  };

  setState = (state, callback) => {
    super.setState(state);
    callback(state);
  };

  getBoundedScaleValue = (value) => {
    let result = value < 1.0 ? 1.0 : value;
    result = value > this.props.maxScale ? this.props.maxScale : result;
    return result;
  };

  getBoundedOffsetValue = (value) => {
    const result = value;
    const size = {
      original: this.size,
      scaled: {
        width: this.size.width * this.getScaleValue(),
        height: this.size.height * this.getScaleValue(),
      },
    };
    const bounds = {
      x: (size.scaled.width - size.original.width) / this.getScaleValue(),
      y: (size.scaled.height - size.original.height) / this.getScaleValue(),
    };
    const offsetValue = this.getOffsetValue();
    result.x = Math.abs(result.x) > bounds.x / 2 ? offsetValue.x : result.x;
    result.y = Math.abs(result.y) > bounds.x / 2 ? offsetValue.y : result.y;
    return result;
  };

  // Supporting function to prevent multiple eslint dangle warnings
  // eslint-disable-next-line no-underscore-dangle
  getScaleValue = () => this.state.scale._value;

  // Supporting function to prevent multiple eslint dangle warnings
  getOffsetValue = () => ({
    x: this.state.offset.x._value, // eslint-disable-line no-underscore-dangle
    y: this.state.offset.y._value, // eslint-disable-line no-underscore-dangle
  });

  // eslint-disable-next-line arrow-body-style
  getZoomAnimation = (scaleValue, offsetValue) => {
    const scaleAnim = Animated.timing(this.state.scale, {
      toValue: scaleValue,
      duration: 300,
    });
    const offsetAnim = {
      x: Animated.timing(this.state.offset.x, {
        toValue: offsetValue.x,
        duration: 300,
      }),
      y: Animated.timing(this.state.offset.y, {
        toValue: offsetValue.y,
        duration: 300,
      }),
    };
    return Animated.parallel([scaleAnim, offsetAnim.x, offsetAnim.y]);
  };

  onScaleChanged = () => {
    this.prevState.isPinch = true;
    const change = {
      previous: this.prevState.scale,
      current: this.getScaleValue(),
    };
    this.props.onScaleChange(change);
  };

  onOffsetChanged = () => {
    this.prevState.isPinch = false;
    const change = {
      previous: this.prevState.offset,
      current: this.getOffsetValue(),
    };
    this.props.onOffsetChange(change);
  };

  onContainerLayout = (event) => {
    const { width, height } = event.nativeEvent.layout;
    this.size = { width, height };
  };

  render() {
    const transform = {
      transform: [
        { scaleX: this.state.scale },
        { scaleY: this.state.scale },
        { translateX: this.state.offset.x },
        { translateY: this.state.offset.y },
      ],
    };
    return (
      <Animated.View
        onLayout={this.onContainerLayout}
        style={[styles.container, this.props.style, transform]}
        {...this.gestureHandlers.panHandlers}
      >
        {this.props.children}
      </Animated.View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
<commit_msg>fix(rkGallery): zoom animation offset change - animated<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
  Animated,
  StyleSheet,
  PanResponder,
} from 'react-native';

export class PinchZoomResponder extends Component {
  static propTypes = {
    maxScale: PropTypes.number,
    onScaleChange: PropTypes.func,
    onOffsetChange: PropTypes.func,

    style: PropTypes.node,
    children: PropTypes.node.isRequired,
  };
  static defaultProps = {
    maxScale: 2,
    onScaleChange: (() => null),
    onOffsetChange: (() => null),

    style: null,
  };
  state = {
    scale: new Animated.Value(1),
    offset: new Animated.ValueXY(),
  };
  prevState = {
    scale: 1,
    offset: {
      x: 0,
      y: 0,
    },
    isPinch: false,
    pinchDistance: 150,
  };

  componentWillMount() {
    this.gestureHandlers = PanResponder.create({
      onStartShouldSetPanResponder: this.onStartShouldSetPanResponder,
      onMoveShouldSetPanResponder: this.onMoveShouldSetPanResponder,
      onPanResponderGrant: this.onPanResponderGrant,
      onPanResponderMove: this.onPanResponderMove,
      onPanResponderRelease: this.onPanResponderRelease,
      onPanResponderTerminationRequest: this.onPanResponderTerminationRequest,
      onShouldBlockNativeResponder: this.onShouldBlockNativeResponder,
    });
  }

  zoomTo = (scale) => {
    // reset offset to { x: 0, y: 0 } if resetting scale
    const offset = scale === 1.0 ? { x: 0, y: 0 } : this.getOffsetValue();
    this.getZoomAnimation(scale, offset).start(this.onZoomAnimationCompleted);
  };

  // Pan Responder callbacks

  /**
   * Does this view want to become responder on the start of a touch?
   * @returns {@code false} to block single touches
   */
  onStartShouldSetPanResponder = () => this.getScaleValue() >= 1;

  /**
   * Called for every touch move on the View when it is not the responder
   * does this view want to "claim" touch responsiveness?
   * @returns {@code true} if pan or pinch gesture recognized.
   */
  onMoveShouldSetPanResponder = (event, state) => {
    if (this.isPanGesture(event, state)) {
      // start pan gesture handling predicate
      return this.getScaleValue() > 1.0;
    }
    if (this.isPinchGesture(event, state)) {
      // start pinch gesture handling predicate
      return true;
    }
    return false;
  };

  /**
   * The View is now responding for touch events.
   */
  onPanResponderGrant = (event, state) => {
    if (this.isPinchGesture(event, state)) {
      const distance = {
        dx: Math.abs(event.nativeEvent.touches[0].pageX - event.nativeEvent.touches[1].pageX),
        dy: Math.abs(event.nativeEvent.touches[0].pageY - event.nativeEvent.touches[1].pageY),
      };
      const powDistance = (distance.dx * distance.dx) + (distance.dy * distance.dy);
      this.prevState.pinchDistance = Math.sqrt(powDistance);
    }
  };

  /**
   * Finger movement
   */
  onPanResponderMove = (event, state) => {
    if (state.numberActiveTouches === 2) {
      this.onPanResponderPinch(event, state);
    } else if (state.numberActiveTouches === 1) {
      this.onPanResponderPan(event, state);
    }
  };

  /**
   * End of the touch
   */
  onPanResponderRelease = () => {
    this.prevState = {
      scale: this.getScaleValue(),
      offset: this.getOffsetValue(),
    };
  };

  /**
   * Something else wants to become responder. Should this view release the responder?
   * @returns {@code true} to allow release.
   */
  onPanResponderTerminationRequest = () => true;

  onShouldBlockNativeResponder = () => false;

  // Supporting functions

  isPanGesture = (event, state) => Math.abs(state.dx) > 2 || Math.abs(state.dy) > 2;

  isPinchGesture = (event, state) => state.numberActiveTouches === 2;

  onPanResponderPan = (event, state) => {
    const distance = {
      dx: this.prevState.isPinch ? 0 : state.dx,
      dy: this.prevState.isPinch ? 0 : state.dy,
    };
    const offset = {
      x: this.prevState.offset.x + (distance.dx / this.getScaleValue()),
      y: this.prevState.offset.y + (distance.dy / this.getScaleValue()),
    };
    const boundedOffsetValue = this.getBoundedOffsetValue(offset);
    this.setState({ offset: new Animated.ValueXY(boundedOffsetValue) }, this.onOffsetChanged);
  };

  onPanResponderPinch = (event) => {
    const touchDiff = {
      x: Math.abs(event.nativeEvent.touches[0].pageX - event.nativeEvent.touches[1].pageX),
      y: Math.abs(event.nativeEvent.touches[0].pageY - event.nativeEvent.touches[1].pageY),
    };
    const pinchDistance = Math.sqrt((touchDiff.x * touchDiff.x) + (touchDiff.y * touchDiff.y));
    const scale = (pinchDistance / this.prevState.pinchDistance) * this.prevState.scale;

    const isBackZoom = scale < this.prevState.scale;
    const offsetValue = this.getOffsetValue();
    const offset = {
      x: isBackZoom ? offsetValue.x * (scale / this.prevState.scale) : offsetValue.x,
      y: isBackZoom ? offsetValue.y * (scale / this.prevState.scale) : offsetValue.y,
    };
    const boundedValue = {
      scale: this.getBoundedScaleValue(scale),
      offset: this.getBoundedOffsetValue(offset),
    };
    this.setState({
      scale: new Animated.Value(boundedValue.scale),
      offset: new Animated.ValueXY(boundedValue.offset),
    }, this.onScaleChanged);
  };

  setState = (state, callback) => {
    super.setState(state);
    callback(state);
  };

  getBoundedScaleValue = (value) => {
    let result = value < 1.0 ? 1.0 : value;
    result = value > this.props.maxScale ? this.props.maxScale : result;
    return result;
  };

  getBoundedOffsetValue = (value) => {
    const result = value;
    const size = {
      original: this.size,
      scaled: {
        width: this.size.width * this.getScaleValue(),
        height: this.size.height * this.getScaleValue(),
      },
    };
    const bounds = {
      x: (size.scaled.width - size.original.width) / this.getScaleValue(),
      y: (size.scaled.height - size.original.height) / this.getScaleValue(),
    };
    const offsetValue = this.getOffsetValue();
    result.x = Math.abs(result.x) > bounds.x / 2 ? offsetValue.x : result.x;
    result.y = Math.abs(result.y) > bounds.x / 2 ? offsetValue.y : result.y;
    return result;
  };

  // Supporting function to prevent multiple eslint dangle warnings
  // eslint-disable-next-line no-underscore-dangle
  getScaleValue = () => this.state.scale._value;

  // Supporting function to prevent multiple eslint dangle warnings
  getOffsetValue = () => ({
    x: this.state.offset.x._value, // eslint-disable-line no-underscore-dangle
    y: this.state.offset.y._value, // eslint-disable-line no-underscore-dangle
  });

  // eslint-disable-next-line arrow-body-style
  getZoomAnimation = (scaleValue, offsetValue) => {
    const scaleAnim = Animated.timing(this.state.scale, {
      toValue: scaleValue,
      duration: 300,
    });
    const offsetAnim = Animated.timing(this.state.offset, {
      toValue: offsetValue,
      duration: 300,
    });
    return Animated.parallel([scaleAnim, offsetAnim]);
  };

  onScaleChanged = () => {
    this.prevState.isPinch = true;
    const change = {
      previous: this.prevState.scale,
      current: this.getScaleValue(),
    };
    this.props.onScaleChange(change);
  };

  onOffsetChanged = () => {
    this.prevState.isPinch = false;
    const change = {
      previous: this.prevState.offset,
      current: this.getOffsetValue(),
    };
    this.props.onOffsetChange(change);
  };

  onZoomAnimationCompleted = () => {
    this.onScaleChanged();
    this.prevState.offset = this.getOffsetValue();
  };

  onContainerLayout = (event) => {
    const { width, height } = event.nativeEvent.layout;
    this.size = { width, height };
  };

  render() {
    const transform = {
      transform: [
        { scaleX: this.state.scale },
        { scaleY: this.state.scale },
        { translateX: this.state.offset.x },
        { translateY: this.state.offset.y },
      ],
    };
    return (
      <Animated.View
        onLayout={this.onContainerLayout}
        style={[styles.container, this.props.style, transform]}
        {...this.gestureHandlers.panHandlers}
      >
        {this.props.children}
      </Animated.View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
<|endoftext|>