<commit_before>/* Public functions used across different files */
var Pontoon = (function (my) {
  const fluentParser = new FluentSyntax.FluentParser({ withSpans: false });
  const fluentSerializer = new FluentSyntax.FluentSerializer();

  return $.extend(true, my, {
    fluent: {

      /*
       * Populate FTL translation area with existing data
       */
      renderEditor: function (translation) {
        $('#ftl-area > .main-value').show().find('input').val('');
        $('#ftl-area .attributes ul:first').empty();
        $('#ftl-area > .main-value ul li:not(":first")').remove();

        var self = this,
            entity = Pontoon.getEditorEntity(),
            isFTLplural = entity.isFTLplural,
            translation = translation || entity.translation[0],
            isTranslated = translation.pk,
            entity_ast = fluentParser.parseEntry(entity.original),
            entityAttributes = [],
            id, value;

        var attributes = entity_ast.attributes;
        if (isTranslated) {
          var translation_ast = fluentParser.parseEntry(translation.string);
          attributes = translation_ast.attributes;
        }

        if (entity_ast.attributes) {
          entityAttributes = $.map(entity_ast.attributes, function(item) {
            return item.id.name;
          });
        }

        // Plurals
        if (isFTLplural) {
          var pluralForms = $(Pontoon.locale.cldr_plurals).map(
            function() {
              return Pontoon.CLDR_PLURALS[this];
            }
          ).get();

          if (translation_ast) {
            var variants = translation_ast.value.elements[0].variants;
          }

          if (!Pontoon.locale.examples) {
            Pontoon.generateLocalePluralExamples();
          }

          $.each(pluralForms, function(pluralForm) {
            var value = '';

            if (translation_ast) {
              for (var i = 0; i < variants.length; i++) {
                if (variants[i].key.name === this.toString()) {
                  value = self.serializePlaceables(variants[i].value.elements);
                  break;
                }
              }
            }

            $('#ftl-area .main-value ul')
              .append(
                '
' +
                  '' +
                    '' + this + ' (e.g. ' + Pontoon.locale.examples[pluralForm] + ')' +
                  '' +
                  self.inputValueElement(this, value) +
                '
');
          });

          $('#entity-value').parents('li').hide();

        // Main Value
        } else if (translation_ast && translation_ast.value) {
          $('#ftl-area > .main-value #entity-value').val(self.serializePlaceables(translation_ast.value.elements));
        }

        // Attributes
        if (!(attributes && attributes.length)) {
          return;
        }

        $.each(attributes, function() {
          id = this.id.name;
          value = isTranslated ? self.serializePlaceables(this.value.elements) : '';

          var maxlength = label = input = cls = '';

          if (id === 'accesskey') {
            maxlength = '1';
            input = '

';
          }
          input += self.inputValueElement(id, value, maxlength);

          if ($.inArray(id, [entityAttributes])) {
            label = '' +
              '' + id + '' +
            '';

          } else {
            cls = ' class="custom-attribute clearfix"';
            label = '
' +
              '<input type="text" class="id" placeholder="enter-attribute-id" value="' + id + '">' +
              '' +
            '
';
          }

          $('#ftl-area .attributes ul:first')
            .append(
              '
' +
                label +
                input +
              '
');
        });

        // Update access keys presentaion
        $('#ftl-area .attributes input').keyup();

        // If no value in source string, no value in translation
        if (!entity_ast.value) {
          $('#ftl-area > .main-value').hide();
        }

        // Ignore editing for anonymous users
        if (!Pontoon.user.id) {
          $('#ftl-area input').prop('readonly', true);
        }
      },

      /*
       * Render editor with given translation
       */
      renderEditorWithTranslation: function (translation) {
        Pontoon.fluent.renderEditor(translation);
        Pontoon.fluent.focusFirstField();
      },

      /*
       * Serialize value with placeables into a simple strings
       */
      serializePlaceables: function (elements) {
        var translatedValue = '';

        $.each(elements, function(i) {
          if (this.type === 'TextElement') {
            translatedValue += this.value;
          } else if (this.type === 'ExternalArgument') {
            translatedValue += ('{$' + this.id.name + '}');
          } else if (this.type === 'MessageReference') {
            translatedValue += ('{' + this.id.name + '}');
          }
        });

        return translatedValue;
      },

      /*
       * Generate Input value element
       */
      inputValueElement: function (id, value, maxLength) {
        var base = '<input class="value" id="ftl-id-' + id + '" type="text" value="' + value + '"';

        if (typeof maxlength !== "undefined"){
          base += ' maxlength="' + maxlength + '"';
        }

        base += ' dir="' + Pontoon.locale.direction + '" data-script="' + Pontoon.locale.script + '" lang="' + Pontoon.locale.code + '">';
        return base;
      },

      /*
       * Toggle FTL button
       */
      toggleButton: function () {
        var entity = Pontoon.getEditorEntity();
        $('#ftl').toggle(entity.format === 'ftl');
      },

      /*
       * Toggle between standard and FTL translation editor
       */
      toggleEditor: function (activate) {
        if (activate) {
          $('#ftl-area').show();
          // TODO: Uncomment once attributes are fully supported (defaults, removing, validation)
          // $('#add-attribute').show();
          $('#translation-length, #copy').hide();

          $('#editor textarea').hide();
          $('#ftl').addClass('active');

          this.renderEditor();
          this.focusFirstField();

        } else {
          $('#ftl-area').hide();
          // TODO: Uncomment once attributes are fully supported (defaults, removing, validation)
          // $('#add-attribute').hide();
          $('#translation-length, #copy').show();

          $('#editor textarea').show().focus();
          $('#ftl').removeClass('active');
        }

        Pontoon.moveCursorToBeginning();
      },

      /*
       * Toggle between standard and FTL original string
       */
      toggleOriginal: function () {
        var self = this,
            entity = Pontoon.getEditorEntity();

        $('#original').show();
        $('#ftl-original').hide();

        if (entity.format !== 'ftl') {
          return;
        }

        var ast = fluentParser.parseEntry(entity.original),
            original = '';

        function renderOriginal(obj) {
          if (entity.isFTLplural) {
            var variants = ast.value.elements[0].variants;
            $.each(variants, function() {
              original += '
' + (this.key.value || this.key.name) + '';
              original += self.serializePlaceables(this.value.elements);
              original += '
';
            });

          } else if (obj.value) {
            original += '
Value';
            original += obj.value.elements[0].value;
            original += '
';
          }

          if (obj.attributes) {
            var id, value;
            $.each(obj.attributes, function() {
              id = this.id.name;
              value = this.value.elements[0].value;

              $('#ftl-original .attributes ul')
                .append(
                  '
' +
                    '' + id + '' +
                    '' + value + '' +
                  '
');
            });
          }
        }

        $('#ftl-original section ul').empty();

        if (entity.isComplexFTL) {
          $('#original').hide();
          $('#ftl-original').show();

          renderOriginal(ast);
          $('#ftl-original .main-value ul').append(original);
        }
      },

      /*
       * Get AST and any errors for the entity's translation
       */
      serializeTranslation: function(entity, translation) {
        if (entity.format !== 'ftl') {
          return translation;
        }

        if ($('#ftl').is('.active')) {
          // Main value
          var value = $('#ftl-area > .main-value input').val(),
              attributes = '';

          // Plurals
          if (entity.isFTLplural) {
            value = '';
            var variants = $('#ftl-area .main-value li:visible'),
                nonEmptyVariants = [],
                def = '';

            variants.each(function(i) {
              var id = $(this).find('.id span:first').html().split(' ')[0],
                  val = $(this).find('.value').val();

              if (id && val) {
                nonEmptyVariants.push('[' + id + '] ' + val);
              }
            });

            for (var i = 0; i < nonEmptyVariants.length; i++) {
              // Mark the last variant as default
              // TODO: Should be removed by bug 1237667
              if (i === nonEmptyVariants.length - 1) {
                def = '*';
              }
              value += '\n  ' + def + nonEmptyVariants[i];
            }

            if (value) {
              var entity_ast = fluentParser.parseEntry(entity.original);
              value = '{ ' + entity_ast.value.elements[0].expression.id.name + ' ->' + value + '\n  }';
            }
          }

          // Attributes
          $('#ftl-area .attributes ul:first li').each(function() {
            var id = $(this).find('.id span').html() || $(this).find('.id').val(),
                val = $(this).find('.value').val();

            if (id && val) {
              attributes += '\n  .' + id + ' = ' + val;
            }
          });

          translation = (value ? ' = ' + value : '') + (attributes || '');

        // Mark multiline strings with indent
        } else if (translation.indexOf('\n') !== -1) {
          translation = ' = \n  ' + translation.replace(/\n/g, '\n  ');

        // Simple strings
        } else {
          translation = ' = ' + translation;
        }

        var content = entity.key + translation,
            ast = fluentParser.parseEntry(content),
            entityAst = fluentParser.parseEntry(entity.original),
            error = null;

        // Parse error
        if (ast.type === 'Junk') {
          error = ast.annotations[0].message;

        // TODO: Should be removed by bug 1237667
        // Detect missing values
        } else if (entityAst && ast && entityAst.value && !ast.value) {
          error = "Please make sure to fill in the value";
        // Detect missing attributes
        } else if (entityAst.attributes && ast.attributes && entityAst.attributes.length !== ast.attributes.length) {
          error = "Please make sure to fill in all the attributes";
        }

        if (error) {
          return {
            error: error
          };
        }

        return fluentSerializer.serializeEntry(ast);
      },

      /*
       * Get simplified preview of the FTL object
       */
      getSimplePreview: function(object, fallback, entity) {
        var response = fallback;

        if (entity.format === 'ftl') {
          // Transfrom complex FTL-based strings into single-value strings
          var source = object.original || object.string;

          if (!source) {
            return response;
          }

          ast = fluentParser.parseEntry(source);

          if (ast.value) {
            response = this.serializePlaceables(ast.value.elements);

          // Attributes
          } else {
            var attributes = ast.attributes;
            if (attributes && attributes.length) {
              response = this.serializePlaceables(attributes[0].value.elements);
            }
          }

          // Plurals
          if (ast.value && ast.value.elements && ast.value.elements.length && ast.value.elements[0].expression && ast.value.elements[0].variants) {
            var variants = ast.value.elements[0].variants,
                isFTLplural = variants.every(function(element) {
                  var key = element.key.name,
                      isPlural = Pontoon.CLDR_PLURALS.indexOf(key) !== -1,
                      isInteger = element.key.type === 'NumberExpression';

                  return isPlural || isInteger;
                });

            if (isFTLplural) {
              response = this.serializePlaceables(variants[0].value.elements);
              entity.isFTLplural = true;
            }
          }

          // Mark complex strings
          if (ast.attributes && ast.attributes.length || (ast.value && ast.value.elements && ast.value.elements.length && ast.value.elements[0].expression && ast.value.elements[0].variants.length > 1)) {
            object.isComplexFTL = true;

          // Update entity and translation objects
          } else {
            if (object.hasOwnProperty('original')) {
              object.original = response;
              response = object.marked = Pontoon.doNotRender(response);
            } else if (object.hasOwnProperty('string')) {
              object.string = response;
            }
          }
        }

        return response;
      },

      /*
       * Focus first field of the FTL editor
       */
      focusFirstField: function(object, fallback, entity) {
        $('#ftl-area input.value:visible:first').focus();
      }

    }
  });
}(Pontoon || {}));

$(function() {

  // Ignore editing for anonymous users
  if (!Pontoon.user.id) {
    return;
  }

  // Toggle FTL mode
  $('#ftl').click(function (e) {
    e.preventDefault();
    Pontoon.fluent.toggleEditor(!$(this).is('.active'));
  });

  // Add attribute
  $('#add-attribute').click(function (e) {
    e.preventDefault();
    $('#ftl-area .attributes ul:first').append($('#ftl-area .attributes .template li').clone());
  });

  // Remove attribute
  $('#ftl-area section').on('click', 'sub.remove', function (e) {
    e.preventDefault();
    $(this).parents('li').remove();
  });

  // Generate access key list
  $('#ftl-area .attributes').on('keyup', 'input:first', function() {
    var active = $('.accesskeys').find('.active').html(),
        unique = $(this).val().toUpperCase().split('').filter(function(item, i, ar) {
          return ar.indexOf(item) === i;
        });

    $('.accesskeys').empty();

    $.each(unique, function(i, v) {
      $('.accesskeys').append('
' + v + '
');
    });
  });

  // Select access key via click
  $('#ftl-area .attributes').on('click', '.accesskeys div', function() {
    var selected = $(this).is('.active');
    $('.accesskeys div').removeClass('active');

    if (!selected) {
      $(this).addClass('active');
    }

    $('#ftl-id-accesskey').val($('.accesskeys div.active').html());
  });

  // Select access key via input
  $('#ftl-area .attributes').on('keyup', '#ftl-id-accesskey', function() {
    var accesskey = $(this).val().toUpperCase();

    if (accesskey) {
      $('.accesskeys div').removeClass('active');
      $('.accesskeys div:contains("' + accesskey + '")').addClass('active');
    }
  });

});
<commit_msg>Bug 1423460: Properly reference external variable<commit_after>/* Public functions used across different files */
var Pontoon = (function (my) {
  const fluentParser = new FluentSyntax.FluentParser({ withSpans: false });
  const fluentSerializer = new FluentSyntax.FluentSerializer();

  return $.extend(true, my, {
    fluent: {

      /*
       * Populate FTL translation area with existing data
       */
      renderEditor: function (translation) {
        $('#ftl-area > .main-value').show().find('input').val('');
        $('#ftl-area .attributes ul:first').empty();
        $('#ftl-area > .main-value ul li:not(":first")').remove();

        var self = this,
            entity = Pontoon.getEditorEntity(),
            isFTLplural = entity.isFTLplural,
            translation = translation || entity.translation[0],
            isTranslated = translation.pk,
            entity_ast = fluentParser.parseEntry(entity.original),
            entityAttributes = [],
            id, value;

        var attributes = entity_ast.attributes;
        if (isTranslated) {
          var translation_ast = fluentParser.parseEntry(translation.string);
          attributes = translation_ast.attributes;
        }

        if (entity_ast.attributes) {
          entityAttributes = $.map(entity_ast.attributes, function(item) {
            return item.id.name;
          });
        }

        // Plurals
        if (isFTLplural) {
          var pluralForms = $(Pontoon.locale.cldr_plurals).map(
            function() {
              return Pontoon.CLDR_PLURALS[this];
            }
          ).get();

          if (translation_ast) {
            var variants = translation_ast.value.elements[0].variants;
          }

          if (!Pontoon.locale.examples) {
            Pontoon.generateLocalePluralExamples();
          }

          $.each(pluralForms, function(pluralForm) {
            var value = '';

            if (translation_ast) {
              for (var i = 0; i < variants.length; i++) {
                if (variants[i].key.name === this.toString()) {
                  value = self.serializePlaceables(variants[i].value.elements);
                  break;
                }
              }
            }

            $('#ftl-area .main-value ul')
              .append(
                '
' +
                  '' +
                    '' + this + ' (e.g. ' + Pontoon.locale.examples[pluralForm] + ')' +
                  '' +
                  self.inputValueElement(this, value) +
                '
');
          });

          $('#entity-value').parents('li').hide();

        // Main Value
        } else if (translation_ast && translation_ast.value) {
          $('#ftl-area > .main-value #entity-value').val(self.serializePlaceables(translation_ast.value.elements));
        }

        // Attributes
        if (!(attributes && attributes.length)) {
          return;
        }

        $.each(attributes, function() {
          id = this.id.name;
          value = isTranslated ? self.serializePlaceables(this.value.elements) : '';

          var maxlength = label = input = cls = '';

          if (id === 'accesskey') {
            maxlength = '1';
            input = '

';
          }
          input += self.inputValueElement(id, value, maxlength);

          if ($.inArray(id, [entityAttributes])) {
            label = '' +
              '' + id + '' +
            '';

          } else {
            cls = ' class="custom-attribute clearfix"';
            label = '
' +
              '<input type="text" class="id" placeholder="enter-attribute-id" value="' + id + '">' +
              '' +
            '
';
          }

          $('#ftl-area .attributes ul:first')
            .append(
              '
' +
                label +
                input +
              '
');
        });

        // Update access keys presentaion
        $('#ftl-area .attributes input').keyup();

        // If no value in source string, no value in translation
        if (!entity_ast.value) {
          $('#ftl-area > .main-value').hide();
        }

        // Ignore editing for anonymous users
        if (!Pontoon.user.id) {
          $('#ftl-area input').prop('readonly', true);
        }
      },

      /*
       * Render editor with given translation
       */
      renderEditorWithTranslation: function (translation) {
        Pontoon.fluent.renderEditor(translation);
        Pontoon.fluent.focusFirstField();
      },

      /*
       * Serialize value with placeables into a simple strings
       */
      serializePlaceables: function (elements) {
        var translatedValue = '';

        $.each(elements, function(i) {
          if (this.type === 'TextElement') {
            translatedValue += this.value;
          } else if (this.type === 'ExternalArgument') {
            translatedValue += ('{$' + this.id.name + '}');
          } else if (this.type === 'MessageReference') {
            translatedValue += ('{' + this.id.name + '}');
          }
        });

        return translatedValue;
      },

      /*
       * Generate Input value element
       */
      inputValueElement: function (id, value, maxLength) {
        var base = '<input class="value" id="ftl-id-' + id + '" type="text" value="' + value + '"';

        if (typeof maxlength !== "undefined"){
          base += ' maxlength="' + maxlength + '"';
        }

        base += ' dir="' + Pontoon.locale.direction + '" data-script="' + Pontoon.locale.script + '" lang="' + Pontoon.locale.code + '">';
        return base;
      },

      /*
       * Toggle FTL button
       */
      toggleButton: function () {
        var entity = Pontoon.getEditorEntity();
        $('#ftl').toggle(entity.format === 'ftl');
      },

      /*
       * Toggle between standard and FTL translation editor
       */
      toggleEditor: function (activate) {
        if (activate) {
          $('#ftl-area').show();
          // TODO: Uncomment once attributes are fully supported (defaults, removing, validation)
          // $('#add-attribute').show();
          $('#translation-length, #copy').hide();

          $('#editor textarea').hide();
          $('#ftl').addClass('active');

          this.renderEditor();
          this.focusFirstField();

        } else {
          $('#ftl-area').hide();
          // TODO: Uncomment once attributes are fully supported (defaults, removing, validation)
          // $('#add-attribute').hide();
          $('#translation-length, #copy').show();

          $('#editor textarea').show().focus();
          $('#ftl').removeClass('active');
        }

        Pontoon.moveCursorToBeginning();
      },

      /*
       * Toggle between standard and FTL original string
       */
      toggleOriginal: function () {
        var self = this,
            entity = Pontoon.getEditorEntity();

        $('#original').show();
        $('#ftl-original').hide();

        if (entity.format !== 'ftl') {
          return;
        }

        var ast = fluentParser.parseEntry(entity.original),
            original = '';

        function renderOriginal(obj) {
          if (entity.isFTLplural) {
            var variants = ast.value.elements[0].variants;
            $.each(variants, function() {
              original += '
' + (this.key.value || this.key.name) + '';
              original += self.serializePlaceables(this.value.elements);
              original += '
';
            });

          } else if (obj.value) {
            original += '
Value';
            original += obj.value.elements[0].value;
            original += '
';
          }

          if (obj.attributes) {
            var id, value;
            $.each(obj.attributes, function() {
              id = this.id.name;
              value = this.value.elements[0].value;

              $('#ftl-original .attributes ul')
                .append(
                  '
' +
                    '' + id + '' +
                    '' + value + '' +
                  '
');
            });
          }
        }

        $('#ftl-original section ul').empty();

        if (entity.isComplexFTL) {
          $('#original').hide();
          $('#ftl-original').show();

          renderOriginal(ast);
          $('#ftl-original .main-value ul').append(original);
        }
      },

      /*
       * Get AST and any errors for the entity's translation
       */
      serializeTranslation: function(entity, translation) {
        if (entity.format !== 'ftl') {
          return translation;
        }

        if ($('#ftl').is('.active')) {
          // Main value
          var value = $('#ftl-area > .main-value input').val(),
              attributes = '';

          // Plurals
          if (entity.isFTLplural) {
            value = '';
            var variants = $('#ftl-area .main-value li:visible'),
                nonEmptyVariants = [],
                def = '';

            variants.each(function(i) {
              var id = $(this).find('.id span:first').html().split(' ')[0],
                  val = $(this).find('.value').val();

              if (id && val) {
                nonEmptyVariants.push('[' + id + '] ' + val);
              }
            });

            for (var i = 0; i < nonEmptyVariants.length; i++) {
              // Mark the last variant as default
              // TODO: Should be removed by bug 1237667
              if (i === nonEmptyVariants.length - 1) {
                def = '*';
              }
              value += '\n  ' + def + nonEmptyVariants[i];
            }

            if (value) {
              var entity_ast = fluentParser.parseEntry(entity.original);
              value = '{ $' + entity_ast.value.elements[0].expression.id.name + ' ->' + value + '\n  }';
            }
          }

          // Attributes
          $('#ftl-area .attributes ul:first li').each(function() {
            var id = $(this).find('.id span').html() || $(this).find('.id').val(),
                val = $(this).find('.value').val();

            if (id && val) {
              attributes += '\n  .' + id + ' = ' + val;
            }
          });

          translation = (value ? ' = ' + value : '') + (attributes || '');

        // Mark multiline strings with indent
        } else if (translation.indexOf('\n') !== -1) {
          translation = ' = \n  ' + translation.replace(/\n/g, '\n  ');

        // Simple strings
        } else {
          translation = ' = ' + translation;
        }

        var content = entity.key + translation,
            ast = fluentParser.parseEntry(content),
            entityAst = fluentParser.parseEntry(entity.original),
            error = null;

        // Parse error
        if (ast.type === 'Junk') {
          error = ast.annotations[0].message;

        // TODO: Should be removed by bug 1237667
        // Detect missing values
        } else if (entityAst && ast && entityAst.value && !ast.value) {
          error = "Please make sure to fill in the value";
        // Detect missing attributes
        } else if (entityAst.attributes && ast.attributes && entityAst.attributes.length !== ast.attributes.length) {
          error = "Please make sure to fill in all the attributes";
        }

        if (error) {
          return {
            error: error
          };
        }

        return fluentSerializer.serializeEntry(ast);
      },

      /*
       * Get simplified preview of the FTL object
       */
      getSimplePreview: function(object, fallback, entity) {
        var response = fallback;

        if (entity.format === 'ftl') {
          // Transfrom complex FTL-based strings into single-value strings
          var source = object.original || object.string;

          if (!source) {
            return response;
          }

          ast = fluentParser.parseEntry(source);

          if (ast.value) {
            response = this.serializePlaceables(ast.value.elements);

          // Attributes
          } else {
            var attributes = ast.attributes;
            if (attributes && attributes.length) {
              response = this.serializePlaceables(attributes[0].value.elements);
            }
          }

          // Plurals
          if (ast.value && ast.value.elements && ast.value.elements.length && ast.value.elements[0].expression && ast.value.elements[0].variants) {
            var variants = ast.value.elements[0].variants,
                isFTLplural = variants.every(function(element) {
                  var key = element.key.name,
                      isPlural = Pontoon.CLDR_PLURALS.indexOf(key) !== -1,
                      isInteger = element.key.type === 'NumberExpression';

                  return isPlural || isInteger;
                });

            if (isFTLplural) {
              response = this.serializePlaceables(variants[0].value.elements);
              entity.isFTLplural = true;
            }
          }

          // Mark complex strings
          if (ast.attributes && ast.attributes.length || (ast.value && ast.value.elements && ast.value.elements.length && ast.value.elements[0].expression && ast.value.elements[0].variants.length > 1)) {
            object.isComplexFTL = true;

          // Update entity and translation objects
          } else {
            if (object.hasOwnProperty('original')) {
              object.original = response;
              response = object.marked = Pontoon.doNotRender(response);
            } else if (object.hasOwnProperty('string')) {
              object.string = response;
            }
          }
        }

        return response;
      },

      /*
       * Focus first field of the FTL editor
       */
      focusFirstField: function(object, fallback, entity) {
        $('#ftl-area input.value:visible:first').focus();
      }

    }
  });
}(Pontoon || {}));

$(function() {

  // Ignore editing for anonymous users
  if (!Pontoon.user.id) {
    return;
  }

  // Toggle FTL mode
  $('#ftl').click(function (e) {
    e.preventDefault();
    Pontoon.fluent.toggleEditor(!$(this).is('.active'));
  });

  // Add attribute
  $('#add-attribute').click(function (e) {
    e.preventDefault();
    $('#ftl-area .attributes ul:first').append($('#ftl-area .attributes .template li').clone());
  });

  // Remove attribute
  $('#ftl-area section').on('click', 'sub.remove', function (e) {
    e.preventDefault();
    $(this).parents('li').remove();
  });

  // Generate access key list
  $('#ftl-area .attributes').on('keyup', 'input:first', function() {
    var active = $('.accesskeys').find('.active').html(),
        unique = $(this).val().toUpperCase().split('').filter(function(item, i, ar) {
          return ar.indexOf(item) === i;
        });

    $('.accesskeys').empty();

    $.each(unique, function(i, v) {
      $('.accesskeys').append('
' + v + '
');
    });
  });

  // Select access key via click
  $('#ftl-area .attributes').on('click', '.accesskeys div', function() {
    var selected = $(this).is('.active');
    $('.accesskeys div').removeClass('active');

    if (!selected) {
      $(this).addClass('active');
    }

    $('#ftl-id-accesskey').val($('.accesskeys div.active').html());
  });

  // Select access key via input
  $('#ftl-area .attributes').on('keyup', '#ftl-id-accesskey', function() {
    var accesskey = $(this).val().toUpperCase();

    if (accesskey) {
      $('.accesskeys div').removeClass('active');
      $('.accesskeys div:contains("' + accesskey + '")').addClass('active');
    }
  });

});
<|endoftext|>


<commit_before>console.log(Intl.getCanonicalLocales('EN-US'));
// expected output: Array ["en-US"]

console.log(Intl.getCanonicalLocales(['EN-US', 'Fr']));
// expected output: Array ["en-US", "fr"]

try {
  Intl.getCanonicalLocales('EN_US');
} catch (err) {
  console.log(err);
  // expected output: RangeError: invalid language tag: EN_US
}
<commit_msg>calling toString on error (getcanonicalLocales) (#2041)<commit_after>console.log(Intl.getCanonicalLocales('EN-US'));
// expected output: Array ["en-US"]

console.log(Intl.getCanonicalLocales(['EN-US', 'Fr']));
// expected output: Array ["en-US", "fr"]

try {
  Intl.getCanonicalLocales('EN_US');
} catch (err) {
  console.log(err.toString());
  // expected output: RangeError: invalid language tag: EN_US
}
<|endoftext|>


<commit_before>/*!
 * Titanium Air by Dawson Toth and Rick Blalock
 * http://www.appcelerator.com/
 *
 * Provides a light, dynamic, MVC framework for building Titanium Mobile applications.
 *
 * Portions of this code base are directly taken from or derived from the Appcelerator Redux project by Dawson Toth.
 * https://github.com/dawsontoth/Appcelerator-Titanium-Redux
 *
 * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
 */

var TiAir = {};
(function(TiAir, context) {

    /**
     * Define our default options. These will take affect if the user does not pass anything to the init function.
     */
    TiAir.options = {
        defaultURL: { controller: 'default', action: 'default' },
        navigator: 'default',
        controllers: [],
        models: [],
        views: {}
    };

    /**
     * The following are private variables, and should not be altered.
     */
    var models = {};
    var views = {};
    var controllers = {}, controllerID, actionID;
    var navigator;
    var openCache = {};

    /**
     * Initializes TiAir, intelligently loading our application.
     * @param options
     */
    TiAir.init = function(options) {
        this.options = this.merge(options, this.options);

        this.initModels();
        this.initViews();
        this.initControllers();

        this.initNavigator();

        if (this.options.defaultURL) {
            this.openURL(this.options.defaultURL);
        }
    };

    /**
     * Initializes the models passed in to TiAir.init.
     */
    TiAir.initModels = function() {
        if (!this.options.models || !this.options.models.length) {
            throw 'TiAir :: Error :: No models passed in to TiAir.init.';
        }
        TiAir.iterateFiles(this.options.models, function(result) {
            if (!result.file.exists()) {
                throw 'TiAir :: Error :: Model passed in to TiAir.init does not exist: ' + result.file.nativePath;
            }
            var model = null;
            eval(result.file.read().toString());
            if (model) {
                TiAir.createModel({
                    id: result.name.split('.').shift(),
                    value: model
                });
            }
            else {
                Ti.API.warn('TiAir :: Warning :: Model ' + result.file.nativePath.split('.') + '');
            }
        }, 'models');
    };

    /**
     * Creates a model.
     * @param args
     */
    TiAir.createModel = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No model passed to TiAir.createModel.';
        }
        if (args.id == null) {
            throw 'TiAir :: Error :: No id set on model passed to TiAir.createModel.';
        }
        models[args.id] = args.value;
    };

    /**
     * Retrieves a model by its id.
     * @param id
     */
    TiAir.getModel = function(id) {
        if (models[id]) {
            return models[id];
        }
        throw 'TiAir :: Error :: No model with the id ' + id + ' exists.';
    };

    /**
     * Initializes the navigator passed in to TiAir.init.
     */
    TiAir.initNavigator = function() {
        var navigator = null;
        eval(Ti.Filesystem.getFile('navigators/' + this.options.navigator).read().toString());
        if (navigator) {
            TiAir.createNavigator({
                id: this.options.navigator.split('.').shift(),
                value: navigator
            });
        }
        else {
            throw 'TiAir :: Error :: No navigator found with id ' + this.options.navigator + ' in navigators directory.';
        }
        navigator.init(this);
    };

    /**
     * Creates a navigator.
     * @param args
     */
    TiAir.createNavigator = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No navigator passed to TiAir.createNavigator.';
        }
        if (!args.id) {
            throw 'TiAir :: Error :: No id set on navigator passed to TiAir.createNavigator.';
        }
        navigator = args.value;
    };

    /**
     * Gets a particular navigator.
     */
    TiAir.getNavigator = function() {
        return navigator;
    };

    /**
     * Initializes the controllers passed in to TiAir.init.
     */
    TiAir.initControllers = function() {
        if (!this.options.controllers || !this.options.controllers.length) {
            throw 'TiAir :: Error :: No controllers passed in to TiAir.init.';
        }
        TiAir.iterateFiles(this.options.controllers, function(result) {
            if (!result.file.exists()) {
                throw 'TiAir :: Error :: Controller passed in to TiAir.init does not exist: ' + result.file.nativePath;
            }
            var controller = null;
            eval(result.file.read().toString());
            if (controller) {
                TiAir.createController({
                    id: result.name.split('.').shift(),
                    value: controller
                });
            }
        }, 'controllers');
    };

    /**
     * Creates a controller so that it can later be used.
     * @param args
     */
    TiAir.createController = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No controller passed to TiAir.createController.';
        }
        if (!args.id) {
            throw 'TiAir :: Error :: No id set on controller passed to TiAir.createController.';
        }
        controllers[args.id] = args.value;
    };

    /**
     * Retrieves a controller based on its id.
     * @param id
     */
    TiAir.getController = function(id) {
        if (controllers[id]) {
            return controllers[id];
        }
        throw 'TiAir :: Error :: No controller with the id ' + id + ' exists.';
    };

    /**
     * Initializes the views passed in to the TiAir.init function.
     */
    TiAir.initViews = function() {
        if (!this.options.views) {
            throw 'TiAir :: Error :: No views passed in to TiAir.init.';
        }
        TiAir.iterateFiles(this.options.views, function(result) {
            if (!result.file.exists()) {
                throw 'TiAir :: Error :: View passed in to TiAir.init does not exist: ' + result.file.nativePath;
            }
            var view = null;
            eval(result.file.read().toString());
            if (view) {
                TiAir.createView({
                    controllerID: result.base.split('/').pop(),
                    id: result.name.split('.').shift(),
                    value: view
                });
            }
        }, 'views');
    };

    /**
     * Creates a view so that it can be later retrieved and displayed.
     * @param args
     */
    TiAir.createView = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No view passed to TiAir.createView.';
        }
        if (!args.id) {
            throw 'TiAir :: Error :: No id set on view passed to TiAir.createView.';
        }
        if (!args.controllerID) {
            throw 'TiAir :: Error :: No controllerID set on view passed to TiAir.createView.';
        }
        if (!views[args.controllerID]) {
            views[args.controllerID] = {};
        }
        views[args.controllerID][args.id] = args.value;
    };

    /**
     * Retrieves a view based on the controller and view ids.
     * @param controllerID
     * @param viewID
     */
    TiAir.getView = function(controllerID, viewID) {
        var retVal = (views[controllerID] && views[controllerID][viewID]) || (views.shared && views.shared[viewID]);
        if (retVal == null) {
            throw 'TiAir :: Error :: No view found with id ' + viewID + '; searched in ' + controllerID + ' and shared.';
        }
        return retVal;
    };

    /**
     * Opens a particular URL, mapping any arguments onto a particular controller's action, then calling the navigator.
     * Note that views are cached, so subsequent calls to open an identical URL will load up much quicker.
     * @param url
     */
    TiAir.openURL = function(url) {
        var controller = this.getController(url.controller);
        var hash;
        for (var n in url) {
            hash += n + ':' + url[n] + '/';
        }
        var args = Array().slice.call(arguments);
        var navigator = this.getNavigator();

        var toOpen;
        if (openCache[hash]) {
            toOpen = openCache[hash];
        }
        else {
            // determine which view needs to be loaded
            controllerID = url.controller;
            actionID = url.action;
            if (controller.actions[actionID] == null) {
                throw 'TiAir :: Error :: No action found with id ' + actionID + ' in ' + controllerID;
            }
            // dynamically map arguments in the URL to the arguments in the action
            var actionArgs = [], expectedArgs = controller.actions[actionID].toString().split('(')[1].split(')')[0].split(',');
            if (expectedArgs.length > 0 && expectedArgs[0] != '') {
                for (var i = 0, l = expectedArgs.length; i < l; i++) {
                    actionArgs.push(url[expectedArgs[i].split(' ').join('')]);
                }
            }
            // call the view
            toOpen = controller.actions[actionID].apply(controller.actions, actionArgs);
            if (url.cache) {
                openCache[hash] = toOpen;
            }
        }

        if (toOpen != null) {
            args.splice(1, 0, toOpen);
            navigator.open.apply(navigator, args);
        }
    };

    /**
     * Closes the particular view, passing the request on to the navigator.
     * @param view The view that should be closed.
     */
    TiAir.close = function(view) {
        this.getNavigator().close(view);
    };

    /**
     * Returns a view, mixing in any model data.ll throw an error if called otherwise.
     * @param {...number} var_args An optional string in the form "viewName" or "controller/viewName", followed by an optional model.
     */
    context.AirView = function() {
        if (!controllerID || !actionID) {
            throw 'TiAir :: Error :: View called from outside of a controller or action.';
        }
        var viewID, model;
        // no arguments? show the view with the same id as the action, then.
        if (arguments.length == 0) {
            viewID = actionID;
        }
        else {
            if (typeof arguments[0] === 'string') {
                var url = arguments[0].split('/');
                if (url.length == 1) {
                    viewID = url[0];
                }
                else {
                    controllerID = url[0];
                    viewID = url[1];
                }
                model = arguments.length > 1 && arguments[1];
            }
            else {
                viewID = actionID;
                model = arguments[0];
            }
        }
        var view = TiAir.getView(controllerID, viewID);
        return typeof view === 'function' ? view(model || {}) : view;
    };

    /**
     * Returns a view, mixing in any model data.
     * @param id The id of the model to retrieve
     */
    context.AirModel = function(id) {
        var model = TiAir.getModel(id);
        return typeof model === 'function' ? model() : model;
    };

    /**
     * Returns the response from an action.
     * @param url The url of the action to call
     * @param {...number} var_args Any arguments that should be passed to the action.
     */
    context.AirAction = function(url) {
        var controller = TiAir.getController(url.controller);
        actionID = url.action;
        if (controller.actions[actionID] == null) {
            throw 'TiAir :: Error :: No action found with id ' + actionID + ' in ' + controllerID;
        }
        var currentActionID = actionID;
        // dynamically map arguments in the URL to the arguments in the action
        var actionArgs = [], expectedArgs = controller.actions[actionID].toString().split('(')[1].split(')')[0].split(',');
        if (expectedArgs.length > 0 && expectedArgs[0] != '') {
            for (var i = 0, l = expectedArgs.length; i < l; i++) {
                var expectedArg = expectedArgs[i].split(' ').join('');
                actionArgs.push(url[expectedArg]);
            }
        }

        // call the action
        var retVal = controller.actions[actionID].apply(controller.actions, actionArgs);
        actionID = currentActionID;
        return retVal;
    };

    /**
     * Merges the properties of the two objects. Taken from redux.
     * @param {Object} original
     * @param {Object} overrider
     */
    TiAir.merge = function merge(original, overrider) {
        if (original == null) {
            return overrider || {};
        }
        if (overrider == null) {
            return original;
        }
        for (var index in overrider) {
            if (overrider.hasOwnProperty(index)) {
                if (typeof original[index] == 'undefined') {
                    original[index] = overrider[index];
                } else if (typeof overrider[index] == 'object') {
                    original[index] = merge(original[index], overrider[index]);
                }
            }
        }
        return original;
    };

    /**
     * Iterates through the files represented in the passed in enumerable.
     * @param arr An array, object, or object of arrays to iterate over.
     * @param callback A function that will be called once for each value in the enumerable.
     */
    TiAir.iterateFiles = function iterateFiles(arr, callback, base) {
        if (arr.length) {
            // arrays contain file names; pass our results to the callback
            for (var i = 0, l = arr.length; i < l; i++) {
                callback({
                    file: (base && Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, base, arr[i]))
                            || Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, arr[i]),
                    name: arr[i],
                    base: base
                });
            }
        }
        else {
            // but objects contain child properties with arrays of child file names (the properties are folders)
            for (var p in arr) {
                // recurse on it, setting it as the base
                iterateFiles(arr[p], callback, base + '/' + p);
            }
        }
    };

})(TiAir, this);<commit_msg>Updated TiAir header<commit_after>/*!
 * Titanium Air by Dawson Toth and Rick Blalock
 * https://github.com/dawsontoth/TiAir
 *
 * Provides a light, dynamic, MVC framework for building Titanium Mobile applications.
 *
 * Portions of this code base are directly taken from or derived from the Appcelerator Redux project by Dawson Toth.
 * https://github.com/dawsontoth/Appcelerator-Titanium-Redux
 *
 * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
 */

var TiAir = {};
(function(TiAir, context) {

    /**
     * Define our default options. These will take affect if the user does not pass anything to the init function.
     */
    TiAir.options = {
        defaultURL: { controller: 'default', action: 'default' },
        navigator: 'default',
        controllers: [],
        models: [],
        views: {}
    };

    /**
     * The following are private variables, and should not be altered.
     */
    var models = {};
    var views = {};
    var controllers = {}, controllerID, actionID;
    var navigator;
    var openCache = {};

    /**
     * Initializes TiAir, intelligently loading our application.
     * @param options
     */
    TiAir.init = function(options) {
        this.options = this.merge(options, this.options);

        this.initModels();
        this.initViews();
        this.initControllers();

        this.initNavigator();

        if (this.options.defaultURL) {
            this.openURL(this.options.defaultURL);
        }
    };

    /**
     * Initializes the models passed in to TiAir.init.
     */
    TiAir.initModels = function() {
        if (!this.options.models || !this.options.models.length) {
            throw 'TiAir :: Error :: No models passed in to TiAir.init.';
        }
        TiAir.iterateFiles(this.options.models, function(result) {
            if (!result.file.exists()) {
                throw 'TiAir :: Error :: Model passed in to TiAir.init does not exist: ' + result.file.nativePath;
            }
            var model = null;
            eval(result.file.read().toString());
            if (model) {
                TiAir.createModel({
                    id: result.name.split('.').shift(),
                    value: model
                });
            }
            else {
                Ti.API.warn('TiAir :: Warning :: Model ' + result.file.nativePath.split('.') + '');
            }
        }, 'models');
    };

    /**
     * Creates a model.
     * @param args
     */
    TiAir.createModel = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No model passed to TiAir.createModel.';
        }
        if (args.id == null) {
            throw 'TiAir :: Error :: No id set on model passed to TiAir.createModel.';
        }
        models[args.id] = args.value;
    };

    /**
     * Retrieves a model by its id.
     * @param id
     */
    TiAir.getModel = function(id) {
        if (models[id]) {
            return models[id];
        }
        throw 'TiAir :: Error :: No model with the id ' + id + ' exists.';
    };

    /**
     * Initializes the navigator passed in to TiAir.init.
     */
    TiAir.initNavigator = function() {
        var navigator = null;
        eval(Ti.Filesystem.getFile('navigators/' + this.options.navigator).read().toString());
        if (navigator) {
            TiAir.createNavigator({
                id: this.options.navigator.split('.').shift(),
                value: navigator
            });
        }
        else {
            throw 'TiAir :: Error :: No navigator found with id ' + this.options.navigator + ' in navigators directory.';
        }
        navigator.init(this);
    };

    /**
     * Creates a navigator.
     * @param args
     */
    TiAir.createNavigator = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No navigator passed to TiAir.createNavigator.';
        }
        if (!args.id) {
            throw 'TiAir :: Error :: No id set on navigator passed to TiAir.createNavigator.';
        }
        navigator = args.value;
    };

    /**
     * Gets a particular navigator.
     */
    TiAir.getNavigator = function() {
        return navigator;
    };

    /**
     * Initializes the controllers passed in to TiAir.init.
     */
    TiAir.initControllers = function() {
        if (!this.options.controllers || !this.options.controllers.length) {
            throw 'TiAir :: Error :: No controllers passed in to TiAir.init.';
        }
        TiAir.iterateFiles(this.options.controllers, function(result) {
            if (!result.file.exists()) {
                throw 'TiAir :: Error :: Controller passed in to TiAir.init does not exist: ' + result.file.nativePath;
            }
            var controller = null;
            eval(result.file.read().toString());
            if (controller) {
                TiAir.createController({
                    id: result.name.split('.').shift(),
                    value: controller
                });
            }
        }, 'controllers');
    };

    /**
     * Creates a controller so that it can later be used.
     * @param args
     */
    TiAir.createController = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No controller passed to TiAir.createController.';
        }
        if (!args.id) {
            throw 'TiAir :: Error :: No id set on controller passed to TiAir.createController.';
        }
        controllers[args.id] = args.value;
    };

    /**
     * Retrieves a controller based on its id.
     * @param id
     */
    TiAir.getController = function(id) {
        if (controllers[id]) {
            return controllers[id];
        }
        throw 'TiAir :: Error :: No controller with the id ' + id + ' exists.';
    };

    /**
     * Initializes the views passed in to the TiAir.init function.
     */
    TiAir.initViews = function() {
        if (!this.options.views) {
            throw 'TiAir :: Error :: No views passed in to TiAir.init.';
        }
        TiAir.iterateFiles(this.options.views, function(result) {
            if (!result.file.exists()) {
                throw 'TiAir :: Error :: View passed in to TiAir.init does not exist: ' + result.file.nativePath;
            }
            var view = null;
            eval(result.file.read().toString());
            if (view) {
                TiAir.createView({
                    controllerID: result.base.split('/').pop(),
                    id: result.name.split('.').shift(),
                    value: view
                });
            }
        }, 'views');
    };

    /**
     * Creates a view so that it can be later retrieved and displayed.
     * @param args
     */
    TiAir.createView = function(args) {
        if (args == null) {
            throw 'TiAir :: Error :: No view passed to TiAir.createView.';
        }
        if (!args.id) {
            throw 'TiAir :: Error :: No id set on view passed to TiAir.createView.';
        }
        if (!args.controllerID) {
            throw 'TiAir :: Error :: No controllerID set on view passed to TiAir.createView.';
        }
        if (!views[args.controllerID]) {
            views[args.controllerID] = {};
        }
        views[args.controllerID][args.id] = args.value;
    };

    /**
     * Retrieves a view based on the controller and view ids.
     * @param controllerID
     * @param viewID
     */
    TiAir.getView = function(controllerID, viewID) {
        var retVal = (views[controllerID] && views[controllerID][viewID]) || (views.shared && views.shared[viewID]);
        if (retVal == null) {
            throw 'TiAir :: Error :: No view found with id ' + viewID + '; searched in ' + controllerID + ' and shared.';
        }
        return retVal;
    };

    /**
     * Opens a particular URL, mapping any arguments onto a particular controller's action, then calling the navigator.
     * Note that views are cached, so subsequent calls to open an identical URL will load up much quicker.
     * @param url
     */
    TiAir.openURL = function(url) {
        var controller = this.getController(url.controller);
        var hash;
        for (var n in url) {
            hash += n + ':' + url[n] + '/';
        }
        var args = Array().slice.call(arguments);
        var navigator = this.getNavigator();

        var toOpen;
        if (openCache[hash]) {
            toOpen = openCache[hash];
        }
        else {
            // determine which view needs to be loaded
            controllerID = url.controller;
            actionID = url.action;
            if (controller.actions[actionID] == null) {
                throw 'TiAir :: Error :: No action found with id ' + actionID + ' in ' + controllerID;
            }
            // dynamically map arguments in the URL to the arguments in the action
            var actionArgs = [], expectedArgs = controller.actions[actionID].toString().split('(')[1].split(')')[0].split(',');
            if (expectedArgs.length > 0 && expectedArgs[0] != '') {
                for (var i = 0, l = expectedArgs.length; i < l; i++) {
                    actionArgs.push(url[expectedArgs[i].split(' ').join('')]);
                }
            }
            // call the view
            toOpen = controller.actions[actionID].apply(controller.actions, actionArgs);
            if (url.cache) {
                openCache[hash] = toOpen;
            }
        }

        if (toOpen != null) {
            args.splice(1, 0, toOpen);
            navigator.open.apply(navigator, args);
        }
    };

    /**
     * Closes the particular view, passing the request on to the navigator.
     * @param view The view that should be closed.
     */
    TiAir.close = function(view) {
        this.getNavigator().close(view);
    };

    /**
     * Returns a view, mixing in any model data.ll throw an error if called otherwise.
     * @param {...number} var_args An optional string in the form "viewName" or "controller/viewName", followed by an optional model.
     */
    context.AirView = function() {
        if (!controllerID || !actionID) {
            throw 'TiAir :: Error :: View called from outside of a controller or action.';
        }
        var viewID, model;
        // no arguments? show the view with the same id as the action, then.
        if (arguments.length == 0) {
            viewID = actionID;
        }
        else {
            if (typeof arguments[0] === 'string') {
                var url = arguments[0].split('/');
                if (url.length == 1) {
                    viewID = url[0];
                }
                else {
                    controllerID = url[0];
                    viewID = url[1];
                }
                model = arguments.length > 1 && arguments[1];
            }
            else {
                viewID = actionID;
                model = arguments[0];
            }
        }
        var view = TiAir.getView(controllerID, viewID);
        return typeof view === 'function' ? view(model || {}) : view;
    };

    /**
     * Returns a view, mixing in any model data.
     * @param id The id of the model to retrieve
     */
    context.AirModel = function(id) {
        var model = TiAir.getModel(id);
        return typeof model === 'function' ? model() : model;
    };

    /**
     * Returns the response from an action.
     * @param url The url of the action to call
     * @param {...number} var_args Any arguments that should be passed to the action.
     */
    context.AirAction = function(url) {
        var controller = TiAir.getController(url.controller);
        actionID = url.action;
        if (controller.actions[actionID] == null) {
            throw 'TiAir :: Error :: No action found with id ' + actionID + ' in ' + controllerID;
        }
        var currentActionID = actionID;
        // dynamically map arguments in the URL to the arguments in the action
        var actionArgs = [], expectedArgs = controller.actions[actionID].toString().split('(')[1].split(')')[0].split(',');
        if (expectedArgs.length > 0 && expectedArgs[0] != '') {
            for (var i = 0, l = expectedArgs.length; i < l; i++) {
                var expectedArg = expectedArgs[i].split(' ').join('');
                actionArgs.push(url[expectedArg]);
            }
        }

        // call the action
        var retVal = controller.actions[actionID].apply(controller.actions, actionArgs);
        actionID = currentActionID;
        return retVal;
    };

    /**
     * Merges the properties of the two objects. Taken from redux.
     * @param {Object} original
     * @param {Object} overrider
     */
    TiAir.merge = function merge(original, overrider) {
        if (original == null) {
            return overrider || {};
        }
        if (overrider == null) {
            return original;
        }
        for (var index in overrider) {
            if (overrider.hasOwnProperty(index)) {
                if (typeof original[index] == 'undefined') {
                    original[index] = overrider[index];
                } else if (typeof overrider[index] == 'object') {
                    original[index] = merge(original[index], overrider[index]);
                }
            }
        }
        return original;
    };

    /**
     * Iterates through the files represented in the passed in enumerable.
     * @param arr An array, object, or object of arrays to iterate over.
     * @param callback A function that will be called once for each value in the enumerable.
     */
    TiAir.iterateFiles = function iterateFiles(arr, callback, base) {
        if (arr.length) {
            // arrays contain file names; pass our results to the callback
            for (var i = 0, l = arr.length; i < l; i++) {
                callback({
                    file: (base && Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, base, arr[i]))
                            || Ti.Filesystem.getFile(Ti.Filesystem.resourcesDirectory, arr[i]),
                    name: arr[i],
                    base: base
                });
            }
        }
        else {
            // but objects contain child properties with arrays of child file names (the properties are folders)
            for (var p in arr) {
                // recurse on it, setting it as the base
                iterateFiles(arr[p], callback, base + '/' + p);
            }
        }
    };

})(TiAir, this);<|endoftext|>


<commit_before>/**
* Implementation of an ImageView. It displays an Image whose url is assumed to be static (for simplicity)
**/
define(["logger", "util", "promise", "imageprocessor", "job", "filesystem", "transformation", "rectangle", "svg", "imagemetadata", "backbone"], function(Logger, Util, Promise, ImageProcessor, Job, FileSystem, Transformation, Rectangle, SVG, ImageMetadata) {
    var fullResolutionGenerationTimeout = 500;
    var BigThumbnailPixelSize = 500; // ideally this could be dynamically computed depending on the device's capabilities
    var SmallThumbnailPixelSize = 100; // ideally this could be dynamically computed depending on the device's capabilities
    var id = 0;
    var ImageView = Backbone.View.extend({
        tagName: "span",
        className: "galleryImage",
        initialize: function() {
            this.id = id++;
//            this.listenTo(this.model, "change", this.render);
            this.model.on("all", function(event) {
//                Logger.log("iv model changed", event);
                this.render();
            }.bind(this));

            if (this.attributes && this.attributes.thumbnailUpdateEnabled) {
                this.setThumbnailUpdateEnabled(true);
            }

            /*
                the DOM will look like this:
                <g transform="translate(...)">
                    <clipPath id=clipID>
                        <rect .../>
                    </clipPath>
                    <rect> <-- background
                    <g clip-path=url(clipID)>
                        <image transform="matrix(...)">
                    </g>
                    <image> <-- full resolution piece of image
                    <rect> <-- border
                </g>
            */

            var instance = this;
            this.el = document.createElementNS("http://www.w3.org/2000/svg", "g");
            this.g = document.createElementNS("http://www.w3.org/2000/svg", "g");
            this.clipPath = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
            this.clipID = _.uniqueId("clip");
            this.clipPath.setAttribute("id", this.clipID);

            this.clipRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");

            this.tileBackground = document.createElementNS("http://www.w3.org/2000/svg", "rect");
            this.tileBackground.setAttribute("class", "background");

            this.tileBorder = document.createElementNS("http://www.w3.org/2000/svg", "rect");
            this.tileBorder.setAttribute("clip-path", "url(#" + this.clipID + ")");

            this.image = document.createElementNS("http://www.w3.org/2000/svg", "image");
            this.fullResolutionImage = document.createElementNS("http://www.w3.org/2000/svg", "image");
            this.g.setAttribute("clip-path", "url(#" + this.clipID + ")");
            this.g.appendChild(this.image);
            this.clipPath.appendChild(this.clipRect);
            this.el.appendChild(this.clipPath);
            this.el.appendChild(this.tileBackground);
            this.el.appendChild(this.g);
            this.el.appendChild(this.fullResolutionImage);
            this.el.appendChild(this.tileBorder);
            this.transformation = "";

            // image contains some lower resolution image
            // fullResolutionImage contains the full resolution piece of picture

            /*
            * Set up attributes for interaction.
            * a user click will intersect either the tile or the image
            */
            var image = this.model;
            this.image.model = image;
            this.fullResolutionImage.model = image;
            this.tileBackground.model = image;
            this.image.view = this;
            this.fullResolutionImage.view = this;
            this.tileBackground.view = this;

            this.width = 0;
            this.height = 0;

            var instance = this;
            this.metadataPromise = new Promise(function(resolveMain, reject) {
                Logger.log("enqueuing job: metadata request");
                function getMetadata(resolve, reject) {
                    instance.getFullImagePromise().then(function(fullImage) {
                        try{
                        // Logger.log("metadata promise");
                        var metadata = new ImageMetadata();
                        metadata.set(ImageMetadata.Keys.FullSize, {
                            width: fullImage.width,
                            height: fullImage.height
                        });
                        var cover = !instance.isThumbnailUpdateEnabled();
                        var size = instance.getThumbnailSize();
                        var thumbnailURI = resizeImage(fullImage, size.width, size.height, cover);
                        metadata.set(ImageMetadata.Keys.ThumbnailURI, thumbnailURI);
                        resolveMain(metadata);
                        resolve();
                        } catch (e) {
                            Logger.log(e);
                        }
                    }, Util.onRejected);
                }
                try {
                    ImageProcessor.getInstance().getQueue().enqueue(new Job({
                        priority: Job.Priority.High + (instance.isThumbnailUpdateEnabled()? 2: 0),
                        f: getMetadata
                    }));
                }
                catch(e) {
                    Logger.log("exception enqueuing metadata request", e);
                }
            });

            // Set up fixed resolution image
            // we don't need the result.
            // just make sure the next thumbnail generation is done after the previous one is done
            this.metadataPromise.then(function(metadata) {
                // Logger.log("metadatapromise done, ready for thumbnail");
                var thumbnailURI = metadata.get(ImageMetadata.Keys.ThumbnailURI);
                instance.image.setAttributeNS('http://www.w3.org/1999/xlink','href', thumbnailURI);
            });

            this.setVisible(true);

            this.render();
        },
        render: function() {
            var image = this.model;
            if (image) {
                this.tileBorder.classList.add("tile");
                if (!this.isThumbnailUpdateEnabled()) {
                    this.tileBorder.classList.toggle("selected", image.get("isSelected"));
                }
                this.tileBorder.classList.toggle("favorite", image.get("isFavorite"));

                // from http://www.eccesignum.org/blog/solving-display-refreshredrawrepaint-issues-in-webkit-browsers
                this.tileBorder.style.display='none';
                this.tileBorder.offsetHeight; // no need to store this anywhere, the reference is enough
                this.tileBorder.style.display='block';
            }
        },
        setThumbnailUpdateEnabled: function(enabled) {
            this.thumbnailUpdateEnabled = enabled;
        },
        isThumbnailUpdateEnabled: function() {
            return this.thumbnailUpdateEnabled;
        },
        /**
        * Returns a promise for the size of the full resolution image
        **/
        getFullSizePromise: function() {
            return this.metadataPromise.then(function(metadata) {
                return metadata.get(ImageMetadata.Keys.FullSize);
            });
        },
        /**
        * Returns the tile size
        **/
        getSize: function() {
            return this.size;
        },
        setSize: function(size) {
            if (this.size &&
                this.size.width === size.width &&
                this.size.height === size.height)
                return;

            this.size = size;
            this.image.setAttribute("width", size.width);
            this.image.setAttribute("height", size.height);
            this.fullResolutionImage.setAttribute("width", size.width);
            this.fullResolutionImage.setAttribute("height", size.height);
            this.clipRect.setAttribute("width", size.width);
            this.clipRect.setAttribute("height", size.height);
            this.tileBorder.setAttribute("width", size.width);
            this.tileBorder.setAttribute("height", size.height);
            this.tileBackground.setAttribute("width", size.width);
            this.tileBackground.setAttribute("height", size.height);

            this.requestThumbnailUpdate();
        },
        getTransformation: function() {
            return this.transformation;
        },
        setTransformation: function(t) {
            this.transformation = t;
            this.image.setAttribute("transform", t);

            // merges the whole transformation chain into just one
            var consolidatedTransform = this.image.transform.baseVal.consolidate();
            // returns null when transform is empty
            if (!consolidatedTransform)
                return;
            var m = consolidatedTransform.matrix;

            var strmatrix = "matrix(" + m.a + ", " + m.c + ", " + m.b + ", " + m.d + ", " + m.e + ", " + m.f + ")";
            this.image.setAttribute("transform", strmatrix);

            this.requestThumbnailUpdate();
        },
        getThumbnailSize: function() {
            var size = this.isThumbnailUpdateEnabled()? BigThumbnailPixelSize: SmallThumbnailPixelSize;
            return {
                width: size,
                height: size
            };
        },
        /**
        * @returns the bounding rectangle of the tile
        **/
        getBoundingClientRect: function() {
            // cannot return this.el as the calculation returns the bounds
            // as if the image was not clipped
            return this.tileBorder.getBoundingClientRect();
        },
        requestThumbnailUpdate: function() {
            if (!this.isThumbnailUpdateEnabled()) {
                return;
            }
            if (this.timer)
                clearTimeout(this.timer);
            if (this.currentJob) {
                ImageProcessor.getInstance().getQueue().remove(this.currentJob);
                this.currentJob = null;
            }
            // clear full resolution image (using a small transparent image)
            this.fullResolutionImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== ");

            this.timer = setTimeout(
                function() {
                    // regenerate full resolution tile
                    var consolidatedTransform = this.image.transform.baseVal.consolidate();
                    // returns null when transform is empty
                    var m;
                    if (consolidatedTransform)
                        m = consolidatedTransform.matrix;
                    else
                        m = SVG.SVGSVGElement.createSVGMatrix();

                    // no skewing, no rotation
                    var instance = this;
                    var newJob = new Job({
                        priority: Job.Priority.Low + (this.isThumbnailUpdateEnabled()? 2: 0),
                        f: generateSubtile
                    });
                    function generateSubtile(resolve, reject) {
                        console.log("generate subtile");
                        Promise.all([instance.getFullImagePromise(), instance.getFullSizePromise()]).then(function(results) {
                            var fullImage = results[0];
                            var fullSize = results[1];
                            var fit = Transformation.getFitMatrix(fullSize, instance.getSize());
                            var modelToDevice = m.multiply(fit);

                            var thumbnailURI = getSubImage(fullImage, instance.getSize(), modelToDevice);
                            // only update the image if no new job has taken over this one
                            if (instance.currentJob === newJob) {
                                instance.fullResolutionImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', thumbnailURI);
                                instance.currentJob = null;
                            }
                            resolve();
                        });
                    }
                    newJob.set("f", generateSubtile);
                    Logger.log("enqueuing job: generate subtile");
                    this.currentJob = newJob;
                    ImageProcessor.getInstance().getQueue().enqueue(this.currentJob);

                }.bind(this), fullResolutionGenerationTimeout);
        },
        getFullImagePromise: function() {
            return new Promise(function(resolve) {
                try {
                    var image = this.model;
                    var fullImage = document.createElement("img");
                    fullImage.onload = function() {
                        resolve(fullImage);
                    };
                    var url = image.get("url");
                    // Logger.log(this.id, "setting fullimage on img", url);
                    var uri = FileSystem.getInstance().getDataURI(url);
                    uri.then(function(uri) {
                        // Logger.log("setting fullimage", url);
                        fullImage.src = uri;
                    });
                }
                catch (e) {
                    Logger.log(e);
                }
            }.bind(this));
        },
        setVisible: function(isVisible) {
            if (this.visible === isVisible)
                return;

            this.visible = isVisible;
            this.render();
        },
        isVisible: function() {
            return this.visible;
        }
    });

    /**
    * Resize an image and return the resized image's data URI
    * @param cover if false, will do contain instead
    **/
    function resizeImage(srcImageObject, width, height, cover) {
        // Logger.log("resizeImage, cover" + cover + "," + width + "," + height);
        var newWidth = width;
        var newHeight = height;

        // Calculate a new scale
        // The new scale will be the max of the two possible scales

        var scale = cover?
            Math.max(newWidth / srcImageObject.width, newHeight / srcImageObject.height) :
            Math.min(newWidth / srcImageObject.width, newHeight / srcImageObject.height);

        // New canvas
        var dst_canvas = document.createElement('canvas');
        if (cover) {
            dst_canvas.width = width;
            dst_canvas.height = height;
        }
        else {
            dst_canvas.width = scale * srcImageObject.width;
            dst_canvas.height = scale * srcImageObject.height;
        }

        // Draw Image content in canvas
        var dst_ctx = dst_canvas.getContext('2d');
        // Center the image if it is cropped:
        // translate by -(scaled size - output size) / 2
        var dx = -(srcImageObject.width * scale - dst_canvas.width) / 2;
        var dy = -(srcImageObject.height * scale - dst_canvas.height) / 2;
        dst_ctx.drawImage(srcImageObject, dx, dy, srcImageObject.width * scale, srcImageObject.height * scale);

        // Replace source of Image
        return dst_canvas.toDataURL();
    }

    /**
    * @param matrix matrix of the full transformation from src image size to device (ie it also contains fit transformation in it)
    **/
    function getSubImage(srcImageObject, thumbnailSize, matrix) {
        // New canvas
        var dst_canvas = document.createElement('canvas');
        dst_canvas.width = thumbnailSize.width;
        dst_canvas.height = thumbnailSize.height;

        // full size
        var fullSize = {
            width: srcImageObject.width,
            height: srcImageObject.height
        };

        var devImageRect = Transformation.transform(fullSize, matrix);

        var thumbnailRect = {
            x: 0,
            y: 0,
            width: thumbnailSize.width,
            height: thumbnailSize.height
        };
        var destRect = Rectangle.getIntersection(devImageRect, thumbnailRect);

        var sourceRect = Transformation.transform(destRect, matrix.inverse());
        // Draw Image content in canvas
        var dst_ctx = dst_canvas.getContext('2d');
        dst_ctx.drawImage(srcImageObject,
                        sourceRect.x, sourceRect.y, sourceRect.width, sourceRect.height,    // source
                        destRect.x, destRect.y, destRect.width, destRect.height);           // destination

        // Replace source of Image
        return dst_canvas.toDataURL();
    }

    return ImageView;
});<commit_msg>fixed deselection on click<commit_after>/**
* Implementation of an ImageView. It displays an Image whose url is assumed to be static (for simplicity)
**/
define(["logger", "util", "promise", "imageprocessor", "job", "filesystem", "transformation", "rectangle", "svg", "imagemetadata", "backbone"], function(Logger, Util, Promise, ImageProcessor, Job, FileSystem, Transformation, Rectangle, SVG, ImageMetadata) {
    var fullResolutionGenerationTimeout = 500;
    var BigThumbnailPixelSize = 500; // ideally this could be dynamically computed depending on the device's capabilities
    var SmallThumbnailPixelSize = 100; // ideally this could be dynamically computed depending on the device's capabilities
    var id = 0;
    var ImageView = Backbone.View.extend({
        tagName: "span",
        className: "galleryImage",
        initialize: function() {
            this.id = id++;
//            this.listenTo(this.model, "change", this.render);
            this.model.on("all", function(event) {
//                Logger.log("iv model changed", event);
                this.render();
            }.bind(this));

            if (this.attributes && this.attributes.thumbnailUpdateEnabled) {
                this.setThumbnailUpdateEnabled(true);
            }

            /*
                the DOM will look like this:
                <g transform="translate(...)">
                    <clipPath id=clipID>
                        <rect .../>
                    </clipPath>
                    <rect> <-- background
                    <g clip-path=url(clipID)>
                        <image transform="matrix(...)">
                    </g>
                    <image> <-- full resolution piece of image
                    <rect> <-- border
                </g>
            */

            var instance = this;
            this.el = document.createElementNS("http://www.w3.org/2000/svg", "g");
            this.g = document.createElementNS("http://www.w3.org/2000/svg", "g");
            this.clipPath = document.createElementNS("http://www.w3.org/2000/svg", "clipPath");
            this.clipID = _.uniqueId("clip");
            this.clipPath.setAttribute("id", this.clipID);

            this.clipRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");

            this.tileBackground = document.createElementNS("http://www.w3.org/2000/svg", "rect");
            this.tileBackground.setAttribute("class", "background");

            this.tileBorder = document.createElementNS("http://www.w3.org/2000/svg", "rect");
            this.tileBorder.setAttribute("clip-path", "url(#" + this.clipID + ")");

            this.image = document.createElementNS("http://www.w3.org/2000/svg", "image");
            this.fullResolutionImage = document.createElementNS("http://www.w3.org/2000/svg", "image");
            this.g.setAttribute("clip-path", "url(#" + this.clipID + ")");
            this.g.appendChild(this.image);
            this.clipPath.appendChild(this.clipRect);
            this.el.appendChild(this.clipPath);
            this.el.appendChild(this.tileBackground);
            this.el.appendChild(this.g);
            this.el.appendChild(this.fullResolutionImage);
            this.el.appendChild(this.tileBorder);
            this.transformation = "";

            // image contains some lower resolution image
            // fullResolutionImage contains the full resolution piece of picture

            /*
            * Set up attributes for interaction.
            * a user click will intersect either the tile or the image
            */
            var image = this.model;
            this.image.model = image;
            this.tileBorder.model = image;
            this.fullResolutionImage.model = image;
            this.tileBackground.model = image;
            this.image.view = this;
            this.fullResolutionImage.view = this;
            this.tileBackground.view = this;

            this.width = 0;
            this.height = 0;

            var instance = this;
            this.metadataPromise = new Promise(function(resolveMain, reject) {
                Logger.log("enqueuing job: metadata request");
                function getMetadata(resolve, reject) {
                    instance.getFullImagePromise().then(function(fullImage) {
                        try{
                        // Logger.log("metadata promise");
                        var metadata = new ImageMetadata();
                        metadata.set(ImageMetadata.Keys.FullSize, {
                            width: fullImage.width,
                            height: fullImage.height
                        });
                        var cover = !instance.isThumbnailUpdateEnabled();
                        var size = instance.getThumbnailSize();
                        var thumbnailURI = resizeImage(fullImage, size.width, size.height, cover);
                        metadata.set(ImageMetadata.Keys.ThumbnailURI, thumbnailURI);
                        resolveMain(metadata);
                        resolve();
                        } catch (e) {
                            Logger.log(e);
                        }
                    }, Util.onRejected);
                }
                try {
                    ImageProcessor.getInstance().getQueue().enqueue(new Job({
                        priority: Job.Priority.High + (instance.isThumbnailUpdateEnabled()? 2: 0),
                        f: getMetadata
                    }));
                }
                catch(e) {
                    Logger.log("exception enqueuing metadata request", e);
                }
            });

            // Set up fixed resolution image
            // we don't need the result.
            // just make sure the next thumbnail generation is done after the previous one is done
            this.metadataPromise.then(function(metadata) {
                // Logger.log("metadatapromise done, ready for thumbnail");
                var thumbnailURI = metadata.get(ImageMetadata.Keys.ThumbnailURI);
                instance.image.setAttributeNS('http://www.w3.org/1999/xlink','href', thumbnailURI);
            });

            this.setVisible(true);

            this.render();
        },
        render: function() {
            var image = this.model;
            if (image) {
                this.tileBorder.classList.add("tile");
                if (!this.isThumbnailUpdateEnabled()) {
                    this.tileBorder.classList.toggle("selected", image.get("isSelected"));
                }
                this.tileBorder.classList.toggle("favorite", image.get("isFavorite"));

                // from http://www.eccesignum.org/blog/solving-display-refreshredrawrepaint-issues-in-webkit-browsers
                this.tileBorder.style.display='none';
                this.tileBorder.offsetHeight; // no need to store this anywhere, the reference is enough
                this.tileBorder.style.display='block';
            }
        },
        setThumbnailUpdateEnabled: function(enabled) {
            this.thumbnailUpdateEnabled = enabled;
        },
        isThumbnailUpdateEnabled: function() {
            return this.thumbnailUpdateEnabled;
        },
        /**
        * Returns a promise for the size of the full resolution image
        **/
        getFullSizePromise: function() {
            return this.metadataPromise.then(function(metadata) {
                return metadata.get(ImageMetadata.Keys.FullSize);
            });
        },
        /**
        * Returns the tile size
        **/
        getSize: function() {
            return this.size;
        },
        setSize: function(size) {
            if (this.size &&
                this.size.width === size.width &&
                this.size.height === size.height)
                return;

            this.size = size;
            this.image.setAttribute("width", size.width);
            this.image.setAttribute("height", size.height);
            this.fullResolutionImage.setAttribute("width", size.width);
            this.fullResolutionImage.setAttribute("height", size.height);
            this.clipRect.setAttribute("width", size.width);
            this.clipRect.setAttribute("height", size.height);
            this.tileBorder.setAttribute("width", size.width);
            this.tileBorder.setAttribute("height", size.height);
            this.tileBackground.setAttribute("width", size.width);
            this.tileBackground.setAttribute("height", size.height);

            this.requestThumbnailUpdate();
        },
        getTransformation: function() {
            return this.transformation;
        },
        setTransformation: function(t) {
            this.transformation = t;
            this.image.setAttribute("transform", t);

            // merges the whole transformation chain into just one
            var consolidatedTransform = this.image.transform.baseVal.consolidate();
            // returns null when transform is empty
            if (!consolidatedTransform)
                return;
            var m = consolidatedTransform.matrix;

            var strmatrix = "matrix(" + m.a + ", " + m.c + ", " + m.b + ", " + m.d + ", " + m.e + ", " + m.f + ")";
            this.image.setAttribute("transform", strmatrix);

            this.requestThumbnailUpdate();
        },
        getThumbnailSize: function() {
            var size = this.isThumbnailUpdateEnabled()? BigThumbnailPixelSize: SmallThumbnailPixelSize;
            return {
                width: size,
                height: size
            };
        },
        /**
        * @returns the bounding rectangle of the tile
        **/
        getBoundingClientRect: function() {
            // cannot return this.el as the calculation returns the bounds
            // as if the image was not clipped
            return this.tileBorder.getBoundingClientRect();
        },
        requestThumbnailUpdate: function() {
            if (!this.isThumbnailUpdateEnabled()) {
                return;
            }
            if (this.timer)
                clearTimeout(this.timer);
            if (this.currentJob) {
                ImageProcessor.getInstance().getQueue().remove(this.currentJob);
                this.currentJob = null;
            }
            // clear full resolution image (using a small transparent image)
            this.fullResolutionImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== ");

            this.timer = setTimeout(
                function() {
                    // regenerate full resolution tile
                    var consolidatedTransform = this.image.transform.baseVal.consolidate();
                    // returns null when transform is empty
                    var m;
                    if (consolidatedTransform)
                        m = consolidatedTransform.matrix;
                    else
                        m = SVG.SVGSVGElement.createSVGMatrix();

                    // no skewing, no rotation
                    var instance = this;
                    var newJob = new Job({
                        priority: Job.Priority.Low + (this.isThumbnailUpdateEnabled()? 2: 0),
                        f: generateSubtile
                    });
                    function generateSubtile(resolve, reject) {
                        console.log("generate subtile");
                        Promise.all([instance.getFullImagePromise(), instance.getFullSizePromise()]).then(function(results) {
                            var fullImage = results[0];
                            var fullSize = results[1];
                            var fit = Transformation.getFitMatrix(fullSize, instance.getSize());
                            var modelToDevice = m.multiply(fit);

                            var thumbnailURI = getSubImage(fullImage, instance.getSize(), modelToDevice);
                            // only update the image if no new job has taken over this one
                            if (instance.currentJob === newJob) {
                                instance.fullResolutionImage.setAttributeNS('http://www.w3.org/1999/xlink', 'href', thumbnailURI);
                                instance.currentJob = null;
                            }
                            resolve();
                        });
                    }
                    newJob.set("f", generateSubtile);
                    Logger.log("enqueuing job: generate subtile");
                    this.currentJob = newJob;
                    ImageProcessor.getInstance().getQueue().enqueue(this.currentJob);

                }.bind(this), fullResolutionGenerationTimeout);
        },
        getFullImagePromise: function() {
            return new Promise(function(resolve) {
                try {
                    var image = this.model;
                    var fullImage = document.createElement("img");
                    fullImage.onload = function() {
                        resolve(fullImage);
                    };
                    var url = image.get("url");
                    // Logger.log(this.id, "setting fullimage on img", url);
                    var uri = FileSystem.getInstance().getDataURI(url);
                    uri.then(function(uri) {
                        // Logger.log("setting fullimage", url);
                        fullImage.src = uri;
                    });
                }
                catch (e) {
                    Logger.log(e);
                }
            }.bind(this));
        },
        setVisible: function(isVisible) {
            if (this.visible === isVisible)
                return;

            this.visible = isVisible;
            this.render();
        },
        isVisible: function() {
            return this.visible;
        }
    });

    /**
    * Resize an image and return the resized image's data URI
    * @param cover if false, will do contain instead
    **/
    function resizeImage(srcImageObject, width, height, cover) {
        // Logger.log("resizeImage, cover" + cover + "," + width + "," + height);
        var newWidth = width;
        var newHeight = height;

        // Calculate a new scale
        // The new scale will be the max of the two possible scales

        var scale = cover?
            Math.max(newWidth / srcImageObject.width, newHeight / srcImageObject.height) :
            Math.min(newWidth / srcImageObject.width, newHeight / srcImageObject.height);

        // New canvas
        var dst_canvas = document.createElement('canvas');
        if (cover) {
            dst_canvas.width = width;
            dst_canvas.height = height;
        }
        else {
            dst_canvas.width = scale * srcImageObject.width;
            dst_canvas.height = scale * srcImageObject.height;
        }

        // Draw Image content in canvas
        var dst_ctx = dst_canvas.getContext('2d');
        // Center the image if it is cropped:
        // translate by -(scaled size - output size) / 2
        var dx = -(srcImageObject.width * scale - dst_canvas.width) / 2;
        var dy = -(srcImageObject.height * scale - dst_canvas.height) / 2;
        dst_ctx.drawImage(srcImageObject, dx, dy, srcImageObject.width * scale, srcImageObject.height * scale);

        // Replace source of Image
        return dst_canvas.toDataURL();
    }

    /**
    * @param matrix matrix of the full transformation from src image size to device (ie it also contains fit transformation in it)
    **/
    function getSubImage(srcImageObject, thumbnailSize, matrix) {
        // New canvas
        var dst_canvas = document.createElement('canvas');
        dst_canvas.width = thumbnailSize.width;
        dst_canvas.height = thumbnailSize.height;

        // full size
        var fullSize = {
            width: srcImageObject.width,
            height: srcImageObject.height
        };

        var devImageRect = Transformation.transform(fullSize, matrix);

        var thumbnailRect = {
            x: 0,
            y: 0,
            width: thumbnailSize.width,
            height: thumbnailSize.height
        };
        var destRect = Rectangle.getIntersection(devImageRect, thumbnailRect);

        var sourceRect = Transformation.transform(destRect, matrix.inverse());
        // Draw Image content in canvas
        var dst_ctx = dst_canvas.getContext('2d');
        dst_ctx.drawImage(srcImageObject,
                        sourceRect.x, sourceRect.y, sourceRect.width, sourceRect.height,    // source
                        destRect.x, destRect.y, destRect.width, destRect.height);           // destination

        // Replace source of Image
        return dst_canvas.toDataURL();
    }

    return ImageView;
});<|endoftext|>