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

/**
  Slots are observable values which can change over time.

  Slots are simple single-value Model-View-Controller Models, but since
  another meaning of 'Model' is already heavily used in FOAM, Slot is
  used to avoid overloading the term.

Types of Slots include:

PropertySlot

ConstantSlot

ExpressionSlot

*/
foam.CLASS({
  package: 'foam.core',
  name: 'Slot', // ???: Rename AbstractSlot or make an Interface

  methods: [
    /**
      Subscribe to the Slot's value, if it has one. If the Slot's
      value changes, then unsubscribe from the previous value and
      resubscribe to the new one.
    */
    function valueSub() {
      var self = this;
      var args = Array.from(arguments);
      var s;
      var l = function() {
        var v = self.get();
        if ( s ) s.destroy();
        if ( v ) s = v.sub.apply(v, args);
      };
      l();
      this.sub(l);
    },

    /**
      Create a sub-Slot for this Slot's value. If this Slot's
      value changes, then the sub-Slot becomes the Slot for
      the new value's sub-Slot instead. Useful for creating
      Slot paths without having to rebuild whenever a value
      along the chain changes.
    */
    function dot(name) {
      return foam.core.internal.SubSlot.create({
        parent: this,
        name:   name
      });
    },

    // TODO: remove when all code ported
    function link(other) {
      console.warn('deprecated use of link(), use linkFrom() instead');
      return this.linkFrom(other);
    },

    /**
      Link two Slots together, setting both to other's value.
      Returns a Destroyable which can be used to break the link.
      After copying a value from one slot to the other, this implementation
      then copies the value back in case the target slot rejected the value.
    */
    function linkFrom(s2) {
      var s1        = this;
      var feedback1 = false, feedback2 = false;

      // TODO: once all slot types property set 'src', these
      // two listeneners can be merged.
      var l1 = function(e) {
        if ( feedback1 ) return;

        if ( ! foam.util.equals(s1.get(), s2.get()) ) {
          feedback1 = true;
          s2.set(s1.get());
          if ( ! foam.util.equals(s1.get(), s2.get()) )
            s1.set(s2.get());
          feedback1 = false;
        }
      };

      var l2 = function(e) {
        if ( feedback2 ) return;

        if ( ! foam.util.equals(s1.get(), s2.get()) ) {
          feedback2 = true;
          s1.set(s2.get());
          if ( ! foam.util.equals(s1.get(), s2.get()) )
            s2.set(s1.get());
          feedback2 = false;
        }
      };

      var sub1 = s1.sub(l1);
      var sub2 = s2.sub(l2)

      l2();

      return {
        destroy: function() {
          sub1 && sub1.destroy();
          sub2 && sub2.destroy();
          sub1 = sub2 = null;
        }
      };
    },

    function linkTo(other) {
      return other.linkFrom(this);
    },

    /**
      Have this Slot dynamically follow other's value.
      Returns a Destroyable which can be used to cancel the binding.
    */
    function follow(other) {
      console.assert(other, 'Slot.follow requires Slot argument.');
      var self = this;
      var l = function() {
        if ( ! foam.util.equals(self.get(), other.get()) ) {
          self.set(other.get());
        }
      };
      l();
      return other.sub(l);
    },

    /**
     * Maps values from one model to another.
     * @param f maps values from srcValue to dstValue
     */
    function mapFrom(other, f) {
      var self = this;
      var l = function() { self.set(f(other.get())); };
      l();
      return other.sub(l);
    },

    function mapTo(other, f) {
      return other.mapFrom(this, f);
    },

    function map(f) {
      return foam.core.ExpressionSlot.create({code: f, args: [this]});
    },

    /**
     * Relate to another Slot.
     * @param f maps from this to other
     * @param fprime maps other to this
     */
    function relateTo(other, f, fPrime) {
      var self     = this;
      var feedback = false;
      var sub      = foam.core.FObject.create();
      var l1 = function() {
        if ( feedback ) return;
        feedback = true;
        other.set(f(self.get()));
        feedback = false;
      };
      var l2 = function() {
        if ( feedback ) return;
        feedback = true;
        self.set(fPrime(other.get()));
        feedback = false;
      };

      sub.onDestroy(this.sub(l1));
      sub.onDestroy(other.sub(l2));

      l1();

      return sub;
    },

    function relateFrom(other, f, fPrime) {
      return other.relateTo(this, fPrime, f);
    }
  ]
});

/**
  PropertySlot represents object properties as Slots.
  Created with calling obj.prop$ or obj.slot('prop').
  For internal use only.
 */
foam.CLASS({
  package: 'foam.core.internal',
  name: 'PropertySlot',
  extends: 'foam.core.Slot',

  methods: [
    function initArgs() { },
    function init() { },

    function get() {
      return this.prop.get(this.obj);
    },

    function set(value) {
      return this.prop.set(this.obj, value);
    },

    function getPrev() {
      return this.oldValue;
    },

    function setPrev(value) {
      return this.oldValue = value;
    },

    function sub(l) {
      var s = this.obj.sub('propertyChange', this.prop.name, l);
      s.src = this;
      return s;
    },

    function isDefined() {
      return this.obj.hasOwnProperty(this.prop.name);
    },

    function clear() {
      this.obj.clearProperty(this.prop.name);
    },

    function toString() {
      return 'PropertySlot(' + this.obj.cls_.id + '.' + this.prop.name + ')';
    }
  ]
});

foam.CLASS({
  package: 'foam.core.internal',
  name: 'SubSlot',
  implements: [ 'foam.core.Slot' ],

  documentation:
      'For internal use only. Is used to implement the Slot.dot() method.',

  properties: [
    'of',
    'parent', // parent slot, not parent object
    'name',
    'value',
    'prevSub'
  ],

  methods: [
    function init() {
      this.parent.sub(this.parentChange);
      this.parentChange();
    },

    function get() {
      var o = this.parent.get();

      return o && o[this.name];
    },

    function set(value) {
      var o = this.parent.get();

      if ( o ) o[this.name] = value;
    },

    /** Needed? **/
    function getPrev() {
      debugger;
      return this.oldValue;
    },

    /** Needed? **/
    function setPrev(value) {
      debugger;
      return this.oldValue = value;
    },

    function sub(l) {
      return this.SUPER('propertyChange', 'value', l);
    },

    function isDefined() {
      return this.parent.get().hasOwnProperty(this.name);
    },

    function clear() {
      this.parent.get().clearProperty(this.name);
    },

    function toString() {
      return 'SubSlot(' + this.parent + ',' + this.name + ')';
    }
  ],

  listeners: [
    function parentChange() {
      this.prevSub && this.prevSub.destroy();
      var o = this.parent.get();

      // If the parent object changes class, then don't
      // update because a new class will have different
      // sub-slots.
      if ( this.of ) {
        if ( ! this.of.isInstance(o) ) return;
      } else {
        if ( o ) this.of = o.cls_;
      }

      this.prevSub = o && o.sub('propertyChange', this.name, this.valueChange);
      this.valueChange();
    },

    function valueChange() {
      var parentValue = this.parent.get();
      this.value = parentValue ? parentValue[this.name] : undefined;
    }
  ]
});

/** An immutable constant valued Slot. */
foam.CLASS({
  package: 'foam.core',
  name: 'ConstantSlot',

  implements: [ 'foam.core.Slot' ],

  properties: [
    {
      name: 'value',
      getter: function() { return this.value_; },
      setter: function() {}
    }
  ],

  methods: [
    function initArgs(args) { this.value_ = args && args.value; },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) { /* nop */ }
  ]
});

/**
  Tracks dependencies for a dynamic function and invalidates if they change.

  foam.CLASS({name: 'Person', properties: ['fname', 'lname']});
  var p = Person.create({fname: 'John', lname: 'Smith'});
  var e = foam.core.ExpressionSlot.create({
    args: [ p.fname$, p.lname$ ],
    code: function(f, l) { return f + ' ' + l; }
  });
  log(e.get());
  e.sub(log);
  p.fname = 'Steve';
  p.lname = 'Jones';
  log(e.get());

  Output:
   > John Smith
   > [object Object] propertyChange value [object Object]
   > [object Object] propertyChange value [object Object]
   > Steve Jones

  var p = foam.CLASS({name: 'Person', properties: [ 'f', 'l' ]}).create({f:'John', l: 'Doe'});
  var e = foam.core.ExpressionSlot.create({
    obj: p,
    code: function(f, l) { return f + ' ' + l; }
  });

*/
foam.CLASS({
  package: 'foam.core',
  name: 'ExpressionSlot',
  implements: [ 'foam.core.Slot' ],

  properties: [
    'obj',
    'code',
    {
      name: 'args',
      expression: function(obj) {
        this.assert(obj, 'ExpressionSlot: "obj" or "args" required.');

        var args = foam.Function.formalArgs(this.code);
        for ( var i = 0 ; i < args.length ; i++ ) {
          args[i] = obj.slot(args[i]);
        }

        // this.invalidate(); // ???: Is this needed?
        this.subToArgs_(args);

        return args;
      },
      postSet: function(_, args) {
        this.subToArgs_(args);
      }
    },
    {
      name: 'value',
      factory: function() {
        return this.code.apply(this.obj || this, this.args.map(function(a) {
          return a.get();
        }));
      }
    },
    'cleanup_', // destroyable to cleanup old subs when obj changes
  ],

  methods: [
    function init() { this.onDestroy(this.cleanup); },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) {
      return arguments.length === 1 ?
        this.SUPER('propertyChange', 'value', l) :
        this.SUPER.apply(this,arguments);
    },

    function subToArgs_(args) {
      this.cleanup();

      var cleanup = foam.core.FObject.create();

      for ( var i = 0 ; i < args.length ; i++ ) {
        cleanup.onDestroy(args[i].sub(this.invalidate));
      }

      this.cleanup_ = cleanup;
    }
  ],

  listeners: [
    function cleanup() { this.cleanup_ && this.cleanup_.destroy(); },
    function invalidate() { this.clearProperty('value'); }
  ]
});
<commit_msg>SubSlot unsubscribes if parent class changes. Fixes memory leak.<commit_after>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
  Slots are observable values which can change over time.

  Slots are simple single-value Model-View-Controller Models, but since
  another meaning of 'Model' is already heavily used in FOAM, Slot is
  used to avoid overloading the term.

Types of Slots include:

PropertySlot

ConstantSlot

ExpressionSlot

*/
foam.CLASS({
  package: 'foam.core',
  name: 'Slot', // ???: Rename AbstractSlot or make an Interface

  methods: [
    /**
      Subscribe to the Slot's value, if it has one. If the Slot's
      value changes, then unsubscribe from the previous value and
      resubscribe to the new one.
    */
    function valueSub() {
      var self = this;
      var args = Array.from(arguments);
      var s;
      var l = function() {
        var v = self.get();
        if ( s ) s.destroy();
        if ( v ) s = v.sub.apply(v, args);
      };
      l();
      this.sub(l);
    },

    /**
      Create a sub-Slot for this Slot's value. If this Slot's
      value changes, then the sub-Slot becomes the Slot for
      the new value's sub-Slot instead. Useful for creating
      Slot paths without having to rebuild whenever a value
      along the chain changes.
    */
    function dot(name) {
      return foam.core.internal.SubSlot.create({
        parent: this,
        name:   name
      });
    },

    // TODO: remove when all code ported
    function link(other) {
      console.warn('deprecated use of link(), use linkFrom() instead');
      return this.linkFrom(other);
    },

    /**
      Link two Slots together, setting both to other's value.
      Returns a Destroyable which can be used to break the link.
      After copying a value from one slot to the other, this implementation
      then copies the value back in case the target slot rejected the value.
    */
    function linkFrom(s2) {
      var s1        = this;
      var feedback1 = false, feedback2 = false;

      // TODO: once all slot types property set 'src', these
      // two listeneners can be merged.
      var l1 = function(e) {
        if ( feedback1 ) return;

        if ( ! foam.util.equals(s1.get(), s2.get()) ) {
          feedback1 = true;
          s2.set(s1.get());
          if ( ! foam.util.equals(s1.get(), s2.get()) )
            s1.set(s2.get());
          feedback1 = false;
        }
      };

      var l2 = function(e) {
        if ( feedback2 ) return;

        if ( ! foam.util.equals(s1.get(), s2.get()) ) {
          feedback2 = true;
          s1.set(s2.get());
          if ( ! foam.util.equals(s1.get(), s2.get()) )
            s2.set(s1.get());
          feedback2 = false;
        }
      };

      var sub1 = s1.sub(l1);
      var sub2 = s2.sub(l2)

      l2();

      return {
        destroy: function() {
          sub1 && sub1.destroy();
          sub2 && sub2.destroy();
          sub1 = sub2 = null;
        }
      };
    },

    function linkTo(other) {
      return other.linkFrom(this);
    },

    /**
      Have this Slot dynamically follow other's value.
      Returns a Destroyable which can be used to cancel the binding.
    */
    function follow(other) {
      console.assert(other, 'Slot.follow requires Slot argument.');
      var self = this;
      var l = function() {
        if ( ! foam.util.equals(self.get(), other.get()) ) {
          self.set(other.get());
        }
      };
      l();
      return other.sub(l);
    },

    /**
     * Maps values from one model to another.
     * @param f maps values from srcValue to dstValue
     */
    function mapFrom(other, f) {
      var self = this;
      var l = function() { self.set(f(other.get())); };
      l();
      return other.sub(l);
    },

    function mapTo(other, f) {
      return other.mapFrom(this, f);
    },

    function map(f) {
      return foam.core.ExpressionSlot.create({code: f, args: [this]});
    },

    /**
     * Relate to another Slot.
     * @param f maps from this to other
     * @param fprime maps other to this
     */
    function relateTo(other, f, fPrime) {
      var self     = this;
      var feedback = false;
      var sub      = foam.core.FObject.create();
      var l1 = function() {
        if ( feedback ) return;
        feedback = true;
        other.set(f(self.get()));
        feedback = false;
      };
      var l2 = function() {
        if ( feedback ) return;
        feedback = true;
        self.set(fPrime(other.get()));
        feedback = false;
      };

      sub.onDestroy(this.sub(l1));
      sub.onDestroy(other.sub(l2));

      l1();

      return sub;
    },

    function relateFrom(other, f, fPrime) {
      return other.relateTo(this, fPrime, f);
    }
  ]
});

/**
  PropertySlot represents object properties as Slots.
  Created with calling obj.prop$ or obj.slot('prop').
  For internal use only.
 */
foam.CLASS({
  package: 'foam.core.internal',
  name: 'PropertySlot',
  extends: 'foam.core.Slot',

  methods: [
    function initArgs() { },
    function init() { },

    function get() {
      return this.prop.get(this.obj);
    },

    function set(value) {
      return this.prop.set(this.obj, value);
    },

    function getPrev() {
      return this.oldValue;
    },

    function setPrev(value) {
      return this.oldValue = value;
    },

    function sub(l) {
      var s = this.obj.sub('propertyChange', this.prop.name, l);
      s.src = this;
      return s;
    },

    function isDefined() {
      return this.obj.hasOwnProperty(this.prop.name);
    },

    function clear() {
      this.obj.clearProperty(this.prop.name);
    },

    function toString() {
      return 'PropertySlot(' + this.obj.cls_.id + '.' + this.prop.name + ')';
    }
  ]
});

foam.CLASS({
  package: 'foam.core.internal',
  name: 'SubSlot',
  implements: [ 'foam.core.Slot' ],

  documentation:
      'For internal use only. Is used to implement the Slot.dot() method.',

  properties: [
    'of',
    'parent', // parent slot, not parent object
    'name',
    'value',
    'prevSub'
  ],

  methods: [
    function init() {
      this.parent.sub(this.parentChange);
      this.parentChange();
    },

    function get() {
      var o = this.parent.get();

      return o && o[this.name];
    },

    function set(value) {
      var o = this.parent.get();

      if ( o ) o[this.name] = value;
    },

    /** Needed? **/
    function getPrev() {
      debugger;
      return this.oldValue;
    },

    /** Needed? **/
    function setPrev(value) {
      debugger;
      return this.oldValue = value;
    },

    function sub(l) {
      return this.SUPER('propertyChange', 'value', l);
    },

    function isDefined() {
      return this.parent.get().hasOwnProperty(this.name);
    },

    function clear() {
      this.parent.get().clearProperty(this.name);
    },

    function toString() {
      return 'SubSlot(' + this.parent + ',' + this.name + ')';
    }
  ],

  listeners: [
    function parentChange(s) {
      this.prevSub && this.prevSub.destroy();
      var o = this.parent.get();

      // If the parent object changes class, then don't update
      // because a new class will have different sub-slots.
      if ( this.of ) {
        if ( this.of !== ( o && o.cls_ ) ) {
          s.destroy();
          return;
        }
      } else {
        if ( o ) this.of = o.cls_;
      }

      this.prevSub = o && o.sub('propertyChange', this.name, this.valueChange);
      this.valueChange();
    },

    function valueChange() {
      var parentValue = this.parent.get();
      this.value = parentValue ? parentValue[this.name] : undefined;
    }
  ]
});

/** An immutable constant valued Slot. */
foam.CLASS({
  package: 'foam.core',
  name: 'ConstantSlot',

  implements: [ 'foam.core.Slot' ],

  properties: [
    {
      name: 'value',
      getter: function() { return this.value_; },
      setter: function() {}
    }
  ],

  methods: [
    function initArgs(args) { this.value_ = args && args.value; },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) { /* nop */ }
  ]
});

/**
  Tracks dependencies for a dynamic function and invalidates if they change.

  foam.CLASS({name: 'Person', properties: ['fname', 'lname']});
  var p = Person.create({fname: 'John', lname: 'Smith'});
  var e = foam.core.ExpressionSlot.create({
    args: [ p.fname$, p.lname$ ],
    code: function(f, l) { return f + ' ' + l; }
  });
  log(e.get());
  e.sub(log);
  p.fname = 'Steve';
  p.lname = 'Jones';
  log(e.get());

  Output:
   > John Smith
   > [object Object] propertyChange value [object Object]
   > [object Object] propertyChange value [object Object]
   > Steve Jones

  var p = foam.CLASS({name: 'Person', properties: [ 'f', 'l' ]}).create({f:'John', l: 'Doe'});
  var e = foam.core.ExpressionSlot.create({
    obj: p,
    code: function(f, l) { return f + ' ' + l; }
  });

*/
foam.CLASS({
  package: 'foam.core',
  name: 'ExpressionSlot',
  implements: [ 'foam.core.Slot' ],

  properties: [
    'obj',
    'code',
    {
      name: 'args',
      expression: function(obj) {
        this.assert(obj, 'ExpressionSlot: "obj" or "args" required.');

        var args = foam.Function.formalArgs(this.code);
        for ( var i = 0 ; i < args.length ; i++ ) {
          args[i] = obj.slot(args[i]);
        }

        // this.invalidate(); // ???: Is this needed?
        this.subToArgs_(args);

        return args;
      },
      postSet: function(_, args) {
        this.subToArgs_(args);
      }
    },
    {
      name: 'value',
      factory: function() {
        return this.code.apply(this.obj || this, this.args.map(function(a) {
          return a.get();
        }));
      }
    },
    'cleanup_', // destroyable to cleanup old subs when obj changes
  ],

  methods: [
    function init() { this.onDestroy(this.cleanup); },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) {
      return arguments.length === 1 ?
        this.SUPER('propertyChange', 'value', l) :
        this.SUPER.apply(this,arguments);
    },

    function subToArgs_(args) {
      this.cleanup();

      var cleanup = foam.core.FObject.create();

      for ( var i = 0 ; i < args.length ; i++ ) {
        cleanup.onDestroy(args[i].sub(this.invalidate));
      }

      this.cleanup_ = cleanup;
    }
  ],

  listeners: [
    function cleanup() { this.cleanup_ && this.cleanup_.destroy(); },
    function invalidate() { this.clearProperty('value'); }
  ]
});
<|endoftext|>


<commit_before>define(function(){
	var buttonHandler = {
		containerPosX : 0
		,	containerPosY : 0
		,	mouseX : 0
		,	mouseY : 0
		,	that : null
		,	lines : null
		,	height : 0
		,	width : 0
		,	page_width : 0
		,	ratio : 0
		,
		toggleFullscreen: function() {
				if ((document.fullScreenElement && document.fullScreenElement !== null) ||    // alternative standard method
					(!document.mozFullScreen && !document.webkitIsFullScreen)) {               // current working methods
					if (document.documentElement.requestFullScreen) {
						document.documentElement.requestFullScreen();
					} else if (document.documentElement.mozRequestFullScreen) {
						document.documentElement.mozRequestFullScreen();
					} else if (document.documentElement.webkitRequestFullScreen) {
						document.documentElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
					}
					Reveal.navigateTo(0,0);
				} else {
					if (document.cancelFullScreen) {
						document.querySelector('body').classList.remove('fullscreen');
						document.cancelFullScreen();
					} else if (document.mozCancelFullScreen) {
						document.querySelector('body').classList.remove('fullscreen');
					document.mozCancelFullScreen();
					} else if (document.webkitCancelFullScreen) {
					document.querySelector('body').classList.remove('fullscreen');
					document.webkitCancelFullScreen();

					}
					document.querySelector('body').classList.remove('fullscreen');
				}
		},
		removeSpan : function(e){
				e.preventDefault();
				e.stopPropagation();
				$(this).remove();
		},
		pDistance: function (x, y, x1, y1, x2, y2) {

			var A = x - x1;
			var B = y - y1;
			var C = x2 - x1;
			var D = y2 - y1;

			var dot = A * C + B * D;
			var len_sq = C * C + D * D;
			var param = dot / len_sq;

			var xx, yy;

			if (param < 0 || (x1 == x2 && y1 == y2)) {
				xx = x1;
				yy = y1;
			}
			else if (param > 1) {
				xx = x2;
				yy = y2;
			}
			else {
				xx = x1 + param * C;
				yy = y1 + param * D;
			}

			var dx = x - xx;
			var dy = y - yy;
			return Math.sqrt(dx * dx + dy * dy);
		},
		moveSpan : function(e){

			e.stopPropagation();
			var that = this
			,	frame = $('.present').eq(0).offset()
			;

			that.style.position = 'absolute';
			this.style.width = 'auto';
			that.style.left = e.pageX - that.clientWidth / 2 - frame.left + 'px';
			that.style.top = e.pageY - that.clientHeight / 2 - frame.top + 'px';

			mouseX = e.pageX;
			mouseY = e.pageY;

			lines = JSON.parse(localStorage.getItem('canvasPoints')) || [];

			$(document).on('mousemove', function (e) {
				buttonHandler.move.call(that, e);
			});

			$(document).on('mouseup', function () {
				$(window).trigger('movemouseup');
				$(document).off('mousemove');
			});
		},
		cancelDuplicate : function (e, that) {
			setTimeout(function () {
				if(!$('.duplicate-direction').length)
				$('#duplicate')
					.removeClass('btn-group')
					.addClass('btn-warning btn')
					.html('Duplicate');
				}, 1000);
		},

		move : function (e) {

			var   left = 0
				, right = 0
				, top = 0
				, l = $('.snap-line')
				, snapped = false
				, that = this
				, frame = $('.present').eq(0).offset()
				;

			left = e.pageX - that.clientWidth / 2 - frame.left;
			top = e.pageY - that.clientHeight / 2 - frame.top;

			lines.map(function (line) {
				var dl = buttonHandler.pDistance(left, top, line.from[0], line.from[1], line.to[0], line.to[1]);
				var dr = buttonHandler.pDistance(right, top, line.from[0], line.from[1], line.to[0], line.to[1]);

				if(dl<30 && dl != dr) {
					if(!l.length) l = $('<div/>').addClass('snap-line');
					else l.removeClass('snap-line').addClass('snap-line');
					if(line.from[0] == line.to[0]) {
						l.css({
							'width' : '2px',
							'height' : line.to[1] - line.from[1],
							'top' : line.from[1],
							'left' : line.from[0]
						}).appendTo('.present');
						if(left > line.from[0]) {
							that.style.left = left - dl + 'px';
							that.style.top = top + 'px';
							snapped = true;
						}
					}
				} else if(dr<30 && dl != dr) {
					if(!l.length) l = $('<div/>').addClass('snap-line');
					else l.removeClass('snap-line').addClass('snap-line');
					if(line.from[0] == line.to[0]) {
						l.css({
							'height' : line.to[1] - line.from[1],
							'top' : line.from[1],
							'left' : line.from[0],
							'width' : '2px'
						}).appendTo('.present');
						if(right < line.from[0]) {
							that.style.top = top + 'px';
							snapped = true;
							that.style.left = right - that.clientWidth + dr + 'px';
						}
					}
				} else if (dr == dl && dl < 40 || dl == that.clientHeight) {
					if(!l.length) l = $('<div/>').addClass('snap-line');
					else l.removeClass('snap-line').addClass('snap-line');

					if(line.from[1] == line.to[1]) {
						l.css({
							'width' : Math.abs(line.to[0] - line.from[0]),
							'top' : line.from[1],
							'left' : Math.min(line.from[0], line.to[0]),
							'height' : '2px'
						}).appendTo('.present');

						if(top > line.from[1]) {
							that.style.top = top - dr + 'px';
							snapped = true;
						}
						if (top < line.from[1]) {
							that.style.top = top + dl - that.clientHeight + 'px';
							snapped = true;
						}
					}
				}
			});

			if(!snapped && left && top) {
				that.style.left = left + 'px';
				that.style.top = top + 'px';
			}
		},

		updateResizeBars: function (resizeR, resizeL, resizeB, img) {
			buttonHandler.initR(resizeR, img);
			buttonHandler.initL(resizeL, img);
			buttonHandler.initB(resizeB, img);
		},

		resize_r: function (e) {
			var img = $('.resizing').eq(0);
			width = img.width();
			height = img.height();
			var offset = img.offset();
			var img_width = e.pageX - offset.left;
			if (img_width > 50) {
				img.width(img_width);
				img.height(img_width * ratio);
			}
		},

		resize_l: function (e) {
			var img = $('.resizing').eq(0);
			width = img.width();
			height = img.height();
			var offset = img.offset();
			var offsetRight = window.innerWidth - parseInt(img.width(), 10) - offset.left;
			var img_width = window.innerWidth - e.pageX - offsetRight;
			if (img_width > 50) {
				img.width(img_width);
				img.height(img.width() * ratio);
			}
		},

		resize_b: function (e) {
			var img = $('.resizing').eq(0);
			width = img.width();
			height = img.height();
			var offset = img.offset();
			var img_height = e.pageY - offset.top;

			if (img_height > 50) {
				img.height(img_height);
				img.width(ratio * img_height);
			}
		},

		mousemove: function (e) {
			var img = $('#active-image')
			,   frame = $('.present').eq(0).offset()
			,	position = img.position();

			width = img.width();
			height = img.height();

			img.css({
				left: (e.pageX -img.width() / 2 - frame.left)+ 'px',
				top: (e.pageY - img.height() / 2 - frame.top) + 'px'
			});
		},

		initR: function (obj, img) {
			obj.style.height = img.height + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) + img.width - 10 + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) + 'px';
		},

		initT: function (obj, img) {
			obj.style.width = img.width + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) - 10 + 'px';
		},

		initL: function (obj, img) {
			obj.style.height = img.height + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) - 10 + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) + 1 + 'px';
		},

		initB: function (obj, img) {
			obj.style.width = img.width + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) + img.height - 10 + 'px';
		},
		clearResize: function (img) {
			console.log('remove');

			img.classList.remove('resizing');
			img.removeEventListener('mousemove', buttonHandler.attachImageMove);
			var resizeR = document.querySelector('.resize-right')
			,	resizeL = document.querySelector('.resize-left')
			,	resizeB = document.querySelector('.resize-bottom')
			;

			resizeR.parentNode.removeChild(resizeR);
			resizeB.parentNode.removeChild(resizeB);
			resizeL.parentNode.removeChild(resizeL);
		},
		attachImageMove : function () {
			if(document.querySelector('#active-image'))
				document.querySelector('#active-image').id = '';
			this.id = 'active-image';
			this.classList.add('moving');
			window.addEventListener('mousemove', buttonHandler.mousemove, false);
		},
		disableInsert : function () {
			if(!$(this).hasClass('crosshair')) {
				$('.present span').attr('contentEditable', true);
			} else {
				$('.present span').attr('contentEditable', false);
			}
		},
		imageResize: function (img) {

			var resizeR = document.createElement('div')
			,	resizeL = document.createElement('div')
			,	resizeB = document.createElement('div')
			;

			resizeR.classList.add('resize-right');
			resizeL.classList.add('resize-left');
			resizeB.classList.add('resize-bottom');
			img.parentNode.appendChild(resizeR);
			img.parentNode.appendChild(resizeL);
			img.parentNode.appendChild(resizeB);
			img.classList.add('resizing');

			img.ondragstart = function() { return false; };

			img.addEventListener('mousedown', buttonHandler.attachImageMove, false);

			resizeR.addEventListener('mousedown', function () {
				ratio = img.height / img.width;
				if (img.style.left == 'auto') {
					img.style.left = parseInt(img.offsetLeft, 10);
					img.style.right = 'auto';
				}
				window.addEventListener('mousemove', buttonHandler.resize_r, false);
			}, false);

			resizeL.addEventListener('mousedown', function () {
				ratio = img.height / img.width;
				var frameOffset = $('.present').offset();
				var offsetRight = window.innerWidth - img.width - img.offsetLeft - 2*frameOffset.left;
				img.style.left = 'auto';
				img.style.right = offsetRight + 'px';
				console.log(offsetRight);
				window.addEventListener('mousemove', buttonHandler.resize_l, false);
			}, false);

			resizeB.addEventListener('mousedown', function () {
				ratio = img.width / img.height;
				if (img.style.top == 'auto') {
                    img.style.top = img.offsetTop;
                    img.style.bottom = 'auto';
                }
				window.addEventListener('mousemove', buttonHandler.resize_b, false);
			}, false);

			window.addEventListener('mouseup', function(e) {
				img.classList.remove('moving');
				window.removeEventListener('mousemove', buttonHandler.resize_r, false);
				window.removeEventListener('mousemove', buttonHandler.resize_l, false);
				window.removeEventListener('mousemove', buttonHandler.resize_b, false);
				window.removeEventListener('mousemove', buttonHandler.mousemove, false);

				buttonHandler.updateResizeBars(resizeR, resizeL, resizeB, img);

			}, false);

			buttonHandler.updateResizeBars(resizeR, resizeL, resizeB, img);
		},

		showLine: function(x1, y1, x2, y2) {
			var $line = $('.snap-line').removeClass('fade-out');
			if(!$line.length) $line = $('<div/>').addClass('snap-line');
			if(x1 == x2) {
				$line.css({
					'top' : y1,
					'left' : x1,
					'height' : y2-y1
				});
			} else {
				$line.css({
					'top' : y1,
					'left' : x1,
					'width' : x2-x1
				});
			}
			$line.appendTo('.present').addClass('fade-out');
		},
		round: function(x, step) {
			if(x%step > step/2) {
				x += step - x%step;
				return x;
			} else {
				x -= x%step;
				return x;
			}
		},
		createRangeEl: function() {
			var range = document.createElement('input');
			range.type="range";
			range.id ="range-handler";
			range.min=-180;
			range.max=180;
			return range;
		}
	};
	return buttonHandler;
});<commit_msg>Fixed resizing multiple images<commit_after>define(function(){
	var buttonHandler = {
		containerPosX : 0
		,	containerPosY : 0
		,	mouseX : 0
		,	mouseY : 0
		,	that : null
		,	lines : null
		,	height : 0
		,	width : 0
		,	page_width : 0
		,	ratio : 0
		,
		toggleFullscreen: function() {
				if ((document.fullScreenElement && document.fullScreenElement !== null) ||    // alternative standard method
					(!document.mozFullScreen && !document.webkitIsFullScreen)) {               // current working methods
					if (document.documentElement.requestFullScreen) {
						document.documentElement.requestFullScreen();
					} else if (document.documentElement.mozRequestFullScreen) {
						document.documentElement.mozRequestFullScreen();
					} else if (document.documentElement.webkitRequestFullScreen) {
						document.documentElement.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
					}
					Reveal.navigateTo(0,0);
				} else {
					if (document.cancelFullScreen) {
						document.querySelector('body').classList.remove('fullscreen');
						document.cancelFullScreen();
					} else if (document.mozCancelFullScreen) {
						document.querySelector('body').classList.remove('fullscreen');
					document.mozCancelFullScreen();
					} else if (document.webkitCancelFullScreen) {
					document.querySelector('body').classList.remove('fullscreen');
					document.webkitCancelFullScreen();

					}
					document.querySelector('body').classList.remove('fullscreen');
				}
		},
		removeSpan : function(e){
				e.preventDefault();
				e.stopPropagation();
				$(this).remove();
		},
		pDistance: function (x, y, x1, y1, x2, y2) {

			var A = x - x1;
			var B = y - y1;
			var C = x2 - x1;
			var D = y2 - y1;

			var dot = A * C + B * D;
			var len_sq = C * C + D * D;
			var param = dot / len_sq;

			var xx, yy;

			if (param < 0 || (x1 == x2 && y1 == y2)) {
				xx = x1;
				yy = y1;
			}
			else if (param > 1) {
				xx = x2;
				yy = y2;
			}
			else {
				xx = x1 + param * C;
				yy = y1 + param * D;
			}

			var dx = x - xx;
			var dy = y - yy;
			return Math.sqrt(dx * dx + dy * dy);
		},
		moveSpan : function(e){

			e.stopPropagation();
			var that = this
			,	frame = $('.present').eq(0).offset()
			;

			that.style.position = 'absolute';
			this.style.width = 'auto';
			that.style.left = e.pageX - that.clientWidth / 2 - frame.left + 'px';
			that.style.top = e.pageY - that.clientHeight / 2 - frame.top + 'px';

			mouseX = e.pageX;
			mouseY = e.pageY;

			lines = JSON.parse(localStorage.getItem('canvasPoints')) || [];

			$(document).on('mousemove', function (e) {
				buttonHandler.move.call(that, e);
			});

			$(document).on('mouseup', function () {
				$(window).trigger('movemouseup');
				$(document).off('mousemove');
			});
		},
		cancelDuplicate : function (e, that) {
			setTimeout(function () {
				if(!$('.duplicate-direction').length)
				$('#duplicate')
					.removeClass('btn-group')
					.addClass('btn-warning btn')
					.html('Duplicate');
				}, 1000);
		},

		move : function (e) {

			var   left = 0
				, right = 0
				, top = 0
				, l = $('.snap-line')
				, snapped = false
				, that = this
				, frame = $('.present').eq(0).offset()
				;

			left = e.pageX - that.clientWidth / 2 - frame.left;
			top = e.pageY - that.clientHeight / 2 - frame.top;

			lines.map(function (line) {
				var dl = buttonHandler.pDistance(left, top, line.from[0], line.from[1], line.to[0], line.to[1]);
				var dr = buttonHandler.pDistance(right, top, line.from[0], line.from[1], line.to[0], line.to[1]);

				if(dl<30 && dl != dr) {
					if(!l.length) l = $('<div/>').addClass('snap-line');
					else l.removeClass('snap-line').addClass('snap-line');
					if(line.from[0] == line.to[0]) {
						l.css({
							'width' : '2px',
							'height' : line.to[1] - line.from[1],
							'top' : line.from[1],
							'left' : line.from[0]
						}).appendTo('.present');
						if(left > line.from[0]) {
							that.style.left = left - dl + 'px';
							that.style.top = top + 'px';
							snapped = true;
						}
					}
				} else if(dr<30 && dl != dr) {
					if(!l.length) l = $('<div/>').addClass('snap-line');
					else l.removeClass('snap-line').addClass('snap-line');
					if(line.from[0] == line.to[0]) {
						l.css({
							'height' : line.to[1] - line.from[1],
							'top' : line.from[1],
							'left' : line.from[0],
							'width' : '2px'
						}).appendTo('.present');
						if(right < line.from[0]) {
							that.style.top = top + 'px';
							snapped = true;
							that.style.left = right - that.clientWidth + dr + 'px';
						}
					}
				} else if (dr == dl && dl < 40 || dl == that.clientHeight) {
					if(!l.length) l = $('<div/>').addClass('snap-line');
					else l.removeClass('snap-line').addClass('snap-line');

					if(line.from[1] == line.to[1]) {
						l.css({
							'width' : Math.abs(line.to[0] - line.from[0]),
							'top' : line.from[1],
							'left' : Math.min(line.from[0], line.to[0]),
							'height' : '2px'
						}).appendTo('.present');

						if(top > line.from[1]) {
							that.style.top = top - dr + 'px';
							snapped = true;
						}
						if (top < line.from[1]) {
							that.style.top = top + dl - that.clientHeight + 'px';
							snapped = true;
						}
					}
				}
			});

			if(!snapped && left && top) {
				that.style.left = left + 'px';
				that.style.top = top + 'px';
			}
		},

		updateResizeBars: function (resizeR, resizeL, resizeB, img) {
			buttonHandler.initR(resizeR, img);
			buttonHandler.initL(resizeL, img);
			buttonHandler.initB(resizeB, img);
		},

		resize_r: function (e) {
			var img = $('#active-image');
			width = img.width();
			height = img.height();
			var offset = img.offset()
			,	img_width = e.pageX - offset.left
			;
			if (img_width > 50) {
				img.width(img_width);
				img.height(img_width * ratio);
			}
		},

		resize_l: function (e) {
			var img = $('#active-image');
			width = img.width();
			height = img.height();
			var offset = img.offset()
			,	offsetRight = window.innerWidth - parseInt(img.width(), 10) - offset.left
			,	img_width = window.innerWidth - e.pageX - offsetRight
			;
			if (img_width > 50) {
				img.width(img_width);
				img.height(img.width() * ratio);
			}
		},

		resize_b: function (e) {
			var img = $('#active-image');
			width = img.width();
			height = img.height();
			var offset = img.offset()
			,	img_height = e.pageY - offset.top
			;

			if (img_height > 50) {
				img.height(img_height);
				img.width(ratio * img_height);
			}
		},

		mousemove: function (e) {
			var img = $('#active-image')
			,   frame = $('.present').eq(0).offset()
			,	position = img.position()
			;

			width = img.width();
			height = img.height();

			img.css({
				left: (e.pageX -img.width() / 2 - frame.left)+ 'px',
				top: (e.pageY - img.height() / 2 - frame.top) + 'px'
			});
		},

		initR: function (obj, img) {
			obj.style.height = img.height + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) + img.width - 10 + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) + 'px';
		},

		initT: function (obj, img) {
			obj.style.width = img.width + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) - 10 + 'px';
		},

		initL: function (obj, img) {
			obj.style.height = img.height + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) - 10 + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) + 1 + 'px';
		},

		initB: function (obj, img) {
			obj.style.width = img.width + 'px';
			obj.style.left = parseInt(img.offsetLeft, 10) + 'px';
			obj.style.top = parseInt(img.offsetTop, 10) + img.height - 10 + 'px';
		},
		clearResize: function (img) {
			img.classList.remove('resizing');
			img.removeEventListener('mousemove', buttonHandler.attachImageMove);
			var resizeR = document.querySelector('.resize-right')
			,	resizeL = document.querySelector('.resize-left')
			,	resizeB = document.querySelector('.resize-bottom')
			;

			resizeR.parentNode.removeChild(resizeR);
			resizeB.parentNode.removeChild(resizeB);
			resizeL.parentNode.removeChild(resizeL);
		},
		attachImageMove : function () {
			if(document.querySelector('#active-image'))
				document.querySelector('#active-image').id = '';
			this.id = 'active-image';
			this.classList.add('moving');
			window.addEventListener('mousemove', buttonHandler.mousemove, false);
		},
		disableInsert : function () {
			if(!$(this).hasClass('crosshair')) {
				$('.present span').attr('contentEditable', true);
			} else {
				$('.present span').attr('contentEditable', false);
			}
		},
		imageResize: function (img) {
			var resizeR = document.createElement('div')
			,	resizeL = document.createElement('div')
			,	resizeB = document.createElement('div')
			;

			resizeR.classList.add('resize-right');
			resizeL.classList.add('resize-left');
			resizeB.classList.add('resize-bottom');
			img.parentNode.appendChild(resizeR);
			img.parentNode.appendChild(resizeL);
			img.parentNode.appendChild(resizeB);
			img.classList.add('resizing');

			img.ondragstart = function() { return false; };

			img.addEventListener('mousedown', buttonHandler.attachImageMove, false);

			resizeR.addEventListener('mousedown', function () {
				ratio = img.height / img.width;
				if (img.style.left == 'auto') {
					img.style.left = parseInt(img.offsetLeft, 10);
					img.style.right = 'auto';
				}
				window.addEventListener('mousemove', buttonHandler.resize_r, false);
			}, false);

			resizeL.addEventListener('mousedown', function () {
				ratio = img.height / img.width;
				var frameOffset = $('.present').offset()
				,	offsetRight = window.innerWidth - img.width - img.offsetLeft - 2*frameOffset.left
				;
				img.style.left = 'auto';
				img.style.right = offsetRight + 'px';
				window.addEventListener('mousemove', buttonHandler.resize_l, false);
			}, false);

			resizeB.addEventListener('mousedown', function () {
				ratio = img.width / img.height;
				if (img.style.top == 'auto') {
                    img.style.top = img.offsetTop;
                    img.style.bottom = 'auto';
                }
				window.addEventListener('mousemove', buttonHandler.resize_b, false);
			}, false);

			window.addEventListener('mouseup', function(e) {
				img.classList.remove('moving');
				window.removeEventListener('mousemove', buttonHandler.resize_r, false);
				window.removeEventListener('mousemove', buttonHandler.resize_l, false);
				window.removeEventListener('mousemove', buttonHandler.resize_b, false);
				window.removeEventListener('mousemove', buttonHandler.mousemove, false);

				buttonHandler.updateResizeBars(resizeR, resizeL, resizeB, img);

			}, false);

			buttonHandler.updateResizeBars(resizeR, resizeL, resizeB, img);
		},

		showLine: function(x1, y1, x2, y2) {
			var $line = $('.snap-line').removeClass('fade-out');
			if(!$line.length) $line = $('<div/>').addClass('snap-line');
			if(x1 == x2) {
				$line.css({
					'top' : y1,
					'left' : x1,
					'height' : y2-y1
				});
			} else {
				$line.css({
					'top' : y1,
					'left' : x1,
					'width' : x2-x1
				});
			}
			$line.appendTo('.present').addClass('fade-out');
		},
		round: function(x, step) {
			if(x%step > step/2) {
				x += step - x%step;
				return x;
			} else {
				x -= x%step;
				return x;
			}
		},
		createRangeEl: function() {
			var range = document.createElement('input');
			range.type="range";
			range.id ="range-handler";
			range.min=-180;
			range.max=180;
			return range;
		}
	};
	return buttonHandler;
});<|endoftext|>


<commit_before>/*
  Interpreter
  Executes abstract syntax tree.
*/

;(function(exports) {
  var Isla, _, multimethod;
  if(typeof module !== 'undefined' && module.exports) { // node
    _ = require("Underscore");
    multimethod = require('multimethod');
    Isla = {};
    Isla.Utils = require('./utils').Utils;
    Isla.Library = require('./library').Library;
    Isla.Parser = require('./parser').Parser;
  } else { // browser
    _ = window._;
    multimethod = window.multimethod;
    Isla = window.Isla;
  }

  exports.Interpreter = {};

  // takes raw Isla code, parses then interprets
  exports.Interpreter.interpret = function(code, env) {
    return interpretAst(Isla.Parser.parse(code), env);
  };

  var interpretAst = multimethod()
    .dispatch(function(ast, env) {
      return ast.tag;
    })

    .when("root", function(ast, env) {
      if(env === undefined) {
        env = Isla.Library.getInitialEnv();
      }

      return runSequence(ast.c, env);
    })

    .when("block", function(ast, env) {
      return runSequence(ast.c, env);
    })

    .when("expression", function(ast, env) {
      return interpretAst(ast.c[0], env);
    })

    .when("value_assignment", function(ast, env) {
      var node = Isla.Parser.extract(ast, "value_assignment");
      var assignee = node[0];
      var valueNode = interpretAst(node[2], env);
      var value = assignmentValue(valueNode);
      var env = assign(env, assignee, value);
      return nreturn(env.ctx);
    })

    .when("type_assignment", function(ast, env) {
      var node = Isla.Parser.extract(ast, "type_assignment");
      var assignee = node[0];
      var typeIdentifier = interpretAst(node[2], env);

      var typeFn = env.ctx.types[typeIdentifier];
      if(typeFn === undefined) {
        typeFn = env.ctx.types.generic;
      }

      var value = instantiateType(typeFn, typeIdentifier);
      var env = assign(env, assignee, value);
      return nreturn(env.ctx);
    })

    .when("list_assignment", function(ast, env) {
      var node = Isla.Parser.extract(ast, "list_assignment");
      var assignee = node[3];
      var currentListEval = evaluateValue(Isla.Parser.extract(assignee,
                                                              "assignee", 0),
                                                              env);
      if(currentListEval.val === undefined) { // no such list - show error
        var ref = currentListEval.ref;
        throw Error("I do not know of a list called "
                    + (Isla.Utils.type(currentListEval.ref) === "Array"
                       ? ref[0] + " " + ref[1] : ref)
                    + ".");
      }
      else {
        var operation = Isla.Parser.extract(node, 0, "list_operation", 0).tag;
        var itemEval = interpretAst(Isla.Parser.extract(node, 1), env);
        var item = assignmentValue(itemEval);

        var list = currentListEval.val;
        list[operation](item);

        var env = assign(env, assignee, currentListEval.val);
        return nreturn(env.ctx);
      }
    })

    .when("invocation", function(ast, env) {
      var fn = resolve({
        ref: interpretAst(Isla.Parser.extract(ast, "invocation", 0), env)
      }, env);
      var param = interpretAst(Isla.Parser.extract(ast, "invocation", 1),
                               env).val;
      var returnVal = fn(env, param);
      return nreturn(env.ctx, returnVal);
    })

    .when("value", function(ast, env) {
      var node = Isla.Parser.extract(ast, "value");
      return evaluateValue(node[0], env);
    })

    .when("integer", function(ast, env) {
      return Isla.Parser.extract(ast, "integer", 0);
    })

    .when("string", function(ast, env) {
      return Isla.Parser.extract(ast, "string", 0);
    })

    .when("identifier", function(ast, env) {
      return Isla.Parser.extract(ast, "identifier", 0);
    })

    .default(function(ast, env) {
      throw "You've forgotten a tag type.";
    });

  var evaluateValue = multimethod()
    .dispatch(function(node, env) {
      return node.tag;
    })

    .when("literal", function(node, env) {
      return { val: interpretAst(node.c[0], env) };
    })

    .when("variable", function(node, env) {
      return evaluateValue(node.c[0], env);
    })

    .when("scalar", function(node, env) {
      var identifier = interpretAst(node.c[0], env);
      return { ref: identifier, val: env.ctx[identifier] };
    })

    .when("object", function(node, env) {
      // make more specific
      var objId = node.c[0].c[0];
      var attrId = node.c[1].c[0];
      var val = env.ctx[objId] !== undefined &&
                env.ctx[objId][attrId] !== undefined ?
                env.ctx[objId][attrId] : undefined;
      return {
        ref: [objId, attrId], // won't work if assign obj-attr to var
        val: val
      }
    });

  var assign = multimethod()
    .dispatch(function(__, assigneeNode) {
      return assigneeNode.c[0].tag;
    })

    .when("scalar", function(env, assigneeNode, value) {
      var identifier = Isla.Parser.extract(assigneeNode, "assignee", 0,
                                           "scalar", 0, "identifier", 0);
      env.ctx[identifier] = value;
      return env;
    })

    .when("object", function(env, assigneeNode, value) {
      var objNode = Isla.Parser.extract(assigneeNode, "assignee", 0, "object");
      var initObjIdentifier = Isla.Parser.extract(objNode, 0, "identifier", 0);
      var objIdentifier = canonical(initObjIdentifier, env);
      var slotIdentifier = Isla.Parser.extract(objNode, 1, "identifier", 0);
      env.ctx[objIdentifier][slotIdentifier] = value;
      return env;
    });

  // returns appropriate value for valueNode being assigned
  // actual value for primitives, refs
  var assignmentValue = multimethod()
    .dispatch(function(valueNode) {
      return typeof(valueNode.val);
    })

    .when("string", function(valueNode) {
      return valueNode.val;
    })

    .when("number", function(valueNode) {
      return valueNode.val;
    })

    .default(function(valueNode) {
      return valueNode.ref === undefined ? valueNode.val :
                                           { ref: valueNode.ref };
    });

  var resolve = multimethod()
    .dispatch(function(thing) {
      if(thing instanceof Isla.Library.List) {
        return "list";
      }
      else if(Isla.Utils.type(thing) === "Object") {
        return thing.ref === undefined ? "object" : "ref";
      }
    })

    .when("ref", function(thing, env) {
      return resolve(env.ctx[thing.ref], env);
    })

    .when("object", function(thing, env) {
      for(var i in thing) {
        if(i !== "_meta") {
          thing[i] = resolve(thing[i], env);
        }
      }

      return thing;
    })

    .when("list", function(thing, env) {
      var items = thing.items();
      var resolvedList = new Isla.Library.List();
      for(var i = 0; i < items.length; i++) {
        resolvedList.add(resolve(items[i], env));
      }

      return resolvedList;
    })

    .default(function(thing) {
      return thing;
    });

  var canonical = function(identifier, env) {
    var next = env.ctx[identifier];
    if (next.ref !== undefined) {
      return canonical(next.ref, env);
    } else {
      return identifier;
    }
  };

  var runSequence = function(nodes, env) {
    if(nodes.length === 0) {
      return env;
    }
    else {
      return runSequence(_.rest(nodes), interpretAst(_.first(nodes),
                         rmRet(env)));
    }
  }

  var instantiateType = function(typeFn, identifier) {
    var value = typeFn();
    value._meta = { type: identifier };
    return value;
  }

  var rmRet = function(env) {
    env.ret = null;
    return env;
  }

  var nreturn = function(ctx, ret) {
    if(ret === undefined) {
      return { ctx: ctx, ret: null };
    }
    else {
      return { ctx: ctx, ret: ret };
    }
  }

  exports.Interpreter.resolve = resolve;
})(typeof exports === 'undefined' ? this.Isla : exports);
<commit_msg>Resolve param before passing to fn.<commit_after>/*
  Interpreter
  Executes abstract syntax tree.
*/

;(function(exports) {
  var Isla, _, multimethod;
  if(typeof module !== 'undefined' && module.exports) { // node
    _ = require("Underscore");
    multimethod = require('multimethod');
    Isla = {};
    Isla.Utils = require('./utils').Utils;
    Isla.Library = require('./library').Library;
    Isla.Parser = require('./parser').Parser;
  } else { // browser
    _ = window._;
    multimethod = window.multimethod;
    Isla = window.Isla;
  }

  exports.Interpreter = {};

  // takes raw Isla code, parses then interprets
  exports.Interpreter.interpret = function(code, env) {
    return interpretAst(Isla.Parser.parse(code), env);
  };

  var interpretAst = multimethod()
    .dispatch(function(ast, env) {
      return ast.tag;
    })

    .when("root", function(ast, env) {
      if(env === undefined) {
        env = Isla.Library.getInitialEnv();
      }

      return runSequence(ast.c, env);
    })

    .when("block", function(ast, env) {
      return runSequence(ast.c, env);
    })

    .when("expression", function(ast, env) {
      return interpretAst(ast.c[0], env);
    })

    .when("value_assignment", function(ast, env) {
      var node = Isla.Parser.extract(ast, "value_assignment");
      var assignee = node[0];
      var valueNode = interpretAst(node[2], env);
      var value = assignmentValue(valueNode);
      var env = assign(env, assignee, value);
      return nreturn(env.ctx);
    })

    .when("type_assignment", function(ast, env) {
      var node = Isla.Parser.extract(ast, "type_assignment");
      var assignee = node[0];
      var typeIdentifier = interpretAst(node[2], env);

      var typeFn = env.ctx.types[typeIdentifier];
      if(typeFn === undefined) {
        typeFn = env.ctx.types.generic;
      }

      var value = instantiateType(typeFn, typeIdentifier);
      var env = assign(env, assignee, value);
      return nreturn(env.ctx);
    })

    .when("list_assignment", function(ast, env) {
      var node = Isla.Parser.extract(ast, "list_assignment");
      var assignee = node[3];
      var currentListEval = evaluateValue(Isla.Parser.extract(assignee,
                                                              "assignee", 0),
                                                              env);
      if(currentListEval.val === undefined) { // no such list - show error
        var ref = currentListEval.ref;
        throw Error("I do not know of a list called "
                    + (Isla.Utils.type(currentListEval.ref) === "Array"
                       ? ref[0] + " " + ref[1] : ref)
                    + ".");
      }
      else {
        var operation = Isla.Parser.extract(node, 0, "list_operation", 0).tag;
        var itemEval = interpretAst(Isla.Parser.extract(node, 1), env);
        var item = assignmentValue(itemEval);

        var list = currentListEval.val;
        list[operation](item);

        var env = assign(env, assignee, currentListEval.val);
        return nreturn(env.ctx);
      }
    })

    .when("invocation", function(ast, env) {
      var fn = resolve({
        ref: interpretAst(Isla.Parser.extract(ast, "invocation", 0), env)
      }, env);
      var param = resolve(interpretAst(Isla.Parser.extract(ast, "invocation", 1),
                                       env).val, env);
      var returnVal = fn(env, param);
      return nreturn(env.ctx, returnVal);
    })

    .when("value", function(ast, env) {
      var node = Isla.Parser.extract(ast, "value");
      return evaluateValue(node[0], env);
    })

    .when("integer", function(ast, env) {
      return Isla.Parser.extract(ast, "integer", 0);
    })

    .when("string", function(ast, env) {
      return Isla.Parser.extract(ast, "string", 0);
    })

    .when("identifier", function(ast, env) {
      return Isla.Parser.extract(ast, "identifier", 0);
    })

    .default(function(ast, env) {
      throw "You've forgotten a tag type.";
    });

  var evaluateValue = multimethod()
    .dispatch(function(node, env) {
      return node.tag;
    })

    .when("literal", function(node, env) {
      return { val: interpretAst(node.c[0], env) };
    })

    .when("variable", function(node, env) {
      return evaluateValue(node.c[0], env);
    })

    .when("scalar", function(node, env) {
      var identifier = interpretAst(node.c[0], env);
      return { ref: identifier, val: env.ctx[identifier] };
    })

    .when("object", function(node, env) {
      // make more specific
      var objId = node.c[0].c[0];
      var attrId = node.c[1].c[0];
      var val = env.ctx[objId] !== undefined &&
                env.ctx[objId][attrId] !== undefined ?
                env.ctx[objId][attrId] : undefined;
      return {
        ref: [objId, attrId], // won't work if assign obj-attr to var
        val: val
      }
    });

  var assign = multimethod()
    .dispatch(function(__, assigneeNode) {
      return assigneeNode.c[0].tag;
    })

    .when("scalar", function(env, assigneeNode, value) {
      var identifier = Isla.Parser.extract(assigneeNode, "assignee", 0,
                                           "scalar", 0, "identifier", 0);
      env.ctx[identifier] = value;
      return env;
    })

    .when("object", function(env, assigneeNode, value) {
      var objNode = Isla.Parser.extract(assigneeNode, "assignee", 0, "object");
      var initObjIdentifier = Isla.Parser.extract(objNode, 0, "identifier", 0);
      var objIdentifier = canonical(initObjIdentifier, env);
      var slotIdentifier = Isla.Parser.extract(objNode, 1, "identifier", 0);
      env.ctx[objIdentifier][slotIdentifier] = value;
      return env;
    });

  // returns appropriate value for valueNode being assigned
  // actual value for primitives, refs
  var assignmentValue = multimethod()
    .dispatch(function(valueNode) {
      return typeof(valueNode.val);
    })

    .when("string", function(valueNode) {
      return valueNode.val;
    })

    .when("number", function(valueNode) {
      return valueNode.val;
    })

    .default(function(valueNode) {
      return valueNode.ref === undefined ? valueNode.val :
                                           { ref: valueNode.ref };
    });

  var resolve = multimethod()
    .dispatch(function(thing) {
      if(thing instanceof Isla.Library.List) {
        return "list";
      }
      else if(Isla.Utils.type(thing) === "Object") {
        return thing.ref === undefined ? "object" : "ref";
      }
    })

    .when("ref", function(thing, env) {
      return resolve(env.ctx[thing.ref], env);
    })

    .when("object", function(thing, env) {
      for(var i in thing) {
        if(i !== "_meta") {
          thing[i] = resolve(thing[i], env);
        }
      }

      return thing;
    })

    .when("list", function(thing, env) {
      var items = thing.items();
      var resolvedList = new Isla.Library.List();
      for(var i = 0; i < items.length; i++) {
        resolvedList.add(resolve(items[i], env));
      }

      return resolvedList;
    })

    .default(function(thing) {
      return thing;
    });

  var canonical = function(identifier, env) {
    var next = env.ctx[identifier];
    if (next.ref !== undefined) {
      return canonical(next.ref, env);
    } else {
      return identifier;
    }
  };

  var runSequence = function(nodes, env) {
    if(nodes.length === 0) {
      return env;
    }
    else {
      return runSequence(_.rest(nodes), interpretAst(_.first(nodes),
                         rmRet(env)));
    }
  }

  var instantiateType = function(typeFn, identifier) {
    var value = typeFn();
    value._meta = { type: identifier };
    return value;
  }

  var rmRet = function(env) {
    env.ret = null;
    return env;
  }

  var nreturn = function(ctx, ret) {
    if(ret === undefined) {
      return { ctx: ctx, ret: null };
    }
    else {
      return { ctx: ctx, ret: ret };
    }
  }

  exports.Interpreter.resolve = resolve;
})(typeof exports === 'undefined' ? this.Isla : exports);
<|endoftext|>


<commit_before>import * as FP from 'fp-ts'
import * as R from 'ramda'
import * as React from 'react'

import * as jsonSchemaUtils from 'utils/json-schema/json-schema'

import { COLUMN_IDS, EMPTY_VALUE } from './constants'

const serializeAddress = (addressPath) => `/${addressPath.join('/')}`

const getAddressPath = (key, parentPath) =>
  key === '' ? parentPath : (parentPath || []).concat(key)

const getSchemaType = R.prop('type')

const getSchemaItem = ({ item, sortIndex, key, parentPath, required }) => ({
  address: getAddressPath(key, parentPath),
  required,
  valueSchema: item,
  sortIndex,
  type: getSchemaType(item),
})

const assocObjValue = R.assocPath

export const getJsonDictValue = (objPath, jsonDict) =>
  R.prop(serializeAddress(objPath), jsonDict)

export const getObjValue = R.path

function moveObjValue(oldObjPath, key, obj) {
  const oldItem = getObjValue(oldObjPath, obj)
  const oldValue = oldItem === undefined ? EMPTY_VALUE : oldItem
  return assocObjValue(
    R.append(key, R.init(oldObjPath)),
    oldValue,
    dissocObjValue(oldObjPath, obj),
  )
}

const dissocObjValue = R.dissocPath

// NOTE: memo is mutated, sortOrder is React.ref and mutated too
export function iterateSchema(schema, sortOrder, parentPath, memo) {
  if (!schema.properties) return memo

  const requiredKeys = schema.required
  getSchemaItemKeys(schema).forEach((key) => {
    // eslint-disable-next-line no-param-reassign
    sortOrder.current.counter += 1

    const rawItem = schema.properties[key]
    const required = requiredKeys ? requiredKeys.includes(key) : false
    const item = getSchemaItem({
      item: rawItem,
      key,
      parentPath,
      required,
      sortIndex: sortOrder.current.counter,
    })
    // eslint-disable-next-line no-param-reassign
    memo[serializeAddress(item.address)] = item

    // eslint-disable-next-line no-param-reassign
    sortOrder.current.counter += 1
    iterateSchema(rawItem, sortOrder, item.address, memo)
  })

  return memo
}

// NOTE: memo is mutated
// weird eslint bug?
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function objToDict(obj, parentPath, memo) {
  const isObjArray = Array.isArray(obj)
  if (isObjArray) {
    obj.forEach((value, index) => {
      const address = getAddressPath(index, parentPath)
      // eslint-disable-next-line no-param-reassign
      memo[serializeAddress(address)] = value

      objToDict(value, address, memo)
    })
    return memo
  }

  if (typeof obj === 'object' && obj !== null && !isObjArray) {
    const keys = Object.keys(obj)

    if (!keys.length) return memo

    keys.forEach((key) => {
      const address = getAddressPath(key, parentPath)
      // eslint-disable-next-line no-param-reassign
      memo[serializeAddress(address)] = obj[key]

      objToDict(obj[key], address, memo)
    })
    return memo
  }

  return memo
}

function calcReactId(valuePath, value) {
  const pathPrefix = serializeAddress(valuePath)
  // TODO: store preview for value, and reuse it for Preview
  return `${pathPrefix}+${JSON.stringify(value)}`
}

function getDefaultValue(jsonDictItem) {
  if (!jsonDictItem?.valueSchema) return EMPTY_VALUE

  const defaultFromSchema = jsonSchemaUtils.getDefaultValue(jsonDictItem?.valueSchema)
  if (defaultFromSchema !== undefined) return defaultFromSchema

  // TODO:
  // get defaults from nested objects
  // const setDefaults = jsonSchemaUtils.makeSchemaDefaultsSetter(jsonDictItem?.valueSchema)
  // const nestedDefaultFromSchema = setDefaults()
  // if (nestedDefaultFromSchema !== undefined) return nestedDefaultFromSchema

  return EMPTY_VALUE
}

const NO_ERRORS = []

function getJsonDictItem(jsonDict, obj, parentPath, key, sortOrder, allErrors) {
  const itemAddress = serializeAddress(getAddressPath(key, parentPath))
  const item = jsonDict[itemAddress]
  const errors = allErrors
    ? allErrors.filter((error) => error.instancePath === itemAddress)
    : NO_ERRORS
  // NOTE: can't use R.pathOr, because Ramda thinks `null` is `undefined` too
  const valuePath = getAddressPath(key, parentPath)
  const storedValue = R.path(valuePath, obj)
  const value = storedValue === undefined ? getDefaultValue(item) : storedValue
  return {
    [COLUMN_IDS.KEY]: key,
    [COLUMN_IDS.VALUE]: value,
    errors,
    reactId: calcReactId(valuePath, storedValue),
    sortIndex: (item && item.sortIndex) || sortOrder.current.dict[itemAddress] || 0,
    ...(item || {}),
  }
}

const noKeys = []

function getObjValueKeys(objValue) {
  if (Array.isArray(objValue)) return R.range(0, objValue.length)
  if (R.is(Object, objValue)) return Object.keys(objValue)
  return noKeys
}

function getObjValueKeysByPath(obj, objPath, rootKeys) {
  if (!objPath.length) return rootKeys

  const objValue = R.path(objPath, obj)
  return getObjValueKeys(objValue)
}

function getSchemaItemKeys(schemaItem) {
  if (!schemaItem || !schemaItem.properties) return noKeys
  const keys = Object.keys(schemaItem.properties)

  if (!schemaItem.required) return keys

  const sortOrder = schemaItem.required.reduce(
    (memo, key, index) => ({
      [key]: index,
      ...memo,
    }),
    {},
  )
  const getSortIndex = (key) =>
    R.ifElse(R.has(key), R.prop(key), R.always(Infinity))(sortOrder)
  return R.sortBy(getSortIndex, keys)
}

function getSchemaItemKeysByPath(jsonDict, objPath) {
  const itemAddress = serializeAddress(objPath)
  const item = jsonDict[itemAddress]
  return item && item.valueSchema ? getSchemaItemKeys(item.valueSchema) : noKeys
}

function getSchemaAndObjKeys(obj, jsonDict, objPath, rootKeys) {
  return R.uniq([
    ...getSchemaItemKeysByPath(jsonDict, objPath),
    ...getObjValueKeysByPath(obj, objPath, rootKeys),
  ])
}

// TODO: refactor data, decrease number of arguments to three
export function iterateJsonDict(jsonDict, obj, fieldPath, rootKeys, sortOrder, errors) {
  if (!fieldPath.length)
    return [
      FP.function.pipe(
        rootKeys,
        R.map((key) => getJsonDictItem(jsonDict, obj, fieldPath, key, sortOrder, errors)),
        R.sortBy(R.prop('sortIndex')),
        (items) => ({
          parent: obj,
          items,
        }),
      ),
    ]

  return ['', ...fieldPath].map((_, index) => {
    const pathPart = R.slice(0, index, fieldPath)

    const keys = getSchemaAndObjKeys(obj, jsonDict, pathPart, rootKeys)
    return FP.function.pipe(
      keys,
      R.map((key) => getJsonDictItem(jsonDict, obj, pathPart, key, sortOrder, errors)),
      R.sortBy(R.prop('sortIndex')),
      (items) => ({
        parent: R.path(pathPart, obj),
        items,
      }),
    )
  })
}

export function mergeSchemaAndObjRootKeys(schema, obj) {
  const schemaKeys = getSchemaItemKeys(schema)
  const objKeys = getObjValueKeys(obj)
  return R.uniq([...schemaKeys, ...objKeys])
}

export default function JsonEditorState({ children, errors, jsonObject, schema }) {
  // NOTE: fieldPath is like URL for editor columns
  //       `['a', 0, 'b']` means we are focused to `{ a: [ { b: %HERE% }, ... ], ... }`
  const [fieldPath, setFieldPath] = React.useState([])

  // NOTE: similar to fieldPath, shows where to open ContextMenu
  const [menuFieldPath, setMenuFieldPath] = React.useState([])

  // NOTE: incremented sortIndex counter,
  //       and cache for sortIndexes: { [keyA]: sortIndexA, [keyB]: sortIndexB }
  //       it's required to place new fields below existing ones
  const sortOrder = React.useRef({ counter: 0, dict: {} })

  // NOTE: stores additional info about every object field besides value, like sortIndex, schema etc.
  //       it's a main source of data after actual JSON object
  const jsonDict = React.useMemo(() => {
    sortOrder.current.counter = Number.MIN_SAFE_INTEGER
    const result = iterateSchema(schema, sortOrder, [], {})
    sortOrder.current.counter = 0
    return result
  }, [schema, sortOrder])

  // NOTE: list of root object keys + root schema keys
  const rootKeys = React.useMemo(
    () => mergeSchemaAndObjRootKeys(schema, jsonObject),
    [schema, jsonObject],
  )

  // NOTE: this data represents table columns shown to user
  //       it's the main source of UI data
  const columns = React.useMemo(
    () => iterateJsonDict(jsonDict, jsonObject, fieldPath, rootKeys, sortOrder, errors),
    [errors, jsonObject, jsonDict, fieldPath, rootKeys],
  )

  // TODO: Use `sortIndex: -1` to "remove" fields that cannot be removed,
  //       like properties from JSON Schema
  const removeField = React.useCallback(
    (removingFieldPath) => dissocObjValue(removingFieldPath, jsonObject),
    [jsonObject],
  )

  const changeValue = React.useCallback(
    (editingFieldPath, columnId, str) => {
      // TODO: str is not string, it's any value
      // TODO: make this `safeStr` conversion inside component
      const safeStr = str === EMPTY_VALUE ? '' : str
      if (columnId === COLUMN_IDS.KEY) {
        return moveObjValue(editingFieldPath, safeStr, jsonObject)
      }

      if (columnId === COLUMN_IDS.VALUE) {
        return assocObjValue(editingFieldPath, safeStr, jsonObject)
      }

      return jsonObject
    },
    [jsonObject],
  )

  const addRow = React.useCallback(
    (addFieldPath, key, value) => {
      // NOTE: value can't be `Symbol('empty')`
      //       because it's imposible to have `{ [newKey]: Symbol('empty') }` object
      sortOrder.current.counter += 1

      const newKeyPath = addFieldPath.concat([key])
      const itemAddress = serializeAddress(newKeyPath)
      sortOrder.current.dict[itemAddress] = sortOrder.current.counter
      return assocObjValue(newKeyPath, value, jsonObject)
    },
    [jsonObject, sortOrder],
  )

  return children({
    addRow,
    changeValue,
    columns,
    fieldPath,
    jsonDict,
    menuFieldPath,
    removeField,
    setFieldPath,
    setMenuFieldPath,
  })
}
<commit_msg>Bigint warning (#3069)<commit_after>import * as FP from 'fp-ts'
import * as R from 'ramda'
import * as React from 'react'

import * as jsonSchemaUtils from 'utils/json-schema/json-schema'

import { COLUMN_IDS, EMPTY_VALUE } from './constants'

const serializeAddress = (addressPath) => `/${addressPath.join('/')}`

const getAddressPath = (key, parentPath) =>
  key === '' ? parentPath : (parentPath || []).concat(key)

const getSchemaType = R.prop('type')

const getSchemaItem = ({ item, sortIndex, key, parentPath, required }) => ({
  address: getAddressPath(key, parentPath),
  required,
  valueSchema: item,
  sortIndex,
  type: getSchemaType(item),
})

const assocObjValue = R.assocPath

export const getJsonDictValue = (objPath, jsonDict) =>
  R.prop(serializeAddress(objPath), jsonDict)

export const getObjValue = R.path

function moveObjValue(oldObjPath, key, obj) {
  const oldItem = getObjValue(oldObjPath, obj)
  const oldValue = oldItem === undefined ? EMPTY_VALUE : oldItem
  return assocObjValue(
    R.append(key, R.init(oldObjPath)),
    oldValue,
    dissocObjValue(oldObjPath, obj),
  )
}

const dissocObjValue = R.dissocPath

// NOTE: memo is mutated, sortOrder is React.ref and mutated too
export function iterateSchema(schema, sortOrder, parentPath, memo) {
  if (!schema.properties) return memo

  const requiredKeys = schema.required
  getSchemaItemKeys(schema).forEach((key) => {
    // eslint-disable-next-line no-param-reassign
    sortOrder.current.counter += 1

    const rawItem = schema.properties[key]
    const required = requiredKeys ? requiredKeys.includes(key) : false
    const item = getSchemaItem({
      item: rawItem,
      key,
      parentPath,
      required,
      sortIndex: sortOrder.current.counter,
    })
    // eslint-disable-next-line no-param-reassign
    memo[serializeAddress(item.address)] = item

    // eslint-disable-next-line no-param-reassign
    sortOrder.current.counter += 1
    iterateSchema(rawItem, sortOrder, item.address, memo)
  })

  return memo
}

// NOTE: memo is mutated
// weird eslint bug?
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function objToDict(obj, parentPath, memo) {
  const isObjArray = Array.isArray(obj)
  if (isObjArray) {
    obj.forEach((value, index) => {
      const address = getAddressPath(index, parentPath)
      // eslint-disable-next-line no-param-reassign
      memo[serializeAddress(address)] = value

      objToDict(value, address, memo)
    })
    return memo
  }

  if (typeof obj === 'object' && obj !== null && !isObjArray) {
    const keys = Object.keys(obj)

    if (!keys.length) return memo

    keys.forEach((key) => {
      const address = getAddressPath(key, parentPath)
      // eslint-disable-next-line no-param-reassign
      memo[serializeAddress(address)] = obj[key]

      objToDict(obj[key], address, memo)
    })
    return memo
  }

  return memo
}

function calcReactId(valuePath, value) {
  const pathPrefix = serializeAddress(valuePath)
  // TODO: store preview for value, and reuse it for Preview
  return `${pathPrefix}+${JSON.stringify(value)}`
}

function getDefaultValue(jsonDictItem) {
  if (!jsonDictItem?.valueSchema) return EMPTY_VALUE

  const defaultFromSchema = jsonSchemaUtils.getDefaultValue(jsonDictItem?.valueSchema)
  if (defaultFromSchema !== undefined) return defaultFromSchema

  // TODO:
  // get defaults from nested objects
  // const setDefaults = jsonSchemaUtils.makeSchemaDefaultsSetter(jsonDictItem?.valueSchema)
  // const nestedDefaultFromSchema = setDefaults()
  // if (nestedDefaultFromSchema !== undefined) return nestedDefaultFromSchema

  return EMPTY_VALUE
}

const NO_ERRORS = []

const bigintError = new Error(
  `We don't support numbers larger than ${Number.MAX_SAFE_INTEGER}.
  Please consider converting it to string.`,
)

function collectErrors(allErrors, itemAddress, value) {
  const errors = allErrors
    ? allErrors.filter((error) => error.instancePath === itemAddress)
    : NO_ERRORS

  if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) {
    return errors.concat(bigintError)
  }
  return errors
}

function getJsonDictItem(jsonDict, obj, parentPath, key, sortOrder, allErrors) {
  const itemAddress = serializeAddress(getAddressPath(key, parentPath))
  const item = jsonDict[itemAddress]
  // NOTE: can't use R.pathOr, because Ramda thinks `null` is `undefined` too
  const valuePath = getAddressPath(key, parentPath)
  const storedValue = R.path(valuePath, obj)
  const value = storedValue === undefined ? getDefaultValue(item) : storedValue
  const errors = collectErrors(allErrors, itemAddress, value)
  return {
    [COLUMN_IDS.KEY]: key,
    [COLUMN_IDS.VALUE]: value,
    errors,
    reactId: calcReactId(valuePath, storedValue),
    sortIndex: (item && item.sortIndex) || sortOrder.current.dict[itemAddress] || 0,
    ...(item || {}),
  }
}

const noKeys = []

function getObjValueKeys(objValue) {
  if (Array.isArray(objValue)) return R.range(0, objValue.length)
  if (R.is(Object, objValue)) return Object.keys(objValue)
  return noKeys
}

function getObjValueKeysByPath(obj, objPath, rootKeys) {
  if (!objPath.length) return rootKeys

  const objValue = R.path(objPath, obj)
  return getObjValueKeys(objValue)
}

function getSchemaItemKeys(schemaItem) {
  if (!schemaItem || !schemaItem.properties) return noKeys
  const keys = Object.keys(schemaItem.properties)

  if (!schemaItem.required) return keys

  const sortOrder = schemaItem.required.reduce(
    (memo, key, index) => ({
      [key]: index,
      ...memo,
    }),
    {},
  )
  const getSortIndex = (key) =>
    R.ifElse(R.has(key), R.prop(key), R.always(Infinity))(sortOrder)
  return R.sortBy(getSortIndex, keys)
}

function getSchemaItemKeysByPath(jsonDict, objPath) {
  const itemAddress = serializeAddress(objPath)
  const item = jsonDict[itemAddress]
  return item && item.valueSchema ? getSchemaItemKeys(item.valueSchema) : noKeys
}

function getSchemaAndObjKeys(obj, jsonDict, objPath, rootKeys) {
  return R.uniq([
    ...getSchemaItemKeysByPath(jsonDict, objPath),
    ...getObjValueKeysByPath(obj, objPath, rootKeys),
  ])
}

// TODO: refactor data, decrease number of arguments to three
export function iterateJsonDict(jsonDict, obj, fieldPath, rootKeys, sortOrder, errors) {
  if (!fieldPath.length)
    return [
      FP.function.pipe(
        rootKeys,
        R.map((key) => getJsonDictItem(jsonDict, obj, fieldPath, key, sortOrder, errors)),
        R.sortBy(R.prop('sortIndex')),
        (items) => ({
          parent: obj,
          items,
        }),
      ),
    ]

  return ['', ...fieldPath].map((_, index) => {
    const pathPart = R.slice(0, index, fieldPath)

    const keys = getSchemaAndObjKeys(obj, jsonDict, pathPart, rootKeys)
    return FP.function.pipe(
      keys,
      R.map((key) => getJsonDictItem(jsonDict, obj, pathPart, key, sortOrder, errors)),
      R.sortBy(R.prop('sortIndex')),
      (items) => ({
        parent: R.path(pathPart, obj),
        items,
      }),
    )
  })
}

export function mergeSchemaAndObjRootKeys(schema, obj) {
  const schemaKeys = getSchemaItemKeys(schema)
  const objKeys = getObjValueKeys(obj)
  return R.uniq([...schemaKeys, ...objKeys])
}

export default function JsonEditorState({ children, errors, jsonObject, schema }) {
  // NOTE: fieldPath is like URL for editor columns
  //       `['a', 0, 'b']` means we are focused to `{ a: [ { b: %HERE% }, ... ], ... }`
  const [fieldPath, setFieldPath] = React.useState([])

  // NOTE: similar to fieldPath, shows where to open ContextMenu
  const [menuFieldPath, setMenuFieldPath] = React.useState([])

  // NOTE: incremented sortIndex counter,
  //       and cache for sortIndexes: { [keyA]: sortIndexA, [keyB]: sortIndexB }
  //       it's required to place new fields below existing ones
  const sortOrder = React.useRef({ counter: 0, dict: {} })

  // NOTE: stores additional info about every object field besides value, like sortIndex, schema etc.
  //       it's a main source of data after actual JSON object
  const jsonDict = React.useMemo(() => {
    sortOrder.current.counter = Number.MIN_SAFE_INTEGER
    const result = iterateSchema(schema, sortOrder, [], {})
    sortOrder.current.counter = 0
    return result
  }, [schema, sortOrder])

  // NOTE: list of root object keys + root schema keys
  const rootKeys = React.useMemo(
    () => mergeSchemaAndObjRootKeys(schema, jsonObject),
    [schema, jsonObject],
  )

  // NOTE: this data represents table columns shown to user
  //       it's the main source of UI data
  const columns = React.useMemo(
    () => iterateJsonDict(jsonDict, jsonObject, fieldPath, rootKeys, sortOrder, errors),
    [errors, jsonObject, jsonDict, fieldPath, rootKeys],
  )

  // TODO: Use `sortIndex: -1` to "remove" fields that cannot be removed,
  //       like properties from JSON Schema
  const removeField = React.useCallback(
    (removingFieldPath) => dissocObjValue(removingFieldPath, jsonObject),
    [jsonObject],
  )

  const changeValue = React.useCallback(
    (editingFieldPath, columnId, str) => {
      // TODO: str is not string, it's any value
      // TODO: make this `safeStr` conversion inside component
      const safeStr = str === EMPTY_VALUE ? '' : str
      if (columnId === COLUMN_IDS.KEY) {
        return moveObjValue(editingFieldPath, safeStr, jsonObject)
      }

      if (columnId === COLUMN_IDS.VALUE) {
        return assocObjValue(editingFieldPath, safeStr, jsonObject)
      }

      return jsonObject
    },
    [jsonObject],
  )

  const addRow = React.useCallback(
    (addFieldPath, key, value) => {
      // NOTE: value can't be `Symbol('empty')`
      //       because it's imposible to have `{ [newKey]: Symbol('empty') }` object
      sortOrder.current.counter += 1

      const newKeyPath = addFieldPath.concat([key])
      const itemAddress = serializeAddress(newKeyPath)
      sortOrder.current.dict[itemAddress] = sortOrder.current.counter
      return assocObjValue(newKeyPath, value, jsonObject)
    },
    [jsonObject, sortOrder],
  )

  return children({
    addRow,
    changeValue,
    columns,
    fieldPath,
    jsonDict,
    menuFieldPath,
    removeField,
    setFieldPath,
    setMenuFieldPath,
  })
}
<|endoftext|>