<commit_before>// manages auto-incrementing of internal ID
var ATKMatcherCollection = Backbone.Collection.extend({
  initialize: function() {
    this.currentId = 0;

    var self = this;
    this.bind('add', function(model, collection) {
      self.currentId++;
      model.id = self.currentId;
      self._byId[model.id] = model;
    });
  }
});

var ATKMatcherView = Backbone.View.extend({
  initialize: function(options) {
    var self = this,
        manditoryProperties = [
          'objectPaths',
          'resourceData',
          'objectPaneCSSId',
          'objectPaneSearchCSSId',
          'objectPanePathsCSSId',
          'resourcePaneCSSId',
          'resourcePaneSearchCSSId',
          'resourcePaneItemsCSSId',
          'matchButtonCSSId',
          'matchPaneCSSId',
          'matchPanePairsCSSId',
          'saveButtonCSSId'
        ];

    // set mandatory properties
    manditoryProperties.forEach(function(property) {
      self[property] = options[property] || alert(property + ' required.');
    });

    // set up matcher template methods
    this.matcherLayoutTemplate  = _.template(options.matcherLayoutTemplate);
    this.objectPathTemplate     = _.template(options.objectPathTemplate);
    this.resourceItemTemplate   = _.template(options.resourceItemTemplate);
    this.matchItemTemplate      = _.template(options.matchItemTemplate);

    // set matcher state maintenance properties
    this.resourceCollection = new ATKMatcherCollection();
    this.selectedResourceCSSId = false; // resource currently selected in UI

    this.pairCollection = new ATKMatcherCollection();
  },

  render: function() {
    $(this.el).append(this.matcherLayoutTemplate());

    // render initial data
    this.renderObjectPaths();
    this.renderResourceData(this.resourceData);

    // activate interface behaviour
    this.activateObjectFiltering();
    this.activateResourceSelection();
    this.activateResourceFiltering();
    this.activateMatchButton();
    this.activateSaveButton();
  },

  renderObjectPaths: function() {
    var self = this,
        index = 0;

    // add each path to object pane
    this.objectPaths.forEach(function(path) {
      $('#' + self.objectPanePathsCSSId).append(
        self.objectPathTemplate({'index': index, 'path': path})
      );
      index++;
    });
  },

  renderResourceData: function(resourceData, level) {
    level = level || 0;

    var padding = '';

    for (var index = 0; index < level; index++) {
      padding = padding + '  ';
    }

    // store internal representation for reference
    this.resourceCollection.add(resourceData);

    var resourceModel = this.resourceCollection.at(this.resourceCollection.length - 1);

    // display resource
    $('#' + this.resourcePaneItemsCSSId).append(
      this.resourceItemTemplate({
        'tempId':             resourceModel.id,
        'padding':            padding,
        'title':              resourceData.title,
        'levelOfDescription': resourceData.levelOfDescription,
        'identifier':         resourceData.identifier,
        'dates':              resourceData.dates
      })
    );

    // recurse if children are found
    if (resourceData.children) {
      var self = this;

      resourceData.children.forEach(function(child) {
        self.renderResourceData(child, level + 1);
      });
    }
  },

  activateObjectFiltering: function() {
    var self = this;
    $('#' + this.objectPaneSearchCSSId + ' > input').keyup(function() {
      var filterTerm = $(this).val();

      // cycle through each object path, hiding or showing based on the filter term
      $('#' + self.objectPanePathsCSSId)
        .children()
        .each(function() {
          if ($(this).children('label').text().indexOf(filterTerm) == -1) {
            $(this).hide();
          } else {
            $(this).show();
          }
        });
    });
  },

  activateResourceSelection: function() {
    var self = this;

    $('#' + this.resourcePaneItemsCSSId + ' > tr').click(function() {
      var newSelectionCSSId = $(this).attr('id');

      // reset background of resource table rows
      $('#' + self.resourcePaneItemsCSSId + ' > tr').css('background-color', '');

      // if a new selection, highlight clicked row and set current selection to its CSS ID
      if (self.selectedResourceCSSId != newSelectionCSSId) {
        $(this).css('background-color', '#ffcc00');
        self.selectedResourceCSSId = newSelectionCSSId;
      } else {
        // ...otherwise, set to no selection
        self.selectedResourceCSSId = false;
      }
    });
  },

  activateResourceFiltering: function() {
    var self = this;

    $('#' + this.resourcePaneSearchCSSId + ' > input').keyup(function() {
      var filterTerm = $(this).val();

      // cycle through each item, hiding or showing based on the filter term
      $('#' + self.resourcePaneItemsCSSId)
        .children()
        .each(function() {
          if ($(this).text().indexOf(filterTerm) == -1) {
            $(this).hide();
          } else {
            $(this).show();
          }
        });
    });
  },

  activateMatchButton: function() {
    var self = this;

    $('#' + this.matchButtonCSSId).click(function() {
      // if a resource is highlighted, attempt to add selected paths
      if (self.selectedResourceCSSId) {
        var selectedPaths = self.getSelectedPaths();

        // if any paths have been selected
        if(selectedPaths.length) {
          selectedPaths.forEach(function(item) {
            // disable the checkbox on the path being matched
            $('#' + item.id + ' > input').attr('disabled', 'disabled');
            $('#' + item.id + ' > label').addClass('atk-matcher-disabled-object-label');

            var resource = self.resourceCollection.get(
              self.indexNumberFromCSSId(self.selectedResourceCSSId)
            );

            // store pair in collection for easy retrieval
            self.pairCollection.add({
              'objectPath':    item.path,
              'resourceIdentifier': resource.get('identifier'),
            });

            var pairModel = self.pairCollection.at(self.pairCollection.length - 1);

            var $newMatchEl = $(self.matchItemTemplate({
              'tempId': pairModel.id,
              'path': item.path,
              'title': resource.get('title'),
              'identifier': resource.get('identifier'),
              'levelOfDescription': resource.get('levelOfDescription'),
              'dates': resource.get('dates')
            }));

            // hide new pair, add it to the pane, then fade it in
            $newMatchEl.hide();
            $('#' + self.matchPanePairsCSSId).append($newMatchEl);
            $newMatchEl.fadeIn('fast');
            $('#' + self.saveButtonCSSId).show();

            (function(index, pathId) {
              $('#match_delete_' + index).click(function() {
                $('#' + pathId + ' > input').removeAttr('disabled');
                $('#' + item.id + ' > label').removeClass('atk-matcher-disabled-object-label');
                $('#match_' + index).remove();
                self.pairCollection.remove(self.pairCollection.get(index));
                if (self.pairCollection.length == 0) {
                  $('#' + self.saveButtonCSSId).hide();
                }
              });
            })(pairModel.id, item.id)
            self.matchIndex++;
          });
        } else {
          self.notify('No objects selected.');
        }
      } else {
        self.notify('No resource selected.');
      }
    });
  },

  activateSaveButton: function() {
    var self = this;
    $('#' + self.saveButtonCSSId).click(function () {
      console.log(self.pairCollection.toJSON());
    });
  },

  getSelectedPaths: function() {
    var self = this,
        selectedPaths = [];

    $('#' + self.objectPanePathsCSSId + ' > div').each(function() {
      if (
        $(this).children('input').attr('checked') == 'checked'
        && $(this).children('input').attr('disabled') != 'disabled'
      ) {
        selectedPaths.push({
          'id': $(this).attr('id'),
          'path': $(this).children('label').text()
        });
      }
    });

    return selectedPaths;
  },

  indexNumberFromCSSId: function(CSSId) {
    return parseInt(CSSId.substring(
      CSSId.indexOf('_') + 1
    ));
  },

  notify: function(text) {
    var dialog = $('
' + text + '
');

    setTimeout(function() {
      $(dialog).dialog('close');
    }, 2000);

    dialog.dialog({
      title: 'Warning',
      width: 640,
      height: 200,
      buttons: [{
        text: 'Dismiss',
        click: function() { $(this).dialog('close'); }
      }]
    });
  }
});
<commit_msg>Added unchecking of object in deleted pair.<commit_after>// manages auto-incrementing of internal ID
var ATKMatcherCollection = Backbone.Collection.extend({
  initialize: function() {
    this.currentId = 0;

    var self = this;
    this.bind('add', function(model, collection) {
      self.currentId++;
      model.id = self.currentId;
      self._byId[model.id] = model;
    });
  }
});

var ATKMatcherView = Backbone.View.extend({
  initialize: function(options) {
    var self = this,
        manditoryProperties = [
          'objectPaths',
          'resourceData',
          'objectPaneCSSId',
          'objectPaneSearchCSSId',
          'objectPanePathsCSSId',
          'resourcePaneCSSId',
          'resourcePaneSearchCSSId',
          'resourcePaneItemsCSSId',
          'matchButtonCSSId',
          'matchPaneCSSId',
          'matchPanePairsCSSId',
          'saveButtonCSSId'
        ];

    // set mandatory properties
    manditoryProperties.forEach(function(property) {
      self[property] = options[property] || alert(property + ' required.');
    });

    // set up matcher template methods
    this.matcherLayoutTemplate  = _.template(options.matcherLayoutTemplate);
    this.objectPathTemplate     = _.template(options.objectPathTemplate);
    this.resourceItemTemplate   = _.template(options.resourceItemTemplate);
    this.matchItemTemplate      = _.template(options.matchItemTemplate);

    // set matcher state maintenance properties
    this.resourceCollection = new ATKMatcherCollection();
    this.selectedResourceCSSId = false; // resource currently selected in UI

    this.pairCollection = new ATKMatcherCollection();
  },

  render: function() {
    $(this.el).append(this.matcherLayoutTemplate());

    // render initial data
    this.renderObjectPaths();
    this.renderResourceData(this.resourceData);

    // activate interface behaviour
    this.activateObjectFiltering();
    this.activateResourceSelection();
    this.activateResourceFiltering();
    this.activateMatchButton();
    this.activateSaveButton();
  },

  renderObjectPaths: function() {
    var self = this,
        index = 0;

    // add each path to object pane
    this.objectPaths.forEach(function(path) {
      $('#' + self.objectPanePathsCSSId).append(
        self.objectPathTemplate({'index': index, 'path': path})
      );
      index++;
    });
  },

  renderResourceData: function(resourceData, level) {
    level = level || 0;

    var padding = '';

    for (var index = 0; index < level; index++) {
      padding = padding + '  ';
    }

    // store internal representation for reference
    this.resourceCollection.add(resourceData);

    var resourceModel = this.resourceCollection.at(this.resourceCollection.length - 1);

    // display resource
    $('#' + this.resourcePaneItemsCSSId).append(
      this.resourceItemTemplate({
        'tempId':             resourceModel.id,
        'padding':            padding,
        'title':              resourceData.title,
        'levelOfDescription': resourceData.levelOfDescription,
        'identifier':         resourceData.identifier,
        'dates':              resourceData.dates
      })
    );

    // recurse if children are found
    if (resourceData.children) {
      var self = this;

      resourceData.children.forEach(function(child) {
        self.renderResourceData(child, level + 1);
      });
    }
  },

  activateObjectFiltering: function() {
    var self = this;
    $('#' + this.objectPaneSearchCSSId + ' > input').keyup(function() {
      var filterTerm = $(this).val();

      // cycle through each object path, hiding or showing based on the filter term
      $('#' + self.objectPanePathsCSSId)
        .children()
        .each(function() {
          if ($(this).children('label').text().indexOf(filterTerm) == -1) {
            $(this).hide();
          } else {
            $(this).show();
          }
        });
    });
  },

  activateResourceSelection: function() {
    var self = this;

    $('#' + this.resourcePaneItemsCSSId + ' > tr').click(function() {
      var newSelectionCSSId = $(this).attr('id');

      // reset background of resource table rows
      $('#' + self.resourcePaneItemsCSSId + ' > tr').css('background-color', '');

      // if a new selection, highlight clicked row and set current selection to its CSS ID
      if (self.selectedResourceCSSId != newSelectionCSSId) {
        $(this).css('background-color', '#ffcc00');
        self.selectedResourceCSSId = newSelectionCSSId;
      } else {
        // ...otherwise, set to no selection
        self.selectedResourceCSSId = false;
      }
    });
  },

  activateResourceFiltering: function() {
    var self = this;

    $('#' + this.resourcePaneSearchCSSId + ' > input').keyup(function() {
      var filterTerm = $(this).val();

      // cycle through each item, hiding or showing based on the filter term
      $('#' + self.resourcePaneItemsCSSId)
        .children()
        .each(function() {
          if ($(this).text().indexOf(filterTerm) == -1) {
            $(this).hide();
          } else {
            $(this).show();
          }
        });
    });
  },

  activateMatchButton: function() {
    var self = this;

    $('#' + this.matchButtonCSSId).click(function() {
      // if a resource is highlighted, attempt to add selected paths
      if (self.selectedResourceCSSId) {
        var selectedPaths = self.getSelectedPaths();

        // if any paths have been selected
        if(selectedPaths.length) {
          selectedPaths.forEach(function(item) {
            // disable the checkbox on the path being matched
            $('#' + item.id + ' > input').attr('disabled', 'disabled');
            $('#' + item.id + ' > label').addClass('atk-matcher-disabled-object-label');

            var resource = self.resourceCollection.get(
              self.indexNumberFromCSSId(self.selectedResourceCSSId)
            );

            // store pair in collection for easy retrieval
            self.pairCollection.add({
              'objectPath':    item.path,
              'resourceIdentifier': resource.get('identifier'),
            });

            var pairModel = self.pairCollection.at(self.pairCollection.length - 1);

            var $newMatchEl = $(self.matchItemTemplate({
              'tempId': pairModel.id,
              'path': item.path,
              'title': resource.get('title'),
              'identifier': resource.get('identifier'),
              'levelOfDescription': resource.get('levelOfDescription'),
              'dates': resource.get('dates')
            }));

            // hide new pair, add it to the pane, then fade it in
            $newMatchEl.hide();
            $('#' + self.matchPanePairsCSSId).append($newMatchEl);
            $newMatchEl.fadeIn('fast');
            $('#' + self.saveButtonCSSId).show();

            // enable deletion of match
            (function(index, pathId) {
              $('#match_delete_' + index).click(function() {
                // enable checkbox and remove greying out of associated label
                $('#' + pathId + ' > input').removeAttr('disabled');
                $('#' + item.id + ' > label').removeClass('atk-matcher-disabled-object-label');

                // un-check checkbox
                $('#' + item.id + ' > input').removeAttr('checked');

                // remove visual and internal pair representations
                $('#match_' + index).remove();
                self.pairCollection.remove(self.pairCollection.get(index));

                // hide save button if no pairs now exist
                if (self.pairCollection.length == 0) {
                  $('#' + self.saveButtonCSSId).hide();
                }
              });
            })(pairModel.id, item.id)
            self.matchIndex++;
          });
        } else {
          self.notify('No objects selected.');
        }
      } else {
        self.notify('No resource selected.');
      }
    });
  },

  activateSaveButton: function() {
    var self = this;
    $('#' + self.saveButtonCSSId).click(function () {
      console.log(self.pairCollection.toJSON());
    });
  },

  getSelectedPaths: function() {
    var self = this,
        selectedPaths = [];

    $('#' + self.objectPanePathsCSSId + ' > div').each(function() {
      if (
        $(this).children('input').attr('checked') == 'checked'
        && $(this).children('input').attr('disabled') != 'disabled'
      ) {
        selectedPaths.push({
          'id': $(this).attr('id'),
          'path': $(this).children('label').text()
        });
      }
    });

    return selectedPaths;
  },

  indexNumberFromCSSId: function(CSSId) {
    return parseInt(CSSId.substring(
      CSSId.indexOf('_') + 1
    ));
  },

  notify: function(text) {
    var dialog = $('
' + text + '
');

    setTimeout(function() {
      $(dialog).dialog('close');
    }, 2000);

    dialog.dialog({
      title: 'Warning',
      width: 640,
      height: 200,
      buttons: [{
        text: 'Dismiss',
        click: function() { $(this).dialog('close'); }
      }]
    });
  }
});
<|endoftext|>


<commit_before>define(
  'ephox.darwin.demo.DarwinTableDemo',

  [
    'ephox.darwin.api.InputHandlers',
    'ephox.fred.PlatformDetection',
    'ephox.fussy.api.WindowSelection',
    'ephox.peanut.Fun',
    'ephox.perhaps.Option',
    'ephox.sugar.api.Attr',
    'ephox.sugar.api.Body',
    'ephox.sugar.api.Compare',
    'ephox.sugar.api.DomEvent',
    'ephox.sugar.api.Element',
    'ephox.sugar.api.Insert',
    'ephox.sugar.api.Replication',
    'ephox.sugar.api.SelectorFind',
    'global!Math',
    'global!document'
  ],

  function (InputHandlers, PlatformDetection, WindowSelection, Fun, Option, Attr, Body, Compare, DomEvent, Element, Insert, Replication, SelectorFind, Math, document) {
    return function () {

      var detection = PlatformDetection.detect();

      var ephoxUi = SelectorFind.first('#ephox-ui').getOrDie();
      Attr.set(ephoxUi, 'contenteditable', 'true');

      var style = Element.fromHtml(
        ''
      );

      var table = Element.fromHtml(
        '
' +
          '
' +
            '
' +
              'A1	' +
              'B1
	' +
              'C1

	' +
              'D1	' +
            '
' +
            '
' +
              'A2	' +
              'B2

	' +
              '
C2

More
	' +
              '
D2	' +
            '
' +
            '
' +
              'A3	' +
              '
	' +
              '
	' +
              'D3	' +
            '
' +
            '
' +
              'A4	' +
              '
	' +
              '
	' +
              'D4	' +
            '
' +
          '
' +
        '
'
      );

      Insert.append(ephoxUi, table);
      Insert.append(Element.fromDom(document.head), style);

      var cloneDiv = Element.fromTag('div');
      Attr.set(cloneDiv, 'contenteditable', 'true');
      var clone = Replication.deep(table);
      Insert.append(cloneDiv, clone);
      Insert.append(Body.body(), cloneDiv);

      Insert.append(Body.body(), Element.fromHtml('(0, 0)'));
      DomEvent.bind(Body.body(), 'mousemove', function (event) {
        document.querySelector('#coords').innerHTML = '(' + event.raw().clientX + ', ' + event.raw().clientY + ')';
      });

      var mouseHandlers = InputHandlers.mouse(ephoxUi);
      var keyHandlers = InputHandlers.keyboard(window, ephoxUi, Fun.curry(Compare.eq, table));

      DomEvent.bind(ephoxUi, 'mousedown', mouseHandlers.mousedown);
      DomEvent.bind(ephoxUi, 'mouseover', mouseHandlers.mouseover);
      DomEvent.bind(ephoxUi, 'mouseup', mouseHandlers.mouseup);

      var handleResponse = function (event, response) {
        if (response.kill()) event.kill();
        response.selection().each(function (ns) {
          WindowSelection.set(window, ns);
        });
      };

      DomEvent.bind(ephoxUi, 'keyup', function (event) {
        // Note, this is an optimisation.
        if (event.raw().shiftKey && event.raw().which >= 37 && event.raw().which <= 40) {
          WindowSelection.get(window).each(function (sel) {
            keyHandlers.keyup(event, sel.start(), sel.soffset(), sel.finish(), sel.foffset()).each(function (response) {
              handleResponse(event, response);
            });
          }, Fun.noop);
        }
      });

      DomEvent.bind(ephoxUi, 'keydown', function (event) {
        // This might get expensive.
        WindowSelection.get(window).each(function (sel) {
          keyHandlers.keydown(event, sel.finish(), sel.foffset()).each(function (response) {
            handleResponse(event, response);
          });
        });
      });
    };
  }
);<commit_msg>TBIO-2621: colspan in demo<commit_after>define(
  'ephox.darwin.demo.DarwinTableDemo',

  [
    'ephox.darwin.api.InputHandlers',
    'ephox.fred.PlatformDetection',
    'ephox.fussy.api.WindowSelection',
    'ephox.peanut.Fun',
    'ephox.perhaps.Option',
    'ephox.sugar.api.Attr',
    'ephox.sugar.api.Body',
    'ephox.sugar.api.Compare',
    'ephox.sugar.api.DomEvent',
    'ephox.sugar.api.Element',
    'ephox.sugar.api.Insert',
    'ephox.sugar.api.Replication',
    'ephox.sugar.api.SelectorFind',
    'global!Math',
    'global!document'
  ],

  function (InputHandlers, PlatformDetection, WindowSelection, Fun, Option, Attr, Body, Compare, DomEvent, Element, Insert, Replication, SelectorFind, Math, document) {
    return function () {

      var detection = PlatformDetection.detect();

      var ephoxUi = SelectorFind.first('#ephox-ui').getOrDie();
      Attr.set(ephoxUi, 'contenteditable', 'true');

      var style = Element.fromHtml(
        ''
      );

      var table = Element.fromHtml(
        '
' +
          '
' +
            '
' +
              'A1	' +
              'B1
	' +
              'C1

	' +
              // 'D1	' +
            '
' +
            '
' +
              'A2	' +
              'B2

	' +
              '
C2

More
	' +
              '
D2	' +
            '
' +
            '
' +
              'A3	' +
              '
	' +
              '
	' +
              'D3	' +
            '
' +
            '
' +
              'A4	' +
              '
	' +
              '
	' +
              'D4	' +
            '
' +
          '
' +
        '
'
      );

      Insert.append(ephoxUi, table);
      Insert.append(Element.fromDom(document.head), style);

      var cloneDiv = Element.fromTag('div');
      Attr.set(cloneDiv, 'contenteditable', 'true');
      var clone = Replication.deep(table);
      Insert.append(cloneDiv, clone);
      Insert.append(Body.body(), cloneDiv);

      Insert.append(Body.body(), Element.fromHtml('(0, 0)'));
      DomEvent.bind(Body.body(), 'mousemove', function (event) {
        document.querySelector('#coords').innerHTML = '(' + event.raw().clientX + ', ' + event.raw().clientY + ')';
      });

      var mouseHandlers = InputHandlers.mouse(ephoxUi);
      var keyHandlers = InputHandlers.keyboard(window, ephoxUi, Fun.curry(Compare.eq, table));

      DomEvent.bind(ephoxUi, 'mousedown', mouseHandlers.mousedown);
      DomEvent.bind(ephoxUi, 'mouseover', mouseHandlers.mouseover);
      DomEvent.bind(ephoxUi, 'mouseup', mouseHandlers.mouseup);

      var handleResponse = function (event, response) {
        if (response.kill()) event.kill();
        response.selection().each(function (ns) {
          WindowSelection.set(window, ns);
        });
      };

      DomEvent.bind(ephoxUi, 'keyup', function (event) {
        // Note, this is an optimisation.
        if (event.raw().shiftKey && event.raw().which >= 37 && event.raw().which <= 40) {
          WindowSelection.get(window).each(function (sel) {
            keyHandlers.keyup(event, sel.start(), sel.soffset(), sel.finish(), sel.foffset()).each(function (response) {
              handleResponse(event, response);
            });
          }, Fun.noop);
        }
      });

      DomEvent.bind(ephoxUi, 'keydown', function (event) {
        // This might get expensive.
        WindowSelection.get(window).each(function (sel) {
          keyHandlers.keydown(event, sel.finish(), sel.foffset()).each(function (response) {
            handleResponse(event, response);
          });
        });
      });
    };
  }
);<|endoftext|>


<commit_before>'use strict';

var math = require( '../../../math' );

var CRp = {};

CRp.getElementTextureCache = function( ele, bb, pxRatio ){
  var r = this;
  var rs = ele._private.rscratch;
  var minTxrH = 50; // the size of the texture cache for small height eles (special case)
  var txrStepH = 100; // the min size of the regular cache, and the size it increases with each step up
  var txrH; // which texture height this ele belongs to
  var maxA = 1e7; // the max area each texture should support
  var zoom = r.cy.zoom();
  var lvl = Math.ceil( Math.log2( zoom * pxRatio ) );
  var scale = Math.pow( 2, lvl );
  var eleScaledH = bb.h * scale;
  var eleScaledW = bb.w * scale;

  if( eleScaledH <= minTxrH ){
    txrH = minTxrH;
  } else if( eleScaledH <= txrStepH ){
    txrH = txrStepH;
  } else {
    txrH = Math.ceil( eleScaledH / txrStepH ) * txrStepH;
  }

  var txrQ = r.data.eleImgCaches[ txrH ] = r.data.eleImgCaches[ txrH ] || [];

  // first try the second last one in case it has space at the end
  var txr = txrQ[ txrQ.length - 2 ];

  var addNewTxr = function(){
    var txr = {};

    txrQ.push( txr );

    txr.height = txrH;
    txr.width = Math.ceil( maxA / txrH );
    txr.usedWidth = 0;

    txr.canvas = document.createElement('canvas');
    txr.canvas.width = txr.width;
    txr.canvas.height = txr.height;

    txr.context = txr.canvas.getContext('2d');

    return txr;
  };

  // try the last one if there is no second last one
  if( !txr ){
    txr = txrQ[ txrQ.length - 1 ];
  }

  // if the last one doesn't exist, we need a first one
  if( !txr ){
    txr = addNewTxr();
  }

  // if there's no room in the current texture, we need a new one
  if( txr.width - txr.usedWidth < eleScaledW ){
    txr = addNewTxr();
  }

  txr.context.translate( txr.usedWidth, 0 );
  txr.context.scale( scale, scale );

  r.drawElement( txr.context, ele, bb );

  txr.context.scale( 1/scale, 1/scale );
  txr.context.translate( -txr.usedWidth, 0 );

  var eleCache = caches[lvl] = {
    x: txr.usedWidth,
    texture: txr,
    level: lvl,
    width: eleScaledW,
    height: eleScaledH
  };

  txr.usedWidth += eleScaledW;

  return eleCache;
};

CRp.invalidateElementTextureCache = function( ele ){
  // TODO

  // for each cache the ele has

    // decrement used width in cache

    // invalidate all entries in the cache if the cache size is small

    // remove the cache references from the element
};

CRp.drawCachedElementShared = function( context, ele, pxRatio, extent ){
  var r = this;
  var _p = ele._private;
  var rs = _p.rscratch;
  var bb = ele.boundingBox();
  var cache = r.getElementTextureCache( ele, bb, pxRatio );

  if( math.boundingBoxesIntersect( bb, extent ) ){
    context.drawImage( cache.texture.canvas, cache.x, 0, cache.width, cache.height, bb.x1, bb.y1, bb.w, bb.h );
  }
};

CRp.drawCachedElementIndiv = function( context, ele, pxRatio, extent ){
  var r = this;
  var _p = ele._private;
  var rs = _p.rscratch;
  var caches = rs.imgCaches = rs.imgCaches || {};
  var bb = ele.boundingBox();
  var zoom = r.cy.zoom();
  var lvl = Math.ceil( Math.log2( zoom * pxRatio ) );
  var cacheZoom = Math.pow( 2, lvl );
  var cache = caches[lvl];

  if( !cache ){
    var lvlCanvas = document.createElement('canvas');
    var lvlContext = lvlCanvas.getContext('2d');
    var scale = cacheZoom;

    lvlCanvas.width = scale * bb.w;
    lvlCanvas.height = scale * bb.h;

    lvlContext.scale( scale, scale );

    r.drawElement( lvlContext, ele, bb );

    lvlContext.scale( 1/scale, 1/scale );

    cache = caches[lvl] = {
      canvas: lvlCanvas,
      context: lvlContext
    };
  }

  if( math.boundingBoxesIntersect( bb, extent ) ){
    context.drawImage( cache.canvas, bb.x1, bb.y1, bb.w, bb.h );
  }
};

// switch to compare individual and shared cache methods
CRp.drawCachedElement = CRp.drawCachedElementIndiv;

CRp.drawElement = function( context, ele, shiftToOriginWithBb ){
  var r = this;

  if( ele.isNode() ){
    r.drawNode( context, ele, shiftToOriginWithBb );
  } else {
    r.drawEdge( context, ele, shiftToOriginWithBb );
  }
};

module.exports = CRp;
<commit_msg>shared texture caching working on par with individual caching -- but shared now works very well with large numbers of elements<commit_after>'use strict';

var math = require( '../../../math' );

var CRp = {};

CRp.getElementTextureCache = function( ele, bb, pxRatio ){
  var r = this;
  var rs = ele._private.rscratch;
  var minTxrH = 50; // the size of the texture cache for small height eles (special case)
  var txrStepH = 100; // the min size of the regular cache, and the size it increases with each step up
  var txrH; // which texture height this ele belongs to
  var maxA = 1e6; // the max area each texture should support
  var zoom = r.cy.zoom();
  var lvl = Math.ceil( Math.log2( zoom * pxRatio ) );

  if( lvl < -2 ){
    lvl = -2; // -2 => 0.25 scale ; when scaling smaller than that we don't need to re-render
  } else if( zoom >= 4 ){ // TODO this value may need tweaking/optimising
    return null; // when larger than this scale just render directly (caching is not helpful)
  }

  var scale = Math.pow( 2, lvl );
  var eleScaledH = bb.h * scale;
  var eleScaledW = bb.w * scale;
  var caches = rs.imgCaches = rs.imgCaches || [];
  var eleCache = caches[lvl];

  if( eleCache ){
    return eleCache;
  }

  if( eleScaledH <= minTxrH ){
    txrH = minTxrH;
  } else if( eleScaledH <= txrStepH ){
    txrH = txrStepH;
  } else {
    txrH = Math.ceil( eleScaledH / txrStepH ) * txrStepH;
  }

  var txrQ = r.data.eleImgCaches[ txrH ] = r.data.eleImgCaches[ txrH ] || [];

  // first try the second last one in case it has space at the end
  var txr = txrQ[ txrQ.length - 2 ];

  var addNewTxr = function(){
    var txr = {};

    txrQ.push( txr );

    txr.height = txrH;
    txr.width = Math.max( 1000, eleScaledW ); // TODO this size needs optimising!!
    txr.usedWidth = 0;

    txr.canvas = document.createElement('canvas');
    txr.canvas.width = txr.width;
    txr.canvas.height = txr.height;

    txr.context = txr.canvas.getContext('2d');

    return txr;
  };

  // try the last one if there is no second last one
  if( !txr ){
    txr = txrQ[ txrQ.length - 1 ];
  }

  // if the last one doesn't exist, we need a first one
  if( !txr ){
    txr = addNewTxr();
  }

  // if there's no room in the current texture, we need a new one
  if( txr.width - txr.usedWidth < eleScaledW ){
    txr = addNewTxr();
  }

  txr.context.translate( txr.usedWidth, 0 );
  txr.context.scale( scale, scale );

  r.drawElement( txr.context, ele, bb );

  txr.context.scale( 1/scale, 1/scale );
  txr.context.translate( -txr.usedWidth, 0 );

  eleCache = caches[lvl] = {
    x: txr.usedWidth,
    texture: txr,
    level: lvl,
    scale: scale,
    width: eleScaledW,
    height: eleScaledH
  };

  txr.usedWidth += eleScaledW;

  return eleCache;
};

CRp.invalidateElementTextureCache = function( ele ){
  // TODO

  // for each cache the ele has

    // decrement used width in cache

    // invalidate all entries in the cache if the cache size is small

    // remove the cache references from the element
};

CRp.drawCachedElementShared = function( context, ele, pxRatio, extent ){
  var r = this;
  var _p = ele._private;
  var rs = _p.rscratch;
  var bb = ele.boundingBox();

  if( math.boundingBoxesIntersect( bb, extent ) ){
    var cache = r.getElementTextureCache( ele, bb, pxRatio );

    if( cache ){
      context.drawImage( cache.texture.canvas, cache.x, 0, cache.width, cache.height, bb.x1, bb.y1, bb.w, bb.h );
    } else { // if the element is not cacheable, then draw directly
      r.drawElement( context, ele );
    }
  }
};

CRp.drawCachedElementIndiv = function( context, ele, pxRatio, extent ){
  var r = this;
  var _p = ele._private;
  var rs = _p.rscratch;
  var caches = rs.imgCaches = rs.imgCaches || {};
  var bb = ele.boundingBox();
  var zoom = r.cy.zoom();
  var lvl = Math.ceil( Math.log2( zoom * pxRatio ) );
  var cacheZoom = Math.pow( 2, lvl );
  var cache = caches[lvl];

  if( !cache ){
    var lvlCanvas = document.createElement('canvas');
    var lvlContext = lvlCanvas.getContext('2d');
    var scale = cacheZoom;

    lvlCanvas.width = scale * bb.w;
    lvlCanvas.height = scale * bb.h;

    lvlContext.scale( scale, scale );

    r.drawElement( lvlContext, ele, bb );

    lvlContext.scale( 1/scale, 1/scale );

    cache = caches[lvl] = {
      canvas: lvlCanvas,
      context: lvlContext
    };
  }

  if( math.boundingBoxesIntersect( bb, extent ) ){
    context.drawImage( cache.canvas, bb.x1, bb.y1, bb.w, bb.h );
  }
};

// switch to compare individual and shared cache methods
CRp.drawCachedElement = CRp.drawCachedElementShared;

CRp.drawElement = function( context, ele, shiftToOriginWithBb ){
  var r = this;

  if( ele.isNode() ){
    r.drawNode( context, ele, shiftToOriginWithBb );
  } else {
    r.drawEdge( context, ele, shiftToOriginWithBb );
  }
};

module.exports = CRp;
<|endoftext|>


<commit_before>/*
  backgrid-select-all
  http://github.com/wyuenho/backgrid

  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
  Licensed under the MIT @license.
*/
(function (window, $, _, Backbone, Backgrid)  {

  /**
     Renders a checkbox for row selection.

     @class Backgrid.Extension.SelectRowCell
     @extends Backbone.View
  */
  var SelectRowCell = Backgrid.Extension.SelectRowCell = Backbone.View.extend({

    /** @property */
    className: "select-row-cell",

    /** @property */
    tagName: "td",

    /** @property */
    events: {
      "keydown :checkbox": "onKeydown",
      "change :checkbox": "onChange",
      "click :checkbox": "enterEditMode"
    },

    /**
       Initializer. If the underlying model triggers a `select` event, this cell
       will change its checked value according to the event's `selected` value.

       @param {Object} options
       @param {Backgrid.Column} options.column
       @param {Backbone.Model} options.model
    */
    initialize: function (options) {
      Backgrid.requireOptions(options, ["model", "column"]);

      this.column = options.column;
      if (!(this.column instanceof Backgrid.Column)) {
        this.column = new Backgrid.Column(this.column);
      }

      this.listenTo(this.model, "backgrid:select", function (model, selected) {
        this.$el.find(":checkbox").prop("checked", selected).change();
      });

    },

    /**
       Focuses the checkbox.
    */
    enterEditMode: function () {
      this.$el.find(":checkbox").focus();
    },

    /**
       Unfocuses the checkbox.
    */
    exitEditMode: function () {
      this.$el.find(":checkbox").blur();
    },

    /**
       Process keyboard navigation.
    */
    onKeydown: function (e) {
      var command = new Backgrid.Command(e);
      if (command.passThru()) return true; // skip ahead to `change`
      if (command.cancel()) {
        e.stopPropagation();
        this.$el.find(":checkbox").blur();
      }
      else if (command.save() || command.moveLeft() || command.moveRight() ||
               command.moveUp() || command.moveDown()) {
        e.preventDefault();
        e.stopPropagation();
        this.model.trigger("backgrid:edited", this.model, this.column, command);
      }
    },

    /**
       When the checkbox's value changes, this method will trigger a Backbone
       `backgrid:selected` event with a reference of the model and the
       checkbox's `checked` value.
    */
    onChange: function (e) {
      var checked = $(e.target).prop('checked');
      this.$el.closest('tr').toggleClass('selected', checked);
      this.model.trigger("backgrid:selected", this.model, checked);
    },

    /**
       Renders a checkbox in a table cell.
    */
    render: function () {
      this.$el.empty().append('<input tabindex="-1" type="checkbox" />');
      this.delegateEvents();
      return this;
    }

  });

  /**
     Renders a checkbox to select all rows on the current page.

     @class Backgrid.Extension.SelectAllHeaderCell
     @extends Backgrid.Extension.SelectRowCell
  */
  var SelectAllHeaderCell = Backgrid.Extension.SelectAllHeaderCell = SelectRowCell.extend({

    /** @property */
    className: "select-all-header-cell",

    /** @property */
    tagName: "th",

    /**
       Initializer. When this cell's checkbox is checked, a Backbone
       `backgrid:select` event will be triggered for each model for the current
       page in the underlying collection. If a `SelectRowCell` instance exists
       for the rows representing the models, they will check themselves. If any
       of the SelectRowCell instances trigger a Backbone `backgrid:selected`
       event with a `false` value, this cell will uncheck its checkbox. In the
       event of a Backbone `backgrid:refresh` event, which is triggered when the
       body refreshes its rows, which can happen under a number of conditions
       such as paging or the columns were reset, this cell will still remember
       the previously selected models and trigger a Backbone `backgrid:select`
       event on them such that the SelectRowCells can recheck themselves upon
       refreshing.

       @param {Object} options
       @param {Backgrid.Column} options.column
       @param {Backbone.Collection} options.collection
    */
    initialize: function (options) {
      Backgrid.requireOptions(options, ["column", "collection"]);

      this.column = options.column;
      if (!(this.column instanceof Backgrid.Column)) {
        this.column = new Backgrid.Column(this.column);
      }

      var collection = this.collection;
      var selectedModels = this.selectedModels = {};
      this.listenTo(collection, "backgrid:selected", function (model, selected) {
        if (selected) selectedModels[model.id || model.cid] = model;
        else {
          delete selectedModels[model.id || model.cid];
          this.$el.find(":checkbox").prop("checked", false);
        }
      });

      this.listenTo(collection, "remove", function (model) {
        delete selectedModels[model.cid];
      });

      this.listenTo(collection, "backgrid:refresh", function () {
        this.$el.find(":checkbox").prop("checked", false);
        for (var i = 0; i < collection.length; i++) {
          var model = collection.at(i);
          if (selectedModels[model.id || model.cid]) {
            model.trigger('backgrid:select', model, true);
          }
        }
      });
    },

    /**
       Progagates the checked value of this checkbox to all the models of the
       underlying collection by triggering a Backbone `backgrid:select` event on
       the models themselves, passing each model and the current `checked` value
       of the checkbox in each event.
    */
    onChange: function (e) {
      var checked = $(e.target).prop("checked");

      var collection = this.collection;
      collection.each(function (model) {
        model.trigger("backgrid:select", model, checked);
      });
    }

  });

  /**
     Convenient method to retrieve a list of selected models. This method only
     exists when the `SelectAll` extension has been included.

     @member Backgrid.Grid
     @return {Array.<Backbone.Model>}
  */
  Backgrid.Grid.prototype.getSelectedModels = function () {
    var selectAllHeaderCell;
    var headerCells = this.header.row.cells;
    for (var i = 0, l = headerCells.length; i < l; i++) {
      var headerCell = headerCells[i];
      if (headerCell instanceof SelectAllHeaderCell) {
        selectAllHeaderCell = headerCell;
        break;
      }
    }

    var result = [];
    if (selectAllHeaderCell) {
      for (var modelId in selectAllHeaderCell.selectedModels) {
        result.push(this.collection.get(modelId));
      }
    }

    return result;
  };

}(window, jQuery, _, Backbone, Backgrid));
<commit_msg>use a more efficient selector for adding 'selected' class in selectrowcell<commit_after>/*
  backgrid-select-all
  http://github.com/wyuenho/backgrid

  Copyright (c) 2013 Jimmy Yuen Ho Wong and contributors
  Licensed under the MIT @license.
*/
(function (window, $, _, Backbone, Backgrid)  {

  /**
     Renders a checkbox for row selection.

     @class Backgrid.Extension.SelectRowCell
     @extends Backbone.View
  */
  var SelectRowCell = Backgrid.Extension.SelectRowCell = Backbone.View.extend({

    /** @property */
    className: "select-row-cell",

    /** @property */
    tagName: "td",

    /** @property */
    events: {
      "keydown :checkbox": "onKeydown",
      "change :checkbox": "onChange",
      "click :checkbox": "enterEditMode"
    },

    /**
       Initializer. If the underlying model triggers a `select` event, this cell
       will change its checked value according to the event's `selected` value.

       @param {Object} options
       @param {Backgrid.Column} options.column
       @param {Backbone.Model} options.model
    */
    initialize: function (options) {
      Backgrid.requireOptions(options, ["model", "column"]);

      this.column = options.column;
      if (!(this.column instanceof Backgrid.Column)) {
        this.column = new Backgrid.Column(this.column);
      }

      this.listenTo(this.model, "backgrid:select", function (model, selected) {
        this.$el.find(":checkbox").prop("checked", selected).change();
      });

    },

    /**
       Focuses the checkbox.
    */
    enterEditMode: function () {
      this.$el.find(":checkbox").focus();
    },

    /**
       Unfocuses the checkbox.
    */
    exitEditMode: function () {
      this.$el.find(":checkbox").blur();
    },

    /**
       Process keyboard navigation.
    */
    onKeydown: function (e) {
      var command = new Backgrid.Command(e);
      if (command.passThru()) return true; // skip ahead to `change`
      if (command.cancel()) {
        e.stopPropagation();
        this.$el.find(":checkbox").blur();
      }
      else if (command.save() || command.moveLeft() || command.moveRight() ||
               command.moveUp() || command.moveDown()) {
        e.preventDefault();
        e.stopPropagation();
        this.model.trigger("backgrid:edited", this.model, this.column, command);
      }
    },

    /**
       When the checkbox's value changes, this method will trigger a Backbone
       `backgrid:selected` event with a reference of the model and the
       checkbox's `checked` value.
    */
    onChange: function (e) {
      var checked = $(e.target).prop('checked');
      this.$el.parent().toggleClass('selected', checked);
      this.model.trigger("backgrid:selected", this.model, checked);
    },

    /**
       Renders a checkbox in a table cell.
    */
    render: function () {
      this.$el.empty().append('<input tabindex="-1" type="checkbox" />');
      this.delegateEvents();
      return this;
    }

  });

  /**
     Renders a checkbox to select all rows on the current page.

     @class Backgrid.Extension.SelectAllHeaderCell
     @extends Backgrid.Extension.SelectRowCell
  */
  var SelectAllHeaderCell = Backgrid.Extension.SelectAllHeaderCell = SelectRowCell.extend({

    /** @property */
    className: "select-all-header-cell",

    /** @property */
    tagName: "th",

    /**
       Initializer. When this cell's checkbox is checked, a Backbone
       `backgrid:select` event will be triggered for each model for the current
       page in the underlying collection. If a `SelectRowCell` instance exists
       for the rows representing the models, they will check themselves. If any
       of the SelectRowCell instances trigger a Backbone `backgrid:selected`
       event with a `false` value, this cell will uncheck its checkbox. In the
       event of a Backbone `backgrid:refresh` event, which is triggered when the
       body refreshes its rows, which can happen under a number of conditions
       such as paging or the columns were reset, this cell will still remember
       the previously selected models and trigger a Backbone `backgrid:select`
       event on them such that the SelectRowCells can recheck themselves upon
       refreshing.

       @param {Object} options
       @param {Backgrid.Column} options.column
       @param {Backbone.Collection} options.collection
    */
    initialize: function (options) {
      Backgrid.requireOptions(options, ["column", "collection"]);

      this.column = options.column;
      if (!(this.column instanceof Backgrid.Column)) {
        this.column = new Backgrid.Column(this.column);
      }

      var collection = this.collection;
      var selectedModels = this.selectedModels = {};
      this.listenTo(collection, "backgrid:selected", function (model, selected) {
        if (selected) selectedModels[model.id || model.cid] = model;
        else {
          delete selectedModels[model.id || model.cid];
          this.$el.find(":checkbox").prop("checked", false);
        }
      });

      this.listenTo(collection, "remove", function (model) {
        delete selectedModels[model.cid];
      });

      this.listenTo(collection, "backgrid:refresh", function () {
        this.$el.find(":checkbox").prop("checked", false);
        for (var i = 0; i < collection.length; i++) {
          var model = collection.at(i);
          if (selectedModels[model.id || model.cid]) {
            model.trigger('backgrid:select', model, true);
          }
        }
      });
    },

    /**
       Progagates the checked value of this checkbox to all the models of the
       underlying collection by triggering a Backbone `backgrid:select` event on
       the models themselves, passing each model and the current `checked` value
       of the checkbox in each event.
    */
    onChange: function (e) {
      var checked = $(e.target).prop("checked");

      var collection = this.collection;
      collection.each(function (model) {
        model.trigger("backgrid:select", model, checked);
      });
    }

  });

  /**
     Convenient method to retrieve a list of selected models. This method only
     exists when the `SelectAll` extension has been included.

     @member Backgrid.Grid
     @return {Array.<Backbone.Model>}
  */
  Backgrid.Grid.prototype.getSelectedModels = function () {
    var selectAllHeaderCell;
    var headerCells = this.header.row.cells;
    for (var i = 0, l = headerCells.length; i < l; i++) {
      var headerCell = headerCells[i];
      if (headerCell instanceof SelectAllHeaderCell) {
        selectAllHeaderCell = headerCell;
        break;
      }
    }

    var result = [];
    if (selectAllHeaderCell) {
      for (var modelId in selectAllHeaderCell.selectedModels) {
        result.push(this.collection.get(modelId));
      }
    }

    return result;
  };

}(window, jQuery, _, Backbone, Backgrid));
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.demos.net.nap.web.model',
  name: 'Messageboard',

  tableColumns: [
    'mark', 'title', 'createdDate', 'creator', 'attachment', 'hits'
  ],

  imports: [
    'blobService',
    'messageboardDAO',
    'user'
  ],

  javaImports: [ 'java.util.Date' ],

  requires: [
    'foam.blob.BlobBlob',
    'foam.nanos.fs.File',
    'foam.u2.dialog.NotificationMessage'
  ],

  properties: [
    {
      class: 'Long',
      name: 'id'
    },
    {
      class: 'Boolean',
      name: 'mark',
      tableCellFormatter: function(value, obj, property) {
        this
          .start()
            .call(function() {
              if ( value ) { this.tag({ class: 'foam.u2.tag.Image', data: 'images/star.svg' }).style({'width': '25'}); }
            })
          .end();
      }
    },
    {
      class: 'String',
      name: 'title',
      displayWidth: 100,
      width: 200
    },
    {
      class: 'String',
      name: 'creator',
      visibility: foam.u2.Visibility.RO,
      factory: function() { return this.user.firstName; }
    },
    {
      class: 'Long',
      name: 'creatorId',
      visibility: foam.u2.Visibility.RO,
      factory: function() { return this.user.id; },
      hidden: true
    },
    {
      class: 'DateTime',
      name: 'createdDate',
      visibility: foam.u2.Visibility.RO,
      factory: function() { return new Date(); },
      javaFactory: 'return new Date();',
      tableCellFormatter: function(date) {
        this.add(date ? date.toISOString().substring(0,10) : '');
      }
    },
    {
      class: 'String',
      name: 'content',
      view: { class: 'foam.u2.tag.TextArea', rows: 30, cols: 120}
    },
    {
      class: 'Long',
      name: 'hits',
      label: 'Hits',
      visibility: foam.u2.Visibility.RO
    },
    {
      class: 'String',
      name: 'viewer',
      visibility: foam.u2.Visibility.RO
    },
    {
      class: 'foam.nanos.fs.FileArray',
      name: 'data',
      label: 'Attachments',
      documentation: 'Additional documents for messageboard',
      view: { class: 'net.nanopay.invoice.ui.InvoiceFileUploadView', data:this.data$}
    }
  ]
});
<commit_msg>fixed a few comments<commit_after>/**
 * @license
 * Copyright 2018 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.demos.net.nap.web.model',
  name: 'Messageboard',

  tableColumns: [
    'mark', 'title', 'createdDate', 'creator', 'attachment', 'hits'
  ],

  imports: [
    'blobService',
    'messageboardDAO',
    'user'
  ],

  javaImports: [ 'java.util.Date' ],

  requires: [
    'foam.blob.BlobBlob',
    'foam.nanos.fs.File',
    'foam.u2.dialog.NotificationMessage'
  ],

  properties: [
    {
      class: 'Long',
      name: 'id'
    },
    {
      class: 'Boolean',
      name: 'starmark',
      tableCellFormatter: function(value, obj, property) {
        this
          .start()
            .callIf(value, function() {
              this.tag({ class: 'foam.u2.tag.Image', data: 'images/star.svg' }).style({'width': '25'});
            })
          .end();
      }
    },
    {
      class: 'String',
      name: 'title',
      displayWidth: 100,
      width: 200
    },
    {
      class: 'String',
      name: 'creator',
      visibility: foam.u2.Visibility.RO,
      factory: function() { return this.user.firstName; }
    },
    {
      class: 'Long',
      name: 'creatorId',
      visibility: foam.u2.Visibility.RO,
      factory: function() { return this.user.id; },
      hidden: true
    },
    {
      class: 'DateTime',
      name: 'createdDate',
      visibility: foam.u2.Visibility.RO,
      factory: function() { return new Date(); },
      javaFactory: 'return new Date();',
      tableCellFormatter: function(date) {
        this.add(date ? date.toISOString().substring(0,10) : '');
      }
    },
    {
      class: 'String',
      name: 'content',
      view: { class: 'foam.u2.tag.TextArea', rows: 30, cols: 120}
    },
    {
      class: 'Long',
      name: 'hits',
      label: 'Hits',
      visibility: foam.u2.Visibility.RO
    },
    {
      class: 'foam.nanos.fs.FileArray',
      name: 'data',
      label: 'Attachments',
      documentation: 'Additional documents for messageboard',
      view: { class: 'net.nanopay.invoice.ui.InvoiceFileUploadView', data:this.data$}
    }
  ]
});
<|endoftext|>


<commit_before>import { Debug } from '../../../core/debug.js';
import { AnimTrack } from '../../../anim/evaluator/anim-track.js';
import { AnimTransition } from '../../../anim/controller/anim-transition.js';
import { ANIM_LAYER_OVERWRITE } from '../../../anim/controller/constants.js';

/** @typedef {import('./component.js').AnimComponent} AnimComponent */

/**
 * The Anim Component Layer allows managers a single layer of the animation state graph.
 */
class AnimComponentLayer {
    /**
     * Create a new AnimComponentLayer instance.
     *
     * @param {string} name - The name of the layer.
     * @param {object} controller - The controller to manage this layers animations.
     * @param {AnimComponent} component - The component that this layer is a member of.
     * @param {number} [weight] - The weight of this layer. Defaults to 1.
     * @param {string} [blendType] - The blend type of this layer. Defaults to {@link ANIM_LAYER_OVERWRITE}.
     */
    constructor(name, controller, component, weight = 1, blendType = ANIM_LAYER_OVERWRITE) {
        this._name = name;
        this._controller = controller;
        this._component = component;
        this._weight = weight;
        this._blendType = blendType;
        this._mask = null;
    }

    /**
     * Returns the name of the layer.
     *
     * @type {string}
     */
    get name() {
        return this._name;
    }

    /**
     * Whether this layer is currently playing.
     *
     * @type {string}
     */
    set playing(value) {
        this._controller.playing = value;
    }

    get playing() {
        return this._controller.playing;
    }

    /**
     * Returns true if a state graph has been loaded and all states in the graph have been assigned
     * animation tracks.
     *
     * @type {string}
     */
    get playable() {
        return this._controller.playable;
    }

    /**
     * Returns the currently active state name.
     *
     * @type {string}
     */
    get activeState() {
        return this._controller.activeStateName;
    }

    /**
     * Returns the previously active state name.
     *
     * @type {string}
     */
    get previousState() {
        return this._controller.previousStateName;
    }

    /**
     * Returns the currently active states progress as a value normalized by the states animation
     * duration. Looped animations will return values greater than 1.
     *
     * @type {number}
     */
    get activeStateProgress() {
        return this._controller.activeStateProgress;
    }

    /**
     * Returns the currently active states duration.
     *
     * @type {number}
     */
    get activeStateDuration() {
        return this._controller.activeStateDuration;
    }

    /**
     * The active states time in seconds.
     *
     * @type {number}
     */
    set activeStateCurrentTime(time) {
        const controller = this._controller;
        const layerPlaying = controller.playing;
        controller.playing = true;
        controller.activeStateCurrentTime = time;
        if (!layerPlaying) {
            controller.update(0);
        }
        controller.playing = layerPlaying;
    }

    get activeStateCurrentTime() {
        return this._controller.activeStateCurrentTime;
    }

    /**
     * Returns whether the anim component layer is currently transitioning between states.
     *
     * @type {boolean}
     */
    get transitioning() {
        return this._controller.transitioning;
    }

    /**
     * If the anim component layer is currently transitioning between states, returns the progress.
     * Otherwise returns null.
     *
     * @type {number|null}
     */
    get transitionProgress() {
        if (this.transitioning) {
            return this._controller.transitionProgress;
        }
        return null;
    }

    /**
     * Lists all available states in this layers state graph.
     *
     * @type {string[]}
     */
    get states() {
        return this._controller.states;
    }

    /**
     * The blending weight of this layer. Used when calculating the value of properties that are
     * animated by more than one layer.
     *
     * @type {number}
     */
    set weight(value) {
        this._weight = value;
        this._component.dirtifyTargets();
    }

    get weight() {
        return this._weight;
    }

    set blendType(value) {
        if (value !== this._blendType) {
            this._blendType = value;
            this._component.rebind();
        }
    }

    get blendType() {
        return this._blendType;
    }

    /**
     *
     * A mask of bones which should be animated or ignored by this layer.
     *
     * @type {object}
     * @example
     * entity.anim.baseLayer.mask = {
     *     // include the spine of the current model and all of its children
     *     "path/to/spine": {
     *         children: true
     *     },
     *     // include the hip of the current model but not all of its children
     *     "path/to/hip": true
     * };
     */
    set mask(value) {
        if (this._controller.assignMask(value)) {
            this._component.rebind();
        }
        this._mask = value;
    }

    get mask() {
        return this._mask;
    }

    /**
     * Start playing the animation in the current state.
     *
     * @param {string} [name] - If provided, will begin playing from the start of the state with
     * this name.
     */
    play(name) {
        this._controller.play(name);
    }

    /**
     * Pause the animation in the current state.
     */
    pause() {
        this._controller.pause();
    }

    /**
     * Reset the animation component to its initial state, including all parameters. The system
     * will be paused.
     */
    reset() {
        this._controller.reset();
    }

    /**
     * Rebind any animations in the layer to the currently present components and model of the anim
     * components entity.
     */
    rebind() {
        this._controller.rebind();
    }

    update(dt) {
        this._controller.update(dt);
    }

    /**
     * Add a mask to this layer.
     *
     * @param {object} [mask] - The mask to assign to the layer. If not provided the current mask
     * in the layer will be removed.
     * @example
     * entity.anim.baseLayer.assignMask({
     *     // include the spine of the current model and all of its children
     *     "path/to/spine": {
     *         children: true
     *     },
     *     // include the hip of the current model but not all of its children
     *     "path/to/hip": true
     * });
     * @ignore
     */
    assignMask(mask) {
        Debug.deprecated('The pc.AnimComponentLayer#assignMask function is now deprecated. Assign masks to the pc.AnimComponentLayer#mask property instead.');
        if (this._controller.assignMask(mask)) {
            this._component.rebind();
        }
        this._mask = mask;
    }

    /**
     * Assigns an animation track to a state or blend tree node in the current graph. If a state
     * for the given nodePath doesn't exist, it will be created. If all states nodes are linked and
     * the {@link AnimComponent#activate} value was set to true then the component will begin
     * playing.
     *
     * @param {string} nodePath - Either the state name or the path to a blend tree node that this
     * animation should be associated with. Each section of a blend tree path is split using a
     * period (`.`) therefore state names should not include this character (e.g "MyStateName" or
     * "MyStateName.BlendTreeNode").
     * @param {object} animTrack - The animation track that will be assigned to this state and
     * played whenever this state is active.
     * @param {number} [speed] - Update the speed of the state you are assigning an animation to.
     * Defaults to 1.
     * @param {boolean} [loop] - Update the loop property of the state you are assigning an
     * animation to. Defaults to true.
     */
    assignAnimation(nodePath, animTrack, speed, loop) {
        if (animTrack.constructor !== AnimTrack) {
            Debug.error('assignAnimation: animTrack supplied to function was not of type AnimTrack');
            return;
        }
        this._controller.assignAnimation(nodePath, animTrack, speed, loop);
        if (this._controller._transitions.length === 0) {
            this._controller._transitions.push(new AnimTransition({
                from: 'START',
                to: nodePath
            }));
        }
        if (this._component.activate && this._component.playable) {
            this._component.playing = true;
        }
    }

    /**
     * Removes animations from a node in the loaded state graph.
     *
     * @param {string} nodeName - The name of the node that should have its animation tracks removed.
     */
    removeNodeAnimations(nodeName) {
        if (this._controller.removeNodeAnimations(nodeName)) {
            this._component.playing = false;
        }
    }

    /**
     *  Returns the asset that is associated with the given state.
     *
     * @param {string} stateName - The name of the state to get the asset for.
     * @type {pc.Asset}
     */
    getAnimationAsset(stateName) {
        return this._component.animationAssets[`${this.name}:${stateName}`];
    }

    /**
     * Transition to any state in the current layers graph. Transitions can be instant or take an
     * optional blend time.
     *
     * @param {string} to - The state that this transition will transition to.
     * @param {number} [time] - The duration of the transition in seconds. Defaults to 0.
     * @param {number} [transitionOffset] - If provided, the destination state will begin playing
     * its animation at this time. Given in normalized time, based on the states duration & must be
     * between 0 and 1. Defaults to null.
     */
    transition(to, time = 0, transitionOffset = null) {
        this._controller.updateStateFromTransition(new AnimTransition({
            from: this._controller.activeStateName,
            to,
            time,
            transitionOffset
        }));
    }
}

export { AnimComponentLayer };
<commit_msg>Anim component public api expansion follow up (#3934)<commit_after>import { Debug } from '../../../core/debug.js';
import { AnimTrack } from '../../../anim/evaluator/anim-track.js';
import { AnimTransition } from '../../../anim/controller/anim-transition.js';
import { ANIM_LAYER_OVERWRITE } from '../../../anim/controller/constants.js';

/** @typedef {import('./component.js').AnimComponent} AnimComponent */
/** @typedef {import('../../../asset/asset.js').Asset} Asset */

/**
 * The Anim Component Layer allows managers a single layer of the animation state graph.
 */
class AnimComponentLayer {
    /**
     * Create a new AnimComponentLayer instance.
     *
     * @param {string} name - The name of the layer.
     * @param {object} controller - The controller to manage this layers animations.
     * @param {AnimComponent} component - The component that this layer is a member of.
     * @param {number} [weight] - The weight of this layer. Defaults to 1.
     * @param {string} [blendType] - The blend type of this layer. Defaults to {@link ANIM_LAYER_OVERWRITE}.
     */
    constructor(name, controller, component, weight = 1, blendType = ANIM_LAYER_OVERWRITE) {
        this._name = name;
        this._controller = controller;
        this._component = component;
        this._weight = weight;
        this._blendType = blendType;
        this._mask = null;
    }

    /**
     * Returns the name of the layer.
     *
     * @type {string}
     */
    get name() {
        return this._name;
    }

    /**
     * Whether this layer is currently playing.
     *
     * @type {string}
     */
    set playing(value) {
        this._controller.playing = value;
    }

    get playing() {
        return this._controller.playing;
    }

    /**
     * Returns true if a state graph has been loaded and all states in the graph have been assigned
     * animation tracks.
     *
     * @type {string}
     */
    get playable() {
        return this._controller.playable;
    }

    /**
     * Returns the currently active state name.
     *
     * @type {string}
     */
    get activeState() {
        return this._controller.activeStateName;
    }

    /**
     * Returns the previously active state name.
     *
     * @type {string}
     */
    get previousState() {
        return this._controller.previousStateName;
    }

    /**
     * Returns the currently active states progress as a value normalized by the states animation
     * duration. Looped animations will return values greater than 1.
     *
     * @type {number}
     */
    get activeStateProgress() {
        return this._controller.activeStateProgress;
    }

    /**
     * Returns the currently active states duration.
     *
     * @type {number}
     */
    get activeStateDuration() {
        return this._controller.activeStateDuration;
    }

    /**
     * The active states time in seconds.
     *
     * @type {number}
     */
    set activeStateCurrentTime(time) {
        const controller = this._controller;
        const layerPlaying = controller.playing;
        controller.playing = true;
        controller.activeStateCurrentTime = time;
        if (!layerPlaying) {
            controller.update(0);
        }
        controller.playing = layerPlaying;
    }

    get activeStateCurrentTime() {
        return this._controller.activeStateCurrentTime;
    }

    /**
     * Returns whether the anim component layer is currently transitioning between states.
     *
     * @type {boolean}
     */
    get transitioning() {
        return this._controller.transitioning;
    }

    /**
     * If the anim component layer is currently transitioning between states, returns the progress.
     * Otherwise returns null.
     *
     * @type {number|null}
     */
    get transitionProgress() {
        if (this.transitioning) {
            return this._controller.transitionProgress;
        }
        return null;
    }

    /**
     * Lists all available states in this layers state graph.
     *
     * @type {string[]}
     */
    get states() {
        return this._controller.states;
    }

    /**
     * The blending weight of this layer. Used when calculating the value of properties that are
     * animated by more than one layer.
     *
     * @type {number}
     */
    set weight(value) {
        this._weight = value;
        this._component.dirtifyTargets();
    }

    get weight() {
        return this._weight;
    }

    set blendType(value) {
        if (value !== this._blendType) {
            this._blendType = value;
            this._component.rebind();
        }
    }

    get blendType() {
        return this._blendType;
    }

    /**
     * A mask of bones which should be animated or ignored by this layer.
     *
     * @type {object}
     * @example
     * entity.anim.baseLayer.mask = {
     *     // include the spine of the current model and all of its children
     *     "path/to/spine": {
     *         children: true
     *     },
     *     // include the hip of the current model but not all of its children
     *     "path/to/hip": true
     * };
     */
    set mask(value) {
        if (this._controller.assignMask(value)) {
            this._component.rebind();
        }
        this._mask = value;
    }

    get mask() {
        return this._mask;
    }

    /**
     * Start playing the animation in the current state.
     *
     * @param {string} [name] - If provided, will begin playing from the start of the state with
     * this name.
     */
    play(name) {
        this._controller.play(name);
    }

    /**
     * Pause the animation in the current state.
     */
    pause() {
        this._controller.pause();
    }

    /**
     * Reset the animation component to its initial state, including all parameters. The system
     * will be paused.
     */
    reset() {
        this._controller.reset();
    }

    /**
     * Rebind any animations in the layer to the currently present components and model of the anim
     * components entity.
     */
    rebind() {
        this._controller.rebind();
    }

    update(dt) {
        this._controller.update(dt);
    }

    /**
     * Add a mask to this layer.
     *
     * @param {object} [mask] - The mask to assign to the layer. If not provided the current mask
     * in the layer will be removed.
     * @example
     * entity.anim.baseLayer.assignMask({
     *     // include the spine of the current model and all of its children
     *     "path/to/spine": {
     *         children: true
     *     },
     *     // include the hip of the current model but not all of its children
     *     "path/to/hip": true
     * });
     * @ignore
     */
    assignMask(mask) {
        Debug.deprecated('The pc.AnimComponentLayer#assignMask function is now deprecated. Assign masks to the pc.AnimComponentLayer#mask property instead.');
        if (this._controller.assignMask(mask)) {
            this._component.rebind();
        }
        this._mask = mask;
    }

    /**
     * Assigns an animation track to a state or blend tree node in the current graph. If a state
     * for the given nodePath doesn't exist, it will be created. If all states nodes are linked and
     * the {@link AnimComponent#activate} value was set to true then the component will begin
     * playing.
     *
     * @param {string} nodePath - Either the state name or the path to a blend tree node that this
     * animation should be associated with. Each section of a blend tree path is split using a
     * period (`.`) therefore state names should not include this character (e.g "MyStateName" or
     * "MyStateName.BlendTreeNode").
     * @param {object} animTrack - The animation track that will be assigned to this state and
     * played whenever this state is active.
     * @param {number} [speed] - Update the speed of the state you are assigning an animation to.
     * Defaults to 1.
     * @param {boolean} [loop] - Update the loop property of the state you are assigning an
     * animation to. Defaults to true.
     */
    assignAnimation(nodePath, animTrack, speed, loop) {
        if (animTrack.constructor !== AnimTrack) {
            Debug.error('assignAnimation: animTrack supplied to function was not of type AnimTrack');
            return;
        }
        this._controller.assignAnimation(nodePath, animTrack, speed, loop);
        if (this._controller._transitions.length === 0) {
            this._controller._transitions.push(new AnimTransition({
                from: 'START',
                to: nodePath
            }));
        }
        if (this._component.activate && this._component.playable) {
            this._component.playing = true;
        }
    }

    /**
     * Removes animations from a node in the loaded state graph.
     *
     * @param {string} nodeName - The name of the node that should have its animation tracks removed.
     */
    removeNodeAnimations(nodeName) {
        if (this._controller.removeNodeAnimations(nodeName)) {
            this._component.playing = false;
        }
    }

    /**
     * Returns the asset that is associated with the given state.
     *
     * @param {string} stateName - The name of the state to get the asset for.
     * @returns {Asset} The asset associated with the given state.
     */
    getAnimationAsset(stateName) {
        return this._component.animationAssets[`${this.name}:${stateName}`];
    }

    /**
     * Transition to any state in the current layers graph. Transitions can be instant or take an
     * optional blend time.
     *
     * @param {string} to - The state that this transition will transition to.
     * @param {number} [time] - The duration of the transition in seconds. Defaults to 0.
     * @param {number} [transitionOffset] - If provided, the destination state will begin playing
     * its animation at this time. Given in normalized time, based on the states duration & must be
     * between 0 and 1. Defaults to null.
     */
    transition(to, time = 0, transitionOffset = null) {
        this._controller.updateStateFromTransition(new AnimTransition({
            from: this._controller.activeStateName,
            to,
            time,
            transitionOffset
        }));
    }
}

export { AnimComponentLayer };
<|endoftext|>


<commit_before>pc.extend(pc.fw, function () {
    /**
     * @name pc.fw.ScriptComponentSystem
     * @constructor Create a new ScriptComponentSystem
     * @class Allows scripts to be attached to an Entity and executed
     * @param {Object} context
     * @extends pc.fw.ComponentSystem
     */
    var ScriptComponentSystem = function ScriptComponentSystem(context) {
        this.id = 'script';
        this.description = "Allows the Entity to run JavaScript fragments to implement custom behavior.";
        context.systems.add(this.id, this);

        this.ComponentType = pc.fw.ScriptComponent;
        this.DataType = pc.fw.ScriptComponentData;

        this.schema = [{
            name: "scripts",
            displayName: "URLs",
            description: "Attach scripts to this Entity",
            type: "script",
            defaultValue: []
        }, {
            name: 'instances',
            exposed: false
        }, {
            name: 'runInTools',
            description: 'Allows scripts to be loaded and executed while in the tools',
            defaultValue: false,
            exposed: false
        }];

        this.exposeProperties();

        this.on('remove', this.onRemove, this);
        pc.fw.ComponentSystem.on('initialize', this.onInitialize, this);
        pc.fw.ComponentSystem.on('update', this.onUpdate, this);
        pc.fw.ComponentSystem.on('fixedUpdate', this.onFixedUpdate, this);
        pc.fw.ComponentSystem.on('postUpdate', this.onPostUpdate, this);
        pc.fw.ComponentSystem.on('toolsUpdate', this.onToolsUpdate, this);
    };
    ScriptComponentSystem = pc.inherits(ScriptComponentSystem, pc.fw.ComponentSystem);

    pc.extend(ScriptComponentSystem.prototype, {
        initializeComponentData: function (component, data, properties) {
            properties = ['runInTools', 'scripts'];

            ScriptComponentSystem._super.initializeComponentData.call(this, component, data, properties);
        },

        cloneComponent: function (entity, clone) {
            // overridden to make sure urls list is duplicated
            var src = this.dataStore[entity.getGuid()];
            var data = {
                runInTools: src.data.runInTools,
                scripts: pc.extend([], src.data.scripts)
            };
            return this.addComponent(clone, data);
        },

        /**
        * @private
        * @name pc.fw.ScriptComponentSystem#onRemove
        * @description Handler for 'remove' event which is fired when a script component is removed from an entity;
        * @param {pc.fw.Entity} entity The entity that the component was removed from
        * @param {pc.fw.ComponentData} data The data object for the removed component
        */
        onRemove: function (entity, data) {
            for (var name in data.instances) {
                if (data.instances.hasOwnProperty(name)) {

                    // Unbind any instance events that were bound when the script was created
                    var events = ['update', 'fixedUpdate', 'postUpdate', 'toolsUpdate'];
                    events.forEach(function (eventName) {
                        if (data.instances[name].instance[eventName]) {
                            this.unbind(eventName, data.instances[name].instance[eventName], data.instances[name].instance);
                        }
                    }, this);

                    if(data.instances[name].instance.destroy) {
                        data.instances[name].instance.destroy();
                    }

                    delete data.instances[name];
                }
            }
        },

        /**
         * @function
         * @private
         * @name pc.fw.ScriptComponentSystem#onInitialize
         * @description Handler for the 'initialize' event which is fired immediately after the Entity hierarchy is loaded, but before the first update loop
         * @param {pc.fw.Entity} root The root of the hierarchy to initialize.
         */
        onInitialize: function (root) {
            this._registerInstances(root);

            if (root.script) {
                for (var name in root.script.data.instances) {
                    if (root.script.data.instances.hasOwnProperty(name)) {
                        if (root.script.data.instances[name].instance.initialize) {
                            root.script.data.instances[name].instance.initialize();
                        }
                    }
                }
            }

            var children = root.getChildren();
            var i, len = children.length;
            for (i = 0; i < len; i++) {
                if (children[i] instanceof pc.fw.Entity) {
                    this.onInitialize(children[i]);
                }
            }
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#onUpdate
        * @description Handler for the 'update' event which is fired every frame
        * @param {Number} dt The time delta since the last update in seconds
        */
        onUpdate: function (dt) {
            this.fire('update', dt);
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#onFixedUpdate
        * @description Handler for the 'fixedUpdate' event which is fired every frame just before the 'update' event but with a fixed timestep
        * @param {Number} dt A fixed timestep of 1/60 seconds
        */
        onFixedUpdate: function (dt) {
            this.fire('fixedUpdate', dt);
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#onPostUpdate
        * @description Handler for the 'postUpdate' event which is fired every frame just after the 'update' event
        * @param {Number} dt The time delta since the last update in seconds
        */
        onPostUpdate: function (dt) {
            this.fire('postUpdate', dt);
        },

        onToolsUpdate: function (dt) {
            this.fire('toolsUpdate', dt);
        },

        /**
         * @function
         * @name pc.fw.ScriptComponentSystem#broadcast
         * @description Send a message to all Script Objects with a specific name.
         * Sending a message is similar to calling a method on a Script Object, except that the message will not fail if the method isn't present
         * @param {String} name The name of the script to send the message to
         * @param {String} functionName The name of the functio nto call on the Script Object
         * @example
         * // Call doDamage(10) on all 'enemy' scripts
         * entityEntity.script.broadcast('enemy', 'doDamage', 10);
         */
        broadcast: function (name, functionName) {
            var args = pc.makeArray(arguments).slice(2);

            var id, data, fn;
            var dataStore = this.store;
            // var results = [];

            for (id in dataStore) {
                if (dataStore.hasOwnProperty(id)) {
                    data = dataStore[id].data;
                    if (data.instances[name]) {
                        fn = data.instances[name].instance[functionName];
                        if(fn) {
                            fn.apply(data.instances[name].instance, args);
                        }
                    }
                }
            }
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#_preRegisterInstance
        * @description Internal method used to store a instance of a script created while loading. Instances are preregistered while loadeding
        * and then all registered at the same time once loading is complete
        * @param {pc.fw.Entity} entity The Entity the script instance is attached to
        * @param {String} url The url of the script
        * @param {String} name The name of the script
        * @param {Object} instance The instance of the Script Object
        */
        _preRegisterInstance: function (entity, url, name, instance) {
            if (entity.script) {
                entity.script.data._instances = entity.script.data._instances || {};
                if (entity.script.data._instances[name]) {
                    throw Error(pc.string.format("Script name collision '{0}'. Scripts from '{1}' and '{2}' {{3}}", name, url, entity.script.data._instances[name].url, entity.getGuid()));
                }
                entity.script.data._instances[name] = {
                    url: url,
                    name: name,
                    instance: instance
                };
            }
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#_registerInstance
        * @description Get all preregistered instances for an entity and 'register' then. This means storing the instance in the ComponentData
        * and binding events for the update, fixedUpdate, postUpdate and toolsUpdate methods.
        * This function is recursive and calls itself for the complete hierarchy down from the supplied Entity
        * @param {pc.fw.Entity} entity The Entity the instances are attached to
        */
        _registerInstances: function (entity) {
            var instance, instanceName;

            if (entity.script) {
                if (entity.script.data._instances) {
                    entity.script.instances = entity.script.data._instances;

                    for (instanceName in entity.script.instances) {
                        instance = entity.script.instances[instanceName];

                        if (entity.script.scripts) {
                            this._createAccessors(entity, instance);
                        }

                        // Make instance accessible from the script component of the Entity
                        if (entity.script[instanceName]) {
                            throw Error(pc.string.format("Script with name '{0}' is already attached to Script Component", instanceName));
                        } else {
                            entity.script[instanceName] = instance.instance;
                        }

                        // Attach events for update, fixedUpdate and postUpdate methods in script instance
                        if (instance.instance.update) {
                            this.on('update', instance.instance.update, instance.instance);
                        }
                        if (instance.instance.fixedUpdate) {
                            this.on('fixedUpdate', instance.instance.fixedUpdate, instance.instance);
                        }
                        if (instance.instance.postUpdate) {
                            this.on('postUpdate', instance.instance.postUpdate, instance.instance);
                        }
                        if (instance.instance.toolsUpdate) {
                            this.on('toolsUpdate', instance.instance.toolsUpdate, instance.instance);
                        }
                    }

                    // Remove temp storage
                    delete entity.script.data._instances;
                }

            }

            var children = entity.getChildren();
            var i, len = children.length;
            for (i = 0; i < len; i++) {
                if (children[i] instanceof pc.fw.Entity) {
                    this._registerInstances(children[i]);
                }
            }
        },

        _createAccessors: function (entity, instance) {
            var self = this;
            var i;
            var len = entity.script.scripts.length;
            var url = instance.url;

            for (i=0; i<len; i++) {
                var script = entity.script.scripts[i];
                if (script.url === url) {
                    var attributes = script.attributes;
                    if (script.name && attributes) {
                        attributes.forEach(function (attribute, index) {
                            self._convertAttributeValue(attribute);

                            Object.defineProperty(instance.instance, attribute.name, {
                                get: function () {
                                    return attribute.value;
                                },
                                set: function (value) {
                                    var oldValue = attribute.value;
                                    attribute.value = value;
                                    self._convertAttributeValue(attribute);
                                    //instance.instance.fire("set", attribute.name, oldValue, value);
                                },
                                // allow the propery to be redefined in case we have updated attributes
                                // from the designer
                                configurable : true
                            });
                        }, this);
                    }
                    break;
                }
            }
        },

        _convertAttributeValue: function (attribute) {
            if (attribute.type === 'rgb' || attribute.type === 'rgba') {
                if (pc.type(attribute.value) === 'array') {
                    attribute.value = new pc.Color(attribute.value);
                }
            }
        }

    });

    return {
        ScriptComponentSystem: ScriptComponentSystem
    };
}());<commit_msg>Fire event on the script instance when a script attribute changes in the designer<commit_after>pc.extend(pc.fw, function () {
    /**
     * @name pc.fw.ScriptComponentSystem
     * @constructor Create a new ScriptComponentSystem
     * @class Allows scripts to be attached to an Entity and executed
     * @param {Object} context
     * @extends pc.fw.ComponentSystem
     */
    var ScriptComponentSystem = function ScriptComponentSystem(context) {
        this.id = 'script';
        this.description = "Allows the Entity to run JavaScript fragments to implement custom behavior.";
        context.systems.add(this.id, this);

        this.ComponentType = pc.fw.ScriptComponent;
        this.DataType = pc.fw.ScriptComponentData;

        this.schema = [{
            name: "scripts",
            displayName: "URLs",
            description: "Attach scripts to this Entity",
            type: "script",
            defaultValue: []
        }, {
            name: 'instances',
            exposed: false
        }, {
            name: 'runInTools',
            description: 'Allows scripts to be loaded and executed while in the tools',
            defaultValue: false,
            exposed: false
        }];

        this.exposeProperties();

        this.on('remove', this.onRemove, this);
        pc.fw.ComponentSystem.on('initialize', this.onInitialize, this);
        pc.fw.ComponentSystem.on('update', this.onUpdate, this);
        pc.fw.ComponentSystem.on('fixedUpdate', this.onFixedUpdate, this);
        pc.fw.ComponentSystem.on('postUpdate', this.onPostUpdate, this);
        pc.fw.ComponentSystem.on('toolsUpdate', this.onToolsUpdate, this);
    };
    ScriptComponentSystem = pc.inherits(ScriptComponentSystem, pc.fw.ComponentSystem);

    pc.extend(ScriptComponentSystem.prototype, {
        initializeComponentData: function (component, data, properties) {
            properties = ['runInTools', 'scripts'];

            ScriptComponentSystem._super.initializeComponentData.call(this, component, data, properties);
        },

        cloneComponent: function (entity, clone) {
            // overridden to make sure urls list is duplicated
            var src = this.dataStore[entity.getGuid()];
            var data = {
                runInTools: src.data.runInTools,
                scripts: pc.extend([], src.data.scripts)
            };
            return this.addComponent(clone, data);
        },

        /**
        * @private
        * @name pc.fw.ScriptComponentSystem#onRemove
        * @description Handler for 'remove' event which is fired when a script component is removed from an entity;
        * @param {pc.fw.Entity} entity The entity that the component was removed from
        * @param {pc.fw.ComponentData} data The data object for the removed component
        */
        onRemove: function (entity, data) {
            for (var name in data.instances) {
                if (data.instances.hasOwnProperty(name)) {

                    // Unbind any instance events that were bound when the script was created
                    var events = ['update', 'fixedUpdate', 'postUpdate', 'toolsUpdate'];
                    events.forEach(function (eventName) {
                        if (data.instances[name].instance[eventName]) {
                            this.unbind(eventName, data.instances[name].instance[eventName], data.instances[name].instance);
                        }
                    }, this);

                    if(data.instances[name].instance.destroy) {
                        data.instances[name].instance.destroy();
                    }

                    delete data.instances[name];
                }
            }
        },

        /**
         * @function
         * @private
         * @name pc.fw.ScriptComponentSystem#onInitialize
         * @description Handler for the 'initialize' event which is fired immediately after the Entity hierarchy is loaded, but before the first update loop
         * @param {pc.fw.Entity} root The root of the hierarchy to initialize.
         */
        onInitialize: function (root) {
            this._registerInstances(root);

            if (root.script) {
                for (var name in root.script.data.instances) {
                    if (root.script.data.instances.hasOwnProperty(name)) {
                        if (root.script.data.instances[name].instance.initialize) {
                            root.script.data.instances[name].instance.initialize();
                        }
                    }
                }
            }

            var children = root.getChildren();
            var i, len = children.length;
            for (i = 0; i < len; i++) {
                if (children[i] instanceof pc.fw.Entity) {
                    this.onInitialize(children[i]);
                }
            }
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#onUpdate
        * @description Handler for the 'update' event which is fired every frame
        * @param {Number} dt The time delta since the last update in seconds
        */
        onUpdate: function (dt) {
            this.fire('update', dt);
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#onFixedUpdate
        * @description Handler for the 'fixedUpdate' event which is fired every frame just before the 'update' event but with a fixed timestep
        * @param {Number} dt A fixed timestep of 1/60 seconds
        */
        onFixedUpdate: function (dt) {
            this.fire('fixedUpdate', dt);
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#onPostUpdate
        * @description Handler for the 'postUpdate' event which is fired every frame just after the 'update' event
        * @param {Number} dt The time delta since the last update in seconds
        */
        onPostUpdate: function (dt) {
            this.fire('postUpdate', dt);
        },

        onToolsUpdate: function (dt) {
            this.fire('toolsUpdate', dt);
        },

        /**
         * @function
         * @name pc.fw.ScriptComponentSystem#broadcast
         * @description Send a message to all Script Objects with a specific name.
         * Sending a message is similar to calling a method on a Script Object, except that the message will not fail if the method isn't present
         * @param {String} name The name of the script to send the message to
         * @param {String} functionName The name of the functio nto call on the Script Object
         * @example
         * // Call doDamage(10) on all 'enemy' scripts
         * entityEntity.script.broadcast('enemy', 'doDamage', 10);
         */
        broadcast: function (name, functionName) {
            var args = pc.makeArray(arguments).slice(2);

            var id, data, fn;
            var dataStore = this.store;
            // var results = [];

            for (id in dataStore) {
                if (dataStore.hasOwnProperty(id)) {
                    data = dataStore[id].data;
                    if (data.instances[name]) {
                        fn = data.instances[name].instance[functionName];
                        if(fn) {
                            fn.apply(data.instances[name].instance, args);
                        }
                    }
                }
            }
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#_preRegisterInstance
        * @description Internal method used to store a instance of a script created while loading. Instances are preregistered while loadeding
        * and then all registered at the same time once loading is complete
        * @param {pc.fw.Entity} entity The Entity the script instance is attached to
        * @param {String} url The url of the script
        * @param {String} name The name of the script
        * @param {Object} instance The instance of the Script Object
        */
        _preRegisterInstance: function (entity, url, name, instance) {
            if (entity.script) {
                entity.script.data._instances = entity.script.data._instances || {};
                if (entity.script.data._instances[name]) {
                    throw Error(pc.string.format("Script name collision '{0}'. Scripts from '{1}' and '{2}' {{3}}", name, url, entity.script.data._instances[name].url, entity.getGuid()));
                }
                entity.script.data._instances[name] = {
                    url: url,
                    name: name,
                    instance: instance
                };
            }
        },

        /**
        * @private
        * @function
        * @name pc.fw.ScriptComponentSystem#_registerInstance
        * @description Get all preregistered instances for an entity and 'register' then. This means storing the instance in the ComponentData
        * and binding events for the update, fixedUpdate, postUpdate and toolsUpdate methods.
        * This function is recursive and calls itself for the complete hierarchy down from the supplied Entity
        * @param {pc.fw.Entity} entity The Entity the instances are attached to
        */
        _registerInstances: function (entity) {
            var instance, instanceName;

            if (entity.script) {
                if (entity.script.data._instances) {
                    entity.script.instances = entity.script.data._instances;

                    for (instanceName in entity.script.instances) {
                        instance = entity.script.instances[instanceName];

                        pc.extend(instance.instance, pc.events);

                        if (entity.script.scripts) {
                            this._createAccessors(entity, instance);
                        }

                        // Make instance accessible from the script component of the Entity
                        if (entity.script[instanceName]) {
                            throw Error(pc.string.format("Script with name '{0}' is already attached to Script Component", instanceName));
                        } else {
                            entity.script[instanceName] = instance.instance;
                        }

                        // Attach events for update, fixedUpdate and postUpdate methods in script instance
                        if (instance.instance.update) {
                            this.on('update', instance.instance.update, instance.instance);
                        }
                        if (instance.instance.fixedUpdate) {
                            this.on('fixedUpdate', instance.instance.fixedUpdate, instance.instance);
                        }
                        if (instance.instance.postUpdate) {
                            this.on('postUpdate', instance.instance.postUpdate, instance.instance);
                        }
                        if (instance.instance.toolsUpdate) {
                            this.on('toolsUpdate', instance.instance.toolsUpdate, instance.instance);
                        }
                    }

                    // Remove temp storage
                    delete entity.script.data._instances;
                }

            }

            var children = entity.getChildren();
            var i, len = children.length;
            for (i = 0; i < len; i++) {
                if (children[i] instanceof pc.fw.Entity) {
                    this._registerInstances(children[i]);
                }
            }
        },

        _createAccessors: function (entity, instance) {
            var self = this;
            var i;
            var len = entity.script.scripts.length;
            var url = instance.url;

            for (i=0; i<len; i++) {
                var script = entity.script.scripts[i];
                if (script.url === url) {
                    var attributes = script.attributes;
                    if (script.name && attributes) {
                        attributes.forEach(function (attribute, index) {
                            self._convertAttributeValue(attribute);
                            instance.instance.fire("set", attribute.name, attribute.value);

                            Object.defineProperty(instance.instance, attribute.name, {
                                get: function () {
                                    return attribute.value;
                                },
                                set: function (value) {
                                    var oldValue = attribute.value;
                                    attribute.value = value;
                                    self._convertAttributeValue(attribute);
                                },
                                // allow the propery to be redefined in case we have updated attributes
                                // from the designer
                                configurable : true
                            });
                        }, this);
                    }
                    break;
                }
            }
        },

        _convertAttributeValue: function (attribute) {
            if (attribute.type === 'rgb' || attribute.type === 'rgba') {
                if (pc.type(attribute.value) === 'array') {
                    attribute.value = new pc.Color(attribute.value);
                }
            }
        }

    });

    return {
        ScriptComponentSystem: ScriptComponentSystem
    };
}());<|endoftext|>