<commit_before>function clearContainer (container) {
    for (var i=0,ilen=container.childNodes.length;i<ilen;i+=1) {
        container.removeChild(container.childNodes[0]);
    }
}

var commenterInfo = {};
var quizMistakes;

function showMistakes () {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    quizMistakes = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=quizmistakes'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber);
    if (false === quizMistakes) return;
    // Empty the container
    var container = document.getElementById('quiz-mistakes');
    clearContainer(container);
    // Get name of commenter from return
    commenterInfo.commenter = quizMistakes.commenter;
    commenterInfo.commenterID = quizMistakes.commenterID;
    // For each mistake ...
    for (var i=0,ilen=quizMistakes.mistakes.length;i<ilen;i+=1) {
        var mistake = quizMistakes.mistakes[i];
        var mistakeDiv = document.createElement('div');
        mistakeDiv.setAttribute('id', 'mistake-' + mistake.questionNumber + '-' + mistake.wrongChoice);
        var rubricText = markdown(mistake.rubric);
        var correctText = markdown(mistake.correct);
        var langBubbles = '';
        if (mistake.langs) {
            var langs = mistake.langs.split(',');
            langBubbles = '
' + langs.join('

') + '
';
        }
        var wrongText = markdown(langBubbles + mistake.wrong);
        var buttonMode = {edit:'none',comment:'inline',save:'none'};
        if (mistake.hasCommenterComment) {
            buttonMode = {edit:'inline',comment:'none',save:'none'};
        }
        mistakeDiv.innerHTML = '

' + rubricText + '
'
            + '
' + correctText + '
'
            + '
' + wrongText + '

'
            + '
'
            + '<input type="button" class="button" '
            + 'id="comment-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.comment
            + '" value="Comment" '
            + 'onclick="newComment(this,\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<input type="button" class="button" '
            + 'id="edit-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.edit
            + '" value="Edit" '
            + 'onclick="openComment(\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<input type="button" class="button" '
            + 'id="save-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.save
            + '" value="Save" '
            + 'onclick="saveComment(\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<input type="button" class="button" '
            + 'id="eg-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.save
            + '" value="e.g." '
            + 'onclick="copyDown(this,\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<select class="rule-dropdown" onclick="" id="rule-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" onfocus="buildRuleSelect(this);" onchange="addRuleToMistake(this)"><option value="none">Tag with a rule</option></select>'
            + '
' + mistake.wrong + '
'
            + '
';
        var questionNumber = mistake.questionNumber;
        var wrongChoice = mistake.wrongChoice;
        for (var j=0,jlen=mistake.comments.length;j<jlen;j+=1) {
            var commenter = mistake.comments[j].commenter;
            var commenterID = mistake.comments[j].commenterID;
            var comment = mistake.comments[j].comment;
            var commentContainer = buildComment(questionNumber,wrongChoice,commenter,commenterID,comment);
            mistakeDiv.appendChild(commentContainer);
        }
        for (var j=0,jlen=mistake.rules.length;j<jlen;j+=1) {
            var rule = mistake.rules[j];
            var ruleNode = buildRule(questionNumber,wrongChoice,rule.ruleid,rule.ruletext);
            mistakeDiv.childNodes[0].appendChild(ruleNode);
        }
        container.appendChild(mistakeDiv);
    }
}

function addRuleToMistake (node) {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    // Figure out which choice we're looking at
    var m = node.id.split('-');
    var questionNumber = m[1];
    var wrongChoice = m[2];
    var ruleid = node.value;

    // Call the server to attach the rule if necessary,
    // and to find out whether it needs to be added
    // in the UI.
    var ruleData = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=attachrule'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice,
            ruleid:ruleid
        }
    );
    if (false === ruleData) return;

    // If it needs to be added, add it.
    if (ruleData.ruleID) {
        var pairNode = node.parentNode.previousSibling;
        var ruleNode = buildRule(questionNumber,wrongChoice,ruleData.ruleID,ruleData.ruleText);
        pairNode.appendChild(ruleNode);
    }
    // Then reset the value on the select
    node.selectedIndex = 0;
};

function buildRuleSelect (node) {
    for (var i=1,ilen=node.childNodes.length;i<ilen;i+=1) {
        node.removeChild(node.childNodes[1]);
    }
    var selections = [];
    for (var i=0,ilen=quizMistakes.selections.length;i<ilen;i+=1) {
        var rule = quizMistakes.selections[i];
        var option = document.createElement('option');
        option.setAttribute('value',rule.ruleid);
        option.innerHTML = markdown(rule.ruletext);
        option.innerHTML = option.textContent;
        selections.push({node:option,str:option.textContent});
    }
    selections.sort(function(a,b){if(a.str>b.str){return 1}else if(a.str>b.str){return -1}else{return 0}});
    for (var i=0,ilen=selections.length;i<ilen;i+=1) {
        var selection = selections[i];
        node.appendChild(selection.node);
    }
};

function copyDown(node,id) {
    var targetNode = document.getElementById(id);
    if (targetNode.childNodes[0].textContent) {
        return;
    }
    var wrongText = node.nextSibling.textContent;
    targetNode.childNodes[0].innerHTML = '> ' + wrongText;
}

function buildComment (questionNumber,wrongChoice,commenter,commenterID,comment) {
    var commentContainer = document.createElement('div');
    commentContainer.setAttribute('class', 'comment-container');
    commentContainer.setAttribute('id','comment-' + commenterID + '-' + questionNumber + '-' + wrongChoice);
    commentContainer.innerHTML = '
' + commenter + '
'
        + '
' + markdown(comment) + '
';
    return commentContainer;
}

function openComment (id) {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    var m = id.split('-');
    var questionNumber = m[2];
    var wrongChoice = m[3];
    var commentText = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=getonecomment'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice
        }
    );
    if (false === commentText) return;

    var node = document.getElementById(id);
    buildOpenComment(node,questionNumber,wrongChoice,commentText);
}

function buildOpenComment(node,questionNumber,wrongChoice,commentText) {
    node.innerHTML = '<textarea style="width:100%" cols="60" rows="3" class="selection-text" placeholder="Saving a comment without content will remove it">'
        + commentText
        + '</textarea>'
    setButtonMode('save',questionNumber,wrongChoice);
}

function saveComment (id) {
    var adminID = getParameterByName('admin');
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    //var commenterID = getParameterByName('commenter');
    var m = id.split('-');
    var questionNumber = m[2];
    var wrongChoice = m[3];
    var commenterID = m[1];
    var node = document.getElementById(id);
    var comment = node.firstChild.value;

    // Check whether the comment text contains one or more rules
    var lst = comment.split('\n\n');
    var rules = {top:null,other:[]};
    for (var i=lst.length-1;i>-1;i+=-1) {
        var m = lst[i].match(/^>>>(.*)/);
        if (m) {
            var rule = m[1].replace(/^\s+/,'').replace(/\s+$/,'');
            if (i === 0) {
                rules.top = rule;
                lst = lst.slice(1);
            } else {
                rules.other.push(rule);
            }
        }
    }
    //console.log("XX "+JSON.stringify(rules));
    // Reconstruct comment
    comment = lst.join('\n\n').replace(/^\s+/,'');
    // Rules
    if (rules.top || rules.other.length) {
        // Send rules to server for saving
        var ruleData = apiRequest(
            '/?commenter='
                + commenterKey
                +'&page=quiz&cmd=getrule'
                + '&classid='
                + classID
                + '&quizno='
                + quizNumber
            , {
                questionno:questionNumber,
                wrongchoice: wrongChoice,
                rules:rules
            }
        );
        if (false === ruleData) return;
        // Refresh dropdown list
        quizMistakes.selections = ruleData.selections;

        // Add top rule to UI
        if (ruleData.ruleID && rules.top) {
            var editButton = document.getElementById('edit-button-' + questionNumber + '-' +wrongChoice);
            var ruleBlock = buildRule(questionNumber,wrongChoice,ruleData.ruleID,rules.top);
            var answerPairNode = editButton.parentNode.previousSibling;
            answerPairNode.appendChild(ruleBlock);
        }
    }
    // Handle comment
    if (comment) {
        var commentBlock = buildComment(questionNumber,wrongChoice,commenterInfo.commenter,commenterInfo.commenterID,comment);
        node.parentNode.insertBefore(commentBlock,node);
        node.parentNode.removeChild(node);
        setButtonMode('edit',questionNumber,wrongChoice);
    } else {
        // Delete from server, remove the node, restore to comment mode
        node.parentNode.removeChild(node);
        setButtonMode('comment',questionNumber,wrongChoice)
    }
    writeComment(questionNumber,wrongChoice,comment);
}

function removeRule (node) {
    // XXX
    // Simple API call
    var m = node.id.split('-');
    var questionNumber = m[1];
    var wrongChoice = m[2];
    var ruleID = m[3];
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=removerule'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice,
            ruleid:ruleID
        }
    );
    node.parentNode.parentNode.parentNode.removeChild(node.parentNode.parentNode);
};

function buildRule (questionNumber,wrongChoice,ruleID,ruleText) {
    // Build the object
    var ruleContainer = document.createElement('div');
    ruleContainer.setAttribute('class', 'rule-container');
    ruleContainer.setAttribute('id', 'rule-' + ruleID + '-' + questionNumber + '-' +wrongChoice);
    // XXXX
    ruleContainer.innerHTML = '
<input type="button" id="removerule-' + questionNumber + '-' + wrongChoice + '-' + ruleID + '" class="button-small" onclick="confirmDelete(this,\'removeRule\')" value="Del"/>

' + markdown(ruleText) + '
';
    // Return
    return ruleContainer;
};

function refreshDropdownList (ruleData,questionNumber,wrongChoice) {
    var dropdownList = document.getElementById('rule-' + questionNumber + '-' + wrongChoice);
    for (var i=1,ilen=dropdownList.childNodes.length;i<ilen;i+=1) {
        dropdownList.removeChild(dropdownList.childNodes[1]);
    }
    for (var i=0,ilen=ruleData.selections.length;i<ilen;i+=1) {
        var selection = ruleData.selections[i];
        var option = document.createElement('option');
        option.setAttribute('value',selection.ruleID);
        option.innerHTML = markdown(selection.ruleText);
        option.innerHTML = option.textContent;
        dropdownList.appendChild(option);
    }

}

function newComment(button,id) {
    var parent = button.parentNode;
    var uncle = parent.nextSibling;
    var adminID = getParameterByName('admin');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    //var commenterID = getParameterByName('commenter');
    var m = id.split('-');
    var questionNumber = m[2];
    var wrongChoice = m[3];
    //var commenterID = m[1];
    var commentContainer = document.createElement('div');
    commentContainer.setAttribute('id',id);
    commentContainer.setAttribute('class','comment-container');
    parent.parentNode.insertBefore(commentContainer,uncle);
    buildOpenComment(commentContainer,questionNumber,wrongChoice,'');
}

function setButtonMode (mode,questionNumber,wrongChoice) {
    var commentButton = document.getElementById('comment-button-' + questionNumber + '-' + wrongChoice);
    var editButton = document.getElementById('edit-button-' + questionNumber + '-' + wrongChoice);
    var saveButton = document.getElementById('save-button-' + questionNumber + '-' + wrongChoice);
    var egButton = document.getElementById('eg-button-' + questionNumber + '-' + wrongChoice);
    if (mode === 'edit') {
        commentButton.setAttribute('style', 'display:none');
        editButton.setAttribute('style', 'display:inline');
        saveButton.setAttribute('style', 'display:none');
        egButton.setAttribute('style', 'display:none');
    } else if (mode === 'save') {
        commentButton.setAttribute('style', 'display:none');
        editButton.setAttribute('style', 'display:none');
        saveButton.setAttribute('style', 'display:inline');
        egButton.setAttribute('style', 'display:inline');
    } else {
        commentButton.setAttribute('style', 'display:inline');
        editButton.setAttribute('style', 'display:none');
        saveButton.setAttribute('style', 'display:none');
        egButton.setAttribute('style', 'display:none');
    }
};

function writeComment (questionNumber,wrongChoice,comment) {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    if (comment) {
        comment = comment.replace(/^\s+/,'').replace(/\s+$/,'');
    }
    var ignoreStr = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=writeonecomment'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice,
            comment: comment
        }
    );
}
<commit_msg>Blur the selector after inserting a rule<commit_after>function clearContainer (container) {
    for (var i=0,ilen=container.childNodes.length;i<ilen;i+=1) {
        container.removeChild(container.childNodes[0]);
    }
}

var commenterInfo = {};
var quizMistakes;

function showMistakes () {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    quizMistakes = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=quizmistakes'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber);
    if (false === quizMistakes) return;
    // Empty the container
    var container = document.getElementById('quiz-mistakes');
    clearContainer(container);
    // Get name of commenter from return
    commenterInfo.commenter = quizMistakes.commenter;
    commenterInfo.commenterID = quizMistakes.commenterID;
    // For each mistake ...
    for (var i=0,ilen=quizMistakes.mistakes.length;i<ilen;i+=1) {
        var mistake = quizMistakes.mistakes[i];
        var mistakeDiv = document.createElement('div');
        mistakeDiv.setAttribute('id', 'mistake-' + mistake.questionNumber + '-' + mistake.wrongChoice);
        var rubricText = markdown(mistake.rubric);
        var correctText = markdown(mistake.correct);
        var langBubbles = '';
        if (mistake.langs) {
            var langs = mistake.langs.split(',');
            langBubbles = '
' + langs.join('

') + '
';
        }
        var wrongText = markdown(langBubbles + mistake.wrong);
        var buttonMode = {edit:'none',comment:'inline',save:'none'};
        if (mistake.hasCommenterComment) {
            buttonMode = {edit:'inline',comment:'none',save:'none'};
        }
        mistakeDiv.innerHTML = '

' + rubricText + '
'
            + '
' + correctText + '
'
            + '
' + wrongText + '

'
            + '
'
            + '<input type="button" class="button" '
            + 'id="comment-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.comment
            + '" value="Comment" '
            + 'onclick="newComment(this,\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<input type="button" class="button" '
            + 'id="edit-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.edit
            + '" value="Edit" '
            + 'onclick="openComment(\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<input type="button" class="button" '
            + 'id="save-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.save
            + '" value="Save" '
            + 'onclick="saveComment(\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<input type="button" class="button" '
            + 'id="eg-button-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" '
            + 'style="display:'
            + buttonMode.save
            + '" value="e.g." '
            + 'onclick="copyDown(this,\'comment-' + commenterInfo.commenterID + '-' + mistake.questionNumber + '-' + mistake.wrongChoice + '\')"'
            +'/>'
            + '<select class="rule-dropdown" onclick="" id="rule-' + mistake.questionNumber + '-' + mistake.wrongChoice + '" onfocus="buildRuleSelect(this);" onchange="addRuleToMistake(this)"><option value="none">Tag with a rule</option></select>'
            + '
' + mistake.wrong + '
'
            + '
';
        var questionNumber = mistake.questionNumber;
        var wrongChoice = mistake.wrongChoice;
        for (var j=0,jlen=mistake.comments.length;j<jlen;j+=1) {
            var commenter = mistake.comments[j].commenter;
            var commenterID = mistake.comments[j].commenterID;
            var comment = mistake.comments[j].comment;
            var commentContainer = buildComment(questionNumber,wrongChoice,commenter,commenterID,comment);
            mistakeDiv.appendChild(commentContainer);
        }
        for (var j=0,jlen=mistake.rules.length;j<jlen;j+=1) {
            var rule = mistake.rules[j];
            var ruleNode = buildRule(questionNumber,wrongChoice,rule.ruleid,rule.ruletext);
            mistakeDiv.childNodes[0].appendChild(ruleNode);
        }
        container.appendChild(mistakeDiv);
    }
}

function addRuleToMistake (node) {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    // Figure out which choice we're looking at
    var m = node.id.split('-');
    var questionNumber = m[1];
    var wrongChoice = m[2];
    var ruleid = node.value;

    // Call the server to attach the rule if necessary,
    // and to find out whether it needs to be added
    // in the UI.
    var ruleData = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=attachrule'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice,
            ruleid:ruleid
        }
    );
    if (false === ruleData) return;

    // If it needs to be added, add it.
    if (ruleData.ruleID) {
        var pairNode = node.parentNode.previousSibling;
        var ruleNode = buildRule(questionNumber,wrongChoice,ruleData.ruleID,ruleData.ruleText);
        pairNode.appendChild(ruleNode);
    }
    // Then reset the value on the select
    node.selectedIndex = 0;
    node.blur();
};

function buildRuleSelect (node) {
    for (var i=1,ilen=node.childNodes.length;i<ilen;i+=1) {
        node.removeChild(node.childNodes[1]);
    }
    var selections = [];
    for (var i=0,ilen=quizMistakes.selections.length;i<ilen;i+=1) {
        var rule = quizMistakes.selections[i];
        var option = document.createElement('option');
        option.setAttribute('value',rule.ruleid);
        option.innerHTML = markdown(rule.ruletext);
        option.innerHTML = option.textContent;
        selections.push({node:option,str:option.textContent});
    }
    selections.sort(function(a,b){if(a.str>b.str){return 1}else if(a.str>b.str){return -1}else{return 0}});
    for (var i=0,ilen=selections.length;i<ilen;i+=1) {
        var selection = selections[i];
        node.appendChild(selection.node);
    }
};

function copyDown(node,id) {
    var targetNode = document.getElementById(id);
    if (targetNode.childNodes[0].textContent) {
        return;
    }
    var wrongText = node.nextSibling.textContent;
    targetNode.childNodes[0].innerHTML = '> ' + wrongText;
}

function buildComment (questionNumber,wrongChoice,commenter,commenterID,comment) {
    var commentContainer = document.createElement('div');
    commentContainer.setAttribute('class', 'comment-container');
    commentContainer.setAttribute('id','comment-' + commenterID + '-' + questionNumber + '-' + wrongChoice);
    commentContainer.innerHTML = '
' + commenter + '
'
        + '
' + markdown(comment) + '
';
    return commentContainer;
}

function openComment (id) {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    var m = id.split('-');
    var questionNumber = m[2];
    var wrongChoice = m[3];
    var commentText = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=getonecomment'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice
        }
    );
    if (false === commentText) return;

    var node = document.getElementById(id);
    buildOpenComment(node,questionNumber,wrongChoice,commentText);
}

function buildOpenComment(node,questionNumber,wrongChoice,commentText) {
    node.innerHTML = '<textarea style="width:100%" cols="60" rows="3" class="selection-text" placeholder="Saving a comment without content will remove it">'
        + commentText
        + '</textarea>'
    setButtonMode('save',questionNumber,wrongChoice);
}

function saveComment (id) {
    var adminID = getParameterByName('admin');
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    //var commenterID = getParameterByName('commenter');
    var m = id.split('-');
    var questionNumber = m[2];
    var wrongChoice = m[3];
    var commenterID = m[1];
    var node = document.getElementById(id);
    var comment = node.firstChild.value;

    // Check whether the comment text contains one or more rules
    var lst = comment.split('\n\n');
    var rules = {top:null,other:[]};
    for (var i=lst.length-1;i>-1;i+=-1) {
        var m = lst[i].match(/^>>>(.*)/);
        if (m) {
            var rule = m[1].replace(/^\s+/,'').replace(/\s+$/,'');
            if (i === 0) {
                rules.top = rule;
                lst = lst.slice(1);
            } else {
                rules.other.push(rule);
            }
        }
    }
    //console.log("XX "+JSON.stringify(rules));
    // Reconstruct comment
    comment = lst.join('\n\n').replace(/^\s+/,'');
    // Rules
    if (rules.top || rules.other.length) {
        // Send rules to server for saving
        var ruleData = apiRequest(
            '/?commenter='
                + commenterKey
                +'&page=quiz&cmd=getrule'
                + '&classid='
                + classID
                + '&quizno='
                + quizNumber
            , {
                questionno:questionNumber,
                wrongchoice: wrongChoice,
                rules:rules
            }
        );
        if (false === ruleData) return;
        // Refresh dropdown list
        quizMistakes.selections = ruleData.selections;

        // Add top rule to UI
        if (ruleData.ruleID && rules.top) {
            var editButton = document.getElementById('edit-button-' + questionNumber + '-' +wrongChoice);
            var ruleBlock = buildRule(questionNumber,wrongChoice,ruleData.ruleID,rules.top);
            var answerPairNode = editButton.parentNode.previousSibling;
            answerPairNode.appendChild(ruleBlock);
        }
    }
    // Handle comment
    if (comment) {
        var commentBlock = buildComment(questionNumber,wrongChoice,commenterInfo.commenter,commenterInfo.commenterID,comment);
        node.parentNode.insertBefore(commentBlock,node);
        node.parentNode.removeChild(node);
        setButtonMode('edit',questionNumber,wrongChoice);
    } else {
        // Delete from server, remove the node, restore to comment mode
        node.parentNode.removeChild(node);
        setButtonMode('comment',questionNumber,wrongChoice)
    }
    writeComment(questionNumber,wrongChoice,comment);
}

function removeRule (node) {
    // XXX
    // Simple API call
    var m = node.id.split('-');
    var questionNumber = m[1];
    var wrongChoice = m[2];
    var ruleID = m[3];
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=removerule'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice,
            ruleid:ruleID
        }
    );
    node.parentNode.parentNode.parentNode.removeChild(node.parentNode.parentNode);
};

function buildRule (questionNumber,wrongChoice,ruleID,ruleText) {
    // Build the object
    var ruleContainer = document.createElement('div');
    ruleContainer.setAttribute('class', 'rule-container');
    ruleContainer.setAttribute('id', 'rule-' + ruleID + '-' + questionNumber + '-' +wrongChoice);
    // XXXX
    ruleContainer.innerHTML = '
<input type="button" id="removerule-' + questionNumber + '-' + wrongChoice + '-' + ruleID + '" class="button-small" onclick="confirmDelete(this,\'removeRule\')" value="Del"/>

' + markdown(ruleText) + '
';
    // Return
    return ruleContainer;
};

function refreshDropdownList (ruleData,questionNumber,wrongChoice) {
    var dropdownList = document.getElementById('rule-' + questionNumber + '-' + wrongChoice);
    for (var i=1,ilen=dropdownList.childNodes.length;i<ilen;i+=1) {
        dropdownList.removeChild(dropdownList.childNodes[1]);
    }
    for (var i=0,ilen=ruleData.selections.length;i<ilen;i+=1) {
        var selection = ruleData.selections[i];
        var option = document.createElement('option');
        option.setAttribute('value',selection.ruleID);
        option.innerHTML = markdown(selection.ruleText);
        option.innerHTML = option.textContent;
        dropdownList.appendChild(option);
    }

}

function newComment(button,id) {
    var parent = button.parentNode;
    var uncle = parent.nextSibling;
    var adminID = getParameterByName('admin');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    //var commenterID = getParameterByName('commenter');
    var m = id.split('-');
    var questionNumber = m[2];
    var wrongChoice = m[3];
    //var commenterID = m[1];
    var commentContainer = document.createElement('div');
    commentContainer.setAttribute('id',id);
    commentContainer.setAttribute('class','comment-container');
    parent.parentNode.insertBefore(commentContainer,uncle);
    buildOpenComment(commentContainer,questionNumber,wrongChoice,'');
}

function setButtonMode (mode,questionNumber,wrongChoice) {
    var commentButton = document.getElementById('comment-button-' + questionNumber + '-' + wrongChoice);
    var editButton = document.getElementById('edit-button-' + questionNumber + '-' + wrongChoice);
    var saveButton = document.getElementById('save-button-' + questionNumber + '-' + wrongChoice);
    var egButton = document.getElementById('eg-button-' + questionNumber + '-' + wrongChoice);
    if (mode === 'edit') {
        commentButton.setAttribute('style', 'display:none');
        editButton.setAttribute('style', 'display:inline');
        saveButton.setAttribute('style', 'display:none');
        egButton.setAttribute('style', 'display:none');
    } else if (mode === 'save') {
        commentButton.setAttribute('style', 'display:none');
        editButton.setAttribute('style', 'display:none');
        saveButton.setAttribute('style', 'display:inline');
        egButton.setAttribute('style', 'display:inline');
    } else {
        commentButton.setAttribute('style', 'display:inline');
        editButton.setAttribute('style', 'display:none');
        saveButton.setAttribute('style', 'display:none');
        egButton.setAttribute('style', 'display:none');
    }
};

function writeComment (questionNumber,wrongChoice,comment) {
    var commenterKey = getParameterByName('commenter');
    var classID = getParameterByName('classid');
    var quizNumber = getParameterByName('quizno');
    if (comment) {
        comment = comment.replace(/^\s+/,'').replace(/\s+$/,'');
    }
    var ignoreStr = apiRequest(
        '/?commenter='
            + commenterKey
            +'&page=quiz&cmd=writeonecomment'
            + '&classid='
            + classID
            + '&quizno='
            + quizNumber
        , {
            questionno:questionNumber,
            wrongchoice: wrongChoice,
            comment: comment
        }
    );
}
<|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/. */

const urlParse = require('url').parse
const base64Encode = require('../lib/base64').encode

// Polyfill similar to this: https://github.com/gorhill/uBlock/blob/de1ed89f62bf041416d2a721ec00741667bf3fa8/assets/ublock/resources.txt#L385
const googleTagManagerRedirect = 'data:application/javascript;base64,' + base64Encode(`(function() { var noopfn = function() { ; }; window.ga = window.ga || noopfn; })();`)
// Polyfill similar to this: https://github.com/gorhill/uBlock/blob/de1ed89f62bf041416d2a721ec00741667bf3fa8/assets/ublock/resources.txt#L257
const googleTagServicesRedirect = 'data:application/javascript;base64,' + base64Encode(`(function() { var p; var noopfn = function() { }; var noopthisfn = function() { return this; }; var noopnullfn = function() { return null; }; var nooparrayfn = function() { return []; }; var noopstrfn = function() { return ''; }; var companionAdsService = { addEventListener: noopthisfn, enableSyncLoading: noopfn, setRefreshUnfilledSlots: noopfn }; var contentService = { addEventListener: noopthisfn, setContent: noopfn }; var PassbackSlot = function() { }; p = PassbackSlot.prototype; p.display = noopfn; p.get = noopnullfn; p.set = noopthisfn; p.setClickUrl = noopthisfn; p.setTagForChildDirectedTreatment = noopthisfn; p.setTargeting = noopthisfn; p.updateTargetingFromMap = noopthisfn; var pubAdsService = { addEventListener: noopthisfn, clear: noopfn, clearCategoryExclusions: noopthisfn, clearTagForChildDirectedTreatment: noopthisfn, clearTargeting: noopthisfn, collapseEmptyDivs: noopfn, defineOutOfPagePassback: function() { return new PassbackSlot(); }, definePassback: function() { return new PassbackSlot(); }, disableInitialLoad: noopfn, display: noopfn, enableAsyncRendering: noopfn, enableSingleRequest: noopfn, enableSyncRendering: noopfn, enableVideoAds: noopfn, get: noopnullfn, getAttributeKeys: nooparrayfn, refresh: noopfn, set: noopthisfn, setCategoryExclusion: noopthisfn, setCentering: noopfn, setCookieOptions: noopthisfn, setLocation: noopthisfn, setPublisherProvidedId: noopthisfn, setTagForChildDirectedTreatment: noopthisfn, setTargeting: noopthisfn, setVideoContent: noopthisfn, updateCorrelator: noopfn }; var SizeMappingBuilder = function() { }; p = SizeMappingBuilder.prototype; p.addSize = noopthisfn; p.build = noopnullfn; var Slot = function() { }; p = Slot.prototype; p.addService = noopthisfn; p.clearCategoryExclusions = noopthisfn; p.clearTargeting = noopthisfn; p.defineSizeMapping = noopthisfn; p.get = noopnullfn; p.getAdUnitPath = nooparrayfn; p.getAttributeKeys = nooparrayfn; p.getCategoryExclusions = nooparrayfn; p.getDomId = noopstrfn; p.getSlotElementId = noopstrfn; p.getSlotId = noopthisfn; p.getTargeting = nooparrayfn; p.getTargetingKeys = nooparrayfn; p.set = noopthisfn; p.setCategoryExclusion = noopthisfn; p.setClickUrl = noopthisfn; p.setCollapseEmptyDiv = noopthisfn; p.setTargeting = noopthisfn; var gpt = window.googletag || {}; window.googletag.destroySlots = function () { }; var cmd = gpt.cmd || []; gpt.apiReady = true; gpt.cmd = []; gpt.cmd.push = function(a) { try { a(); } catch (ex) { } return 1; }; gpt.companionAds = function() { return companionAdsService; }; gpt.content = function() { return contentService; }; gpt.defineOutOfPageSlot = function() { return new Slot(); }; gpt.defineSlot = function() { return new Slot(); }; gpt.disablePublisherConsole = noopfn; gpt.display = noopfn; gpt.enableServices = noopfn; gpt.getVersion = noopstrfn; gpt.pubads = function() { return pubAdsService; }; gpt.pubadsReady = true; gpt.sizeMapping = function() { return new SizeMappingBuilder(); }; window.googletag = gpt; while ( cmd.length !== 0 ) { gpt.cmd.push(cmd.shift()); } })();`)

const emptyDataURI = {
  enableForAdblock: true,
  enableForTrackingProtection: true,
  onBeforeRequest: function(details) {
    return {
      redirectURL: 'data:application/javascript;base64,MA=='
    }
  }
}

module.exports = {
  'sp1.nypost.com': emptyDataURI,
  'sp.nasdaq.com': emptyDataURI,
  'forbes.com': {
    onBeforeSendHeaders: function(details) {
      return {
        customCookie: details.requestHeaders.Cookie + `; forbes_ab=true; welcomeAd=true; adblock_session=Off; dailyWelcomeCookie=true`
      }
    },
  },
  'www.cityam.com': {
    userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 Googlebot'
  },
  // For links like: https://player.twitch.tv/?channel=iwilldominate
  'player.twitch.tv': {
    allowRunningInsecureContent: true,
    enableForAll: true
  },
  'www.wired.com': {
    // Site hack from
    // https://github.com/gorhill/uBlock/blob/ce2d235e4fd2ade2be101fa7030870044b30fd3c/assets/ublock/resources.txt#L699
    pageLoadEndScript: `(function() {
      var sto = window.setTimeout,
        re = /^function n\(\)/;
      window.setTimeout = function(a, b) {
          if ( b !== 50 || !re.test(a.toString()) ) {
                sto(a, b);
              }
        };
    })();`
  },
  'www.extremetech.com': {
    pageLoadStartScript: `(function() {
      var sto = window.setTimeout;
      window.setTimeout = function(a, b) {
          if ( b !== 250 ) {
                sto(a, b);
              }
        };
    })();`
  },
  'www.twitch.tv': {
    allowRunningInsecureContent: true
  },
  'imasdk.googleapis.com': {
    enableForAdblock: true,
    onBeforeRequest: function (details) {
      console.log('details', details)
      if (urlParse(details.firstPartyUrl).hostname !== 'www.y8.com' ||
          urlParse(details.url).pathname !== '/js/sdkloader/outstream.js') {
        return
      }
      return {
        cancel: false
      }
    }
  },
  'www.googletagmanager.com': {
    enableForAdblock: true,
    enableForTrackingProtection: true,
    onBeforeRequest: function(details) {
      if (urlParse(details.url).pathname !== '/gtm.js') {
        return
      }
      return {
        redirectURL: googleTagManagerRedirect
      }
    }
  },
  'www.googletagservices.com': {
    enableForAdblock: true,
    enableForTrackingProtection: true,
    onBeforeRequest: function(details) {
      if (urlParse(details.url).pathname !== '/tag/js/gpt.js') {
        return
      }
      return {
        redirectURL: googleTagServicesRedirect
       }
    }
  },
  'play.spotify.com': {
    enableFlashCTP: true
  }
}
<commit_msg>Remove unnecessary logging<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/. */

const urlParse = require('url').parse
const base64Encode = require('../lib/base64').encode

// Polyfill similar to this: https://github.com/gorhill/uBlock/blob/de1ed89f62bf041416d2a721ec00741667bf3fa8/assets/ublock/resources.txt#L385
const googleTagManagerRedirect = 'data:application/javascript;base64,' + base64Encode(`(function() { var noopfn = function() { ; }; window.ga = window.ga || noopfn; })();`)
// Polyfill similar to this: https://github.com/gorhill/uBlock/blob/de1ed89f62bf041416d2a721ec00741667bf3fa8/assets/ublock/resources.txt#L257
const googleTagServicesRedirect = 'data:application/javascript;base64,' + base64Encode(`(function() { var p; var noopfn = function() { }; var noopthisfn = function() { return this; }; var noopnullfn = function() { return null; }; var nooparrayfn = function() { return []; }; var noopstrfn = function() { return ''; }; var companionAdsService = { addEventListener: noopthisfn, enableSyncLoading: noopfn, setRefreshUnfilledSlots: noopfn }; var contentService = { addEventListener: noopthisfn, setContent: noopfn }; var PassbackSlot = function() { }; p = PassbackSlot.prototype; p.display = noopfn; p.get = noopnullfn; p.set = noopthisfn; p.setClickUrl = noopthisfn; p.setTagForChildDirectedTreatment = noopthisfn; p.setTargeting = noopthisfn; p.updateTargetingFromMap = noopthisfn; var pubAdsService = { addEventListener: noopthisfn, clear: noopfn, clearCategoryExclusions: noopthisfn, clearTagForChildDirectedTreatment: noopthisfn, clearTargeting: noopthisfn, collapseEmptyDivs: noopfn, defineOutOfPagePassback: function() { return new PassbackSlot(); }, definePassback: function() { return new PassbackSlot(); }, disableInitialLoad: noopfn, display: noopfn, enableAsyncRendering: noopfn, enableSingleRequest: noopfn, enableSyncRendering: noopfn, enableVideoAds: noopfn, get: noopnullfn, getAttributeKeys: nooparrayfn, refresh: noopfn, set: noopthisfn, setCategoryExclusion: noopthisfn, setCentering: noopfn, setCookieOptions: noopthisfn, setLocation: noopthisfn, setPublisherProvidedId: noopthisfn, setTagForChildDirectedTreatment: noopthisfn, setTargeting: noopthisfn, setVideoContent: noopthisfn, updateCorrelator: noopfn }; var SizeMappingBuilder = function() { }; p = SizeMappingBuilder.prototype; p.addSize = noopthisfn; p.build = noopnullfn; var Slot = function() { }; p = Slot.prototype; p.addService = noopthisfn; p.clearCategoryExclusions = noopthisfn; p.clearTargeting = noopthisfn; p.defineSizeMapping = noopthisfn; p.get = noopnullfn; p.getAdUnitPath = nooparrayfn; p.getAttributeKeys = nooparrayfn; p.getCategoryExclusions = nooparrayfn; p.getDomId = noopstrfn; p.getSlotElementId = noopstrfn; p.getSlotId = noopthisfn; p.getTargeting = nooparrayfn; p.getTargetingKeys = nooparrayfn; p.set = noopthisfn; p.setCategoryExclusion = noopthisfn; p.setClickUrl = noopthisfn; p.setCollapseEmptyDiv = noopthisfn; p.setTargeting = noopthisfn; var gpt = window.googletag || {}; window.googletag.destroySlots = function () { }; var cmd = gpt.cmd || []; gpt.apiReady = true; gpt.cmd = []; gpt.cmd.push = function(a) { try { a(); } catch (ex) { } return 1; }; gpt.companionAds = function() { return companionAdsService; }; gpt.content = function() { return contentService; }; gpt.defineOutOfPageSlot = function() { return new Slot(); }; gpt.defineSlot = function() { return new Slot(); }; gpt.disablePublisherConsole = noopfn; gpt.display = noopfn; gpt.enableServices = noopfn; gpt.getVersion = noopstrfn; gpt.pubads = function() { return pubAdsService; }; gpt.pubadsReady = true; gpt.sizeMapping = function() { return new SizeMappingBuilder(); }; window.googletag = gpt; while ( cmd.length !== 0 ) { gpt.cmd.push(cmd.shift()); } })();`)

const emptyDataURI = {
  enableForAdblock: true,
  enableForTrackingProtection: true,
  onBeforeRequest: function(details) {
    return {
      redirectURL: 'data:application/javascript;base64,MA=='
    }
  }
}

module.exports = {
  'sp1.nypost.com': emptyDataURI,
  'sp.nasdaq.com': emptyDataURI,
  'forbes.com': {
    onBeforeSendHeaders: function(details) {
      return {
        customCookie: details.requestHeaders.Cookie + `; forbes_ab=true; welcomeAd=true; adblock_session=Off; dailyWelcomeCookie=true`
      }
    },
  },
  'www.cityam.com': {
    userAgent: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36 Googlebot'
  },
  // For links like: https://player.twitch.tv/?channel=iwilldominate
  'player.twitch.tv': {
    allowRunningInsecureContent: true,
    enableForAll: true
  },
  'www.wired.com': {
    // Site hack from
    // https://github.com/gorhill/uBlock/blob/ce2d235e4fd2ade2be101fa7030870044b30fd3c/assets/ublock/resources.txt#L699
    pageLoadEndScript: `(function() {
      var sto = window.setTimeout,
        re = /^function n\(\)/;
      window.setTimeout = function(a, b) {
          if ( b !== 50 || !re.test(a.toString()) ) {
                sto(a, b);
              }
        };
    })();`
  },
  'www.extremetech.com': {
    pageLoadStartScript: `(function() {
      var sto = window.setTimeout;
      window.setTimeout = function(a, b) {
          if ( b !== 250 ) {
                sto(a, b);
              }
        };
    })();`
  },
  'www.twitch.tv': {
    allowRunningInsecureContent: true
  },
  'imasdk.googleapis.com': {
    enableForAdblock: true,
    onBeforeRequest: function (details) {
      if (urlParse(details.firstPartyUrl).hostname !== 'www.y8.com' ||
          urlParse(details.url).pathname !== '/js/sdkloader/outstream.js') {
        return
      }
      return {
        cancel: false
      }
    }
  },
  'www.googletagmanager.com': {
    enableForAdblock: true,
    enableForTrackingProtection: true,
    onBeforeRequest: function(details) {
      if (urlParse(details.url).pathname !== '/gtm.js') {
        return
      }
      return {
        redirectURL: googleTagManagerRedirect
      }
    }
  },
  'www.googletagservices.com': {
    enableForAdblock: true,
    enableForTrackingProtection: true,
    onBeforeRequest: function(details) {
      if (urlParse(details.url).pathname !== '/tag/js/gpt.js') {
        return
      }
      return {
        redirectURL: googleTagServicesRedirect
       }
    }
  },
  'play.spotify.com': {
    enableFlashCTP: true
  }
}
<|endoftext|>


<commit_before>var card_width = 300;
var card_height = 186;
var half_card_width = card_width / 2;
var half_card_height = card_height / 2;

var next_outgoing_i = 0; /* the index of the next node that's outgoing */
var next_incoming_i = 0;

/* Which card is currently displayed in the middle of the screen? */
var current_card = null;

/* Height of rendered graph */
var graph_height = null;

var node0 = {
    "summary": "Why a max length of 140 characters? Well, it sure as hell works for Twitter. Seems like just enough to pack some good info into an argument.",
    "outgoing": [],
    "incoming": [node2],
}

var node1 = {
    "summary": "Taxation is theft.",
    "outgoing": [],
    "incoming": [node2],
}

var node2 = {
    "summary": "You pay taxes in exchange for voluntarily living in a country and using its public services.",
    "outgoing": [node0, node1],
    "incoming": [],
}

var link20 = {
    "from": node2,
    "to": node0,
    "type": "support"
}

var link21 = {
    "from": node2,
    "to": node1,
    "type": "oppose"
}

var nodes = [node2, node0, node1];

function is_outgoing(node) {
    return current_card.outgoing.indexOf(node) != -1;
}

function x_pos(node) {
    if (node == current_card) {
        node.i = 0; // doesn't matter for current_card
        return (window.innerWidth / 2) - half_card_width;
    } else if (is_outgoing(node)) {
        if (node.i == null) {
            node.i = next_outgoing_i;
            next_outgoing_i++;
        }
        return node.i * (card_width + 10) + 10;
    } else {
        node.i = 0; // disappear to the top-middle
        return (window.innerWidth / 2) - half_card_width;
    }
}

function y_pos(node) {
    if (node == current_card) {
        return (graph_height / 2) - half_card_height;
    } else if (is_outgoing(node)) {
        return 10;
    } else {
        return -10 - card_height; // just hide it for now
    }
}

function pos2str(pos) {
    return pos[0] + "," + pos[1] + " ";
}

function compute_link_bezier_curve(link) {
    var from = link.from;
    var to = link.to;
    var start_pos = [x_pos(from, from.i) + half_card_width, y_pos(from, from.i)];
    var end_pos = [x_pos(to, to.i) + half_card_width,
                    /* marker-widht is 3, stroke-width is 5 */
                   y_pos(to, to.i) + card_height + (5 * 3)];
    var height = end_pos[1] - start_pos[1];
    var control1 = [start_pos[0], start_pos[1] + (height / 2)];
    var control2 = [end_pos[0], end_pos[1] - (height / 2)];
    return "M" + pos2str(start_pos) + "C" + pos2str(control1)
            + pos2str(control2) + pos2str(end_pos);
}

function reset_globals() {
    for (var i=0; i < nodes.length; i++) {
        nodes[i].i = null;
    }
    next_outgoing_i = 0;
    next_incoming_i = 0;
}

current_card = node2;

function draw_graph() {
    reset_globals();

    var svg = d3.select("svg#graph");
    var html_nodes = svg.selectAll("g").data(nodes);
    var new_nodes = html_nodes.enter().append("g").classed("argument", true);
    new_nodes.append("rect").attr("width", card_width)
        .attr("height", card_height);
    var switch_objects = new_nodes.append("switch");
    switch_objects.append("foreignObject").classed("foreign-object", true)
            .attr("requiredFeatures", "http://www.w3.org/TR/SVG11/feature#Extensibility")
            .attr("width", card_width).attr("height", card_height)
            .append("xhtml:div").classed("summary", true).append("p")
            .classed("summary", true).text(function (d) {
                return d.summary;
            }).attr("xmlns", "http://www.w3.org/1999/xhtml");
    switch_objects.append("text").attr("x", x_pos).attr("y", y_pos)
        .text("Sorry, your browser is not supported.");
    d3.selectAll("g rect").transition().duration(500).attr("x", x_pos).attr("y", y_pos);
    // http://stackoverflow.com/a/11743721/257583
    d3.selectAll(".foreign-object").transition().duration(500).attr("x", x_pos).attr("y", y_pos);
    html_nodes.on("click", function (d) {
        current_card = d;
        draw_graph();
    })
    var html_links = svg.selectAll("path").data([link21, link20]);
    var new_links = html_links.enter().append("path")
        .attr("class", function(d) {
            return d.type;
        }).attr("d", compute_link_bezier_curve)
        .attr("marker-end", function(d) {
            return "url(#arrow-" + d.type + ")"
        });
}

window.onload = function () {
    graph_height = $("#graph").height();
    draw_graph();
}<commit_msg>Refactored out card-creation code.<commit_after>var card_width = 300;
var card_height = 186;
var half_card_width = card_width / 2;
var half_card_height = card_height / 2;

var next_outgoing_i = 0; /* the index of the next node that's outgoing */
var next_incoming_i = 0;

/* Which card is currently displayed in the middle of the screen? */
var current_card = null;

/* Height of rendered graph */
var graph_height = null;

var nodes = [];
var cards = [];

var node0 = {
    "summary": "Why a max length of 140 characters? Well, it sure as hell works for Twitter. Seems like just enough to pack some good info into an argument.",
    "outgoing": [],
    "incoming": [node2],
}

var node1 = {
    "summary": "Taxation is theft.",
    "outgoing": [],
    "incoming": [node2],
}

var node2 = {
    "summary": "You pay taxes in exchange for voluntarily living in a country and using its public services.",
    "outgoing": [node0, node1],
    "incoming": [],
}

var link20 = {
    "from": node2,
    "to": node0,
    "type": "support"
}

var link21 = {
    "from": node2,
    "to": node1,
    "type": "oppose"
}

var nodes = [node2, node0, node1];
current_card = node2;

function is_outgoing(node) {
    return current_card.outgoing.indexOf(node) != -1;
}

function x_pos(node) {
    if (node == current_card) {
        node.i = 0; // doesn't matter for current_card
        return (window.innerWidth / 2) - half_card_width;
    } else if (is_outgoing(node)) {
        if (node.i == null) {
            node.i = next_outgoing_i;
            next_outgoing_i++;
        }
        return node.i * (card_width + 10) + 10;
    } else {
        node.i = 0; // disappear to the top-middle
        return (window.innerWidth / 2) - half_card_width;
    }
}

function y_pos(node) {
    if (node == current_card) {
        return (graph_height / 2) - half_card_height;
    } else if (is_outgoing(node)) {
        return 10;
    } else {
        return -10 - card_height; // just hide it for now
    }
}

function pos2str(pos) {
    return pos[0] + "," + pos[1] + " ";
}

function compute_link_bezier_curve(link) {
    var from = link.from;
    var to = link.to;
    var start_pos = [x_pos(from, from.i) + half_card_width, y_pos(from, from.i)];
    var end_pos = [x_pos(to, to.i) + half_card_width,
                    /* marker-widht is 3, stroke-width is 5 */
                   y_pos(to, to.i) + card_height + (5 * 3)];
    var height = end_pos[1] - start_pos[1];
    var control1 = [start_pos[0], start_pos[1] + (height / 2)];
    var control2 = [end_pos[0], end_pos[1] - (height / 2)];
    return "M" + pos2str(start_pos) + "C" + pos2str(control1)
            + pos2str(control2) + pos2str(end_pos);
}

function reset_globals() {
    for (var i=0; i < nodes.length; i++) {
        nodes[i].i = null;
    }
    next_outgoing_i = 0;
    next_incoming_i = 0;
}

function make_cards() {
    var svg = d3.select("svg#graph");
    cards = svg.selectAll("g").data(nodes);
    new_cards = cards.enter().append("g").classed("argument", true);
    new_cards.append("rect").attr("width", card_width).attr("height", card_height);
    var switch_objects = new_cards.append("switch");
    switch_objects.append("foreignObject").classed("foreign-object", true)
            .attr("requiredFeatures", "http://www.w3.org/TR/SVG11/feature#Extensibility")
            .attr("width", card_width).attr("height", card_height)
            .append("xhtml:div").classed("summary", true).append("p")
            .classed("summary", true).text(function (d) {
                return d.summary;
            }).attr("xmlns", "http://www.w3.org/1999/xhtml");
    switch_objects.append("text").attr("x", x_pos).attr("y", y_pos)
        .text("Sorry, your browser is not supported.");
    new_cards.on("click", function (d) {
        current_card = d;
        draw_graph();
    })
}

function draw_graph() {
    reset_globals();
    make_cards();

    var svg = d3.select("svg#graph");
    d3.selectAll("g rect").transition().duration(500).attr("x", x_pos).attr("y", y_pos);
    // http://stackoverflow.com/a/11743721/257583
    d3.selectAll(".foreign-object").transition().duration(500).attr("x", x_pos).attr("y", y_pos);
    var html_links = svg.selectAll("path").data([link21, link20]);
    var new_links = html_links.enter().append("path")
        .attr("class", function(d) {
            return d.type;
        }).attr("d", compute_link_bezier_curve)
        .attr("marker-end", function(d) {
            return "url(#arrow-" + d.type + ")"
        });
}

window.onload = function () {
    graph_height = $("#graph").height();
    draw_graph();
}<|endoftext|>


<commit_before>/* FHEM tablet ui */
/**
* Just another dashboard for FHEM
*
* Version: 1.3.0
* Requires: jQuery v1.7+, font-awesome, jquery.gridster, jquery.toast
*
* Copyright (c) 2015 Mario Stephan <mstephan@shared-files.de>
* Under MIT License (http://www.opensource.org/licenses/mit-license.php)
*
*/
var deviceStates={};
var readings = {"STATE":true};
var devices = {};
var types = {};
var ready = true;
var reading_cntr;
var DEBUG = false;
var doLongPoll = false;
var timer;
var dir;
var shortpollInterval = 30 * 1000; // 30 seconds
var devs=Array();

var plugins = {
  modules: [],
  addModule: function (module) {
    this.modules.push(module);
  },
  load: function (name) {
  	$.ajax({
		url: dir+'/'+name+'.js',
		dataType: "script",
		cache: true,
		//async: false,
		context:{name: name},
		success: function () {
				DEBUG && console.log('Loaded plugin: '+this.name);
				var module = eval(this.name);
				plugins.addModule(module);
				module.init();
		},
	});
  },
  update: function (dev,par) {
    $.each(this.modules, function (index, module) {
      //Iterate each module and run update function
      module.update(dev,par);
    });
  }
}

$( document ).ready(function() {

	wx = parseInt( $("meta[name='widget_base_width']").attr("content") );
	wy = parseInt( $("meta[name='widget_base_height']").attr("content") );
	doLongPoll = ($("meta[name='longpoll']").attr("content") == '1');
	DEBUG  = ($("meta[name='debug']").attr("content") == '1');

	//self path
	dir = $('script[src$="fhem-tablet-ui.js"]').attr('src');
	var name = dir.split('/').pop();
	dir = dir.replace('/'+name,"");
	DEBUG && console.log('Plugin dir: '+dir);

	//init gridster
	gridster = $(".gridster > ul").gridster({
          widget_base_dimensions: [wx, wy],
          widget_margins: [5, 5],
          draggable: {
            handle: 'header'
          }
        }).data('gridster');

	//make it HTML conform (remove this after migration)
	$('div[type]').each(function() {
    	$(this).attr({
            'data-type' : $(this).attr('type'),
        })
        .removeAttr('type');
	});
	$('div[device]').each(function() {
    	$(this).attr({
            'data-device' : $(this).attr('device'),
        })
        .removeAttr('device');
	});
	//end **** (remove this after migration)

	//collect required widgets types
	$('div[data-type]').each(function(index){
		var t = $(this).data("type");
		if(!types[t])
			types[t] = true;
	});

	//init widgets
	for (var widget_type in types) {
		plugins.load('widget_'+widget_type);
	}

	//collect required readings
	$('[data-get]').each(function(index){
		var reading = $(this).data("get");
		if(!readings[reading])
			readings[reading] = true;
	});

	//collect required devices
	$('div[data-device]').each(function(index){
		var device = $(this).data("device");
		if(!devices[device])
			devices[device] = true; devs.push(device);
	});

	//get current values of readings
	for (var reading in readings) {
		requestFhem(reading);
	}
	reading_cntr = Object.keys(readings).length;

	if ( doLongPoll ){
		setTimeout(function() {
				longPoll();
		}, 1000);
		shortpollInterval = 15 * 60 * 1000; // 15 minutes
	}

	$("*").focus(function(){
    	$(this).blur();
  	});

	// refresh every x secs
	startInterval();

});

function startInterval() {
     clearInterval(timer);
     timer = setInterval(function () {
		//get current values of readings every x seconds
		for (var reading in readings) {
			requestFhem(reading);
		}
     }, shortpollInterval);
 }

function update(dev,par) {
	ready = false;
 	plugins.update(dev,par);
 	ready = true;
	DEBUG && console.log('update done for device:'+dev+' parameter:'+par);
}

function setFhemStatus(device,status) {
	startInterval();
    DEBUG && console.log("set "+device+" "+status);
	$.ajax({
		async: true,
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: "set "+device+" "+status,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
	})
  	.done ( function( data ) {
  		if ( !doLongPoll ){
			setTimeout(function(){
				for (var reading in readings) {
					requestFhem(reading);
				}
			}, 4000);
		}
	});
}

var xhr;
var currLine=0;
function longPoll(roomName) {
/* try to avoid this terrible fmt=JSON output format
	- no separat node for parameter name
	- multiple nodes with the same data (2xdate)
*/
	DEBUG && console.log('start longpoll');

	if (xhr)
		xhr.abort();
	currLine=0;

	$.ajax({
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		cache: false,
		complete: function() {
			setTimeout(function() {
				longPoll();
			}, 100);
		},
		timeout: 60000,
		async: true,
		data: {
			XHR:1,
			inform: "type=raw;filter=.*"
		},
		xhr: function() {
			xhr = new window.XMLHttpRequest();
			xhr.addEventListener("readystatechange", function(e){
				var data = e.target.responseText;
		  		if ( e.target.readyState == 4) {
    				//$.toast("Connection lost, trying to reconnect in 5 seconds.");
    				return;
  				}
				if ( e.target.readyState == 3 )
				{
					var lines = data.replace(/
/g,"").split(/\n/);
					var regDevice = /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(\S*)\s(\S*)\s(.*)/;
					var regDate = /^([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;
					var regParaname = /^(\S{3,}):\s(.*)$/;
					lines.pop(); //remove last empty line

					for (var i=currLine; i < lines.length; i++) {
						var date;
						var line = $.trim( lines[i] );
						//console.log('#'+line+'#');

						if ( regDate.test( line ))
							date = $.trim(line.match( regDate )[1]);
						if ( regDevice.test( line )) {
							//Bad parse hack, but the JSON is not well formed
							var room = $.trim( line.match( regDevice )[1] );
							var key = $.trim( line.match( regDevice )[2] );
							var parname_val = $.trim(line.match( regDevice )[3]);
							var params = deviceStates[key] || {};
							var paraname;
							var val;
							if ( regParaname.test(parname_val) ){
								var paraname = $.trim(parname_val.match( regParaname )[1]);
								var val = $.trim(parname_val.match( regParaname )[2]);
							}
							else {
								var paraname = 'STATE';
								var val = parname_val;
							}
							if ( (paraname in readings) && (key in devices) ){
								var value = {"date": date,
											  "room": room,
												"val": val
											};
								params[paraname]=value;
								deviceStates[key]=params;
								DEBUG && console.log(date + ' / ' + key+' / '+paraname+' / '+val);
								update(key,paraname);
							}
							//console.log(date + ' / ' + key+' / '+paraname+' / '+val);
						}
					}
					currLine = lines.length;
				}

    		}, false);
			return xhr;
			}
	});
}

function requestFhem(paraname) {
/* 'list' is still the fastest cmd to get all important data
*/
	$.ajax({
		async: true,
		cache: false,
		context:{paraname: paraname},
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: "list " + devs.join() + " " + paraname,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
  	})
  	.done (function( data ) {
			var lines = data.replace(/\n\)/g,")\n").split(/\n/);
			var regDevice = /^(\S*)\s.*/;
			var regState = (this.paraname!='STATE')
						? /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(.*)/
						: /^\S*\s*(.*)/;
			var regDate = /\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;

			for (var i=0; i < lines.length; i++) {
				var date;
				var line = $.trim( lines[i] );
				if ( regDate.test( line ))
					date = $.trim( line.match( regDate )[1] );
				if (regState.test(line) && regDevice.test(line) ) {
					var key = $.trim( line.match( regDevice )[1] );
					var val = $.trim( line.match( regState )[1] );
					var params = deviceStates[key] || {};
					var paraname = this.paraname;
					var value = {"date": date, "val": val};
					params[paraname]=value;
					if (key in devices)
						deviceStates[key]=params;
				}
			}
		reading_cntr--;
		if ( reading_cntr < 1 ) {
			update('*','*');
			reading_cntr = Object.keys(readings).length;
 		}
	});

}

this.getPart = function (s,p) {
	var c = (s && typeof s != "undefined") ? s.split(" ") : '';
	return (c.length >= p && p>0 ) ? c[p-1] : s;
};

this.getDeviceValue = function (device, src) {
	var devname	= device.data('device');
	var paraname =	(src && src != '') ? device.data(src) : Object.keys(readings)[0];
	if (devname && devname.length>0){
		var params = deviceStates[devname];
		return ( params && params[paraname] ) ? params[paraname].val : null;
	}
	return null;
}

<commit_msg>Leerzeichen nach Doppelpunkt in Eventstrings ist optional<commit_after>/* FHEM tablet ui */
/**
* Just another dashboard for FHEM
*
* Version: 1.3.0
* Requires: jQuery v1.7+, font-awesome, jquery.gridster, jquery.toast
*
* Copyright (c) 2015 Mario Stephan <mstephan@shared-files.de>
* Under MIT License (http://www.opensource.org/licenses/mit-license.php)
*
*/
var deviceStates={};
var readings = {"STATE":true};
var devices = {};
var types = {};
var ready = true;
var reading_cntr;
var DEBUG = false;
var doLongPoll = false;
var timer;
var dir;
var shortpollInterval = 30 * 1000; // 30 seconds
var devs=Array();

var plugins = {
  modules: [],
  addModule: function (module) {
    this.modules.push(module);
  },
  load: function (name) {
  	$.ajax({
		url: dir+'/'+name+'.js',
		dataType: "script",
		cache: true,
		//async: false,
		context:{name: name},
		success: function () {
				DEBUG && console.log('Loaded plugin: '+this.name);
				var module = eval(this.name);
				plugins.addModule(module);
				module.init();
		},
	});
  },
  update: function (dev,par) {
    $.each(this.modules, function (index, module) {
      //Iterate each module and run update function
      module.update(dev,par);
    });
  }
}

$( document ).ready(function() {

	wx = parseInt( $("meta[name='widget_base_width']").attr("content") );
	wy = parseInt( $("meta[name='widget_base_height']").attr("content") );
	doLongPoll = ($("meta[name='longpoll']").attr("content") == '1');
	DEBUG  = ($("meta[name='debug']").attr("content") == '1');

	//self path
	dir = $('script[src$="fhem-tablet-ui.js"]').attr('src');
	var name = dir.split('/').pop();
	dir = dir.replace('/'+name,"");
	DEBUG && console.log('Plugin dir: '+dir);

	//init gridster
	gridster = $(".gridster > ul").gridster({
          widget_base_dimensions: [wx, wy],
          widget_margins: [5, 5],
          draggable: {
            handle: 'header'
          }
        }).data('gridster');

	//make it HTML conform (remove this after migration)
	$('div[type]').each(function() {
    	$(this).attr({
            'data-type' : $(this).attr('type'),
        })
        .removeAttr('type');
	});
	$('div[device]').each(function() {
    	$(this).attr({
            'data-device' : $(this).attr('device'),
        })
        .removeAttr('device');
	});
	//end **** (remove this after migration)

	//collect required widgets types
	$('div[data-type]').each(function(index){
		var t = $(this).data("type");
		if(!types[t])
			types[t] = true;
	});

	//init widgets
	for (var widget_type in types) {
		plugins.load('widget_'+widget_type);
	}

	//collect required readings
	$('[data-get]').each(function(index){
		var reading = $(this).data("get");
		if(!readings[reading])
			readings[reading] = true;
	});

	//collect required devices
	$('div[data-device]').each(function(index){
		var device = $(this).data("device");
		if(!devices[device])
			devices[device] = true; devs.push(device);
	});

	//get current values of readings
	for (var reading in readings) {
		requestFhem(reading);
	}
	reading_cntr = Object.keys(readings).length;

	if ( doLongPoll ){
		setTimeout(function() {
				longPoll();
		}, 1000);
		shortpollInterval = 15 * 60 * 1000; // 15 minutes
	}

	$("*").focus(function(){
    	$(this).blur();
  	});

	// refresh every x secs
	startInterval();

});

function startInterval() {
     clearInterval(timer);
     timer = setInterval(function () {
		//get current values of readings every x seconds
		for (var reading in readings) {
			requestFhem(reading);
		}
     }, shortpollInterval);
 }

function update(dev,par) {
	ready = false;
 	plugins.update(dev,par);
 	ready = true;
	DEBUG && console.log('update done for device:'+dev+' parameter:'+par);
}

function setFhemStatus(device,status) {
	startInterval();
    DEBUG && console.log("set "+device+" "+status);
	$.ajax({
		async: true,
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: "set "+device+" "+status,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
	})
  	.done ( function( data ) {
  		if ( !doLongPoll ){
			setTimeout(function(){
				for (var reading in readings) {
					requestFhem(reading);
				}
			}, 4000);
		}
	});
}

var xhr;
var currLine=0;
function longPoll(roomName) {
/* try to avoid this terrible fmt=JSON output format
	- no separat node for parameter name
	- multiple nodes with the same data (2xdate)
*/
	DEBUG && console.log('start longpoll');

	if (xhr)
		xhr.abort();
	currLine=0;

	$.ajax({
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		cache: false,
		complete: function() {
			setTimeout(function() {
				longPoll();
			}, 100);
		},
		timeout: 60000,
		async: true,
		data: {
			XHR:1,
			inform: "type=raw;filter=.*"
		},
		xhr: function() {
			xhr = new window.XMLHttpRequest();
			xhr.addEventListener("readystatechange", function(e){
				var data = e.target.responseText;
		  		if ( e.target.readyState == 4) {
    				//$.toast("Connection lost, trying to reconnect in 5 seconds.");
    				return;
  				}
				if ( e.target.readyState == 3 )
				{
					var lines = data.replace(/
/g,"").split(/\n/);
					var regDevice = /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(\S*)\s(\S*)\s(.*)/;
					var regDate = /^([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;
					var regParaname = /^(\S{3,}):\s?(.*)$/;
					lines.pop(); //remove last empty line

					for (var i=currLine; i < lines.length; i++) {
						var date;
						var line = $.trim( lines[i] );
						//console.log('#'+line+'#');

						if ( regDate.test( line ))
							date = $.trim(line.match( regDate )[1]);
						if ( regDevice.test( line )) {
							//Bad parse hack, but the JSON is not well formed
							var room = $.trim( line.match( regDevice )[1] );
							var key = $.trim( line.match( regDevice )[2] );
							var parname_val = $.trim(line.match( regDevice )[3]);
							var params = deviceStates[key] || {};
							var paraname;
							var val;
							if ( regParaname.test(parname_val) ){
								var paraname = $.trim(parname_val.match( regParaname )[1]);
								var val = $.trim(parname_val.match( regParaname )[2]);
							}
							else {
								var paraname = 'STATE';
								var val = parname_val;
							}
							if ( (paraname in readings) && (key in devices) ){
								var value = {"date": date,
											  "room": room,
												"val": val
											};
								params[paraname]=value;
								deviceStates[key]=params;
								DEBUG && console.log(date + ' / ' + key+' / '+paraname+' / '+val);
								update(key,paraname);
							}
							//console.log(date + ' / ' + key+' / '+paraname+' / '+val);
						}
					}
					currLine = lines.length;
				}

    		}, false);
			return xhr;
			}
	});
}

function requestFhem(paraname) {
/* 'list' is still the fastest cmd to get all important data
*/
	$.ajax({
		async: true,
		cache: false,
		context:{paraname: paraname},
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: "list " + devs.join() + " " + paraname,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
  	})
  	.done (function( data ) {
			var lines = data.replace(/\n\)/g,")\n").split(/\n/);
			var regDevice = /^(\S*)\s.*/;
			var regState = (this.paraname!='STATE')
						? /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(.*)/
						: /^\S*\s*(.*)/;
			var regDate = /\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;

			for (var i=0; i < lines.length; i++) {
				var date;
				var line = $.trim( lines[i] );
				if ( regDate.test( line ))
					date = $.trim( line.match( regDate )[1] );
				if (regState.test(line) && regDevice.test(line) ) {
					var key = $.trim( line.match( regDevice )[1] );
					var val = $.trim( line.match( regState )[1] );
					var params = deviceStates[key] || {};
					var paraname = this.paraname;
					var value = {"date": date, "val": val};
					params[paraname]=value;
					if (key in devices)
						deviceStates[key]=params;
				}
			}
		reading_cntr--;
		if ( reading_cntr < 1 ) {
			update('*','*');
			reading_cntr = Object.keys(readings).length;
 		}
	});

}

this.getPart = function (s,p) {
	var c = (s && typeof s != "undefined") ? s.split(" ") : '';
	return (c.length >= p && p>0 ) ? c[p-1] : s;
};

this.getDeviceValue = function (device, src) {
	var devname	= device.data('device');
	var paraname =	(src && src != '') ? device.data(src) : Object.keys(readings)[0];
	if (devname && devname.length>0){
		var params = deviceStates[devname];
		return ( params && params[paraname] ) ? params[paraname].val : null;
	}
	return null;
}

<|endoftext|>


<commit_before>/* FHEM tablet ui */
/**
* Just another dashboard for FHEM
*
* Version: 1.3.1
* Requires: jQuery v1.7+, font-awesome, jquery.gridster, jquery.toast
*
* Copyright (c) 2015 Mario Stephan <mstephan@shared-files.de>
* Under MIT License (http://www.opensource.org/licenses/mit-license.php)
*
*/
var deviceStates={};
var readings = {"STATE":true};
var devices = {};
var types = {};
var ready = true;
var reading_cntr;
var DEBUG = false;
var doLongPoll = false
var timer;
var dir;
var shortpollInterval = 30 * 1000; // 30 seconds
var devs=Array();

var plugins = {
  modules: [],
  addModule: function (module) {
    this.modules.push(module);
  },
  load: function (name) {
  	$.ajax({
		url: dir+'/'+name+'.js',
		dataType: "script",
		cache: true,
		async: false,
		context:{name: name},
		success: function () {
				DEBUG && console.log('Loaded plugin: '+this.name);
				var module = eval(this.name);
				plugins.addModule(module);
				module.init();
		},
	});
  },
  update: function (dev,par) {
    $.each(this.modules, function (index, module) {
      //Iterate each module and run update function
      module.update(dev,par);
    });
  }
}

$( document ).ready(function() {

	wx = parseInt( $("meta[name='widget_base_width']").attr("content") );
	wy = parseInt( $("meta[name='widget_base_height']").attr("content") );
	doLongPoll = ($("meta[name='longpoll']").attr("content") == '1');
	DEBUG  = ($("meta[name='debug']").attr("content") == '1');

	//self path
	dir = $('script[src$="fhem-tablet-ui.js"]').attr('src');
	var name = dir.split('/').pop();
	dir = dir.replace('/'+name,"");
	DEBUG && console.log('Plugin dir: '+dir);

	//init gridster
	gridster = $(".gridster > ul").gridster({
          widget_base_dimensions: [wx, wy],
          widget_margins: [5, 5],
          draggable: {
            handle: 'header'
          }
        }).data('gridster');

	//make it HTML conform (remove this after migration)
	$('div[type]').each(function() {
    	$(this).attr({
            'data-type' : $(this).attr('type'),
        })
        .removeAttr('type');
	});
	$('div[device]').each(function() {
    	$(this).attr({
            'data-device' : $(this).attr('device'),
        })
        .removeAttr('device');
	});
	//end **** (remove this after migration)

	//collect required widgets types
	$('div[data-type]').each(function(index){
		var t = $(this).data("type");
		if(!types[t])
			types[t] = true;
	});

	//init widgets
	for (var widget_type in types) {
		plugins.load('widget_'+widget_type);
	}
	DEBUG && console.log('Collecting required readings');
	//collect required readings
	$('[data-get]').each(function(index){
		var reading = $(this).data("get");
		if(!readings[reading])
			readings[reading] = true;
	});

	//collect required devices
	$('div[data-device]').each(function(index){
		var device = $(this).data("device");
		if(!devices[device])
			devices[device] = true; devs.push(device);
	});

	//get current values of readings
	for (var reading in readings) {
		requestFhem(reading);
	}
	reading_cntr = Object.keys(readings).length;

	if ( doLongPoll ){
		setTimeout(function() {
				longPoll();
		}, 1000);
		shortpollInterval = 15 * 60 * 1000; // 15 minutes
	}

	$("*").focus(function(){
    	$(this).blur();
  	});

	// refresh every x secs
	startInterval();
});

function startInterval() {
     clearInterval(timer);
     timer = setInterval(function () {
		//get current values of readings every x seconds
		for (var reading in readings) {
			requestFhem(reading);
		}
     }, shortpollInterval);
 }

function update(dev,par) {
	ready = false;
 	plugins.update(dev,par);
 	ready = true;
	DEBUG && console.log('update done for device:'+dev+' parameter:'+par);
}

function setFhemStatus(cmdline) {
	startInterval();
    DEBUG && console.log('send to FHEM: '+cmdline);
	$.ajax({
		async: true,
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: cmdline,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
	})
  	.done ( function( data ) {
  		if ( !doLongPoll ){
			setTimeout(function(){
				for (var reading in readings) {
					requestFhem(reading);
				}
			}, 4000);
		}
	});
}

var xhr;
var currLine=0;
function longPoll(roomName) {
/* try to avoid this terrible fmt=JSON output format
	- no separat node for parameter name
	- multiple nodes with the same data (2xdate)
*/
	DEBUG && console.log('start longpoll');

	if (xhr)
		xhr.abort();
	currLine=0;

	$.ajax({
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		cache: false,
		complete: function() {
			setTimeout(function() {
				longPoll();
			}, 100);
		},
		timeout: 60000,
		async: true,
		data: {
			XHR:1,
			inform: "type=raw;filter=.*"
		},
		xhr: function() {
			xhr = new window.XMLHttpRequest();
			xhr.addEventListener("readystatechange", function(e){
				var data = e.target.responseText;
		  		if ( e.target.readyState == 4) {
    				//$.toast("Connection lost, trying to reconnect in 5 seconds.");
    				return;
  				}
				if ( e.target.readyState == 3 )
				{
					var lines = data.replace(/
/g,"").split(/\n/);
					var regDevice = /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(\S*)\s(\S*)\s(.*)/;
					var regDate = /^([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;
					var regParaname = /^(\S{3,}):\s(.*)$/;
					lines.pop(); //remove last empty line

					for (var i=currLine; i < lines.length; i++) {
						var date;
						var line = $.trim( lines[i] );
						//console.log('#'+line+'#');

						if ( regDate.test( line ))
							date = $.trim(line.match( regDate )[1]);
						if ( regDevice.test( line )) {
							//Bad parse hack, but the JSON is not well formed
							var room = $.trim( line.match( regDevice )[1] );
							var key = $.trim( line.match( regDevice )[2] );
							var parname_val = $.trim(line.match( regDevice )[3]);
							var params = deviceStates[key] || {};
							var paraname;
							var val;
							if ( regParaname.test(parname_val) ){
								var paraname = $.trim(parname_val.match( regParaname )[1]);
								var val = $.trim(parname_val.match( regParaname )[2]);
							}
							else {
								var paraname = 'STATE';
								var val = parname_val;
							}
							if ( (paraname in readings) && (key in devices) ){
								var value = {"date": date,
											  "room": room,
												"val": val
											};
								params[paraname]=value;
								deviceStates[key]=params;
								DEBUG && console.log(date + ' / ' + key+' / '+paraname+' / '+val);
								update(key,paraname);
							}
							//console.log(date + ' / ' + key+' / '+paraname+' / '+val);
						}
					}
					currLine = lines.length;
				}

    		}, false);
			return xhr;
			}
	});
}

function requestFhem(paraname) {
/* 'list' is still the fastest cmd to get all important data
*/
	$.ajax({
		async: true,
		cache: false,
		context:{paraname: paraname},
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: "list " + devs.join() + " " + paraname,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
  	})
  	.done (function( data ) {
			var lines = data.replace(/\n\)/g,")\n").split(/\n/);
			var regDevice = /^(\S*)\s.*/;
			var regState = (this.paraname!='STATE')
						? /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(.*)/
						: /^\S*\s*(.*)/;
			var regDate = /\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;

			for (var i=0; i < lines.length; i++) {
				var date;
				var line = $.trim( lines[i] );
				if ( regDate.test( line ))
					date = $.trim( line.match( regDate )[1] );
				if (regState.test(line) && regDevice.test(line) ) {
					var key = $.trim( line.match( regDevice )[1] );
					var val = $.trim( line.match( regState )[1] );
					var params = deviceStates[key] || {};
					var paraname = this.paraname;
					var value = {"date": date, "val": val};
					params[paraname]=value;
					if (key in devices)
						deviceStates[key]=params;
				}
			}
		reading_cntr--;
		if ( reading_cntr < 1 ) {
			update('*','*');
			reading_cntr = Object.keys(readings).length;
 		}
	});

}

this.getPart = function (s,p) {
	var c = (s && typeof s != "undefined") ? s.split(" ") : '';
	return (c.length >= p && p>0 ) ? c[p-1] : s;
};

this.getDeviceValue = function (device, src) {
	var devname	= device.data('device');
	var paraname =	(src && src != '') ? device.data(src) : Object.keys(readings)[0];
	if (devname && devname.length>0){
		var params = deviceStates[devname];
		return ( params && params[paraname] ) ? params[paraname].val : null;
	}
	return null;
}

<commit_msg>Added RegEx for data-part<commit_after>/* FHEM tablet ui */
/**
* Just another dashboard for FHEM
*
* Version: 1.3.1
* Requires: jQuery v1.7+, font-awesome, jquery.gridster, jquery.toast
*
* Copyright (c) 2015 Mario Stephan <mstephan@shared-files.de>
* Under MIT License (http://www.opensource.org/licenses/mit-license.php)
*
*/
var deviceStates={};
var readings = {"STATE":true};
var devices = {};
var types = {};
var ready = true;
var reading_cntr;
var DEBUG = false;
var doLongPoll = false
var timer;
var dir;
var shortpollInterval = 30 * 1000; // 30 seconds
var devs=Array();

var plugins = {
  modules: [],
  addModule: function (module) {
    this.modules.push(module);
  },
  load: function (name) {
  	$.ajax({
		url: dir+'/'+name+'.js',
		dataType: "script",
		cache: true,
		async: false,
		context:{name: name},
		success: function () {
				DEBUG && console.log('Loaded plugin: '+this.name);
				var module = eval(this.name);
				plugins.addModule(module);
				module.init();
		},
	});
  },
  update: function (dev,par) {
    $.each(this.modules, function (index, module) {
      //Iterate each module and run update function
      module.update(dev,par);
    });
  }
}

$( document ).ready(function() {

	wx = parseInt( $("meta[name='widget_base_width']").attr("content") );
	wy = parseInt( $("meta[name='widget_base_height']").attr("content") );
	doLongPoll = ($("meta[name='longpoll']").attr("content") == '1');
	DEBUG  = ($("meta[name='debug']").attr("content") == '1');

	//self path
	dir = $('script[src$="fhem-tablet-ui.js"]').attr('src');
	var name = dir.split('/').pop();
	dir = dir.replace('/'+name,"");
	DEBUG && console.log('Plugin dir: '+dir);

	//init gridster
	gridster = $(".gridster > ul").gridster({
          widget_base_dimensions: [wx, wy],
          widget_margins: [5, 5],
          draggable: {
            handle: 'header'
          }
        }).data('gridster');

	//make it HTML conform (remove this after migration)
	$('div[type]').each(function() {
    	$(this).attr({
            'data-type' : $(this).attr('type'),
        })
        .removeAttr('type');
	});
	$('div[device]').each(function() {
    	$(this).attr({
            'data-device' : $(this).attr('device'),
        })
        .removeAttr('device');
	});
	//end **** (remove this after migration)

	//collect required widgets types
	$('div[data-type]').each(function(index){
		var t = $(this).data("type");
		if(!types[t])
			types[t] = true;
	});

	//init widgets
	for (var widget_type in types) {
		plugins.load('widget_'+widget_type);
	}
	DEBUG && console.log('Collecting required readings');
	//collect required readings
	$('[data-get]').each(function(index){
		var reading = $(this).data("get");
		if(!readings[reading])
			readings[reading] = true;
	});

	//collect required devices
	$('div[data-device]').each(function(index){
		var device = $(this).data("device");
		if(!devices[device])
			devices[device] = true; devs.push(device);
	});

	//get current values of readings
	for (var reading in readings) {
		requestFhem(reading);
	}
	reading_cntr = Object.keys(readings).length;

	if ( doLongPoll ){
		setTimeout(function() {
				longPoll();
		}, 1000);
		shortpollInterval = 15 * 60 * 1000; // 15 minutes
	}

	$("*").focus(function(){
    	$(this).blur();
  	});

	// refresh every x secs
	startInterval();
});

function startInterval() {
     clearInterval(timer);
     timer = setInterval(function () {
		//get current values of readings every x seconds
		for (var reading in readings) {
			requestFhem(reading);
		}
     }, shortpollInterval);
 }

function update(dev,par) {
	ready = false;
 	plugins.update(dev,par);
 	ready = true;
	DEBUG && console.log('update done for device:'+dev+' parameter:'+par);
}

function setFhemStatus(cmdline) {
	startInterval();
    DEBUG && console.log('send to FHEM: '+cmdline);
	$.ajax({
		async: true,
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: cmdline,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
	})
  	.done ( function( data ) {
  		if ( !doLongPoll ){
			setTimeout(function(){
				for (var reading in readings) {
					requestFhem(reading);
				}
			}, 4000);
		}
	});
}

var xhr;
var currLine=0;
function longPoll(roomName) {
/* try to avoid this terrible fmt=JSON output format
	- no separat node for parameter name
	- multiple nodes with the same data (2xdate)
*/
	DEBUG && console.log('start longpoll');

	if (xhr)
		xhr.abort();
	currLine=0;

	$.ajax({
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		cache: false,
		complete: function() {
			setTimeout(function() {
				longPoll();
			}, 100);
		},
		timeout: 60000,
		async: true,
		data: {
			XHR:1,
			inform: "type=raw;filter=.*"
		},
		xhr: function() {
			xhr = new window.XMLHttpRequest();
			xhr.addEventListener("readystatechange", function(e){
				var data = e.target.responseText;
		  		if ( e.target.readyState == 4) {
    				//$.toast("Connection lost, trying to reconnect in 5 seconds.");
    				return;
  				}
				if ( e.target.readyState == 3 )
				{
					var lines = data.replace(/
/g,"").split(/\n/);
					var regDevice = /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(\S*)\s(\S*)\s(.*)/;
					var regDate = /^([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;
					var regParaname = /^(\S{3,}):\s?(.*)$/;
					lines.pop(); //remove last empty line

					for (var i=currLine; i < lines.length; i++) {
						var date;
						var line = $.trim( lines[i] );
						//console.log('#'+line+'#');

						if ( regDate.test( line ))
							date = $.trim(line.match( regDate )[1]);
						if ( regDevice.test( line )) {
							//Bad parse hack, but the JSON is not well formed
							var room = $.trim( line.match( regDevice )[1] );
							var key = $.trim( line.match( regDevice )[2] );
							var parname_val = $.trim(line.match( regDevice )[3]);
							var params = deviceStates[key] || {};
							var paraname;
							var val;
							if ( regParaname.test(parname_val) ){
								var paraname = $.trim(parname_val.match( regParaname )[1]);
								var val = $.trim(parname_val.match( regParaname )[2]);
							}
							else {
								var paraname = 'STATE';
								var val = parname_val;
							}
							if ( (paraname in readings) && (key in devices) ){
								var value = {"date": date,
											  "room": room,
												"val": val
											};
								params[paraname]=value;
								deviceStates[key]=params;
								DEBUG && console.log(date + ' / ' + key+' / '+paraname+' / '+val);
								update(key,paraname);
							}
							//console.log(date + ' / ' + key+' / '+paraname+' / '+val);
						}
					}
					currLine = lines.length;
				}

    		}, false);
			return xhr;
			}
	});
}

function requestFhem(paraname) {
/* 'list' is still the fastest cmd to get all important data
*/
	$.ajax({
		async: true,
		cache: false,
		context:{paraname: paraname},
		url: $("meta[name='fhemweb_url']").attr("content") || "../fhem/",
		data: {
			cmd: "list " + devs.join() + " " + paraname,
			XHR: "1"
		}
	})
	.fail (function(jqXHR, textStatus, errorThrown) {
    		$.toast("Error: " + textStatus + ": " + errorThrown);
  	})
  	.done (function( data ) {
			var lines = data.replace(/\n\)/g,")\n").split(/\n/);
			var regDevice = /^(\S*)\s.*/;
			var regState = (this.paraname!='STATE')
						? /\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9]\s(.*)/
						: /^\S*\s*(.*)/;
			var regDate = /\s([0-9]{4}-[0-9]{2}-[0-9]{2}\s[0-2][0-9]:[0-5][0-9]:[0-5][0-9])\s/;

			for (var i=0; i < lines.length; i++) {
				var date;
				var line = $.trim( lines[i] );
				if ( regDate.test( line ))
					date = $.trim( line.match( regDate )[1] );
				if (regState.test(line) && regDevice.test(line) ) {
					var key = $.trim( line.match( regDevice )[1] );
					var val = $.trim( line.match( regState )[1] );
					var params = deviceStates[key] || {};
					var paraname = this.paraname;
					var value = {"date": date, "val": val};
					params[paraname]=value;
					if (key in devices)
						deviceStates[key]=params;
				}
			}
		reading_cntr--;
		if ( reading_cntr < 1 ) {
			update('*','*');
			reading_cntr = Object.keys(readings).length;
 		}
	});

}

this.getPart = function (s,p) {
	if ($.isNumeric(p)){
		var c = (s && typeof s != "undefined") ? s.split(" ") : '';
		return (c.length >= p && p>0 ) ? c[p-1] : s;
	}
	else {
		var matches = s.match( RegExp('^' + p + '$') );
		var ret='';
		if (matches) {
			for (var i=1;i<matches.length;i++) {
				ret+=matches[i];
			}
		}
		return ret;
	}

};

this.getDeviceValue = function (device, src) {
	var devname	= device.data('device');
	var paraname =	(src && src != '') ? device.data(src) : Object.keys(readings)[0];
	if (devname && devname.length>0){
		var params = deviceStates[devname];
		return ( params && params[paraname] ) ? params[paraname].val : null;
	}
	return null;
}

<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.u2',
  name: 'Entity',
  // TODO: Make both Entity and Element extend a common base-Model (Node?)

  documentation: 'Virtual-DOM Entity.',

  properties: [
    {
      name: 'name',
      // parser: seq(alphaChar, repeat0(wordChar)),
      regex: /^[a-z]\w*$/i
    }
  ],

  methods: [
    function output(out) { out('&', this.name, ';'); }
  ]
});
<commit_msg>U2: Fixed Entity regex to allow for number format entities.<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

CLASS({
  package: 'foam.u2',
  name: 'Entity',
  // TODO: Make both Entity and Element extend a common base-Model (Node?)

  documentation: 'Virtual-DOM Entity.',

  properties: [
    {
      name: 'name',
      // parser: seq(alphaChar, repeat0(wordChar)),
      regex: /^[a-z#]\w*$/i
    }
  ],

  methods: [
    function output(out) { out('&', this.name, ';'); }
  ]
});
<|endoftext|>