<commit_before>var message_edit = (function () {
var exports = {};
var currently_editing_messages = {};

// Returns true if the edit task should end.
exports.save = function (row, from_topic_edited_only) {
    var msg_list = current_msg_list;
    var message_id;

    if (row.hasClass('recipient_row')) {
        message_id = rows.id_for_recipient_row(row);
    } else {
        message_id = rows.id(row);
    }
    var message = current_msg_list.get(message_id);
    var changed = false;

    var new_content = row.find(".message_edit_content").val();
    var topic_changed = false;
    var new_topic;
    if (message.type === "stream") {
        new_topic = row.find(".message_edit_topic").val();
        topic_changed = (new_topic !== message.subject && new_topic.trim() !== "");
    }

    // Editing a not-yet-acked message (because the original send attempt failed)
    // just results in the in-memory message being changed
    if (message.local_id !== undefined) {
        // No changes
        if (new_content === message.raw_content && !topic_changed) {
            return true;
        }
        echo.edit_locally(message, new_content, topic_changed ? new_topic : undefined);
        return true;
    }

    var request = {message_id: message.id};
    if (topic_changed) {
        request.subject = new_topic;
        if (feature_flags.propagate_topic_edits) {
            var selected_topic_propagation = row.find("select.message_edit_topic_propagate").val() || "change_later";
            request.propagate_mode = selected_topic_propagation;
        }
        changed = true;
    }

    if (new_content !== message.raw_content && !from_topic_edited_only) {
        request.content = new_content;
        message.is_me_message = new_content.lastIndexOf('/me', 0) === 0;
        changed = true;
    }
    if (!changed) {
        // If they didn't change anything, just cancel it.
        return true;
    }
    channel.post({
        url: '/json/update_message',
        data: request,
        success: function (data) {
            if (msg_list === current_msg_list) {
                return true;
            }
        },
        error: function (xhr, error_type, xhn) {
            var message = channel.xhr_error_message("Error saving edit", xhr);
            row.find(".edit_error").text(message).show();
        }
    });
    // The message will automatically get replaced when it arrives.
};

function handle_edit_keydown(from_topic_edited_only, e) {
    var row, code = e.keyCode || e.which;

    if (e.target.id === "message_edit_content" && code === 13 &&
        (e.metaKey || e.ctrlKey)) {
        row = $(".message_edit_content").filter(":focus").closest(".message_row");
        if (message_edit.save(row, from_topic_edited_only) === true) {
            message_edit.end(row);
        }
    } else if (e.target.id === "message_edit_topic" && code === 13) {
        // Hitting enter in topic field isn't so great.
        e.stopPropagation();
        e.preventDefault();
    }
}

function timer_text(seconds_left) {
    var minutes = Math.floor(seconds_left / 60);
    var seconds = seconds_left % 60;
    if (minutes >= 1) {
        return i18n.t("__minutes__ min to edit", {'minutes': minutes.toString()});
    } else if (seconds_left >= 10) {
        return i18n.t("__seconds__ sec to edit", {'seconds': (seconds - seconds % 5).toString()});
    }
    return i18n.t("__seconds__ sec to edit", {'seconds': seconds.toString()});
}

function edit_message (row, raw_content) {
    var content_top = row.find('.message_content')[0]
        .getBoundingClientRect().top;

    var message = current_msg_list.get(rows.id(row));
    var edit_row = row.find(".message_edit");
    var form = $(templates.render('message_edit_form',
                                  {is_stream: message.is_stream,
                                   topic: message.subject,
                                   content: raw_content,
                                   minutes_to_edit: Math.floor(page_params.realm_message_content_edit_limit_seconds / 60)}));

    var edit_obj = {form: form, raw_content: raw_content};
    var original_topic = message.subject;

    current_msg_list.show_edit_message(row, edit_obj);

    form.keydown(_.partial(handle_edit_keydown, false));

    // We potentially got to this function by clicking a button that implied the
    // user would be able to edit their message.  Give a little bit of buffer in
    // case the button has been around for a bit, e.g. we show the
    // edit_content_button (hovering pencil icon) as long as the user would have
    // been able to click it at the time the mouse entered the message_row. Also
    // a buffer in case their computer is slow, or stalled for a second, etc
    // If you change this number also change edit_limit_buffer in
    // zerver.views.messages.update_message_backend
    var seconds_left_buffer = 5;

    var now = new XDate();
    var seconds_left = page_params.realm_message_content_edit_limit_seconds +
        now.diffSeconds(message.timestamp * 1000);
    var can_edit_content = (page_params.realm_message_content_edit_limit_seconds === 0) ||
        (seconds_left + seconds_left_buffer > 0);
    if (!can_edit_content) {
        row.find('textarea.message_edit_content').attr("disabled","disabled");
    }

    // If we allow editing at all, give them at least 10 seconds to do it.
    // If you change this number also change edit_limit_buffer in
    // zerver.views.messages.update_message_backend
    var min_seconds_to_edit = 10;
    seconds_left = Math.floor(Math.max(seconds_left, min_seconds_to_edit));

    if (page_params.realm_message_content_edit_limit_seconds > 0) {
        row.find('.message-edit-timer-control-group').show();
        $('#message_edit_tooltip').tooltip({ animation: false, placement: 'left',
                                             template: '

'});
        var timer_row = row.find('.message_edit_countdown_timer');
        if (can_edit_content) {  // Add a visual timer
            // I believe these need to be defined outside the countdown_timer, since
            // row just refers to something like the currently selected message, and
            // can change out from under us
            var message_content_row = row.find('textarea.message_edit_content');
            var message_topic_row, message_topic_propagate_row;
            if (message.is_stream) {
                message_topic_row = row.find('input.message_edit_topic');
                message_topic_propagate_row = row.find('select.message_edit_topic_propagate');
            }
            var message_save_row = row.find('button.message_edit_save');
            // Do this right away, rather than waiting for the timer to do its first update,
            // since otherwise there is a noticeable lag
            timer_row.text(timer_text(seconds_left));
            var countdown_timer = setInterval(function () {
                if (--seconds_left <= 0) {
                    clearInterval(countdown_timer);
                    message_content_row.attr("disabled","disabled");
                    if (message.is_stream) {
                        message_topic_row.attr("disabled","disabled");
                        message_topic_propagate_row.hide();
                    }
                    // We don't go directly to "Topic editing only" state (with an active Save button),
                    // since it isn't clear what to do with the half-finished edit. It's nice to keep
                    // the half-finished edit around so that they can copy-paste it, but we don't want
                    // people to think "Save" will save the half-finished edit.
                    message_save_row.addClass("disabled");
                    timer_row.text(i18n.t("Time's up!"));
                } else {
                    timer_row.text(timer_text(seconds_left));
                }
            }, 1000);
        } else { // otherwise, give a hint as to why you can edit the topic but not the message content
            timer_row.text(i18n.t("Topic editing only"));
        }
    }

    currently_editing_messages[message.id] = edit_obj;
    if ((message.type === 'stream' && message.subject === compose.empty_topic_placeholder()) ||
        !can_edit_content) {
        edit_row.find(".message_edit_topic").focus();
    } else {
        edit_row.find(".message_edit_content").focus();
    }

    // Scroll to keep the message content in the same place
    var edit_top = edit_row.find('.message_edit_content')[0]
        .getBoundingClientRect().top;

    var scroll_by = edit_top - content_top + 5 /* border and padding */;
    edit_obj.scrolled_by = scroll_by;
    viewport.scrollTop(viewport.scrollTop() + scroll_by);

    if (feature_flags.propagate_topic_edits && message.local_id === undefined) {
        var topic_input = edit_row.find(".message_edit_topic");
        topic_input.keyup( function () {
            var new_topic = topic_input.val();
            row.find('.message_edit_topic_propagate').toggle(new_topic !== original_topic);
        });
    }

    composebox_typeahead.initialize_compose_typeahead("#message_edit_content", {emoji: true});

}

function start_edit_maintaining_scroll(row, content) {
    edit_message(row, content);
    var row_bottom = row.height() + row.offset().top;
    var composebox_top = $("#compose").offset().top;
    if (row_bottom > composebox_top) {
        viewport.scrollTop(viewport.scrollTop() + row_bottom - composebox_top);
    }
}

exports.start = function (row) {
    var message = current_msg_list.get(rows.id(row));
    var msg_list = current_msg_list;
    channel.post({
        url: '/json/fetch_raw_message',
        idempotent: true,
        data: {message_id: message.id},
        success: function (data) {
            if (current_msg_list === msg_list) {
                message.raw_content = data.raw_content;
                start_edit_maintaining_scroll(row, data.raw_content);
            }
        }
    });
};

exports.start_local_failed_edit = function (row, message) {
    start_edit_maintaining_scroll(row, message.raw_content);
};

exports.start_topic_edit = function (recipient_row) {
    var form = $(templates.render('topic_edit_form'));
    current_msg_list.show_edit_topic(recipient_row, form);
    form.keydown(_.partial(handle_edit_keydown, true));
    var msg_id = rows.id_for_recipient_row(recipient_row);
    var message = current_msg_list.get(msg_id);
    var topic = message.subject;
    if (topic === compose.empty_topic_placeholder()) {
        topic = '';
    }
    form.find(".message_edit_topic").val(topic).select().focus();
};

exports.is_editing = function (id) {
    return currently_editing_messages[id] !== undefined;
};

exports.end = function (row) {
    var message = current_msg_list.get(rows.id(row));
    if (message !== undefined &&
        currently_editing_messages[message.id] !== undefined) {
        var scroll_by = currently_editing_messages[message.id].scrolled_by;
        viewport.scrollTop(viewport.scrollTop() - scroll_by);
        delete currently_editing_messages[message.id];
        current_msg_list.hide_edit_message(row);
    }
};

exports.maybe_show_edit = function (row, id) {
    if (currently_editing_messages[id] !== undefined) {
        current_msg_list.show_edit_message(row, currently_editing_messages[id]);
    }
};

$(document).on('narrow_deactivated.zulip', function (event) {
    _.each(currently_editing_messages, function (elem, idx) {
        if (current_msg_list.get(idx) !== undefined) {
            var row = current_msg_list.get_row(idx);
            current_msg_list.show_edit_message(row, elem);
        }
    });
});

return exports;
}());
<commit_msg>javascript: Change all message.is_stream to (message.type === "stream").<commit_after>var message_edit = (function () {
var exports = {};
var currently_editing_messages = {};

// Returns true if the edit task should end.
exports.save = function (row, from_topic_edited_only) {
    var msg_list = current_msg_list;
    var message_id;

    if (row.hasClass('recipient_row')) {
        message_id = rows.id_for_recipient_row(row);
    } else {
        message_id = rows.id(row);
    }
    var message = current_msg_list.get(message_id);
    var changed = false;

    var new_content = row.find(".message_edit_content").val();
    var topic_changed = false;
    var new_topic;
    if (message.type === "stream") {
        new_topic = row.find(".message_edit_topic").val();
        topic_changed = (new_topic !== message.subject && new_topic.trim() !== "");
    }

    // Editing a not-yet-acked message (because the original send attempt failed)
    // just results in the in-memory message being changed
    if (message.local_id !== undefined) {
        // No changes
        if (new_content === message.raw_content && !topic_changed) {
            return true;
        }
        echo.edit_locally(message, new_content, topic_changed ? new_topic : undefined);
        return true;
    }

    var request = {message_id: message.id};
    if (topic_changed) {
        request.subject = new_topic;
        if (feature_flags.propagate_topic_edits) {
            var selected_topic_propagation = row.find("select.message_edit_topic_propagate").val() || "change_later";
            request.propagate_mode = selected_topic_propagation;
        }
        changed = true;
    }

    if (new_content !== message.raw_content && !from_topic_edited_only) {
        request.content = new_content;
        message.is_me_message = new_content.lastIndexOf('/me', 0) === 0;
        changed = true;
    }
    if (!changed) {
        // If they didn't change anything, just cancel it.
        return true;
    }
    channel.post({
        url: '/json/update_message',
        data: request,
        success: function (data) {
            if (msg_list === current_msg_list) {
                return true;
            }
        },
        error: function (xhr, error_type, xhn) {
            var message = channel.xhr_error_message("Error saving edit", xhr);
            row.find(".edit_error").text(message).show();
        }
    });
    // The message will automatically get replaced when it arrives.
};

function handle_edit_keydown(from_topic_edited_only, e) {
    var row, code = e.keyCode || e.which;

    if (e.target.id === "message_edit_content" && code === 13 &&
        (e.metaKey || e.ctrlKey)) {
        row = $(".message_edit_content").filter(":focus").closest(".message_row");
        if (message_edit.save(row, from_topic_edited_only) === true) {
            message_edit.end(row);
        }
    } else if (e.target.id === "message_edit_topic" && code === 13) {
        // Hitting enter in topic field isn't so great.
        e.stopPropagation();
        e.preventDefault();
    }
}

function timer_text(seconds_left) {
    var minutes = Math.floor(seconds_left / 60);
    var seconds = seconds_left % 60;
    if (minutes >= 1) {
        return i18n.t("__minutes__ min to edit", {'minutes': minutes.toString()});
    } else if (seconds_left >= 10) {
        return i18n.t("__seconds__ sec to edit", {'seconds': (seconds - seconds % 5).toString()});
    }
    return i18n.t("__seconds__ sec to edit", {'seconds': seconds.toString()});
}

function edit_message (row, raw_content) {
    var content_top = row.find('.message_content')[0]
        .getBoundingClientRect().top;

    var message = current_msg_list.get(rows.id(row));
    var edit_row = row.find(".message_edit");
    var form = $(templates.render('message_edit_form',
                                  {is_stream: (message.type === 'stream'),
                                   topic: message.subject,
                                   content: raw_content,
                                   minutes_to_edit: Math.floor(page_params.realm_message_content_edit_limit_seconds / 60)}));

    var edit_obj = {form: form, raw_content: raw_content};
    var original_topic = message.subject;

    current_msg_list.show_edit_message(row, edit_obj);

    form.keydown(_.partial(handle_edit_keydown, false));

    // We potentially got to this function by clicking a button that implied the
    // user would be able to edit their message.  Give a little bit of buffer in
    // case the button has been around for a bit, e.g. we show the
    // edit_content_button (hovering pencil icon) as long as the user would have
    // been able to click it at the time the mouse entered the message_row. Also
    // a buffer in case their computer is slow, or stalled for a second, etc
    // If you change this number also change edit_limit_buffer in
    // zerver.views.messages.update_message_backend
    var seconds_left_buffer = 5;

    var now = new XDate();
    var seconds_left = page_params.realm_message_content_edit_limit_seconds +
        now.diffSeconds(message.timestamp * 1000);
    var can_edit_content = (page_params.realm_message_content_edit_limit_seconds === 0) ||
        (seconds_left + seconds_left_buffer > 0);
    if (!can_edit_content) {
        row.find('textarea.message_edit_content').attr("disabled","disabled");
    }

    // If we allow editing at all, give them at least 10 seconds to do it.
    // If you change this number also change edit_limit_buffer in
    // zerver.views.messages.update_message_backend
    var min_seconds_to_edit = 10;
    seconds_left = Math.floor(Math.max(seconds_left, min_seconds_to_edit));

    if (page_params.realm_message_content_edit_limit_seconds > 0) {
        row.find('.message-edit-timer-control-group').show();
        $('#message_edit_tooltip').tooltip({ animation: false, placement: 'left',
                                             template: '

'});
        var timer_row = row.find('.message_edit_countdown_timer');
        if (can_edit_content) {  // Add a visual timer
            // I believe these need to be defined outside the countdown_timer, since
            // row just refers to something like the currently selected message, and
            // can change out from under us
            var message_content_row = row.find('textarea.message_edit_content');
            var message_topic_row, message_topic_propagate_row;
            if (message.type === 'stream') {
                message_topic_row = row.find('input.message_edit_topic');
                message_topic_propagate_row = row.find('select.message_edit_topic_propagate');
            }
            var message_save_row = row.find('button.message_edit_save');
            // Do this right away, rather than waiting for the timer to do its first update,
            // since otherwise there is a noticeable lag
            timer_row.text(timer_text(seconds_left));
            var countdown_timer = setInterval(function () {
                if (--seconds_left <= 0) {
                    clearInterval(countdown_timer);
                    message_content_row.attr("disabled","disabled");
                    if (message.type === 'stream') {
                        message_topic_row.attr("disabled","disabled");
                        message_topic_propagate_row.hide();
                    }
                    // We don't go directly to "Topic editing only" state (with an active Save button),
                    // since it isn't clear what to do with the half-finished edit. It's nice to keep
                    // the half-finished edit around so that they can copy-paste it, but we don't want
                    // people to think "Save" will save the half-finished edit.
                    message_save_row.addClass("disabled");
                    timer_row.text(i18n.t("Time's up!"));
                } else {
                    timer_row.text(timer_text(seconds_left));
                }
            }, 1000);
        } else { // otherwise, give a hint as to why you can edit the topic but not the message content
            timer_row.text(i18n.t("Topic editing only"));
        }
    }

    currently_editing_messages[message.id] = edit_obj;
    if ((message.type === 'stream' && message.subject === compose.empty_topic_placeholder()) ||
        !can_edit_content) {
        edit_row.find(".message_edit_topic").focus();
    } else {
        edit_row.find(".message_edit_content").focus();
    }

    // Scroll to keep the message content in the same place
    var edit_top = edit_row.find('.message_edit_content')[0]
        .getBoundingClientRect().top;

    var scroll_by = edit_top - content_top + 5 /* border and padding */;
    edit_obj.scrolled_by = scroll_by;
    viewport.scrollTop(viewport.scrollTop() + scroll_by);

    if (feature_flags.propagate_topic_edits && message.local_id === undefined) {
        var topic_input = edit_row.find(".message_edit_topic");
        topic_input.keyup( function () {
            var new_topic = topic_input.val();
            row.find('.message_edit_topic_propagate').toggle(new_topic !== original_topic);
        });
    }

    composebox_typeahead.initialize_compose_typeahead("#message_edit_content", {emoji: true});

}

function start_edit_maintaining_scroll(row, content) {
    edit_message(row, content);
    var row_bottom = row.height() + row.offset().top;
    var composebox_top = $("#compose").offset().top;
    if (row_bottom > composebox_top) {
        viewport.scrollTop(viewport.scrollTop() + row_bottom - composebox_top);
    }
}

exports.start = function (row) {
    var message = current_msg_list.get(rows.id(row));
    var msg_list = current_msg_list;
    channel.post({
        url: '/json/fetch_raw_message',
        idempotent: true,
        data: {message_id: message.id},
        success: function (data) {
            if (current_msg_list === msg_list) {
                message.raw_content = data.raw_content;
                start_edit_maintaining_scroll(row, data.raw_content);
            }
        }
    });
};

exports.start_local_failed_edit = function (row, message) {
    start_edit_maintaining_scroll(row, message.raw_content);
};

exports.start_topic_edit = function (recipient_row) {
    var form = $(templates.render('topic_edit_form'));
    current_msg_list.show_edit_topic(recipient_row, form);
    form.keydown(_.partial(handle_edit_keydown, true));
    var msg_id = rows.id_for_recipient_row(recipient_row);
    var message = current_msg_list.get(msg_id);
    var topic = message.subject;
    if (topic === compose.empty_topic_placeholder()) {
        topic = '';
    }
    form.find(".message_edit_topic").val(topic).select().focus();
};

exports.is_editing = function (id) {
    return currently_editing_messages[id] !== undefined;
};

exports.end = function (row) {
    var message = current_msg_list.get(rows.id(row));
    if (message !== undefined &&
        currently_editing_messages[message.id] !== undefined) {
        var scroll_by = currently_editing_messages[message.id].scrolled_by;
        viewport.scrollTop(viewport.scrollTop() - scroll_by);
        delete currently_editing_messages[message.id];
        current_msg_list.hide_edit_message(row);
    }
};

exports.maybe_show_edit = function (row, id) {
    if (currently_editing_messages[id] !== undefined) {
        current_msg_list.show_edit_message(row, currently_editing_messages[id]);
    }
};

$(document).on('narrow_deactivated.zulip', function (event) {
    _.each(currently_editing_messages, function (elem, idx) {
        if (current_msg_list.get(idx) !== undefined) {
            var row = current_msg_list.get_row(idx);
            current_msg_list.show_edit_message(row, elem);
        }
    });
});

return exports;
}());
<|endoftext|>


<commit_before>'use strict'

const Model      = require('./model')
const isTemplate = require('./utils').isTemplate
const importNode = require('./utils').importNode

const parser     = require('./parser')
const ast        = require('./parser/ast')

const idom = require('incremental-dom')

const applyAttributeTyped = idom.attributes[idom.symbols.default]

function applyWithMeta(fallback) {
  return function(el, name, value) {
    if (value && typeof value === 'object' && value instanceof ast.Reference) {
      if (name in el) {
        return
      }

      const reference = value
      Object.defineProperties(el, {
        ['__' + name]: {
          get: () => reference
        },
        [name]: {
          get: () => reference.get(),
          set: val => reference.set(val),
          enumerable: true
        }
      })

      el.removeAttribute(name)
    } else {
      fallback(el, name, value)
    }
  }
}

idom.attributes[idom.symbols.default] = applyWithMeta(applyAttributeTyped)

idom.attributes.checked = function(el, name, value) {
  // idom.applyAttr(el, name, value)
  el[name] = value !== null
}

const valueFallback = applyWithMeta(idom.applyProp)
idom.attributes.value = function(el, name, value) {
  // <select value="{{ foobar }}"></select>
  if (el.tagName === 'SELECT') {
    // delay until select content has been created
    setTimeout(function() {
      // also update the selected html attribute, to make reset buttons work
      // as expected
      const selected = el.querySelectorAll('option[selected]')
      for (let i = 0; i < selected.length; ++i) {
        selected[i].removeAttribute('selected')
        selected[i].selected = false
      }

      if (value !== undefined && value !== null) {
        const option = el.querySelector('option[value="' + value + '"]')
        if (option) {
          option.setAttribute('selected', '')
          option.selected = true
        }
      }
    })
  } else {
    valueFallback(el, name, value)
  }
}

idom.attributes.content = function(el, name, value) {
  if (isTemplate(el)) {
    el.content.appendChild(value)
  } else {
    applyAttributeTyped(el, name, value)
  }
}

let started = false
const observing = []

function start() {
  if (started) {
    return
  }

  started = true

  const frameLength = 33 // this is ~1/30th of a second, in milliseconds (1000/30)
  let lastFrame = 0
  requestAnimationFrame(function animate(delta) {
    if(delta - lastFrame > frameLength) {
      lastFrame = delta

      for (let i = 0; i < observing.length; ++i) {
        const o = observing[i]

        if (!o.root.parentNode) { // removed from DOM
          observing.splice(i--, 1) // remove
          continue
        }

        let hasChanged = false
        for (let j = 0, len = o.handlers.length; j < len; ++j) {
          const handler = o.handlers[j]
          hasChanged = !handler.model.eql(handler.before)
          if (hasChanged) {
            break
          }
        }

        if (hasChanged) {
          // reset changed state on all handlers of the current observer
          for (let j = 0, len = o.handlers.length; j < len; ++j) {
            const handler = o.handlers[j]
            handler.before = handler.model.state()
          }

          console.log('ipoxy: has changed')

          // call update callback
          o.callback()
        }
      }
    }

    requestAnimationFrame(animate)
  })
}

function observe(root, handlers, callback) {
  if (!handlers.length) {
    return
  }

  observing.push({ root: root, handlers: handlers, callback: callback })
  start()
}

module.exports = function bind(target, template, locals) {
  if(!target || target.nodeType !== Node.ELEMENT_NODE) {
    throw new TypeError('Target must be an element node')
  }

  if (!isTemplate(template)) {
    locals = template
    template = undefined
  }

  if (locals === undefined) {
    locals = []
  }

  if (typeof locals !== 'object') {
    throw new TypeError('locals must be an object or an array')
  }

  locals = Array.isArray(locals) ? locals : [locals]

  const root = target
  let content = root
  if (template) {
    content.innerHTML = '' // clear
    content = importNode(template.content, true)
  }

  const update = prepare(root, content)
  const updateFn = function() {
    idom.patch(target, update(locals))
  }

  // patch content in place
  idom.patch(content, update(locals))
  if (template) {
    // append result to root, if a template was used
    root.appendChild(content)
  }

  const handlers = locals
  .filter(local => Model.isModel(local))
  .map(local => {
    return { before: local.state(), model: local }
  })

  observe(target, handlers, updateFn)
  start()

  return updateFn
}

function prepare(root, content) {
  const ctx = { id: 0 }
  const children = prepareChildren(ctx, root, content)
  return function(locals) {
    return function() {
      children(locals)
    }
  }
}

function prepareChildren(ctx, node, content) {
  const callbacks = []

  for (let child = content.firstChild; child; child = child.nextSibling) {
    switch (child.nodeType) {
      case Node.ELEMENT_NODE:
        callbacks.push.apply(callbacks, visitElementNode(ctx, node, child))
        break
      case Node.TEXT_NODE:
        callbacks.push.apply(callbacks, visitTextNode(child))
        break
      default:
        continue
    }
  }

  return function(locals, id) {
    callbacks.forEach(function(fn) {
      fn(locals, id)
    })
  }
}

const RepeatMixin       = require('./mixin/repeat')
const IfMixin           = require('./mixin/if')
const UnlessMixin       = require('./mixin/unless')

const Attribute         = require('./attribute/attribute')
const BooleanAttribute  = require('./attribute/boolean')
const InputAttribute    = require('./attribute/input')

const MIXINS = [IfMixin, UnlessMixin, RepeatMixin]

function visitElementNode(ctx, parent, node, content) {
  const id = ++ctx.id

  // attributes
  const statics = []
  const attributes = []
  for (let i = 0, len = node.attributes.length; i < len; ++i) {
    const attr = node.attributes[i]
    const template = parser.parse(attr.value)

    if (!template.hasExpressions) {
      statics.push(attr.name, attr.value)
    } else {
      let attribute

      if (BooleanAttribute.isBooleanAttribute(node, attr)) {
        attribute = new BooleanAttribute(template, node, attr)
      } else if (InputAttribute.isInputValue(node, attr)) {
        attribute = new InputAttribute(template, node, attr)
      } else {
        attribute = new Attribute(template, node, attr)
      }

      attributes.push(attribute)
    }
  }

  // <textarea>
  const tagName = node.tagName.toLowerCase()
  if (tagName === 'textarea') {
    const attribute = new InputAttribute(parser.parse(node.value), node, { name: 'input' })
    statics.push.apply(statics, attribute.statics())
    attributes.push(attribute)
  }

  // <template>
  const isTempl = isTemplate(node)
  if (isTempl) {
    let head

    MIXINS.forEach(function(Mixin) {
      if (!Mixin['is' + Mixin.name](node)) {
        return
      }

      const template = parser.parse(node.getAttribute(Mixin.property) || '') || null

      if (!template.isSingleExpression) {
        throw new TypeError('Only one single expression allowd for mixins, '
                          + 'got: ' + template.source)
      }

      const content = head || prepareChildren(ctx, node, node.content)
      head = function(locals, keySuffix) {
        const mixin = new Mixin(template, content)
        mixin.update(locals)
        return mixin.execute()
      }
    })

    if (head) {
      return [head]
    }

    statics.push('content', node.content)
  }

  idom.initData(node, tagName, id)
  if (parent) {
    idom.registerChild(parent, id, node)
  }

  const selfContained = node.hasAttribute('self-contained')
  const children = !selfContained && prepareChildren(ctx, node, content || node)

  return [function(locals, idOverride) {
    // TODO: ignore statics, cause node re-use
    const args = [tagName, idOverride || id || null, statics.slice()]
    if (isTempl || selfContained) {
      args.push('locals', locals)
    }
    attributes.forEach(attr => {
      attr.update(locals)
      args.push.apply(args, attr.statics())
      args.push.apply(args, attr.render())
    })
    if (selfContained) {
      const el = idom.elementPlaceholder.apply(idom, args)
      if (el !== node && !el.hasChildNodes()) {
        el.innerHTML = node.innerHTML
      }
    } else {
      idom.elementOpen.apply(idom, args)
      children(locals)
      idom.elementClose(tagName)
    }
  }]
}

const TextNode = require('./node/text')
const HTMLNode = require('./node/html')

function visitTextNode(node) {
  // ignore formatting
  if (node.nodeValue.match(/^\s+$/)) {
    return []
  }

  const template = parser.parse(node.nodeValue)

  if (!template.hasExpressions) {
    const text = node.nodeValue
    return [function() {
      idom.text(text)
    }]
  }

  return template.body.map(function(child) {
    if (child instanceof ast.Expression) {
      let isHTML = false
      for (let i = 0, len = child.filters.length; i < len; ++i) {
        if (child.filters[i].name === 'html') {
          isHTML = true
          child.filters.splice(i, 1)
          break
        }
      }

      const widget = isHTML ? new HTMLNode(child)
                          : new TextNode(child)
      return function(locals) {
        widget.update(locals)
        widget.render()
      }
    } else {
      return function() {
        if (child.text) {
          idom.text(child.text)
        }
      }
    }
  })
}

<commit_msg>remove debug log<commit_after>'use strict'

const Model      = require('./model')
const isTemplate = require('./utils').isTemplate
const importNode = require('./utils').importNode

const parser     = require('./parser')
const ast        = require('./parser/ast')

const idom = require('incremental-dom')

const applyAttributeTyped = idom.attributes[idom.symbols.default]

function applyWithMeta(fallback) {
  return function(el, name, value) {
    if (value && typeof value === 'object' && value instanceof ast.Reference) {
      if (name in el) {
        return
      }

      const reference = value
      Object.defineProperties(el, {
        ['__' + name]: {
          get: () => reference
        },
        [name]: {
          get: () => reference.get(),
          set: val => reference.set(val),
          enumerable: true
        }
      })

      el.removeAttribute(name)
    } else {
      fallback(el, name, value)
    }
  }
}

idom.attributes[idom.symbols.default] = applyWithMeta(applyAttributeTyped)

idom.attributes.checked = function(el, name, value) {
  // idom.applyAttr(el, name, value)
  el[name] = value !== null
}

const valueFallback = applyWithMeta(idom.applyProp)
idom.attributes.value = function(el, name, value) {
  // <select value="{{ foobar }}"></select>
  if (el.tagName === 'SELECT') {
    // delay until select content has been created
    setTimeout(function() {
      // also update the selected html attribute, to make reset buttons work
      // as expected
      const selected = el.querySelectorAll('option[selected]')
      for (let i = 0; i < selected.length; ++i) {
        selected[i].removeAttribute('selected')
        selected[i].selected = false
      }

      if (value !== undefined && value !== null) {
        const option = el.querySelector('option[value="' + value + '"]')
        if (option) {
          option.setAttribute('selected', '')
          option.selected = true
        }
      }
    })
  } else {
    valueFallback(el, name, value)
  }
}

idom.attributes.content = function(el, name, value) {
  if (isTemplate(el)) {
    el.content.appendChild(value)
  } else {
    applyAttributeTyped(el, name, value)
  }
}

let started = false
const observing = []

function start() {
  if (started) {
    return
  }

  started = true

  const frameLength = 33 // this is ~1/30th of a second, in milliseconds (1000/30)
  let lastFrame = 0
  requestAnimationFrame(function animate(delta) {
    if(delta - lastFrame > frameLength) {
      lastFrame = delta

      for (let i = 0; i < observing.length; ++i) {
        const o = observing[i]

        if (!o.root.parentNode) { // removed from DOM
          observing.splice(i--, 1) // remove
          continue
        }

        let hasChanged = false
        for (let j = 0, len = o.handlers.length; j < len; ++j) {
          const handler = o.handlers[j]
          hasChanged = !handler.model.eql(handler.before)
          if (hasChanged) {
            break
          }
        }

        if (hasChanged) {
          // reset changed state on all handlers of the current observer
          for (let j = 0, len = o.handlers.length; j < len; ++j) {
            const handler = o.handlers[j]
            handler.before = handler.model.state()
          }

          // call update callback
          o.callback()
        }
      }
    }

    requestAnimationFrame(animate)
  })
}

function observe(root, handlers, callback) {
  if (!handlers.length) {
    return
  }

  observing.push({ root: root, handlers: handlers, callback: callback })
  start()
}

module.exports = function bind(target, template, locals) {
  if(!target || target.nodeType !== Node.ELEMENT_NODE) {
    throw new TypeError('Target must be an element node')
  }

  if (!isTemplate(template)) {
    locals = template
    template = undefined
  }

  if (locals === undefined) {
    locals = []
  }

  if (typeof locals !== 'object') {
    throw new TypeError('locals must be an object or an array')
  }

  locals = Array.isArray(locals) ? locals : [locals]

  const root = target
  let content = root
  if (template) {
    content.innerHTML = '' // clear
    content = importNode(template.content, true)
  }

  const update = prepare(root, content)
  const updateFn = function() {
    idom.patch(target, update(locals))
  }

  // patch content in place
  idom.patch(content, update(locals))
  if (template) {
    // append result to root, if a template was used
    root.appendChild(content)
  }

  const handlers = locals
  .filter(local => Model.isModel(local))
  .map(local => {
    return { before: local.state(), model: local }
  })

  observe(target, handlers, updateFn)
  start()

  return updateFn
}

function prepare(root, content) {
  const ctx = { id: 0 }
  const children = prepareChildren(ctx, root, content)
  return function(locals) {
    return function() {
      children(locals)
    }
  }
}

function prepareChildren(ctx, node, content) {
  const callbacks = []

  for (let child = content.firstChild; child; child = child.nextSibling) {
    switch (child.nodeType) {
      case Node.ELEMENT_NODE:
        callbacks.push.apply(callbacks, visitElementNode(ctx, node, child))
        break
      case Node.TEXT_NODE:
        callbacks.push.apply(callbacks, visitTextNode(child))
        break
      default:
        continue
    }
  }

  return function(locals, id) {
    callbacks.forEach(function(fn) {
      fn(locals, id)
    })
  }
}

const RepeatMixin       = require('./mixin/repeat')
const IfMixin           = require('./mixin/if')
const UnlessMixin       = require('./mixin/unless')

const Attribute         = require('./attribute/attribute')
const BooleanAttribute  = require('./attribute/boolean')
const InputAttribute    = require('./attribute/input')

const MIXINS = [IfMixin, UnlessMixin, RepeatMixin]

function visitElementNode(ctx, parent, node, content) {
  const id = ++ctx.id

  // attributes
  const statics = []
  const attributes = []
  for (let i = 0, len = node.attributes.length; i < len; ++i) {
    const attr = node.attributes[i]
    const template = parser.parse(attr.value)

    if (!template.hasExpressions) {
      statics.push(attr.name, attr.value)
    } else {
      let attribute

      if (BooleanAttribute.isBooleanAttribute(node, attr)) {
        attribute = new BooleanAttribute(template, node, attr)
      } else if (InputAttribute.isInputValue(node, attr)) {
        attribute = new InputAttribute(template, node, attr)
      } else {
        attribute = new Attribute(template, node, attr)
      }

      attributes.push(attribute)
    }
  }

  // <textarea>
  const tagName = node.tagName.toLowerCase()
  if (tagName === 'textarea') {
    const attribute = new InputAttribute(parser.parse(node.value), node, { name: 'input' })
    statics.push.apply(statics, attribute.statics())
    attributes.push(attribute)
  }

  // <template>
  const isTempl = isTemplate(node)
  if (isTempl) {
    let head

    MIXINS.forEach(function(Mixin) {
      if (!Mixin['is' + Mixin.name](node)) {
        return
      }

      const template = parser.parse(node.getAttribute(Mixin.property) || '') || null

      if (!template.isSingleExpression) {
        throw new TypeError('Only one single expression allowd for mixins, '
                          + 'got: ' + template.source)
      }

      const content = head || prepareChildren(ctx, node, node.content)
      head = function(locals, keySuffix) {
        const mixin = new Mixin(template, content)
        mixin.update(locals)
        return mixin.execute()
      }
    })

    if (head) {
      return [head]
    }

    statics.push('content', node.content)
  }

  idom.initData(node, tagName, id)
  if (parent) {
    idom.registerChild(parent, id, node)
  }

  const selfContained = node.hasAttribute('self-contained')
  const children = !selfContained && prepareChildren(ctx, node, content || node)

  return [function(locals, idOverride) {
    // TODO: ignore statics, cause node re-use
    const args = [tagName, idOverride || id || null, statics.slice()]
    if (isTempl || selfContained) {
      args.push('locals', locals)
    }
    attributes.forEach(attr => {
      attr.update(locals)
      args.push.apply(args, attr.statics())
      args.push.apply(args, attr.render())
    })
    if (selfContained) {
      const el = idom.elementPlaceholder.apply(idom, args)
      if (el !== node && !el.hasChildNodes()) {
        el.innerHTML = node.innerHTML
      }
    } else {
      idom.elementOpen.apply(idom, args)
      children(locals)
      idom.elementClose(tagName)
    }
  }]
}

const TextNode = require('./node/text')
const HTMLNode = require('./node/html')

function visitTextNode(node) {
  // ignore formatting
  if (node.nodeValue.match(/^\s+$/)) {
    return []
  }

  const template = parser.parse(node.nodeValue)

  if (!template.hasExpressions) {
    const text = node.nodeValue
    return [function() {
      idom.text(text)
    }]
  }

  return template.body.map(function(child) {
    if (child instanceof ast.Expression) {
      let isHTML = false
      for (let i = 0, len = child.filters.length; i < len; ++i) {
        if (child.filters[i].name === 'html') {
          isHTML = true
          child.filters.splice(i, 1)
          break
        }
      }

      const widget = isHTML ? new HTMLNode(child)
                          : new TextNode(child)
      return function(locals) {
        widget.update(locals)
        widget.render()
      }
    } else {
      return function() {
        if (child.text) {
          idom.text(child.text)
        }
      }
    }
  })
}

<|endoftext|>


<commit_before>'use strict';

exports = module.exports = boil;

// ## boil(path [, options])
// > normalize a path
//
// arguments
//  - path, type `string`
//  - options, type `object` holding all extra information
//
// returns
//  - true for supported and non-empty types
//  - null for non supported types
//
// --
// api.private
// --

var urlRE = /\/?[?#][^!=:][^ ]+/g;
var boilRE = /([^\/ ]*\/[^\/ ]*|[^\. ]+\.)/g;

function boil(path, o){
  if(typeof path !== 'string'){ return null; }

  o = o || {};
  o.path = path.replace(/[ ]{2,}/g, ' ').trim();
  if(!o.path){ return null; }
  var url = o.path.match(/[^\/ ]*\/\S*/g);

  if(url){
    url = url[0];
    o.path = o.path.replace(url, url.replace(urlRE, '') || '/');
    if(o.notFound){ o.url = url; }
  }

  var argv = o.path.replace(boilRE, '$& ').trim().split(/[ ]+/);
  o.depth = argv.length;
  return argv;
}
<commit_msg>lint and separated boil from the main function<commit_after>'use strict';

exports = module.exports = boil;

// ## boil(path [, options])
// > normalize a path
//
// arguments
//  - path, type `string`
//  - options, type `object` holding all extra information
//
// returns
//  - true for supported and non-empty types
//  - null for non supported types
//
// --
// api.private
// --

var urlRE = /\/?[?#][^!=:][^ ]+/g;
var boilRE = /([^\/ ]*\/[^\/ ]*|[^\. ]+\.)/g;

function boil(path, o){
  if(typeof path !== 'string'){ return null; }
  o = o || {};
  o.path = path.trim();
  if(!o.path){ return null; }
  var url = o.path.match(/[^\/ ]*\/\S*/g);

  if(url){
    url = url[0];
    o.path = o.path.replace(url, url.replace(urlRE, '') || '/');
    if(o.notFound){ o.url = url; }
  }
  o.depth = boil.argv(o.path).length;
  return o;
}

// separated for paths that were already boiled before
//
boil.argv = function(path){
  return path.replace(boilRE, '$& ').trim().split(/[ ]+/);
};
<|endoftext|>


<commit_before>LOG("Loading websock.js");
var succeeded = true;
function WebSock(){
    if (this===window) return new WebSock();
    var self=this;
    var ws = closed;
    var url = '';
    var listeners=self.listeners={};
    var closed = {
	sendEnc:function(){LOG("websock not connected")},
	close  :function(){LOG("websock not connected")}    };
    self.type='websock';
    self.url=function(_url,at){url=_url+at;return self};
    self.addListener=function(x,ns){listeners[ns]=x;return self};
    self.addPlugin=function(plugin,ns){
	self.addListener(plugin,ns);
	plugin.sendEnc=function(data){self.sendEnc(data,ns);return plugin};
	plugin.ns=ns;
	return self;};
    var on=function(ns,verb,msg){
	if(!ns){
	    loop(listeners,function(k,v){
		    on(k,verb,msg);
		});
	    return;
	}
	var nspace=listeners[ns];
	if(!nspace)return;
	var fn=nspace[verb];
	if(!fn){
	    if (nspace.$unknown)
		nspace.$unknown(self,msg,verb);
	    return;
	}
	try{
	    fn(self, msg);
	}catch(e){
	    LOG("  ERR 8 @ ON = "+ns+"::"+verb+"<"+str(msg)+"> err="+e);
	}
    };
    self.isClosed=function() {
	if(ws===closed)return true;
	return ws.closed();    };
    self.loginInfo=function(u,p){
	self.username=u;
	self.password=p;
	return self};
    self.login=function(){
	succeeded=!succeeded;
	if (succeeded)
	    self._loginSuccess(self,'vat');
	else
	    self._loginFailure(self);
	return self};
    self.loginSuccess=function(x){self._loginSuccess=x;return self};
    self.loginFailure=function(x){self._loginFailure=x;return self};
    self.connect=function(message){
	ws = new WebSocket(url);
	ws.onmessage=function(message){
	    var msg=JSON.parse(message.data);
	    on(msg.ns,msg.method,msg.params,self);
	};
	ws.onopen =function(evt){on('','$open', evt)};
	ws.onclose=function(evt){on('','$close',evt)};
	ws.onerror=function(evt){on('','$error',evt)};
	return self}
    self.sendEnc=function(msg,ns){
	LOG("S S SEND ENC" + str([msg,ns]));
	if(ns)msg.ns=ns;
	ws.send(str(msg));
	return self}
    self.close = function(){
	ws.close();
	ws=closed;
	return self}}<commit_msg>only one websocket per websock<commit_after>LOG("Loading websock.js");
var succeeded = true;
function WebSock(){
    if (this===window) return new WebSock();
    var self=this;
    var ws = closed;
    var url = '';
    var listeners=self.listeners={};
    var closed = {
	sendEnc:function(){LOG("websock not connected")},
	close  :function(){LOG("websock not connected")}    };
    self.type='websock';
    self.url=function(_url,at){url=_url+at;return self};
    self.addListener=function(x,ns){listeners[ns]=x;return self};
    self.addPlugin=function(plugin,ns){
	self.addListener(plugin,ns);
	plugin.sendEnc=function(data){self.sendEnc(data,ns);return plugin};
	plugin.ns=ns;
	return self;};
    var on=function(ns,verb,msg){
	if(!ns){
	    loop(listeners,function(k,v){
		    on(k,verb,msg);
		});
	    return;
	}
	var nspace=listeners[ns];
	if(!nspace)return;
	var fn=nspace[verb];
	if(!fn){
	    if (nspace.$unknown)
		nspace.$unknown(self,msg,verb);
	    return;
	}
	try{
	    fn(self, msg);
	}catch(e){
	    LOG("  ERR 8 @ ON = "+ns+"::"+verb+"<"+str(msg)+"> err="+e);
	}
    };
    self.isClosed=function() {
	if(ws===closed)return true;
	return ws.closed();    };
    self.loginInfo=function(u,p){
	self.username=u;
	self.password=p;
	return self};
    self.login=function(){
	succeeded=!succeeded;
	if (succeeded)
	    self._loginSuccess(self,'vat');
	else
	    self._loginFailure(self);
	return self};
    self.loginSuccess=function(x){self._loginSuccess=x;return self};
    self.loginFailure=function(x){self._loginFailure=x;return self};
    self.connect=function(message){
	if (ws) ws.close();
	ws = new WebSocket(url);
	ws.onmessage=function(message){
	    var msg=JSON.parse(message.data);
	    on(msg.ns,msg.method,msg.params,self);
	};
	ws.onopen =function(evt){on('','$open', evt)};
	ws.onclose=function(evt){on('','$close',evt)};
	ws.onerror=function(evt){on('','$error',evt)};
	return self}
    self.sendEnc=function(msg,ns){
	LOG("S S SEND ENC" + str([msg,ns]));
	if(ns)msg.ns=ns;
	ws.send(str(msg));
	return self}
    self.close = function(){
	ws.close();
	ws=closed;
	return self}}<|endoftext|>


<commit_before>'use strict';

import debug from 'debug';
import { v4 as uuidv4 } from 'uuid';
import utils from './utils';

import { EventEmitter } from 'events';

const DEBUG = debug('sylkrtc:Call');

class Call extends EventEmitter {
    constructor(account) {
        super();
        this._account = account;
        this._id = null;
        this._callId = null;
        this._direction = null;
        this._pc = null;
        this._state = null;
        this._terminated = false;
        this._incomingSdp = null;
        this._remoteMediaDirections = {};
        this._localIdentity = new utils.Identity(account.id, account.displayName);
        this._remoteIdentity = null;
        this._remoteStreams = new MediaStream();
        this._localStreams = new MediaStream();
        this._previousTrack = null;
        this._sharingScreen = false;
        this._dtmfSender = null;
        this._delay_established = false;  // set to true when we need to delay posting the state change to 'established'
        this._setup_in_progress = false;  // set while we set the remote description and setup the peer copnnection

        // bind some handlers to this instance
        this._onDtmf = this._onDtmf.bind(this);
    }

    get account() {
        return this._account;
    }

    get id() {
        return this._id;
    }

    get callId() {
        return this._callId;
    }

    get sharingScreen() {
        return this._sharingScreen;
    }

    get direction() {
        return this._direction;
    }

    get state() {
        return this._state;
    }

    get localIdentity() {
        return this._localIdentity;
    }

    get remoteIdentity() {
        return this._remoteIdentity;
    }

    get remoteMediaDirections() {
        return this._remoteMediaDirections;
    }

    getLocalStreams() {
        if (this._pc !== null) {
            if (this._pc.getSenders) {
                this._pc.getSenders().forEach((e) => {
                    if (e.track != null) {
                        if (e.track.readyState !== "ended") {
                            this._localStreams.addTrack(e.track);
                        } else {
                            this._localStreams.removeTrack(e.track);
                        }
                    }
                });
                return [this._localStreams];
            } else {
                return this._pc.getLocalStreams();
            }
        } else {
            return [];
        }
    }

    getRemoteStreams() {
        if (this._pc !== null) {
           if (this._pc.getReceivers) {
              this._pc.getReceivers().forEach((e) => {
                  if (e.track.readyState !== "ended") {
                    this._remoteStreams.addTrack(e.track);
                  }
              });
              return [this._remoteStreams];
          } else {
                return this._pc.getRemoteStreams();
          }
        } else {
            return [];
        }
    }

    getSenders() {
        if (this._pc !== null) {
           return this._pc.getSenders();
        } else {
            return [];
        }
    }

    getReceivers() {
        if (this._pc !== null) {
           return this._pc.getReceivers();
        } else {
            return [];
        }
    }

    answer(options = {}) {
        if (this._state !== 'incoming') {
            throw new Error('Call is not in the incoming state: ' + this._state);
        }

        if (!options.localStream) {
            throw new Error('Missing localStream');
        }

        const pcConfig = options.pcConfig || {iceServers:[]};
        const answerOptions = options.answerOptions;

        // Create the RTCPeerConnection
        this._initRTCPeerConnection(pcConfig);

        this._pc.addStream(options.localStream);
        this.emit('localStreamAdded', options.localStream);
        this._pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: this._incomingSdp}))
            // success
            .then(() => {
                utils.createLocalSdp(this._pc, 'answer', answerOptions)
                    .then((sdp) => {
                        DEBUG('Local SDP: %s', sdp);
                        this._sendAnswer(sdp);
                    })
                    .catch((reason) => {
                        DEBUG(reason);
                        this.terminate();
                    });
            })
            // failure
            .catch((error) => {
                DEBUG('Error setting remote description: %s', error);
                this.terminate();
            });
    }

    startScreensharing(newTrack) {
        let oldTrack = this.getLocalStreams()[0].getVideoTracks()[0];
        this.replaceTrack(oldTrack, newTrack, true, (value) => {
            this._sharingScreen = value;
        });
    }

    stopScreensharing() {
        let oldTrack = this.getLocalStreams()[0].getVideoTracks()[0];
        this.replaceTrack(oldTrack, this._previousTrack);
        this._sharingScreen = false;
    }

    replaceTrack(oldTrack, newTrack, keep=false, cb=null) {
        let sender;
        for (sender of this._pc.getSenders()) {
            if (sender.track === oldTrack) {
                break;
            }
        }

        sender.replaceTrack(newTrack)
            .then(() => {
                if (keep) {
                    this._previousTrack = oldTrack;
                } else {
                    if (oldTrack) {
                        oldTrack.stop();
                    }
                    if (newTrack === this._previousTrack) {
                        this._previousTrack = null;
                    }
                }

                if (oldTrack) {
                    this._localStreams.removeTrack(oldTrack);
                }
                this._localStreams.addTrack(newTrack);

                if (cb) {
                    cb(true);
                }
            }).catch((error)=> {
                DEBUG('Error replacing track: %s', error);
            });
    }

    terminate() {
        if (this._terminated) {
            return;
        }
        DEBUG('Terminating call');
        this._sendTerminate();
    }

    sendDtmf(tones, duration=100, interToneGap=70) {
        DEBUG('sendDtmf()');
        if (this._dtmfSender === null) {
            if (this._pc !== null) {
                let track = null;
                try {
                    track = this._pc.getLocalStreams()[0].getAudioTracks()[0];
                } catch (e) {
                    // ignore
                }
                if (track !== null) {
                    DEBUG('Creating DTMF sender');
                     if (this._pc.getSenders) {
                         this._dtmfSender = this._pc.getSenders()[0].dtmf;
                     } else {
                         DEBUG("Your browser doesn't support RTCPeerConnection.getSenders(), so " +
                             "falling back to use deprecated createDTMFSender() " +
                             "instead.");
                        this._dtmfSender = this._pc.createDTMFSender(track);
                     }
                    if (this._dtmfSender) {
                        this._dtmfSender.addEventListener('tonechange', this._onDtmf);
                    }
                }
            }
        }
        if (this._dtmfSender) {
            DEBUG('Sending DTMF tones');
            this._dtmfSender.insertDTMF(tones, duration, interToneGap);
        }
    }

    // Private API

    _initOutgoing(uri, options={}) {
        if (uri.indexOf('@') === -1) {
            throw new Error('Invalid URI');
        }

        if (!options.localStream) {
            throw new Error('Missing localStream');
        }

        this._id = options.id || uuidv4();
        this._direction = 'outgoing';
        this._remoteIdentity = new utils.Identity(uri);

        const pcConfig = options.pcConfig || {iceServers:[]};
        const offerOptions = options.offerOptions;

        // Create the RTCPeerConnection
        this._initRTCPeerConnection(pcConfig);

        this._pc.addStream(options.localStream);
        this.emit('localStreamAdded', options.localStream);
        utils.createLocalSdp(this._pc, 'offer', offerOptions)
            .then((sdp) => {
                DEBUG('Local SDP: %s', sdp);
                this._sendCall(uri, sdp);
            })
            .catch((reason) => {
                DEBUG(reason);
                this._localTerminate(reason);
            });
    }

    _initIncoming(id, caller, sdp, callId) {
        this._id = id;
        this._remoteIdentity = new utils.Identity(caller.uri, caller.display_name);
        this._incomingSdp = sdp;
        this._direction = 'incoming';
        this._state = 'incoming';
        this._callId = callId;
        this._remoteMediaDirections = Object.assign(
            {audio: [], video:[]}, utils.getMediaDirections(sdp)
        );
        DEBUG('Remote SDP: %s', sdp);
    }

    _handleEvent(message) {
        DEBUG('Call event: %o', message);
        switch (message.event) {
            case 'state':
                let oldState = this._state;
                let newState = message.state;
                this._state = newState;

                if ((newState === 'accepted' || newState === 'early-media') && this._direction === 'outgoing') {
                    DEBUG('Call accepted or early media');
                    this.emit('stateChanged', oldState, newState, {});
                    if (message.sdp !== undefined) {
                        const sdp = utils.mungeSdp(message.sdp);
                        DEBUG('Remote SDP: %s', sdp);
                        this._remoteMediaDirections = Object.assign(
                            {audio: [], video:[]}, utils.getMediaDirections(sdp)
                        );
                        this._setup_in_progress = true;
                        this._callId = message.call_id;
                        this._pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}))
                            // success
                            .then(() => {
                                this._setup_in_progress = false;
                                if (!this._terminated) {
                                    if (this._delay_established) {
                                        oldState = this._state;
                                        this._state = 'established';
                                        DEBUG('Setting delayed established state!');
                                        this.emit('stateChanged', oldState, this._state, {});
                                        this._delay_established = false;
                                    }
                                }
                            })
                            // failure
                            .catch((error) => {
                                DEBUG('Error accepting call or early media: %s', error);
                                this.terminate();
                            });
                    }
                } else if (newState === 'established' && this._direction === 'outgoing') {
                    if (this._setup_in_progress) {
                        this._delay_established = true;
                    } else {
                        this.emit('stateChanged', oldState, newState, {});
                    }
                } else if (newState === 'terminated') {
                    this.emit('stateChanged', oldState, newState, {reason: message.reason});
                    this._terminated = true;
                    this._account._calls.delete(this.id);
                    this._closeRTCPeerConnection();
                } else {
                    this.emit('stateChanged', oldState, newState, {});
                }
                break;
            default:
                break;
        }
    }

    _initRTCPeerConnection(pcConfig) {
        if (this._pc !== null) {
            throw new Error('RTCPeerConnection already initialized');
        }

        this._pc = new RTCPeerConnection(pcConfig);
        this._pc.addEventListener('addstream', (event) => {
            DEBUG('Stream added');
            this.emit('streamAdded', event.stream);
        });
        this._pc.addEventListener('icecandidate', (event) => {
            if (event.candidate !== null) {
                DEBUG('New ICE candidate %o', event.candidate);
            } else {
                DEBUG('ICE candidate gathering finished');
            }
            this._sendTrickle(event.candidate);
        });
    }

    _sendRequest(req, cb) {
        this._account._sendRequest(req, cb);
    }

    _sendCall(uri, sdp) {
        const req = {
            sylkrtc: 'session-create',
            account: this.account.id,
            session: this.id,
            uri: uri,
            sdp: sdp
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Call error: %s', error);
                this._localTerminate(error);
            }
        });
    }

    _sendTerminate() {
        const req = {
            sylkrtc: 'session-terminate',
            session: this.id
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Error terminating call: %s', error);
                this._localTerminate(error);
            }
        });
        setTimeout(() => {
            if (!this._terminated) {
                DEBUG('Timeout terminating call');
                this._localTerminate('200 OK');
            }
            this._terminated = true;
        }, 150);
    }

    _sendTrickle(candidate) {
        const req = {
            sylkrtc: 'session-trickle',
            session: this.id,
            candidates: candidate !== null ? [candidate] : [],
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Trickle error: %s', error);
                this._localTerminate(error);
            }
        });
    }

    _sendAnswer(sdp) {
        const req = {
            sylkrtc: 'session-answer',
            session: this.id,
            sdp: sdp
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Answer error: %s', error);
                this.terminate();
            }
        });
    }

    _closeRTCPeerConnection() {
        DEBUG('Closing RTCPeerConnection');
        if (this._pc !== null) {
            let tempStream;
            if (this._pc.getSenders) {
                let tracks = [];
                for (let track of this._pc.getSenders()) {
                    if (track.track != null ) {
                        tracks = tracks.concat(track.track);
                    }
                    if (this._previousTrack !== null) {
                        tracks = tracks.concat(this._previousTrack);
                    }
                }
                if (tracks.length !== 0) {
                    tempStream = new MediaStream(tracks);
                    utils.closeMediaStream(tempStream);
                }
            } else {
                for (let stream of this._pc.getLocalStreams()) {
                    if (this._previousTrack !== null) {
                        stream = stream.concat(this._previousTrack);
                    }
                    utils.closeMediaStream(stream);
                }
            }

            if (this._pc.getReceivers) {
                let tracks = [];
                for (let track of this._pc.getReceivers()) {
                    tracks = tracks.concat(track.track);
                }
                tempStream = new MediaStream(tracks);
                utils.closeMediaStream(tempStream);
            } else {
                for (let stream of this._pc.getRemoteStreams()) {
                    utils.closeMediaStream(stream);
                }
            }
            this._pc.close();
            this._pc = null;
            if (this._dtmfSender !== null) {
                this._dtmfSender.removeEventListener('tonechange', this._onDtmf);
                this._dtmfSender = null;
            }
        }
    }

    _localTerminate(error) {
        if (this._terminated) {
            return;
        }
        DEBUG('Local terminate');
        this._account._calls.delete(this.id);
        this._terminated = true;
        const oldState = this._state;
        const newState = 'terminated';
        const data = {
            reason: error.toString()
        };
        this._closeRTCPeerConnection();
        this.emit('stateChanged', oldState, newState, data);
    }

    _onDtmf(event) {
        DEBUG('Sent DTMF tone %s', event.tone);
        this.emit('dtmfToneSent', event.tone);
    }
}

export { Call };
<commit_msg>Added statistics gathering to calls<commit_after>'use strict';

import debug from 'debug';
import { v4 as uuidv4 } from 'uuid';
import utils from './utils';

import { EventEmitter } from 'events';

import { Statistics } from './statistics';

const DEBUG = debug('sylkrtc:Call');

class Call extends EventEmitter {
    constructor(account) {
        super();
        this._account = account;
        this._id = null;
        this._callId = null;
        this._direction = null;
        this._pc = null;
        this._state = null;
        this._terminated = false;
        this._incomingSdp = null;
        this._remoteMediaDirections = {};
        this._localIdentity = new utils.Identity(account.id, account.displayName);
        this._remoteIdentity = null;
        this._remoteStreams = new MediaStream();
        this._localStreams = new MediaStream();
        this._previousTrack = null;
        this._sharingScreen = false;
        this._dtmfSender = null;
        this._delay_established = false;  // set to true when we need to delay posting the state change to 'established'
        this._setup_in_progress = false;  // set while we set the remote description and setup the peer copnnection
	this._statistics = new Statistics();

        // bind some handlers to this instance
        this._onDtmf = this._onDtmf.bind(this);
    }

    get account() {
        return this._account;
    }

    get id() {
        return this._id;
    }

    get callId() {
        return this._callId;
    }

    get statistics() {
        return this._statistics;
    }

    get sharingScreen() {
        return this._sharingScreen;
    }

    get direction() {
        return this._direction;
    }

    get state() {
        return this._state;
    }

    get localIdentity() {
        return this._localIdentity;
    }

    get remoteIdentity() {
        return this._remoteIdentity;
    }

    get remoteMediaDirections() {
        return this._remoteMediaDirections;
    }

    getLocalStreams() {
        if (this._pc !== null) {
            if (this._pc.getSenders) {
                this._pc.getSenders().forEach((e) => {
                    if (e.track != null) {
                        if (e.track.readyState !== "ended") {
                            this._localStreams.addTrack(e.track);
                        } else {
                            this._localStreams.removeTrack(e.track);
                        }
                    }
                });
                return [this._localStreams];
            } else {
                return this._pc.getLocalStreams();
            }
        } else {
            return [];
        }
    }

    getRemoteStreams() {
        if (this._pc !== null) {
           if (this._pc.getReceivers) {
              this._pc.getReceivers().forEach((e) => {
                  if (e.track.readyState !== "ended") {
                    this._remoteStreams.addTrack(e.track);
                  }
              });
              return [this._remoteStreams];
          } else {
                return this._pc.getRemoteStreams();
          }
        } else {
            return [];
        }
    }

    getSenders() {
        if (this._pc !== null) {
           return this._pc.getSenders();
        } else {
            return [];
        }
    }

    getReceivers() {
        if (this._pc !== null) {
           return this._pc.getReceivers();
        } else {
            return [];
        }
    }

    answer(options = {}) {
        if (this._state !== 'incoming') {
            throw new Error('Call is not in the incoming state: ' + this._state);
        }

        if (!options.localStream) {
            throw new Error('Missing localStream');
        }

        const pcConfig = options.pcConfig || {iceServers:[]};
        const answerOptions = options.answerOptions;

        // Create the RTCPeerConnection
        this._initRTCPeerConnection(pcConfig);

        this._pc.addStream(options.localStream);
        this.emit('localStreamAdded', options.localStream);
        this._pc.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: this._incomingSdp}))
            // success
            .then(() => {
                utils.createLocalSdp(this._pc, 'answer', answerOptions)
                    .then((sdp) => {
                        DEBUG('Local SDP: %s', sdp);
                        this._sendAnswer(sdp);
                    })
                    .catch((reason) => {
                        DEBUG(reason);
                        this.terminate();
                    });
            })
            // failure
            .catch((error) => {
                DEBUG('Error setting remote description: %s', error);
                this.terminate();
            });
    }

    startScreensharing(newTrack) {
        let oldTrack = this.getLocalStreams()[0].getVideoTracks()[0];
        this.replaceTrack(oldTrack, newTrack, true, (value) => {
            this._sharingScreen = value;
        });
    }

    stopScreensharing() {
        let oldTrack = this.getLocalStreams()[0].getVideoTracks()[0];
        this.replaceTrack(oldTrack, this._previousTrack);
        this._sharingScreen = false;
    }

    replaceTrack(oldTrack, newTrack, keep=false, cb=null) {
        let sender;
        for (sender of this._pc.getSenders()) {
            if (sender.track === oldTrack) {
                break;
            }
        }

        sender.replaceTrack(newTrack)
            .then(() => {
                if (keep) {
                    this._previousTrack = oldTrack;
                } else {
                    if (oldTrack) {
                        oldTrack.stop();
                    }
                    if (newTrack === this._previousTrack) {
                        this._previousTrack = null;
                    }
                }

                if (oldTrack) {
                    this._localStreams.removeTrack(oldTrack);
                }
                this._localStreams.addTrack(newTrack);

                if (cb) {
                    cb(true);
                }
            }).catch((error)=> {
                DEBUG('Error replacing track: %s', error);
            });
    }

    terminate() {
        if (this._terminated) {
            return;
        }
        DEBUG('Terminating call');
        this._sendTerminate();
    }

    sendDtmf(tones, duration=100, interToneGap=70) {
        DEBUG('sendDtmf()');
        if (this._dtmfSender === null) {
            if (this._pc !== null) {
                let track = null;
                try {
                    track = this._pc.getLocalStreams()[0].getAudioTracks()[0];
                } catch (e) {
                    // ignore
                }
                if (track !== null) {
                    DEBUG('Creating DTMF sender');
                     if (this._pc.getSenders) {
                         this._dtmfSender = this._pc.getSenders()[0].dtmf;
                     } else {
                         DEBUG("Your browser doesn't support RTCPeerConnection.getSenders(), so " +
                             "falling back to use deprecated createDTMFSender() " +
                             "instead.");
                        this._dtmfSender = this._pc.createDTMFSender(track);
                     }
                    if (this._dtmfSender) {
                        this._dtmfSender.addEventListener('tonechange', this._onDtmf);
                    }
                }
            }
        }
        if (this._dtmfSender) {
            DEBUG('Sending DTMF tones');
            this._dtmfSender.insertDTMF(tones, duration, interToneGap);
        }
    }

    // Private API

    _initOutgoing(uri, options={}) {
        if (uri.indexOf('@') === -1) {
            throw new Error('Invalid URI');
        }

        if (!options.localStream) {
            throw new Error('Missing localStream');
        }

        this._id = options.id || uuidv4();
        this._direction = 'outgoing';
        this._remoteIdentity = new utils.Identity(uri);

        const pcConfig = options.pcConfig || {iceServers:[]};
        const offerOptions = options.offerOptions;

        // Create the RTCPeerConnection
        this._initRTCPeerConnection(pcConfig);

        this._pc.addStream(options.localStream);
        this.emit('localStreamAdded', options.localStream);
        utils.createLocalSdp(this._pc, 'offer', offerOptions)
            .then((sdp) => {
                DEBUG('Local SDP: %s', sdp);
                this._sendCall(uri, sdp);
            })
            .catch((reason) => {
                DEBUG(reason);
                this._localTerminate(reason);
            });
    }

    _initIncoming(id, caller, sdp, callId) {
        this._id = id;
        this._remoteIdentity = new utils.Identity(caller.uri, caller.display_name);
        this._incomingSdp = sdp;
        this._direction = 'incoming';
        this._state = 'incoming';
        this._callId = callId;
        this._remoteMediaDirections = Object.assign(
            {audio: [], video:[]}, utils.getMediaDirections(sdp)
        );
        DEBUG('Remote SDP: %s', sdp);
    }

    _handleEvent(message) {
        DEBUG('Call event: %o', message);
        switch (message.event) {
            case 'state':
                let oldState = this._state;
                let newState = message.state;
                this._state = newState;

                if ((newState === 'accepted' || newState === 'early-media') && this._direction === 'outgoing') {
                    DEBUG('Call accepted or early media');
                    this.emit('stateChanged', oldState, newState, {});
                    if (message.sdp !== undefined) {
                        const sdp = utils.mungeSdp(message.sdp);
                        DEBUG('Remote SDP: %s', sdp);
                        this._remoteMediaDirections = Object.assign(
                            {audio: [], video:[]}, utils.getMediaDirections(sdp)
                        );
                        this._setup_in_progress = true;
                        this._callId = message.call_id;
                        this._pc.setRemoteDescription(new RTCSessionDescription({type: 'answer', sdp: sdp}))
                            // success
                            .then(() => {
                                this._setup_in_progress = false;
                                if (!this._terminated) {
                                    if (this._delay_established) {
                                        oldState = this._state;
                                        this._state = 'established';
                                        DEBUG('Setting delayed established state!');
                                        this.emit('stateChanged', oldState, this._state, {});
                                        this._delay_established = false;
                                    }
                                }
                            })
                            // failure
                            .catch((error) => {
                                DEBUG('Error accepting call or early media: %s', error);
                                this.terminate();
                            });
                    }
                } else if (newState === 'established' && this._direction === 'outgoing') {
                    if (this._setup_in_progress) {
                        this._delay_established = true;
                    } else {
                        this.emit('stateChanged', oldState, newState, {});
                    }
                } else if (newState === 'terminated') {
                    this.emit('stateChanged', oldState, newState, {reason: message.reason});
                    this._terminated = true;
                    this._account._calls.delete(this.id);
                    this._closeRTCPeerConnection();
                } else {
                    this.emit('stateChanged', oldState, newState, {});
                }
                break;
            default:
                break;
        }
    }

    _initRTCPeerConnection(pcConfig) {
        if (this._pc !== null) {
            throw new Error('RTCPeerConnection already initialized');
        }

        this._pc = new RTCPeerConnection(pcConfig);
        this._pc.addEventListener('addstream', (event) => {
            DEBUG('Stream added');
            this.emit('streamAdded', event.stream);
        });
        this._pc.addEventListener('icecandidate', (event) => {
            if (event.candidate !== null) {
                DEBUG('New ICE candidate %o', event.candidate);
            } else {
                DEBUG('ICE candidate gathering finished');
            }
            this._sendTrickle(event.candidate);
        });
        this._statistics.addConnection({pc:this._pc, peerId: this._id});
    }

    _sendRequest(req, cb) {
        this._account._sendRequest(req, cb);
    }

    _sendCall(uri, sdp) {
        const req = {
            sylkrtc: 'session-create',
            account: this.account.id,
            session: this.id,
            uri: uri,
            sdp: sdp
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Call error: %s', error);
                this._localTerminate(error);
            }
        });
    }

    _sendTerminate() {
        const req = {
            sylkrtc: 'session-terminate',
            session: this.id
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Error terminating call: %s', error);
                this._localTerminate(error);
            }
        });
        setTimeout(() => {
            if (!this._terminated) {
                DEBUG('Timeout terminating call');
                this._localTerminate('200 OK');
            }
            this._terminated = true;
        }, 150);
    }

    _sendTrickle(candidate) {
        const req = {
            sylkrtc: 'session-trickle',
            session: this.id,
            candidates: candidate !== null ? [candidate] : [],
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Trickle error: %s', error);
                this._localTerminate(error);
            }
        });
    }

    _sendAnswer(sdp) {
        const req = {
            sylkrtc: 'session-answer',
            session: this.id,
            sdp: sdp
        };
        this._sendRequest(req, (error) => {
            if (error) {
                DEBUG('Answer error: %s', error);
                this.terminate();
            }
        });
    }

    _closeRTCPeerConnection() {
        DEBUG('Closing RTCPeerConnection');
        if (this._pc !== null) {
            let tempStream;
            if (this._pc.getSenders) {
                let tracks = [];
                for (let track of this._pc.getSenders()) {
                    if (track.track != null ) {
                        tracks = tracks.concat(track.track);
                    }
                    if (this._previousTrack !== null) {
                        tracks = tracks.concat(this._previousTrack);
                    }
                }
                if (tracks.length !== 0) {
                    tempStream = new MediaStream(tracks);
                    utils.closeMediaStream(tempStream);
                }
            } else {
                for (let stream of this._pc.getLocalStreams()) {
                    if (this._previousTrack !== null) {
                        stream = stream.concat(this._previousTrack);
                    }
                    utils.closeMediaStream(stream);
                }
            }

            if (this._pc.getReceivers) {
                let tracks = [];
                for (let track of this._pc.getReceivers()) {
                    tracks = tracks.concat(track.track);
                }
                tempStream = new MediaStream(tracks);
                utils.closeMediaStream(tempStream);
            } else {
                for (let stream of this._pc.getRemoteStreams()) {
                    utils.closeMediaStream(stream);
                }
            }
            this._statistics.removeConnection({pc:this._pc, peerId: this._id});
            this._pc.close();
            this._pc = null;
            if (this._dtmfSender !== null) {
                this._dtmfSender.removeEventListener('tonechange', this._onDtmf);
                this._dtmfSender = null;
            }
        }
    }

    _localTerminate(error) {
        if (this._terminated) {
            return;
        }
        DEBUG('Local terminate');
        this._account._calls.delete(this.id);
        this._terminated = true;
        const oldState = this._state;
        const newState = 'terminated';
        const data = {
            reason: error.toString()
        };
        this._closeRTCPeerConnection();
        this.emit('stateChanged', oldState, newState, data);
    }

    _onDtmf(event) {
        DEBUG('Sent DTMF tone %s', event.tone);
        this.emit('dtmfToneSent', event.tone);
    }
}

export { Call };
<|endoftext|>


<commit_before>/* super-search
Author: Kushagra Gour (http://kushagragour.in)
MIT Licensed
*/
(function () {
	var searchFile = '/feed.xml',
		searchEl,
		searchInputEl,
		searchResultsEl,
		currentInputValue = '',
		lastSearchResultHash,
		posts = [];

	// Changes XML to JSON
	// Modified version from here: http://davidwalsh.name/convert-xml-json
	function xmlToJson(xml) {
		// Create the return object
		var obj = {};
		if (xml.nodeType == 3) { // text
			obj = xml.nodeValue;
		}

		// do children
		// If all text nodes inside, get concatenated text from them.
		var textNodes = [].slice.call(xml.childNodes).filter(function (node) { return node.nodeType === 3; });
		if (xml.hasChildNodes() && xml.childNodes.length === textNodes.length) {
			obj = [].slice.call(xml.childNodes).reduce(function (text, node) { return text + node.nodeValue; }, '');
		}
		else if (xml.hasChildNodes()) {
			for(var i = 0; i < xml.childNodes.length; i++) {
				var item = xml.childNodes.item(i);
				var nodeName = item.nodeName;
				if (typeof(obj[nodeName]) == "undefined") {
					obj[nodeName] = xmlToJson(item);
				} else {
					if (typeof(obj[nodeName].push) == "undefined") {
						var old = obj[nodeName];
						obj[nodeName] = [];
						obj[nodeName].push(old);
					}
					obj[nodeName].push(xmlToJson(item));
				}
			}
		}
		return obj;
	}

	function getPostsFromXml(xml) {
		var json = xmlToJson(xml);
		if(Object.prototype.toString.call(json.entry) !== '[object Array]'){
			return [json.entry];
		}
		return json.entry;
	}

	window.toggleSearch = function toggleSearch() {
		searchEl.classList.toggle('is-active');
		if (searchEl.classList.contains('is-active')) {
			// while opening
			searchInputEl.value = '';
		} else {
			// while closing
			searchResultsEl.classList.add('is-hidden');
		}
		setTimeout(function () {
			searchInputEl.focus();
		}, 210);
	}

	function handleInput() {
		var currentResultHash, d;

		currentInputValue = (searchInputEl.value + '').toLowerCase();
		if (!currentInputValue || currentInputValue.length < 3) {
			lastSearchResultHash = '';
			searchResultsEl.classList.add('is-hidden');
			return;
		}
		searchResultsEl.style.offsetWidth;

		var matchingPosts = posts.filter(function (post) {
			if ((post.title + '').toLowerCase().indexOf(currentInputValue) !== -1 || (post.content + '').toLowerCase().indexOf(currentInputValue) !== -1) {
				return true;
			}
		});
		if (!matchingPosts.length) {
			searchResultsEl.classList.add('is-hidden');
		}
		currentResultHash = matchingPosts.reduce(function(hash, post) { return post.title + hash; }, '');
		if (matchingPosts.length && currentResultHash !== lastSearchResultHash) {
			searchResultsEl.classList.remove('is-hidden');
			searchResultsEl.innerHTML = matchingPosts.map(function (post) {
				d = new Date(post.published);
				return '
' + post.title + '» ' + d.toUTCString().replace(/.*(\d{2})\s+(\w{3})\s+(\d{4}).*/,'$2 $1, $3') + '
';
			}).join('');
		}
		lastSearchResultHash = currentResultHash;
	}

	function init(options) {
		searchFile = options.searchFile || searchFile;
		searchEl = document.querySelector(options.searchSelector || '#js-super-search');
		searchInputEl = document.querySelector(options.inputSelector || '#js-super-search__input');
		searchResultsEl = document.querySelector(options.resultsSelector || '#js-super-search__results');

		var xmlhttp = new XMLHttpRequest();
		xmlhttp.open('GET', searchFile);
		xmlhttp.onreadystatechange = function () {
			if (xmlhttp.readyState != 4) return;
			if (xmlhttp.status != 200 && xmlhttp.status != 304) { return; }
			var node = (new DOMParser).parseFromString(xmlhttp.responseText, 'text/xml');
			node = node.children[0];
			posts = getPostsFromXml(node);
		}
		xmlhttp.send();

		// Toggle on ESC key
		window.addEventListener('keyup', function onKeyPress(e) {
			if (e.which === 27) {
				toggleSearch();
			}
		});
		// Open on '/' key
		window.addEventListener('keypress', function onKeyPress(e) {
			if (e.which === 47 && !searchEl.classList.contains('is-active')) {
				toggleSearch();
			}
		});

		searchInputEl.addEventListener('input', function onInputChange() {
			handleInput();
		});
	}

	init.toggle = toggleSearch;

	window.superSearch = init;

})();
<commit_msg>don't open search on '/' key<commit_after>/* super-search
Author: Kushagra Gour (http://kushagragour.in)
MIT Licensed
*/
(function () {
	var searchFile = '/feed.xml',
		searchEl,
		searchInputEl,
		searchResultsEl,
		currentInputValue = '',
		lastSearchResultHash,
		posts = [];

	// Changes XML to JSON
	// Modified version from here: http://davidwalsh.name/convert-xml-json
	function xmlToJson(xml) {
		// Create the return object
		var obj = {};
		if (xml.nodeType == 3) { // text
			obj = xml.nodeValue;
		}

		// do children
		// If all text nodes inside, get concatenated text from them.
		var textNodes = [].slice.call(xml.childNodes).filter(function (node) { return node.nodeType === 3; });
		if (xml.hasChildNodes() && xml.childNodes.length === textNodes.length) {
			obj = [].slice.call(xml.childNodes).reduce(function (text, node) { return text + node.nodeValue; }, '');
		}
		else if (xml.hasChildNodes()) {
			for(var i = 0; i < xml.childNodes.length; i++) {
				var item = xml.childNodes.item(i);
				var nodeName = item.nodeName;
				if (typeof(obj[nodeName]) == "undefined") {
					obj[nodeName] = xmlToJson(item);
				} else {
					if (typeof(obj[nodeName].push) == "undefined") {
						var old = obj[nodeName];
						obj[nodeName] = [];
						obj[nodeName].push(old);
					}
					obj[nodeName].push(xmlToJson(item));
				}
			}
		}
		return obj;
	}

	function getPostsFromXml(xml) {
		var json = xmlToJson(xml);
		if(Object.prototype.toString.call(json.entry) !== '[object Array]'){
			return [json.entry];
		}
		return json.entry;
	}

	window.toggleSearch = function toggleSearch() {
		searchEl.classList.toggle('is-active');
		if (searchEl.classList.contains('is-active')) {
			// while opening
			searchInputEl.value = '';
		} else {
			// while closing
			searchResultsEl.classList.add('is-hidden');
		}
		setTimeout(function () {
			searchInputEl.focus();
		}, 210);
	}

	function handleInput() {
		var currentResultHash, d;

		currentInputValue = (searchInputEl.value + '').toLowerCase();
		if (!currentInputValue || currentInputValue.length < 3) {
			lastSearchResultHash = '';
			searchResultsEl.classList.add('is-hidden');
			return;
		}
		searchResultsEl.style.offsetWidth;

		var matchingPosts = posts.filter(function (post) {
			if ((post.title + '').toLowerCase().indexOf(currentInputValue) !== -1 || (post.content + '').toLowerCase().indexOf(currentInputValue) !== -1) {
				return true;
			}
		});
		if (!matchingPosts.length) {
			searchResultsEl.classList.add('is-hidden');
		}
		currentResultHash = matchingPosts.reduce(function(hash, post) { return post.title + hash; }, '');
		if (matchingPosts.length && currentResultHash !== lastSearchResultHash) {
			searchResultsEl.classList.remove('is-hidden');
			searchResultsEl.innerHTML = matchingPosts.map(function (post) {
				d = new Date(post.published);
				return '
' + post.title + '» ' + d.toUTCString().replace(/.*(\d{2})\s+(\w{3})\s+(\d{4}).*/,'$2 $1, $3') + '
';
			}).join('');
		}
		lastSearchResultHash = currentResultHash;
	}

	function init(options) {
		searchFile = options.searchFile || searchFile;
		searchEl = document.querySelector(options.searchSelector || '#js-super-search');
		searchInputEl = document.querySelector(options.inputSelector || '#js-super-search__input');
		searchResultsEl = document.querySelector(options.resultsSelector || '#js-super-search__results');

		var xmlhttp = new XMLHttpRequest();
		xmlhttp.open('GET', searchFile);
		xmlhttp.onreadystatechange = function () {
			if (xmlhttp.readyState != 4) return;
			if (xmlhttp.status != 200 && xmlhttp.status != 304) { return; }
			var node = (new DOMParser).parseFromString(xmlhttp.responseText, 'text/xml');
			node = node.children[0];
			posts = getPostsFromXml(node);
		}
		xmlhttp.send();

		// Toggle on ESC key
		window.addEventListener('keyup', function onKeyPress(e) {
			if (e.which === 27) {
				toggleSearch();
			}
		});

		searchInputEl.addEventListener('input', function onInputChange() {
			handleInput();
		});
	}

	init.toggle = toggleSearch;

	window.superSearch = init;

})();
<|endoftext|>