<commit_before>(function () {

/**
* MapItem is a ko representation for `item` objects.
*
* @param item: a raw object which contains keys called `key` and `value`.
*              the `value` in a item itself is an object, a mapping
*              of language codes to strings
* @param mappingContext: an object which has context of current UI language and whether
*                 `value` of MapItem is a file-path to an icon or a simple string
*/
var MapItem = function(item, index, mappingContext){
    var self = this;
    this.key = ko.observable(item.key);

    // attach a media-manager if item.value is a file-path to icon
    if (mappingContext.values_are_icons) {
        this.iconManager = new AppMenuMediaManager({
            ref: {
                "path": item.value[mappingContext.lang],
                "icon_type": "icon-picture",
                "media_type": "Image",
                "media_class": "CommCareImage",
                "icon_class": "icon-picture"
            },
            objectMap: mappingContext.multimedia,
            uploadController: iconUploader,
            defaultPath: 'jr://file/commcare/image/kv-icon' + index + '.png',
            inputElement: $("#" + makeSafeForCSS(this.key())),
        });
    }

    this.cssId = ko.computed(function(){
        return makeSafeForCSS(this.key());
    }, this);

    this.value = ko.computed(function() {
        // ko.observable for item.value
        var new_value = [];
        var langs = _.union(_(item.value).keys(), [mappingContext.lang]) ;
        _.each(langs, function(lang){
            // return ko reference to path in `iconManager` for current UI language value
            if (mappingContext.values_are_icons && lang == mappingContext.lang){
                new_value.push([lang, self.iconManager.customPath]);
            }
            // return new ko.observable for other languages
            else{
                new_value.push([lang, ko.observable(item.value[lang])]);
            }
        });
        return _.object(new_value);
    }, this);

    this.key.subscribe(function(newValue) {
        if(mappingContext.duplicatedItems.indexOf(newValue) === -1 && mappingContext._isItemDuplicated(newValue)) {
            mappingContext.duplicatedItems.push(newValue);
        }

    });

    this.key.subscribe(function(oldValue) {
        var index = mappingContext.duplicatedItems.indexOf(oldValue);
        if(index !== -1 && !mappingContext._isItemDuplicated(oldValue, 2)) {
            mappingContext.duplicatedItems.remove(oldValue);
        }
    }, null, "beforeChange");
};

/**
 * A MapList is an ordered list MapItem objects
 */
function MapList(o) {
    var self = this;
    self.lang = o.lang;
    self.langs = [o.lang].concat(o.langs);
    self.items = ko.observableArray();
    self.duplicatedItems = ko.observableArray();
    self.values_are_icons = o.values_are_icons || false;
    self.multimedia = o.multimedia;

    self.setItems = function (items) {
        self.items(_(items).map(function (item, i) {
            return new MapItem(item, i, self);
        }));
    };
    self.setItems(o.items);

    self.backup = function (value) {
        var backup;
        for (var i = 0; i < self.langs.length; i += 1) {
            var lang = self.langs[i];
            backup = value[lang];
            if (backup && backup() !== '') {
                return {lang: lang, value: backup()};
            }
        }
        return {lang: null, value: null};
    };
    self.removeItem = function (item) {
        self.items.remove(item);
        if(!self._isItemDuplicated(ko.utils.unwrapObservable(item.key)))
            self.duplicatedItems.remove(ko.utils.unwrapObservable(item.key));
    };
    self.addItem = function () {
        var raw_item = {key: '', value: {}};
        raw_item.value[self.lang] = '';

        var item = new MapItem(raw_item, self.items.length, self);
        self.items.push(item);
        if(self.duplicatedItems.indexOf('') === -1 && self._isItemDuplicated('')) {
            self.duplicatedItems.push('');
        }
    };

    self._isItemDuplicated = function(key, max_counts) {
        if(typeof(max_counts) === 'undefined') max_counts = 1;
        var items = self.getItems();
        var counter = 0;
        for(var i = 0; i < items.length; i++) {
            var item = items[i];
            if(ko.utils.unwrapObservable(item.key) === key) {
                counter++;
                if(counter > max_counts) return true;
            }
        }
        return false;
    };

    self.isItemDuplicated = function(key) {
        return self.duplicatedItems.indexOf(key) !== -1;
    };

    self.getItems = function () {
        return _(self.items()).map(function (item) {
            return {
                key: ko.utils.unwrapObservable(item.key),
                value: _.object(_(item.value()).map(function (value, lang) {
                    return [lang, ko.utils.unwrapObservable(value)];
                }))
            };
        });

    };
}

uiElement.key_value_mapping = function (o) {
    var m = new MapList(o);
    m.edit = ko.observable(true);
    m.buttonText = o.buttonText || "Edit",
    m.openModal = function () {
        // create a throw-away modal every time
        // lets us create a sandbox for editing that you can cancel
        var $modalDiv = $('

');
        var copy = new MapList({
            lang: o.lang,
            langs: o.langs,
            items: m.getItems(),
            values_are_icons: m.values_are_icons,
            multimedia: m.multimedia
        });
        $modalDiv.koApplyBindings({
            modalTitle: o.modalTitle,
            mapList: copy,
            save: function (data, e) {
                if(copy.duplicatedItems().length > 0) {
                    e.stopImmediatePropagation();
                } else {
                    m.setItems(copy.getItems());
                }
            }
        });

        var $modal = $modalDiv.find('.modal');
        $modal.appendTo('body');
        $modal.modal('show');
        $modal.on('hidden', function () {
            $modal.remove();
        });
    };
    m.setEdit = function (edit) {
        m.edit(edit);
    };
    var $div = $('

');
    $div.koApplyBindings(m);
    m.ui = $div;
    eventize(m);
    m.items.subscribe(function () {
        m.fire('change');
    });
    return m;
};

}());

// To stack icon-uploader modal on top of key-value-mapping modal
// Hide modal into the stack
$(document).on('show.bs.modal', '#hqimage', function () {
    var $km = $(".modal.in");
    $km.addClass("stacked-modal");
    $km.hide();
});
// Pop out hidden stack onto top
$(document).on('hide.bs.modal', '#hqimage', function () {
    var $km = $(".stacked-modal");
    $km.removeClass("stacked-modal");
    $km.show();
});

// To autogenerate cssid from random string
// copied from http://stackoverflow.com/questions/7627000/javascript-convert-string-to-safe-class-name-for-css
function makeSafeForCSS(name) {
    if (!name) {
        return "";
    }
    return name.replace(/[^a-z0-9]/g, function(s) {
        var c = s.charCodeAt(0);
        if (c == 32) return '-';
        if (c >= 65 && c <= 90) return '_' + s.toLowerCase();
        return '__' + ('000' + c.toString(16)).slice(-4);
    });
}<commit_msg>import js<commit_after>(function () {

/**
* MapItem is a ko representation for `item` objects.
*
* @param item: a raw object which contains keys called `key` and `value`.
*              the `value` in a item itself is an object, a mapping
*              of language codes to strings
* @param mappingContext: an object which has context of current UI language and whether
*                 `value` of MapItem is a file-path to an icon or a simple string
*/
var MapItem = function(item, index, mappingContext){
    var self = this;
    this.key = ko.observable(item.key);

    var app_manager = hqImport('app_manager/js/app_manager_media.js');
    var uploaders = hqImport('#app_manager/partials/nav_menu_media_js_common.html');
    // attach a media-manager if item.value is a file-path to icon
    if (mappingContext.values_are_icons) {
        this.iconManager = new app_manager.AppMenuMediaManager({
            ref: {
                "path": item.value[mappingContext.lang],
                "icon_type": "icon-picture",
                "media_type": "Image",
                "media_class": "CommCareImage",
                "icon_class": "icon-picture"
            },
            objectMap: mappingContext.multimedia,
            uploadController: uploaders.iconUploader,
            defaultPath: 'jr://file/commcare/image/kv-icon' + index + '.png',
            inputElement: $("#" + makeSafeForCSS(this.key())),
        });
    }

    this.cssId = ko.computed(function(){
        return makeSafeForCSS(this.key());
    }, this);

    this.value = ko.computed(function() {
        // ko.observable for item.value
        var new_value = [];
        var langs = _.union(_(item.value).keys(), [mappingContext.lang]) ;
        _.each(langs, function(lang){
            // return ko reference to path in `iconManager` for current UI language value
            if (mappingContext.values_are_icons && lang == mappingContext.lang){
                new_value.push([lang, self.iconManager.customPath]);
            }
            // return new ko.observable for other languages
            else{
                new_value.push([lang, ko.observable(item.value[lang])]);
            }
        });
        return _.object(new_value);
    }, this);

    this.key.subscribe(function(newValue) {
        if(mappingContext.duplicatedItems.indexOf(newValue) === -1 && mappingContext._isItemDuplicated(newValue)) {
            mappingContext.duplicatedItems.push(newValue);
        }

    });

    this.key.subscribe(function(oldValue) {
        var index = mappingContext.duplicatedItems.indexOf(oldValue);
        if(index !== -1 && !mappingContext._isItemDuplicated(oldValue, 2)) {
            mappingContext.duplicatedItems.remove(oldValue);
        }
    }, null, "beforeChange");
};

/**
 * A MapList is an ordered list MapItem objects
 */
function MapList(o) {
    var self = this;
    self.lang = o.lang;
    self.langs = [o.lang].concat(o.langs);
    self.items = ko.observableArray();
    self.duplicatedItems = ko.observableArray();
    self.values_are_icons = o.values_are_icons || false;
    self.multimedia = o.multimedia;

    self.setItems = function (items) {
        self.items(_(items).map(function (item, i) {
            return new MapItem(item, i, self);
        }));
    };
    self.setItems(o.items);

    self.backup = function (value) {
        var backup;
        for (var i = 0; i < self.langs.length; i += 1) {
            var lang = self.langs[i];
            backup = value[lang];
            if (backup && backup() !== '') {
                return {lang: lang, value: backup()};
            }
        }
        return {lang: null, value: null};
    };
    self.removeItem = function (item) {
        self.items.remove(item);
        if(!self._isItemDuplicated(ko.utils.unwrapObservable(item.key)))
            self.duplicatedItems.remove(ko.utils.unwrapObservable(item.key));
    };
    self.addItem = function () {
        var raw_item = {key: '', value: {}};
        raw_item.value[self.lang] = '';

        var item = new MapItem(raw_item, self.items.length, self);
        self.items.push(item);
        if(self.duplicatedItems.indexOf('') === -1 && self._isItemDuplicated('')) {
            self.duplicatedItems.push('');
        }
    };

    self._isItemDuplicated = function(key, max_counts) {
        if(typeof(max_counts) === 'undefined') max_counts = 1;
        var items = self.getItems();
        var counter = 0;
        for(var i = 0; i < items.length; i++) {
            var item = items[i];
            if(ko.utils.unwrapObservable(item.key) === key) {
                counter++;
                if(counter > max_counts) return true;
            }
        }
        return false;
    };

    self.isItemDuplicated = function(key) {
        return self.duplicatedItems.indexOf(key) !== -1;
    };

    self.getItems = function () {
        return _(self.items()).map(function (item) {
            return {
                key: ko.utils.unwrapObservable(item.key),
                value: _.object(_(item.value()).map(function (value, lang) {
                    return [lang, ko.utils.unwrapObservable(value)];
                }))
            };
        });

    };
}

uiElement.key_value_mapping = function (o) {
    var m = new MapList(o);
    m.edit = ko.observable(true);
    m.buttonText = o.buttonText || "Edit",
    m.openModal = function () {
        // create a throw-away modal every time
        // lets us create a sandbox for editing that you can cancel
        var $modalDiv = $('

');
        var copy = new MapList({
            lang: o.lang,
            langs: o.langs,
            items: m.getItems(),
            values_are_icons: m.values_are_icons,
            multimedia: m.multimedia
        });
        $modalDiv.koApplyBindings({
            modalTitle: o.modalTitle,
            mapList: copy,
            save: function (data, e) {
                if(copy.duplicatedItems().length > 0) {
                    e.stopImmediatePropagation();
                } else {
                    m.setItems(copy.getItems());
                }
            }
        });

        var $modal = $modalDiv.find('.modal');
        $modal.appendTo('body');
        $modal.modal('show');
        $modal.on('hidden', function () {
            $modal.remove();
        });
    };
    m.setEdit = function (edit) {
        m.edit(edit);
    };
    var $div = $('

');
    $div.koApplyBindings(m);
    m.ui = $div;
    eventize(m);
    m.items.subscribe(function () {
        m.fire('change');
    });
    return m;
};

}());

// To stack icon-uploader modal on top of key-value-mapping modal
// Hide modal into the stack
$(document).on('show.bs.modal', '#hqimage', function () {
    var $km = $(".modal.in");
    $km.addClass("stacked-modal");
    $km.hide();
});
// Pop out hidden stack onto top
$(document).on('hide.bs.modal', '#hqimage', function () {
    var $km = $(".stacked-modal");
    $km.removeClass("stacked-modal");
    $km.show();
});

// To autogenerate cssid from random string
// copied from http://stackoverflow.com/questions/7627000/javascript-convert-string-to-safe-class-name-for-css
function makeSafeForCSS(name) {
    if (!name) {
        return "";
    }
    return name.replace(/[^a-z0-9]/g, function(s) {
        var c = s.charCodeAt(0);
        if (c == 32) return '-';
        if (c >= 65 && c <= 90) return '_' + s.toLowerCase();
        return '__' + ('000' + c.toString(16)).slice(-4);
    });
}<|endoftext|>


<commit_before>function makeOp(name, sym, f) {
  f.toString = function() { return sym; };
  return {
    name: name,
    label: sym,
    action: function() { this.op = f; }
  };
}

var DEFAULT_OP = function(a1) { return a1; };
DEFAULT_OP.toString = function() { return ""; };

FOAModel({
  name: 'Calc',

  properties: [
    { name: 'a1', defaultValue: '0' },
    { name: 'a2', defaultValue: '0' },
    {
      name: 'op',
      postSet: function() { this.a1 = this.a2; this.a2 = 0; this.pos = 1; },
      defaultValue: DEFAULT_OP
    },
    { name: 'history', model_: 'StringArrayProperty', factory: function() { return []; } }
  ],

  methods: {
    num: function(n) { this.a2 = this.a2 == 0 ? n : this.a2.toString() + n; }
  },

  actions: [
    {
      name: 'ac',
      label: 'AC',
      action: function() { this.a1 = this.a2 = 0; this.history = []; this.op = DEFAULT_OP; }
    },
    {
      name: 'sign',
      label: '+/-',
      action: function() { this.a2 = - this.a2; }
    },
    { name: '1', action: function() { this.num(1); } },
    { name: '2', action: function() { this.num(2); } },
    { name: '3', action: function() { this.num(3); } },
    { name: '4', action: function() { this.num(4); } },
    { name: '5', action: function() { this.num(5); } },
    { name: '6', action: function() { this.num(6); } },
    { name: '7', action: function() { this.num(7); } },
    { name: '8', action: function() { this.num(8); } },
    { name: '9', action: function() { this.num(9); } },
    { name: '0', action: function() { this.num(0); } },
    makeOp('div', '/', function(a1, a2) { return a1 / a2; }),
    makeOp('div', '*', function(a1, a2) { return a1 * a2; }),
    makeOp('div', '+', function(a1, a2) { return a1 + a2; }),
    makeOp('div', '-', function(a1, a2) { return a1 - a2; }),
    {
      name: 'point',
      label: '.',
      action: function() { if ( this.a2.toString().indexOf('.') == -1 ) this.a2 = this.a2 + '.'; }
    },
    {
      name: 'equal',
      label: '=',
      action: function() {
        var a1 = this.a1;
        var a2 = this.a2;
        this.a1 = a2;
        this.history = this.history.concat([this.a2 = this.op(a1, a2)]);
      }
    }
  ]
});
<commit_msg>Switched / and * symbols to Unicode for division and multiplication.<commit_after>function makeOp(name, sym, f) {
  f.toString = function() { return sym; };
  return {
    name: name,
    label: sym,
    action: function() { this.op = f; }
  };
}

var DEFAULT_OP = function(a1) { return a1; };
DEFAULT_OP.toString = function() { return ''; };

FOAModel({
  name: 'Calc',

  properties: [
    { name: 'a1', defaultValue: '0' },
    { name: 'a2', defaultValue: '0' },
    {
      name: 'op',
      postSet: function() { this.a1 = this.a2; this.a2 = 0; this.pos = 1; },
      defaultValue: DEFAULT_OP
    },
    { name: 'history', model_: 'StringArrayProperty', factory: function() { return []; } }
  ],

  methods: {
    num: function(n) { this.a2 = this.a2 == 0 ? n : this.a2.toString() + n; }
  },

  actions: [
    {
      name: 'ac',
      label: 'AC',
      action: function() { this.a1 = this.a2 = 0; this.history = []; this.op = DEFAULT_OP; }
    },
    {
      name: 'sign',
      label: '+/-',
      action: function() { this.a2 = - this.a2; }
    },
    { name: '1', action: function() { this.num(1); } },
    { name: '2', action: function() { this.num(2); } },
    { name: '3', action: function() { this.num(3); } },
    { name: '4', action: function() { this.num(4); } },
    { name: '5', action: function() { this.num(5); } },
    { name: '6', action: function() { this.num(6); } },
    { name: '7', action: function() { this.num(7); } },
    { name: '8', action: function() { this.num(8); } },
    { name: '9', action: function() { this.num(9); } },
    { name: '0', action: function() { this.num(0); } },
    makeOp('div', '\u00F7', function(a1, a2) { return a1 / a2; }),
    makeOp('div', '\u00D7', function(a1, a2) { return a1 * a2; }),
    makeOp('div', '+', function(a1, a2) { return a1 + a2; }),
    makeOp('div', '-', function(a1, a2) { return a1 - a2; }),
    {
      name: 'point',
      label: '.',
      action: function() { if ( this.a2.toString().indexOf('.') == -1 ) this.a2 = this.a2 + '.'; }
    },
    {
      name: 'equal',
      label: '=',
      action: function() {
        var a1 = this.a1;
        var a2 = this.a2;
        this.a1 = a2;
        this.history = this.history.concat([this.a2 = this.op(a1, a2)]);
      }
    }
  ]
});
<|endoftext|>


<commit_before>module('apps.cssParser').requires('lively.Network', 'lively.morphic.StyleSheetRepresentation').toRun(function(m) {

(function loadLib() {
    // FIXME load async!
    var url = URL.codeBase.withFilename('lib/cssParser.js'),
        src = url.asWebResource().beSync().get().content,
        // lib has to be adapted to suit our needs:
        // - declarations shall not be multiplied with vendor prefixes
        // - shorthands shall never be expanded
        vendorRepl = "kCSS_VENDOR_PREFIXES = {properties:[]};",
        expRepl = 'aExpandShorthands = this.expandShorthands;',
        parseRepl = 'aTryToPreserveComments, expandShorthands) {\n'
                  +'this.expandShorthands = expandShorthands;',
        rewritten = '(function (){'
                  + src.replace(/kCSS_VENDOR_PREFIXES([\s\S]*?)};/, vendorRepl)
                       .replace('aTryToPreserveComments) {', parseRepl)
                       .replace(/(?=this.preserveState\(\);[\x20\t\r\n\f]*var blocks = \[\];)/, expRepl)
                  + 'this.CSSParser = CSSParser;'
                  + '}).call(apps.cssParser);';
    try {
        eval(rewritten);
    } catch(e) {
        alert('CSS Parser lib failed to load because of:\n' + e);
    }
})();

Object.extend(apps.cssParser, {

    calculateCSSRuleSpecificity: function(selector) {
        /*
         * Code taken from Firebug Lite 1.4.0
         * Copyright (c) 2007, Parakey Inc.
        */
        var reSelectorTag = /(^|\s)(?:\w+)/g,
            reSelectorClass = /\.[\w\d_-]+/g,
            reSelectorId = /#[\w\d_-]+/g;

        var match = selector.match(reSelectorTag);
        var tagCount = match ? match.length : 0;

        match = selector.match(reSelectorClass);
        var classCount = match ? match.length : 0;

        match = selector.match(reSelectorId);
        var idCount = match ? match.length : 0;

        // FIXME: like that, tag count and class count can't
        // exceed 9 in one selector without distort the result
        return tagCount + 10*classCount + 100*idCount;
    },

    convertParsedStyleSheet: function (styleSheet, originMorph) {
        // Convert JSCSSP obj to our own style sheet object

        var notSupportedRuleAsComment = function (msg, rule) {
            console.warn(msg + ': ' + rule.parsedCssText);
            return new lively.morphic.StyleSheetComment('/* ' + msg + '\n' + rule.parsedCssText + '\n*/');
        };

        return new lively.morphic.StyleSheet(
        styleSheet.cssRules.collect(function (rule) {
            switch(rule.type) {
                case 0:
                    // Rule could not be parsed
                    console.warn('CSS Error ' + rule.error + ': ' + rule.parsedCssText);
                    return new lively.morphic.StyleSheetComment('/* CSS Error '
                        + rule.error + ':\n' + rule.parsedCssText + '*/');
                case 1:
                    return new lively.morphic.StyleSheetRule(
                    rule.selectorText(),
                    rule.declarations.collect(function (decl) {
                        if (decl.type === 1000) {
                            var vals;
                            if (apps.cssParser.isCommaSeparated(decl.property)) {
                                vals = [decl.valueText]
                            } else {
                                vals = decl.values.collect(
                                    function (val) {
                                        return val.value
                                    });
                            }
                            if(apps.cssParser.isShorthand(decl.property)) {
                                return new lively.morphic.StyleSheetShorthandDeclaration(
                                decl.property, vals, null, decl.priority);
                            } else {
                                return new lively.morphic.StyleSheetDeclaration(
                                decl.property, vals, null, decl.priority);
                            }
                        } else {
                            return new lively.morphic.StyleSheetInlineComment(
                                decl.parsedCssText);
                        }
                    }));
                case 2:
                    return notSupportedRuleAsComment('Charset rules not supported yet, sry!', rule);
                case 3:
                    return notSupportedRuleAsComment('Imports not supported yet, sry!', rule);
                case 4:
                    return notSupportedRuleAsComment('Media rules not supported yet, sry!', rule);
                case 5:
                    return new lively.morphic.StyleSheetFontFaceRule(
                    rule.descriptors.collect(function (decl) {
                        return new lively.morphic.StyleSheetDeclaration(
                        decl.property,
                        decl.values.collect(function (val) {
                            return val.value;
                        }),
                        null,
                        decl.priority)
                    }));
                case 6:
                    return notSupportedRuleAsComment('Page rules not supported yet, sry!', rule);
                case 7:
                case 8:
                    return notSupportedRuleAsComment('Keyframe rules not supported yet, sry!', rule);
                case 100:
                    return notSupportedRuleAsComment('Namespace rules not supported yet, sry!', rule);
                case 101:
                    return new lively.morphic.StyleSheetComment(
                    rule.parsedCssText);
                default:
                    return notSupportedRuleAsComment('This type of rule is not supported yet, sry!', rule);
            }
        }),
        originMorph);
    },
    isShorthand: function(property) {
        var propList = apps.cssParser.getPropList(),
            shorthand = propList[property] && propList[property].shorthandFor;
        return Object.isArray(shorthand) && shorthand.length > 0;
    },
    isCommaSeparated: function(property) {
        var propList = apps.cssParser.getPropList()
        return propList[property] && propList[property].commaSeparated;
    },
    parseShorthand: function(shorthandDeclaration) {
        var simStyleSheet = '* {' + shorthandDeclaration.getText() + ' }',
            parsedSimStyleSheet = apps.cssParser.parse(simStyleSheet,
                shorthandDeclaration.getOriginMorph(), true),
            decls = parsedSimStyleSheet.getRules().first().getDeclarations();
        if (!decls || decls.length === 0) return [shorthandDeclaration];
        return decls.collect(function(decl) {
            decl.setRule(shorthandDeclaration.getRule());
            return decl;
        });
    },

    parse: function(cssString, originMorph, expandShorthands) {
        // cssString should specifiy the css rules, e.g.
        // ".some-class { color: red; }"
        // returns the rules as javascript objects with the interface:
        // TODO define the interface

        var parser = new apps.cssParser.CSSParser(),
            parsedStyleSheet = parser.parse(cssString, false, true, expandShorthands);

        //console.log(parsedStyleSheet);
        return apps.cssParser.convertParsedStyleSheet(parsedStyleSheet, originMorph);
    },

    enhancePropList: function(orgPropList) {
        // Enhances the property list in apps.cssParser.props
        // by adding conclusive shorthands and shorthandFor
        // attributes to make it faster to use for parsing.
        var propList = {},
            markShorthands = function (property, properties) {
                    var shorthand = orgPropList[property].shorthand;
                    propList[property].shorthandFor =
                        propList[property].shorthandFor.concat(properties || []);
                    if (shorthand && orgPropList[shorthand]) {
                        if (properties) {
                            properties.push(property);
                        } else {
                            properties = [property];
                        }
                        // TODO: avoid running in circles
                        var shorthandsFor = markShorthands(shorthand, properties || []);
                        propList[property].shorthands =
                            propList[property].shorthands.concat(shorthandsFor);
                        return shorthandsFor.concat(property);
                    } else {
                        return [property];
                    }
                };

        // Prepare proplist
        for (var x in orgPropList) {
            propList[x] = {};
            propList[x].commaSeparated = orgPropList[x].commaSeparated || false;
            propList[x].values = orgPropList[x].values.slice() || [];
            propList[x].shorthands = [];
            propList[x].shorthandFor = [];
        }
        // Mark shorthands
        for (var x in orgPropList) {
            markShorthands(x);
        }
        // Make sure there are no duplicates in the shorthand attrs
        for (var x in propList) {
            propList[x].shorthands = propList[x].shorthands.uniq();
            propList[x].shorthandFor = propList[x].shorthandFor.uniq();
        }
        return propList;
    },
    getPropList: function() {
        // Returns the shorthand enhanced property list.
        // If already created return cached version
        if (apps.cssParser.enhancedPropertyList) {
            return apps.cssParser.enhancedPropertyList;
        } else {
            apps.cssParser.enhancedPropertyList = apps.cssParser.enhancePropList(
                apps.cssParser.props);
            return apps.cssParser.enhancedPropertyList;
        }
    },
    props: {
        /*
        Information to interpret and manipulate CSS properties.
        Since there are a lot of CSS properties out there, this
        is only a selection.
        Feel free to add missing properties!

        A property can have several value counts (i.e. the value of
        border-color could be 'black', but it could also be 'black black black blue').

        A value is of a certain type:
        0: Plain text (i.e. font-family; edit through text field)
        1: Length (i.e. width; edit through slider)
        2: Option (i.e. border-style; edit with drop-down box)
        3: Color (i.e. color; edit with color chooser)
        4: Shadow (i.e. box-shadow; edit with shadow dialog)

        Additionally, a property can also have a shorthand.
        I.e. 'border-top-color' is implicitly set by the shorthand 'border-color'
        */
        'background-color': {
            shorthand: 'background',
            values: [ // only one value for this property
            [3]]
        },
        'border': {
            values: [
            [1, 0, 3]]
        },
        'border-width': {
            shorthand: 'border',
            values: [
            // either one value ...
            [1],
            // ... or four
            [1, 1, 1, 1]]
        },
        'border-bottom-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-left-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-top-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-right-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-color': {
            shorthand: 'border',
            values: [
            // either one value ...
            [3],
            // ... or four
            [3, 3, 3, 3]]
        },
        'border-top-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-bottom-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-left-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-right-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-radius': {
            values: [
            // either one value ...
            [1],
            // ... or four
            [1, 1, 1, 1]]
        },
        'color': {
            values: [ // only one value for this property
            [3]]
        },
        'font-family': {
            commaSeparated: true,
            values: [[0]]
        },
        'background-image': {
            commaSeparated: true,
            values: [[0]]
        },
        'cursor': {
            commaSeparated: true,
            values: [[0]]
        },
        'box-shadow': {
            commaSeparated: true,
            values: [[4]]
        },
        'text-shadow': {
            commaSeparated: true,
            values: [[4]]
        }
    }

});

}); // end of module<commit_msg>More cleanup<commit_after>module('apps.cssParser').requires('lively.Network', 'lively.morphic.StyleSheetRepresentation').toRun(function(m) {

(function loadLib() {
    // FIXME load async!
    var url = URL.codeBase.withFilename('lib/cssParser.js'),
        src = url.asWebResource().beSync().get().content,
        // lib has to be adapted to suit our needs:
        // - declarations shall not be multiplied with vendor prefixes
        // - shorthands shall never be expanded
        vendorRepl = "kCSS_VENDOR_PREFIXES = {properties:[]};",
        expRepl = 'aExpandShorthands = this.expandShorthands;',
        parseRepl = 'aTryToPreserveComments, expandShorthands) {\n'
                  +'this.expandShorthands = expandShorthands;',
        rewritten = '(function (){'
                  + src.replace(/kCSS_VENDOR_PREFIXES([\s\S]*?)};/, vendorRepl)
                       .replace('aTryToPreserveComments) {', parseRepl)
                       .replace(/(?=this.preserveState\(\);[\x20\t\r\n\f]*var blocks = \[\];)/, expRepl)
                  + 'this.CSSParser = CSSParser;'
                  + '}).call(apps.cssParser);';
    try {
        eval(rewritten);
    } catch(e) {
        alert('CSS Parser lib failed to load because of:\n' + e);
    }
})();

Object.extend(apps.cssParser, {

    calculateCSSRuleSpecificity: function(selector) {
        /*
         * Code taken from Firebug Lite 1.4.0
         * Copyright (c) 2007, Parakey Inc.
        */
        var reSelectorTag = /(^|\s)(?:\w+)/g,
            reSelectorClass = /\.[\w\d_-]+/g,
            reSelectorId = /#[\w\d_-]+/g,
            match, tagCount, classCount, idCount;

        match = selector.match(reSelectorTag),
        tagCount = match ? match.length : 0;

        match = selector.match(reSelectorClass);
        classCount = match ? match.length : 0;

        match = selector.match(reSelectorId);
        idCount = match ? match.length : 0;

        // FIXME: like that, tag count and class count can't
        // exceed 9 in one selector without distort the result
        return tagCount + 10*classCount + 100*idCount;
    },

    convertParsedStyleSheet: function (styleSheet, originMorph) {
        // Convert JSCSSP obj to our own style sheet object

        var notSupportedRuleAsComment = function (msg, rule) {
            console.warn(msg + ': ' + rule.parsedCssText);
            return new lively.morphic.StyleSheetComment('/* ' + msg + '\n' + rule.parsedCssText + '\n*/');
        };

        return new lively.morphic.StyleSheet(
        styleSheet.cssRules.collect(function (rule) {
            switch(rule.type) {
                case 0:
                    // Rule could not be parsed
                    console.warn('CSS Error ' + rule.error + ': ' + rule.parsedCssText);
                    return new lively.morphic.StyleSheetComment('/* CSS Error '
                        + rule.error + ':\n' + rule.parsedCssText + '*/');
                case 1:
                    return new lively.morphic.StyleSheetRule(
                    rule.selectorText(),
                    rule.declarations.collect(function (decl) {
                        if (decl.type === 1000) {
                            var vals;
                            if (apps.cssParser.isCommaSeparated(decl.property)) {
                                vals = [decl.valueText]
                            } else {
                                vals = decl.values.collect(
                                    function (val) {
                                        return val.value
                                    });
                            }
                            if(apps.cssParser.isShorthand(decl.property)) {
                                return new lively.morphic.StyleSheetShorthandDeclaration(
                                decl.property, vals, null, decl.priority);
                            } else {
                                return new lively.morphic.StyleSheetDeclaration(
                                decl.property, vals, null, decl.priority);
                            }
                        } else {
                            return new lively.morphic.StyleSheetInlineComment(
                                decl.parsedCssText);
                        }
                    }));
                case 2:
                    return notSupportedRuleAsComment('Charset rules not supported yet, sry!', rule);
                case 3:
                    return notSupportedRuleAsComment('Imports not supported yet, sry!', rule);
                case 4:
                    return notSupportedRuleAsComment('Media rules not supported yet, sry!', rule);
                case 5:
                    return new lively.morphic.StyleSheetFontFaceRule(
                    rule.descriptors.collect(function (decl) {
                        return new lively.morphic.StyleSheetDeclaration(
                        decl.property,
                        decl.values.collect(function (val) {
                            return val.value;
                        }),
                        null,
                        decl.priority)
                    }));
                case 6:
                    return notSupportedRuleAsComment('Page rules not supported yet, sry!', rule);
                case 7:
                case 8:
                    return notSupportedRuleAsComment('Keyframe rules not supported yet, sry!', rule);
                case 100:
                    return notSupportedRuleAsComment('Namespace rules not supported yet, sry!', rule);
                case 101:
                    return new lively.morphic.StyleSheetComment(
                    rule.parsedCssText);
                default:
                    return notSupportedRuleAsComment('This type of rule is not supported yet, sry!', rule);
            }
        }),
        originMorph);
    },
    isShorthand: function(property) {
        var propList = apps.cssParser.getPropList(),
            shorthand = propList[property] && propList[property].shorthandFor;
        return Object.isArray(shorthand) && shorthand.length > 0;
    },
    isCommaSeparated: function(property) {
        var propList = apps.cssParser.getPropList()
        return propList[property] && propList[property].commaSeparated;
    },
    parseShorthand: function(shorthandDeclaration) {
        var simStyleSheet = '* {' + shorthandDeclaration.getText() + ' }',
            parsedSimStyleSheet = apps.cssParser.parse(simStyleSheet,
                shorthandDeclaration.getOriginMorph(), true),
            decls = parsedSimStyleSheet.getRules().first().getDeclarations();
        if (!decls || decls.length === 0) return [shorthandDeclaration];
        return decls.collect(function(decl) {
            decl.setRule(shorthandDeclaration.getRule());
            return decl;
        });
    },

    parse: function(cssString, originMorph, expandShorthands) {
        // cssString should specifiy the css rules, e.g.
        // ".some-class { color: red; }"
        // returns the rules as javascript objects with the interface:
        // TODO define the interface

        var parser = new apps.cssParser.CSSParser(),
            parsedStyleSheet = parser.parse(cssString, false, true, expandShorthands);

        //console.log(parsedStyleSheet);
        return apps.cssParser.convertParsedStyleSheet(parsedStyleSheet, originMorph);
    },

    enhancePropList: function(orgPropList) {
        // Enhances the property list in apps.cssParser.props
        // by adding conclusive shorthands and shorthandFor
        // attributes to make it faster to use for parsing.
        var propList = {}, x;
        function markShorthands(property, properties) {
            var shorthand = orgPropList[property].shorthand;
            propList[property].shorthandFor =
                propList[property].shorthandFor.concat(properties || []);
            if (shorthand && orgPropList[shorthand]) {
                if (properties) {
                    properties.push(property);
                } else {
                    properties = [property];
                }
                // TODO: avoid running in circles
                var shorthandsFor = markShorthands(shorthand, properties || []);
                propList[property].shorthands =
                    propList[property].shorthands.concat(shorthandsFor);
                return shorthandsFor.concat(property);
            } else {
                return [property];
            }
        };

        // Prepare proplist
        for (x in orgPropList) {
            propList[x] = {};
            propList[x].commaSeparated = orgPropList[x].commaSeparated || false;
            propList[x].values = orgPropList[x].values.slice() || [];
            propList[x].shorthands = [];
            propList[x].shorthandFor = [];
        }
        // Mark shorthands
        for (x in orgPropList) {
            markShorthands(x);
        }
        // Make sure there are no duplicates in the shorthand attrs
        for (x in propList) {
            propList[x].shorthands = propList[x].shorthands.uniq();
            propList[x].shorthandFor = propList[x].shorthandFor.uniq();
        }
        return propList;
    },
    getPropList: function() {
        // Returns the shorthand enhanced property list.
        // If already created return cached version
        if (!apps.cssParser.enhancedPropertyList)
            apps.cssParser.enhancedPropertyList = apps.cssParser.enhancePropList(
                apps.cssParser.props);
        return apps.cssParser.enhancedPropertyList;
    },
    props: {
        /*
        Information to interpret and manipulate CSS properties.
        Since there are a lot of CSS properties out there, this
        is only a selection.
        Feel free to add missing properties!

        A property can have several value counts (i.e. the value of
        border-color could be 'black', but it could also be 'black black black
        blue').

        A value is of a certain type:
        0: Plain text (i.e. font-family; edit through text field)
        1: Length (i.e. width; edit through slider)
        2: Option (i.e. border-style; edit with drop-down box)
        3: Color (i.e. color; edit with color chooser)
        4: Shadow (i.e. box-shadow; edit with shadow dialog)

        Additionally, a property can also have a shorthand. I.e.
        'border-top-color' is implicitly set by the shorthand 'border-color'
        */
        'background-color': {
            shorthand: 'background',
            values: [ // only one value for this property
            [3]]
        },
        'border': {
            values: [
            [1, 0, 3]]
        },
        'border-width': {
            shorthand: 'border',
            values: [
            // either one value ...
            [1],
            // ... or four
            [1, 1, 1, 1]]
        },
        'border-bottom-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-left-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-top-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-right-width': {
            shorthand: 'border-width',
            values: [ // only one value for this property
            [1]]
        },
        'border-color': {
            shorthand: 'border',
            values: [
            // either one value ...
            [3],
            // ... or four
            [3, 3, 3, 3]]
        },
        'border-top-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-bottom-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-left-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-right-color': {
            shorthand: 'border-color',
            values: [ // only one value for this property
            [3]]
        },
        'border-radius': {
            values: [
            // either one value ...
            [1],
            // ... or four
            [1, 1, 1, 1]]
        },
        'color': {
            values: [ // only one value for this property
            [3]]
        },
        'font-family': {
            commaSeparated: true,
            values: [[0]]
        },
        'background-image': {
            commaSeparated: true,
            values: [[0]]
        },
        'cursor': {
            commaSeparated: true,
            values: [[0]]
        },
        'box-shadow': {
            commaSeparated: true,
            values: [[4]]
        },
        'text-shadow': {
            commaSeparated: true,
            values: [[4]]
        }
    }

});

}); // end of module<|endoftext|>


<commit_before>/* @flow */
"use strict";

/*globals cordova*/
import Emitter from "yasmf-emitter";

// private properties -- we need some symbols
let _selectors = Symbol();
let _useSmoothScrolling = Symbol();
let _smoothScrollDuration = Symbol();
let _showElementUnderKeyboard = Symbol();

/**
 * given a selector string, return matching elements in an Array
 * @param {string} selectorString
 * @return Array<Node>
 */
function getScrollContainers(selectorString/*: string*/)/*: Array<Node>*/ {
    return Array.from(document.querySelectorAll(selectorString));
}

/**
 * Given an element, returns an array representing the start and end of
 * the current selection.
 * @param {Node} focusedElement
 * @return Array<number>
 */
function getElementSelection(focusedElement/*: Node*/)/*: Array<number>*/ {
    return [focusedElement.selectionStart, focusedElement.selectionEnd];
}

/**
 * Given an element and a tuple representing the start and end of the selection,
 * set the selection on the element.
 * @param {Node} focusedElement
 * @param {number} selectionStart the start of the selection
 * @param {number} selectionEnd   the end of the selection
 */
function setElementSelection(focusedElement/*: Node*/, [selectionStart/*: number*/, selectionEnd/*: number*/] = [0, 0])/*: void*/ {
    focusedElement.selectionStart = selectionStart;
    focusedElement.selectionEnd = selectionEnd;
}

function showElementUnderKeyboard(keyboardHeight/*: number*/) {
    let e = document.createElement("div");
    e.className = "sk-element-under-keyboard";
    e.style.position = "absolute";
    e.style.bottom = "0";
    e.style.left = "0";
    e.style.right = "0";
    e.style.zIndex = "9999999999999";
    e.style.height = `${keyboardHeight}px`;
    document.body.appendChild(e);
}

function hideElementUnderKeyboard()/*: void*/ {
    let els = Array.from(document.querySelectorAll(".sk-element-under-keyboard"));
    els.forEach((el) => document.body.removeChild(el));
}

/**
 * Saves the element's current text selection, then resets it to 0. After a tick, it
 * restores the element's saved text selection.
 * This is to fix iOS's buggy behavior regarding cursor positioning after the scrolling
 * of an element.
 *
 * @param {Node} focusedElement
 */
function handleTextSelection(focusedElement/*: Node*/)/*: void*/ {
    setTimeout(() => {
        // save the selection
        let selection = getElementSelection(focusedElement);

        // reset the selection to 0,0
        setElementSelection(focusedElement);

        // after a short delay, restore the selection
        setTimeout(() => setElementSelection(focusedElement, selection), 33);
    }, 0);
}

/**
 * Provides methods for avoiding the soft keyboard. This tries to be automatic,
 * but you will need to supply scrollable selectors.
 */
export default class SoftKeyboard extends Emitter {

    /**
     * Construct an instance of the SoftKeyboard
     *
     * @return SoftKeyboard
     */
    constructor(options) {
        super(options);
    }

    /**
     * Initialize a SoftKeyboard. Will be called automatically during construction
     * @param {Array<string>} [selectors]       defaults to an empty array
     * @param {boolean} [useSmoothScrolling]    defaults to true
     * @param {number} [smoothScrollDuration]   defaults to 100
     * @param {boolean} [showElementUnderKeyboard]   defaults to false
     */
    init({selectors=[], useSmoothScrolling = true, smoothScrollDuration = 100,
          showElementUnderKeyboard = false} = {}) {
        // selectors: Array, useSmoothScrolling: boolean, smoothScrollDuration: number
        if (typeof cordova !== "undefined") {
            if (cordova.plugins && cordova.plugins.Keyboard) {
                cordova.plugins.Keyboard.disableScroll(true);
                window.addEventListener("native.keyboardshow", this.keyboardShown.bind(this));
                window.addEventListener("native.keyboardhide", this.keyboardHidden.bind(this));
            }
        }
        this[_selectors] = new Set();
        selectors.forEach(sel => this.addSelector(sel));
        this[_useSmoothScrolling] = useSmoothScrolling;
        this[_smoothScrollDuration] = smoothScrollDuration;
        this[_showElementUnderKeyboard] = showElementUnderKeyboard;
    }

    /**
     * Adds a selector
     * @param {string} selector    A CSS selector that identifies a scrolling container
     * @return {SoftKeyboard}
     */
    addSelector(selector/*: string*/)/*: SoftKeyboard*/ {
        this[_selectors].add(selector);
        return this;
    }

    /**
     * Removes a selector
     * @param {string} selector    A CSS selector that identifies a scrolling container
     * @return {SoftKeyboard}
     */
    removeSelector(selector/*: string*/)/*: SoftKeyboard*/ {
        this[_selectors].delete(selector);
        return this;
    }

    get selectors()/*: Array*/ {
        return Array.from(this[_selectors]);
    }

    get selectorString()/*: string*/ {
        return this.selectors.join(", ");
    }

    get useSmoothScrolling()/*: boolean*/ {
        return this[_useSmoothScrolling];
    }
    set useSmoothScrolling(b/*: boolean*/)/*: void*/ {
        this[_useSmoothScrolling] = b;
    }

    get smoothScrollDuration()/*: number*/ {
        return this[_smoothScrollDuration];
    }
    set smoothScrollDuration(d/*: number*/)/*: void*/ {
        this[_smoothScrollDuration] = d;
    }

    get showElementUnderKeyboard()/*: boolean*/ {
        return this[_showElementUnderKeyboard];
    }
    set showElementUnderKeyboard(b/*: boolean*/)/*: void*/ {
        this[_showElementUnderKeyboard] = b;
    }

    /**
     * Shows the keyboard, if possible
     */
    showKeyboard(force/*: boolean*/ = false)/*: void*/ {
        if (typeof cordova !== "undefined") {
            if (cordova.plugins && cordova.plugins.Keyboard) {
                cordova.plugins.Keyboard.show();
                return;
            }
        }
        if (force) {
            this.keyboardShown({keyboardHeight: 240});
        }
    }

    /**
     * Hide the keyboard, if possible
     */
    hideKeyboard(force/*: boolean*/ = false)/*: void*/ {
        if (typeof cordova !== "undefined") {
            if (cordova.plugins && cordova.plugins.Keyboard) {
                cordova.plugins.Keyboard.hide();
                return;
            }
        }
        if (force) {
            this.keyboardHidden({});
        }
    }

    /**
     * Triggered when the soft keyboard is displayed
     * @param {{keyboardHeight: number}} e    the event triggered from the keyboard plugin
     */
    keyboardShown(e)/*: void*/ {
        this.emit("keyboardShown", e);
        this.emit("willResize", e);
        setTimeout(() => {
            let screenHeight = window.innerHeight; //(document.body.clientWidth === window.screen.height ? window.screen.width : window.screen.height);
            let scrollContainers = getScrollContainers(this.selectorString);
            let keyboardHeight = 0; //e.keyboardHeight;

            if (this.showElementUnderKeyboard) {
                showElementUnderKeyboard(keyboardHeight);
            }

            // for each scroll container in the DOM, we need to calculate the
            // the height it should be to fit in the reduced view
            scrollContainers.forEach((sc) => {
                let scTop = sc.getBoundingClientRect().top;
                // now that we know the top of the scroll container, the height of the
                // the screen, and the height of the keyboard, we can calculate the
                // appropriate max-height for the container.
                sc.style.maxHeight = "" + (screenHeight - keyboardHeight - scTop) + "px";
            });
            this.emit("didResize", e);
            // changing the height isn't sufficient: we need to scroll any focused
            // element into view.
            setTimeout(() => {
                let focusedElement = document.querySelector(":focus");
                if (focusedElement) {
                    if (!this.useSmoothScrolling || !window.requestAnimationFrame) {
                        // scroll the element into view, but only if we have to
                        if (focusedElement.scrollIntoViewIfNeeded) {
                            focusedElement.scrollIntoViewIfNeeded();
                        } else {
                            // aim for the bottom of the viewport
                            focusedElement.scrollIntoView(false);
                        }
                        // iOS doesn't always position the cursor correctly after
                        // a scroll operation. Clear the selection so that iOS is
                        // forced to recompute where the cursor should appear.
                        handleTextSelection(focusedElement);
                    } else {
                        // to scroll the element smoothly into view, things become a little
                        // more difficult.
                        let fElTop = focusedElement.getBoundingClientRect().top,
                            sc = focusedElement, scTop, scBottom,
                            selectorString = this.selectorString;
                        // find the containing scroll container if we can
                        while (sc && !sc.matches(selectorString)) {
                            sc = sc.parentElement;
                        }
                        if (sc) {
                            scTop = sc.getBoundingClientRect().top;
                            scBottom = sc.getBoundingClientRect().bottom;
                            if (fElTop < scTop || fElTop > (((scBottom - scTop) / 2) + scTop)) {
                                // the element isn't above the keyboard (or is too far above),
                                // scroll it into view smoothly
                                let targetTop = ((scBottom - scTop) / 2) + scTop,
                                    deltaTop = fElTop - targetTop,
                                    origScrollTop = sc.scrollTop,
                                    startTimestamp = null;
                                // animate our scroll
                                let scrollStep;
                                window.requestAnimationFrame(scrollStep = (timestamp) => {
                                    if (!startTimestamp) {
                                        startTimestamp = timestamp;
                                    }
                                    var progressDelta = timestamp - startTimestamp,
                                        pct = progressDelta / this.smoothScrollDuration;
                                    sc.scrollTop = origScrollTop + (deltaTop * pct);
                                    if (progressDelta < this.smoothScrollDuration) {
                                        window.requestAnimationFrame(scrollStep);
                                    } else {
                                        // set the scroll to the final desired position, just in case
                                        // we didn't actually get there (or overshot)
                                        sc.scrollTop = origScrollTop + deltaTop;
                                        handleTextSelection(focusedElement);
                                    }
                                });
                            }
                        }
                    }
                }
            }, 0);
        }, 0);
    }

    /**
     * Triggered when the soft keyboard is hidden
     * @param {*} e
     */
    keyboardHidden(e)/*: void*/ {
        setTimeout(() => {
            this.emit("keyboardHidden", e);
            this.emit("willResize", e);
            let scrollContainers = getScrollContainers(this.selectorString);
            scrollContainers.forEach(el => el.style.maxHeight = "");
            hideElementUnderKeyboard();
            this.emit("didResize", e);
        }, 0);
    }
}

export function createSoftKeyboard(...args) {
    return new SoftKeyboard(...args);
}
<commit_msg>log some stuff<commit_after>/* @flow */
"use strict";

/*globals cordova*/
import Emitter from "yasmf-emitter";

// private properties -- we need some symbols
let _selectors = Symbol();
let _useSmoothScrolling = Symbol();
let _smoothScrollDuration = Symbol();
let _showElementUnderKeyboard = Symbol();

/**
 * given a selector string, return matching elements in an Array
 * @param {string} selectorString
 * @return Array<Node>
 */
function getScrollContainers(selectorString/*: string*/)/*: Array<Node>*/ {
    return Array.from(document.querySelectorAll(selectorString));
}

/**
 * Given an element, returns an array representing the start and end of
 * the current selection.
 * @param {Node} focusedElement
 * @return Array<number>
 */
function getElementSelection(focusedElement/*: Node*/)/*: Array<number>*/ {
    return [focusedElement.selectionStart, focusedElement.selectionEnd];
}

/**
 * Given an element and a tuple representing the start and end of the selection,
 * set the selection on the element.
 * @param {Node} focusedElement
 * @param {number} selectionStart the start of the selection
 * @param {number} selectionEnd   the end of the selection
 */
function setElementSelection(focusedElement/*: Node*/, [selectionStart/*: number*/, selectionEnd/*: number*/] = [0, 0])/*: void*/ {
    focusedElement.selectionStart = selectionStart;
    focusedElement.selectionEnd = selectionEnd;
}

function showElementUnderKeyboard(keyboardHeight/*: number*/) {
    let e = document.createElement("div");
    e.className = "sk-element-under-keyboard";
    e.style.position = "absolute";
    e.style.bottom = "0";
    e.style.left = "0";
    e.style.right = "0";
    e.style.zIndex = "9999999999999";
    e.style.height = `${keyboardHeight}px`;
    document.body.appendChild(e);
}

function hideElementUnderKeyboard()/*: void*/ {
    let els = Array.from(document.querySelectorAll(".sk-element-under-keyboard"));
    els.forEach((el) => document.body.removeChild(el));
}

/**
 * Saves the element's current text selection, then resets it to 0. After a tick, it
 * restores the element's saved text selection.
 * This is to fix iOS's buggy behavior regarding cursor positioning after the scrolling
 * of an element.
 *
 * @param {Node} focusedElement
 */
function handleTextSelection(focusedElement/*: Node*/)/*: void*/ {
    setTimeout(() => {
        // save the selection
        let selection = getElementSelection(focusedElement);

        // reset the selection to 0,0
        setElementSelection(focusedElement);

        // after a short delay, restore the selection
        setTimeout(() => setElementSelection(focusedElement, selection), 33);
    }, 0);
}

/**
 * Provides methods for avoiding the soft keyboard. This tries to be automatic,
 * but you will need to supply scrollable selectors.
 */
export default class SoftKeyboard extends Emitter {

    /**
     * Construct an instance of the SoftKeyboard
     *
     * @return SoftKeyboard
     */
    constructor(options) {
        super(options);
    }

    /**
     * Initialize a SoftKeyboard. Will be called automatically during construction
     * @param {Array<string>} [selectors]       defaults to an empty array
     * @param {boolean} [useSmoothScrolling]    defaults to true
     * @param {number} [smoothScrollDuration]   defaults to 100
     * @param {boolean} [showElementUnderKeyboard]   defaults to false
     */
    init({selectors=[], useSmoothScrolling = true, smoothScrollDuration = 100,
          showElementUnderKeyboard = false} = {}) {
        // selectors: Array, useSmoothScrolling: boolean, smoothScrollDuration: number
        if (typeof cordova !== "undefined") {
            if (cordova.plugins && cordova.plugins.Keyboard) {
                cordova.plugins.Keyboard.disableScroll(true);
                window.addEventListener("native.keyboardshow", this.keyboardShown.bind(this));
                window.addEventListener("native.keyboardhide", this.keyboardHidden.bind(this));
            }
        }
        this[_selectors] = new Set();
        selectors.forEach(sel => this.addSelector(sel));
        this[_useSmoothScrolling] = useSmoothScrolling;
        this[_smoothScrollDuration] = smoothScrollDuration;
        this[_showElementUnderKeyboard] = showElementUnderKeyboard;
    }

    /**
     * Adds a selector
     * @param {string} selector    A CSS selector that identifies a scrolling container
     * @return {SoftKeyboard}
     */
    addSelector(selector/*: string*/)/*: SoftKeyboard*/ {
        this[_selectors].add(selector);
        return this;
    }

    /**
     * Removes a selector
     * @param {string} selector    A CSS selector that identifies a scrolling container
     * @return {SoftKeyboard}
     */
    removeSelector(selector/*: string*/)/*: SoftKeyboard*/ {
        this[_selectors].delete(selector);
        return this;
    }

    get selectors()/*: Array*/ {
        return Array.from(this[_selectors]);
    }

    get selectorString()/*: string*/ {
        return this.selectors.join(", ");
    }

    get useSmoothScrolling()/*: boolean*/ {
        return this[_useSmoothScrolling];
    }
    set useSmoothScrolling(b/*: boolean*/)/*: void*/ {
        this[_useSmoothScrolling] = b;
    }

    get smoothScrollDuration()/*: number*/ {
        return this[_smoothScrollDuration];
    }
    set smoothScrollDuration(d/*: number*/)/*: void*/ {
        this[_smoothScrollDuration] = d;
    }

    get showElementUnderKeyboard()/*: boolean*/ {
        return this[_showElementUnderKeyboard];
    }
    set showElementUnderKeyboard(b/*: boolean*/)/*: void*/ {
        this[_showElementUnderKeyboard] = b;
    }

    /**
     * Shows the keyboard, if possible
     */
    showKeyboard(force/*: boolean*/ = false)/*: void*/ {
        if (typeof cordova !== "undefined") {
            if (cordova.plugins && cordova.plugins.Keyboard) {
                cordova.plugins.Keyboard.show();
                return;
            }
        }
        if (force) {
            this.keyboardShown({keyboardHeight: 240});
        }
    }

    /**
     * Hide the keyboard, if possible
     */
    hideKeyboard(force/*: boolean*/ = false)/*: void*/ {
        if (typeof cordova !== "undefined") {
            if (cordova.plugins && cordova.plugins.Keyboard) {
                cordova.plugins.Keyboard.hide();
                return;
            }
        }
        if (force) {
            this.keyboardHidden({});
        }
    }

    /**
     * Triggered when the soft keyboard is displayed
     * @param {{keyboardHeight: number}} e    the event triggered from the keyboard plugin
     */
    keyboardShown(e)/*: void*/ {
        this.emit("keyboardShown", e);
        this.emit("willResize", e);
        console.log ("keyboard shown", e.keyboardHeight);
        setTimeout(() => {
            let screenHeight = window.innerHeight; //(document.body.clientWidth === window.screen.height ? window.screen.width : window.screen.height);
            let scrollContainers = getScrollContainers(this.selectorString);
            let keyboardHeight = 0; //e.keyboardHeight;

            if (this.showElementUnderKeyboard) {
                showElementUnderKeyboard(keyboardHeight);
            }

            // for each scroll container in the DOM, we need to calculate the
            // the height it should be to fit in the reduced view
            scrollContainers.forEach((sc) => {
                let scTop = sc.getBoundingClientRect().top;
                // now that we know the top of the scroll container, the height of the
                // the screen, and the height of the keyboard, we can calculate the
                // appropriate max-height for the container.
                let maxHeight = "" + (screenHeight - keyboardHeight - scTop);
                console.log("New height", maxHeight);
                if (maxHeight > 100) {
                    sc.style.maxHeight = maxHeight + "px";
                }
            });
            this.emit("didResize", e);
            // changing the height isn't sufficient: we need to scroll any focused
            // element into view.
            setTimeout(() => {
                let focusedElement = document.querySelector(":focus");
                if (focusedElement) {
                    if (!this.useSmoothScrolling || !window.requestAnimationFrame) {
                        // scroll the element into view, but only if we have to
                        if (focusedElement.scrollIntoViewIfNeeded) {
                            focusedElement.scrollIntoViewIfNeeded();
                        } else {
                            // aim for the bottom of the viewport
                            focusedElement.scrollIntoView(false);
                        }
                        // iOS doesn't always position the cursor correctly after
                        // a scroll operation. Clear the selection so that iOS is
                        // forced to recompute where the cursor should appear.
                        handleTextSelection(focusedElement);
                    } else {
                        // to scroll the element smoothly into view, things become a little
                        // more difficult.
                        let fElTop = focusedElement.getBoundingClientRect().top,
                            sc = focusedElement, scTop, scBottom,
                            selectorString = this.selectorString;
                        // find the containing scroll container if we can
                        while (sc && !sc.matches(selectorString)) {
                            sc = sc.parentElement;
                        }
                        if (sc) {
                            scTop = sc.getBoundingClientRect().top;
                            scBottom = sc.getBoundingClientRect().bottom;
                            if (fElTop < scTop || fElTop > (((scBottom - scTop) / 2) + scTop)) {
                                // the element isn't above the keyboard (or is too far above),
                                // scroll it into view smoothly
                                let targetTop = ((scBottom - scTop) / 2) + scTop,
                                    deltaTop = fElTop - targetTop,
                                    origScrollTop = sc.scrollTop,
                                    startTimestamp = null;
                                // animate our scroll
                                let scrollStep;
                                window.requestAnimationFrame(scrollStep = (timestamp) => {
                                    if (!startTimestamp) {
                                        startTimestamp = timestamp;
                                    }
                                    var progressDelta = timestamp - startTimestamp,
                                        pct = progressDelta / this.smoothScrollDuration;
                                    sc.scrollTop = origScrollTop + (deltaTop * pct);
                                    if (progressDelta < this.smoothScrollDuration) {
                                        window.requestAnimationFrame(scrollStep);
                                    } else {
                                        // set the scroll to the final desired position, just in case
                                        // we didn't actually get there (or overshot)
                                        sc.scrollTop = origScrollTop + deltaTop;
                                        handleTextSelection(focusedElement);
                                    }
                                });
                            }
                        }
                    }
                }
            }, 0);
        }, 0);
    }

    /**
     * Triggered when the soft keyboard is hidden
     * @param {*} e
     */
    keyboardHidden(e)/*: void*/ {
        setTimeout(() => {
            this.emit("keyboardHidden", e);
            this.emit("willResize", e);
            let scrollContainers = getScrollContainers(this.selectorString);
            scrollContainers.forEach(el => el.style.maxHeight = "");
            hideElementUnderKeyboard();
            this.emit("didResize", e);
        }, 0);
    }
}

export function createSoftKeyboard(...args) {
    return new SoftKeyboard(...args);
}
<|endoftext|>


<commit_before>function visualiserApp(luigi) {
    var templates = {};
    var invertDependencies = false;

    function loadTemplates() {
        $("script[type='text/template']").each(function(i, element) {
            var name = $(element).attr("name");
            var content = $(element).text();
            templates[name] = content;
        });
    }

    function renderTemplate(templateName, dataObject) {
        return $("<div>").html(Mustache.render(templates[templateName], dataObject));
    }

    function formatTime(dateObject) {
        return dateObject.getHours() + ":" + dateObject.getMinutes() + ":" + dateObject.getSeconds();
    }

    function taskToDisplayTask(task) {
        var taskIdParts = /([A-Za-z0-9_]*)\((.*)\)/.exec(task.taskId);
        var taskName = taskIdParts[1];
        var taskParams = taskIdParts[2];
        var displayTime = new Date(Math.floor(task.start_time*1000)).toLocaleString();
        if (task.status == "RUNNING" && "time_running" in task) {
          displayTime += " | " + new Date(Math.floor(task.time_running*1000)).toLocaleString();
        }
        return {
            taskId: task.taskId,
            taskName: taskName,
            taskParams: taskParams,
            priority: task.priority,
            displayTime: displayTime,
            displayTimestamp : task.start_time,
            trackingUrl: task.trackingUrl,
            status: task.status,
            graph: (task.status == "PENDING" || task.status == "RUNNING" || task.status == "DONE"),
            error: task.status == "FAILED"
        };
    }

    function indexByProperty(tasks, fieldName) {
        indexedTasks = {};
        $.each(tasks, function(i, task) {
            if (!indexedTasks.hasOwnProperty(task[fieldName])) {
                indexedTasks[task[fieldName]] = [];
            }
            indexedTasks[task[fieldName]].push(task);
        });
        return indexedTasks;
    }

    function entryList(object) {
        var list = [];
        $.each(object, function(key, value) {
            list.push({key: key, value:value});
        });
        return list;
    }

    function renderTasks(tasks) {
        var displayTasks = tasks.map(taskToDisplayTask);
        displayTasks.sort(function(a,b) { return b.displayTimestamp - a.displayTimestamp; });
        var tasksByFamily = entryList(indexByProperty(displayTasks, "taskName"));
        tasksByFamily.sort(function(a,b) { return a.key.localeCompare(b.key); });
        return renderTemplate("rowTemplate", {tasks: tasksByFamily});
    }

    function switchTab(tabId) {
        $(".tabButton").parent().removeClass("active");
        $(".tab-pane").removeClass("active");
        $("#"+tabId).addClass("active");
        $(".tabButton[data-tab="+tabId+"]").parent().addClass("active");
    }

    function showErrorTrace(error) {
        $("#errorModal").empty().append(renderTemplate("errorTemplate", error));
        $("#errorModal").modal({});
    }

    function processHashChange() {
        var hash = location.hash;
        if (hash) {
            var taskId = hash.substr(1);
            $("#graphContainer").hide();
            $("#graphPlaceholder svg").empty();
            $("#searchError").empty();
            $("#searchError").removeClass();
            if (taskId != "g") {
                depGraphCallback = function(dependencyGraph) {
                    $("#graphPlaceholder svg").empty();
                    $("#searchError").empty();
                    $("#searchError").removeClass();
                    if(dependencyGraph.length > 0) {
                      $("#dependencyTitle").text(taskId);
                      $("#graphPlaceholder").get(0).graph.updateData(dependencyGraph);
                      $("#graphContainer").show();
                      bindGraphEvents();
                    } else {
                      $("#searchError").addClass("alert alert-error")
                      $("#searchError").append("Couldn't find task " + taskId)
                    }
                }
                if (invertDependencies) {
                    luigi.getInverseDependencyGraph(taskId, depGraphCallback);
                } else {
                    luigi.getDependencyGraph(taskId, depGraphCallback);
                }
            }
            switchTab("dependencyGraph");
        } else {
            switchTab("taskList");
        }
    }

    function bindGraphEvents() {
        $(".graph-node-a").click(function(event) {
            var taskId = $(this).attr("data-task-id");
            var status = $(this).attr("data-task-status");
            if (status=="FAILED") {
                event.preventDefault();
                luigi.getErrorTrace(taskId, function(error) {
                   showErrorTrace(error);
                });
            }
        });
    }

    function bindListEvents() {
        $("[data-action=expandTaskRows]").click(function(event) {
            event.preventDefault();
            var icon = $(this).find("span");
            if (icon.hasClass("icon-plus")) {
                icon.removeClass("icon-plus");
                icon.addClass("icon-minus");
            } else {
                icon.removeClass("icon-minus");
                icon.addClass("icon-plus");
            }
            var taskRows = $(this).closest(".taskFamily").find(".taskRows").slideToggle("fast");
        });
        $(window).on('hashchange', processHashChange);
        $("#invertCheckbox").click(function() {
            invertDependencies = this.checked;
            processHashChange();
        });
        $("a[href=#list]").click(function() { location.hash=""; });
        $("#loadTaskForm").submit(function(event) {
            event.preventDefault();
            location.hash = $(this).find("input").val();
        });
        $(".error-trace-button").click(function() {
            luigi.getErrorTrace($(this).attr("data-task-id"), function(error) {
               showErrorTrace(error);
            });
        });
    }

    $(document).ready(function() {
        loadTemplates();

        luigi.getFailedTaskList(function(failedTasks) {
            luigi.getUpstreamFailedTaskList(function(upstreamFailedTasks) {
                luigi.getRunningTaskList(function(runningTasks) {
                    luigi.getPendingTaskList(function(pendingTasks) {
                        luigi.getDoneTaskList(function(doneTasks) {
                            $("#failedTasks").append(renderTasks(failedTasks));
                            $("#upstreamFailedTasks").append(renderTasks(upstreamFailedTasks));
                            $("#runningTasks").append(renderTasks(runningTasks));
                            $("#pendingTasks").append(renderTasks(pendingTasks));
                            $("#doneTasks").append(renderTasks(doneTasks));
                            bindListEvents();
                        });
                    });
                });
            });
        });

        var graph = new Graph.DependencyGraph($("#graphPlaceholder")[0]);
        $("#graphPlaceholder")[0].graph = graph;
        processHashChange();
    });
}
<commit_msg>show running worker in luigi dashbaord<commit_after>function visualiserApp(luigi) {
    var templates = {};
    var invertDependencies = false;

    function loadTemplates() {
        $("script[type='text/template']").each(function(i, element) {
            var name = $(element).attr("name");
            var content = $(element).text();
            templates[name] = content;
        });
    }

    function renderTemplate(templateName, dataObject) {
        return $("<div>").html(Mustache.render(templates[templateName], dataObject));
    }

    function formatTime(dateObject) {
        return dateObject.getHours() + ":" + dateObject.getMinutes() + ":" + dateObject.getSeconds();
    }

    function taskToDisplayTask(task) {
        var taskIdParts = /([A-Za-z0-9_]*)\((.*)\)/.exec(task.taskId);
        var taskName = taskIdParts[1];
        var taskParams = taskIdParts[2];
        var displayTime = new Date(Math.floor(task.start_time*1000)).toLocaleString();
        if (task.status == "RUNNING" && "time_running" in task) {
          displayTime += " | " + new Date(Math.floor(task.time_running*1000)).toLocaleString();
          if ("worker_running" in task) {
            displayTime += " (" + task.worker_running + ")"
          }
        }
        return {
            taskId: task.taskId,
            taskName: taskName,
            taskParams: taskParams,
            priority: task.priority,
            displayTime: displayTime,
            displayTimestamp : task.start_time,
            trackingUrl: task.trackingUrl,
            status: task.status,
            graph: (task.status == "PENDING" || task.status == "RUNNING" || task.status == "DONE"),
            error: task.status == "FAILED"
        };
    }

    function indexByProperty(tasks, fieldName) {
        indexedTasks = {};
        $.each(tasks, function(i, task) {
            if (!indexedTasks.hasOwnProperty(task[fieldName])) {
                indexedTasks[task[fieldName]] = [];
            }
            indexedTasks[task[fieldName]].push(task);
        });
        return indexedTasks;
    }

    function entryList(object) {
        var list = [];
        $.each(object, function(key, value) {
            list.push({key: key, value:value});
        });
        return list;
    }

    function renderTasks(tasks) {
        var displayTasks = tasks.map(taskToDisplayTask);
        displayTasks.sort(function(a,b) { return b.displayTimestamp - a.displayTimestamp; });
        var tasksByFamily = entryList(indexByProperty(displayTasks, "taskName"));
        tasksByFamily.sort(function(a,b) { return a.key.localeCompare(b.key); });
        return renderTemplate("rowTemplate", {tasks: tasksByFamily});
    }

    function switchTab(tabId) {
        $(".tabButton").parent().removeClass("active");
        $(".tab-pane").removeClass("active");
        $("#"+tabId).addClass("active");
        $(".tabButton[data-tab="+tabId+"]").parent().addClass("active");
    }

    function showErrorTrace(error) {
        $("#errorModal").empty().append(renderTemplate("errorTemplate", error));
        $("#errorModal").modal({});
    }

    function processHashChange() {
        var hash = location.hash;
        if (hash) {
            var taskId = hash.substr(1);
            $("#graphContainer").hide();
            $("#graphPlaceholder svg").empty();
            $("#searchError").empty();
            $("#searchError").removeClass();
            if (taskId != "g") {
                depGraphCallback = function(dependencyGraph) {
                    $("#graphPlaceholder svg").empty();
                    $("#searchError").empty();
                    $("#searchError").removeClass();
                    if(dependencyGraph.length > 0) {
                      $("#dependencyTitle").text(taskId);
                      $("#graphPlaceholder").get(0).graph.updateData(dependencyGraph);
                      $("#graphContainer").show();
                      bindGraphEvents();
                    } else {
                      $("#searchError").addClass("alert alert-error")
                      $("#searchError").append("Couldn't find task " + taskId)
                    }
                }
                if (invertDependencies) {
                    luigi.getInverseDependencyGraph(taskId, depGraphCallback);
                } else {
                    luigi.getDependencyGraph(taskId, depGraphCallback);
                }
            }
            switchTab("dependencyGraph");
        } else {
            switchTab("taskList");
        }
    }

    function bindGraphEvents() {
        $(".graph-node-a").click(function(event) {
            var taskId = $(this).attr("data-task-id");
            var status = $(this).attr("data-task-status");
            if (status=="FAILED") {
                event.preventDefault();
                luigi.getErrorTrace(taskId, function(error) {
                   showErrorTrace(error);
                });
            }
        });
    }

    function bindListEvents() {
        $("[data-action=expandTaskRows]").click(function(event) {
            event.preventDefault();
            var icon = $(this).find("span");
            if (icon.hasClass("icon-plus")) {
                icon.removeClass("icon-plus");
                icon.addClass("icon-minus");
            } else {
                icon.removeClass("icon-minus");
                icon.addClass("icon-plus");
            }
            var taskRows = $(this).closest(".taskFamily").find(".taskRows").slideToggle("fast");
        });
        $(window).on('hashchange', processHashChange);
        $("#invertCheckbox").click(function() {
            invertDependencies = this.checked;
            processHashChange();
        });
        $("a[href=#list]").click(function() { location.hash=""; });
        $("#loadTaskForm").submit(function(event) {
            event.preventDefault();
            location.hash = $(this).find("input").val();
        });
        $(".error-trace-button").click(function() {
            luigi.getErrorTrace($(this).attr("data-task-id"), function(error) {
               showErrorTrace(error);
            });
        });
    }

    $(document).ready(function() {
        loadTemplates();

        luigi.getFailedTaskList(function(failedTasks) {
            luigi.getUpstreamFailedTaskList(function(upstreamFailedTasks) {
                luigi.getRunningTaskList(function(runningTasks) {
                    luigi.getPendingTaskList(function(pendingTasks) {
                        luigi.getDoneTaskList(function(doneTasks) {
                            $("#failedTasks").append(renderTasks(failedTasks));
                            $("#upstreamFailedTasks").append(renderTasks(upstreamFailedTasks));
                            $("#runningTasks").append(renderTasks(runningTasks));
                            $("#pendingTasks").append(renderTasks(pendingTasks));
                            $("#doneTasks").append(renderTasks(doneTasks));
                            bindListEvents();
                        });
                    });
                });
            });
        });

        var graph = new Graph.DependencyGraph($("#graphPlaceholder")[0]);
        $("#graphPlaceholder")[0].graph = graph;
        processHashChange();
    });
}
<|endoftext|>


<commit_before>$.widget('boom.justifyAssets', {
	$el : null,
	targetRightOffset : null,
	windowWidth : null,

	_bind : function() {
		var self = this;

		$(window).resize(function() {
			self._setDimensions();
			self._justify();
		});
	},

	_create : function() {
		this.$el = $(this.element);

		this._bind();

		this._setDimensions();
		this.justify();
	},

	_getOffset : function($el) {
		var offset = $el.offset();
		offset.right = this.windowWidth - (offset.left + $el.outerWidth());

		return offset;
	},

	justify : function() {
		var currentRow = new Row();
		var prevRow;
		var self = this;

		this.$el.children().each(function(index, element) {
			var $child = $(element);
			$child.offset = self._getOffset($child);

			if ( ! $child.css('height') || ! $child.attr('data-aspect-ratio')) {
				$child.remove();
				return true;
			}

			prevRow = jQuery.extend({}, currentRow);
			currentRow.addElementToRow($child);

			if (currentRow.isAtStart() && ! prevRow.isAtStart()) {
				prevRow.expandTo(self.targetRightOffset);
			}

		});

		var lastRowGap = currentRow.determineGap(this.targetRightOffset);

		if (lastRowGap <= (this.$el.outerWidth() * 0.75)) {
			currentRow.expandTo(self.targetRightOffset);
		} else {
			prevRow.merge(currentRow);
		}
	},

	_setDimensions : function() {
		this.windowWidth = $(window).width();
		this.targetRightOffset = (this.windowWidth - (this.$el.offset().left + this.$el.outerWidth()));
	}
});

function Row() {
	this.elements = [];
	this.aspectRatioSum = 0;

	Row.prototype.addElementToRow = function($el) {
		if (this._elementStartsRow($el)) {
			this.elements = [];
			this.aspectRatioSum = 0;
		}

		this.elements.push($el);
		this.aspectRatioSum += parseFloat($el.attr('data-aspect-ratio'));

		return this;
	};

	Row.prototype.determineGap = function(offset) {
		return this.elements[this.elements.length - 1].offset.right - offset;
	};

	Row.prototype.expandTo = function(offset) {
		var endOfRowGap = this.determineGap(offset);

		if (endOfRowGap > 0) {
			var increaseBy = Math.floor(endOfRowGap / this.aspectRatioSum);

			if (increaseBy <= endOfRowGap) {
				$.each(this.elements, function(index, $el) {
					$el
						.height('+=' + increaseBy)
						.width('+=' + Math.floor(increaseBy * $el.attr('data-aspect-ratio')));
				});
			}
		}
	};

	Row.prototype.shrinkBy = function(size) {
		var perElement = Math.ceil(size / this.elements.length);

		$.each(this.elements, function(index, $el) {
			$el
				.height('-=' + Math.floor(perElement / $el.attr('data-aspect-ratio')))
				.width('-=' + perElement);
		});
	};

	Row.prototype.getWidth = function() {
		var width = 0;

		$.each(this.elements, function(index, $el) {
			width += $el.outerWidth();
		});

		return width;
	};

	Row.prototype.isAtStart = function() {
		return this.elements.length <= 1;
	};

	Row.prototype.merge = function(row) {
		var width = row.getWidth();
		this.shrinkBy(width);

		var heightDif = this.elements[0].height() - row.elements[0].height();

		$.each(row.elements, function(index, $el) {
			$el.height('+=' + heightDif);
		});
	};

	Row.prototype._elementStartsRow = function($el) {
		if ( ! this.elements.length) {
			return true;
		}

		return ($el.offset.top >= (this.elements[this.elements.length - 1].offset.top + $el.height()));
	};
}
<commit_msg>Bugfix<commit_after>$.widget('boom.justifyAssets', {
	$el : null,
	targetRightOffset : null,
	windowWidth : null,

	_bind : function() {
		var self = this;

		$(window).resize(function() {
			self._setDimensions();
			self.justify();
		});
	},

	_create : function() {
		this.$el = $(this.element);

		this._bind();

		this._setDimensions();
		this.justify();
	},

	_getOffset : function($el) {
		var offset = $el.offset();
		offset.right = this.windowWidth - (offset.left + $el.outerWidth());

		return offset;
	},

	justify : function() {
		var currentRow = new Row();
		var prevRow;
		var self = this;

		this.$el.children().each(function(index, element) {
			var $child = $(element);
			$child.offset = self._getOffset($child);

			if ( ! $child.css('height') || ! $child.attr('data-aspect-ratio')) {
				$child.remove();
				return true;
			}

			prevRow = jQuery.extend({}, currentRow);
			currentRow.addElementToRow($child);

			if (currentRow.isAtStart() && ! prevRow.isAtStart()) {
				prevRow.expandTo(self.targetRightOffset);
			}

		});

		var lastRowGap = currentRow.determineGap(this.targetRightOffset);

		if (lastRowGap <= (this.$el.outerWidth() * 0.75)) {
			currentRow.expandTo(self.targetRightOffset);
		} else {
			prevRow.merge(currentRow);
		}
	},

	_setDimensions : function() {
		this.windowWidth = $(window).width();
		this.targetRightOffset = (this.windowWidth - (this.$el.offset().left + this.$el.outerWidth()));
	}
});

function Row() {
	this.elements = [];
	this.aspectRatioSum = 0;

	Row.prototype.addElementToRow = function($el) {
		if (this._elementStartsRow($el)) {
			this.elements = [];
			this.aspectRatioSum = 0;
		}

		this.elements.push($el);
		this.aspectRatioSum += parseFloat($el.attr('data-aspect-ratio'));

		return this;
	};

	Row.prototype.determineGap = function(offset) {
		return this.elements[this.elements.length - 1].offset.right - offset;
	};

	Row.prototype.expandTo = function(offset) {
		var endOfRowGap = this.determineGap(offset);

		if (endOfRowGap > 0) {
			var increaseBy = Math.floor(endOfRowGap / this.aspectRatioSum);

			if (increaseBy <= endOfRowGap) {
				$.each(this.elements, function(index, $el) {
					$el
						.height('+=' + increaseBy)
						.width('+=' + Math.floor(increaseBy * $el.attr('data-aspect-ratio')));
				});
			}
		}
	};

	Row.prototype.shrinkBy = function(size) {
		var perElement = Math.ceil(size / this.elements.length);

		$.each(this.elements, function(index, $el) {
			$el
				.height('-=' + Math.floor(perElement / $el.attr('data-aspect-ratio')))
				.width('-=' + perElement);
		});
	};

	Row.prototype.getWidth = function() {
		var width = 0;

		$.each(this.elements, function(index, $el) {
			width += $el.outerWidth();
		});

		return width;
	};

	Row.prototype.isAtStart = function() {
		return this.elements.length <= 1;
	};

	Row.prototype.merge = function(row) {
		var width = row.getWidth();
		this.shrinkBy(width);

		var heightDif = this.elements[0].height() - row.elements[0].height();

		$.each(row.elements, function(index, $el) {
			$el.height('+=' + heightDif);
		});
	};

	Row.prototype._elementStartsRow = function($el) {
		if ( ! this.elements.length) {
			return true;
		}

		return ($el.offset.top >= (this.elements[this.elements.length - 1].offset.top + $el.height()));
	};
}<|endoftext|>