<commit_before>var Alloy = require('alloy'),
	_ = Alloy._,
	Backbone = Alloy.Backbone,

	fbSDK = require('facebook'),
	Acl = require('RebelFrame/Acl');

/**
 * Facebook provides a wrapper for the Facebook SDK and Ti Facebook module.
 *
 * @class Facebook
 * @singleton
 */
var FB = {
	appId: Alloy.CFG.Facebook.appId,
	permissions: Alloy.CFG.Facebook.permissions,

	login: function(options) {
		// Wrap the success function
		// options.success = _.wrap(options.success, function(func, userData) {
		// 	// Call register function on the Cloud API
		// 	Acl.registerUserWithCloud(userData, {
		// 		success: func,
		// 		error: options.error,
		// 		scope: options.scope
		// 	});
		// });

		// Make the call using the FB SDK
		_authorize(options);
	},

	connect: function(options) {
		// Wrap the success function
		options.success = _.wrap(options.success, function(func, userData) {
			// Call connect function on the Cloud API
			Acl.connectSocialMediaToUser(userData, {
				success: func,
				error: options.error,
				scope: options.scope
			});
		});

		// Make the call using the FB SDK
		_authorize(callbackObj);
	},

	requestWriteAccess: function(callback) {
		if (fbSDK.loggedIn)
			_reAuthorize(callback);
		else
			_authorize({
				success: function(userData) {
					_reAuthorize(callback);
				},
				error: function(evt) {

				},
				scope: FB
			});
	},

	isLoggedin: function() {
		return fbSDK.loggedIn;
	},

	getPermissions: function() {
		return fbSDK.permissions;
	},

	hasWritePermissions: function() {
		return _.indexOf(fbSDK.permissions, FB.permissions.write[0]) !== -1;
	}

};

function _authorize(options) {
	var callbackWrapper = function(evt) {
		fbSDK.removeEventListener('login', callbackWrapper);

		// Add network to event object
		evt.network = 'facebook';

		if (evt.success) {
			//Write to cloud storage
			var userData = {
				facebook_id: fbSDK.uid,
				facebook_access_token: fbSDK.accessToken,
				facebook_expiration: fbSDK.expirationDate,
				type: 'facebook'
			};

			options.success(userData);

			// Log this event
			// Analytics.recordEvent('facebook', 'connect', 'facebook');
		} else if (evt.cancelled) {
			options.error(evt);
		} else {
			handleError(evt);

			options.error(evt);
		}

	};

	fbSDK.addEventListener('login', callbackWrapper);
	fbSDK.appid = FB.appId;

	// if (OS_IOS)
	// 	fbSDK.permissions = FB.permissions.read;
	// else
	// 	fbSDK.permissions = _.extend(FB.permissions.read, FB.permissions.write);

	fbSDK.forceDialogAuth = false;

	if (OS_IOS)
		fbSDK.initialize();

	// If user is loggedin to Facebook, log him/her out first
	if (fbSDK.loggedIn) {
		Ti.API.info('Already loggedin!');
	} else {
		fbSDK.authorize();
	}
}

function _reAuthorize(options) {
	var callbackWrapper = function(evt) {
		Ti.API.info(evt);

		if (evt.success) {
			//Write to cloud storage
			options.success(evt);

			// Log this event
			// Analytics.recordEvent('facebook', 'connect', 'facebook');
		} else if (evt.cancelled) {
			// Do nothing?
			Ti.API.info('Cancelled');
		} else {
			handleError(evt);

			options.error(evt);
		}
	};

	if (OS_IOS)
		fbSDK.requestNewPublishPermissions(FB.permissions.write, fbSDK.audienceFriends, callbackWrapper);
	else {
		// Android already has write permissions
		options.success();
	}
}

function handleError(evt) {
	var message;

	if (OS_IOS) {
		// For all of these errors - assume the user is logged out
		// so show your login UI
		if (evt.error.indexOf('Go to Settings') === 0) {
			message = L('fb_no_premission', 'Snowciety needs permission to access your facebook account. You can control this by going to Settings > Facebook on your phone.');
		} else if (evt.error.indexOf('Session Login Error') === 0) {
			// Session was invalid - e.g. the user deauthorized your app, etc
			// alert('Please login again.');
		} else if (evt.error.indexOf('OTHER:') !== 0) {
			// another error that may require user attention
			message = L('lFacebookError') + evt.error;
		} else {
			// This must be an error message that the user was already notified about
			// Due to the automatic error handling in the graph call
			// Don't surface it again
		}
	} else
		message = L('lFacebookError') + evt.error;

	if (message) {
		var dialog = Ti.UI.createAlertDialog({
			title: L('tFacebookError'),
			message: message,
			buttonNames: [L('Okay')],
			cancel: 0
		});
		dialog.show();
	}
}

module.exports = FB;
<commit_msg>Fixing Facebook lib<commit_after>var Alloy = require('alloy'),
	_ = Alloy._,
	Backbone = Alloy.Backbone,

	fbSDK = require('facebook'),
	Acl = require('RebelFrame/Acl');

/**
 * Facebook provides a wrapper for the Facebook SDK and Ti Facebook module.
 *
 * @class Facebook
 * @singleton
 */
var FB = {
	appId: Alloy.CFG.Facebook.appId,
	permissions: Alloy.CFG.Facebook.permissions,

	login: function(options) {
		// Wrap the success function
		// options.success = _.wrap(options.success, function(func, userData) {
		// 	// Call register function on the Cloud API
		// 	Acl.registerUserWithCloud(userData, {
		// 		success: func,
		// 		error: options.error,
		// 		scope: options.scope
		// 	});
		// });

		// Make the call using the FB SDK
		_authorize(options);
	},

	connect: function(options) {
		// Wrap the success function
		options.success = _.wrap(options.success, function(func, userData) {
			// Call connect function on the Cloud API
			Acl.connectSocialMediaToUser(userData, {
				success: func,
				error: options.error,
				scope: options.scope
			});
		});

		// Make the call using the FB SDK
		_authorize(callbackObj);
	},

	requestWriteAccess: function(callback) {
		if (fbSDK.loggedIn)
			_reAuthorize(callback);
		else
			_authorize({
				success: function(userData) {
					_reAuthorize(callback);
				},
				error: function(evt) {

				},
				scope: FB
			});
	},

	isLoggedin: function() {
		return fbSDK.loggedIn;
	},

	getPermissions: function() {
		return fbSDK.permissions;
	},

	hasWritePermissions: function() {
		return _.indexOf(fbSDK.permissions, FB.permissions.write[0]) !== -1;
	}

};

function _authorize(options) {
	var callbackWrapper = function(evt) {
		fbSDK.removeEventListener('login', callbackWrapper);

		// Add network to event object
		evt.network = 'facebook';

		if (evt.success) {
			//Write to cloud storage
			var userData = {
				facebook_id: fbSDK.uid,
				facebook_access_token: fbSDK.accessToken,
				facebook_expiration: fbSDK.expirationDate,
				type: 'facebook'
			};

			options.success(userData);

			// Log this event
			// Analytics.recordEvent('facebook', 'connect', 'facebook');
		} else if (evt.cancelled) {
			options.error(evt);
		} else {
			handleError(evt);

			options.error(evt);
		}

	};

	fbSDK.addEventListener('login', callbackWrapper);
	fbSDK.appid = FB.appId;

	if (OS_IOS)
		fbSDK.permissions = FB.permissions.read;
	else
		fbSDK.permissions = _.extend(FB.permissions.read, FB.permissions.write);

	fbSDK.forceDialogAuth = false;

	if (OS_IOS)
		fbSDK.initialize();

	// If user is loggedin to Facebook, log him/her out first
	if (fbSDK.loggedIn) {
		Ti.API.info('Already loggedin!');
	} else {
		fbSDK.authorize();
	}
}

function _reAuthorize(options) {
	var callbackWrapper = function(evt) {
		Ti.API.info(evt);

		if (evt.success) {
			//Write to cloud storage
			options.success(evt);

			// Log this event
			// Analytics.recordEvent('facebook', 'connect', 'facebook');
		} else if (evt.cancelled) {
			// Do nothing?
			Ti.API.info('Cancelled');
		} else {
			handleError(evt);

			options.error(evt);
		}
	};

	if (OS_IOS)
		fbSDK.requestNewPublishPermissions(FB.permissions.write, fbSDK.audienceFriends, callbackWrapper);
	else {
		// Android already has write permissions
		options.success();
	}
}

function handleError(evt) {
	var message;

	if (OS_IOS) {
		// For all of these errors - assume the user is logged out
		// so show your login UI
		if (evt.error.indexOf('Go to Settings') === 0) {
			message = L('fb_no_premission', 'Snowciety needs permission to access your facebook account. You can control this by going to Settings > Facebook on your phone.');
		} else if (evt.error.indexOf('Session Login Error') === 0) {
			// Session was invalid - e.g. the user deauthorized your app, etc
			// alert('Please login again.');
		} else if (evt.error.indexOf('OTHER:') !== 0) {
			// another error that may require user attention
			message = L('lFacebookError') + evt.error;
		} else {
			// This must be an error message that the user was already notified about
			// Due to the automatic error handling in the graph call
			// Don't surface it again
		}
	} else
		message = L('lFacebookError') + evt.error;

	if (message) {
		var dialog = Ti.UI.createAlertDialog({
			title: L('tFacebookError'),
			message: message,
			buttonNames: [L('Okay')],
			cancel: 0
		});
		dialog.show();
	}
}

module.exports = FB;
<|endoftext|>


<commit_before>/*jsl:option explicit*/

var cyberDojo = (function($cd, $j) {

  $cd.loadFile = function(filename) {
    var cf = $cd.currentFilename();
    var fc = $cd.fileContentFor(cf);
    var left = fc.data('scrollLeft') || fc.scrollLeft();
    var top = fc.data('scrollTop') || fc.scrollTop();

    // I want to
    //    1. restore scrollTop position
    //    2. restore scrollLeft position
    //    3. restore focus (also restores cursor position)
    // Restoring the focus loses the scrollTop/Left
    // positions so I have to save them in the dom so
    // I can set the back _after_ the call to focus()
    fc.data('scrollTop', top);
    fc.data('scrollLeft', left);

    $cd.fileDiv($cd.currentFilename()).hide();
    $cd.selectFileInFileList(filename);
    $cd.fileDiv(filename).show();

    // Resetting the focus here allows you to carry on
    // typing at the point the cursor left off.
    fc = $cd.fileContentFor(filename);
    fc.focus();

    // Restore the saved scrolLTop/Left positions.
    // Note that doing the seemingly equivalent
    //   fc.scrollTop(top);
    //   fc.scrollLeft(left);
    // here does _not_ work. I use animate instead with a very fast duration==1
    top = fc.data('scrollTop') || 0;
    left = fc.data('scrollLeft') || 0;
    fc.animate({scrollTop: top, scrollLeft: left}, 1);
    $j('#current_filename').val(filename);
  };

  $cd.selectFileInFileList = function(filename) {
    // Can't do $j('radio_' + filename) because filename
    // could contain characters that aren't strictly legal
    // characters in a dom node id
    // NB: This fails if the filename contains a double quote
    var node = $j('[id="radio_' + filename + '"]');
    var previousFilename = $cd.currentFilename();
    var previous = $j('[id="radio_' + previousFilename + '"]');

    $cd.radioEntrySwitch(previous, node);
    $cd.setRenameAndDeleteButtons(filename);
  };

  $cd.setRenameAndDeleteButtons = function(filename) {
    var file_ops = $j('#file_operation_buttons');
    var renameFile = file_ops.find('#rename');
    var deleteFile = file_ops.find('#delete');

    if ($cd.cantBeRenamedOrDeleted(filename)) {
      renameFile.attr('disabled', true);
      renameFile.removeAttr('title');
      deleteFile.attr('disabled', true);
      deleteFile.removeAttr('title');
    } else {
      renameFile.removeAttr('disabled');
      renameFile.attr('title', 'Rename the current file');
      deleteFile.removeAttr('disabled');
      deleteFile.attr('title', 'Delete the current file');
    }
  };

  $cd.cantBeRenamedOrDeleted = function(filename) {
    var filenames = [ 'cyberdojo.sh', 'output' ];
    return $cd.inArray(filename, filenames);
  };

  $cd.radioEntrySwitch = function(previous, current) {
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // This is used by the run-tests-page filename radio-list
    // and also the create-page languages/exercises radio-lists
    // See the comment for makeFileListEntry() in
    // cyberdojo-files.js
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $cd.deselectRadioEntry(previous.parent());
    $cd.selectRadioEntry(current);
  };

  $cd.loadNextFile = function() {
    var filenames = $cd.filenames().sort();
    var index = $j.inArray($cd.currentFilename(), filenames);
    var nextFilename = filenames[(index + 1) % filenames.length];
    $cd.loadFile(nextFilename);
  };

  return $cd;
})(cyberDojo || {}, $);

<commit_msg>renamed file retains its scrollTop/Left position<commit_after>/*jsl:option explicit*/

var cyberDojo = (function($cd, $j) {

  $cd.loadFile = function(filename) {
    var cf = $cd.currentFilename();
    var fc = $cd.fileContentFor(cf);
    var left = fc.scrollLeft();
    var top = fc.scrollTop();

    // I want to
    //    1. restore scrollTop position
    //    2. restore scrollLeft position
    //    3. restore focus (also restores cursor position)
    // Restoring the focus loses the scrollTop/Left
    // positions so I have to save them in the dom so
    // I can set the back _after_ the call to focus()
    fc.data('scrollTop', top);
    fc.data('scrollLeft', left);

    $cd.fileDiv($cd.currentFilename()).hide();
    $cd.selectFileInFileList(filename);
    $cd.fileDiv(filename).show();

    // Resetting the focus here allows you to carry on
    // typing at the point the cursor left off.
    fc = $cd.fileContentFor(filename);
    fc.focus();

    // Restore the saved scrolLTop/Left positions.
    // Note that doing the seemingly equivalent
    //   fc.scrollTop(top);
    //   fc.scrollLeft(left);
    // here does _not_ work. I use animate instead with a very fast duration==1
    top = fc.data('scrollTop') || 0;
    left = fc.data('scrollLeft') || 0;
    fc.animate({scrollTop: top, scrollLeft: left}, 1);
    $j('#current_filename').val(filename);
  };

  $cd.selectFileInFileList = function(filename) {
    // Can't do $j('radio_' + filename) because filename
    // could contain characters that aren't strictly legal
    // characters in a dom node id
    // NB: This fails if the filename contains a double quote
    var node = $j('[id="radio_' + filename + '"]');
    var previousFilename = $cd.currentFilename();
    var previous = $j('[id="radio_' + previousFilename + '"]');

    $cd.radioEntrySwitch(previous, node);
    $cd.setRenameAndDeleteButtons(filename);
  };

  $cd.setRenameAndDeleteButtons = function(filename) {
    var file_ops = $j('#file_operation_buttons');
    var renameFile = file_ops.find('#rename');
    var deleteFile = file_ops.find('#delete');

    if ($cd.cantBeRenamedOrDeleted(filename)) {
      renameFile.attr('disabled', true);
      renameFile.removeAttr('title');
      deleteFile.attr('disabled', true);
      deleteFile.removeAttr('title');
    } else {
      renameFile.removeAttr('disabled');
      renameFile.attr('title', 'Rename the current file');
      deleteFile.removeAttr('disabled');
      deleteFile.attr('title', 'Delete the current file');
    }
  };

  $cd.cantBeRenamedOrDeleted = function(filename) {
    var filenames = [ 'cyberdojo.sh', 'output' ];
    return $cd.inArray(filename, filenames);
  };

  $cd.radioEntrySwitch = function(previous, current) {
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    // This is used by the run-tests-page filename radio-list
    // and also the create-page languages/exercises radio-lists
    // See the comment for makeFileListEntry() in
    // cyberdojo-files.js
    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    $cd.deselectRadioEntry(previous.parent());
    $cd.selectRadioEntry(current);
  };

  $cd.loadNextFile = function() {
    var filenames = $cd.filenames().sort();
    var index = $j.inArray($cd.currentFilename(), filenames);
    var nextFilename = filenames[(index + 1) % filenames.length];
    $cd.loadFile(nextFilename);
  };

  return $cd;
})(cyberDojo || {}, $);

<|endoftext|>


<commit_before>/* global Fae */

Fae.form.validator = {

  init: function () {
    if ($('form').length) {
      this.password_confirmation_validation.init();
      this.password_presence_conditional();
      this.bind_validation_events();
      this.form_validate();
      this.length_counter.init();
    }
  },

  vars: {
    IS_VALID: ''
  },

  el: {},

  // validate the entire form on submit and stop it if the form is invalid
  form_validate: function () {
    var self = this;
    $(document).on('form', 'submit', function (e) {
      self.vars.IS_VALID = true;
      $('[data-validate]').each(function () {
        if ($(this).data('validate').length) {
          self.judge_it($(this));
        }
      });
      if (self.vars.IS_VALID === false) {
        Fae.navigation.language.check_for_hidden_errors();
        Fae.helpers.scroll_to($('span.error').first());
        e.preventDefault();
      }
    });
  },

  // bind validation events based on input type
  bind_validation_events: function () {
    var _this = this;
    $('[data-validate]').each(function () {
      var $this = $(this);
      if ($this.data('validate').length) {
        if ($this.is('input:not(.hasDatepicker), textarea')) {
          // normal inputs validate on blur
          $this.blur(function () {
            _this.judge_it($this);
          });
        } else if ($this.is('.hasDatepicker')) {
          // date pickers need a little delay
          $this.blur(function () {
            setTimeout(function(){ _this.judge_it($this); }, 500);
          });
        } else if ($this.is('select')) {
          // selects validate on change
          $this.change(function () {
            _this.judge_it($this);
          });
        }
      }
    });
  },

  // 'private functions'

  // main judge call
  judge_it: function ($input) {
    var self = this;
    judge.validate($input[0], {
      valid: function () {
        self.create_success_class($input);
      },
      invalid: function (input, messages) {
        self.vars.IS_VALID = false;
        self.label_named_message($input, messages);
        self.create_or_replace_error($input, messages);
      }
    });
  },

  // sets the input element that is focused on chosen elements
  chosen_input: function ($input) {
    return $input.next('.chosen-container').find('input');
  },

  // returns a BOOL based on if the input is a chosen input
  is_chosen: function ($input) {
    return $input.next('.chosen-container').length;
  },

  label_named_message: function ($el, messages) {
    var $label;
    var index = 0;

    if ($el.is(':radio')) {
      $label = $el.parent().closest('span').siblings('label');
    } else {
      $label = $el.siblings('label');
    }

    if ($label.get(0).childNodes[0].nodeName === "ABBR") {
      index = 1;
    }

    for (var i = messages.length - 1; i >= 0; i--) {
      messages[i] = $label.get(0).childNodes[index].nodeValue + " " + messages[i];
    }
  },

  // adds and removes the appropriate classes to display the success styles
  create_success_class: function ($input) {
    var $styled_input = this.set_chosen_input($input);
    $styled_input.addClass('valid').removeClass('invalid');

    $input.parent().removeClass('field_with_errors').children('.error').remove();
  },

  // adds and removes the appropriate classes to display the error styles
  create_or_replace_error: function ($input, messages) {
    var $styled_input = this.set_chosen_input($input);
    $styled_input.addClass('invalid').removeClass('valid');

    var $wrapper = $input.closest('.input');
    if ($wrapper.children('.error').length) {
      $wrapper.children('.error').text(messages.join(','));
    } else {
      $wrapper.addClass('field_with_errors').append("" + messages.join(',') + "");
    }
  },

  // a DRY method for setting the element that should take the .valid or .invalid style
  set_chosen_input: function ($input) {
    var $styled_input = $input;
    if (this.is_chosen($input)) {
      if ($input.next('.chosen-container').find('.chosen-single').length) {
        $styled_input = $input.next('.chosen-container').find('.chosen-single');
      } else if ($input.next('.chosen-container').find('.chosen-choices').length) {
        $styled_input = $input.next('.chosen-container').find('.chosen-choices');
      }
    }

    return $styled_input;
  },

  // strips a fields Judge validation
  // $field: input fields as a jQuery object
  // kind: the kind of validation (e.g. 'presence' or 'confirmation')
  strip_validation: function($field, kind) {
    var validations = $field.data('validate');
    for (var i = 0; i < validations.length; i++) {
      // validation items can be strings or JSON objects
      // let's convert the strings to JSON so we're dealing with consistent types
      if (typeof validations[i] == 'string' || validations[i] instanceof String) {
        validations[i] = JSON.parse(validations[i]);
      }

      // if the kind matches, remove it from the array
      if (validations[i]['kind'] === kind) {
        validations.splice(i, 1);
      }

      // convert JSON back to a string
      validations[i] = JSON.stringify(validations[i]);
    }
    $field.attr('data-validate', '[' + validations + ']');
  },

  // Judge validates confirmation on the original field
  // this is a hack to remove Judge's validation and add it to the confirmation field
  password_confirmation_validation: {
    $password_field: null,
    $password_confirmation_field: null,

    init: function() {
      var self = this;

      self.$password_field = $('#user_password');
      self.$password_confirmation_field = $('#user_password_confirmation');

      if (self.$password_confirmation_field.length) {
        Validator.strip_validation(self.$password_field, 'confirmation');
        self.add_custom_validation();
      }
    },

    add_custom_validation: function() {
      var self = this;
      self.$password_confirmation_field.on('blur', function() {
        self.validate_confirmation(self);
      });
      $('form').on('submit', function(ev) {
        Fae.validator.vars.IS_VALID = true;
        self.validate_confirmation(self);
        if (!Fae.validator.vars.IS_VALID) {
          ev.preventDefault();
        }
      });
    },

    validate_confirmation: function(self) {
      var validator = Fae.validator;
      if (self.$password_field.val() == self.$password_confirmation_field.val()) {
        Validator.create_success_class(self.$password_confirmation_field);
      } else {
        var message = ['must match Password'];
        validator.vars.IS_VALID = false;
        validator.label_named_message(self.$password_confirmation_field, message);
        validator.create_or_replace_error(self.$password_confirmation_field, message);
      }
    }
  },

  // Judge always read the `on: :create` validations,
  // so we need to strip the password presence validation
  // on the user edit form
  password_presence_conditional: function() {
    var $edit_user_password = $('.edit_user #user_password');
    if ($edit_user_password.length) {
      this.strip_validation($edit_user_password, 'presence');
    }
  },

  length_counter: {

    init: function(){
      this.find_length_validations();
    },

    find_length_validations: function() {
      var self = this;
      $('[data-validate]').each(function () {
        var $this = $(this);
        if ($this.data('validate').length ) {
          var validations = $this.data('validate');
          $.grep(validations, function(item){
            if (item.kind == 'length'){
              var max = item.options.maximum;
              self.set_counter($this, max);
            }
          });
        }
      });
    },

    set_counter: function($elem, max, current) {
      current = current || 0 + (max - $elem.val().length);

      var text = this._create_counter_text($elem, max, current);

      if ($elem.siblings('.counter').length) {
        $elem.siblings('.counter').remove();
        this.create_counter_elem($elem, max, current, text);
      } else {
        this.create_counter_elem($elem, max, current, text);
        this.add_counter_listener($elem, max);
      }
    },

    _create_counter_text: function($elem, max, current) {
      var prep = "Maximum Characters: " + max;
      if (current > 0 || $elem.val().length > 0) {
        prep += " / Characters Left: " + current + "";
      }
      return prep;
    },

    create_counter_elem: function($elem, max, current, text){
      $( "

" + text + "

" ).insertAfter( $elem );
      if (current <= 0 || $elem.val().length >= 100){
        $elem.siblings('.counter').children('p').children('.characters-left').addClass('overCount');
      }
    },

    add_counter_listener: function($elem, max) {
      var self = this;
      $elem.keyup(function() {
        var current = (max - ($elem.val().length));
        self.set_counter($elem, max, current);
      });
      $elem.keypress(function(e) {
        var current = (max - $elem.val().length);
        if (current <= 0) {
          if (e.keyCode !== 8 || e.keyCode !== 46) {
            e.preventDefault();
          }
        }
      });
    }
  },

};
<commit_msg>fix breaking javascript error<commit_after>/* global Fae */

Fae.form.validator = {

  init: function () {
    if ($('form').length) {
      this.password_confirmation_validation.init();
      this.password_presence_conditional();
      this.bind_validation_events();
      this.form_validate();
      this.length_counter.init();
    }
  },

  vars: {
    IS_VALID: ''
  },

  el: {},

  // validate the entire form on submit and stop it if the form is invalid
  form_validate: function () {
    var self = this;
    $(document).on('form', 'submit', function (e) {
      self.vars.IS_VALID = true;
      $('[data-validate]').each(function () {
        if ($(this).data('validate').length) {
          self.judge_it($(this));
        }
      });
      if (self.vars.IS_VALID === false) {
        Fae.navigation.language.check_for_hidden_errors();
        Fae.helpers.scroll_to($('span.error').first());
        e.preventDefault();
      }
    });
  },

  // bind validation events based on input type
  bind_validation_events: function () {
    var _this = this;
    $('[data-validate]').each(function () {
      var $this = $(this);
      if ($this.data('validate').length) {
        if ($this.is('input:not(.hasDatepicker), textarea')) {
          // normal inputs validate on blur
          $this.blur(function () {
            _this.judge_it($this);
          });
        } else if ($this.is('.hasDatepicker')) {
          // date pickers need a little delay
          $this.blur(function () {
            setTimeout(function(){ _this.judge_it($this); }, 500);
          });
        } else if ($this.is('select')) {
          // selects validate on change
          $this.change(function () {
            _this.judge_it($this);
          });
        }
      }
    });
  },

  // 'private functions'

  // main judge call
  judge_it: function ($input) {
    var self = this;
    judge.validate($input[0], {
      valid: function () {
        self.create_success_class($input);
      },
      invalid: function (input, messages) {
        self.vars.IS_VALID = false;
        self.label_named_message($input, messages);
        self.create_or_replace_error($input, messages);
      }
    });
  },

  // sets the input element that is focused on chosen elements
  chosen_input: function ($input) {
    return $input.next('.chosen-container').find('input');
  },

  // returns a BOOL based on if the input is a chosen input
  is_chosen: function ($input) {
    return $input.next('.chosen-container').length;
  },

  label_named_message: function ($el, messages) {
    var $label;
    var index = 0;

    if ($el.is(':radio')) {
      $label = $el.parent().closest('span').siblings('label');
    } else {
      $label = $el.siblings('label');
    }

    if ($label.get(0).childNodes[0].nodeName === "ABBR") {
      index = 1;
    }

    for (var i = messages.length - 1; i >= 0; i--) {
      messages[i] = $label.get(0).childNodes[index].nodeValue + " " + messages[i];
    }
  },

  // adds and removes the appropriate classes to display the success styles
  create_success_class: function ($input) {
    var $styled_input = this.set_chosen_input($input);
    $styled_input.addClass('valid').removeClass('invalid');

    $input.parent().removeClass('field_with_errors').children('.error').remove();
  },

  // adds and removes the appropriate classes to display the error styles
  create_or_replace_error: function ($input, messages) {
    var $styled_input = this.set_chosen_input($input);
    $styled_input.addClass('invalid').removeClass('valid');

    var $wrapper = $input.closest('.input');
    if ($wrapper.children('.error').length) {
      $wrapper.children('.error').text(messages.join(','));
    } else {
      $wrapper.addClass('field_with_errors').append("" + messages.join(',') + "");
    }
  },

  // a DRY method for setting the element that should take the .valid or .invalid style
  set_chosen_input: function ($input) {
    var $styled_input = $input;
    if (this.is_chosen($input)) {
      if ($input.next('.chosen-container').find('.chosen-single').length) {
        $styled_input = $input.next('.chosen-container').find('.chosen-single');
      } else if ($input.next('.chosen-container').find('.chosen-choices').length) {
        $styled_input = $input.next('.chosen-container').find('.chosen-choices');
      }
    }

    return $styled_input;
  },

  // strips a fields Judge validation
  // $field: input fields as a jQuery object
  // kind: the kind of validation (e.g. 'presence' or 'confirmation')
  strip_validation: function($field, kind) {
    var validations = $field.data('validate');
    for (var i = 0; i < validations.length; i++) {
      // validation items can be strings or JSON objects
      // let's convert the strings to JSON so we're dealing with consistent types
      if (typeof validations[i] == 'string' || validations[i] instanceof String) {
        validations[i] = JSON.parse(validations[i]);
      }

      // if the kind matches, remove it from the array
      if (validations[i]['kind'] === kind) {
        validations.splice(i, 1);
      }

      // convert JSON back to a string
      validations[i] = JSON.stringify(validations[i]);
    }
    $field.attr('data-validate', '[' + validations + ']');
  },

  // Judge validates confirmation on the original field
  // this is a hack to remove Judge's validation and add it to the confirmation field
  password_confirmation_validation: {
    $password_field: null,
    $password_confirmation_field: null,

    init: function() {
      var self = this;

      self.$password_field = $('#user_password');
      self.$password_confirmation_field = $('#user_password_confirmation');

      if (self.$password_confirmation_field.length) {
        Fae.form.validator.strip_validation(self.$password_field, 'confirmation');
        self.add_custom_validation();
      }
    },

    add_custom_validation: function() {
      var self = this;
      self.$password_confirmation_field.on('blur', function() {
        self.validate_confirmation(self);
      });
      $('form').on('submit', function(ev) {
        Fae.validator.vars.IS_VALID = true;
        self.validate_confirmation(self);
        if (!Fae.validator.vars.IS_VALID) {
          ev.preventDefault();
        }
      });
    },

    validate_confirmation: function(self) {
      var validator = Fae.validator;
      if (self.$password_field.val() == self.$password_confirmation_field.val()) {
        Fae.form.validator.create_success_class(self.$password_confirmation_field);
      } else {
        var message = ['must match Password'];
        validator.vars.IS_VALID = false;
        validator.label_named_message(self.$password_confirmation_field, message);
        validator.create_or_replace_error(self.$password_confirmation_field, message);
      }
    }
  },

  // Judge always read the `on: :create` validations,
  // so we need to strip the password presence validation
  // on the user edit form
  password_presence_conditional: function() {
    var $edit_user_password = $('.edit_user #user_password');
    if ($edit_user_password.length) {
      this.strip_validation($edit_user_password, 'presence');
    }
  },

  length_counter: {

    init: function(){
      this.find_length_validations();
    },

    find_length_validations: function() {
      var self = this;
      $('[data-validate]').each(function () {
        var $this = $(this);
        if ($this.data('validate').length ) {
          var validations = $this.data('validate');
          $.grep(validations, function(item){
            if (item.kind == 'length'){
              var max = item.options.maximum;
              self.set_counter($this, max);
            }
          });
        }
      });
    },

    set_counter: function($elem, max, current) {
      current = current || 0 + (max - $elem.val().length);

      var text = this._create_counter_text($elem, max, current);

      if ($elem.siblings('.counter').length) {
        $elem.siblings('.counter').remove();
        this.create_counter_elem($elem, max, current, text);
      } else {
        this.create_counter_elem($elem, max, current, text);
        this.add_counter_listener($elem, max);
      }
    },

    _create_counter_text: function($elem, max, current) {
      var prep = "Maximum Characters: " + max;
      if (current > 0 || $elem.val().length > 0) {
        prep += " / Characters Left: " + current + "";
      }
      return prep;
    },

    create_counter_elem: function($elem, max, current, text){
      $( "

" + text + "

" ).insertAfter( $elem );
      if (current <= 0 || $elem.val().length >= 100){
        $elem.siblings('.counter').children('p').children('.characters-left').addClass('overCount');
      }
    },

    add_counter_listener: function($elem, max) {
      var self = this;
      $elem.keyup(function() {
        var current = (max - ($elem.val().length));
        self.set_counter($elem, max, current);
      });
      $elem.keypress(function(e) {
        var current = (max - $elem.val().length);
        if (current <= 0) {
          if (e.keyCode !== 8 || e.keyCode !== 46) {
            e.preventDefault();
          }
        }
      });
    }
  },

};
<|endoftext|>


<commit_before>/* eslint-disable */
((global) => {
  const customValidationFlag = 'gl-field-error-ignore';

  class GlFieldErrors {
    constructor(form) {
      this.form = $(form);
      this.state = {
        inputs: [],
        valid: false
      };
      this.initValidators();
    }

    initValidators () {
      // register selectors here as needed
      const validateSelectors = [':text', ':password', '[type=email]']
        .map((selector) => `input${selector}`).join(',');

      this.state.inputs = this.form.find(validateSelectors).toArray()
        .filter((input) => !input.classList.contains(customValidationFlag))
        .map((input) => new global.GlFieldError({ input, formErrors: this }));

      this.form.on('submit', this.catchInvalidFormSubmit);
    }

    /* Neccessary to prevent intercept and override invalid form submit
     * because Safari & iOS quietly allow form submission when form is invalid
     * and prevents disabling of invalid submit button by application.js */

    catchInvalidFormSubmit (event) {
      if (!event.currentTarget.checkValidity()) {
        event.preventDefault();
        event.stopPropagation();
      }
    }

    focusOnFirstInvalid () {
      const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
      firstInvalid.inputElement.focus();
    }
  }

  global.GlFieldErrors = GlFieldErrors;

})(window.gl || (window.gl = {}));
<commit_msg>List gl_field_error as gl_field_errors dep.<commit_after>/* eslint-disable */

//= require gl_field_error

((global) => {
  const customValidationFlag = 'gl-field-error-ignore';

  class GlFieldErrors {
    constructor(form) {
      this.form = $(form);
      this.state = {
        inputs: [],
        valid: false
      };
      this.initValidators();
    }

    initValidators () {
      // register selectors here as needed
      const validateSelectors = [':text', ':password', '[type=email]']
        .map((selector) => `input${selector}`).join(',');

      this.state.inputs = this.form.find(validateSelectors).toArray()
        .filter((input) => !input.classList.contains(customValidationFlag))
        .map((input) => new global.GlFieldError({ input, formErrors: this }));

      this.form.on('submit', this.catchInvalidFormSubmit);
    }

    /* Neccessary to prevent intercept and override invalid form submit
     * because Safari & iOS quietly allow form submission when form is invalid
     * and prevents disabling of invalid submit button by application.js */

    catchInvalidFormSubmit (event) {
      if (!event.currentTarget.checkValidity()) {
        event.preventDefault();
        event.stopPropagation();
      }
    }

    focusOnFirstInvalid () {
      const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
      firstInvalid.inputElement.focus();
    }
  }

  global.GlFieldErrors = GlFieldErrors;

})(window.gl || (window.gl = {}));
<|endoftext|>


<commit_before>/* eslint-disable no-unreachable */
/*
Language: JavaScript
Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions.
Category: common, scripting
Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript
*/

import * as ECMAScript from './lib/ecmascript.js';
import * as regex from '../lib/regex.js';

/** @type LanguageFn */
export default function(hljs) {
  /**
   * Takes a string like "<Booger" and checks to see
   * if we can find a matching "</Booger" later in the
   * content.
   * @param {RegExpMatchArray} match
   * @param {{after:number}} param1
   */
  const hasClosingTag = (match, { after }) => {
    const tag = match[0].replace("<", "</");
    const pos = match.input.indexOf(tag, after);
    return pos !== -1;
  };

  const IDENT_RE = ECMAScript.IDENT_RE;
  const FRAGMENT = {
    begin: '<>',
    end: '</>'
  };
  const XML_TAG = {
    begin: /<[A-Za-z0-9\\._:-]+/,
    end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
    /**
     * @param {RegExpMatchArray} match
     * @param {CallbackResponse} response
     */
    isTrulyOpeningTag: (match, response) => {
      const afterMatchIndex = match[0].length + match.index;
      const nextChar = match.input[afterMatchIndex];
      // nested type?
      // HTML should not include another raw `<` inside a tag
      // But a type might: `<Array<Array<number>>`, etc.
      if (nextChar === "<") {
        response.ignoreMatch();
        return;
      }
      // <something>
      // This is now either a tag or a type.
      if (nextChar === ">") {
        // if we cannot find a matching closing tag, then we
        // will ignore it
        if (!hasClosingTag(match, { after: afterMatchIndex })) {
          response.ignoreMatch();
        }
      }
    }
  };
  const KEYWORDS = {
    $pattern: ECMAScript.IDENT_RE,
    keyword: ECMAScript.KEYWORDS.join(" "),
    literal: ECMAScript.LITERALS.join(" "),
    built_in: ECMAScript.BUILT_INS.join(" ")
  };

  // https://tc39.es/ecma262/#sec-literals-numeric-literals
  const decimalDigits = '[0-9](_?[0-9])*';
  const frac = `\\.(${decimalDigits})`;
  // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
  // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
  const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;
  const NUMBER = {
    className: 'number',
    variants: [
      // DecimalLiteral
      { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
        `[eE][+-]?(${decimalDigits})\\b` },
      { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },

      // DecimalBigIntegerLiteral
      { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` },

      // NonDecimalIntegerLiteral
      { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" },
      { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" },
      { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" },

      // LegacyOctalIntegerLiteral (does not include underscore separators)
      // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
      { begin: "\\b0[0-7]+n?\\b" },
    ],
    relevance: 0
  };

  const SUBST = {
    className: 'subst',
    begin: '\\$\\{',
    end: '\\}',
    keywords: KEYWORDS,
    contains: [] // defined later
  };
  const HTML_TEMPLATE = {
    begin: 'html`',
    end: '',
    starts: {
      end: '`',
      returnEnd: false,
      contains: [
        hljs.BACKSLASH_ESCAPE,
        SUBST
      ],
      subLanguage: 'xml'
    }
  };
  const CSS_TEMPLATE = {
    begin: 'css`',
    end: '',
    starts: {
      end: '`',
      returnEnd: false,
      contains: [
        hljs.BACKSLASH_ESCAPE,
        SUBST
      ],
      subLanguage: 'css'
    }
  };
  const TEMPLATE_STRING = {
    className: 'string',
    begin: '`',
    end: '`',
    contains: [
      hljs.BACKSLASH_ESCAPE,
      SUBST
    ]
  };
  const JSDOC_COMMENT = hljs.COMMENT(
    '/\\*\\*',
    '\\*/',
    {
      relevance: 0,
      contains: [
        {
          className: 'doctag',
          begin: '@[A-Za-z]+',
          contains: [
            {
              className: 'type',
              begin: '\\{',
              end: '\\}',
              relevance: 0
            },
            {
              className: 'variable',
              begin: IDENT_RE + '(?=\\s*(-)|$)',
              endsParent: true,
              relevance: 0
            },
            // eat spaces (not newlines) so we can find
            // types or variables
            {
              begin: /(?=[^\n])\s/,
              relevance: 0
            }
          ]
        }
      ]
    }
  );
  const COMMENT = {
    className: "comment",
    variants: [
      JSDOC_COMMENT,
      hljs.C_BLOCK_COMMENT_MODE,
      hljs.C_LINE_COMMENT_MODE
    ]
  };
  const SUBST_INTERNALS = [
    hljs.APOS_STRING_MODE,
    hljs.QUOTE_STRING_MODE,
    HTML_TEMPLATE,
    CSS_TEMPLATE,
    TEMPLATE_STRING,
    NUMBER,
    hljs.REGEXP_MODE
  ];
  SUBST.contains = SUBST_INTERNALS
    .concat({
      // we need to pair up {} inside our subst to prevent
      // it from ending too early by matching another }
      begin: /{/,
      end: /}/,
      keywords: KEYWORDS,
      contains: [
        "self"
      ].concat(SUBST_INTERNALS)
    });
  const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);
  const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([
    // eat recursive parens in sub expressions
    {
      begin: /\(/,
      end: /\)/,
      keywords: KEYWORDS,
      contains: ["self"].concat(SUBST_AND_COMMENTS)
    }
  ]);
  const PARAMS = {
    className: 'params',
    begin: /\(/,
    end: /\)/,
    excludeBegin: true,
    excludeEnd: true,
    keywords: KEYWORDS,
    contains: PARAMS_CONTAINS
  };

  return {
    name: 'Javascript',
    aliases: ['js', 'jsx', 'mjs', 'cjs'],
    keywords: KEYWORDS,
    // this will be extended by TypeScript
    exports: { PARAMS_CONTAINS },
    illegal: /#(?![$_A-z])/,
    contains: [
      hljs.SHEBANG({
        label: "shebang",
        binary: "node",
        relevance: 5
      }),
      {
        label: "use_strict",
        className: 'meta',
        relevance: 10,
        begin: /^\s*['"]use (strict|asm)['"]/
      },
      hljs.APOS_STRING_MODE,
      hljs.QUOTE_STRING_MODE,
      HTML_TEMPLATE,
      CSS_TEMPLATE,
      TEMPLATE_STRING,
      COMMENT,
      NUMBER,
      { // object attr container
        begin: regex.concat(/[{,\n]\s*/,
          // we need to look ahead to make sure that we actually have an
          // attribute coming up so we don't steal a comma from a potential
          // "value" container
          //
          // NOTE: this might not work how you think.  We don't actually always
          // enter this mode and stay.  Instead it might merely match `,
          // <comments up next>` and then immediately end after the , because it
          // fails to find any actual attrs. But this still does the job because
          // it prevents the value contain rule from grabbing this instead and
          // prevening this rule from firing when we actually DO have keys.
          regex.lookahead(regex.concat(
            // we also need to allow for multiple possible comments inbetween
            // the first key:value pairing
            /(\/\/.*$)*/,
            /(\/\*(.|\n)*\*\/)*/,
            /\s*/,
            IDENT_RE + '\\s*:'))),
        relevance: 0,
        contains: [
          {
            className: 'attr',
            begin: IDENT_RE + regex.lookahead('\\s*:'),
            relevance: 0
          }
        ]
      },
      { // "value" container
        begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*',
        keywords: 'return throw case',
        contains: [
          COMMENT,
          hljs.REGEXP_MODE,
          {
            className: 'function',
            // we have to count the parens to make sure we actually have the
            // correct bounding ( ) before the =>.  There could be any number of
            // sub-expressions inside also surrounded by parens.
            begin: '(\\(' +
            '[^()]*(\\(' +
            '[^()]*(\\(' +
            '[^()]*' +
            '\\))*[^()]*' +
            '\\))*[^()]*' +
            '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>',
            returnBegin: true,
            end: '\\s*=>',
            contains: [
              {
                className: 'params',
                variants: [
                  {
                    begin: hljs.UNDERSCORE_IDENT_RE
                  },
                  {
                    className: null,
                    begin: /\(\s*\)/,
                    skip: true
                  },
                  {
                    begin: /\(/,
                    end: /\)/,
                    excludeBegin: true,
                    excludeEnd: true,
                    keywords: KEYWORDS,
                    contains: PARAMS_CONTAINS
                  }
                ]
              }
            ]
          },
          { // could be a comma delimited list of params to a function call
            begin: /,/, relevance: 0
          },
          {
            className: '',
            begin: /\s/,
            end: /\s*/,
            skip: true
          },
          { // JSX
            variants: [
              { begin: FRAGMENT.begin, end: FRAGMENT.end },
              {
                begin: XML_TAG.begin,
                // we carefully check the opening tag to see if it truly
                // is a tag and not a false positive
                'on:begin': XML_TAG.isTrulyOpeningTag,
                end: XML_TAG.end
              }
            ],
            subLanguage: 'xml',
            contains: [
              {
                begin: XML_TAG.begin,
                end: XML_TAG.end,
                skip: true,
                contains: ['self']
              }
            ]
          }
        ],
        relevance: 0
      },
      {
        className: 'function',
        beginKeywords: 'function',
        end: /[{;]/,
        excludeEnd: true,
        keywords: KEYWORDS,
        contains: [
          'self',
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
          PARAMS
        ],
        illegal: /%/
      },
      {
        className: 'function',
        // we have to count the parens to make sure we actually have the correct
        // bounding ( ).  There could be any number of sub-expressions inside
        // also surrounded by parens.
        begin: hljs.UNDERSCORE_IDENT_RE +
          '\\(' + // first parens
          '[^()]*(\\(' +
            '[^()]*(\\(' +
              '[^()]*' +
            '\\))*[^()]*' +
          '\\))*[^()]*' +
          '\\)\\s*{', // end parens
        returnBegin:true,
        contains: [
          PARAMS,
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
        ]
      },
      // hack: prevents detection of keywords in some circumstances
      // .keyword()
      // $keyword = x
      {
        variants: [
          { begin: '\\.' + IDENT_RE },
          { begin: '\\$' + IDENT_RE }
        ],
        relevance: 0
      },
      { // ES6 class
        className: 'class',
        beginKeywords: 'class',
        end: /[{;=]/,
        excludeEnd: true,
        illegal: /[:"\[\]]/,
        contains: [
          { beginKeywords: 'extends' },
          hljs.UNDERSCORE_TITLE_MODE
        ]
      },
      {
        begin: /\b(?=constructor)/,
        end: /[\{;]/,
        excludeEnd: true,
        contains: [
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
          'self',
          PARAMS
        ]
      },
      {
        begin: '(get|set)\\s+(?=' + IDENT_RE + '\\()',
        end: /{/,
        keywords: "get set",
        contains: [
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
          { begin: /\(\)/ }, // eat to avoid empty params
          PARAMS
        ]
      },
      {
        begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`
      }
    ]
  };
}
<commit_msg>(chore) remove old linter hint<commit_after>/*
Language: JavaScript
Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions.
Category: common, scripting
Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript
*/

import * as ECMAScript from './lib/ecmascript.js';
import * as regex from '../lib/regex.js';

/** @type LanguageFn */
export default function(hljs) {
  /**
   * Takes a string like "<Booger" and checks to see
   * if we can find a matching "</Booger" later in the
   * content.
   * @param {RegExpMatchArray} match
   * @param {{after:number}} param1
   */
  const hasClosingTag = (match, { after }) => {
    const tag = match[0].replace("<", "</");
    const pos = match.input.indexOf(tag, after);
    return pos !== -1;
  };

  const IDENT_RE = ECMAScript.IDENT_RE;
  const FRAGMENT = {
    begin: '<>',
    end: '</>'
  };
  const XML_TAG = {
    begin: /<[A-Za-z0-9\\._:-]+/,
    end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
    /**
     * @param {RegExpMatchArray} match
     * @param {CallbackResponse} response
     */
    isTrulyOpeningTag: (match, response) => {
      const afterMatchIndex = match[0].length + match.index;
      const nextChar = match.input[afterMatchIndex];
      // nested type?
      // HTML should not include another raw `<` inside a tag
      // But a type might: `<Array<Array<number>>`, etc.
      if (nextChar === "<") {
        response.ignoreMatch();
        return;
      }
      // <something>
      // This is now either a tag or a type.
      if (nextChar === ">") {
        // if we cannot find a matching closing tag, then we
        // will ignore it
        if (!hasClosingTag(match, { after: afterMatchIndex })) {
          response.ignoreMatch();
        }
      }
    }
  };
  const KEYWORDS = {
    $pattern: ECMAScript.IDENT_RE,
    keyword: ECMAScript.KEYWORDS.join(" "),
    literal: ECMAScript.LITERALS.join(" "),
    built_in: ECMAScript.BUILT_INS.join(" ")
  };

  // https://tc39.es/ecma262/#sec-literals-numeric-literals
  const decimalDigits = '[0-9](_?[0-9])*';
  const frac = `\\.(${decimalDigits})`;
  // DecimalIntegerLiteral, including Annex B NonOctalDecimalIntegerLiteral
  // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
  const decimalInteger = `0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*`;
  const NUMBER = {
    className: 'number',
    variants: [
      // DecimalLiteral
      { begin: `(\\b(${decimalInteger})((${frac})|\\.)?|(${frac}))` +
        `[eE][+-]?(${decimalDigits})\\b` },
      { begin: `\\b(${decimalInteger})\\b((${frac})\\b|\\.)?|(${frac})\\b` },

      // DecimalBigIntegerLiteral
      { begin: `\\b(0|[1-9](_?[0-9])*)n\\b` },

      // NonDecimalIntegerLiteral
      { begin: "\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b" },
      { begin: "\\b0[bB][0-1](_?[0-1])*n?\\b" },
      { begin: "\\b0[oO][0-7](_?[0-7])*n?\\b" },

      // LegacyOctalIntegerLiteral (does not include underscore separators)
      // https://tc39.es/ecma262/#sec-additional-syntax-numeric-literals
      { begin: "\\b0[0-7]+n?\\b" },
    ],
    relevance: 0
  };

  const SUBST = {
    className: 'subst',
    begin: '\\$\\{',
    end: '\\}',
    keywords: KEYWORDS,
    contains: [] // defined later
  };
  const HTML_TEMPLATE = {
    begin: 'html`',
    end: '',
    starts: {
      end: '`',
      returnEnd: false,
      contains: [
        hljs.BACKSLASH_ESCAPE,
        SUBST
      ],
      subLanguage: 'xml'
    }
  };
  const CSS_TEMPLATE = {
    begin: 'css`',
    end: '',
    starts: {
      end: '`',
      returnEnd: false,
      contains: [
        hljs.BACKSLASH_ESCAPE,
        SUBST
      ],
      subLanguage: 'css'
    }
  };
  const TEMPLATE_STRING = {
    className: 'string',
    begin: '`',
    end: '`',
    contains: [
      hljs.BACKSLASH_ESCAPE,
      SUBST
    ]
  };
  const JSDOC_COMMENT = hljs.COMMENT(
    '/\\*\\*',
    '\\*/',
    {
      relevance: 0,
      contains: [
        {
          className: 'doctag',
          begin: '@[A-Za-z]+',
          contains: [
            {
              className: 'type',
              begin: '\\{',
              end: '\\}',
              relevance: 0
            },
            {
              className: 'variable',
              begin: IDENT_RE + '(?=\\s*(-)|$)',
              endsParent: true,
              relevance: 0
            },
            // eat spaces (not newlines) so we can find
            // types or variables
            {
              begin: /(?=[^\n])\s/,
              relevance: 0
            }
          ]
        }
      ]
    }
  );
  const COMMENT = {
    className: "comment",
    variants: [
      JSDOC_COMMENT,
      hljs.C_BLOCK_COMMENT_MODE,
      hljs.C_LINE_COMMENT_MODE
    ]
  };
  const SUBST_INTERNALS = [
    hljs.APOS_STRING_MODE,
    hljs.QUOTE_STRING_MODE,
    HTML_TEMPLATE,
    CSS_TEMPLATE,
    TEMPLATE_STRING,
    NUMBER,
    hljs.REGEXP_MODE
  ];
  SUBST.contains = SUBST_INTERNALS
    .concat({
      // we need to pair up {} inside our subst to prevent
      // it from ending too early by matching another }
      begin: /{/,
      end: /}/,
      keywords: KEYWORDS,
      contains: [
        "self"
      ].concat(SUBST_INTERNALS)
    });
  const SUBST_AND_COMMENTS = [].concat(COMMENT, SUBST.contains);
  const PARAMS_CONTAINS = SUBST_AND_COMMENTS.concat([
    // eat recursive parens in sub expressions
    {
      begin: /\(/,
      end: /\)/,
      keywords: KEYWORDS,
      contains: ["self"].concat(SUBST_AND_COMMENTS)
    }
  ]);
  const PARAMS = {
    className: 'params',
    begin: /\(/,
    end: /\)/,
    excludeBegin: true,
    excludeEnd: true,
    keywords: KEYWORDS,
    contains: PARAMS_CONTAINS
  };

  return {
    name: 'Javascript',
    aliases: ['js', 'jsx', 'mjs', 'cjs'],
    keywords: KEYWORDS,
    // this will be extended by TypeScript
    exports: { PARAMS_CONTAINS },
    illegal: /#(?![$_A-z])/,
    contains: [
      hljs.SHEBANG({
        label: "shebang",
        binary: "node",
        relevance: 5
      }),
      {
        label: "use_strict",
        className: 'meta',
        relevance: 10,
        begin: /^\s*['"]use (strict|asm)['"]/
      },
      hljs.APOS_STRING_MODE,
      hljs.QUOTE_STRING_MODE,
      HTML_TEMPLATE,
      CSS_TEMPLATE,
      TEMPLATE_STRING,
      COMMENT,
      NUMBER,
      { // object attr container
        begin: regex.concat(/[{,\n]\s*/,
          // we need to look ahead to make sure that we actually have an
          // attribute coming up so we don't steal a comma from a potential
          // "value" container
          //
          // NOTE: this might not work how you think.  We don't actually always
          // enter this mode and stay.  Instead it might merely match `,
          // <comments up next>` and then immediately end after the , because it
          // fails to find any actual attrs. But this still does the job because
          // it prevents the value contain rule from grabbing this instead and
          // prevening this rule from firing when we actually DO have keys.
          regex.lookahead(regex.concat(
            // we also need to allow for multiple possible comments inbetween
            // the first key:value pairing
            /(\/\/.*$)*/,
            /(\/\*(.|\n)*\*\/)*/,
            /\s*/,
            IDENT_RE + '\\s*:'))),
        relevance: 0,
        contains: [
          {
            className: 'attr',
            begin: IDENT_RE + regex.lookahead('\\s*:'),
            relevance: 0
          }
        ]
      },
      { // "value" container
        begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*',
        keywords: 'return throw case',
        contains: [
          COMMENT,
          hljs.REGEXP_MODE,
          {
            className: 'function',
            // we have to count the parens to make sure we actually have the
            // correct bounding ( ) before the =>.  There could be any number of
            // sub-expressions inside also surrounded by parens.
            begin: '(\\(' +
            '[^()]*(\\(' +
            '[^()]*(\\(' +
            '[^()]*' +
            '\\))*[^()]*' +
            '\\))*[^()]*' +
            '\\)|' + hljs.UNDERSCORE_IDENT_RE + ')\\s*=>',
            returnBegin: true,
            end: '\\s*=>',
            contains: [
              {
                className: 'params',
                variants: [
                  {
                    begin: hljs.UNDERSCORE_IDENT_RE
                  },
                  {
                    className: null,
                    begin: /\(\s*\)/,
                    skip: true
                  },
                  {
                    begin: /\(/,
                    end: /\)/,
                    excludeBegin: true,
                    excludeEnd: true,
                    keywords: KEYWORDS,
                    contains: PARAMS_CONTAINS
                  }
                ]
              }
            ]
          },
          { // could be a comma delimited list of params to a function call
            begin: /,/, relevance: 0
          },
          {
            className: '',
            begin: /\s/,
            end: /\s*/,
            skip: true
          },
          { // JSX
            variants: [
              { begin: FRAGMENT.begin, end: FRAGMENT.end },
              {
                begin: XML_TAG.begin,
                // we carefully check the opening tag to see if it truly
                // is a tag and not a false positive
                'on:begin': XML_TAG.isTrulyOpeningTag,
                end: XML_TAG.end
              }
            ],
            subLanguage: 'xml',
            contains: [
              {
                begin: XML_TAG.begin,
                end: XML_TAG.end,
                skip: true,
                contains: ['self']
              }
            ]
          }
        ],
        relevance: 0
      },
      {
        className: 'function',
        beginKeywords: 'function',
        end: /[{;]/,
        excludeEnd: true,
        keywords: KEYWORDS,
        contains: [
          'self',
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
          PARAMS
        ],
        illegal: /%/
      },
      {
        className: 'function',
        // we have to count the parens to make sure we actually have the correct
        // bounding ( ).  There could be any number of sub-expressions inside
        // also surrounded by parens.
        begin: hljs.UNDERSCORE_IDENT_RE +
          '\\(' + // first parens
          '[^()]*(\\(' +
            '[^()]*(\\(' +
              '[^()]*' +
            '\\))*[^()]*' +
          '\\))*[^()]*' +
          '\\)\\s*{', // end parens
        returnBegin:true,
        contains: [
          PARAMS,
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
        ]
      },
      // hack: prevents detection of keywords in some circumstances
      // .keyword()
      // $keyword = x
      {
        variants: [
          { begin: '\\.' + IDENT_RE },
          { begin: '\\$' + IDENT_RE }
        ],
        relevance: 0
      },
      { // ES6 class
        className: 'class',
        beginKeywords: 'class',
        end: /[{;=]/,
        excludeEnd: true,
        illegal: /[:"\[\]]/,
        contains: [
          { beginKeywords: 'extends' },
          hljs.UNDERSCORE_TITLE_MODE
        ]
      },
      {
        begin: /\b(?=constructor)/,
        end: /[\{;]/,
        excludeEnd: true,
        contains: [
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
          'self',
          PARAMS
        ]
      },
      {
        begin: '(get|set)\\s+(?=' + IDENT_RE + '\\()',
        end: /{/,
        keywords: "get set",
        contains: [
          hljs.inherit(hljs.TITLE_MODE, { begin: IDENT_RE }),
          { begin: /\(\)/ }, // eat to avoid empty params
          PARAMS
        ]
      },
      {
        begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something`
      }
    ]
  };
}
<|endoftext|>


<commit_before>/* globals $ Backbone */
(function(window) {
  /**
   * Splits a string at the first occurrance of a character.
   */
  function splitAtFirst(string, char) {
    var index = string.indexOf(char);

    if (index === -1) {
      return [string];
    }

    return [string.slice(0, index + 1), string.slice(index)];
  }

  /**
   * Renders an error message when ETEngine threw an exception and returned a
   * backtrace. Highlights lines referring to queries and inputs.
   */
  var TraceView = Backbone.View.extend({
    events: { 'click .expand': 'expand' },

    expand: function() {
      this.options.expanded = true;
      this.$el.empty();
      this.render();
    },

    render: function() {
      var message = this.options.message;
      var split = splitAtFirst(message, '\n');

      if (split[0].match(/\/(present|future)/)) {
        var title = split[0].trim().split('/', 2);
        title[0] = '' + title[0] + '';
        split[0] = title.join('/');
      } else if (split[0].slice(0, 11) == 'SyntaxError') {
        split[0] = split[0].replace(/in (\w+)/g, 'in $1');
      }

      this.$el.append($('
').text(split[0]), this.renderTrace(split[1]));

      return this.$el;
    },

    renderTrace: function(trace) {
      if (!trace || trace.trim().length === 0) {
        return null;
      }

      // Query syntax errors separate the error title from the backtrace with a
      // pipe character.
      var split = trace.trim().split(/\n/);
      var expander = null;

      if (!this.options.expanded) {
        expander = $('').html('···');
        split = split.slice(0, 8);
      }

      // Highlight substring in the first backtrace lines which refer to queries
      // or inputs.
      split = split.map(function(line) {
        return line.replace(
          /(etsource\/(?:inputs|gqueries).*?):/g,
          '$1:'
        );
      });

      return $('
').append(
        $('
').html(split.join('\n')),
        expander
      );
    }
  });

  /**
   * View which hides the ETM interface and shows the user a message describing
   * the error which occurred.
   */
  var ApiErrorView = Backbone.View.extend({
    id: 'api-error',

    events: {
      'click .refresh': function(event) {
        event.preventDefault();
        window.location = window.location; // eslint-disable-line no-self-assign
      },
      'click .hide': function(event) {
        event.preventDefault();

        this.$el.remove();
        $('body').removeClass('has-api-error');
      }
    },

    render: function() {
      this.$el.append(
        $('
').append(
          $(''),
          ' ETEngine returned an error'
        )
      );

      this.$el.append(
        $('
').append(
          $('← Back to the home page'),
          $('Refresh and try again').attr(
            'href',
            location.pathname
          ),
          $('Hide this message').attr('href', '#'),
          $('View scenario in ETEngine').attr(
            'href',
            this.options.scenarioURL
          )
        )
      );

      if (
        this.options.status >= 400 &&
        this.options.status < 500 &&
        this.options.responseJSON
      ) {
        this.$el.append(this.renderJSONErrors());
      } else {
        this.$el.append(
          $('
').text(this.options.responseText)
        );
      }

      return this.$el;
    },

    renderJSONErrors: function() {
      return $('
').append(
        this.options.responseJSON.errors.map(this.renderJSONError)
      );
    },

    renderJSONError: function(message) {
      var li = $('
');

      if (message.indexOf('|') || message.indexOf('\n')) {
        li.append(new TraceView({ message: message }).render());
      } else {
        li.text(message);
      }

      return li;
    }
  });

  window.ApiErrorView = ApiErrorView;
})(window);
<commit_msg>ApiError: Improve HTML escaping<commit_after>/* globals $ Backbone */
(function(window) {
  /**
   * Splits a string at the first occurrance of a character.
   */
  function splitAtFirst(string, char) {
    var index = string.indexOf(char);

    if (index === -1) {
      return [string];
    }

    return [string.slice(0, index + 1), string.slice(index)];
  }

  /**
   * Renders an error message when ETEngine threw an exception and returned a
   * backtrace. Highlights lines referring to queries and inputs.
   */
  var TraceView = Backbone.View.extend({
    events: { 'click .expand': 'expand' },

    expand: function() {
      this.options.expanded = true;
      this.$el.empty();
      this.render();
    },

    render: function() {
      var message = this.options.message;
      var split = splitAtFirst(message, '\n');

      if (split[0].match(/\/(present|future)/)) {
        var title = split[0].trim().split('/');

        split[0] = $('').append(
          $('').text(title[0]),
          '/',
          $('').text(title.slice(1).join('/'))
        );
      } else if (split[0].slice(0, 11) == 'SyntaxError') {
        split[0] = split[0].replace(/in (\w+)/g, 'in $1');
      }

      this.$el.append($('
').html(split[0]), this.renderTrace(split[1]));

      return this.$el;
    },

    renderTrace: function(trace) {
      if (!trace || trace.trim().length === 0) {
        return null;
      }

      // Query syntax errors separate the error title from the backtrace with a
      // pipe character.
      var split = trace.trim().split(/\n/);
      var expander = null;

      if (!this.options.expanded) {
        expander = $('').html('···');
        split = split.slice(0, 8);
      }

      // Highlight substring in the first backtrace lines which refer to queries
      // or inputs.
      split = split.map(function(line) {
        return line.replace(
          /(etsource\/(?:inputs|gqueries).*?):/g,
          '$1:'
        );
      });

      return $('
').append(
        $('
').html(split.join('\n')),
        expander
      );
    }
  });

  /**
   * View which hides the ETM interface and shows the user a message describing
   * the error which occurred.
   */
  var ApiErrorView = Backbone.View.extend({
    id: 'api-error',

    events: {
      'click .refresh': function(event) {
        event.preventDefault();
        window.location = window.location; // eslint-disable-line no-self-assign
      },
      'click .hide': function(event) {
        event.preventDefault();

        this.$el.remove();
        $('body').removeClass('has-api-error');
      }
    },

    render: function() {
      this.$el.append(
        $('
').append(
          $(''),
          ' ETEngine returned an error'
        )
      );

      this.$el.append(
        $('
').append(
          $('← Back to the home page'),
          $('Refresh and try again').attr(
            'href',
            location.pathname
          ),
          $('Hide this message').attr('href', '#'),
          $('View scenario in ETEngine').attr(
            'href',
            this.options.scenarioURL
          )
        )
      );

      if (
        this.options.status >= 400 &&
        this.options.status < 500 &&
        this.options.responseJSON
      ) {
        this.$el.append(this.renderJSONErrors());
      } else {
        this.$el.append(
          $('
').text(this.options.responseText)
        );
      }

      return this.$el;
    },

    renderJSONErrors: function() {
      return $('
').append(
        this.options.responseJSON.errors.map(this.renderJSONError)
      );
    },

    renderJSONError: function(message) {
      var li = $('
');

      if (message.indexOf('|') || message.indexOf('\n')) {
        li.append(new TraceView({ message: message }).render());
      } else {
        li.text(message);
      }

      return li;
    }
  });

  window.ApiErrorView = ApiErrorView;
})(window);
<|endoftext|>


<commit_before>import Diff from 'fast-diff'
import debounce from 'lodash.debounce'
import peerColor from './peer-color'

const DEBOUNCE_CUSOR_ACTIVITY_MS = 2000

const bindCodeMirror = (doc, titleEditor, editor) => {
  const thisPeerId = doc.app.ipfs._peerInfo.id.toB58String()
  let cursorGossip
  let titleCollab
  let initialised = false
  let editorLocked = false
  let markers = new Map()

  const onCodeMirrorChange = (editor, change) => {
    if (!initialised || editorLocked) {
      return
    }

    editorLocked = true

    const diffs = Diff(doc.shared.value().join(''), editor.getValue())

    let pos = 0
    diffs.forEach((d) => {
      if (d[0] === 0) { // EQUAL
        pos += d[1].length
      } else if (d[0] === -1) { // DELETE
        const delText = d[1]
        for (let i = delText.length - 1; i >=0; i--) {
          try {
            doc.shared.removeAt(pos + i)
          } catch (err) {
            console.error(err)
            onStateChanged()
          }
        }
      } else { // INSERT
        d[1].split('').forEach((c) => {
          doc.shared.insertAt(pos, c)
          pos ++
        })
      }
    })

    editorLocked = false
  }

  editor.on('change', onCodeMirrorChange)

  const onStateChanged = (fromSelf) => {
    if (fromSelf || editorLocked) {
      return
    }

    const oldText = editor.getValue()
    const newText = doc.shared.value().join('')

    if (oldText === newText) {
      return
    }
    editorLocked = true

    const cursor = editor.getCursor()
    let cursorPos = editor.indexFromPos(cursor)

    const diffs = Diff(oldText, newText)
    let pos = 0
    diffs.forEach((d) => {
      const [op, text] = d
      if (op === 0) { // EQUAL
        pos += text.length
      } else if (op === -1) { // DELETE
        if (text.length) {
          const fromPos = editor.posFromIndex(pos)
          fromPos.external = true
          const toPos = editor.posFromIndex(pos + text.length)
          toPos.external = true
          editor.replaceRange('', fromPos, toPos)

          if (pos < cursorPos) {
            cursorPos -= text.length
          }

          moveMarkersIfAfter(pos, -text.length)
        }
      } else { // INSERT
        if (text.length) {
          const fromPos = editor.posFromIndex(pos)
          fromPos.external = true
          editor.replaceRange(text, fromPos)

          if (pos < cursorPos) {
            cursorPos += text.length
          }
          moveMarkersIfAfter(pos, text.length)
        }
      }
    })
    editor.setCursor(editor.posFromIndex(cursorPos))
    editorLocked = false
  }

  doc.on('state changed', onStateChanged)

  editor.setValue(doc.shared.value().join(''))

  const onTitleStateChanged = () => {
    const oldTitle = titleEditor.value
    const newTitle = titleCollab.shared.value().join('')
    if (newTitle === oldTitle) {
      return
    }

    titleEditor.value = newTitle
  }

  doc.sub('title', 'rga').then((_titleCollab) => {
    titleCollab = _titleCollab
    titleCollab.on('state changed', onTitleStateChanged)
    const title = titleCollab.shared.value().join('')
    titleEditor.value = title
  })

  const onTitleEditorChanged = () => {
    if (!titleCollab) {
      return
    }

    const oldTitle = titleCollab.shared.value().join('')
    const newTitle = titleEditor.value

    const diffs = Diff(oldTitle, newTitle)

    let pos = 0
    diffs.forEach((d) => {
      if (d[0] === 0) { // EQUAL
        pos += d[1].length
      } else if (d[0] === -1) { // DELETE
        const delText = d[1]
        for (let i = delText.length - 1; i >=0; i--) {
          try {
            titleCollab.shared.removeAt(pos + i)
          } catch (err) {
            console.error(err)
            onStateChanged()
          }
        }
      } else { // INSERT
        d[1].split('').forEach((c) => {
          titleCollab.shared.insertAt(pos, c)
          pos ++
        })
      }
    })
  }

  titleEditor.addEventListener('input', onTitleEditorChanged)

  const onCursorGossipMessage = (cursor, fromPeerId) => {
    if (fromPeerId === thisPeerId) {
      return
    }

    const previousMarkers = markers.get(fromPeerId)
    if (previousMarkers) {
      previousMarkers.forEach((marker) => marker.clear())
    }

    const color = peerColor(fromPeerId)

    const [head, fromPos, toPos] = cursor

    const widget = getCursorWidget(head, color)

    const bookmark = editor.setBookmark(head, { widget })
    const range = editor.markText(fromPos, toPos, {
      css: `background-color: ${color}; opacity: 0.8`,
      title: fromPeerId
    })
    markers.set(fromPeerId, [bookmark, range])
  }

  doc.gossip('cursors').then((_cursorGossip) => {
    cursorGossip = _cursorGossip
    cursorGossip.on('message', onCursorGossipMessage)
  })

  const onEditorCursorActivity = () => {
    if (cursorGossip) {
      const cursor = [
        editor.getCursor('head'),
        editor.getCursor('from'),
        editor.getCursor('to')]
      cursorGossip.broadcast(cursor)
    }
  }

  const onEditorCursorActivityDebounced = debounce(onEditorCursorActivity, DEBOUNCE_CUSOR_ACTIVITY_MS)
  editor.on('cursorActivity', onEditorCursorActivityDebounced)

  initialised = true

  return () => {
    // unbind
    doc.removeListener('state changed', onStateChanged)
    editor.off('change', onCodeMirrorChange)
    titleEditor.removeEventListener('input', onTitleEditorChanged)
    if (titleCollab) {
      titleCollab.removeListener('state changed', onTitleStateChanged)
    }
    editor.off('cursorActivity', onEditorCursorActivityDebounced)
    if (cursorGossip) {
      cursorGossip.removeListener('message', onCursorGossipMessage)
    }
  }

  function getCursorWidget (cursorPos, color) {
    const cursorCoords = editor.cursorCoords(cursorPos)
    const cursorElement = document.createElement('span')
    cursorElement.style.borderLeftStyle = 'solid'
    cursorElement.style.borderLeftWidth = '2px'
    cursorElement.style.borderLeftColor = color
    cursorElement.style.height = `${(cursorCoords.bottom - cursorCoords.top)}px`
    cursorElement.style.padding = 0
    cursorElement.style.zIndex = 0

    return cursorElement
  }

  function moveMarkersIfAfter (pos, diff) {
    for (let peer of markers.keys()) {
      const peerMarkers = markers.get(peer)
      moveMarkerIfAfter(peer, peerMarkers, pos, diff)
    }
  }

  function moveMarkerIfAfter (peer, peerMarkers, changePos, diff) {
    peerMarkers.forEach((marker, index) => {
      const markerPos = marker.find()
      if (markerPos) {
        const posIndex = editor.indexFromPos(markerPos)
        if (posIndex >= changePos) {
          marker.clear()
        }
      } else {
        marker.clear()
      }
    })

    markers.delete(peer)
  }

//   function moveMarkerIfAfterBuggy (peer, peerMarkers, changePos, diff) {
//     let pos
//     let posIndex
//     let newMarkers = []
//     peerMarkers.forEach((marker, index) => {
//       const markerPos = marker.find()
//       if (markerPos) {
//         console.log('marker:', marker)
//         pos = markerPos
//         posIndex = editor.indexFromPos(pos)
//         if (posIndex >= changePos) {
//           peerMarkers.splice(index, 1)
//           marker.clear()
//           posIndex += diff
//           const newPos = editor.posFromIndex(posIndex)
//           const color = peerColor(peer)
//           const widget = getCursorWidget(newPos, color)
//           const bookmark = editor.setBookmark(newPos, { widget })
//           newMarkers.push(bookmark)
//         }
//       } else {
//         newMarkers.push(marker)
//       }
//     })

//     markers.set(peer, newMarkers)
//   }
}

export default (doc, title, editor, type) => {
  if (type === 'markdown' || type === 'math') {
    return bindCodeMirror(doc, title, editor)
  }

  throw new Error('unsupported type ' + type)
}

<commit_msg>fix: rechecking whether state is in sync with editor<commit_after>import Diff from 'fast-diff'
import debounce from 'lodash.debounce'
import peerColor from './peer-color'

const DEBOUNCE_CUSOR_ACTIVITY_MS = 2000

const bindCodeMirror = (doc, titleEditor, editor) => {
  const thisPeerId = doc.app.ipfs._peerInfo.id.toB58String()
  let cursorGossip
  let titleCollab
  let initialised = false
  let editorLocked = false
  let markers = new Map()

  const onCodeMirrorChange = (editor, change) => {
    if (!initialised || editorLocked) {
      return
    }

    editorLocked = true

    const diffs = Diff(doc.shared.value().join(''), editor.getValue())

    let pos = 0
    diffs.forEach((d) => {
      if (d[0] === 0) { // EQUAL
        pos += d[1].length
      } else if (d[0] === -1) { // DELETE
        const delText = d[1]
        for (let i = delText.length - 1; i >=0; i--) {
          try {
            doc.shared.removeAt(pos + i)
          } catch (err) {
            console.error(err)
            onStateChanged()
          }
        }
      } else { // INSERT
        d[1].split('').forEach((c) => {
          doc.shared.insertAt(pos, c)
          pos ++
        })
      }
    })

    editorLocked = false
  }

  editor.on('change', onCodeMirrorChange)

  const onStateChanged = (fromSelf) => {
    if (editorLocked) {
      return
    }

    let oldText = editor.getValue()
    let newText = doc.shared.value().join('')

    if (oldText === newText) {
      return
    }
    editorLocked = true

    const cursor = editor.getCursor()
    let cursorPos = editor.indexFromPos(cursor)

    const diffs = Diff(oldText, newText)
    let pos = 0
    diffs.forEach((d) => {
      const [op, text] = d
      if (op === 0) { // EQUAL
        pos += text.length
      } else if (op === -1) { // DELETE
        if (text.length) {
          const fromPos = editor.posFromIndex(pos)
          fromPos.external = true
          const toPos = editor.posFromIndex(pos + text.length)
          toPos.external = true
          editor.replaceRange('', fromPos, toPos)

          if (pos < cursorPos) {
            cursorPos -= text.length
          }

          moveMarkersIfAfter(pos, -text.length)
        }
      } else { // INSERT
        if (text.length) {
          const fromPos = editor.posFromIndex(pos)
          fromPos.external = true
          editor.replaceRange(text, fromPos)

          if (pos < cursorPos) {
            cursorPos += text.length
          }
          moveMarkersIfAfter(pos, text.length)
        }
      }
    })
    editor.setCursor(editor.posFromIndex(cursorPos))
    editorLocked = false

    oldText = editor.getValue()
    newText = doc.shared.value().join('')

    if (oldText !== newText) {
      onStateChanged(fromSelf)
    }
  }

  doc.on('state changed', onStateChanged)

  editor.setValue(doc.shared.value().join(''))

  const onTitleStateChanged = () => {
    const oldTitle = titleEditor.value
    const newTitle = titleCollab.shared.value().join('')
    if (newTitle === oldTitle) {
      return
    }

    titleEditor.value = newTitle
  }

  doc.sub('title', 'rga').then((_titleCollab) => {
    titleCollab = _titleCollab
    titleCollab.on('state changed', onTitleStateChanged)
    const title = titleCollab.shared.value().join('')
    titleEditor.value = title
  })

  const onTitleEditorChanged = () => {
    if (!titleCollab) {
      return
    }

    const oldTitle = titleCollab.shared.value().join('')
    const newTitle = titleEditor.value

    const diffs = Diff(oldTitle, newTitle)

    let pos = 0
    diffs.forEach((d) => {
      if (d[0] === 0) { // EQUAL
        pos += d[1].length
      } else if (d[0] === -1) { // DELETE
        const delText = d[1]
        for (let i = delText.length - 1; i >=0; i--) {
          try {
            titleCollab.shared.removeAt(pos + i)
          } catch (err) {
            console.error(err)
            onStateChanged()
          }
        }
      } else { // INSERT
        d[1].split('').forEach((c) => {
          titleCollab.shared.insertAt(pos, c)
          pos ++
        })
      }
    })
  }

  titleEditor.addEventListener('input', onTitleEditorChanged)

  const onCursorGossipMessage = (cursor, fromPeerId) => {
    if (fromPeerId === thisPeerId) {
      return
    }

    const previousMarkers = markers.get(fromPeerId)
    if (previousMarkers) {
      previousMarkers.forEach((marker) => marker.clear())
    }

    const color = peerColor(fromPeerId)

    const [head, fromPos, toPos] = cursor

    const widget = getCursorWidget(head, color)

    const bookmark = editor.setBookmark(head, { widget })
    const range = editor.markText(fromPos, toPos, {
      css: `background-color: ${color}; opacity: 0.8`,
      title: fromPeerId
    })
    markers.set(fromPeerId, [bookmark, range])
  }

  doc.gossip('cursors').then((_cursorGossip) => {
    cursorGossip = _cursorGossip
    cursorGossip.on('message', onCursorGossipMessage)
  })

  const onEditorCursorActivity = () => {
    if (cursorGossip) {
      const cursor = [
        editor.getCursor('head'),
        editor.getCursor('from'),
        editor.getCursor('to')]
      cursorGossip.broadcast(cursor)
    }
  }

  const onEditorCursorActivityDebounced = debounce(onEditorCursorActivity, DEBOUNCE_CUSOR_ACTIVITY_MS)
  editor.on('cursorActivity', onEditorCursorActivityDebounced)

  initialised = true

  return () => {
    // unbind
    doc.removeListener('state changed', onStateChanged)
    editor.off('change', onCodeMirrorChange)
    titleEditor.removeEventListener('input', onTitleEditorChanged)
    if (titleCollab) {
      titleCollab.removeListener('state changed', onTitleStateChanged)
    }
    editor.off('cursorActivity', onEditorCursorActivityDebounced)
    if (cursorGossip) {
      cursorGossip.removeListener('message', onCursorGossipMessage)
    }
  }

  function getCursorWidget (cursorPos, color) {
    const cursorCoords = editor.cursorCoords(cursorPos)
    const cursorElement = document.createElement('span')
    cursorElement.style.borderLeftStyle = 'solid'
    cursorElement.style.borderLeftWidth = '2px'
    cursorElement.style.borderLeftColor = color
    cursorElement.style.height = `${(cursorCoords.bottom - cursorCoords.top)}px`
    cursorElement.style.padding = 0
    cursorElement.style.zIndex = 0

    return cursorElement
  }

  function moveMarkersIfAfter (pos, diff) {
    for (let peer of markers.keys()) {
      const peerMarkers = markers.get(peer)
      moveMarkerIfAfter(peer, peerMarkers, pos, diff)
    }
  }

  function moveMarkerIfAfter (peer, peerMarkers, changePos, diff) {
    peerMarkers.forEach((marker, index) => {
      const markerPos = marker.find()
      if (markerPos) {
        const posIndex = editor.indexFromPos(markerPos)
        if (posIndex >= changePos) {
          marker.clear()
        }
      } else {
        marker.clear()
      }
    })

    markers.delete(peer)
  }

//   function moveMarkerIfAfterBuggy (peer, peerMarkers, changePos, diff) {
//     let pos
//     let posIndex
//     let newMarkers = []
//     peerMarkers.forEach((marker, index) => {
//       const markerPos = marker.find()
//       if (markerPos) {
//         console.log('marker:', marker)
//         pos = markerPos
//         posIndex = editor.indexFromPos(pos)
//         if (posIndex >= changePos) {
//           peerMarkers.splice(index, 1)
//           marker.clear()
//           posIndex += diff
//           const newPos = editor.posFromIndex(posIndex)
//           const color = peerColor(peer)
//           const widget = getCursorWidget(newPos, color)
//           const bookmark = editor.setBookmark(newPos, { widget })
//           newMarkers.push(bookmark)
//         }
//       } else {
//         newMarkers.push(marker)
//       }
//     })

//     markers.set(peer, newMarkers)
//   }
}

export default (doc, title, editor, type) => {
  if (type === 'markdown' || type === 'math') {
    return bindCodeMirror(doc, title, editor)
  }

  throw new Error('unsupported type ' + type)
}

<|endoftext|>