<commit_before>//mapManager prototype. Currently only used for reflections.

var Fiber = Npm.require('fibers');
var readline  = Npm.require('readline');
var fs = Npm.require('fs');
var Future = Npm.require('fibers/future');

var MapManager = require('./mapManager');
var PythonCall = require('./pythonCall');

var ManagerFileCabinet = new Mongo.Collection('ManagerFileCabinet');
var ManagerAddressBook = new Mongo.Collection('ManagerAddressBook');
var LayerPostOffice = new Mongo.Collection('LayerPostOffice');
var Windows = new Mongo.Collection('Windows');
var Path = Npm.require('path');

function read_nodenames(managers_doc,callback) {
    //function for reading the nodeIds (both feature and sample) from a
    // reflection datafile

    var filename = Path.join(FEATURE_SPACE_DIR, managers_doc.mapId.split(('/'))[0] + '/' + managers_doc.datapath);

    var node_names = [];
    var first = true;

    fs.stat(filename,function(err,stats) {
        if (err) {
            //console.log(err);
            return ;
        }
        else if (stats.isFile() ){
            var featOrSamp = managers_doc.featOrSamp;

            var rl = readline.createInterface({
                input : fs.createReadStream(filename),
                terminal: false
            });

            //if we are dealing with sample nodes we only read the header,
            // if dealing with features need to grab first element after first line
            if(featOrSamp === 'feature') {
                rl.on('line', function (line) {
                    if(!first) {
                        //console.log(line.split('\t')[0])
                        node_names.push(line.split('\t')[0]);
                    } else {
                        first = false;
                    }
                });
            } else if (featOrSamp === 'sample') {
                rl.on('line', function (line) {
                    //console.log('typeOfline:',typeof(line));
                    node_names = line.split('\t').splice(1);
                    //console.log('node_names',node_names);
                    rl.close();
                });
            }
            //after we read stuff in we put it in the database (callback should do that)
            rl.on('close',function() {
                callback(managers_doc,node_names);
            });
        }
        else {
            callback(managers_doc,[])
        }
    });

}

function insertNodeNames(doc,node_names){
    //inserts a list of nodes into the proper FileCabinet entry
    new Fiber( function () {
            ManagerFileCabinet.update(doc, {
                $set: {
                    available_nodes: node_names
                }
            });
    }).run();
}

function initManagerHelper() {
    //This function initializes the databases needed for map attribute transfer.
    // Initialized by reading from Meteor's settings.json file
    // erases old db entries and starts fresh everytime the server is booted

    //remove old dbs
    Windows.remove({});
    ManagerAddressBook.remove({});
    ManagerFileCabinet.remove({});
    LayerPostOffice.remove({});

    //insert ManangerAddressBook entries
    var addyentries =
        Meteor.settings.server.mapManagerHelper.ManagerAddressBook;

    _.each(addyentries,function(entry){
        ManagerAddressBook.insert(entry);
        //console.log(entry);
        //read_nodenames(entry,insertNodeNames);
    });

    //insert ManagerFileCabinet entries
    var cabinentEntries =
        Meteor.settings.server.mapManagerHelper.ManagerFileCabinet;

    _.each(cabinentEntries,function(entry){
        ManagerFileCabinet.insert(entry);
        //console.log(entry.datapath);
        //console.log(FEATURE_SPACE_DIR)
        read_nodenames(entry,insertNodeNames);
    });
}

//The following are helper functions specific to reflection functionality
function colorMapMaker(){
    //TODO: make a more flexible colorMapMaker (this one only for reflections)
    var newColorMap = [
        //order needs to be preserved because index needs to be equal to the
        // category
        {name:'low',color:'#32cd32',fileColor:'#32cd32'},
        {name:'middle',color:'#737373',fileColor:'#737373'},
        {name:'high',color:'#ce2029',fileColor:'#ce2029'}
                  ];
    return newColorMap;
}

function layerMaker(layer_name){
    var newLayer={};
    newLayer.timestamp = Math.floor(Date.now()/1000); //UTC timestamp in seconds
    newLayer.layer_name = layer_name;
    newLayer.selection  = true;
    newLayer.n          = 300;
    newLayer.magnitude = 3;//TODO: should this be 2?
    return newLayer;
}

function arrayLayer(layerData){

    //makes parallel arrays out of layerData
    var Json = layerData.data;
    var nodes = [];
    var vals = [];

    for (var node in Json){
        nodes.push(node);
        vals.push(Json[node]);
    }

    return {node_ids: nodes, values: vals};

}

function make_layer_name_unique (new_layer_name,old_layer_names) {

    // We're done if the name is unique
    if (old_layer_names.indexOf(new_layer_name) === -1) {
        return new_layer_name;
    }

    var last_suffix,
        name = new_layer_name,
        seq = 1;

    // Keep looking for a name until it is unique
    while (true) {

        // We're done if the name is unique
        if (old_layer_names.indexOf(name) === -1) { break; }

        // Find any previously tried sequence suffix
        if (seq > 1) {
            last_suffix = ' ' + (seq - 1);
            if (name.endsWith(last_suffix)) {

                // Remove the existing sequence suffix
                name = name.slice(0, name.length - last_suffix.length);
            }
        }
        name += ' ' + seq;
        seq += 1;
    }
    return name;
}
////////////////////

function dropInLayerBox(layerData,user,toMapId){
    //function adds reflection computation to DB
    new Fiber( function (){
        //console.log("Reflection: dropInLayerBox Called with user, layer_name",
        //             user, layerData.layer_name);

        layerData.data = arrayLayer(layerData);
        var old_layer_names = [];
        _.each(LayerPostOffice.findOne({user:user,toMapId:toMapId}).layers,
               function(layer){
                  old_layer_names.push(layer.layer_name);
               });

        //console.log('dropInLayerBox: old_layer_names:', old_layer_names);

        layerData.layer_name =
            make_layer_name_unique(layerData.layer_name,old_layer_names);

        //console.log('dropInLayerBox: layerData.layer_name:', layerData.layer_name);

        var rc = LayerPostOffice.update({user:user,toMapId:toMapId},
                               {$set: {lastChange: 'inserted'},
                                $push: {layers : layerData}});

        //console.log('dropInLayerBox: rc:', rc);

    }).run();
}

function parmMaker(mapId,toMapId, operation,argsObj) {
    //function that accesses the ManagerFileCabinet DB in order to produce
    // the correcct parmameter Json object for the reflection python script.

    var scriptDoc =
        ManagerFileCabinet.findOne({operation: operation,
                                    datatype: argsObj.datatype,
                                    mapId: mapId,
                                    toMapId: toMapId
                                    });
    //console.log("parmMaker being called for mapManager" +
    //            "/reflection with these two maps:", mapId,toMapId);

    var parm = {};
    //TODO: make cleaner by using precedence and writing over if necessary (?)
    //go through the necessary arguments
    _.each(scriptDoc.args, function (arg) {

        if ( scriptDoc[arg] ) {
            parm[arg] = scriptDoc[arg];
        } else if (argsObj[arg]) {
            parm[arg] = argsObj[arg];
        } else {
            //complain like alot
        }
    });

    //go through optional arguments
    _.each(scriptDoc.opts, function (opt) {

        if (argsObj[opt]) {
            parm[opt] = argsObj[opt];
        } else {
            //complain like a lot
        }
    });

    if(parm.datapath){
        //TODO: this may cause a problem if you are reflecting outside
        // TODO: of the major directory, for example
        // TODO: cont: geneMap -> geneMap instance.
        // TODO: Will need a refactoring of the databases to make this smooth
        parm.datapath = Path.join(FEATURE_SPACE_DIR,
                        mapId.split('/')[0] +
                        '/' +
                        parm.datapath);
    }

    return parm;

}

exports.reflection_post_calc = function (result, context) {

    // Process the results of the reflection request where:
    // result: { statusCode: <http-statusCode>, data: <result-data> }

    var newLayer = context.post_calc_parms.newLayer,
        userId = context.post_calc_parms.userId,
        toMapId = context.post_calc_parms.toMapId;

    //console.log('reflection_post_calc: newLayer, userId, toMapId:',
    //    newLayer, userId, toMapId);
    //console.log('context.post_calc_parms:', context.post_calc_parms);

    // Report any errors
    if (result.statusCode !== 200) {
        PythonCall.report_calc_result (result, context);
        return;
    }
    newLayer.data = context.js_result;

    dropInLayerBox(newLayer, userId, toMapId);

    PythonCall.report_calc_result(result, context);
}

Meteor.methods({

    // For calling python functions from the client
    mapManager: function (operation,
                          dataType,
                          userId,
                          mapId,
                          toMapId,
                          nodeIds,
                          selectionSelected) {
        //console.log(Meteor.userId());
        this.unblock();
        var post_calc_parms = {
                newLayer: layerMaker(selectionSelected+'_' +dataType+ '_Reflect'),
            };
        post_calc_parms.newLayer.colormap = colorMapMaker();

        var ctx = {
            post_calc_parms,
            future: new Future(),
        };

        if ( operation === 'reflection' ) {

            //load parameters specific to reflection python script
            var userArgs = {node_ids : nodeIds, datatype : dataType};
            var parameters = parmMaker(mapId,toMapId, operation, userArgs);

            // Save some values need in the post-calculation function
            ctx.post_calc = MapManager.reflection_post_calc;
            post_calc_parms.userId = userId;
            post_calc_parms.toMapId = toMapId;

            //console.log('pre-pythonCall: userId, toMapId:', userId, toMapId);
            //console.log('- ctx:', ctx);

            PythonCall.call(operation, parameters, ctx);

        } else  {
            console.log('Incorrect toMapId input into mapManager');
        }

        return ctx.future.wait();
    }
});

/*
        var newLayer = layerMaker(selectionSelected+'_' +dataType+ '_Reflect');
        newLayer.colormap = colorMapMaker();

        if ( operation === 'reflection' ) {
            //load parameters specific to reflection python script
            var userArgs = {node_ids : nodeIds, datatype : dataType};
            var parameters = parmMaker(mapId,toMapId, operation, userArgs);
            //console.log("mapManager calling python with:",parameters);

            ctx. post_calc: MapManager.reflection_post_calc,
               newLayer: newLayer,
               userId: userId,
               toMapId: toMapId,
            }

            // Success, so call the python function
            PythonCall.call(operation, parameters, context);

        } else  {
            console.log('Incorrect toMapId input into mapManager');
        }
        return ctx.future.wait();
    }
});
*/

//subscribe is in checkLayerBox.js
Meteor.publish('userLayerBox', function(userId, currMapId) {
    if(!this.userId) { return this.ready(); }

    //If layerbox is empty put something in there
    if( ! LayerPostOffice.findOne({user: userId, toMapId: currMapId}) ) {
        //console.log('mapManager: No layerbox found, making empty entry');
        var emptyLayers=[];
        LayerPostOffice.insert({user: userId,
                                toMapId: currMapId,
                                layers: emptyLayers,
                                lastChange:"created"
                                });
    }

    var LayerBoxCursor = LayerPostOffice.find({user: userId,
                                               toMapId: currMapId
                                               });
    //always return Cursor from publish
    return LayerBoxCursor;
});

//Helper function for Windows database
function getWindowCount(WindowsDoc,mapId){
    /*
    Input is windows document from the Windows collection.
    Returns how many windows are open for a given mapId
     */
    var count = 0;

    if (WindowsDoc) {
        _.each(WindowsDoc.maps, function (val) {
            if (val.mapId === mapId) { //flip switch and return
                count = val.count;
            }
        });
    }

    return count;

}

function initPublishers () {

    //The following 2 publish functions are for manipulating the Windows database.
    // the windows database is
    // a state database that keeps track of how many and which windows
    // are opened by a client
    // We keep track of this so that the manager can open a new window if desired
    Meteor.publish('OpenedWindow', function(userId,mapId) {
        //prevents update if user isn't signed in, or not on defined map
        if(!this.userId || ! userId || ! mapId) { return this.stop(); }

        //if we don't have a window open then make an entry
        if( !Windows.findOne({user: userId, "maps.mapId": mapId}) ) {
            Windows.upsert({user: userId},
                           {$push :
                               { maps :
                                   {mapId: mapId, count : 1 } }} );
        } else { //otherwise update
            Windows.update( {user: userId,"maps.mapId":mapId},
                            {$inc : {"maps.$.count": 1} } );
        }
        //console.log('user',userId, 'has just opened a window for', mapId);
        return this.stop();
    });

    Meteor.publish('ClosedWindow', function(userId,mapId) {

        //Decrement count, and make sure it is never negative
        if (getWindowCount(
                Windows.findOne({user: userId,"maps.mapId":mapId}),
                mapId
                ) > 0) {
            Windows.update({user: userId,"maps.mapId":mapId},
                           {$inc : {"maps.$.count": -1} } );
        }

        return this.stop();
    });

    //deletes a layer (specified by layer_name)
    // from a (userId, mapId) LayerBox entry
    Meteor.publish('deleteLayer',function(userId,mapId,layer_name) {
        // if not logged in function won't do anything
        if(!this.userId) { return this.stop(); }

        LayerPostOffice.update({user: userId, toMapId: mapId},
                               {$set :
                                  { lastChange: "removed"},
                                   $pull:
                                      {layers: {layer_name: layer_name}} });
    });

    //Gives client access to the address database
    // so that user knows where they can 'reflect' to.
    Meteor.publish('reflectionToMapIds', function(currMapId) {
        //console.log('publish ManagerAddressBook currMapId:', currMapId);
        return ManagerAddressBook.find({mapId: currMapId, operation: 'reflection'});
    });

    Meteor.publish('ManagerFileCabinet',function(mapId) {
        //publishes a subset of the fileCabinet, mainly so the client is aware of
        // nodes reflection is available to

        // if not logged in function won't do anything
        if(!this.userId) { return this.stop(); }

        return ManagerFileCabinet.find({mapId: mapId},{fields:{mapId:1,available_nodes:1, datatype:1,toMapId:1,operation:1}});

    });
}

Meteor.methods({
    isWindowOpen: function (userId, mapId) {
        this.unblock();
        var future = new Future();

        var userWindowsDoc = Windows.findOne({user: userId,
                                              "maps.mapId": mapId});

        future.return(getWindowCount(userWindowsDoc,mapId) !== 0);

        return future.wait();
    }
});

//populate the helper database from settings.json file
initManagerHelper();

initPublishers();

//end Windows collection manipulators
<commit_msg>userLayerBox publish function makes entries for possible target maps of reflection.<commit_after>//mapManager prototype. Currently only used for reflections.

var Fiber = Npm.require('fibers');
var readline  = Npm.require('readline');
var fs = Npm.require('fs');
var Future = Npm.require('fibers/future');

var MapManager = require('./mapManager');
var PythonCall = require('./pythonCall');

var ManagerFileCabinet = new Mongo.Collection('ManagerFileCabinet');
var ManagerAddressBook = new Mongo.Collection('ManagerAddressBook');
var LayerPostOffice = new Mongo.Collection('LayerPostOffice');
var Windows = new Mongo.Collection('Windows');
var Path = Npm.require('path');

function read_nodenames(managers_doc,callback) {
    //function for reading the nodeIds (both feature and sample) from a
    // reflection datafile

    var filename = Path.join(FEATURE_SPACE_DIR, managers_doc.mapId.split(('/'))[0] + '/' + managers_doc.datapath);

    var node_names = [];
    var first = true;

    fs.stat(filename,function(err,stats) {
        if (err) {
            //console.log(err);
            return ;
        }
        else if (stats.isFile() ){
            var featOrSamp = managers_doc.featOrSamp;

            var rl = readline.createInterface({
                input : fs.createReadStream(filename),
                terminal: false
            });

            //if we are dealing with sample nodes we only read the header,
            // if dealing with features need to grab first element after first line
            if(featOrSamp === 'feature') {
                rl.on('line', function (line) {
                    if(!first) {
                        //console.log(line.split('\t')[0])
                        node_names.push(line.split('\t')[0]);
                    } else {
                        first = false;
                    }
                });
            } else if (featOrSamp === 'sample') {
                rl.on('line', function (line) {
                    //console.log('typeOfline:',typeof(line));
                    node_names = line.split('\t').splice(1);
                    //console.log('node_names',node_names);
                    rl.close();
                });
            }
            //after we read stuff in we put it in the database (callback should do that)
            rl.on('close',function() {
                callback(managers_doc,node_names);
            });
        }
        else {
            callback(managers_doc,[])
        }
    });

}

function insertNodeNames(doc,node_names){
    //inserts a list of nodes into the proper FileCabinet entry
    new Fiber( function () {
            ManagerFileCabinet.update(doc, {
                $set: {
                    available_nodes: node_names
                }
            });
    }).run();
}

function initManagerHelper() {
    //This function initializes the databases needed for map attribute transfer.
    // Initialized by reading from Meteor's settings.json file
    // erases old db entries and starts fresh everytime the server is booted

    //remove old dbs
    Windows.remove({});
    ManagerAddressBook.remove({});
    ManagerFileCabinet.remove({});
    LayerPostOffice.remove({});

    //insert ManangerAddressBook entries
    var addyentries =
        Meteor.settings.server.mapManagerHelper.ManagerAddressBook;

    _.each(addyentries,function(entry){
        ManagerAddressBook.insert(entry);
        //console.log(entry);
        //read_nodenames(entry,insertNodeNames);
    });

    //insert ManagerFileCabinet entries
    var cabinentEntries =
        Meteor.settings.server.mapManagerHelper.ManagerFileCabinet;

    _.each(cabinentEntries,function(entry){
        ManagerFileCabinet.insert(entry);
        //console.log(entry.datapath);
        //console.log(FEATURE_SPACE_DIR)
        read_nodenames(entry,insertNodeNames);
    });
}

//The following are helper functions specific to reflection functionality
function colorMapMaker(){
    //TODO: make a more flexible colorMapMaker (this one only for reflections)
    var newColorMap = [
        //order needs to be preserved because index needs to be equal to the
        // category
        {name:'low',color:'#32cd32',fileColor:'#32cd32'},
        {name:'middle',color:'#737373',fileColor:'#737373'},
        {name:'high',color:'#ce2029',fileColor:'#ce2029'}
                  ];
    return newColorMap;
}

function layerMaker(layer_name){
    var newLayer={};
    newLayer.timestamp = Math.floor(Date.now()/1000); //UTC timestamp in seconds
    newLayer.layer_name = layer_name;
    newLayer.selection  = true;
    newLayer.n          = 300;
    newLayer.magnitude = 3;//TODO: should this be 2?
    return newLayer;
}

function arrayLayer(layerData){

    //makes parallel arrays out of layerData
    var Json = layerData.data;
    var nodes = [];
    var vals = [];

    for (var node in Json){
        nodes.push(node);
        vals.push(Json[node]);
    }

    return {node_ids: nodes, values: vals};

}

function make_layer_name_unique (new_layer_name,old_layer_names) {

    // We're done if the name is unique
    if (old_layer_names.indexOf(new_layer_name) === -1) {
        return new_layer_name;
    }

    var last_suffix,
        name = new_layer_name,
        seq = 1;

    // Keep looking for a name until it is unique
    while (true) {

        // We're done if the name is unique
        if (old_layer_names.indexOf(name) === -1) { break; }

        // Find any previously tried sequence suffix
        if (seq > 1) {
            last_suffix = ' ' + (seq - 1);
            if (name.endsWith(last_suffix)) {

                // Remove the existing sequence suffix
                name = name.slice(0, name.length - last_suffix.length);
            }
        }
        name += ' ' + seq;
        seq += 1;
    }
    return name;
}
////////////////////

function dropInLayerBox(layerData,user,toMapId){
    //function adds reflection computation to DB
    new Fiber( function (){
        //console.log("Reflection: dropInLayerBox Called with user, layer_name",
        //             user, layerData.layer_name);

        layerData.data = arrayLayer(layerData);
        var old_layer_names = [];

        console.log("dropInLayerbox user and toMapId", user, toMapId)

        _.each(LayerPostOffice.findOne({user:user,toMapId:toMapId}).layers,
               function(layer){
                  old_layer_names.push(layer.layer_name);
               });

        //console.log('dropInLayerBox: old_layer_names:', old_layer_names);

        layerData.layer_name =
            make_layer_name_unique(layerData.layer_name,old_layer_names);

        //console.log('dropInLayerBox: layerData.layer_name:', layerData.layer_name);

        var rc = LayerPostOffice.update({user:user,toMapId:toMapId},
                               {$set: {lastChange: 'inserted'},
                                $push: {layers : layerData}});

        //console.log('dropInLayerBox: rc:', rc);

    }).run();
}

function parmMaker(mapId,toMapId, operation,argsObj) {
    //function that accesses the ManagerFileCabinet DB in order to produce
    // the correcct parmameter Json object for the reflection python script.

    var scriptDoc =
        ManagerFileCabinet.findOne({operation: operation,
                                    datatype: argsObj.datatype,
                                    mapId: mapId,
                                    toMapId: toMapId
                                    });
    //console.log("parmMaker being called for mapManager" +
    //            "/reflection with these two maps:", mapId,toMapId);

    var parm = {};
    //TODO: make cleaner by using precedence and writing over if necessary (?)
    //go through the necessary arguments
    _.each(scriptDoc.args, function (arg) {

        if ( scriptDoc[arg] ) {
            parm[arg] = scriptDoc[arg];
        } else if (argsObj[arg]) {
            parm[arg] = argsObj[arg];
        } else {
            //complain like alot
        }
    });

    //go through optional arguments
    _.each(scriptDoc.opts, function (opt) {

        if (argsObj[opt]) {
            parm[opt] = argsObj[opt];
        } else {
            //complain like a lot
        }
    });

    if(parm.datapath){
        //TODO: this may cause a problem if you are reflecting outside
        // TODO: of the major directory, for example
        // TODO: cont: geneMap -> geneMap instance.
        // TODO: Will need a refactoring of the databases to make this smooth
        parm.datapath = Path.join(FEATURE_SPACE_DIR,
                        mapId.split('/')[0] +
                        '/' +
                        parm.datapath);
    }

    return parm;

}

exports.reflection_post_calc = function (result, context) {

    // Process the results of the reflection request where:
    // result: { statusCode: <http-statusCode>, data: <result-data> }

    var newLayer = context.post_calc_parms.newLayer,
        userId = context.post_calc_parms.userId,
        toMapId = context.post_calc_parms.toMapId;

    //console.log('reflection_post_calc: newLayer, userId, toMapId:',
    //    newLayer, userId, toMapId);
    //console.log('context.post_calc_parms:', context.post_calc_parms);

    // Report any errors
    if (result.statusCode !== 200) {
        PythonCall.report_calc_result (result, context);
        return;
    }
    newLayer.data = context.js_result;

    dropInLayerBox(newLayer, userId, toMapId);

    PythonCall.report_calc_result(result, context);
}

Meteor.methods({

    // For calling python functions from the client
    mapManager: function (operation,
                          dataType,
                          userId,
                          mapId,
                          toMapId,
                          nodeIds,
                          selectionSelected) {
        //console.log(Meteor.userId());
        this.unblock();
        var post_calc_parms = {
                newLayer: layerMaker(selectionSelected+'_' +dataType+ '_Reflect'),
            };
        post_calc_parms.newLayer.colormap = colorMapMaker();

        var ctx = {
            post_calc_parms,
            future: new Future(),
        };

        if ( operation === 'reflection' ) {

            //load parameters specific to reflection python script
            var userArgs = {node_ids : nodeIds, datatype : dataType};
            var parameters = parmMaker(mapId,toMapId, operation, userArgs);

            // Save some values need in the post-calculation function
            ctx.post_calc = MapManager.reflection_post_calc;
            post_calc_parms.userId = userId;
            post_calc_parms.toMapId = toMapId;

            //console.log('pre-pythonCall: userId, toMapId:', userId, toMapId);
            //console.log('- ctx:', ctx);

            PythonCall.call(operation, parameters, ctx);

        } else  {
            console.log('Incorrect toMapId input into mapManager');
        }

        return ctx.future.wait();
    }
});

/*
        var newLayer = layerMaker(selectionSelected+'_' +dataType+ '_Reflect');
        newLayer.colormap = colorMapMaker();

        if ( operation === 'reflection' ) {
            //load parameters specific to reflection python script
            var userArgs = {node_ids : nodeIds, datatype : dataType};
            var parameters = parmMaker(mapId,toMapId, operation, userArgs);
            //console.log("mapManager calling python with:",parameters);

            ctx. post_calc: MapManager.reflection_post_calc,
               newLayer: newLayer,
               userId: userId,
               toMapId: toMapId,
            }

            // Success, so call the python function
            PythonCall.call(operation, parameters, context);

        } else  {
            console.log('Incorrect toMapId input into mapManager');
        }
        return ctx.future.wait();
    }
});
*/

//subscribe is in checkLayerBox.js
Meteor.publish('userLayerBox', function(userId, currMapId) {
    if(!this.userId) { return this.ready(); }

    // For each map id the user could reflect to, check and see if the
    // user has a layer box for that map, if not then make one.

    // Get the entry in the data base holding the array of map ids reflection
    // is possible for.
    var reflectionAddress =
        ManagerAddressBook.findOne({mapId: currMapId, operation: 'reflection'});

    // Empty layers for new entries in the database.
    var emptyLayers=[];

    // Grab the array of mapIds and if there is not a database entry for the
    // user with a mapId, make one.
    if (reflectionAddress) {
        var toMapIds = reflectionAddress.toMapIds;
        _.each(toMapIds, function (toMapId) {
            //If layerbox is empty put something in there
            if (!LayerPostOffice.findOne({user: userId, toMapId: toMapId})) {
                LayerPostOffice.insert({
                    user: userId,
                    toMapId: toMapId,
                    layers: emptyLayers,
                    lastChange: "created"
                });
            }
        })
    }

    // Make sure there is also an entry for the map the user is currently on.
    if (!LayerPostOffice.findOne({user: userId, toMapId: currMapId})) {
        LayerPostOffice.insert({
            user: userId,
            toMapId: currMapId,
            layers: emptyLayers,
            lastChange: "created"
        });
    }

    // Publish the current maps's layer entry.
    var LayerBoxCursor = LayerPostOffice.find({user: userId,
                                               toMapId: currMapId
                                               });
    // Always return Cursor from publish
    return LayerBoxCursor;
});

//Helper function for Windows database
function getWindowCount(WindowsDoc,mapId){
    /*
    Input is windows document from the Windows collection.
    Returns how many windows are open for a given mapId
     */
    var count = 0;

    if (WindowsDoc) {
        _.each(WindowsDoc.maps, function (val) {
            if (val.mapId === mapId) { //flip switch and return
                count = val.count;
            }
        });
    }

    return count;

}

function initPublishers () {

    //The following 2 publish functions are for manipulating the Windows database.
    // the windows database is
    // a state database that keeps track of how many and which windows
    // are opened by a client
    // We keep track of this so that the manager can open a new window if desired
    Meteor.publish('OpenedWindow', function(userId,mapId) {
        //prevents update if user isn't signed in, or not on defined map
        if(!this.userId || ! userId || ! mapId) { return this.stop(); }

        //if we don't have a window open then make an entry
        if( !Windows.findOne({user: userId, "maps.mapId": mapId}) ) {
            Windows.upsert({user: userId},
                           {$push :
                               { maps :
                                   {mapId: mapId, count : 1 } }} );
        } else { //otherwise update
            Windows.update( {user: userId,"maps.mapId":mapId},
                            {$inc : {"maps.$.count": 1} } );
        }
        //console.log('user',userId, 'has just opened a window for', mapId);
        return this.stop();
    });

    Meteor.publish('ClosedWindow', function(userId,mapId) {

        //Decrement count, and make sure it is never negative
        if (getWindowCount(
                Windows.findOne({user: userId,"maps.mapId":mapId}),
                mapId
                ) > 0) {
            Windows.update({user: userId,"maps.mapId":mapId},
                           {$inc : {"maps.$.count": -1} } );
        }

        return this.stop();
    });

    //deletes a layer (specified by layer_name)
    // from a (userId, mapId) LayerBox entry
    Meteor.publish('deleteLayer',function(userId,mapId,layer_name) {
        // if not logged in function won't do anything
        if(!this.userId) { return this.stop(); }

        LayerPostOffice.update({user: userId, toMapId: mapId},
                               {$set :
                                  { lastChange: "removed"},
                                   $pull:
                                      {layers: {layer_name: layer_name}} });
    });

    //Gives client access to the address database
    // so that user knows where they can 'reflect' to.
    Meteor.publish('reflectionToMapIds', function(currMapId) {
        //console.log('publish ManagerAddressBook currMapId:', currMapId);
        return ManagerAddressBook.find({mapId: currMapId, operation: 'reflection'});
    });

    Meteor.publish('ManagerFileCabinet',function(mapId) {
        //publishes a subset of the fileCabinet, mainly so the client is aware of
        // nodes reflection is available to

        // if not logged in function won't do anything
        if(!this.userId) { return this.stop(); }

        return ManagerFileCabinet.find({mapId: mapId},{fields:{mapId:1,available_nodes:1, datatype:1,toMapId:1,operation:1}});

    });
}

Meteor.methods({
    isWindowOpen: function (userId, mapId) {
        this.unblock();
        var future = new Future();

        var userWindowsDoc = Windows.findOne({user: userId,
                                              "maps.mapId": mapId});

        future.return(getWindowCount(userWindowsDoc,mapId) !== 0);

        return future.wait();
    }
});

//populate the helper database from settings.json file
initManagerHelper();

initPublishers();

//end Windows collection manipulators
<|endoftext|>


<commit_before>define(['d3', 'lib/knockout', 'scripts/Utils', 'dagre-d3', 'jquery', 'lettuce', 'text!templates/description.html', 'scripts/Node', 'jquery.tooltipster'],
  function (d3, ko, Utils, dagreD3, $, Lettuce, description_template, Node) {
    'use strict';
    function renderPage(skills_data) {
      function setSkillNode() {
        ko.utils.arrayForEach(skills_data.skills, function (skill) {
          var value = skill;
          value.label = skill.title;
          value.height = value.width = 40;
          value.rx = value.ry = 10;
          g.setNode(skill.title, value);
        });
      }

      function setSkillEdge() {
        ko.utils.arrayForEach(skills_data.skills, function (skill) {
          var skill_id = skill.id;
          if (skill.depends) {
            ko.utils.arrayForEach(skill.depends, function (id) {
              var dependents_name = Utils.getSkillById(skills_data.skills, id).title;
              var skill_name = Utils.getSkillById(skills_data.skills, skill_id).title;
              g.setEdge(dependents_name, skill_name, {label: '', lineInterpolate: 'basis'});
            });
          }
        });
      }

      var lettuce = new Lettuce();
      var g = new dagreD3.graphlib.Graph().setGraph({});
      setSkillNode();
      setSkillEdge();

      var render = new dagreD3.render();
      var svg = d3.select('svg');
      var inner = svg.append('g');
      var url_hash = [];

      render(inner, g);

      var vm = new Node(g._nodes);
      ko.applyBindings(vm);

      inner.selectAll('rect')
        .attr('class', 'inner')
        .on('click', function (d, i) {
          if(vm.skills()[i].canAddPoints()){
            url_hash.push(String.fromCharCode(i + 96));
            if (vm.skills()[i].hasMultiplePoints()) {
              url_hash.push(vm.skills()[i].points());
            }
          }
          console.log(url_hash);
          if(vm.skills()[i].canAddPoints() ){
            vm.skills()[i].addPoint(1);
            d3.select(this).style('opacity', '0.7');
            d3.select(this).style('fill', '#27ae60');
          }
          if(vm.skills()[i].hasMaxPoints()){
            d3.select(this).style('fill', '#009688');
          }
        });

      inner.selectAll('g.node')
        .attr('data-bind', function(){
          return 'css: { "can-add-points": canAddPoints, "has-points": hasPoints, "has-max-points": hasMaxPoints }';
        });
      inner.selectAll('g.node rect').attr('data-bind', function () {
        return 'click: addPoint, rightClick: removePoint';
      });

      //console.log(g.node('HTML'));

      /* add tips */
      inner.selectAll('g.node')
        .each(function (v, id) {

          g.node(v).books = Utils.handleEmptyDocs(g.node(v).books);
          g.node(v).links = Utils.handleEmptyDocs(g.node(v).links);

          var data = {
            id: id,
            name: v,
            description: g.node(v).description,
            books: g.node(v).books,
            links: g.node(v).links
          };
          var results = lettuce.Template.tmpl(description_template, data);

          $(this).tooltipster({
            content: $(results),
            contentAsHTML: true,
            position: 'left',
            animation: 'grow',
            interactive: true});
          $(this).find('rect').css('fill', '#ecf0f1');
        });

      svg.attr('height', g.graph().height + 120);
    }

    return {
      renderPage: renderPage
    };
  });
<commit_msg>move url generate on click event<commit_after>define(['d3', 'lib/knockout', 'scripts/Utils', 'dagre-d3', 'jquery', 'lettuce', 'text!templates/description.html', 'scripts/Node', 'jquery.tooltipster'],
  function (d3, ko, Utils, dagreD3, $, Lettuce, description_template, Node) {
    'use strict';
    function renderPage(skills_data) {
      function setSkillNode() {
        ko.utils.arrayForEach(skills_data.skills, function (skill) {
          var value = skill;
          value.label = skill.title;
          value.height = value.width = 40;
          value.rx = value.ry = 10;
          g.setNode(skill.title, value);
        });
      }

      function setSkillEdge() {
        ko.utils.arrayForEach(skills_data.skills, function (skill) {
          var skill_id = skill.id;
          if (skill.depends) {
            ko.utils.arrayForEach(skill.depends, function (id) {
              var dependents_name = Utils.getSkillById(skills_data.skills, id).title;
              var skill_name = Utils.getSkillById(skills_data.skills, skill_id).title;
              g.setEdge(dependents_name, skill_name, {label: '', lineInterpolate: 'basis'});
            });
          }
        });
      }

      var lettuce = new Lettuce();
      var g = new dagreD3.graphlib.Graph().setGraph({});
      setSkillNode();
      setSkillEdge();

      var render = new dagreD3.render();
      var svg = d3.select('svg');
      var inner = svg.append('g');
      var url_hash = [];

      render(inner, g);

      var vm = new Node(g._nodes);
      ko.applyBindings(vm);

      inner.selectAll('rect')
        .attr('class', 'inner')
        .on('click', function (d, i) {
		      var skill = vm.skills()[i];

          if(skill.canAddPoints() ){
            skill.addPoint(1);
            if (skill.hasPoints()) {
              url_hash.push(String.fromCharCode(i + 96));
              if (skill.hasMultiplePoints()) {
                url_hash.push(skill.points());
              }
            }
            d3.select(this).style('opacity', '0.7');
            d3.select(this).style('fill', '#27ae60');
          }
          if(skill.hasMaxPoints()){
            d3.select(this).style('fill', '#009688');
          }
          console.log(url_hash);
        });

      inner.selectAll('g.node')
        .attr('data-bind', function(){
          return 'css: { "can-add-points": canAddPoints, "has-points": hasPoints, "has-max-points": hasMaxPoints }';
        });
      inner.selectAll('g.node rect').attr('data-bind', function () {
        return 'click: addPoint, rightClick: removePoint';
      });

      //console.log(g.node('HTML'));

      /* add tips */
      inner.selectAll('g.node')
        .each(function (v, id) {

          g.node(v).books = Utils.handleEmptyDocs(g.node(v).books);
          g.node(v).links = Utils.handleEmptyDocs(g.node(v).links);

          var data = {
            id: id,
            name: v,
            description: g.node(v).description,
            books: g.node(v).books,
            links: g.node(v).links
          };
          var results = lettuce.Template.tmpl(description_template, data);

          $(this).tooltipster({
            content: $(results),
            contentAsHTML: true,
            position: 'left',
            animation: 'grow',
            interactive: true});
          $(this).find('rect').css('fill', '#ecf0f1');
        });

      svg.attr('height', g.graph().height + 120);
    }

    return {
      renderPage: renderPage
    };
  });
<|endoftext|>


<commit_before>import ControlKit from "controlkit";

// STYLES

let style = document.createElement("style");
document.head.appendChild(style);

style.sheet.insertRule(`
  #controlKit .panel .group-list .group .sub-group-list .sub-group .wrap .label {
    width: 40% !important;
  }
`, 0);

style.sheet.insertRule(`
  #controlKit .panel .group-list .group .sub-group-list .sub-group .wrap .wrap {
    width: 60% !important;
  }
`, 0);

style.sheet.insertRule(`
  #controlKit .panel .wrap-slider {
    width: 60% !important;
  }
`, 0);

style.sheet.insertRule(`
  #controlKit .options ul {
    max-height: 300px !important;
    overflow-y: scroll !important;
    overflow-x: hidden !important;
  }
`, 0);

// UTILS

function componentToHex(c) {
  let hex = c.toString(16);
  return hex.length == 1 ? "0" + hex : hex;
}

function rgbToHex(r, g, b) {
  return "#" + componentToHex(r * 255) + componentToHex(g * 255) + componentToHex(b * 255);
}

function hexToRgb(hex) {
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16) / 255,
    g: parseInt(result[2], 16) / 255,
    b: parseInt(result[3], 16) / 255
  } : null;
}

function colorFromHex(color, hex) {
  let colorValue = hexToRgb(hex);

  if (color.r !== undefined) {
    Object.assign(color, colorValue);
  } else if (color.x !== undefined) {
    [color.x, color.y, color.z] = [colorValue.r, colorValue.g, colorValue.b];
  } else {
    [color[0], color[1], color[2]] = [colorValue.r, colorValue.g, colorValue.b];
  }

  return color;
}

function colorToHex(color) {
  return rgbToHex(
    color.r !== undefined ? color.r : color.x !== undefined ? color.x : color[0],
    color.g !== undefined ? color.g : color.y !== undefined ? color.y : color[1],
    color.b !== undefined ? color.b : color.z !== undefined ? color.z : color[2]
  );
}

function normalizeString(string) {
  return `${string.toLowerCase().replace(/[^\w-]/g, "")}`;
}

function urlHashRegExpFromKey(key) {
  return new RegExp(`([#&]gui/${key}=)([^=&#?]*)`, "g");
}

const COMPONENTS = [];

// GUI

const GUI_REG_EXP = /([#&]gui=)({.*})([&?]*)/;

let DATA;
(function() {
  let matches = GUI_REG_EXP.exec(window.location.hash);
  DATA = matches ? JSON.parse(matches[2]) : {};
})();

const CONTROL_KIT_CONTAINERS = new Map();

let positionOffset = 0;

class GUIComponent {
  constructor(object, key, type, controlKitComponent) {
    this._type = type;
    this._object = object;
    this._key = key;
    this._controlKitComponent = controlKitComponent;
  }
  get object() {
    return this._object;
  }
  get key() {
    return this._key;
  }
  get value() {
    return this.object[this.key];
  }
  get type() {
    return this._type;
  }
  get controlKitComponent() {
    return this._controlKitComponent;
  }
  remove() {
    this.controlKitComponent.disable();
    this.controlKitComponent._node._element.remove();
  }
}

class GUI {
  constructor() {
    this._controlKit = new ControlKit();
  }

  update() {
    for (let component of COMPONENTS) {
      switch (component.type) {
        case "color":
          component.controlKitComponent._obj._value = component.controlKitComponent._value = colorToHex(component.object[component.key]);
          component.controlKitComponent._updateColor();
          break;
      }
    }

    requestAnimationFrame(() => {
      this._controlKit.update();
    });
  }

  open(panel, group) {
    CONTROL_KIT_CONTAINERS.get(normalizeString(panel) + (group ? "/" + normalizeString(group) : "")).enable();
  }

  close(panel, group) {
    CONTROL_KIT_CONTAINERS.get(normalizeString(panel) + (group ? "/" + normalizeString(group) : "")).disable();
  }

  add(object, key, {type = typeof object[key], label = key, panel = "Main", group = "", reload = false, options, range, onChange} = {}) {
    let labelKey = normalizeString(label);

    let panelKey = normalizeString(panel);
    let controlKitPanel = CONTROL_KIT_CONTAINERS.get(panelKey);
    if(!controlKitPanel) {
      controlKitPanel = this._controlKit.addPanel({
        fixed: false,
        label: panel,
        width: 240,
        align: "left",
        position: [positionOffset, 0]
      });
      CONTROL_KIT_CONTAINERS.set(panelKey, controlKitPanel);
      positionOffset += 240;
    }

    let groupKey = normalizeString(group);
    let controlKitGroup = CONTROL_KIT_CONTAINERS.get(`${panelKey}/`);
    if(groupKey) {
      controlKitGroup = CONTROL_KIT_CONTAINERS.get(`${panelKey}/${groupKey}`);
      if(!controlKitGroup) {
        controlKitPanel.addGroup({
          label: group
        });
        controlKitGroup = controlKitPanel._groups[controlKitPanel._groups.length - 1];
        CONTROL_KIT_CONTAINERS.set(`${panelKey}/${groupKey}`, controlKitGroup);
      }
    }

    if(controlKitGroup) {
      controlKitPanel._groups.push(controlKitPanel._groups.splice(controlKitPanel._groups.indexOf(controlKitGroup), 1)[0]);
    }

    let savedValue;

    if (groupKey && DATA[panelKey] && DATA[panelKey][groupKey]) {
      savedValue = DATA[panelKey][groupKey][labelKey];
    } else if (DATA[panelKey]) {
      savedValue = DATA[panelKey][labelKey];
    }

    let changeValueData = (value = object[key]) => {
      let containerData = DATA[panelKey];
      if(!containerData) {
        containerData = DATA[panelKey] = {};
      }
      if(group) {
        let groupData = containerData[groupKey];
        if(!groupData) {
          groupData = {};
          containerData[groupKey] = groupData;
        }
        containerData = groupData;
      }

      containerData[labelKey] = value;

      if(GUI_REG_EXP.test(window.location.hash)) {
        window.location.hash = window.location.hash.replace(GUI_REG_EXP, `$1${JSON.stringify(DATA)}$3`);
      } else {
        let prefix = window.location.hash ? "&" : "#";
        window.location.hash += `${prefix}gui=${JSON.stringify(DATA)}`;
      }

      if(onChange) {
        onChange(object[key]);
      }

      if (reload) {
        window.location.reload();
      }
    }

    switch (type) {
      case "boolean":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addCheckbox(object, key, {
          onChange: changeValueData,
          label
        });
        break;
      case "number":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addNumberInput(object, key, {
          onChange: changeValueData,
          label
        });
        break;
      case "color":
        let color = {};
        if(savedValue !== undefined) {
          color.value = `#${savedValue}`;
          colorFromHex(object[key], color.value);
        } else {
          color.value = colorToHex(object[key]);
        }
        controlKitPanel.addColor(color, "value", {
          onChange: function(value) {
            colorFromHex(object[key], value);
            changeValueData(value.replace("#", ""));
          },
          label,
          colorMode: "hex"
        });
        break;
      case "select":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addSelect({
          options,
          selection: object[key]
        }, "options", {
          onChange: function(index) {
            object[key] = options[index];
            changeValueData(object[key]);
          },
          label
        });
        break;
      case "slider":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        let slider = {
          value: object[key],
          range: range || [0, 1]
        };
        controlKitPanel.addSlider(slider, "value", "range", {
          onChange: function(value) {
            object[key] = slider.value;
            changeValueData(slider.value);
          },
          label
        });
        break;
      case "xy":
        let xy = {};
        if(object[key].x !== undefined) {
          if(matches) {
            xy.value = savedValue;
            [object[key].x, object[key].y] = [xy.value[0], xy.value[1]];
          } else {
            xy.value = [object[key].x, object[key].y];
          }
        } else {
          object[key] = savedValue === undefined ?  object[key] : savedValue;
          xy.value = object[key];
        }
        controlKitPanel.addPad(xy, "value", {
          onChange: function() {
            if(object[key].x) {
              [object[key].x, object[key].y] = [xy.value[0], xy.value[1]];
            }
            changeValueData(xy.value);
          },
          label
        });
        break;
    }

    if(!CONTROL_KIT_CONTAINERS.get(`${panelKey}/`)) {
      CONTROL_KIT_CONTAINERS.set(`${panelKey}/`, controlKitPanel._groups[0]);
    }

    if(onChange) {
      onChange(object[key]);
    }

    if(!controlKitGroup) {
      controlKitGroup = controlKitPanel._groups[controlKitPanel._groups.length - 1];
    }
    let controlKitComponent = controlKitGroup._components[controlKitGroup._components.length - 1];

    let component = new GUIComponent(object, key, type, controlKitComponent);
    COMPONENTS.push(component)
    return component;
  }

  _changeURLValue(key, value) {
    let regExp = urlHashRegExpFromKey(key);
    let matches = regExp.exec(window.location.hash);
    if (!matches) {
      let prefix = window.location.hash ? "&" : "#";
      window.location.hash += `${prefix}gui-${key}=${value}`;
    } else {
      window.location.hash = window.location.hash.replace(matches[0], matches[0].replace(regExp, `$1${value}`));
    }
  }
}

export default new GUI();
<commit_msg>add string type to gui<commit_after>import ControlKit from "controlkit";

// STYLES

let style = document.createElement("style");
document.head.appendChild(style);

style.sheet.insertRule(`
  #controlKit .panel .group-list .group .sub-group-list .sub-group .wrap .label {
    width: 40% !important;
  }
`, 0);

style.sheet.insertRule(`
  #controlKit .panel .group-list .group .sub-group-list .sub-group .wrap .wrap {
    width: 60% !important;
  }
`, 0);

style.sheet.insertRule(`
  #controlKit .panel .wrap-slider {
    width: 60% !important;
  }
`, 0);

style.sheet.insertRule(`
  #controlKit .options ul {
    max-height: 300px !important;
    overflow-y: scroll !important;
    overflow-x: hidden !important;
  }
`, 0);

// UTILS

function componentToHex(c) {
  let hex = c.toString(16);
  return hex.length == 1 ? "0" + hex : hex;
}

function rgbToHex(r, g, b) {
  return "#" + componentToHex(r * 255) + componentToHex(g * 255) + componentToHex(b * 255);
}

function hexToRgb(hex) {
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16) / 255,
    g: parseInt(result[2], 16) / 255,
    b: parseInt(result[3], 16) / 255
  } : null;
}

function colorFromHex(color, hex) {
  let colorValue = hexToRgb(hex);

  if (color.r !== undefined) {
    Object.assign(color, colorValue);
  } else if (color.x !== undefined) {
    [color.x, color.y, color.z] = [colorValue.r, colorValue.g, colorValue.b];
  } else {
    [color[0], color[1], color[2]] = [colorValue.r, colorValue.g, colorValue.b];
  }

  return color;
}

function colorToHex(color) {
  return rgbToHex(
    color.r !== undefined ? color.r : color.x !== undefined ? color.x : color[0],
    color.g !== undefined ? color.g : color.y !== undefined ? color.y : color[1],
    color.b !== undefined ? color.b : color.z !== undefined ? color.z : color[2]
  );
}

function normalizeString(string) {
  return `${string.toLowerCase().replace(/[^\w-]/g, "")}`;
}

function urlHashRegExpFromKey(key) {
  return new RegExp(`([#&]gui/${key}=)([^=&#?]*)`, "g");
}

const COMPONENTS = [];

// GUI

const GUI_REG_EXP = /([#&]gui=)({.*})([&?]*)/;

let DATA;
(function() {
  let matches = GUI_REG_EXP.exec(window.location.hash);
  DATA = matches ? JSON.parse(matches[2]) : {};
})();

const CONTROL_KIT_CONTAINERS = new Map();

let positionOffset = 0;

class GUIComponent {
  constructor(object, key, type, controlKitComponent) {
    this._type = type;
    this._object = object;
    this._key = key;
    this._controlKitComponent = controlKitComponent;
  }
  get object() {
    return this._object;
  }
  get key() {
    return this._key;
  }
  get value() {
    return this.object[this.key];
  }
  get type() {
    return this._type;
  }
  get controlKitComponent() {
    return this._controlKitComponent;
  }
  remove() {
    this.controlKitComponent.disable();
    this.controlKitComponent._node._element.remove();
  }
}

class GUI {
  constructor() {
    this._controlKit = new ControlKit();
  }

  update() {
    for (let component of COMPONENTS) {
      switch (component.type) {
        case "color":
          component.controlKitComponent._obj._value = component.controlKitComponent._value = colorToHex(component.object[component.key]);
          component.controlKitComponent._updateColor();
          break;
      }
    }

    requestAnimationFrame(() => {
      this._controlKit.update();
    });
  }

  open(panel, group) {
    CONTROL_KIT_CONTAINERS.get(normalizeString(panel) + (group ? "/" + normalizeString(group) : "")).enable();
  }

  close(panel, group) {
    CONTROL_KIT_CONTAINERS.get(normalizeString(panel) + (group ? "/" + normalizeString(group) : "")).disable();
  }

  add(object, key, {type = typeof object[key], label = key, panel = "Main", group = "", reload = false, options, range, onChange} = {}) {
    let labelKey = normalizeString(label);

    let panelKey = normalizeString(panel);
    let controlKitPanel = CONTROL_KIT_CONTAINERS.get(panelKey);
    if(!controlKitPanel) {
      controlKitPanel = this._controlKit.addPanel({
        fixed: false,
        label: panel,
        width: 240,
        align: "left",
        position: [positionOffset, 0]
      });
      CONTROL_KIT_CONTAINERS.set(panelKey, controlKitPanel);
      positionOffset += 240;
    }

    let groupKey = normalizeString(group);
    let controlKitGroup = CONTROL_KIT_CONTAINERS.get(`${panelKey}/`);
    if(groupKey) {
      controlKitGroup = CONTROL_KIT_CONTAINERS.get(`${panelKey}/${groupKey}`);
      if(!controlKitGroup) {
        controlKitPanel.addGroup({
          label: group
        });
        controlKitGroup = controlKitPanel._groups[controlKitPanel._groups.length - 1];
        CONTROL_KIT_CONTAINERS.set(`${panelKey}/${groupKey}`, controlKitGroup);
      }
    }

    if(controlKitGroup) {
      controlKitPanel._groups.push(controlKitPanel._groups.splice(controlKitPanel._groups.indexOf(controlKitGroup), 1)[0]);
    }

    let savedValue;

    if (groupKey && DATA[panelKey] && DATA[panelKey][groupKey]) {
      savedValue = DATA[panelKey][groupKey][labelKey];
    } else if (DATA[panelKey]) {
      savedValue = DATA[panelKey][labelKey];
    }

    let changeValueData = (value = object[key]) => {
      let containerData = DATA[panelKey];
      if(!containerData) {
        containerData = DATA[panelKey] = {};
      }
      if(group) {
        let groupData = containerData[groupKey];
        if(!groupData) {
          groupData = {};
          containerData[groupKey] = groupData;
        }
        containerData = groupData;
      }

      containerData[labelKey] = value;

      if(GUI_REG_EXP.test(window.location.hash)) {
        window.location.hash = window.location.hash.replace(GUI_REG_EXP, `$1${JSON.stringify(DATA)}$3`);
      } else {
        let prefix = window.location.hash ? "&" : "#";
        window.location.hash += `${prefix}gui=${JSON.stringify(DATA)}`;
      }

      if(onChange) {
        onChange(object[key]);
      }

      if (reload) {
        window.location.reload();
      }
    }

    switch (type) {
      case "boolean":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addCheckbox(object, key, {
          onChange: changeValueData,
          label
        });
        break;
      case "number":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addNumberInput(object, key, {
          onChange: changeValueData,
          label
        });
        break;
      case "string":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addStringInput(object, key, {
          onChange: changeValueData,
          label
        });
        break;
      case "color":
        let color = {};
        if(savedValue !== undefined) {
          color.value = `#${savedValue}`;
          colorFromHex(object[key], color.value);
        } else {
          color.value = colorToHex(object[key]);
        }
        controlKitPanel.addColor(color, "value", {
          onChange: function(value) {
            colorFromHex(object[key], value);
            changeValueData(value.replace("#", ""));
          },
          label,
          colorMode: "hex"
        });
        break;
      case "select":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        controlKitPanel.addSelect({
          options,
          selection: object[key]
        }, "options", {
          onChange: function(index) {
            object[key] = options[index];
            changeValueData(object[key]);
          },
          label
        });
        break;
      case "slider":
        object[key] = savedValue === undefined ?  object[key] : savedValue;
        let slider = {
          value: object[key],
          range: range || [0, 1]
        };
        controlKitPanel.addSlider(slider, "value", "range", {
          onChange: function(value) {
            object[key] = slider.value;
            changeValueData(slider.value);
          },
          label
        });
        break;
      case "xy":
        let xy = {};
        if(object[key].x !== undefined) {
          if(matches) {
            xy.value = savedValue;
            [object[key].x, object[key].y] = [xy.value[0], xy.value[1]];
          } else {
            xy.value = [object[key].x, object[key].y];
          }
        } else {
          object[key] = savedValue === undefined ?  object[key] : savedValue;
          xy.value = object[key];
        }
        controlKitPanel.addPad(xy, "value", {
          onChange: function() {
            if(object[key].x) {
              [object[key].x, object[key].y] = [xy.value[0], xy.value[1]];
            }
            changeValueData(xy.value);
          },
          label
        });
        break;
    }

    if(!CONTROL_KIT_CONTAINERS.get(`${panelKey}/`)) {
      CONTROL_KIT_CONTAINERS.set(`${panelKey}/`, controlKitPanel._groups[0]);
    }

    if(onChange) {
      onChange(object[key]);
    }

    if(!controlKitGroup) {
      controlKitGroup = controlKitPanel._groups[controlKitPanel._groups.length - 1];
    }
    let controlKitComponent = controlKitGroup._components[controlKitGroup._components.length - 1];

    let component = new GUIComponent(object, key, type, controlKitComponent);
    COMPONENTS.push(component)
    return component;
  }

  _changeURLValue(key, value) {
    let regExp = urlHashRegExpFromKey(key);
    let matches = regExp.exec(window.location.hash);
    if (!matches) {
      let prefix = window.location.hash ? "&" : "#";
      window.location.hash += `${prefix}gui-${key}=${value}`;
    } else {
      window.location.hash = window.location.hash.replace(matches[0], matches[0].replace(regExp, `$1${value}`));
    }
  }
}

export default new GUI();
<|endoftext|>


<commit_before>'use strict';

import React from 'react';
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import generalOptions from '../../../config/generalOptions';
import {plusPage, minusPage, handleInputPageNumber} from '../../../store/actions'

//presentation of the counter in calc small
class TPTableModal extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            emptyInput: false,
            alert: false
        };
        this.handleChange = this.handleChange.bind(this);
    };

    handleChange(e) {
        if (e.target.value === '') {
            this.setState({emptyInput: true})
        } else {
            if (this.state.emptyInput)
                this.setState({emptyInput: false})
        }
        const number = parseInt(e.target.value);

        if (number > this.props.maxPageNumber) {
            this.setState({alert: true})
        }
        this.props.handleInputPageNumber(number);
    }

    handleBlur() {
        this.setState({emptyInput: false});
    }

    redirectTo(type) {
        const {service, level, deadline, pageNumber} = this.props;
        let redirectTo = generalOptions.siteMyUrl
            + `/${type}.html?csi=` + service.id
            + '&cli=' + level.id
            + '&cdi=' + deadline.id
            + '&ccu=' + pageNumber;
        if (generalOptions.rid) {
            redirectTo += `&rid=${generalOptions.rid}`
        }
        location.href = redirectTo;
    }

    estimateDay() {
        const {deadline} = this.props;
        const nowMs = new Date().getTime();
        const deadlineMs = deadline.hours * 60 * 60 * 1000;
        const date = new Date((nowMs + deadlineMs));
        return date.getDate() +
            "/" + (date.getMonth() + 1) +
            "/" + date.getFullYear();
    }

    render() {
        const {onClickPlus, onClickMinus, fullPrice, discount, pageNumber, service, level} = this.props;
        const fullPriceDiv = (discount === 0) ?
 :

                ${(fullPrice * pageNumber).toFixed(2)}

;
        return (

 {
                    e.stopPropagation();
                    this.props.closeModal();
                }} className="tp-modal__cross">×

                        Estimate date:
                        {this.estimateDay()}

                            -

                            <input type="text"
                                   className="tp-modal__page-input"
                                   onBlur={() => this.handleBlur()}
                                   value={(this.state.emptyInput) ? '' : pageNumber}
                                   onChange={(e) => this.handleChange(e)}
                            />
                            {(pageNumber === 1) ? 'page' : 'pages'}

                            +

                        Estimated price

                            {fullPriceDiv}

                                ${(fullPrice * (1 - discount) * pageNumber).toFixed(2)}

 this.redirectTo('inquiry')} className="tp-modal__btn tp-modal__btn--qoute">
                            free quote

 this.redirectTo('order')} className="tp-modal__btn tp-modal__btn--order">
                            order now

        )
    }
}

TPTableModal.propTypes = {
    onClickPlus: PropTypes.func.isRequired,
    onClickMinus: PropTypes.func.isRequired,
    pageNumber: PropTypes.number.isRequired,
    fullPrice: PropTypes.number.isRequired,
    discount: PropTypes.number.isRequired,
    service: PropTypes.object.isRequired,
    level: PropTypes.object.isRequired,
    deadline: PropTypes.object.isRequired,

};

//container to match redux state to component props and dispatch redux actions to callback props
const mapStateToProps = (reduxState, ownProps) => {
    const state = reduxState.calculatorSmall[ownProps.calcId];
    return {
        discount: reduxState.discount,
        fullPrice: state.deadline.price,
        pageNumber: state.pageNumber,
        currentDeadline: state.deadline,
        service: state.service,
        level: state.level,
        deadline: state.deadline,
    }
};

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        onClickMinus: () => {
            dispatch(minusPage(ownProps.calcId))
        },
        onClickPlus: () => {
            dispatch(plusPage(ownProps.calcId))
        },
        handleInputPageNumber: (number) => {
            dispatch(handleInputPageNumber(number, ownProps.calcId));
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(TPTableModal);
<commit_msg>fix modal TP<commit_after>'use strict';

import React from 'react';
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import generalOptions from '../../../config/generalOptions';
import {plusPage, minusPage, handleInputPageNumber} from '../../../store/actions'

//presentation of the counter in calc small
class TPTableModal extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            emptyInput: false,
            alert: false
        };
        this.handleChange = this.handleChange.bind(this);
    };

    handleChange(e) {
        if (e.target.value === '') {
            this.setState({emptyInput: true})
        } else {
            if (this.state.emptyInput)
                this.setState({emptyInput: false})
        }
        const number = parseInt(e.target.value);

        if (number > this.props.maxPageNumber) {
            this.setState({alert: true})
        }
        this.props.handleInputPageNumber(number);
    }

    handleBlur() {
        this.setState({emptyInput: false});
    }

    redirectTo(type) {
        const {service, level, deadline, pageNumber} = this.props;
        let redirectTo = generalOptions.siteMyUrl
            + `/${type}.html?csi=` + service.id
            + '&cli=' + level.id
            + '&cdi=' + deadline.id
            + '&ccu=' + pageNumber;
        if (generalOptions.rid) {
            redirectTo += `&rid=${generalOptions.rid}`
        }
        location.href = redirectTo;
    }

    estimateDay() {
        const {deadline} = this.props;
        const nowMs = new Date().getTime();
        const deadlineMs = deadline.hours * 60 * 60 * 1000;
        const date = new Date((nowMs + deadlineMs));
        return date.getDate() +
            "/" + (date.getMonth() + 1) +
            "/" + date.getFullYear();
    }

    render() {
        const {onClickPlus, onClickMinus, fullPrice, discount, pageNumber, service, level} = this.props;
        const fullPriceDiv = (discount === 0) ?
 :

                ${(fullPrice * pageNumber).toFixed(2)}

;
        return (

 {
                    e.stopPropagation();
                    this.props.closeModal();
                }} className="tp-modal__cross">×

                        Estimate date:
                        {this.estimateDay()}

                            -

                            <input type="text"
                                   className="tp-modal__page-input"
                                   onBlur={() => this.handleBlur()}
                                   value={(this.state.emptyInput) ? '' : pageNumber}
                                   onChange={(e) => this.handleChange(e)}
                            />
                            {(pageNumber === 1) ? 'page' : 'pages'}

                            +

                        Estimated price:

                            {fullPriceDiv}

                                ${(fullPrice * (1 - discount) * pageNumber).toFixed(2)}

 this.redirectTo('inquiry')} className="tp-modal__btn tp-modal__btn--qoute">
                            free quote

 this.redirectTo('order')} className="tp-modal__btn tp-modal__btn--order">
                            order now

        )
    }
}

TPTableModal.propTypes = {
    onClickPlus: PropTypes.func.isRequired,
    onClickMinus: PropTypes.func.isRequired,
    pageNumber: PropTypes.number.isRequired,
    fullPrice: PropTypes.number.isRequired,
    discount: PropTypes.number.isRequired,
    service: PropTypes.object.isRequired,
    level: PropTypes.object.isRequired,
    deadline: PropTypes.object.isRequired,

};

//container to match redux state to component props and dispatch redux actions to callback props
const mapStateToProps = (reduxState, ownProps) => {
    const state = reduxState.calculatorSmall[ownProps.calcId];
    return {
        discount: reduxState.discount,
        fullPrice: state.deadline.price,
        pageNumber: state.pageNumber,
        currentDeadline: state.deadline,
        service: state.service,
        level: state.level,
        deadline: state.deadline,
    }
};

const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        onClickMinus: () => {
            dispatch(minusPage(ownProps.calcId))
        },
        onClickPlus: () => {
            dispatch(plusPage(ownProps.calcId))
        },
        handleInputPageNumber: (number) => {
            dispatch(handleInputPageNumber(number, ownProps.calcId));
        }
    }
};

export default connect(mapStateToProps, mapDispatchToProps)(TPTableModal);
<|endoftext|>


<commit_before>import React, { Component } from 'react'
import { Route, Switch } from 'react-router'

import {
  isAdmin,
  isAuthenticated,
  isParticipant,
  shouldNotBeAuthenticated,
} from './auth'

import {
  Activity,
  Admin,
  Calendar,
  ConversationsIndex,
  ConversationsNew,
  ConversationsShow,
  EventsEdit,
  EventsNew,
  EventsShow,
  ForgotPassword,
  Home,
  Login,
  NotFound,
  Page,
  PlacesEdit,
  PlacesIndex,
  PlacesNew,
  PlacesShow,
  Profile,
  Register,
  ResetPassword,
  ResourcesEdit,
  ResourcesIndex,
  ResourcesNew,
  Stream,
  Tickets,
} from './views'

export default class Routes extends Component {
  render() {
    return (
      <main role="main">
        <Switch>
          <Route component={Home} exact={true} path="/" />
          <Route component={shouldNotBeAuthenticated(Login)} path="/login" />
          <Route component={shouldNotBeAuthenticated(Register)} path="/register" />
          <Route component={shouldNotBeAuthenticated(Tickets)} path="/tickets" />
          <Route component={shouldNotBeAuthenticated(ForgotPassword)} path="/forgot" />
          <Route component={shouldNotBeAuthenticated(ResetPassword)} path="/reset/:token" />
          <Route component={Page} path="/pages/:slug" />
          <Route component={Calendar} path="/calendar" />
          <Route component={isAuthenticated(Profile)} path="/profile" />
          <Route component={isAuthenticated(Stream)} path="/stream" />
          <Route component={isAuthenticated(EventsShow)} path="/events/:slug" />
          <Route component={isParticipant(Activity)} path="/activity" />
          <Route component={isParticipant(EventsNew)} path="/new/event" />
          <Route component={isParticipant(PlacesNew)} path="/new/place" />
          <Route component={isParticipant(ResourcesNew)} path="/new/resource" />
          <Route component={isParticipant(EventsEdit)} path="/events/:slug/edit" />
          <Route component={isParticipant(PlacesEdit)} path="/places/:slug/edit" />
          <Route component={isParticipant(PlacesShow)} path="/places/:slug" />
          <Route component={isParticipant(PlacesIndex)} path="/places" />
          <Route component={isParticipant(ResourcesEdit)} path="/resources/:slug/edit" />
          <Route component={isParticipant(ResourcesIndex)} path="/resources" />
          <Route component={isParticipant(ConversationsNew)} path="/inbox/new" />
          <Route component={isParticipant(ConversationsShow)} path="/inbox/conversations/:id" />
          <Route component={isParticipant(ConversationsIndex)} path="/inbox" />
          <Route component={isAdmin(Admin)} path="/admin" />
          <Route component={NotFound} />
        </Switch>
      </main>
    )
  }
}
<commit_msg>Fix broken edit link for events<commit_after>import React, { Component } from 'react'
import { Route, Switch } from 'react-router'

import {
  isAdmin,
  isAuthenticated,
  isParticipant,
  shouldNotBeAuthenticated,
} from './auth'

import {
  Activity,
  Admin,
  Calendar,
  ConversationsIndex,
  ConversationsNew,
  ConversationsShow,
  EventsEdit,
  EventsNew,
  EventsShow,
  ForgotPassword,
  Home,
  Login,
  NotFound,
  Page,
  PlacesEdit,
  PlacesIndex,
  PlacesNew,
  PlacesShow,
  Profile,
  Register,
  ResetPassword,
  ResourcesEdit,
  ResourcesIndex,
  ResourcesNew,
  Stream,
  Tickets,
} from './views'

export default class Routes extends Component {
  render() {
    return (
      <main role="main">
        <Switch>
          <Route component={Home} exact={true} path="/" />
          <Route component={shouldNotBeAuthenticated(Login)} path="/login" />
          <Route component={shouldNotBeAuthenticated(Register)} path="/register" />
          <Route component={shouldNotBeAuthenticated(Tickets)} path="/tickets" />
          <Route component={shouldNotBeAuthenticated(ForgotPassword)} path="/forgot" />
          <Route component={shouldNotBeAuthenticated(ResetPassword)} path="/reset/:token" />
          <Route component={Page} path="/pages/:slug" />
          <Route component={Calendar} path="/calendar" />
          <Route component={isAuthenticated(Profile)} path="/profile" />
          <Route component={isAuthenticated(Stream)} path="/stream" />
          <Route component={isParticipant(Activity)} path="/activity" />
          <Route component={isParticipant(EventsNew)} path="/new/event" />
          <Route component={isParticipant(PlacesNew)} path="/new/place" />
          <Route component={isParticipant(ResourcesNew)} path="/new/resource" />
          <Route component={isParticipant(EventsEdit)} path="/events/:slug/edit" />
          <Route component={isAuthenticated(EventsShow)} path="/events/:slug" />
          <Route component={isParticipant(PlacesEdit)} path="/places/:slug/edit" />
          <Route component={isParticipant(PlacesShow)} path="/places/:slug" />
          <Route component={isParticipant(PlacesIndex)} path="/places" />
          <Route component={isParticipant(ResourcesEdit)} path="/resources/:slug/edit" />
          <Route component={isParticipant(ResourcesIndex)} path="/resources" />
          <Route component={isParticipant(ConversationsNew)} path="/inbox/new" />
          <Route component={isParticipant(ConversationsShow)} path="/inbox/conversations/:id" />
          <Route component={isParticipant(ConversationsIndex)} path="/inbox" />
          <Route component={isAdmin(Admin)} path="/admin" />
          <Route component={NotFound} />
        </Switch>
      </main>
    )
  }
}
<|endoftext|>


<commit_before>import {checkRendering} from '../../lib/utils.js';

const usage = `Usage:
var customRefinementList = connectRefinementList(function render(params) {
  // params = {
  //   isFromSearch,
  //   createURL,
  //   items,
  //   refine,
  //   searchForItems,
  //   instantSearchInstance,
  //   canRefine,
  //   widgetParams,
  // }
});
search.addWidget(
  customRefinementList({
    attributeName,
    [ operator = 'or' ],
    [ limit ],
    [ sortBy = ['isRefined', 'count:desc'] ],
  })
);
Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectRefinementList.html
`;

export const checkUsage = ({attributeName, operator, usageMessage}) => {
  const noAttributeName = attributeName === undefined;
  const invalidOperator = !(/^(and|or)$/).test(operator);

  if (noAttributeName || invalidOperator) {
    throw new Error(usageMessage);
  }
};

/**
 * @typedef {Object} RefinementListRenderingOptions
 * @property {string} attributeName the attribute in the records that are used by the widget
 * @property {string} operator how the filters are combined together
 * @property {number} limit the max number of items displayed
 * @property {string[]|function} [sortBy = ['isRefined', 'count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
 */

/**
 * @typedef {Object} RefinementListRenderingOptions
 * @property {Object[]} items the list of filtering values returned from Algolia
 * @property {function} createURL create the next state url
 * @property {function} refine set the next state url
 * @property {function} searchForItems search for values inside the list
 * @property {boolean} isFromSearch indicates if the values are from an index search
 * @property {boolean} canRefine indicates if a refinement can be applied
 * @property {Object} widgetParams all original options forwarded to rendering
 * @property {InstantSearch} instantSearchInstance the instance of instantsearch on which the widget is attached
 */

/**
 * Creates a custom widget for a refinement list.
 * @type {Connector}
 * @param {function(RefinementListRenderingOptions, boolean)} renderFn function that renders the refinement list widget
 * @returns {function(RefinementListWidgetOptions)} a custom refinement list widget factory
 */
export default function connectRefinementList(renderFn) {
  checkRendering(renderFn, usage);

  return (widgetParams = {}) => {
    const {
      attributeName,
      operator = 'or',
      limit,
      sortBy = ['isRefined', 'count:desc'],
    } = widgetParams;

    checkUsage({attributeName, operator, usage});

    const formatItems = ({name: label, ...item}) =>
      ({...item, label, value: label, highlighted: label});

    const render = ({items, state, createURL,
                    helperSpecializedSearchFacetValues,
                    refine, isFromSearch, isFirstSearch, instantSearchInstance}) => {
      // Compute a specific createURL method able to link to any facet value state change
      const _createURL = facetValue => createURL(state.toggleRefinement(attributeName, facetValue));

      // Do not mistake searchForFacetValues and searchFacetValues which is the actual search
      // function
      const searchFacetValues = helperSpecializedSearchFacetValues &&
        helperSpecializedSearchFacetValues(
          state,
          createURL,
          helperSpecializedSearchFacetValues,
          refine,
          instantSearchInstance,
        );

      renderFn({
        createURL: _createURL,
        items,
        refine,
        searchForItems: searchFacetValues,
        instantSearchInstance,
        isFromSearch,
        canRefine: isFromSearch || items.length > 0,
        widgetParams,
      }, isFirstSearch);
    };

    let lastResultsFromMainSearch;
    let searchForFacetValues;
    let refine;

    const createSearchForFacetValues = helper =>
      (state, createURL, helperSpecializedSearchFacetValues, toggleRefinement, instantSearchInstance) =>
      query => {
        if (query === '' && lastResultsFromMainSearch) {
          // render with previous data from the helper.
          render({
            items: lastResultsFromMainSearch,
            state,
            createURL,
            helperSpecializedSearchFacetValues,
            refine: toggleRefinement,
            isFromSearch: false,
            isFirstSearch: false,
            instantSearchInstance,
          });
        } else {
          helper.searchForFacetValues(attributeName, query).then(results => {
            const facetValues = results.facetHits.map(formatItems);

            render({
              items: facetValues,
              state,
              createURL,
              helperSpecializedSearchFacetValues,
              refine: toggleRefinement,
              isFromSearch: true,
              isFirstSearch: false,
              instantSearchInstance,
            });
          });
        }
      };

    return {
      getConfiguration: (configuration = {}) => {
        const widgetConfiguration = {
          [operator === 'and' ? 'facets' : 'disjunctiveFacets']: [attributeName],
        };

        if (limit !== undefined) {
          const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
          widgetConfiguration.maxValuesPerFacet = Math.max(currentMaxValuesPerFacet, limit);
        }

        return widgetConfiguration;
      },
      init({helper, createURL, instantSearchInstance}) {
        refine = facetValue => helper
          .toggleRefinement(attributeName, facetValue)
          .search();

        searchForFacetValues = createSearchForFacetValues(helper);

        render({
          items: [],
          state: helper.state,
          createURL,
          helperSpecializedSearchFacetValues: searchForFacetValues,
          refine,
          isFromSearch: false,
          isFirstSearch: true,
          instantSearchInstance,
        });
      },
      render({results, state, createURL, instantSearchInstance}) {
        const items = results
          .getFacetValues(attributeName, {sortBy})
          .map(formatItems);

        lastResultsFromMainSearch = items;

        render({
          items,
          state,
          createURL,
          helperSpecializedSearchFacetValues: searchForFacetValues,
          refine,
          isFromSearch: false,
          isFirstSearch: false,
          instantSearchInstance,
        });
      },
    };
  };
}
<commit_msg>doc: connectRefinementList<commit_after>import {checkRendering} from '../../lib/utils.js';

const usage = `Usage:
var customRefinementList = connectRefinementList(function render(params) {
  // params = {
  //   isFromSearch,
  //   createURL,
  //   items,
  //   refine,
  //   searchForItems,
  //   instantSearchInstance,
  //   canRefine,
  //   widgetParams,
  // }
});
search.addWidget(
  customRefinementList({
    attributeName,
    [ operator = 'or' ],
    [ limit ],
    [ sortBy = ['isRefined', 'count:desc'] ],
  })
);
Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectRefinementList.html
`;

export const checkUsage = ({attributeName, operator, usageMessage}) => {
  const noAttributeName = attributeName === undefined;
  const invalidOperator = !(/^(and|or)$/).test(operator);

  if (noAttributeName || invalidOperator) {
    throw new Error(usageMessage);
  }
};

/**
 * @typedef {Object} CustomRefinementListWidgetOptions
 * @property {string} attributeName The name of the attribute in the records.
 * @property {"and"|"or"} [operator = "or"] How the filters are combined together.
 * @property {number} [limit] The max number of items to display.
 * @property {string[]|function} [sortBy = ['isRefined', 'count:desc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
 */

/**
 * @typedef {Object} RefinementListRenderingOptions
 * @property {Object[]} items The list of filtering values returned from Algolia API.
 * @property {function} createURL Creates the next state url for a selected refinement.
 * @property {function} refine Action to apply selected refinements.
 * @property {function} searchForItems Search for values inside the list.
 * @property {boolean} isFromSearch Indicates if the values are from an index search.
 * @property {boolean} canRefine Indicates if a refinement can be applied.
 * @property {InstantSearch} instantSearchInstance Instance of instantsearch on which the widget is attached.
 * @property {Object} widgetParams All original `CustomRefinementListWidgetOptions` forwarded to the `renderFn`.
 */

/**
 * **RefinementList** connector provides the logic to build a custom widget that will give the user the ability to choose multiple values for a specific facet.
 * @type {Connector}
 * @param {function(RefinementListRenderingOptions, boolean)} renderFn Rendering function for the custom **RefinementList** widget.
 * @return {function(CustomRefinementListWidgetOptions)} Re-usable widget factory for a custom **RefinementList** widget.
 * @example
 * var $ = window.$;
 * var instantsearch = window.instantsearch;
 *
 * // custom `renderFn` to render the custom NumericRefinementList widget
 * function renderFn(RefinementListRenderingOptions, isFirstRendering) {
 *   if (isFirstRendering) {
 *     RefinementListRenderingOptions.widgetParams.containerNode
 *       .html('

')
 *   }
 *
 *     RefinementListRenderingOptions.widgetParams.containerNode
 *       .find('li[data-refine-value]')
 *       .each(function() { $(this).off('click'); });
 *
 *   if (RefinementListRenderingOptions.canRefine) {
 *     var list = RefinementListRenderingOptions.items.map(function(item) {
 *       return '
' +
 *         '<input type="checkbox" value="' + item.value + '"' + item.isRefined ? ' checked' : '' + '/>' +
 *         '' + item.label + '' +
 *         '
';
 *     });
 *
 *     RefinementListRenderingOptions.widgetParams.containerNode.find('ul').html(list);
 *     RefinementListRenderingOptions.widgetParams.containerNode
 *       .find('li[data-refine-value]')
 *       .each(function() {
 *         $(this).on('click', function(event) {
 *           event.stopPropagation();
 *           event.preventDefault();
 *
 *           RefinementListRenderingOptions.refine($(this).data('refine-value'));
 *         });
 *       });
 *   } else {
 *     RefinementListRenderingOptions.widgetParams.containerNode.find('ul').html('');
 *   }
 *
 * // connect `renderFn` to RefinementList logic
 * var customRefinementList = instantsearch.connectors.connectRefinementList(renderFn);
 *
 * // mount widget on the page
 * search.addWidget(
 *   customRefinementList({
 *     containerNode: $('#custom-clear-all-container'),
 *     attributeName: 'categories',
 *   })
 * );
 */
export default function connectRefinementList(renderFn) {
  checkRendering(renderFn, usage);

  return (widgetParams = {}) => {
    const {
      attributeName,
      operator = 'or',
      limit,
      sortBy = ['isRefined', 'count:desc'],
    } = widgetParams;

    checkUsage({attributeName, operator, usage});

    const formatItems = ({name: label, ...item}) =>
      ({...item, label, value: label, highlighted: label});

    const render = ({items, state, createURL,
                    helperSpecializedSearchFacetValues,
                    refine, isFromSearch, isFirstSearch, instantSearchInstance}) => {
      // Compute a specific createURL method able to link to any facet value state change
      const _createURL = facetValue => createURL(state.toggleRefinement(attributeName, facetValue));

      // Do not mistake searchForFacetValues and searchFacetValues which is the actual search
      // function
      const searchFacetValues = helperSpecializedSearchFacetValues &&
        helperSpecializedSearchFacetValues(
          state,
          createURL,
          helperSpecializedSearchFacetValues,
          refine,
          instantSearchInstance,
        );

      renderFn({
        createURL: _createURL,
        items,
        refine,
        searchForItems: searchFacetValues,
        instantSearchInstance,
        isFromSearch,
        canRefine: isFromSearch || items.length > 0,
        widgetParams,
      }, isFirstSearch);
    };

    let lastResultsFromMainSearch;
    let searchForFacetValues;
    let refine;

    const createSearchForFacetValues = helper =>
      (state, createURL, helperSpecializedSearchFacetValues, toggleRefinement, instantSearchInstance) =>
      query => {
        if (query === '' && lastResultsFromMainSearch) {
          // render with previous data from the helper.
          render({
            items: lastResultsFromMainSearch,
            state,
            createURL,
            helperSpecializedSearchFacetValues,
            refine: toggleRefinement,
            isFromSearch: false,
            isFirstSearch: false,
            instantSearchInstance,
          });
        } else {
          helper.searchForFacetValues(attributeName, query).then(results => {
            const facetValues = results.facetHits.map(formatItems);

            render({
              items: facetValues,
              state,
              createURL,
              helperSpecializedSearchFacetValues,
              refine: toggleRefinement,
              isFromSearch: true,
              isFirstSearch: false,
              instantSearchInstance,
            });
          });
        }
      };

    return {
      getConfiguration: (configuration = {}) => {
        const widgetConfiguration = {
          [operator === 'and' ? 'facets' : 'disjunctiveFacets']: [attributeName],
        };

        if (limit !== undefined) {
          const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
          widgetConfiguration.maxValuesPerFacet = Math.max(currentMaxValuesPerFacet, limit);
        }

        return widgetConfiguration;
      },
      init({helper, createURL, instantSearchInstance}) {
        refine = facetValue => helper
          .toggleRefinement(attributeName, facetValue)
          .search();

        searchForFacetValues = createSearchForFacetValues(helper);

        render({
          items: [],
          state: helper.state,
          createURL,
          helperSpecializedSearchFacetValues: searchForFacetValues,
          refine,
          isFromSearch: false,
          isFirstSearch: true,
          instantSearchInstance,
        });
      },
      render({results, state, createURL, instantSearchInstance}) {
        const items = results
          .getFacetValues(attributeName, {sortBy})
          .map(formatItems);

        lastResultsFromMainSearch = items;

        render({
          items,
          state,
          createURL,
          helperSpecializedSearchFacetValues: searchForFacetValues,
          refine,
          isFromSearch: false,
          isFirstSearch: false,
          instantSearchInstance,
        });
      },
    };
  };
}
<|endoftext|>