<commit_before>(function(){
  var count = 0;

  function init(){
    $("[data-id]").each(function(){
      var that = $(this);
      that.find(".workflowimg-container").hide();
      initProjectExperiment(that);
    });
  }

  function initProjectExperiment(element){
    var container = element.find(".workflowimg-container");
    var url = container.data("check-img");
    var timestamp = container.data("timestamp");
    var img_url = container.data('updated-img');

    animateSpinner(container, true);
    checkUpdatedImg(img_url, url, timestamp, container);
  }

  // checks if the experiment image is updated
  function checkUpdatedImg(img_url, url, timestamp, container){
    if (count < 30 && timestamp){
      $.ajax({
        url: url,
        type: "GET",
        data: { "timestamp": timestamp },
        dataType: "json",
        success: function (data) {
          getNewWorkforwImg(container, img_url);
          container.show();
          animateSpinner(container, false);
        },
        error: function (ev) {
          if (ev.status == 404) {
            setTimeout(checkUpdatedImg(img_url, url, timestamp, container), 500);
          } else {
            animateSpinner(container, false);
          }
          count++;
          }
      });
    } else {
      animateSpinner(container, false);
    }
  }

  // fetch the new experiment image
  function getNewWorkforwImg(el, url){
    $.ajax({
      url: url,
      type: "GET",
      dataType: "json",
      success: function (data) {
        el.append(data.workflowimg);
      },
      error: function (ev) {
        // TODO
      }
    });
  }
  // init
  init();
})();

/* Initialize the first-time demo tutorial if needed. */
(function(){
  function initializeTutorial() {
    if (showTutorial()) {
      introJs()
        .setOptions({
          overlayOpacity: '0.2',
          hidePrev: true,
          nextLabel: 'Next',
          doneLabel: 'End tutorial',
          skipLabel: 'End tutorial',
          showBullets: false,
          showStepNumbers: false,
          exitOnOverlayClick: false,
          exitOnEsc: false,
          tooltipClass: 'custom next-page-link',
          disableInteraction: true
        })
        .onafterchange(function (tarEl) {
          Cookies.set('current_tutorial_step', this._currentStep + 2);

          if (this._currentStep == 1) {
            setTimeout(function() {
              $('.next-page-link a.introjs-nextbutton')
                .removeClass('introjs-disabled')
                .attr('href', $('[data-canvas-link]').data('canvasLink'));
              $('.introjs-disableInteraction').remove();
            }, 500);
          } else {

          }
        })
        .start();

      window.onresize = function() {
        if (Cookies.get('current_tutorial_step') == 4 ) {
          $(".introjs-tooltip").css("right", ($(".new-element.initial").width() + 60)  + "px");
        }
      };

      // Destroy first-time tutorial cookies when skip tutorial
      // or end tutorial is clicked
      $(".introjs-skipbutton").each(function (){
        $(this).click(function (){
          Cookies.remove('tutorial_data');
          Cookies.remove('current_tutorial_step');
          restore_after_tutorial();
        });
      });
    }
  }

  function showTutorial() {
    var tutorialData;

    if (Cookies.get('tutorial_data'))
      tutorialData = JSON.parse(Cookies.get('tutorial_data'));
    else
      return false;
    var currentStep = Cookies.get('current_tutorial_step');
    if (currentStep < 3 || currentStep > 5)
      return false;
    var tutorialProjectId = tutorialData[0].project;
    var currentProjectId = $("#data-holder").attr("data-project-id");

    return tutorialProjectId == currentProjectId;
  }

  function project_tutorial_helper(){
    $(document).ready(function(){
      if( $('div').hasClass('introjs-overlay')){
        $.each( $(".panel-title"), function(){
          $(this).css({ 'pointer-events': 'none' });
        });
        $.each( $(".dropdown-experiment-actions").find("li"),
          function(){
            $(this).css({ 'pointer-events': 'none' });
        });
      }
    });
  }

  function restore_after_tutorial(){
    $.each( $(".panel-title"), function(){
      $(this).css({ 'pointer-events': 'auto' });
    });
    $.each( $(".dropdown-experiment-actions").find("li"),
      function(){
        $(this).css({ 'pointer-events': 'auto' });
    });
  }

  $(document).ready(function(){
    initializeTutorial();
    project_tutorial_helper();
  });

})();
<commit_msg>fixed workflowimg link<commit_after>(function(){
  var count = 0;

  function init(){
    $("[data-id]").each(function(){
      var that = $(this);
      that.find(".workflowimg-container").hide();
      initProjectExperiment(that);
    });
  }

  function initProjectExperiment(element){
    var container = element.find(".workflowimg-container");
    var url = container.data("check-img");
    var timestamp = container.data("timestamp");
    var img_url = container.data('updated-img');

    animateSpinner(container, true);
    checkUpdatedImg(img_url, url, timestamp, container);
  }

  // checks if the experiment image is updated
  function checkUpdatedImg(img_url, url, timestamp, container){
    if (count < 30 && timestamp){
      $.ajax({
        url: url,
        type: "GET",
        data: { "timestamp": timestamp },
        dataType: "json",
        success: function (data) {
          getNewWorkforwImg(container, img_url);
          container.show();
          animateSpinner(container, false);
        },
        error: function (ev) {
          if (ev.status == 404) {
            setTimeout(checkUpdatedImg(img_url, url, timestamp, container), 500);
          } else {
            animateSpinner(container, false);
          }
          count++;
          }
      });
    } else {
      animateSpinner(container, false);
    }
  }

  // fetch the new experiment image
  function getNewWorkforwImg(el, url){
    $.ajax({
      url: url,
      type: "GET",
      dataType: "json",
      success: function (data) {
        el.append(data.workflowimg);
      },
      error: function (ev) {
        // TODO
      }
    });
  }
  // init
  init();
})();

/* Initialize the first-time demo tutorial if needed. */
(function(){
  function initializeTutorial() {
    if (showTutorial()) {
      introJs()
        .setOptions({
          overlayOpacity: '0.2',
          hidePrev: true,
          nextLabel: 'Next',
          doneLabel: 'End tutorial',
          skipLabel: 'End tutorial',
          showBullets: false,
          showStepNumbers: false,
          exitOnOverlayClick: false,
          exitOnEsc: false,
          tooltipClass: 'custom next-page-link',
          disableInteraction: true
        })
        .onafterchange(function (tarEl) {
          Cookies.set('current_tutorial_step', this._currentStep + 2);

          if (this._currentStep == 1) {
            setTimeout(function() {
              $('.next-page-link a.introjs-nextbutton')
                .removeClass('introjs-disabled')
                .attr('href', $('[data-canvas-link]').data('canvasLink'));
              $('.introjs-disableInteraction').remove();
            }, 500);
          } else {

          }
        })
        .start();

      window.onresize = function() {
        if (Cookies.get('current_tutorial_step') == 4 ) {
          $(".introjs-tooltip").css("right", ($(".new-element.initial").width() + 60)  + "px");
        }
      };

      // Destroy first-time tutorial cookies when skip tutorial
      // or end tutorial is clicked
      $(".introjs-skipbutton").each(function (){
        $(this).click(function (){
          Cookies.remove('tutorial_data');
          Cookies.remove('current_tutorial_step');
          restore_after_tutorial();
        });
      });
    }
  }

  function showTutorial() {
    var tutorialData;

    if (Cookies.get('tutorial_data'))
      tutorialData = JSON.parse(Cookies.get('tutorial_data'));
    else
      return false;
    var currentStep = Cookies.get('current_tutorial_step');
    if (currentStep < 3 || currentStep > 5)
      return false;
    var tutorialProjectId = tutorialData[0].project;
    var currentProjectId = $("#data-holder").attr("data-project-id");

    return tutorialProjectId == currentProjectId;
  }

  function project_tutorial_helper(){
    $(document).ready(function(){
      if( $('div').hasClass('introjs-overlay')){
        $.each( $(".panel-title"), function(){
          $(this).css({ "pointer-events": "none" });
        });
        $.each( $(".workflowimg-container"), function(){
          $(this).css({ "pointer-events": "none" });
        });
        $.each( $(".dropdown-experiment-actions").find("li"),
          function(){
            $(this).css({ "pointer-events": "none" });
        });
      }
    });
  }

  function restore_after_tutorial(){
    $.each( $(".panel-title"), function(){
      $(this).css({ "pointer-events": "auto" });
    });
    $.each( $(".workflowimg-container"), function(){
      $(this).css({ "pointer-events": "auto" });
    });
    $.each( $(".dropdown-experiment-actions").find("li"),
      function(){
        $(this).css({ "pointer-events": "auto" });
    });
  }

  $(document).ready(function(){
    initializeTutorial();
    project_tutorial_helper();
  });

})();
<|endoftext|>


<commit_before>"use strict";
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var modal_1 = require('./modal');
var Server = (function () {
    function Server(kind) {
        this.kind = kind;
        this.params = { kind: kind };
    }
    Server.is_inprogress = function (state) {
        return !(state === 'running' || state === 'stopped');
    };
    Server.prototype.msgs = function () { return t('js.server_status.' + this.kind); };
    Server.prototype.start = function () { Server.ajax.start(this.params); };
    Server.prototype.stop = function () { Server.ajax.stop(this.params); };
    Server.prototype.status = function (background) {
        var p = _.merge({ background: background }, this.params);
        return Server.ajax.status(p);
    };
    Server.prototype.watch = function (callback) {
        var ws = ws_connector('server_status', this.kind);
        ws.onmessage = function (msg) {
            callback(msg.data);
        };
    };
    Server.ajax = new AjaxSet.Resources('server_status');
    return Server;
}());
Server.ajax.add_member('start', 'POST', 'kind');
Server.ajax.add_member('stop', 'POST', 'kind');
Server.ajax.add_member('status', 'POST', 'kind');
var App = (function (_super) {
    __extends(App, _super);
    function App(model, el) {
        _super.call(this);
        this.model = model;
        this._init({
            el: el,
            data: { state: this.state },
            template: App.TEMPLATE_ID,
            methods: {
                start: function () {
                  var _this = this;
                  modal_1.Confirm(this.model.msgs().title, this.model.msgs().confirm_start).done(function () {
                    _this.model.start();
                  })
                },
                stop: function () {
                    var _this = this;
                    modal_1.Confirm(this.model.msgs().title, this.model.msgs().confirm_stop).done(function () {
                      _this.model.stop();
                    });
                },
                status: function (background) {
                  var _this = this;
                  this.model.status(background).done(function (state) {
                    _this.state = state;
                    if (background || _this.is_inprogress) {
                      _this.model.watch(function (w_state) {
                        _this.state = w_state;
                      });
                    }
                  });
                },
                toggle: function () {
                  if (this.running) {
                    this.stop();
                  }
                  else if (this.stopped) {
                    this.start();
                  }
                },
            },
            computed: {
                running: function () { return this.state === 'running'; },
                stopped: function () { return this.state === 'stopped'; },
                is_inprogress: function () { return Server.is_inprogress(this.state); },
                status_text: function () {
                  var res = _.capitalize(this.model.kind) + ' Server ';
                  if (this.running) {
                    res += 'is ' + this.state + '.';
                  }
                  else if (this.stopped) {
                    res += 'was ' + this.state + '.';
                  }
                  else if (this.is_inprogress) {
                    res += this.state + '...';
                  }
                  return res;
                },
                btn_class: function () {
                  if (this.running) {
                    return 'btn-success';
                  }
                  else if (this.stopped) {
                    return 'btn-default';
                  }
                  else if (this.is_inprogress) {
                    return 'btn-warning';
                  }
                },
                tooltip: function () {
                  if (this.is_inprogress) {
                    return "";
                  }
                  return this.model.msgs().tooltip;
                },
            },
            created: function () {
              console.log(this);
              this.status(true);
            },
        });
    }
    App.TEMPLATE_ID = '#toggle-button-template';
    return App;
}(Vue));
function Build(kind) {
    var el = document.createElement('div');
    var parent = document.querySelector(App.TEMPLATE_ID).parentElement;
    parent.appendChild(el);
    var vm = new App(new Server(kind), el);
}
function Available() {
    return !!document.querySelector(App.TEMPLATE_ID);
}
function Do() {
    if (!Available()) {
        return;
    }
    Build('zabbix');
    Build('chef');
}
exports.Do = Do;
<commit_msg>Fix: フッターのボタンの隙間がなくなっていたのを修正<commit_after>"use strict";
var __extends = (this && this.__extends) || function (d, b) {
    for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
    function __() { this.constructor = d; }
    d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
var modal_1 = require('./modal');
var Server = (function () {
    function Server(kind) {
        this.kind = kind;
        this.params = { kind: kind };
    }
    Server.is_inprogress = function (state) {
        return !(state === 'running' || state === 'stopped');
    };
    Server.prototype.msgs = function () { return t('js.server_status.' + this.kind); };
    Server.prototype.start = function () { Server.ajax.start(this.params); };
    Server.prototype.stop = function () { Server.ajax.stop(this.params); };
    Server.prototype.status = function (background) {
        var p = _.merge({ background: background }, this.params);
        return Server.ajax.status(p);
    };
    Server.prototype.watch = function (callback) {
        var ws = ws_connector('server_status', this.kind);
        ws.onmessage = function (msg) {
            callback(msg.data);
        };
    };
    Server.ajax = new AjaxSet.Resources('server_status');
    return Server;
}());
Server.ajax.add_member('start', 'POST', 'kind');
Server.ajax.add_member('stop', 'POST', 'kind');
Server.ajax.add_member('status', 'POST', 'kind');
var App = (function (_super) {
    __extends(App, _super);
    function App(model, el) {
        _super.call(this);
        this.model = model;
        this._init({
            el: el,
            data: { state: this.state },
            template: App.TEMPLATE_ID,
            methods: {
                start: function () {
                  var _this = this;
                  modal_1.Confirm(this.model.msgs().title, this.model.msgs().confirm_start).done(function () {
                    _this.model.start();
                  })
                },
                stop: function () {
                    var _this = this;
                    modal_1.Confirm(this.model.msgs().title, this.model.msgs().confirm_stop).done(function () {
                      _this.model.stop();
                    });
                },
                status: function (background) {
                  var _this = this;
                  this.model.status(background).done(function (state) {
                    _this.state = state;
                    if (background || _this.is_inprogress) {
                      _this.model.watch(function (w_state) {
                        _this.state = w_state;
                      });
                    }
                  });
                },
                toggle: function () {
                  if (this.running) {
                    this.stop();
                  }
                  else if (this.stopped) {
                    this.start();
                  }
                },
            },
            computed: {
                running: function () { return this.state === 'running'; },
                stopped: function () { return this.state === 'stopped'; },
                is_inprogress: function () { return Server.is_inprogress(this.state); },
                status_text: function () {
                  var res = _.capitalize(this.model.kind) + ' Server ';
                  if (this.running) {
                    res += 'is ' + this.state + '.';
                  }
                  else if (this.stopped) {
                    res += 'was ' + this.state + '.';
                  }
                  else if (this.is_inprogress) {
                    res += this.state + '...';
                  }
                  return res;
                },
                btn_class: function () {
                  if (this.running) {
                    return 'btn-success';
                  }
                  else if (this.stopped) {
                    return 'btn-default';
                  }
                  else if (this.is_inprogress) {
                    return 'btn-warning';
                  }
                },
                tooltip: function () {
                  if (this.is_inprogress) {
                    return "";
                  }
                  return this.model.msgs().tooltip;
                },
            },
            created: function () {
              console.log(this);
              this.status(true);
            },
        });
    }
    App.TEMPLATE_ID = '#toggle-button-template';
    return App;
}(Vue));
function Build(kind) {
    var el = document.createElement('div');
    var parent = document.querySelector(App.TEMPLATE_ID).parentElement;
    parent.appendChild(el);
    parent.appendChild(document.createTextNode('\n'));
    var vm = new App(new Server(kind), el);
}
function Available() {
    return !!document.querySelector(App.TEMPLATE_ID);
}
function Do() {
    if (!Available()) {
        return;
    }
    Build('zabbix');
    Build('chef');
}
exports.Do = Do;
<|endoftext|>


<commit_before>/**
 * Enables automatic observability of plain javascript object for ES5 compatible browsers. Also, converts promise properties into observables that are updated when the promise resolves.
 * @module observable
 * @requires system
 * @requires binder
 * @requires knockout
 */
define(['durandal/system', 'durandal/binder', 'knockout'], function(system, binder, ko) {
    var observableModule,
        toString = Object.prototype.toString,
        nonObservableTypes = ['[object Function]', '[object String]', '[object Boolean]', '[object Number]', '[object Date]', '[object RegExp]'],
        observableArrayMethods = ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'],
        arrayMethods = ['pop', 'reverse', 'sort', 'shift', 'slice'],
        additiveArrayFunctions = ['push', 'unshift'],
        es5Functions = ['filter', 'map', 'reduce', 'reduceRight', 'forEach', 'every', 'some'],
        arrayProto = Array.prototype,
        observableArrayFunctions = ko.observableArray.fn,
        logConversion = false,
        changeDetectionMethod = undefined,
        skipPromises = false,
        shouldIgnorePropertyName;

    /**
     * You can call observable(obj, propertyName) to get the observable function for the specified property on the object.
     * @class ObservableModule
     */

    if (!('getPropertyDescriptor' in Object)) {
        var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
        var getPrototypeOf = Object.getPrototypeOf;

        Object['getPropertyDescriptor'] = function(o, name) {
            var proto = o, descriptor;

            while(proto && !(descriptor = getOwnPropertyDescriptor(proto, name))) {
                proto = getPrototypeOf(proto);
            }

            return descriptor;
        };
    }

    function defaultShouldIgnorePropertyName(propertyName){
        var first = propertyName[0];
        return first === '_' || first === '$' || (changeDetectionMethod && propertyName === changeDetectionMethod);
    }

    function isNode(obj) {
        return !!(obj && obj.nodeType !== undefined && system.isNumber(obj.nodeType));
    }

    function canConvertType(value) {
        if (!value || isNode(value) || value.ko === ko || value.jquery) {
            return false;
        }

        var type = toString.call(value);

        return nonObservableTypes.indexOf(type) == -1 && !(value === true || value === false);
    }

    function createLookup(obj) {
        var value = {};

        Object.defineProperty(obj, "__observable__", {
            enumerable: false,
            configurable: false,
            writable: false,
            value: value
        });

        return value;
    }

    function makeObservableArray(original, observable, hasChanged) {
        var lookup = original.__observable__, notify = true;

        if(lookup && lookup.__full__){
            return;
        }

        lookup = lookup || createLookup(original);
        lookup.__full__ = true;

        es5Functions.forEach(function (methodName) {
            observable[methodName] = function () {
                return arrayProto[methodName].apply(original, arguments);
            };
        });

        observableArrayMethods.forEach(function(methodName) {
            original[methodName] = function() {
                notify = false;
                var methodCallResult = observableArrayFunctions[methodName].apply(observable, arguments);
                notify = true;
                return methodCallResult;
            };
        });

        arrayMethods.forEach(function(methodName) {
            original[methodName] = function() {
                if(notify){
                    observable.valueWillMutate();
                }

                var methodCallResult = arrayProto[methodName].apply(original, arguments);

                if(notify){
                    observable.valueHasMutated();
                }

                return methodCallResult;
            };
        });

        additiveArrayFunctions.forEach(function(methodName){
            original[methodName] = function() {
                for (var i = 0, len = arguments.length; i < len; i++) {
                    convertObject(arguments[i], hasChanged);
                }

                if(notify){
                    observable.valueWillMutate();
                }

                var methodCallResult = arrayProto[methodName].apply(original, arguments);

                if(notify){
                    observable.valueHasMutated();
                }

                return methodCallResult;
            };
        });

        original['splice'] = function() {
            for (var i = 2, len = arguments.length; i < len; i++) {
                convertObject(arguments[i], hasChanged);
            }

            if(notify){
                observable.valueWillMutate();
            }

            var methodCallResult = arrayProto['splice'].apply(original, arguments);

            if(notify){
                observable.valueHasMutated();
            }

            return methodCallResult;
        };

        for (var i = 0, len = original.length; i < len; i++) {
            convertObject(original[i], hasChanged);
        }
    }

    /**
     * Converts an entire object into an observable object by re-writing its attributes using ES5 getters and setters. Attributes beginning with '_' or '$' are ignored.
     * @method convertObject
     * @param {object} obj The target object to convert.
     */
    function convertObject(obj, hasChanged) {
        var lookup, value;

        if (changeDetectionMethod) {
            if(obj && obj[changeDetectionMethod]) {
                if (hasChanged) {
                    hasChanged = hasChanged.slice(0);
                } else {
                    hasChanged = [];
                }
                hasChanged.push(obj[changeDetectionMethod]);
            }
        }

        if(!canConvertType(obj)){
            return;
        }

        lookup = obj.__observable__;

        if(lookup && lookup.__full__){
            return;
        }

        lookup = lookup || createLookup(obj);
        lookup.__full__ = true;

        if (system.isArray(obj)) {
            var observable = ko.observableArray(obj);
            makeObservableArray(obj, observable, hasChanged);
        } else {
            for (var propertyName in obj) {
                if(shouldIgnorePropertyName(propertyName)){
                    continue;
                }

                if (!lookup[propertyName]) {
                    var descriptor = Object.getPropertyDescriptor(obj, propertyName);
                    if (descriptor && (descriptor.get || descriptor.set)) {
                        defineProperty(obj, propertyName, {
                            get:descriptor.get,
                            set:descriptor.set
                        });
                    } else {
                        value = obj[propertyName];

                        if(!system.isFunction(value)) {
                            convertProperty(obj, propertyName, value, hasChanged);
                        }
                    }
                }
            }
        }

        if(logConversion) {
            system.log('Converted', obj);
        }
    }

    function innerSetter(observable, newValue, isArray) {
        //if this was originally an observableArray, then always check to see if we need to add/replace the array methods (if newValue was an entirely new array)
        if (isArray) {
            if (!newValue) {
                //don't allow null, force to an empty array
                newValue = [];
                makeObservableArray(newValue, observable);
            }
            else if (!newValue.destroyAll) {
                makeObservableArray(newValue, observable);
            }
        } else {
            convertObject(newValue);
        }

        //call the update to the observable after the array as been updated.
        observable(newValue);
    }

    /**
     * Converts a normal property into an observable property using ES5 getters and setters.
     * @method convertProperty
     * @param {object} obj The target object on which the property to convert lives.
     * @param {string} propertyName The name of the property to convert.
     * @param {object} [original] The original value of the property. If not specified, it will be retrieved from the object.
     * @return {KnockoutObservable} The underlying observable.
     */
    function convertProperty(obj, propertyName, original, hasChanged) {
        var observable,
            isArray,
            lookup = obj.__observable__ || createLookup(obj);

        if(original === undefined){
            original = obj[propertyName];
        }

        if (system.isArray(original)) {
            observable = ko.observableArray(original);
            makeObservableArray(original, observable, hasChanged);
            isArray = true;
        } else if (typeof original == "function") {
            if(ko.isObservable(original)){
                observable = original;
            }else{
                return null;
            }
        } else if(!skipPromises && system.isPromise(original)) {
            observable = ko.observable();

            original.then(function (result) {
                if(system.isArray(result)) {
                    var oa = ko.observableArray(result);
                    makeObservableArray(result, oa, hasChanged);
                    result = oa;
                }

                observable(result);
            });
        } else {
            observable = ko.observable(original);
            convertObject(original, hasChanged);
        }

        if (hasChanged && hasChanged.length > 0) {
            hasChanged.forEach(function (func) {
                if (system.isArray(original)) {
                    observable.subscribe(function (arrayChanges) {
                        func(obj, propertyName, null, arrayChanges);
                    }, null, "arrayChange");
                } else {
                    observable.subscribe(function (newValue) {
                        func(obj, propertyName, newValue, null);
                    });
                }
            });
        }

        Object.defineProperty(obj, propertyName, {
            configurable: true,
            enumerable: true,
            get: observable,
            set: ko.isWriteableObservable(observable) ? (function (newValue) {
                if (newValue && system.isPromise(newValue) && !skipPromises) {
                    newValue.then(function (result) {
                        innerSetter(observable, result, system.isArray(result));
                    });
                } else {
                    innerSetter(observable, newValue, isArray);
                }
            }) : undefined
        });

        lookup[propertyName] = observable;
        return observable;
    }

    /**
     * Defines a computed property using ES5 getters and setters.
     * @method defineProperty
     * @param {object} obj The target object on which to create the property.
     * @param {string} propertyName The name of the property to define.
     * @param {function|object} evaluatorOrOptions The Knockout computed function or computed options object.
     * @return {KnockoutObservable} The underlying computed observable.
     */
    function defineProperty(obj, propertyName, evaluatorOrOptions) {
        var computedOptions = { owner: obj, deferEvaluation: true },
            computed;

        if (typeof evaluatorOrOptions === 'function') {
            computedOptions.read = evaluatorOrOptions;
        } else {
            if ('value' in evaluatorOrOptions) {
                system.error('For defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');
            }

            if (typeof evaluatorOrOptions.get !== 'function' && typeof evaluatorOrOptions.read !== 'function') {
                system.error('For defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');
            }

            computedOptions.read = evaluatorOrOptions.get || evaluatorOrOptions.read;
            computedOptions.write = evaluatorOrOptions.set || evaluatorOrOptions.write;
        }

        computed = ko.computed(computedOptions);
        obj[propertyName] = computed;

        return convertProperty(obj, propertyName, computed);
    }

    observableModule = function(obj, propertyName){
        var lookup, observable, value;

        if (!obj) {
            return null;
        }

        lookup = obj.__observable__;
        if(lookup){
            observable = lookup[propertyName];
            if(observable){
                return observable;
            }
        }

        value = obj[propertyName];

        if(ko.isObservable(value)){
            return value;
        }

        hasChanged = [];
        if (changeDetectionMethod) {
            if(obj && obj[changeDetectionMethod]) {
                hasChanged.push(obj[changeDetectionMethod]);
            }
        }

        return convertProperty(obj, propertyName, value, hasChanged);
    };

    observableModule.defineProperty = defineProperty;
    observableModule.convertProperty = convertProperty;
    observableModule.convertObject = convertObject;

    /**
     * Installs the plugin into the view model binder's `beforeBind` hook so that objects are automatically converted before being bound.
     * @method install
     */
    observableModule.install = function(options) {
        var original = binder.binding;

        binder.binding = function(obj, view, instruction) {
            if(instruction.applyBindings && !instruction.skipConversion){
                convertObject(obj);
            }

            original(obj, view);
        };

        logConversion = options.logConversion;
        if (options.changeDetection) {
            changeDetectionMethod = options.changeDetection;
        }

        skipPromises = options.skipPromises;
        shouldIgnorePropertyName = options.shouldIgnorePropertyName || defaultShouldIgnorePropertyName;
    };

    return observableModule;
});<commit_msg>fix(observable): Fixes issue 642<commit_after>/**
 * Enables automatic observability of plain javascript object for ES5 compatible browsers. Also, converts promise properties into observables that are updated when the promise resolves.
 * @module observable
 * @requires system
 * @requires binder
 * @requires knockout
 */
define(['durandal/system', 'durandal/binder', 'knockout'], function(system, binder, ko) {
    var observableModule,
        toString = Object.prototype.toString,
        nonObservableTypes = ['[object Function]', '[object String]', '[object Boolean]', '[object Number]', '[object Date]', '[object RegExp]'],
        observableArrayMethods = ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'],
        arrayMethods = ['pop', 'reverse', 'sort', 'shift', 'slice'],
        additiveArrayFunctions = ['push', 'unshift'],
        es5Functions = ['filter', 'map', 'reduce', 'reduceRight', 'forEach', 'every', 'some'],
        arrayProto = Array.prototype,
        observableArrayFunctions = ko.observableArray.fn,
        logConversion = false,
        changeDetectionMethod = undefined,
        skipPromises = false,
        shouldIgnorePropertyName;

    /**
     * You can call observable(obj, propertyName) to get the observable function for the specified property on the object.
     * @class ObservableModule
     */

    if (!('getPropertyDescriptor' in Object)) {
        var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
        var getPrototypeOf = Object.getPrototypeOf;

        Object['getPropertyDescriptor'] = function(o, name) {
            var proto = o, descriptor;

            while(proto && !(descriptor = getOwnPropertyDescriptor(proto, name))) {
                proto = getPrototypeOf(proto);
            }

            return descriptor;
        };
    }

    function defaultShouldIgnorePropertyName(propertyName){
        var first = propertyName[0];
        return first === '_' || first === '$' || (changeDetectionMethod && propertyName === changeDetectionMethod);
    }

    function isNode(obj) {
        return !!(obj && obj.nodeType !== undefined && system.isNumber(obj.nodeType));
    }

    function canConvertType(value) {
        if (!value || isNode(value) || value.ko === ko || value.jquery) {
            return false;
        }

        var type = toString.call(value);

        return nonObservableTypes.indexOf(type) == -1 && !(value === true || value === false);
    }

    function createLookup(obj) {
        var value = {};

        Object.defineProperty(obj, "__observable__", {
            enumerable: false,
            configurable: false,
            writable: false,
            value: value
        });

        return value;
    }

    function makeObservableArray(original, observable, hasChanged) {
        var lookup = original.__observable__, notify = true;

        if(lookup && lookup.__full__){
            return;
        }

        lookup = lookup || createLookup(original);
        lookup.__full__ = true;

        es5Functions.forEach(function (methodName) {
            observable[methodName] = function () {
                return arrayProto[methodName].apply(original, arguments);
            };
        });

        observableArrayMethods.forEach(function(methodName) {
            original[methodName] = function() {
                notify = false;
                var methodCallResult = observableArrayFunctions[methodName].apply(observable, arguments);
                notify = true;
                return methodCallResult;
            };
        });

        arrayMethods.forEach(function(methodName) {
            original[methodName] = function() {
                if(notify){
                    observable.valueWillMutate();
                }

                var methodCallResult = arrayProto[methodName].apply(original, arguments);

                if(notify){
                    observable.valueHasMutated();
                }

                return methodCallResult;
            };
        });

        additiveArrayFunctions.forEach(function(methodName){
            original[methodName] = function() {
                for (var i = 0, len = arguments.length; i < len; i++) {
                    convertObject(arguments[i], hasChanged);
                }

                if(notify){
                    observable.valueWillMutate();
                }

                var methodCallResult = arrayProto[methodName].apply(original, arguments);

                if(notify){
                    observable.valueHasMutated();
                }

                return methodCallResult;
            };
        });

        original['splice'] = function() {
            for (var i = 2, len = arguments.length; i < len; i++) {
                convertObject(arguments[i], hasChanged);
            }

            if(notify){
                observable.valueWillMutate();
            }

            var methodCallResult = arrayProto['splice'].apply(original, arguments);

            if(notify){
                observable.valueHasMutated();
            }

            return methodCallResult;
        };

        for (var i = 0, len = original.length; i < len; i++) {
            convertObject(original[i], hasChanged);
        }
    }

    /**
     * Converts an entire object into an observable object by re-writing its attributes using ES5 getters and setters. Attributes beginning with '_' or '$' are ignored.
     * @method convertObject
     * @param {object} obj The target object to convert.
     */
    function convertObject(obj, hasChanged) {
        var lookup, value;

        if (changeDetectionMethod) {
            if(obj && obj[changeDetectionMethod]) {
                if (hasChanged) {
                    hasChanged = hasChanged.slice(0);
                } else {
                    hasChanged = [];
                }
                hasChanged.push(obj[changeDetectionMethod]);
            }
        }

        if(!canConvertType(obj)){
            return;
        }

        lookup = obj.__observable__;

        if(lookup && lookup.__full__){
            return;
        }

        lookup = lookup || createLookup(obj);
        lookup.__full__ = true;

        if (system.isArray(obj)) {
            var observable = ko.observableArray(obj);
            makeObservableArray(obj, observable, hasChanged);
        } else {
            for (var propertyName in obj) {
                if(shouldIgnorePropertyName(propertyName)){
                    continue;
                }

                if (!lookup[propertyName]) {
                    var descriptor = Object.getPropertyDescriptor(obj, propertyName);
                    if (descriptor && (descriptor.get || descriptor.set)) {
                        defineProperty(obj, propertyName, {
                            get:descriptor.get,
                            set:descriptor.set
                        });
                    } else {
                        value = obj[propertyName];

                        if(!system.isFunction(value)) {
                            convertProperty(obj, propertyName, value, hasChanged);
                        }
                    }
                }
            }
        }

        if(logConversion) {
            system.log('Converted', obj);
        }
    }

    function innerSetter(observable, newValue, isArray) {
        //if this was originally an observableArray, then always check to see if we need to add/replace the array methods (if newValue was an entirely new array)
        if (isArray) {
            if (!newValue) {
                //don't allow null, force to an empty array
                newValue = [];
                makeObservableArray(newValue, observable);
            }
            else if (!newValue.destroyAll) {
                makeObservableArray(newValue, observable);
            }
        } else {
            convertObject(newValue);
        }

        //call the update to the observable after the array as been updated.
        observable(newValue);
    }

    /**
     * Converts a normal property into an observable property using ES5 getters and setters.
     * @method convertProperty
     * @param {object} obj The target object on which the property to convert lives.
     * @param {string} propertyName The name of the property to convert.
     * @param {object} [original] The original value of the property. If not specified, it will be retrieved from the object.
     * @return {KnockoutObservable} The underlying observable.
     */
    function convertProperty(obj, propertyName, original, hasChanged) {
        var observable,
            isArray,
            lookup = obj.__observable__ || createLookup(obj);

        if(original === undefined){
            original = obj[propertyName];
        }

        if (system.isArray(original)) {
            observable = ko.observableArray(original);
            makeObservableArray(original, observable, hasChanged);
            isArray = true;
        } else if (typeof original == "function") {
            if(ko.isObservable(original)){
                observable = original;
            }else{
                return null;
            }
        } else if(!skipPromises && system.isPromise(original)) {
            observable = ko.observable();

            original.then(function (result) {
                if(system.isArray(result)) {
                    var oa = ko.observableArray(result);
                    makeObservableArray(result, oa, hasChanged);
                    result = oa;
                }

                observable(result);
            });
        } else {
            observable = ko.observable(original);
            convertObject(original, hasChanged);
        }

        if (hasChanged && hasChanged.length > 0) {
            hasChanged.forEach(function (func) {
                if (system.isArray(original)) {
                    observable.subscribe(function (arrayChanges) {
                        func(obj, propertyName, null, arrayChanges);
                    }, null, "arrayChange");
                } else {
                    observable.subscribe(function (newValue) {
                        func(obj, propertyName, newValue, null);
                    });
                }
            });
        }

        Object.defineProperty(obj, propertyName, {
            configurable: true,
            enumerable: true,
            get: observable,
            set: ko.isWriteableObservable(observable) ? (function (newValue) {
                if (newValue && system.isPromise(newValue) && !skipPromises) {
                    newValue.then(function (result) {
                        innerSetter(observable, result, system.isArray(result));
                    });
                } else {
                    innerSetter(observable, newValue, isArray);
                }
            }) : undefined
        });

        lookup[propertyName] = observable;
        return observable;
    }

    /**
     * Defines a computed property using ES5 getters and setters.
     * @method defineProperty
     * @param {object} obj The target object on which to create the property.
     * @param {string} propertyName The name of the property to define.
     * @param {function|object} evaluatorOrOptions The Knockout computed function or computed options object.
     * @return {KnockoutObservable} The underlying computed observable.
     */
    function defineProperty(obj, propertyName, evaluatorOrOptions) {
        var computedOptions = { owner: obj, deferEvaluation: true },
            computed;

        if (typeof evaluatorOrOptions === 'function') {
            computedOptions.read = evaluatorOrOptions;
        } else {
            if ('value' in evaluatorOrOptions) {
                system.error('For defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');
            }

            if (typeof evaluatorOrOptions.get !== 'function' && typeof evaluatorOrOptions.read !== 'function') {
                system.error('For defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');
            }

            computedOptions.read = evaluatorOrOptions.get || evaluatorOrOptions.read;
            computedOptions.write = evaluatorOrOptions.set || evaluatorOrOptions.write;
        }

        computed = ko.computed(computedOptions);
        obj[propertyName] = computed;

        return convertProperty(obj, propertyName, computed);
    }

    observableModule = function(obj, propertyName){
        var lookup, observable, value;

        if (!obj) {
            return null;
        }

        lookup = obj.__observable__;
        if(lookup){
            observable = lookup[propertyName];
            if(observable){
                return observable;
            }
        }

        value = obj[propertyName];

        if(ko.isObservable(value)){
            return value;
        }

        var hasChanged = [];
        if (changeDetectionMethod) {
            if(obj && obj[changeDetectionMethod]) {
                hasChanged.push(obj[changeDetectionMethod]);
            }
        }

        return convertProperty(obj, propertyName, value, hasChanged);
    };

    observableModule.defineProperty = defineProperty;
    observableModule.convertProperty = convertProperty;
    observableModule.convertObject = convertObject;

    /**
     * Installs the plugin into the view model binder's `beforeBind` hook so that objects are automatically converted before being bound.
     * @method install
     */
    observableModule.install = function(options) {
        var original = binder.binding;

        binder.binding = function(obj, view, instruction) {
            if(instruction.applyBindings && !instruction.skipConversion){
                convertObject(obj);
            }

            original(obj, view);
        };

        logConversion = options.logConversion;
        if (options.changeDetection) {
            changeDetectionMethod = options.changeDetection;
        }

        skipPromises = options.skipPromises;
        shouldIgnorePropertyName = options.shouldIgnorePropertyName || defaultShouldIgnorePropertyName;
    };

    return observableModule;
});<|endoftext|>


<commit_before>const setupOnce = require('setup_once');

(function(){

  let SharesEditor = Backbone.View.extend({

    events: {
      'click .shares-editor__delete-variant': 'deleteVariant',
      'click .shares-editor__toggle-edit': 'toggleEditor',
      'click .shares-editor__new-type-toggle .btn': 'switchVariantForm',
      'ajax:success form.shares-editor__new-form': 'clearForm',
    },

    initialize: function(){
      $.subscribe('page:errors', this.openEditorForErrors());
      $.subscribe('page:saved', this.updateSummaryRows());

      // this is the kind of DOM hosuekeeping that makes me want to use react
      $.subscribe('image:success', this.addImageSelectors());
      $.subscribe('image:destroyed', this.pruneImageSelectors());
    },

    deleteVariant: function(){
      console.log("TOGGLED DELETE")
    },

    toggleEditor: function(e) {
      let $target = this.$(e.target);
      $target = $target.is('tr') ? $target : $target.parents('tr');
      let $btn = $target.find('.shares-editor__toggle-edit');
      $target.next('.shares-editor__edit-row').toggleClass('hidden-closed');
      $btn.text( $btn.text() == "Edit" ? "Done" : "Edit" );
    },

    openEditor: function($edit_row){
      let $prev = $edit_row.prev('.shares-editor__summary-row');
      let $btn = $prev.find('.shares-editor__toggle-edit');
      $btn.text( "Done" );
      $edit_row.removeClass('hidden-closed');
    },

    switchVariantForm: function(e){
      let $target = this.$(e.target)
      const desired = $target.data('state');
      if (desired) {
        this.$('.shares-editor__new-type-toggle .btn').removeClass('btn-primary');
        $target.addClass('btn-primary');
        this.$('.shares-editor__new-form').addClass('hidden-closed');
        this.$(`.shares-editor__new-form[data-share="${desired}"]`).removeClass('hidden-closed');
      }
    },

    clearForm: function(e){
      $(e.target).find('input[type="text"], textarea').val('')
    },

    openEditorForErrors: function(){
      return () => { // closure for this in callback
        this.openEditor(this.$('.has-error').parents('.shares-editor__edit-row'));
      }
    },

    updateSummaryRows: function(){
      return (e, data) => { // closure for `this` in callback
        $.get(`/api/pages/${data.id}/share-rows`, (rows) => {
          _.each(rows, (row) => {
            let $row = $(row.html);
            $row = $(`#${$row.prop('id')}`).replaceWith($row);
            $row = $(`#${$row.prop('id')}`);
            if (!$row.next('.shares-editor__edit-row').hasClass('hidden-closed')) {
              $row.find('.shares-editor__toggle-edit').text('Done');
            }
          })
        });
      }
    },

    addImageSelectors: function(){
      return (e, file, id, html) => { // closure for `this` in callback
        let newOption = `<option value='${id}'>${file.name}</option>`;
        this.$('.shares-editor__image-selector').append(newOption);
      }
    },

    pruneImageSelectors: function(){
      return (e, id) => { // closure for `this` in callback
        this.$(`option[value="${id}"]`).remove()
      }
    },

  });

  $.subscribe("shares:edit", function(){
    setupOnce('.shares-editor', SharesEditor);
  });
}());
<commit_msg>Add basic handling for deleting buttons<commit_after>const setupOnce = require('setup_once');

(function(){

  let SharesEditor = Backbone.View.extend({

    events: {
      'click .shares-editor__delete-variant': 'deleteVariant',
      'click .shares-editor__toggle-edit': 'toggleEditor',
      'click .shares-editor__new-type-toggle .btn': 'switchVariantForm',
      'ajax:success form.shares-editor__new-form': 'clearForm',
    },

    initialize: function(){
      $.subscribe('page:errors', this.openEditorForErrors());
      $.subscribe('page:saved', this.updateSummaryRows());

      // this is the kind of DOM hosuekeeping that makes me want to use react
      $.subscribe('image:success', this.addImageSelectors());
      $.subscribe('image:destroyed', this.pruneImageSelectors());
    },

    deleteVariant: function(e){
        let $delete_button = this.$(e.target);
        let $summary_row = $delete_button.parents('.shares-editor__summary-row');
        let share_id = $summary_row.attr('id');
        // TODO: parse the row ID for variant type and ID: e.g. share_summary_facebook_12
        $.post(`/api/pages/1/delete-share`, {}, (whatever) => {
            console.log(whatever)
        })
    },

    toggleEditor: function(e) {
      let $target = this.$(e.target);
      $target = $target.is('tr') ? $target : $target.parents('tr');
      let $btn = $target.find('.shares-editor__toggle-edit');
      $target.next('.shares-editor__edit-row').toggleClass('hidden-closed');
      $btn.text( $btn.text() == "Edit" ? "Done" : "Edit" );
    },

    openEditor: function($edit_row){
      let $prev = $edit_row.prev('.shares-editor__summary-row');
      let $btn = $prev.find('.shares-editor__toggle-edit');
      $btn.text( "Done" );
      $edit_row.removeClass('hidden-closed');
    },

    switchVariantForm: function(e){
      let $target = this.$(e.target)
      const desired = $target.data('state');
      if (desired) {
        this.$('.shares-editor__new-type-toggle .btn').removeClass('btn-primary');
        $target.addClass('btn-primary');
        this.$('.shares-editor__new-form').addClass('hidden-closed');
        this.$(`.shares-editor__new-form[data-share="${desired}"]`).removeClass('hidden-closed');
      }
    },

    clearForm: function(e){
      $(e.target).find('input[type="text"], textarea').val('')
    },

    openEditorForErrors: function(){
      return () => { // closure for this in callback
        this.openEditor(this.$('.has-error').parents('.shares-editor__edit-row'));
      }
    },

    updateSummaryRows: function(){
      return (e, data) => { // closure for `this` in callback
        $.get(`/api/pages/${data.id}/share-rows`, (rows) => {
          _.each(rows, (row) => {
            let $row = $(row.html);
            $row = $(`#${$row.prop('id')}`).replaceWith($row);
            $row = $(`#${$row.prop('id')}`);
            if (!$row.next('.shares-editor__edit-row').hasClass('hidden-closed')) {
              $row.find('.shares-editor__toggle-edit').text('Done');
            }
          })
        });
      }
    },

    addImageSelectors: function(){
      return (e, file, id, html) => { // closure for `this` in callback
        let newOption = `<option value='${id}'>${file.name}</option>`;
        this.$('.shares-editor__image-selector').append(newOption);
      }
    },

    pruneImageSelectors: function(){
      return (e, id) => { // closure for `this` in callback
        this.$(`option[value="${id}"]`).remove()
      }
    },

  });

  $.subscribe("shares:edit", function(){
    setupOnce('.shares-editor', SharesEditor);
  });
}());
<|endoftext|>


<commit_before>'use strict';

var logger = require('../thalilogger')('thaliManager');

var thaliConfig = require('./thaliConfig');
var ThaliMobile = require('./thaliMobile');
var ThaliSendNotificationBasedOnReplication =
  require('./replication/thaliSendNotificationBasedOnReplication');
var ThaliPullReplicationFromNotification =
  require('./replication/thaliPullReplicationFromNotification');

var express = require('express');
var salti = require('salti');
var path = require('path');
var Promise = require('lie');
var assert = require('assert');

/**
 * @classdesc This may look like a class but it really should only have one
 * instance or bad stuff can happen.
 * @public
 * @param {expressPouchdb} expressPouchDB The express-pouchdb object we are
 * supposed to use to create the router.
 * @param {PouchDB} PouchDB PouchDB object we are supposed to use to create
 * dbs. Typically this should have PouchDB.defaults set with db to
 * require('leveldown-mobile') and prefix to the path where the application
 * wishes to store the DB.
 * @param {string} dbName Name of the db, both locally and remotely that we are
 * interacting with.
 * @param {Crypto.ECDH} ecdhForLocalDevice A Crypto.ECDH object initialized with
 * the local device's public and private keys.
 * @param {module:thaliPeerPoolInterface~ThaliPeerPoolInterface} [thaliPeerPoolInterface]
 * If your app doesn't specify its own pool interface you will seriously
 * regret it as the default one has awful behavior. Building your own
 * thaliPeerPoolInterface is pretty much a requirement for a decent Thali app.
 * @constructor
 */
function ThaliManager(expressPouchDB,
                      PouchDB,
                      dbName,
                      ecdhForLocalDevice,
                      thaliPeerPoolInterface) {
  var self = this;

  this._router = express.Router();
  this._router.all('*', function (req, res, next) {
    logger.debug(
      'connected pskIdentity', req.connection.pskIdentity,
      'for path', req.path
    );

    if (req.connection.authorized) {
      var secret = self._getPskIdToSecret(req.connection.pskIdentity);
      if (secret === thaliConfig.BEACON_KEY) {
        /*
         * When folks want to access the beacon they need to connect via TLS with a
         * predefined PSK. So we need to check for the magic PSK and if used then the
         * caller will only be allowed to make a GET request to the beacon path and
         * nothing else.
         * The current magic PSK identity value is 'beacons' and the secret is a binary
         * array consisting of 16 zero bytes in a row.
         */
        req.connection.pskRole = 'beacon';
      } else if (secret) {
        /*
         * In this role the user connected over TLS using a PSK that we can associate
         * with an identity (this is generated as part of beacon generation). We would
         * then surface the public key for the identity to the router who would check
         * that the path for the request is from our white list. The white list will
         * contain Express-Pouch DB paths and methods that the caller can get to. These
         * will include paths/methods needed for pull replication as well as the ability
         * to get to _Local.
         */
        req.connection.pskRole = 'replication';
      } else {
        // Default role. It is usually unused.
        req.connection.pskRole = 'public';
      }
    } else {
      logger.debug('connected pskIdentity is not authorized');
      return res.status(401).send({
        success: false,
        message: 'Unauthorized'
      });
    }

    next();
  });

  this._router.all('*', salti(
    dbName,
    ThaliManager._acl,
    function (thaliId, req) {
      /*
       * In the case of an id that begins with the string [LOCAL_SEQ_POINT_PREFIX] we MUST enforce
       * that the value of the :id EXACTLY matches the hashed public key of the
       * caller as generated by thaliNotificationBeacons.createPublicKeyHash.
       */
      var publicKey = self._getPskIdToPublicKey(req.connection.pskIdentity);
      assert(Buffer.isBuffer(publicKey), 'publicKey should be a buffer');
      return Buffer.compare(new Buffer(thaliId, 'base64'), publicKey) === 0;
    },
    { dbPrefix: thaliConfig.BASE_DB_PATH }
  ));

  logger.debug('creating ThaliSendNotificationBasedOnReplication instance');

  this._thaliSendNotificationBasedOnReplication =
    new ThaliSendNotificationBasedOnReplication(
        this._router,
        ecdhForLocalDevice,
        thaliConfig.BEACON_MILLISECONDS_TO_EXPIRE,
        new PouchDB(dbName));
  this._getPskIdToSecret =
    self._thaliSendNotificationBasedOnReplication.getPskIdToSecret();
  this._getPskIdToPublicKey =
    self._thaliSendNotificationBasedOnReplication.getPskIdToPublicKey();

  logger.debug('creating ThaliPullReplicationFromNotification instance');

  this._thaliPullReplicationFromNotification =
    new ThaliPullReplicationFromNotification(
        PouchDB,
        dbName,
        thaliPeerPoolInterface,
        ecdhForLocalDevice);

  logger.debug('creating express pouchdb instance');

  this._router.use(thaliConfig.BASE_DB_PATH, expressPouchDB(PouchDB, {
    mode: 'minimumForPouchDB'
  }));

  this.state = ThaliManager.STATES.CREATED;
}

ThaliManager.STATES = {
  CREATED: 'created',
  STARTING: 'starting',
  STARTED: 'started',
  STOPPING: 'stopping',
  STOPPED: 'stopped'
};

/**
 * Starts up everything including listening for advertisements, sending out
 * our advertisements, generating beacons to notify peers that we have data
 * for them as well as running pull replication jobs to pull data from other
 * peers who have data for us.
 *
 * This method is not idempotent as each call to start with different arguments
 * will cause the related states to be changed.
 *
 * @param {Buffer[]} [arrayOfRemoteKeys] This is the list of ECDH public keys
 * that we should be willing to send notifications of our changes to and receive
 * notifications of changes from.
 * @returns {Promise<?Error>}
 */
ThaliManager.prototype.start = function (arrayOfRemoteKeys) {
  var self = this;

  // Can we start now?
  var args = arguments;
  switch (this.state) {
    case ThaliManager.STATES.STARTING: {
      return this.startingPromise;
    }
    case ThaliManager.STATES.STARTED: {
      return Promise.resolve();
    }
    case ThaliManager.STATES.STOPPING: {
      return this.stoppingPromise
        .then(function () {
          return self.start.apply(self, args);
        });
    }
  }
  assert(
    this.state === ThaliManager.STATES.CREATED ||
    this.state === ThaliManager.STATES.STOPPED,
    'ThaliManager state should be \'CREATED\' or \'STOPPED\' for start'
  );
  this.state = ThaliManager.STATES.STARTING;

  logger.debug('starting thaliPullReplicationFromNotification');
  this._thaliPullReplicationFromNotification.start(arrayOfRemoteKeys);

  logger.debug('starting ThaliMobile');
  this.startingPromise = ThaliMobile.start(this._router, this._getPskIdToSecret)

  .then(function () {
    /*
    Ideally we could call startListening and startUpdateAdvertising separately
    but this causes problems in iOS. The issue is that we deal with the
    restriction that there can only be one MCSession between two devices by
    having a leader election where one device will always form the session.
    Imagine that there is device A and B. B is advertising. A hears the
    advertisement and wants to connect but it can't start the session. So what
    it has to do is send an invite to B, who will reject the invite but then
    respond with its own invite to A. For this to work however B has to be
    listening for advertisements (not just making them) because otherwise
    there is no way in iOS for A to ask B for a session (that will be
    refused). Simultaneously A must be listening for advertisements (or it
    wouldn't have heard B) and it must be advertising itself or B couldn't
    establish the MCSession with it. So in practice this means that in iOS
    everyone has to be both listening and advertising at the same time.
    */
    logger.debug('start listening for advertisements');
    return ThaliMobile.startListeningForAdvertisements();
  })
  .then(function () {
    logger.debug('start update advertising and listening');
    return ThaliMobile.startUpdateAdvertisingAndListening();
  })
  .then(function () {
    logger.debug('starting thaliSendNotificationBasedOnReplication');
    self._thaliSendNotificationBasedOnReplication.start(arrayOfRemoteKeys);

    self.state = ThaliManager.STATES.STARTED;
    self.startingPromise = undefined;
  });

  return this.startingPromise;
};

/**
 * Shuts down the radios and deactivates all replications.
 *
 * @returns {Promise<?Error>}
 */
ThaliManager.prototype.stop = function () {
  var self = this;

  // Can we stop now?
  var args = arguments;
  switch (this.state) {
    case ThaliManager.STATES.CREATED:
    case ThaliManager.STATES.STOPPED: {
      return Promise.resolve();
    }
    case ThaliManager.STATES.STOPPING: {
      return this.stoppingPromise;
    }
    case ThaliManager.STATES.STARTING: {
      return this.startingPromise
        .then(function () {
          return self.stop.apply(self, args);
        });
    }
  }
  assert(
    this.state === ThaliManager.STATES.STARTED,
    'ThaliManager state should be \'STARTED\' for stop'
  );
  this.state = ThaliManager.STATES.STOPPING;

  logger.debug('stopping thaliSendNotificationBasedOnReplication');
  this._thaliSendNotificationBasedOnReplication.stop();

  logger.debug('stopping advertising and listening');
  this.stoppingPromise = ThaliMobile.stopAdvertisingAndListening()

  .then(function () {
    logger.debug('stopping listening for advertisements');
    return ThaliMobile.stopListeningForAdvertisements();
  })
  .then(function () {
    logger.debug('stopping ThaliMobile');
    return ThaliMobile.stop();
  })
  .then(function () {
    logger.debug('stopping thaliPullReplicationFromNotification');
    self._thaliPullReplicationFromNotification.stop();

    self.state = ThaliManager.STATES.STOPPED;
    self.stoppingPromise = undefined;
  });

  return this.stoppingPromise;
};

ThaliManager._acl = [
  {
    'role': 'replication',
    'paths': [
      {
        'path': '/',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}/_all_docs',
        'verbs': ['GET', 'HEAD', 'POST']
      },
      {
        'path': '/{:db}/_bulk_docs',
        'verbs': ['GET', 'HEAD', 'POST']
      },
      {
        'path': '/{:db}/_bulk_get',
        'verbs': ['POST']
      },
      {
        'path': '/{:db}/_changes',
        'verbs': ['GET', 'POST']
      },
      {
        'path': '/{:db}/_revs_diff',
        'verbs': ['POST']
      },
      {
        'path': '/{:db}/:id',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}/:id/attachment',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}/_local/{:id}',
        'verbs': ['GET', 'PUT', 'DELETE']
      },
      {
        'path': '/{:db}/_local/' + thaliConfig.LOCAL_SEQ_POINT_PREFIX + '{:id}',
        'verbs': ['GET', 'PUT', 'DELETE']
      }
    ]
  },
  {
    'role': 'beacon',
    'paths': [
      {
        'path': '/NotificationBeacons',
        'verbs': ['GET']
      }
    ]
  }
];

module.exports = ThaliManager;
<commit_msg>added 503 handler<commit_after>'use strict';

var logger = require('../thalilogger')('thaliManager');

var thaliConfig = require('./thaliConfig');
var ThaliMobile = require('./thaliMobile');
var ThaliSendNotificationBasedOnReplication =
  require('./replication/thaliSendNotificationBasedOnReplication');
var ThaliPullReplicationFromNotification =
  require('./replication/thaliPullReplicationFromNotification');

var express = require('express');
var salti = require('salti');
var path = require('path');
var Promise = require('lie');
var assert = require('assert');

/**
 * @classdesc This may look like a class but it really should only have one
 * instance or bad stuff can happen.
 * @public
 * @param {expressPouchdb} expressPouchDB The express-pouchdb object we are
 * supposed to use to create the router.
 * @param {PouchDB} PouchDB PouchDB object we are supposed to use to create
 * dbs. Typically this should have PouchDB.defaults set with db to
 * require('leveldown-mobile') and prefix to the path where the application
 * wishes to store the DB.
 * @param {string} dbName Name of the db, both locally and remotely that we are
 * interacting with.
 * @param {Crypto.ECDH} ecdhForLocalDevice A Crypto.ECDH object initialized with
 * the local device's public and private keys.
 * @param {module:thaliPeerPoolInterface~ThaliPeerPoolInterface} [thaliPeerPoolInterface]
 * If your app doesn't specify its own pool interface you will seriously
 * regret it as the default one has awful behavior. Building your own
 * thaliPeerPoolInterface is pretty much a requirement for a decent Thali app.
 * @constructor
 */
function ThaliManager(expressPouchDB,
                      PouchDB,
                      dbName,
                      ecdhForLocalDevice,
                      thaliPeerPoolInterface) {
  var self = this;

  this._router = express.Router();
  this._router.all('*', this._connectionFilter.bind(this));

  this._router.all('*', salti(
    dbName,
    ThaliManager._acl,
    this._resolveThaliId.bind(this),
    { dbPrefix: thaliConfig.BASE_DB_PATH }
  ));

  logger.debug('creating ThaliSendNotificationBasedOnReplication instance');

  this._thaliSendNotificationBasedOnReplication =
    new ThaliSendNotificationBasedOnReplication(
        this._router,
        ecdhForLocalDevice,
        thaliConfig.BEACON_MILLISECONDS_TO_EXPIRE,
        new PouchDB(dbName));
  this._getPskIdToSecret =
    self._thaliSendNotificationBasedOnReplication.getPskIdToSecret();
  this._getPskIdToPublicKey =
    self._thaliSendNotificationBasedOnReplication.getPskIdToPublicKey();

  logger.debug('creating ThaliPullReplicationFromNotification instance');

  this._thaliPullReplicationFromNotification =
    new ThaliPullReplicationFromNotification(
        PouchDB,
        dbName,
        thaliPeerPoolInterface,
        ecdhForLocalDevice);

  logger.debug('creating express pouchdb instance');

  this._router.use(thaliConfig.BASE_DB_PATH, expressPouchDB(PouchDB, {
    mode: 'minimumForPouchDB'
  }));

  this.state = ThaliManager.STATES.CREATED;
}

ThaliManager.STATES = {
  CREATED: 'created',
  STARTING: 'starting',
  STARTED: 'started',
  STOPPING: 'stopping',
  STOPPED: 'stopped'
};

/**
 * Starts up everything including listening for advertisements, sending out
 * our advertisements, generating beacons to notify peers that we have data
 * for them as well as running pull replication jobs to pull data from other
 * peers who have data for us.
 *
 * This method is not idempotent as each call to start with different arguments
 * will cause the related states to be changed.
 *
 * @param {Buffer[]} [arrayOfRemoteKeys] This is the list of ECDH public keys
 * that we should be willing to send notifications of our changes to and receive
 * notifications of changes from.
 * @returns {Promise<?Error>}
 */
ThaliManager.prototype.start = function (arrayOfRemoteKeys) {
  var self = this;

  // Can we start now?
  var args = arguments;
  switch (this.state) {
    case ThaliManager.STATES.STARTING: {
      return this.startingPromise;
    }
    case ThaliManager.STATES.STARTED: {
      return Promise.resolve();
    }
    case ThaliManager.STATES.STOPPING: {
      return this.stoppingPromise
        .then(function () {
          return self.start.apply(self, args);
        });
    }
  }
  assert(
    this.state === ThaliManager.STATES.CREATED ||
    this.state === ThaliManager.STATES.STOPPED,
    'ThaliManager state should be \'CREATED\' or \'STOPPED\' for start'
  );
  this.state = ThaliManager.STATES.STARTING;

  logger.debug('starting thaliPullReplicationFromNotification');
  this._thaliPullReplicationFromNotification.start(arrayOfRemoteKeys);

  logger.debug('starting ThaliMobile');
  this.startingPromise = ThaliMobile.start(this._router, this._getPskIdToSecret)

  .then(function () {
    /*
    Ideally we could call startListening and startUpdateAdvertising separately
    but this causes problems in iOS. The issue is that we deal with the
    restriction that there can only be one MCSession between two devices by
    having a leader election where one device will always form the session.
    Imagine that there is device A and B. B is advertising. A hears the
    advertisement and wants to connect but it can't start the session. So what
    it has to do is send an invite to B, who will reject the invite but then
    respond with its own invite to A. For this to work however B has to be
    listening for advertisements (not just making them) because otherwise
    there is no way in iOS for A to ask B for a session (that will be
    refused). Simultaneously A must be listening for advertisements (or it
    wouldn't have heard B) and it must be advertising itself or B couldn't
    establish the MCSession with it. So in practice this means that in iOS
    everyone has to be both listening and advertising at the same time.
    */
    logger.debug('start listening for advertisements');
    return ThaliMobile.startListeningForAdvertisements();
  })
  .then(function () {
    logger.debug('start update advertising and listening');
    return ThaliMobile.startUpdateAdvertisingAndListening();
  })
  .then(function () {
    logger.debug('starting thaliSendNotificationBasedOnReplication');
    self._thaliSendNotificationBasedOnReplication.start(arrayOfRemoteKeys);
  })
  .then(function () {
    self.state = ThaliManager.STATES.STARTED;
    self.startingPromise = undefined;
  });

  return this.startingPromise;
};

/**
 * Shuts down the radios and deactivates all replications.
 *
 * @returns {Promise<?Error>}
 */
ThaliManager.prototype.stop = function () {
  var self = this;

  // Can we stop now?
  var args = arguments;
  switch (this.state) {
    case ThaliManager.STATES.CREATED:
    case ThaliManager.STATES.STOPPED: {
      return Promise.resolve();
    }
    case ThaliManager.STATES.STOPPING: {
      return this.stoppingPromise;
    }
    case ThaliManager.STATES.STARTING: {
      return this.startingPromise
        .then(function () {
          return self.stop.apply(self, args);
        });
    }
  }
  assert(
    this.state === ThaliManager.STATES.STARTED,
    'ThaliManager state should be \'STARTED\' for stop'
  );
  this.state = ThaliManager.STATES.STOPPING;

  logger.debug('stopping thaliSendNotificationBasedOnReplication');
  this._thaliSendNotificationBasedOnReplication.stop();

  logger.debug('stopping advertising and listening');
  this.stoppingPromise = ThaliMobile.stopAdvertisingAndListening()

  .then(function () {
    logger.debug('stopping listening for advertisements');
    return ThaliMobile.stopListeningForAdvertisements();
  })
  .then(function () {
    logger.debug('stopping ThaliMobile');
    return ThaliMobile.stop();
  })
  .then(function () {
    logger.debug('stopping thaliPullReplicationFromNotification');
    self._thaliPullReplicationFromNotification.stop();
  })
  .then(function () {
    self.state = ThaliManager.STATES.STOPPED;
    self.stoppingPromise = undefined;
  });

  return this.stoppingPromise;
};

ThaliManager.prototype._connectionFilter = function (req, res, next) {
  logger.debug(
    'connected pskIdentity', req.connection.pskIdentity,
    'for path', req.path
  );

  // We are not able to accept any connection now.
  if (this.state !== ThaliManager.STATES.STARTED) {
    logger.debug('ThaliManager is not ready to accept this connection now');
    return res.status(503).send({
      success: false,
      message: 'Service Unavailable'
    });
  }

  if (req.connection.authorized) {
    var secret = this._getPskIdToSecret(req.connection.pskIdentity);
    if (secret === thaliConfig.BEACON_KEY) {
      /*
        * When folks want to access the beacon they need to connect via TLS with a
        * predefined PSK. So we need to check for the magic PSK and if used then the
        * caller will only be allowed to make a GET request to the beacon path and
        * nothing else.
        * The current magic PSK identity value is 'beacons' and the secret is a binary
        * array consisting of 16 zero bytes in a row.
        */
      req.connection.pskRole = 'beacon';
    } else if (secret) {
      /*
        * In this role the user connected over TLS using a PSK that we can associate
        * with an identity (this is generated as part of beacon generation). We would
        * then surface the public key for the identity to the router who would check
        * that the path for the request is from our white list. The white list will
        * contain Express-Pouch DB paths and methods that the caller can get to. These
        * will include paths/methods needed for pull replication as well as the ability
        * to get to _Local.
        */
      req.connection.pskRole = 'replication';
    } else {
      // Default role. It is usually unused.
      req.connection.pskRole = 'public';
    }
  } else {
    logger.debug('connected pskIdentity is not authorized');
    return res.status(401).send({
      success: false,
      message: 'Unauthorized'
    });
  }

  next();
}

ThaliManager.prototype._resolveThaliId = function (thaliId, req) {
  // In the case of an id that begins with the string [LOCAL_SEQ_POINT_PREFIX] we MUST enforce
  // that the value of the :id EXACTLY matches the hashed public key of the
  // caller as generated by thaliNotificationBeacons.createPublicKeyHash.

  var publicKey = this._getPskIdToPublicKey(req.connection.pskIdentity);
  assert(Buffer.isBuffer(publicKey), 'publicKey should be a buffer');
  return Buffer.compare(new Buffer(thaliId, 'base64'), publicKey) === 0;
}

ThaliManager._acl = [
  {
    'role': 'replication',
    'paths': [
      {
        'path': '/',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}/_all_docs',
        'verbs': ['GET', 'HEAD', 'POST']
      },
      {
        'path': '/{:db}/_bulk_docs',
        'verbs': ['GET', 'HEAD', 'POST']
      },
      {
        'path': '/{:db}/_bulk_get',
        'verbs': ['POST']
      },
      {
        'path': '/{:db}/_changes',
        'verbs': ['GET', 'POST']
      },
      {
        'path': '/{:db}/_revs_diff',
        'verbs': ['POST']
      },
      {
        'path': '/{:db}/:id',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}/:id/attachment',
        'verbs': ['GET']
      },
      {
        'path': '/{:db}/_local/{:id}',
        'verbs': ['GET', 'PUT', 'DELETE']
      },
      {
        'path': '/{:db}/_local/' + thaliConfig.LOCAL_SEQ_POINT_PREFIX + '{:id}',
        'verbs': ['GET', 'PUT', 'DELETE']
      }
    ]
  },
  {
    'role': 'beacon',
    'paths': [
      {
        'path': '/NotificationBeacons',
        'verbs': ['GET']
      }
    ]
  }
];

module.exports = ThaliManager;
<|endoftext|>


<commit_before><commit_msg>loading data icons<commit_after><|endoftext|>


<commit_before>function browserSupportsHtml5HistoryApi() {
  return !! (history && history.replaceState && history.pushState);
}

$(document).ready(function() {
  //_gaq.push(['_trackEvent', 'Citizen-Format-Smartanswer', 'Load']);
  if(browserSupportsHtml5HistoryApi()) {
    var formSelector = ".current form";

    initializeHistory();

      // events
      // get new questions on submit
      $(formSelector).live('submit', function(event) {
        $('input[type=submit]', this).attr('disabled', 'disabled');
        var form = $(this);
        var postData = form.serializeArray();
        reloadQuestions(form.attr('action'), postData);
        event.preventDefault();
        return false;
      });

    // we want to start over with whatever gets provided if someone clicks to change the answer
    $(".undo a").live('click', function() {
      /*
        This is short lived tracking - to help inform a design decision, should not be permanent

        * Cat/Action/Label convention is taken from GOVUK.Analytics.Trackers in static
        * Slug extraction is based on position logic in GOVUK.Analytics.Trackers.smart_answer
      */
      var slug = document.URL.split('/')[3].split("#")[0].split("?")[0];
      window._gaq && window._gaq.push(['_trackEvent', "MS_smart_answer", slug, "Change Answer"]);

      reloadQuestions($(this).attr("href"), "");
      return false;
    });

    // manage next/back by tracking popstate event
    window.onpopstate = function (event) {
      if(event.state !== null) {
        updateContent(event.state['html_fragment']);
      } else {
        return false;
      }
    };
  }

  $('#current-error').focus();

  // helper functions
  function toJsonUrl(url) {
    var parts = url.split('?');
    var json_url = parts[0].replace(/\/$/, "") + ".json";
    if (parts[1]) {
      json_url += "?";
      json_url += parts[1];
    }
    return window.location.protocol + "//" + window.location.host + json_url;
  }

  function fromJsonUrl(url) {
    return url.replace(/\.json$/, "");
  }

  function redirectToNonAjax(url) {
    window.location = url;
  }

  // replace all the questions currently in the page with whatever is returned for given url
  function reloadQuestions(url, params) {
    var url = toJsonUrl(url);

    addLoading('
Loading next step…
');

    $.ajax(url, {
      type: 'GET',
      dataType:'json',
      data: params,
      timeout: 5000,
      error: function(jqXHR, textStatus, errorStr) {
        var paramStr = $.param(params);
        redirectToNonAjax(url.replace('.json', '?' + paramStr));
      },
      success: function(data, textStatus, jqXHR) {
        addToHistory(data);
        updateContent(data['html_fragment']);
      }
    });
  }

  // manage the URL
  function addToHistory(data) {
    history.pushState(data, data['title'], data['url']);
    window._gaq && window._gaq.push(['_trackPageview', data['url']]);
  }

  // add an indicator of loading
  function addLoading(fragment){
    $('#content .step.current')
      .addClass('loading')
      .find('form .next-question')
      .append(fragment);
    $.event.trigger('smartanswerAnswer');
  };

  // update the content (i.e. plonk in the html fragment)
  function updateContent(fragment){
    $('.smart_answer #js-replaceable').html(fragment);
    $.event.trigger('smartanswerAnswer');
    if ($(".outcome").length !== 0) {
      $.event.trigger('smartanswerOutcome');
    }
  }

  function initializeHistory(data) {
    if (! browserSupportsHtml5HistoryApi() && window.location.pathname.match(/\/.*\//) ) {
      addToHistory({url: window.location.pathname});
    }

    data = {
      html_fragment: $('.smart_answer #js-replaceable').html(),
      title: "Question",
      url: window.location.toString()
    };
    history.replaceState(data, data['title'], data['url']);
  }

  var contentPosition = {
    latestQuestionTop : 0,
    latestQuestionIsOffScreen: function($latestQuestion) {
      var top_of_view = $(window).scrollTop();

      this.latestQuestionTop = $latestQuestion.offset().top;
      return (this.latestQuestionTop < top_of_view);
    },
    correctOffscreen: function() {
      $latestQuestion = $('.smart_answer .done-questions li.done:last-child');
      if (!$latestQuestion.length) {
        $latestQuestion = $('.smart_answer .step.current');
      }

      if(this.latestQuestionIsOffScreen($latestQuestion)) {
        $(window).scrollTop(this.latestQuestionTop);
      }
    },
    init: function() {
      var self = this;
      $(document).bind('smartanswerAnswer', function() {
        self.correctOffscreen();
        $('.meta-wrapper').show();
      });
      // Show feedback form in outcomes
      $(document).bind('smartanswerOutcome', function() {
        $('.report-a-problem-container form #url').val(window.location.href);
        $('.meta-wrapper').show();
      });
    }
  }

  contentPosition.init();

});
<commit_msg>Fix previous response redirects in JavaScript<commit_after>function browserSupportsHtml5HistoryApi() {
  return !! (history && history.replaceState && history.pushState);
}

$(document).ready(function() {
  //_gaq.push(['_trackEvent', 'Citizen-Format-Smartanswer', 'Load']);
  if(browserSupportsHtml5HistoryApi()) {
    var formSelector = ".current form";

    initializeHistory();

      // events
      // get new questions on submit
      $(formSelector).live('submit', function(event) {
        $('input[type=submit]', this).attr('disabled', 'disabled');
        var form = $(this);
        var postData = form.serializeArray();
        reloadQuestions(form.attr('action'), postData);
        event.preventDefault();
        return false;
      });

    // we want to start over with whatever gets provided if someone clicks to change the answer
    $(".undo a").live('click', function() {
      /*
        This is short lived tracking - to help inform a design decision, should not be permanent

        * Cat/Action/Label convention is taken from GOVUK.Analytics.Trackers in static
        * Slug extraction is based on position logic in GOVUK.Analytics.Trackers.smart_answer
      */
      var slug = document.URL.split('/')[3].split("#")[0].split("?")[0];
      window._gaq && window._gaq.push(['_trackEvent', "MS_smart_answer", slug, "Change Answer"]);

      reloadQuestions($(this).attr("href"), "");
      return false;
    });

    // manage next/back by tracking popstate event
    window.onpopstate = function (event) {
      if(event.state !== null) {
        updateContent(event.state['html_fragment']);
      } else {
        return false;
      }
    };
  }

  $('#current-error').focus();
  // helper functions
  function toJsonUrl(url) {
    var parts = url.split('?');
    var json_url = parts[0].replace(/\/$/, "") + ".json";

    if (parts[1]) {
      json_url += "?";
      json_url += parts[1];
    }

    return window.location.protocol + "//" + window.location.host + json_url;
  }

  function fromJsonUrl(url) {
    return url.replace(/\.json$/, "");
  }

  function redirectToNonAjax(url) {
    window.location = url;
  }

  // replace all the questions currently in the page with whatever is returned for given url
  function reloadQuestions(url, params) {
    var url = toJsonUrl(url);

    addLoading('
Loading next step…
');

    $.ajax(url, {
      type: 'GET',
      dataType:'json',
      data: params,
      timeout: 10000,
      error: function(jqXHR, textStatus, errorStr) {
        console.log("nope");
        console.log(errorStr);
        var paramStr = $.param(params);
        redirectToNonAjax(url.replace('.json', '?' + paramStr).replace('??', '?'));
      },
      success: function(data, textStatus, jqXHR) {
        addToHistory(data);
        updateContent(data['html_fragment']);
      }
    });
  }

  // manage the URL
  function addToHistory(data) {
    history.pushState(data, data['title'], data['url']);
    window._gaq && window._gaq.push(['_trackPageview', data['url']]);
  }

  // add an indicator of loading
  function addLoading(fragment){
    $('#content .step.current')
      .addClass('loading')
      .find('form .next-question')
      .append(fragment);
    $.event.trigger('smartanswerAnswer');
  };

  // update the content (i.e. plonk in the html fragment)
  function updateContent(fragment){
    $('.smart_answer #js-replaceable').html(fragment);
    $.event.trigger('smartanswerAnswer');
    if ($(".outcome").length !== 0) {
      $.event.trigger('smartanswerOutcome');
    }
  }

  function initializeHistory(data) {
    if (! browserSupportsHtml5HistoryApi() && window.location.pathname.match(/\/.*\//) ) {
      addToHistory({url: window.location.pathname});
    }

    data = {
      html_fragment: $('.smart_answer #js-replaceable').html(),
      title: "Question",
      url: window.location.toString()
    };
    history.replaceState(data, data['title'], data['url']);
  }

  var contentPosition = {
    latestQuestionTop : 0,
    latestQuestionIsOffScreen: function($latestQuestion) {
      var top_of_view = $(window).scrollTop();

      this.latestQuestionTop = $latestQuestion.offset().top;
      return (this.latestQuestionTop < top_of_view);
    },
    correctOffscreen: function() {
      $latestQuestion = $('.smart_answer .done-questions li.done:last-child');
      if (!$latestQuestion.length) {
        $latestQuestion = $('.smart_answer .step.current');
      }

      if(this.latestQuestionIsOffScreen($latestQuestion)) {
        $(window).scrollTop(this.latestQuestionTop);
      }
    },
    init: function() {
      var self = this;
      $(document).bind('smartanswerAnswer', function() {
        self.correctOffscreen();
        $('.meta-wrapper').show();
      });
      // Show feedback form in outcomes
      $(document).bind('smartanswerOutcome', function() {
        $('.report-a-problem-container form #url').val(window.location.href);
        $('.meta-wrapper').show();
      });
    }
  }

  contentPosition.init();

});
<|endoftext|>


<commit_before>export default {
    install (Vue) {
        Vue.mixin({
            beforeDestroy () {
                if (!Array.isArray(this._relateSingleton)) {
                    return;
                }
                this._relateSingleton.forEach((Component) => {
                    Component.dependent.splice(Component.dependent.indexOf(this), 1);
                    if (Component.dependent.length === 0) {
                        Component.instance.$destroy();
                        document.body.removeChild(Component.instance.$el);
                        Component.instance = null;
                    }
                });
            },
            methods: {
                $singleton (Component, propsData = {}, dependent) {
                    Component = Vue.extend(Component);
                    if (!Component.instance) {
                        Component.instance = new Component({
                            // 处理store
                            store: this.$store,
                            propsData,
                        });

                        Component.dependent = [];
                        Component.instance.$mount();
                        document.body.appendChild(Component.instance.$el);
                    } else {
                        Object.assign(Component.instance, propsData);
                    }

                    if (dependent instanceof Vue && !Component.dependent.includes(dependent)) {
                        Component.dependent.push(dependent);
                        if (!Array.isArray(dependent._relateSingleton)) {
                            dependent._relateSingleton = [];
                        }
                        if (!dependent._relateSingleton.includes(Component)) {
                            dependent._relateSingleton.push(Component);
                        }
                    }

                    return Component.instance;
                },
            },

        });
    },
}
;
<commit_msg>vue-singleton $singleton直接挂原型上<commit_after>export default {
    install (Vue) {
        Vue.prototype.$singleton = function (Component, propsData = {}, dependent) {
            Component = Vue.extend(Component);
            if (!Component.instance) {
                Component.instance = new Component({
                    // 处理store
                    store: this.$store,
                    propsData,
                });

                Component.dependent = [];
                Component.instance.$mount();
                document.body.appendChild(Component.instance.$el);
            } else {
                Object.assign(Component.instance, propsData);
            }

            if (dependent instanceof Vue && !Component.dependent.includes(dependent)) {
                Component.dependent.push(dependent);
                if (!Array.isArray(dependent._relateSingleton)) {
                    dependent._relateSingleton = [];
                }
                if (!dependent._relateSingleton.includes(Component)) {
                    dependent._relateSingleton.push(Component);
                }
            }

            return Component.instance;
        };

        Vue.mixin({
            beforeDestroy () {
                if (!Array.isArray(this._relateSingleton)) {
                    return;
                }
                this._relateSingleton.forEach((Component) => {
                    Component.dependent.splice(Component.dependent.indexOf(this), 1);
                    if (Component.dependent.length === 0) {
                        Component.instance.$destroy();
                        document.body.removeChild(Component.instance.$el);
                        Component.instance = null;
                    }
                });
            },
        });
    },
};
<|endoftext|>