<commit_before>/*
Copyright 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
{
    action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
    room_id: $ROOM_ID,
    user_id: $USER_ID
    // additional request fields
}

The complete request object is returned to the caller with an additional "response" key like so:
{
    action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
    room_id: $ROOM_ID,
    user_id: $USER_ID,
    // additional request fields
    response: { ... }
}

The "action" determines the format of the request and response. All actions can return an error response.
An error response is a "response" object which consists of a sole "error" key to indicate an error.
They look like:
{
    error: {
        message: "Unable to invite user into room.",
        _error: <Original Error Object>
    }
}
The "message" key should be a human-friendly string.

ACTIONS
=======
All actions can return an error response instead of the response outlined below.

invite
------
Invites a user into a room.

Request:
 - room_id is the room to invite the user into.
 - user_id is the user ID to invite.
 - No additional fields.
Response:
{
    success: true
}
Example:
{
    action: "invite",
    room_id: "!foo:bar",
    user_id: "@invitee:bar",
    response: {
        success: true
    }
}

set_bot_options
---------------
Set the m.room.bot.options state event for a bot user.

Request:
 - room_id is the room to send the state event into.
 - user_id is the user ID of the bot who you're setting options for.
 - "content" is an object consisting of the content you wish to set.
Response:
{
    success: true
}
Example:
{
    action: "set_bot_options",
    room_id: "!foo:bar",
    user_id: "@bot:bar",
    content: {
        default_option: "alpha"
    },
    response: {
        success: true
    }
}

membership_state AND bot_options
--------------------------------
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.

NB: Whilst this API is basically equivalent to getStateEvent, we specifically do not
    want external entities to be able to query any state event for any room, hence the
    restrictive API outlined here.

Request:
 - room_id is the room which has the state event.
 - user_id is the state_key parameter which in both cases is a user ID (the member or the bot).
 - No additional fields.
Response:
 - The event content. If there is no state event, the "response" key should be null.
Example:
{
    action: "membership_state",
    room_id: "!foo:bar",
    user_id: "@somemember:bar",
    response: {
        membership: "join",
        displayname: "Bob",
        avatar_url: null
    }
}
*/

const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher");

function sendResponse(event, res) {
    const data = JSON.parse(JSON.stringify(event.data));
    data.response = res;
    event.source.postMessage(data, event.origin);
}

function sendError(event, msg, nestedError) {
    console.error("Action:" + event.data.action + " failed with message: " + msg);
    const data = JSON.parse(JSON.stringify(event.data));
    data.response = {
        error: {
            message: msg,
        },
    };
    if (nestedError) {
        data.response.error._error = nestedError;
    }
    event.source.postMessage(data, event.origin);
}

function inviteUser(event, roomId, userId) {
    console.log(`Received request to invite ${userId} into room ${roomId}`);
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }
    const room = client.getRoom(roomId);
    if (room) {
        // if they are already invited we can resolve immediately.
        const member = room.getMember(userId);
        if (member && member.membership === "invite") {
            sendResponse(event, {
                success: true,
            });
            return;
        }
    }

    client.invite(roomId, userId).done(function() {
        sendResponse(event, {
            success: true,
        });
    }, function(err) {
        sendError(event, "You need to be able to invite users to do that.", err);
    });
}

function setBotOptions(event, roomId, userId) {
    console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }
    client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
        sendResponse(event, {
            success: true,
        });
    }, (err) => {
        sendError(event, err.message ? err.message : "Failed to send request.", err);
    });
}

function setBotPower(event, roomId, userId, level) {
    if (!(Number.isInteger(level) && level >= 0)) {
        sendError(event, "Power level must be positive integer.");
        return;
    }

    console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }

    client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
        let powerEvent = new MatrixEvent(
            {
                type: "m.room.power_levels",
                content: powerLevels,
            }
        );

        client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
            sendResponse(event, {
                success: true,
            });
        }, (err) => {
            sendError(event, err.message ? err.message : "Failed to send request.", err);
        });
    });
}

function getMembershipState(event, roomId, userId) {
    console.log(`membership_state of ${userId} in room ${roomId} requested.`);
    returnStateEvent(event, roomId, "m.room.member", userId);
}

function getJoinRules(event, roomId) {
    console.log(`join_rules of ${roomId} requested.`);
    returnStateEvent(event, roomId, "m.room.join_rules", "");
}

function botOptions(event, roomId, userId) {
    console.log(`bot_options of ${userId} in room ${roomId} requested.`);
    returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
}

function returnStateEvent(event, roomId, eventType, stateKey) {
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }
    const room = client.getRoom(roomId);
    if (!room) {
        sendError(event, "This room is not recognised.");
        return;
    }
    const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
    if (!stateEvent) {
        sendResponse(event, null);
        return;
    }
    sendResponse(event, stateEvent.getContent());
}

var currentRoomId = null;
var currentRoomAlias = null;

// Listen for when a room is viewed
dis.register(onAction);
function onAction(payload) {
    if (payload.action !== "view_room") {
        return;
    }
    currentRoomId = payload.room_id;
    currentRoomAlias = payload.room_alias;
}

const onMessage = function(event) {
    if (!event.origin) { // stupid chrome
        event.origin = event.originalEvent.origin;
    }

    // check it is from the integrations UI URL (remove trailing spaces)
    let url = SdkConfig.get().integrations_ui_url;
    if (url.endsWith("/")) {
        url = url.substr(0, url.length - 1);
    }
    if (url !== event.origin) {
        console.warn("Unauthorised postMessage received. Source URL: " + event.origin);
        return;
    }

    const roomId = event.data.room_id;
    const userId = event.data.user_id;
    if (!roomId) {
        sendError(event, "Missing room_id in request");
        return;
    }
    let promise = Promise.resolve(currentRoomId);
    if (!currentRoomId) {
        if (!currentRoomAlias) {
            sendError(event, "Must be viewing a room");
        }
        // no room ID but there is an alias, look it up.
        console.log("Looking up alias " + currentRoomAlias);
        promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
            return res.room_id;
        });
    }

    promise.then((viewingRoomId) => {
        if (roomId !== viewingRoomId) {
            sendError(event, "Room " + roomId + " not visible");
            return;
        }

        // Getting join rules does not require userId
        if (event.data.action === "join_rules_state") {
            getJoinRules(event, roomId);
            return;
        }

        if (!userId) {
            sendError(event, "Missing user_id in request");
            return;
        }
        switch (event.data.action) {
            case "membership_state":
                getMembershipState(event, roomId, userId);
                break;
            case "invite":
                inviteUser(event, roomId, userId);
                break;
            case "bot_options":
                botOptions(event, roomId, userId);
                break;
            case "set_bot_options":
                setBotOptions(event, roomId, userId);
                break;
            case "set_bot_power":
                setBotPower(event, roomId, userId, event.data.level);
                break;
            default:
                console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
                break;
        }
    }, (err) => {
        console.error(err);
        sendError(event, "Failed to lookup current room.");
    })
};

module.exports = {
    startListening: function() {
        window.addEventListener("message", onMessage, false);
    },

    stopListening: function() {
        window.removeEventListener("message", onMessage);
    },
};
<commit_msg>Return after sending an error<commit_after>/*
Copyright 2016 OpenMarket Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

/*
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
{
    action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
    room_id: $ROOM_ID,
    user_id: $USER_ID
    // additional request fields
}

The complete request object is returned to the caller with an additional "response" key like so:
{
    action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
    room_id: $ROOM_ID,
    user_id: $USER_ID,
    // additional request fields
    response: { ... }
}

The "action" determines the format of the request and response. All actions can return an error response.
An error response is a "response" object which consists of a sole "error" key to indicate an error.
They look like:
{
    error: {
        message: "Unable to invite user into room.",
        _error: <Original Error Object>
    }
}
The "message" key should be a human-friendly string.

ACTIONS
=======
All actions can return an error response instead of the response outlined below.

invite
------
Invites a user into a room.

Request:
 - room_id is the room to invite the user into.
 - user_id is the user ID to invite.
 - No additional fields.
Response:
{
    success: true
}
Example:
{
    action: "invite",
    room_id: "!foo:bar",
    user_id: "@invitee:bar",
    response: {
        success: true
    }
}

set_bot_options
---------------
Set the m.room.bot.options state event for a bot user.

Request:
 - room_id is the room to send the state event into.
 - user_id is the user ID of the bot who you're setting options for.
 - "content" is an object consisting of the content you wish to set.
Response:
{
    success: true
}
Example:
{
    action: "set_bot_options",
    room_id: "!foo:bar",
    user_id: "@bot:bar",
    content: {
        default_option: "alpha"
    },
    response: {
        success: true
    }
}

membership_state AND bot_options
--------------------------------
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.

NB: Whilst this API is basically equivalent to getStateEvent, we specifically do not
    want external entities to be able to query any state event for any room, hence the
    restrictive API outlined here.

Request:
 - room_id is the room which has the state event.
 - user_id is the state_key parameter which in both cases is a user ID (the member or the bot).
 - No additional fields.
Response:
 - The event content. If there is no state event, the "response" key should be null.
Example:
{
    action: "membership_state",
    room_id: "!foo:bar",
    user_id: "@somemember:bar",
    response: {
        membership: "join",
        displayname: "Bob",
        avatar_url: null
    }
}
*/

const SdkConfig = require('./SdkConfig');
const MatrixClientPeg = require("./MatrixClientPeg");
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
const dis = require("./dispatcher");

function sendResponse(event, res) {
    const data = JSON.parse(JSON.stringify(event.data));
    data.response = res;
    event.source.postMessage(data, event.origin);
}

function sendError(event, msg, nestedError) {
    console.error("Action:" + event.data.action + " failed with message: " + msg);
    const data = JSON.parse(JSON.stringify(event.data));
    data.response = {
        error: {
            message: msg,
        },
    };
    if (nestedError) {
        data.response.error._error = nestedError;
    }
    event.source.postMessage(data, event.origin);
}

function inviteUser(event, roomId, userId) {
    console.log(`Received request to invite ${userId} into room ${roomId}`);
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }
    const room = client.getRoom(roomId);
    if (room) {
        // if they are already invited we can resolve immediately.
        const member = room.getMember(userId);
        if (member && member.membership === "invite") {
            sendResponse(event, {
                success: true,
            });
            return;
        }
    }

    client.invite(roomId, userId).done(function() {
        sendResponse(event, {
            success: true,
        });
    }, function(err) {
        sendError(event, "You need to be able to invite users to do that.", err);
    });
}

function setBotOptions(event, roomId, userId) {
    console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }
    client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
        sendResponse(event, {
            success: true,
        });
    }, (err) => {
        sendError(event, err.message ? err.message : "Failed to send request.", err);
    });
}

function setBotPower(event, roomId, userId, level) {
    if (!(Number.isInteger(level) && level >= 0)) {
        sendError(event, "Power level must be positive integer.");
        return;
    }

    console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }

    client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
        let powerEvent = new MatrixEvent(
            {
                type: "m.room.power_levels",
                content: powerLevels,
            }
        );

        client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
            sendResponse(event, {
                success: true,
            });
        }, (err) => {
            sendError(event, err.message ? err.message : "Failed to send request.", err);
        });
    });
}

function getMembershipState(event, roomId, userId) {
    console.log(`membership_state of ${userId} in room ${roomId} requested.`);
    returnStateEvent(event, roomId, "m.room.member", userId);
}

function getJoinRules(event, roomId) {
    console.log(`join_rules of ${roomId} requested.`);
    returnStateEvent(event, roomId, "m.room.join_rules", "");
}

function botOptions(event, roomId, userId) {
    console.log(`bot_options of ${userId} in room ${roomId} requested.`);
    returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
}

function returnStateEvent(event, roomId, eventType, stateKey) {
    const client = MatrixClientPeg.get();
    if (!client) {
        sendError(event, "You need to be logged in.");
        return;
    }
    const room = client.getRoom(roomId);
    if (!room) {
        sendError(event, "This room is not recognised.");
        return;
    }
    const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
    if (!stateEvent) {
        sendResponse(event, null);
        return;
    }
    sendResponse(event, stateEvent.getContent());
}

var currentRoomId = null;
var currentRoomAlias = null;

// Listen for when a room is viewed
dis.register(onAction);
function onAction(payload) {
    if (payload.action !== "view_room") {
        return;
    }
    currentRoomId = payload.room_id;
    currentRoomAlias = payload.room_alias;
}

const onMessage = function(event) {
    if (!event.origin) { // stupid chrome
        event.origin = event.originalEvent.origin;
    }

    // check it is from the integrations UI URL (remove trailing spaces)
    let url = SdkConfig.get().integrations_ui_url;
    if (url.endsWith("/")) {
        url = url.substr(0, url.length - 1);
    }
    if (url !== event.origin) {
        console.warn("Unauthorised postMessage received. Source URL: " + event.origin);
        return;
    }

    const roomId = event.data.room_id;
    const userId = event.data.user_id;
    if (!roomId) {
        sendError(event, "Missing room_id in request");
        return;
    }
    let promise = Promise.resolve(currentRoomId);
    if (!currentRoomId) {
        if (!currentRoomAlias) {
            sendError(event, "Must be viewing a room");
            return;
        }
        // no room ID but there is an alias, look it up.
        console.log("Looking up alias " + currentRoomAlias);
        promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
            return res.room_id;
        });
    }

    promise.then((viewingRoomId) => {
        if (roomId !== viewingRoomId) {
            sendError(event, "Room " + roomId + " not visible");
            return;
        }

        // Getting join rules does not require userId
        if (event.data.action === "join_rules_state") {
            getJoinRules(event, roomId);
            return;
        }

        if (!userId) {
            sendError(event, "Missing user_id in request");
            return;
        }
        switch (event.data.action) {
            case "membership_state":
                getMembershipState(event, roomId, userId);
                break;
            case "invite":
                inviteUser(event, roomId, userId);
                break;
            case "bot_options":
                botOptions(event, roomId, userId);
                break;
            case "set_bot_options":
                setBotOptions(event, roomId, userId);
                break;
            case "set_bot_power":
                setBotPower(event, roomId, userId, event.data.level);
                break;
            default:
                console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
                break;
        }
    }, (err) => {
        console.error(err);
        sendError(event, "Failed to lookup current room.");
    })
};

module.exports = {
    startListening: function() {
        window.addEventListener("message", onMessage, false);
    },

    stopListening: function() {
        window.removeEventListener("message", onMessage);
    },
};
<|endoftext|>


<commit_before>/**
 * TODO:
 * - [ ] Continuously update aggregate subRow groups after initial aggregation
 * - [ ] Continuously update subrow data cell renderings
 *
 * MAYBEDO:
 * - [ ] Move some of expander lifecycle up to base class
 */
Ext.define('Jarvus.aggregrid.RollupAggregrid', {
    extend: 'Jarvus.aggregrid.Aggregrid',
    xtype: 'jarvus-rollupaggregrid',

    config: {
        subRowsStore: null,
        subDataStore: null,

        parentRowMapper: 'parent_row_id',
        subRowMapper: 'sub_row_id',

        subRowHeaderField: 'title',
        subRowHeaderTpl: false,

        subCellTpl: '{records.length}',
        subCellRenderer: false,

        expandable: true,
        expanderHeadersTpl: [
            '
',
                '
',
                    '<tpl for="subRows">',
                        '
',
                            '',
                                '',
                                    '{% values.rowHeaderTpl.applyOut(values, out, parent) %}',
                                '',
                            '	',
                        '
',
                    '</tpl>',
                '
',
            '
'
        ],
        expanderBodyTpl: [
            '
',
                '
',
                    '<tpl for="subRows">',
                        '
',
                            '<tpl for="columns">',
                                '{text}	',
                            '</tpl>',
                        '
',
                    '</tpl>',
                '
',
            '
'
        ]
    },

    // config handlers
    applySubRowsStore: function(store) {
        return Ext.StoreMgr.lookup(store);
    },

    // updateSubRowsStore: function(store, oldStore) {
    //     var me = this;

    //     if (oldStore) {
    //         oldStore.un('datachanged', 'refresh', me);
    //     }

    //     if (store) {
    //         me.refresh();
    //         store.on('datachanged', 'refresh', me);
    //     }
    // },

    applySubDataStore: function(store) {
        return Ext.StoreMgr.lookup(store);
    },

    // updateSubDataStore: function(store, oldStore) {
    //     var me = this,
    //         listeners = {
    //             scope: me,
    //             load: 'onDataStoreLoad',
    //             add: 'onDataStoreAdd',
    //             remove: 'onDataStoreRemove',
    //             update: 'onDataStoreUpdate'
    //         };

    //     if (oldStore) {
    //         oldStore.un(listeners);
    //     }

    //     if (store) {
    //         store.on(listeners);
    //     }
    // },

    applySubRowHeaderTpl: function(tpl) {
        if (!tpl) {
            tpl = new Ext.XTemplate(
                '{[typeof values === "string" ? values : values["' + this.getSubRowHeaderField() + '"]]}'
            );
        } else if (!tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    applyParentRowMapper: function(mapper) {
        if (!Ext.isString(mapper)) {
            return mapper;
        }

        return function(subRowRecord, rowsStore) {
            return rowsStore.getById(subRowRecord.get(mapper));
        };
    },

    applySubRowMapper: function(mapper) {
        if (!Ext.isString(mapper)) {
            return mapper;
        }

        return function(dataRecord, subRowsStore) {
            return subRowsStore.getById(dataRecord.get(mapper));
        };
    },

    applySubCellTpl: function(tpl) {
        if (tpl && !tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    applyExpanderHeadersTpl: function(tpl) {
        if (tpl && !tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    applyExpanderBodyTpl: function(tpl) {
        if (tpl && !tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    // event handlers

    // component methods
    doRefresh: function(me) {
        var rowsSubMetadata = me.rowsSubMetadata = {},
            rowsStore = me.getRowsStore(),
            rowsCount = rowsStore.getCount(),
            rowIndex = 0, row, rowId;

        me.callParent(arguments);

        // initialize subrows data structure
        for (; rowIndex < rowsCount; rowIndex++) {
            row = rowsStore.getAt(rowIndex);
            rowId = row.getId();

            rowsSubMetadata[rowId] = {
                row: row,
                rowId: rowId,
                subRows: [],
                groups: {}
            };
        }
    },

    doExpand: function(me, rowId) {
        console.info('%s.doExpand(%o)', this.getId(), rowId);

        var rowSubMetadata = me.rowsSubMetadata[rowId],
            expanderTplData = me.buildExpanderTplData(rowId);

        if (!rowSubMetadata.cellsRendered) {
            rowSubMetadata.headersEl = me.getExpanderHeadersTpl().overwrite(me.rowHeaderExpanderEls[rowId], expanderTplData, true);
            rowSubMetadata.bodyEl = me.getExpanderBodyTpl().overwrite(me.rowExpanderEls[rowId], expanderTplData, true);

            me.afterExpanderRefresh(rowId);

            me.aggregateSubRows(rowId);

            me.fireEventedAction('rendersubcells', [me, rowId], 'doRenderSubCells', me);
        }

        me.callParent(arguments);
    },

    buildExpanderTplData: function(rowId) {
        var me = this,
            rowHeaderTpl = me.getSubRowHeaderTpl() || me.getRowHeaderTpl(),

            columnsStore = me.getColumnsStore(),
            columnsCount = columnsStore.getCount(),
            columnIndex = 0,

            subRows = me.getSubRows(rowId),
            subRowsCount = subRows.length,
            subRowIndex = 0,

            data = {},
            columnsData = data.columns = [],
            subRowsData = data.subRows = [];

        // generate columns and rows render data
        for (; columnIndex < columnsCount; columnIndex++) {
            columnsData.push(columnsStore.getAt(columnIndex).getData());
        }

        for (; subRowIndex < subRowsCount; subRowIndex++) {
            subRowsData.push(Ext.apply({
                rowHeaderTpl: rowHeaderTpl,
                columns: columnsData
            }, subRows[subRowIndex].getData()));
        }

        return data;
    },

    groupSubRows: function() {
        if (this.subRowParents) {
            return;
        }

        var me = this,
            rowsSubMetadata = me.rowsSubMetadata,
            subRowParents = me.subRowParents = {},

            rowsStore = me.getRowsStore(),
            subRowsStore = me.getSubRowsStore(),
            parentRowMapper = me.getParentRowMapper(),
            subRowsCount = subRowsStore.getCount(),
            subRowIndex = 0, subRow, parentRow;

        for (; subRowIndex < subRowsCount; subRowIndex++) {
            subRow = subRowsStore.getAt(subRowIndex);
            parentRow = parentRowMapper(subRow, rowsStore);

            subRowParents[subRow.getId()] = parentRow;
            rowsSubMetadata[parentRow.getId()].subRows.push(subRow);
        }
    },

    getSubRows: function(rowId) {
        this.groupSubRows();
        return this.rowsSubMetadata[rowId].subRows;
    },

    getParentRow: function(subRowId) {
        this.groupSubRows();
        return this.subRowParents[subRowId];
    },

    afterExpanderRefresh: function(rowId) {
        console.info('%s.afterExpanderRefresh(%o)', this.getId(), rowId);

        var me = this,
            rowSubMetadata = me.rowsSubMetadata[rowId],
            headersEl = rowSubMetadata.headersEl,
            bodyEl = rowSubMetadata.bodyEl,
            groups = rowSubMetadata.groups,

            subRowEls = rowSubMetadata.subRowEls = {},
            subRowHeaderEls = rowSubMetadata.subRowHeaderEls = {},

            subRows = rowSubMetadata.subRows,
            subRowsCount = subRows.length,
            subRowIndex = 0, subRow, subRowId, subRowGroups, subRowEl,

            columnsStore = me.getColumnsStore(),
            columnsCount = columnsStore.getCount(),
            columnIndex, column, columnId,
            group;

        // READ phase: query dom to collect references to key elements
        for (; subRowIndex < subRowsCount; subRowIndex++) {
            subRow = subRows[subRowIndex];
            subRowId = subRow.getId();
            subRowGroups = groups[subRowId] || (groups[subRowId] = {});

            subRowHeaderEls[subRowId] = headersEl.down('.jarvus-aggregrid-subrow[data-subrow-id="'+subRowId+'"]');
            subRowEl = subRowEls[subRowId] = bodyEl.down('.jarvus-aggregrid-subrow[data-subrow-id="'+subRowId+'"]');

            for (columnIndex = 0; columnIndex < columnsCount; columnIndex++) {
                column = columnsStore.getAt(columnIndex);
                columnId = column.getId();

                group = subRowGroups[columnId] || (subRowGroups[columnId] = {});
                group.cellEl = subRowEl.down('.jarvus-aggregrid-cell[data-column-id="'+columnId+'"]');
                group.row = subRow;
                group.subRowId = subRowId;
                group.column = column;
                group.columnId = columnId;
            }
        }

        // READ->WRITE phase: sync row heights
        me.syncSubRowHeights(rowId);

        me.fireEvent('afterexpanderrefresh', me, rowId);
    },

    /**
     * @public
     * Synchronizes the heights of rows between the headers and data tables
     */
    syncSubRowHeights: function(rowId) {
        var me = this,
            rowSubMetadata = me.rowsSubMetadata[rowId],
            subRowEls = rowSubMetadata.subRowEls,
            subRowHeaderEls = rowSubMetadata.subRowHeaderEls,
            table1RowHeights = {},
            table2RowHeights = {},
            rowKey, maxHeight;

        Ext.batchLayouts(function() {
            // read all the row height in batch first for both tables
            for (rowKey in subRowHeaderEls) { // eslint-disable-line guard-for-in
                table1RowHeights[rowKey] = subRowHeaderEls[rowKey].getHeight();
                table2RowHeights[rowKey] = subRowEls[rowKey].getHeight();
            }

            // write all the max row heights
            for (rowKey in subRowHeaderEls) { // eslint-disable-line guard-for-in
                maxHeight = Math.max(table1RowHeights[rowKey], table2RowHeights[rowKey]);
                subRowHeaderEls[rowKey].select('td, th').setHeight(maxHeight);
                subRowEls[rowKey].select('td, th').setHeight(maxHeight);
            }
        });
    },

    aggregateSubRows: function(rowId) {
        var me = this,
            store = me.getSubDataStore();

        if (!store || !store.isLoaded()) {
            return;
        }

        console.info('%s.aggregateSubRows(%o)', this.getId(), rowId);

        me.fireEventedAction('aggregatesubrows', [me, rowId], 'doAggregateSubRows', me);
    },

    doAggregateSubRows: function(me) {
        if (me.subRecordsMetadata) {
            return;
        }

        var rowsSubMetadata = me.rowsSubMetadata,
            subRecordsMetadata = me.subRecordsMetadata = {},
            subDataStore = me.getSubDataStore(),
            subRowsStore = me.getSubRowsStore(),
            columnsStore = me.getColumnsStore(),
            subRowMapper = me.getSubRowMapper(),
            columnMapper = me.getColumnMapper(),
            subRecordsCount = subDataStore.getCount(),
            subRecordIndex = 0, subRecord, subRecordId, parentSubMetadata, subRecordMetadata,
            subRow, subRowId, parentRow, column, columnId, group, groupRecords;

        for (; subRecordIndex < subRecordsCount; subRecordIndex++) {
            subRecord = subDataStore.getAt(subRecordIndex);
            subRecordId = subRecord.getId();

            // get target row and column for this record
            subRow = subRowMapper(subRecord, subRowsStore);
            parentRow = subRow && me.getParentRow(subRow.getId());
            column = columnMapper(subRecord, columnsStore);

            if (!subRow || !parentRow || !column) {
                Ext.Logger.warn('Data record ' + subRecordId + ' not matched to ' + (!subRow ? 'subRow' : !parentRow ? 'parentRow' : 'column')); // eslint-disable-line no-negated-condition, no-nested-ternary
                continue;
            }

            // create metadata container for record indexed by its id
            subRecordMetadata = subRecordsMetadata[subRecordId] = {
                record: subRecord,
                subRow: subRow,
                parentRow: parentRow,
                column: column
            };

            // push record to records array for group at [rowId][columnId]
            parentSubMetadata = rowsSubMetadata[parentRow.getId()];
            group = parentSubMetadata.groups;

            subRowId = subRow.getId();
            group = group[subRowId] || (group[subRowId] = {});

            columnId = column.getId();
            group = group[columnId] || (group[columnId] = {});
            groupRecords = group.records || (group.records = []);

            subRecordMetadata.group = group;
            groupRecords.push(subRecordMetadata);
        }
    },

    doRenderSubCells: function(me, rowId) {
        console.info('%s.doRenderSubCells(%o)', this.getId(), rowId);

        var rowSubMetadata = me.rowsSubMetadata[rowId],
            groups = rowSubMetadata.groups,
            subCellTpl = me.getSubCellTpl(),
            subCellRenderer = me.getSubCellRenderer(),
            subRowId, columns, columnId, group, cellEl;

        if (!subCellTpl && !subCellRenderer) {
            return;
        }

        rowSubMetadata.cellsRendered = true;

        for (subRowId in groups) { // eslint-disable-line guard-for-in
            columns = groups[subRowId];

            for (columnId in columns) { // eslint-disable-line guard-for-in
                group = columns[columnId];
                cellEl = group.cellEl;

                if (subCellTpl) {
                    subCellTpl.overwrite(cellEl, group);
                }

                if (subCellRenderer) {
                    subCellRenderer.call(this, group, cellEl);
                }
            }
        }
    }
});<commit_msg>Add todo<commit_after>/**
 * TODO:
 * - [ ] Apply refined lifecycle from Aggregrid
 * - [ ] Continuously update aggregate subRow groups after initial aggregation
 * - [ ] Continuously update subrow data cell renderings
 *
 * MAYBEDO:
 * - [ ] Move some of expander lifecycle up to base class
 */
Ext.define('Jarvus.aggregrid.RollupAggregrid', {
    extend: 'Jarvus.aggregrid.Aggregrid',
    xtype: 'jarvus-rollupaggregrid',

    config: {
        subRowsStore: null,
        subDataStore: null,

        parentRowMapper: 'parent_row_id',
        subRowMapper: 'sub_row_id',

        subRowHeaderField: 'title',
        subRowHeaderTpl: false,

        subCellTpl: '{records.length}',
        subCellRenderer: false,

        expandable: true,
        expanderHeadersTpl: [
            '
',
                '
',
                    '<tpl for="subRows">',
                        '
',
                            '',
                                '',
                                    '{% values.rowHeaderTpl.applyOut(values, out, parent) %}',
                                '',
                            '	',
                        '
',
                    '</tpl>',
                '
',
            '
'
        ],
        expanderBodyTpl: [
            '
',
                '
',
                    '<tpl for="subRows">',
                        '
',
                            '<tpl for="columns">',
                                '{text}	',
                            '</tpl>',
                        '
',
                    '</tpl>',
                '
',
            '
'
        ]
    },

    // config handlers
    applySubRowsStore: function(store) {
        return Ext.StoreMgr.lookup(store);
    },

    // updateSubRowsStore: function(store, oldStore) {
    //     var me = this;

    //     if (oldStore) {
    //         oldStore.un('datachanged', 'refresh', me);
    //     }

    //     if (store) {
    //         me.refresh();
    //         store.on('datachanged', 'refresh', me);
    //     }
    // },

    applySubDataStore: function(store) {
        return Ext.StoreMgr.lookup(store);
    },

    // updateSubDataStore: function(store, oldStore) {
    //     var me = this,
    //         listeners = {
    //             scope: me,
    //             load: 'onDataStoreLoad',
    //             add: 'onDataStoreAdd',
    //             remove: 'onDataStoreRemove',
    //             update: 'onDataStoreUpdate'
    //         };

    //     if (oldStore) {
    //         oldStore.un(listeners);
    //     }

    //     if (store) {
    //         store.on(listeners);
    //     }
    // },

    applySubRowHeaderTpl: function(tpl) {
        if (!tpl) {
            tpl = new Ext.XTemplate(
                '{[typeof values === "string" ? values : values["' + this.getSubRowHeaderField() + '"]]}'
            );
        } else if (!tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    applyParentRowMapper: function(mapper) {
        if (!Ext.isString(mapper)) {
            return mapper;
        }

        return function(subRowRecord, rowsStore) {
            return rowsStore.getById(subRowRecord.get(mapper));
        };
    },

    applySubRowMapper: function(mapper) {
        if (!Ext.isString(mapper)) {
            return mapper;
        }

        return function(dataRecord, subRowsStore) {
            return subRowsStore.getById(dataRecord.get(mapper));
        };
    },

    applySubCellTpl: function(tpl) {
        if (tpl && !tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    applyExpanderHeadersTpl: function(tpl) {
        if (tpl && !tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    applyExpanderBodyTpl: function(tpl) {
        if (tpl && !tpl.isTemplate) {
            tpl = new Ext.XTemplate(tpl);
        }

        return tpl;
    },

    // event handlers

    // component methods
    doRefresh: function(me) {
        var rowsSubMetadata = me.rowsSubMetadata = {},
            rowsStore = me.getRowsStore(),
            rowsCount = rowsStore.getCount(),
            rowIndex = 0, row, rowId;

        me.callParent(arguments);

        // initialize subrows data structure
        for (; rowIndex < rowsCount; rowIndex++) {
            row = rowsStore.getAt(rowIndex);
            rowId = row.getId();

            rowsSubMetadata[rowId] = {
                row: row,
                rowId: rowId,
                subRows: [],
                groups: {}
            };
        }
    },

    doExpand: function(me, rowId) {
        console.info('%s.doExpand(%o)', this.getId(), rowId);

        var rowSubMetadata = me.rowsSubMetadata[rowId],
            expanderTplData = me.buildExpanderTplData(rowId);

        if (!rowSubMetadata.cellsRendered) {
            rowSubMetadata.headersEl = me.getExpanderHeadersTpl().overwrite(me.rowHeaderExpanderEls[rowId], expanderTplData, true);
            rowSubMetadata.bodyEl = me.getExpanderBodyTpl().overwrite(me.rowExpanderEls[rowId], expanderTplData, true);

            me.afterExpanderRefresh(rowId);

            me.aggregateSubRows(rowId);

            me.fireEventedAction('rendersubcells', [me, rowId], 'doRenderSubCells', me);
        }

        me.callParent(arguments);
    },

    buildExpanderTplData: function(rowId) {
        var me = this,
            rowHeaderTpl = me.getSubRowHeaderTpl() || me.getRowHeaderTpl(),

            columnsStore = me.getColumnsStore(),
            columnsCount = columnsStore.getCount(),
            columnIndex = 0,

            subRows = me.getSubRows(rowId),
            subRowsCount = subRows.length,
            subRowIndex = 0,

            data = {},
            columnsData = data.columns = [],
            subRowsData = data.subRows = [];

        // generate columns and rows render data
        for (; columnIndex < columnsCount; columnIndex++) {
            columnsData.push(columnsStore.getAt(columnIndex).getData());
        }

        for (; subRowIndex < subRowsCount; subRowIndex++) {
            subRowsData.push(Ext.apply({
                rowHeaderTpl: rowHeaderTpl,
                columns: columnsData
            }, subRows[subRowIndex].getData()));
        }

        return data;
    },

    groupSubRows: function() {
        if (this.subRowParents) {
            return;
        }

        var me = this,
            rowsSubMetadata = me.rowsSubMetadata,
            subRowParents = me.subRowParents = {},

            rowsStore = me.getRowsStore(),
            subRowsStore = me.getSubRowsStore(),
            parentRowMapper = me.getParentRowMapper(),
            subRowsCount = subRowsStore.getCount(),
            subRowIndex = 0, subRow, parentRow;

        for (; subRowIndex < subRowsCount; subRowIndex++) {
            subRow = subRowsStore.getAt(subRowIndex);
            parentRow = parentRowMapper(subRow, rowsStore);

            subRowParents[subRow.getId()] = parentRow;
            rowsSubMetadata[parentRow.getId()].subRows.push(subRow);
        }
    },

    getSubRows: function(rowId) {
        this.groupSubRows();
        return this.rowsSubMetadata[rowId].subRows;
    },

    getParentRow: function(subRowId) {
        this.groupSubRows();
        return this.subRowParents[subRowId];
    },

    afterExpanderRefresh: function(rowId) {
        console.info('%s.afterExpanderRefresh(%o)', this.getId(), rowId);

        var me = this,
            rowSubMetadata = me.rowsSubMetadata[rowId],
            headersEl = rowSubMetadata.headersEl,
            bodyEl = rowSubMetadata.bodyEl,
            groups = rowSubMetadata.groups,

            subRowEls = rowSubMetadata.subRowEls = {},
            subRowHeaderEls = rowSubMetadata.subRowHeaderEls = {},

            subRows = rowSubMetadata.subRows,
            subRowsCount = subRows.length,
            subRowIndex = 0, subRow, subRowId, subRowGroups, subRowEl,

            columnsStore = me.getColumnsStore(),
            columnsCount = columnsStore.getCount(),
            columnIndex, column, columnId,
            group;

        // READ phase: query dom to collect references to key elements
        for (; subRowIndex < subRowsCount; subRowIndex++) {
            subRow = subRows[subRowIndex];
            subRowId = subRow.getId();
            subRowGroups = groups[subRowId] || (groups[subRowId] = {});

            subRowHeaderEls[subRowId] = headersEl.down('.jarvus-aggregrid-subrow[data-subrow-id="'+subRowId+'"]');
            subRowEl = subRowEls[subRowId] = bodyEl.down('.jarvus-aggregrid-subrow[data-subrow-id="'+subRowId+'"]');

            for (columnIndex = 0; columnIndex < columnsCount; columnIndex++) {
                column = columnsStore.getAt(columnIndex);
                columnId = column.getId();

                group = subRowGroups[columnId] || (subRowGroups[columnId] = {});
                group.cellEl = subRowEl.down('.jarvus-aggregrid-cell[data-column-id="'+columnId+'"]');
                group.row = subRow;
                group.subRowId = subRowId;
                group.column = column;
                group.columnId = columnId;
            }
        }

        // READ->WRITE phase: sync row heights
        me.syncSubRowHeights(rowId);

        me.fireEvent('afterexpanderrefresh', me, rowId);
    },

    /**
     * @public
     * Synchronizes the heights of rows between the headers and data tables
     */
    syncSubRowHeights: function(rowId) {
        var me = this,
            rowSubMetadata = me.rowsSubMetadata[rowId],
            subRowEls = rowSubMetadata.subRowEls,
            subRowHeaderEls = rowSubMetadata.subRowHeaderEls,
            table1RowHeights = {},
            table2RowHeights = {},
            rowKey, maxHeight;

        Ext.batchLayouts(function() {
            // read all the row height in batch first for both tables
            for (rowKey in subRowHeaderEls) { // eslint-disable-line guard-for-in
                table1RowHeights[rowKey] = subRowHeaderEls[rowKey].getHeight();
                table2RowHeights[rowKey] = subRowEls[rowKey].getHeight();
            }

            // write all the max row heights
            for (rowKey in subRowHeaderEls) { // eslint-disable-line guard-for-in
                maxHeight = Math.max(table1RowHeights[rowKey], table2RowHeights[rowKey]);
                subRowHeaderEls[rowKey].select('td, th').setHeight(maxHeight);
                subRowEls[rowKey].select('td, th').setHeight(maxHeight);
            }
        });
    },

    aggregateSubRows: function(rowId) {
        var me = this,
            store = me.getSubDataStore();

        if (!store || !store.isLoaded()) {
            return;
        }

        console.info('%s.aggregateSubRows(%o)', this.getId(), rowId);

        me.fireEventedAction('aggregatesubrows', [me, rowId], 'doAggregateSubRows', me);
    },

    doAggregateSubRows: function(me) {
        if (me.subRecordsMetadata) {
            return;
        }

        var rowsSubMetadata = me.rowsSubMetadata,
            subRecordsMetadata = me.subRecordsMetadata = {},
            subDataStore = me.getSubDataStore(),
            subRowsStore = me.getSubRowsStore(),
            columnsStore = me.getColumnsStore(),
            subRowMapper = me.getSubRowMapper(),
            columnMapper = me.getColumnMapper(),
            subRecordsCount = subDataStore.getCount(),
            subRecordIndex = 0, subRecord, subRecordId, parentSubMetadata, subRecordMetadata,
            subRow, subRowId, parentRow, column, columnId, group, groupRecords;

        for (; subRecordIndex < subRecordsCount; subRecordIndex++) {
            subRecord = subDataStore.getAt(subRecordIndex);
            subRecordId = subRecord.getId();

            // get target row and column for this record
            subRow = subRowMapper(subRecord, subRowsStore);
            parentRow = subRow && me.getParentRow(subRow.getId());
            column = columnMapper(subRecord, columnsStore);

            if (!subRow || !parentRow || !column) {
                Ext.Logger.warn('Data record ' + subRecordId + ' not matched to ' + (!subRow ? 'subRow' : !parentRow ? 'parentRow' : 'column')); // eslint-disable-line no-negated-condition, no-nested-ternary
                continue;
            }

            // create metadata container for record indexed by its id
            subRecordMetadata = subRecordsMetadata[subRecordId] = {
                record: subRecord,
                subRow: subRow,
                parentRow: parentRow,
                column: column
            };

            // push record to records array for group at [rowId][columnId]
            parentSubMetadata = rowsSubMetadata[parentRow.getId()];
            group = parentSubMetadata.groups;

            subRowId = subRow.getId();
            group = group[subRowId] || (group[subRowId] = {});

            columnId = column.getId();
            group = group[columnId] || (group[columnId] = {});
            groupRecords = group.records || (group.records = []);

            subRecordMetadata.group = group;
            groupRecords.push(subRecordMetadata);
        }
    },

    doRenderSubCells: function(me, rowId) {
        console.info('%s.doRenderSubCells(%o)', this.getId(), rowId);

        var rowSubMetadata = me.rowsSubMetadata[rowId],
            groups = rowSubMetadata.groups,
            subCellTpl = me.getSubCellTpl(),
            subCellRenderer = me.getSubCellRenderer(),
            subRowId, columns, columnId, group, cellEl;

        if (!subCellTpl && !subCellRenderer) {
            return;
        }

        rowSubMetadata.cellsRendered = true;

        for (subRowId in groups) { // eslint-disable-line guard-for-in
            columns = groups[subRowId];

            for (columnId in columns) { // eslint-disable-line guard-for-in
                group = columns[columnId];
                cellEl = group.cellEl;

                if (subCellTpl) {
                    subCellTpl.overwrite(cellEl, group);
                }

                if (subCellRenderer) {
                    subCellRenderer.call(this, group, cellEl);
                }
            }
        }
    }
});<|endoftext|>


<commit_before>(function(root, factory) {
  if (typeof module !== 'undefined' && module.exports) {
      var d3 = require('d3');
      window.d3 = d3;
      module.exports = factory(d3);
  } else {
      root.ClimateChart = factory(root.d3);
  }
}(this, function(d3) {

  function Chart(selector, _options) {

      if (typeof selector === 'undefined') {
          throw new Error('You need to specify a selector.');
      }

      if (typeof _options === 'undefined' || !_options.path) {
          throw new Error('You need to specify options: path');
      }

      var optionsDefault = {
          xAccessor: 'gdp_2014',
          yAccessor: 'co2_t_pc_2012',
          sizeAccessor: 'population_2014',
          xAxisLabel: 'gdp 2014',
          yAxisLabel: 'unemployment 2013',
          colors: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'],
          pointRange: [10, 1000],
          xTicks: 5,
          yTicks: 5,
          height: 400,
          lang: 'de',
          interactive: true,
          showLegend: false,
          margin: {
              top: 40,
              right: 50,
              bottom: 40,
              left: 75
          },
          filter: null
      };

      _options = _merge(_options, optionsDefault);

      d3.csv(_options.path, function(err, data) {
        if(err) {
          throw new Error('Data not found.');
          return false;
        }
        data = _parseData(data, _options);
        renderChart(selector, _options, data);
      });

      return this;
  }

  var svg, x, y, r, color, xAxis, yAxis, xExtent, yExtent, bubbleGroup;

  var width = 960,
      height = 500;

  var margin = {
    top: 50,
    right : 50,
    bottom : 50,
    left : 50
  }

  function renderChart(selector, options, data) {

    svg = d3.select(selector)
      .append('svg')
      .attr('width', width + margin.right + margin.left)
      .attr('height', height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    xExtent = d3.extent(data, function(d,i) { return d[options.xAccessor] });
    yExtent = d3.extent(data, function(d,i) { return d[options.yAccessor] });
    rExtent = d3.extent(data, function(d,i) { return d[options.sizeAccessor] });

    xAxis = d3.svg.axis().orient('bottom').scale(x);
    yAxis = d3.svg.axis().orient('left').scale(y);

    x = d3.scale.linear().domain(xExtent).range([margin.left, width - margin.right]);
    y = d3.scale.linear().domain(yExtent).range([height - margin.bottom, margin.top]);
    r = d3.scale.sqrt().domain(rExtent).range([3,10]);
    color = d3.scale.ordinal().range(options.colors);

    // bubbleGroup = svg.append('g')
    //   .classed('bubble-group', true)
    //   .attr('width', width + margin.right + margin.left)
    //   .attr('height', height + margin.top + margin.bottom);

    svg.selectAll('circle.bubble')
      .data(data)
      .enter()
      .append('circle')
      .classed('bubble', true)
      .attr('cx', function(d,i) { return x(d[options.xAccessor]) })
      .attr('cy', function(d,i) { return y(d[options.yAccessor]) })
      .attr('r', function(d,i) { return r(d[options.sizeAccessor]) })
      .attr('fill', function(d,i) { return color(d['fraction'] )});

    svg.call(createAxis);
  }

  function createAxis(d) {
    console.log(this);
    this.append('g')
      .classed('y axis', true)
      .call(yAxis);
  }

  ///////////////////////
  // private functions //
  ///////////////////////

  function _parseData(data, o) {
    data.forEach(function(d,i) {
      d[o.xAccessor] = parseFloat(d[o.xAccessor]);
      d[o.yAccessor] = parseFloat(d[o.yAccessor]);
      d[o.sizeAccessor] = parseFloat(d[o.sizeAccessor]);
    });

    data = data.filter(function(d,i) {
      return !isNaN(d[o.xAccessor]) && !isNaN(d[o.yAccessor]) && !isNaN(d[o.sizeAccessor]);
    });

    return data;
  }

  function _crunchData(data, options) {
      var keys = ['A', 'B', 'C', 'D'];
      var cleanData = keys.map(function(key) {
          return {
              color: options.colors[key],
              key: key,
              values: []
          }
      });

      var dataToCrunch = options.filter ? data.filter(options.filter) : data;

      dataToCrunch.forEach(function(d) {
          if (d.fraction) {
              var index = keys.indexOf(d.fraction);
              cleanData[index].values.push({
                  x: d[options.xAccessor],
                  y: d[options.yAccessor],
                  size: d[options.sizeAccessor]
              });
          }
      });

      return cleanData;
  }

  // function _parseData(data) {
  //     // cast strings to number if data is numeric
  //     for (var key in data) {
  //         var d = +data[key];
  //         if (!isNaN(d) && isFinite(d)) {
  //             data[key] = d;
  //         }
  //     }
  //     return data;
  // }

  function _merge() {
      var obj = {},
          key;

      for (var i = 0; i < arguments.length; i++) {
          for (key in arguments[i]) {
              if (arguments[i].hasOwnProperty(key)) {
                  obj[key] = arguments[i][key];
              }
          }
      }
      return obj;
  }

  return Chart;

}));
<commit_msg>Update colors to comply with spec.<commit_after>(function(root, factory) {
  if (typeof module !== 'undefined' && module.exports) {
      var d3 = require('d3');
      window.d3 = d3;
      module.exports = factory(d3);
  } else {
      root.ClimateChart = factory(root.d3);
  }
}(this, function(d3) {

  function Chart(selector, _options) {

      if (typeof selector === 'undefined') {
          throw new Error('You need to specify a selector.');
      }

      if (typeof _options === 'undefined' || !_options.path) {
          throw new Error('You need to specify options: path');
      }

      var optionsDefault = {
          xAccessor: 'gdp_2014',
          yAccessor: 'co2_t_pc_2012',
          sizeAccessor: 'population_2014',
          xAxisLabel: 'gdp 2014',
          yAxisLabel: 'unemployment 2013',
          colors: ['#005fcc', '#5c0000', '#009300', '#ea8500'],
          pointRange: [10, 1000],
          xTicks: 5,
          yTicks: 5,
          height: 400,
          lang: 'de',
          interactive: true,
          showLegend: false,
          margin: {
              top: 40,
              right: 50,
              bottom: 40,
              left: 75
          },
          filter: null
      };

      _options = _merge(_options, optionsDefault);

      d3.csv(_options.path, function(err, data) {
        if(err) {
          throw new Error('Data not found.');
          return false;
        }
        data = _parseData(data, _options);
        renderChart(selector, _options, data);
      });

      return this;
  }

  var svg, x, y, r, color, xAxis, yAxis, xExtent, yExtent, bubbleGroup;

  var width = 960,
      height = 500;

  var margin = {
    top: 50,
    right : 50,
    bottom : 50,
    left : 50
  }

  function renderChart(selector, options, data) {

    svg = d3.select(selector)
      .append('svg')
      .attr('width', width + margin.right + margin.left)
      .attr('height', height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    xExtent = d3.extent(data, function(d,i) { return d[options.xAccessor] });
    yExtent = d3.extent(data, function(d,i) { return d[options.yAccessor] });
    rExtent = d3.extent(data, function(d,i) { return d[options.sizeAccessor] });

    xAxis = d3.svg.axis().orient('bottom').scale(x);
    yAxis = d3.svg.axis().orient('left').scale(y);

    x = d3.scale.linear().domain(xExtent).range([margin.left, width - margin.right]);
    y = d3.scale.linear().domain(yExtent).range([height - margin.bottom, margin.top]);
    r = d3.scale.sqrt().domain(rExtent).range([3,10]);
    color = d3.scale.ordinal().range(options.colors);

    // bubbleGroup = svg.append('g')
    //   .classed('bubble-group', true)
    //   .attr('width', width + margin.right + margin.left)
    //   .attr('height', height + margin.top + margin.bottom);

    svg.selectAll('circle.bubble')
      .data(data)
      .enter()
      .append('circle')
      .classed('bubble', true)
      .attr('cx', function(d,i) { return x(d[options.xAccessor]) })
      .attr('cy', function(d,i) { return y(d[options.yAccessor]) })
      .attr('r', function(d,i) { return r(d[options.sizeAccessor]) })
      .attr('fill', function(d,i) { return color(d['fraction'] )});

    svg.call(createAxis);
  }

  function createAxis(d) {
    console.log(this);
    this.append('g')
      .classed('y axis', true)
      .call(yAxis);
  }

  ///////////////////////
  // private functions //
  ///////////////////////

  function _parseData(data, o) {
    data.forEach(function(d,i) {
      d[o.xAccessor] = parseFloat(d[o.xAccessor]);
      d[o.yAccessor] = parseFloat(d[o.yAccessor]);
      d[o.sizeAccessor] = parseFloat(d[o.sizeAccessor]);
    });

    data = data.filter(function(d,i) {
      return !isNaN(d[o.xAccessor]) && !isNaN(d[o.yAccessor]) && !isNaN(d[o.sizeAccessor]);
    });

    return data;
  }

  function _crunchData(data, options) {
      var keys = ['A', 'B', 'C', 'D'];
      var cleanData = keys.map(function(key) {
          return {
              color: options.colors[key],
              key: key,
              values: []
          }
      });

      var dataToCrunch = options.filter ? data.filter(options.filter) : data;

      dataToCrunch.forEach(function(d) {
          if (d.fraction) {
              var index = keys.indexOf(d.fraction);
              cleanData[index].values.push({
                  x: d[options.xAccessor],
                  y: d[options.yAccessor],
                  size: d[options.sizeAccessor]
              });
          }
      });

      return cleanData;
  }

  // function _parseData(data) {
  //     // cast strings to number if data is numeric
  //     for (var key in data) {
  //         var d = +data[key];
  //         if (!isNaN(d) && isFinite(d)) {
  //             data[key] = d;
  //         }
  //     }
  //     return data;
  // }

  function _merge() {
      var obj = {},
          key;

      for (var i = 0; i < arguments.length; i++) {
          for (key in arguments[i]) {
              if (arguments[i].hasOwnProperty(key)) {
                  obj[key] = arguments[i][key];
              }
          }
      }
      return obj;
  }

  return Chart;

}));
<|endoftext|>


<commit_before>function Chord() {
  this.keys = new Set();
  this.VALID_KEYS = ['#','S-','T-','P-','H-','K-','W-','R-','A-','O-','*','-E','-U','-F','-R','-P','-B','-L','-G','-T','-S','-G','-Z'];

  this.addKey = function(key) {
    if (this.VALID_KEYS.indexOf(key) >= 0) {
      this.keys.add(key);
    }
  };

  this.clear = function() {
    this.keys.clear();
  };

  this.toString = function() {
    var result = "";
    if (this.keys.size > 0) {
      if (this.keys.has('#')) result+='#';
      if (this.keys.has('S-')) result+='S';
      if (this.keys.has('T-')) result+='T';
      if (this.keys.has('K-')) result+='K';
      if (this.keys.has('P-')) result+='P';
      if (this.keys.has('W-')) result+='W';
      if (this.keys.has('H-')) result+='H';
      if (this.keys.has('R-')) result+='R';
      if (this.keys.has('A-')) result+='A';
      if (this.keys.has('O-')) result+='O';
      if (this.keys.has('*')) result+='*';
      if (this.keys.has('-E')) result+='E';
      if (this.keys.has('-U')) result+='U';
      if (needs_hyphen(this.keys)) result += "-";
      if (this.keys.has('-F')) result+='F';
      if (this.keys.has('-R')) result+='R';
      if (this.keys.has('-P')) result+='P';
      if (this.keys.has('-B')) result+='B';
      if (this.keys.has('-L')) result+='L';
      if (this.keys.has('-G')) result+='G';
      if (this.keys.has('-T')) result+='T';
      if (this.keys.has('-S')) result+='S';
      if (this.keys.has('-D')) result+='D';
      if (this.keys.has('-Z')) result+='Z';
    }
    //trim a final hyphen
    result = result.replace(/\-$/, '');
    return result;
  };

  function needs_hyphen(keys) {
    var IMPLICIT_HYPHENS = ['A-','O-','5-','0-','-E','-U','*'];
    return ! IMPLICIT_HYPHENS.some(function (key) {
      return keys.has(key);
    });
  }
}

<commit_msg>-D was not included in VALID_KEYS<commit_after>function Chord() {
  this.keys = new Set();
  this.VALID_KEYS = ['#','S-','T-','P-','H-','K-','W-','R-','A-','O-','*','-E','-U','-F','-R','-P','-B','-L','-G','-T','-S','-D','-Z'];

  this.addKey = function(key) {
    if (this.VALID_KEYS.indexOf(key) >= 0) {
      this.keys.add(key);
    }
  };

  this.clear = function() {
    this.keys.clear();
  };

  this.toString = function() {
    var result = "";
    if (this.keys.size > 0) {
      if (this.keys.has('#')) result+='#';
      if (this.keys.has('S-')) result+='S';
      if (this.keys.has('T-')) result+='T';
      if (this.keys.has('K-')) result+='K';
      if (this.keys.has('P-')) result+='P';
      if (this.keys.has('W-')) result+='W';
      if (this.keys.has('H-')) result+='H';
      if (this.keys.has('R-')) result+='R';
      if (this.keys.has('A-')) result+='A';
      if (this.keys.has('O-')) result+='O';
      if (this.keys.has('*')) result+='*';
      if (this.keys.has('-E')) result+='E';
      if (this.keys.has('-U')) result+='U';
      if (needs_hyphen(this.keys)) result += "-";
      if (this.keys.has('-F')) result+='F';
      if (this.keys.has('-R')) result+='R';
      if (this.keys.has('-P')) result+='P';
      if (this.keys.has('-B')) result+='B';
      if (this.keys.has('-L')) result+='L';
      if (this.keys.has('-G')) result+='G';
      if (this.keys.has('-T')) result+='T';
      if (this.keys.has('-S')) result+='S';
      if (this.keys.has('-D')) result+='D';
      if (this.keys.has('-Z')) result+='Z';
    }
    //trim a final hyphen
    result = result.replace(/\-$/, '');
    return result;
  };

  function needs_hyphen(keys) {
    var IMPLICIT_HYPHENS = ['A-','O-','5-','0-','-E','-U','*'];
    return ! IMPLICIT_HYPHENS.some(function (key) {
      return keys.has(key);
    });
  }
}

<|endoftext|>


<commit_before>
<commit_msg>Update script.js<commit_after>$(function() {
  $('a[href*="#"]:not([href="#"])').click(function() {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
        $('html, body').animate({
          scrollTop: target.offset().top
        }, 1000);
        return false;
      }
    }
  });
});
<|endoftext|>


<commit_before>$(document).ready(function() {
  // nav
  // nav clase
  $(".nav a").on("click", function(){
   $(".nav").find(".active").removeClass("active");
   $(this).parent().addClass("active");
});
  // new
  $('.navbar-collapse a').click(function(){
    $(".navbar-collapse").collapse('hide');
});
  // end new
    $('nav').affix({
      offset: {
        top: function() { return  $('#imagen-inicial').height(); }
      }
     });

  $('a[href*="#"]:not([href="#"])').click(function(event) {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
      if (this.hash !== "") {

          // Prevent default anchor click behavior
          event.preventDefault();

          // Store hash
          var hash = this.hash;

          // Using jQuery's animate() method to add smooth page scroll
          // The optional number (800) specifies the number of milliseconds it takes to scroll to the specified area
        if ( $("nav").hasClass("affix")) {
          $('html, body').animate({
            scrollTop: $(hash).offset().top - 51
          }, 800, function(){

          // Add hash (#) to URL when done scrolling (default click behavior)
            window.location.hash = hash;
          });
      } else {
          $('html, body').animate({
            scrollTop: $(hash).offset().top - 120
          }, 800, function(){

          // Add hash (#) to URL when done scrolling (default click behavior)
            window.location.hash = hash;
          });
      }

        } // End if

      /*
      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
        $('html, body').animate({
          scrollTop: target.offset().top - 51
        }, 300);
        return false;
      }
      */
    }
  });

  // Animación
  var $window           = $(window),
      win_height_padded = $window.height() * 1.1,
      isTouch           = Modernizr.touch;

  if (isTouch) { $('.revealOnScroll').addClass('animated'); }

  $window.on('scroll', revealOnScroll);

  function revealOnScroll() {
    var scrolled = $window.scrollTop(),
        win_height_padded = $window.height() * 1.1;

    // Showed...
    $(".revealOnScroll:not(.animated)").each(function () {
      var $this     = $(this),
          offsetTop = $this.offset().top;

      if (scrolled + win_height_padded > offsetTop) {
        if ($this.data('timeout')) {
          window.setTimeout(function(){
            $this.addClass('animated ' + $this.data('animation'));
          }, parseInt($this.data('timeout'),10));
        } else {
          $this.addClass('animated ' + $this.data('animation'));
        }
      }
    });
    // Hidden...
   $(".revealOnScroll.animated").each(function (index) {
      var $this     = $(this),
          offsetTop = $this.offset().top;
      if (scrolled + win_height_padded < offsetTop) {
        $(this).removeClass('animated fadeInUp flipInX lightSpeedIn')
      }
    });
  }

  revealOnScroll();

});
<commit_msg>Update script.js<commit_after>$(document).ready(function() {
  // nav
  // nav clase
  $(".nav a").on("click", function(){
   $(".nav").find(".active").removeClass("active");
   $(this).parent().addClass("active");
});
  // new
  $('.navbar-collapse a').click(function(){
    $(".navbar-collapse").collapse('hide');
});
  // end new
    $('nav').affix({
      offset: {
        top: function() { return  $('#imagen-inicial').height(); }
      }
     });

  $('a[href*="#"]:not([href="#"])').click(function(event) {
    if (location.pathname.replace(/^\//,'') == this.pathname.replace(/^\//,'') && location.hostname == this.hostname) {
      if (this.hash !== "") {

          // Prevent default anchor click behavior
          event.preventDefault();

          // Store hash
          var hash = this.hash;

          // Using jQuery's animate() method to add smooth page scroll
          // The optional number (800) specifies the number of milliseconds it takes to scroll to the specified area
        //if ( $("nav").hasClass("affix")) {
          $('html, body').animate({
            scrollTop: $(hash).offset().top// - 51
         // }, 800, function(){

          // Add hash (#) to URL when done scrolling (default click behavior)
            window.location.hash = hash;
          });
      //} else {
      //    $('html, body').animate({
      //      scrollTop: $(hash).offset().top - 120
      //    }, 800, function(){

          // Add hash (#) to URL when done scrolling (default click behavior)
      //      window.location.hash = hash;
      //    });
      //}

        } // End if

      /*
      var target = $(this.hash);
      target = target.length ? target : $('[name=' + this.hash.slice(1) +']');
      if (target.length) {
        $('html, body').animate({
          scrollTop: target.offset().top - 51
        }, 300);
        return false;
      }
      */
    }
  });

  // Animación
  var $window           = $(window),
      win_height_padded = $window.height() * 1.1,
      isTouch           = Modernizr.touch;

  if (isTouch) { $('.revealOnScroll').addClass('animated'); }

  $window.on('scroll', revealOnScroll);

  function revealOnScroll() {
    var scrolled = $window.scrollTop(),
        win_height_padded = $window.height() * 1.1;

    // Showed...
    $(".revealOnScroll:not(.animated)").each(function () {
      var $this     = $(this),
          offsetTop = $this.offset().top;

      if (scrolled + win_height_padded > offsetTop) {
        if ($this.data('timeout')) {
          window.setTimeout(function(){
            $this.addClass('animated ' + $this.data('animation'));
          }, parseInt($this.data('timeout'),10));
        } else {
          $this.addClass('animated ' + $this.data('animation'));
        }
      }
    });
    // Hidden...
   $(".revealOnScroll.animated").each(function (index) {
      var $this     = $(this),
          offsetTop = $this.offset().top;
      if (scrolled + win_height_padded < offsetTop) {
        $(this).removeClass('animated fadeInUp flipInX lightSpeedIn')
      }
    });
  }

  revealOnScroll();

});
<|endoftext|>


<commit_before>const fs = require('fs');
const oneLine = require('common-tags').oneLine;
const Command = require('../base');

module.exports = class LoadCommandCommand extends Command {
	constructor(client) {
		super(client, {
			name: 'load',
			aliases: ['load-command'],
			group: 'commands',
			memberName: 'load',
			description: 'Loads a new command.',
			details: oneLine`
				The argument must be full name of the command in the format of \`group:memberName\`.
				Only the bot owner may use this command.
			`,
			examples: ['load some-command'],
			guarded: true,

			args: [
				{
					key: 'command',
					prompt: 'Which command would you like to load?',
					validate: val => new Promise(resolve => {
						if(!val) return resolve(false);
						const split = val.split(':');
						if(split.length !== 2) return resolve(false);
						if(this.client.registry.findCommands(val).length > 0) {
							return resolve('That command is already registered.');
						}
						const cmdPath = this.client.registry.resolveCommandPath(split[0], `${split[1]}.js`);
						fs.access(cmdPath, fs.constants.R_OK, err => err ? resolve(false) : resolve(true));
						return null;
					}),
					parse: val => {
						const split = val.split(':');
						const cmdPath = this.client.registry.resolveCommandPath(split[0], `${split[1]}.js`);
						delete require.cache[cmdPath];
						return require(cmdPath);
					}
				}
			]
		});
	}

	hasPermission(msg) {
		return msg.author.id === this.client.options.owner;
	}

	async run(msg, args) {
		this.client.registry.registerCommand(args.command);
		msg.reply(`Loaded \`${this.client.registry.commands.last().name}\` command.`);
		return null;
	}
};
<commit_msg>Fixed load command<commit_after>const fs = require('fs');
const oneLine = require('common-tags').oneLine;
const Command = require('../base');

module.exports = class LoadCommandCommand extends Command {
	constructor(client) {
		super(client, {
			name: 'load',
			aliases: ['load-command'],
			group: 'commands',
			memberName: 'load',
			description: 'Loads a new command.',
			details: oneLine`
				The argument must be full name of the command in the format of \`group:memberName\`.
				Only the bot owner may use this command.
			`,
			examples: ['load some-command'],
			guarded: true,

			args: [
				{
					key: 'command',
					prompt: 'Which command would you like to load?',
					validate: val => new Promise(resolve => {
						if(!val) return resolve(false);
						const split = val.split(':');
						if(split.length !== 2) return resolve(false);
						if(this.client.registry.findCommands(val).length > 0) {
							return resolve('That command is already registered.');
						}
						const cmdPath = this.client.registry.resolveCommandPath(split[0], split[1]);
						fs.access(cmdPath, fs.constants.R_OK, err => err ? resolve(false) : resolve(true));
						return null;
					}),
					parse: val => {
						const split = val.split(':');
						const cmdPath = this.client.registry.resolveCommandPath(split[0], split[1]);
						delete require.cache[cmdPath];
						return require(cmdPath);
					}
				}
			]
		});
	}

	hasPermission(msg) {
		return msg.author.id === this.client.options.owner;
	}

	async run(msg, args) {
		this.client.registry.registerCommand(args.command);
		msg.reply(`Loaded \`${this.client.registry.commands.last().name}\` command.`);
		return null;
	}
};
<|endoftext|>


<commit_before>//= require "jquery"
//= require "jquery.flot"
//= require "jquery.flot.time"
//= require "jquery.isotope"
//= require "jquery.pin"
//= require "jquery.matchHeight"
//= require "share"
//= require_self

$(function() {
  var share = new Share("#share-button-top", {
    networks: {
      facebook: {
        app_id: "1604147083144211",
      }
    }
  });

  // This is a band-aid for the broken menu bar problem
  $(window).on('resize', function(){
    $('.navbar').attr('style', '').removeData('pin');
    $('.navbar').pin();
  });

  var sortAscending = {title: true};

  $(".projects").isotope({
    layoutMode: "fitRows",
    getSortData: {
      stars: "[data-stars] parseInt",
      forks: "[data-forks] parseInt",
      issues: "[data-issues] parseInt",
      language: "[data-language]",
      title: "[data-title]"
    }
  });

  $('.landing .card').matchHeight();

  $("select[name='filter']").change(function(e) {
    console.log("Filter by: %o", $(this).val());
    $(".projects").isotope({filter: '.' + $(this).val().replace(/[^A-Z0-9_-]/gi, '\\$&')});
  });

  $("select[name='sort']").change(function(e) {
    var val = $(this).val();
    $(".projects").isotope({sortBy: val, sortAscending: sortAscending[val] || false});
  });
});
<commit_msg>Make sure '.' only added to filter if other text.<commit_after>//= require "jquery"
//= require "jquery.flot"
//= require "jquery.flot.time"
//= require "jquery.isotope"
//= require "jquery.pin"
//= require "jquery.matchHeight"
//= require "share"
//= require_self

$(function() {
  var share = new Share("#share-button-top", {
    networks: {
      facebook: {
        app_id: "1604147083144211",
      }
    }
  });

  // This is a band-aid for the broken menu bar problem
  $(window).on('resize', function(){
    $('.navbar').attr('style', '').removeData('pin');
    $('.navbar').pin();
  });

  var sortAscending = {title: true};

  $(".projects").isotope({
    layoutMode: "fitRows",
    getSortData: {
      stars: "[data-stars] parseInt",
      forks: "[data-forks] parseInt",
      issues: "[data-issues] parseInt",
      language: "[data-language]",
      title: "[data-title]"
    }
  });

  $('.landing .card').matchHeight();

  $("select[name='filter']").change(function(e) {
    var v = $(this).val();
    console.log("Filter by: %o", v);
    if (v) {
      v = '.' + v.replace(/[^A-Z0-9_-]/gi, '\\$&');
    }
    $(".projects").isotope({filter: v});
  });

  $("select[name='sort']").change(function(e) {
    var val = $(this).val();
    $(".projects").isotope({sortBy: val, sortAscending: sortAscending[val] || false});
  });
});
<|endoftext|>


<commit_before>(function (Q, $) {
/**
 * @module Q-tools
 */

/**
 * Implements lazy-loading for various types of elements.
 * By default, has implementations for "img" and "Q_tool" selectors.
 * The "Q_tool" handler only works with tools whose elements have a "data-q-lazyload" attribute .
 * Note that the elements must have a stable, nonzero width height set in the CSS even
 * if they are empty, otherwise they might not be lazy-loaded, and they might
 * thrash back and forth if removed.
 * @class Q lazyload
 * @constructor
 * @param {Object} [options] Override various options for this tool
 * @param {Array} [options.handle] Set this to
 * @param {Object} [options.handlers] You can modify the defaults by passing an object
 *    that looks like {handlerName: { selector: String, entering: Function, exiting: Function, preparing: Function}}
 *    Function "entering" receives (element, intersectionObserverEntry)
 *    Function "exiting" receives (element, intersectionObserverEntry)
 *    Function "preparing" receives (element) and is used the first time to prepare the element
 *    Both functions must return true if the element was modified.
 * @param {Object} [observerOptions] Override any options to pass to IntersectionObserver
 * @return {Q.Tool}
 */
Q.Tool.define('Q/lazyload', function (options) {

	var tool = this;
	var state = this.state;

	var Elp = Element.prototype;

	if (!window.IntersectionObserver) {
		console.warn("Q/lazyload tool: need to use IntersectionObserver polyfill");
		return; // do nothing, this is an older browser
	}

	// Observe whatever is on the page already
	tool.observer = _createObserver(tool, tool.element);
	tool.findAndObserve(tool.element);

	// Override innerHTML

	var originalSet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').set;
	var originalGet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').get;

	Object.defineProperty(Elp, 'innerHTML', {
		set: function (html) {
			var element = document.createElement('div');
			originalSet.call(element, html);
			var found = false;
			Q.each(state.handlers, function (name, info) {
				var elements = element.querySelectorAll
					? Array.from(element.querySelectorAll(info.selector))
					: [];
				Q.each(elements, function (i, element) {
					if (info.preparing.call(tool, element, true) === true) {
						found = true;
					}
				});
			});
			if (found) {
				html = originalGet.call(element);
			}
			originalSet.call(this, html);
			if (found) {
				tool.findAndObserve(this);
			}
			return html;
		},
		get: originalGet
	});

	Q.each(['insertBefore', 'appendChild'], function (i, fn) {
		var orig = Elp[fn];
		Elp[fn] = function (element) {
			if (!element) {
				return;
			}
			var found = false;
			Q.each(state.handlers, function (name, info) {
				var elements = element.querySelectorAll
					? Array.from(element.querySelectorAll(info.selector))
					: [];
				if (element.matches && element.matches(info.selector)) {
					elements.push(element);
				}
				Q.each(elements, function (i, element) {
					if (info.preparing.call(tool, element, true) === true) {
						found = true;
						tool.observer.observe(element);
					}
				});
			});
			return orig.apply(this, arguments);
		};
	});

},

{
	root: null,
	handlers: {
		img: {
			selector: 'img',
			entering: function (img, entry) {
				function _loaded() {
					img.addClass('Q_lazy_loaded');
				}
				var src = img.getAttribute('data-lazyload-src');
				if (src) {
					img.setAttribute('src', Q.url(src));
					img.removeAttribute('data-lazyload-src');
					img.addClass('Q_lazy_load');
					if (img.complete) {
						_loaded();
					}
					img.addEventListener('load', _loaded);
				}
				return true;
			},
			exiting: function (img) {
				// no need to do anything
				return true;
			},
			preparing: function (img, beingInsertedIntoDOM) {
				if (!beingInsertedIntoDOM) {
					return true; // too late anyway, browser will load image
				}
				var src = img.getAttribute('src');
				if (src && src.substr(0, 5) !== 'data:'
				&& !img.hasAttribute('data-lazyload-src')) {
					img.setAttribute('data-lazyload-src', Q.url(src));
					img.setAttribute('src', Q.url(
						Q.getObject('Q.images.lazyload.loadingSrc')
						|| "{{Q}}/img/throbbers/transparent.gif"
					));
				}
				return true;
			}
		},
		tool: {
			selector: '.Q_tool',
			entering: function (element, entry) {
				if (element.hasAttribute('data-q-lazyload')
				&& (!element.Q || !element.Q.tool)) {
					element.addClass('Q_lazy_load');
					Q.activate(element, function () {
						element.addClass('Q_lazy_loaded');
					});
					return true;
				}
			},
			exiting: function (element, entry) {
				if (element.hasAttribute('data-q-lazyload')
				&& element.Q.tool) {
					Q.Tool.remove(element);
					element.removeClass('Q_lazy_loading');
					element.removeClass('Q_lazy_loaded');
					if (this.state.handlers.tool.exitingRemoveHTML) {
						element.innerHTML = '';
					}
				}
				return true;
			},
			preparing: function (element) {
				return true;
			},
			exitingRemoveHTML: true
		}
	},
	observerOptions: {
		root: null,
		rootMargin: '0px',
		threshold: 0
	}
},

{
	findAndObserve: function (container) {
		var tool = this;
		var found = false;
		Q.each(tool.state.handlers, function (name, info) {
			var elements = container.querySelectorAll
				? Array.from(container.querySelectorAll(info.selector))
				: [];
			if (container.matches && container.matches(info.selector)) {
				elements.push(container);
			}
			Q.each(elements, function (i, element) {
				if (info.preparing.call(tool, element) === true) {
					found = true;
					tool.observer.observe(element);
				}
			});
		});
	}
});

function _createObserver(tool, container) {
	return new IntersectionObserver(function (entries, observer) {
		Q.each(entries, function (i, entry) {
			Q.each(tool.state.handlers, function (name, info) {
				if (entry.target.matches && entry.target.matches(info.selector)) {
					if (entry.isIntersecting) {
						info.entering.call(tool, entry.target, entry);
					} else  {
						var rect = entry.target.getBoundingClientRect();
						if (rect.width > 0 && rect.height > 0) {
							// also covers !document.body.contains(entry.target)
							info.exiting.call(tool, entry.target, entry);
						} // otherwise it might get a false positive
					}
				}
			});
		});
	}, tool.state.observerOptions);
}

})(Q, jQuery);<commit_msg>Modified Q/lazyload implementation<commit_after>(function (Q, $) {
/**
 * @module Q-tools
 */

/**
 * Implements lazy-loading for various types of elements.
 * By default, has implementations for "img" and "Q_tool" selectors.
 * The "Q_tool" handler only works with tools whose elements have a "data-q-lazyload" attribute .
 * Note that the elements must have a stable, nonzero width height set in the CSS even
 * if they are empty, otherwise they might not be lazy-loaded, and they might
 * thrash back and forth if removed.
 * @class Q lazyload
 * @constructor
 * @param {Object} [options] Override various options for this tool
 * @param {Array} [options.handle] Set this to
 * @param {Object} [options.handlers] You can modify the defaults by passing an object
 *    that looks like {handlerName: { selector: String, entering: Function, exiting: Function, preparing: Function}}
 *    Function "entering" receives (element, intersectionObserverEntry)
 *    Function "exiting" receives (element, intersectionObserverEntry)
 *    Function "preparing" receives (element) and is used the first time to prepare the element
 *    Both functions must return true if the element was modified.
 * @param {Object} [observerOptions] Override any options to pass to IntersectionObserver
 * @return {Q.Tool}
 */
Q.Tool.define('Q/lazyload', function (options) {

	var tool = this;
	var state = this.state;

	var Elp = Element.prototype;

	if (!window.IntersectionObserver) {
		console.warn("Q/lazyload tool: need to use IntersectionObserver polyfill");
		return; // do nothing, this is an older browser
	}

	// Observe whatever is on the page already
	tool.observer = _createObserver(tool, tool.element);
	tool.findAndObserve(tool.element, false);

	// Override innerHTML

	var originalSet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').set;
	var originalGet = Object.getOwnPropertyDescriptor(Elp, 'innerHTML').get;

	Object.defineProperty(Elp, 'innerHTML', {
		set: function (html) {
			var element = document.createElement('div');
			originalSet.call(element, html);
			var found = false;
			Q.each(state.handlers, function (name, info) {
				var elements = element.querySelectorAll
					? Array.from(element.querySelectorAll(info.selector))
					: [];
				if (elements.length) {
					found = true;
					return false;
				}
			});
			if (found) {
				html = originalGet.call(element);
			}
			originalSet.call(this, html);
			if (found) {
				tool.findAndObserve(this, true);
			}
			return html;
		},
		get: originalGet
	});

	Q.each(['insertBefore', 'appendChild'], function (i, fn) {
		var orig = Elp[fn];
		Elp[fn] = function (element) {
			if (!element) {
				return;
			}
			tool.findAndObserve(this, true);
			return orig.apply(this, arguments);
		};
	});

},

{
	root: null,
	handlers: {
		img: {
			selector: 'img',
			entering: function (img, entry) {
				function _loaded() {
					img.addClass('Q_lazy_loaded');
				}
				var src = img.getAttribute('data-lazyload-src');
				if (src) {
					img.setAttribute('src', Q.url(src));
					img.removeAttribute('data-lazyload-src');
					img.addClass('Q_lazy_load');
					if (img.complete) {
						_loaded();
					}
					img.addEventListener('load', _loaded);
				}
				return true;
			},
			exiting: function (img) {
				// no need to do anything
				return true;
			},
			preparing: function (img, beingInsertedIntoDOM) {
				if (!beingInsertedIntoDOM) {
					return true; // too late anyway, browser will load image
				}
				var src = img.getAttribute('src');
				if (src && src.substr(0, 5) !== 'data:'
				&& !img.hasAttribute('data-lazyload-src')) {
					img.setAttribute('data-lazyload-src', Q.url(src));
					img.setAttribute('src', Q.url(
						Q.getObject('Q.images.lazyload.loadingSrc')
						|| "{{Q}}/img/throbbers/transparent.gif"
					));
				}
				return true;
			}
		},
		tool: {
			selector: '.Q_tool',
			entering: function (element, entry) {
				if (element.hasAttribute('data-q-lazyload')
				&& (!element.Q || !element.Q.tool)) {
					element.addClass('Q_lazy_load');
					Q.activate(element, function () {
						element.addClass('Q_lazy_loaded');
					});
					return true;
				}
			},
			exiting: function (element, entry) {
				if (element.hasAttribute('data-q-lazyload')
				&& element.Q.tool) {
					Q.Tool.remove(element);
					element.removeClass('Q_lazy_loading');
					element.removeClass('Q_lazy_loaded');
					if (this.state.handlers.tool.exitingRemoveHTML) {
						element.innerHTML = '';
					}
				}
				return true;
			},
			preparing: function (element) {
				return true;
			},
			exitingRemoveHTML: true
		}
	},
	observerOptions: {
		root: null,
		rootMargin: '0px',
		threshold: 0
	}
},

{
	findAndObserve: function (container, beingInsertedIntoDOM) {
		var tool = this;
		var found = [];
		Q.each(tool.state.handlers, function (name, info) {
			var elements = container.querySelectorAll
				? Array.from(container.querySelectorAll(info.selector))
				: [];
			if (container.matches && container.matches(info.selector)) {
				elements.push(container);
			}
			Q.each(elements, function (i, element) {
				if (info.preparing.call(tool, element, beingInsertedIntoDOM) === true) {
					found.push(element);
					tool.observer.observe(element);
				}
			});
		});
		return found;
	}
});

function _createObserver(tool, container) {
	return new IntersectionObserver(function (entries, observer) {
		Q.each(entries, function (i, entry) {
			Q.each(tool.state.handlers, function (name, info) {
				if (entry.target.matches && entry.target.matches(info.selector)) {
					if (entry.isIntersecting) {
						info.entering.call(tool, entry.target, entry);
					} else  {
						var rect = entry.target.getBoundingClientRect();
						if (rect.width > 0 && rect.height > 0) {
							// also covers !document.body.contains(entry.target)
							info.exiting.call(tool, entry.target, entry);
						} // otherwise it might get a false positive
					}
				}
			});
		});
	}, tool.state.observerOptions);
}

})(Q, jQuery);<|endoftext|>


<commit_before>// This can't be replaced with `new URL()` because browsers don't support git+http urls

const protocol = '(?:([^:]+:)?(?://)?)?';
const auth = '(?:(\\S+(?::\\S*)?)@)?';
const host = '([^/:]*)';
const path = '([/]?[^#]*)';
const hash = '(#.+)?';
const urlLaxRegex = new RegExp(protocol + auth + host + path + hash);

export function parse(url) {
  const match = url.match(urlLaxRegex);
  if (match) {
    let path = match[4];
    if (path && path[0] !== '/') {
      path = `/${path}`;
    }

    return {
      protocol: match[1],
      auth: match[2],
      host: match[3],
      path,
      hash: match[5]
    };
  }
}
<commit_msg>Enable parsing of more modules<commit_after>// This can't be replaced with `new URL()` because browsers don't support git+http urls

const protocol = '(?:([^:]+:)?(?://)?)?';
const auth = '(?:(\\S+(?::\\S*)?)@)?';
const host = '([^/:]*)';
const path = '([/]?[^#]*)';
const hash = '(#.+)?';
const urlLaxRegex = new RegExp(protocol + auth + host + path + hash);

export function parse(url) {
  const match = url.match(urlLaxRegex);
  if (match) {
    let path = match[4];
    if (path && path[0] !== '/') {
      path = `/${path}`;
    }

    return {
      protocol: match[1],
      auth: match[2],
      host: match[3],
      path,
      hash: match[5]
    };
  }
}

const URL = window.URL;
export {URL};
<|endoftext|>


<commit_before>// @flow
import * as Ship from '../index';

type Effect = {
  type: 'HttpRequest',
  url: string,
};

function httpRequest<Commit, State>(url: string): Ship.Ship<Effect, Commit, State, string> {
  return Ship.call({
    type: 'HttpRequest',
    url,
  });
}

type EyeState = {
  color: ?string,
  isLoading: bool,
};

const initialEyeState: EyeState = {
  color: null,
  isLoading: false,
};

type EyeCommit = {
  type: 'LoadStart',
} | {
  type: 'LoadSuccess',
  color: string,
};

export function eyeReduce(state: EyeState, commit: EyeCommit): EyeState {
  switch (commit.type) {
  case 'LoadStart':
    return {
      ...state,
      isLoading: true,
    };
  case 'LoadSuccess':
    return {
      ...state,
      color: commit.color,
      isLoading: false,
    };
  default:
    return state;
  }
}

type EyeControl<A> = Ship.Ship<*, EyeCommit, EyeState, A>;

function* eyeControl(): EyeControl<void> {
  const currentEyeColor = yield* Ship.getState(state => state.color);
  if (!currentEyeColor) {
    yield* Ship.commit({type: 'LoadStart'});
    const [r2d2] = yield* Ship.all2(
      httpRequest('http://swapi.co/api/people/3/'),
      httpRequest('http://swapi.co/api/people/4/')
    );
    const eyeColor = JSON.parse(r2d2).eye_color;
    yield* Ship.commit({type: 'LoadSuccess', color: eyeColor});
  }
}

type State = {
  eye: EyeState,
};

const initialState: State = {
  eye: initialEyeState,
};

type Commit = {
  type: 'Eye',
  commit: EyeCommit,
};

type Control<A> = Ship.Ship<*, Commit, State, A>;

function* control(): Control<void> {
  yield* Ship.map(
    commit => ({type: 'Eye', commit}),
    state => state.eye,
    eyeControl()
  );
}

describe('map', () => {
  test('without eye', () => {
    const effectSnapshot = (id, name, eyeColor) => ({
      type: 'Effect',
      effect: {
        type: 'HttpRequest',
        url: `http://swapi.co/api/people/${id}/`,
      },
      result: JSON.stringify({
        eye_color: eyeColor,
        name,
      }),
    });
    const snapshot = [
      {type: 'GetState'},
      {type: 'Commit', commit: {type: 'Eye', commit: {type: 'LoadStart'}}},
      {
        type: 'All',
        snapshots: [
          [effectSnapshot('3', 'R2-D2', 'red')],
          [effectSnapshot('4', 'Darth Vader', 'yellow')],
        ],
      },
      {type: 'Commit', commit: {type: 'Eye', commit: {type: 'LoadSuccess', color: 'red'}}},
    ];
    expect(Ship.simulate(control(), snapshot)).toEqual(snapshot);
  });
  test('with eye', () => {
    const snapshot = [
      {type: 'GetState', state: 'red'}
    ];
    expect(Ship.simulate(control(), snapshot)).toEqual(snapshot);
  });
});

describe('run', () => {
  function runEffect(effect: Effect) {
    switch (effect.type) {
    case 'HttpRequest':
      return JSON.stringify({
        eye_color: 'red',
        name: 'R2-D2',
      });
    default:
      return;
    }
  }

  function createStore(initialState: EyeState) {
    let state = initialState;
    return {
      dispatch(commit) {
        state = eyeReduce(state, commit);
      },
      getState() {
        return state;
      }
    };
  }

  test('without eye', async () => {
    const store = createStore(initialEyeState);
    await Ship.run(runEffect, store, eyeControl());
    expect(store.getState()).toMatchSnapshot();
  });

  test('twice', async () => {
    const store = createStore(initialEyeState);
    await Ship.run(runEffect, store, eyeControl());
    await Ship.run(runEffect, store, eyeControl());
    expect(store.getState()).toMatchSnapshot();
  });
});
<commit_msg>Fix ESLint error<commit_after>// @flow
import * as Ship from '../index';

type Effect = {
  type: 'HttpRequest',
  url: string,
};

function httpRequest<Commit, State>(url: string): Ship.Ship<Effect, Commit, State, string> {
  return Ship.call({
    type: 'HttpRequest',
    url,
  });
}

type EyeState = {
  color: ?string,
  isLoading: bool,
};

const initialEyeState: EyeState = {
  color: null,
  isLoading: false,
};

type EyeCommit = {
  type: 'LoadStart',
} | {
  type: 'LoadSuccess',
  color: string,
};

export function eyeReduce(state: EyeState, commit: EyeCommit): EyeState {
  switch (commit.type) {
  case 'LoadStart':
    return {
      ...state,
      isLoading: true,
    };
  case 'LoadSuccess':
    return {
      ...state,
      color: commit.color,
      isLoading: false,
    };
  default:
    return state;
  }
}

type EyeControl<A> = Ship.Ship<*, EyeCommit, EyeState, A>;

function* eyeControl(): EyeControl<void> {
  const currentEyeColor = yield* Ship.getState(state => state.color);
  if (!currentEyeColor) {
    yield* Ship.commit({type: 'LoadStart'});
    const [r2d2] = yield* Ship.all2(
      httpRequest('http://swapi.co/api/people/3/'),
      httpRequest('http://swapi.co/api/people/4/')
    );
    const eyeColor = JSON.parse(r2d2).eye_color;
    yield* Ship.commit({type: 'LoadSuccess', color: eyeColor});
  }
}

type State = {
  eye: EyeState,
};

type Commit = {
  type: 'Eye',
  commit: EyeCommit,
};

type Control<A> = Ship.Ship<*, Commit, State, A>;

function* control(): Control<void> {
  yield* Ship.map(
    commit => ({type: 'Eye', commit}),
    state => state.eye,
    eyeControl()
  );
}

describe('map', () => {
  test('without eye', () => {
    const effectSnapshot = (id, name, eyeColor) => ({
      type: 'Effect',
      effect: {
        type: 'HttpRequest',
        url: `http://swapi.co/api/people/${id}/`,
      },
      result: JSON.stringify({
        eye_color: eyeColor,
        name,
      }),
    });
    const snapshot = [
      {type: 'GetState'},
      {type: 'Commit', commit: {type: 'Eye', commit: {type: 'LoadStart'}}},
      {
        type: 'All',
        snapshots: [
          [effectSnapshot('3', 'R2-D2', 'red')],
          [effectSnapshot('4', 'Darth Vader', 'yellow')],
        ],
      },
      {type: 'Commit', commit: {type: 'Eye', commit: {type: 'LoadSuccess', color: 'red'}}},
    ];
    expect(Ship.simulate(control(), snapshot)).toEqual(snapshot);
  });
  test('with eye', () => {
    const snapshot = [
      {type: 'GetState', state: 'red'}
    ];
    expect(Ship.simulate(control(), snapshot)).toEqual(snapshot);
  });
});

describe('run', () => {
  function runEffect(effect: Effect) {
    switch (effect.type) {
    case 'HttpRequest':
      return JSON.stringify({
        eye_color: 'red',
        name: 'R2-D2',
      });
    default:
      return;
    }
  }

  function createStore(initialState: EyeState) {
    let state = initialState;
    return {
      dispatch(commit) {
        state = eyeReduce(state, commit);
      },
      getState() {
        return state;
      }
    };
  }

  test('without eye', async () => {
    const store = createStore(initialEyeState);
    await Ship.run(runEffect, store, eyeControl());
    expect(store.getState()).toMatchSnapshot();
  });

  test('twice', async () => {
    const store = createStore(initialEyeState);
    await Ship.run(runEffect, store, eyeControl());
    await Ship.run(runEffect, store, eyeControl());
    expect(store.getState()).toMatchSnapshot();
  });
});
<|endoftext|>