<commit_before>/**
 * @class Oskari.mapframework.bundle.publisher.view.PublisherLayerForm
 *
 */
Oskari.clazz.define('Oskari.mapframework.bundle.publisher.view.PublisherLayerForm',

/**
 * @method create called automatically on construction
 * @static
 */
function(localization, instance) {
	this.loc = localization;
	this.instance = instance;
	this.panel = null;
	this.plugin = null;

    this.templateHelp = jQuery('

');
    this.templateTool = jQuery('
<input type="checkbox"/>
');

    this.config = {
        layers : {
            promote : [{
                text : this.loc.layerselection.promote,
                id : [24] // , 203
            }],
            preselect : 'base_35'
        }
    };
    this.showLayerSelection = false;
}, {
	init : function() {
		if(!this.panel) {
	        this.panel = Oskari.clazz.create('Oskari.userinterface.component.AccordionPanel');
	        this.panel.setTitle(this.loc.layers.label);
		}

		this.plugin = Oskari.clazz.create('Oskari.mapframework.bundle.mapmodule.plugin.LayerSelectionPlugin');
	},
	getPanel : function() {
        this._populateMapLayerPanel();
		return this.panel;
	},
	_enablePlugin : function(blnEnabled) {
        if (blnEnabled) {
            this.plugin.startPlugin(this.instance.sandbox);
        } else {
            this.plugin.stopPlugin(this.instance.sandbox);
        }
	},
	start : function() {
        var mapModule = this.instance.sandbox.findRegisteredModuleInstance('MainMapModule');
        mapModule.registerPlugin(this.plugin);
	},
	stop : function() {
		this._enablePlugin(false);
        var mapModule = this.instance.sandbox.findRegisteredModuleInstance('MainMapModule');
        mapModule.unregisterPlugin(this.plugin);
	},
	getValues : function() {
		var values = {
		};
		if(this.showLayerSelection) {
			values.layerSelection = {
				id : 'Oskari.mapframework.bundle.mapmodule.plugin.LayerSelectionPlugin',
				config : {
				}
			}
			var pluginValues = this.plugin.getBaseLayers();
			if(pluginValues.defaultBaseLayer) {
				values.layerSelection.config.baseLayers = pluginValues.baseLayers;
				values.layerSelection.config.defaultBaseLayer = pluginValues.defaultBaseLayer;
			}
		}

		return values;
    },
	validate : function() {
		var errors = [];
		return errors;
   },
    /**
     * @method getLayersList
     * Handles the published map layer selection
     * FIXME: this is completely under construction, rights aren't managed in any
     * way etc
     */
    getLayersList : function() {
        var layers = [];
        var selectedLayers = this.instance.sandbox.findAllSelectedMapLayers();
        return selectedLayers;
        /*
        for (var i = 0; i < selectedLayers.length; ++i) {
            var layer = {
                id : selectedLayers[i].getId(),
                name : selectedLayers[i].getName(),
                opacity : selectedLayers[i].getOpacity()
            };
            layers.push(layer);
        }
        return layers;
        */
    },
    /**
     * @method _populateMapLayerPanel
     * @private
     * Populates the map layers panel in publisher
     */
    _populateMapLayerPanel : function() {

        var me = this;
        var contentPanel = this.panel.getContainer();

        // tooltip
        var tooltipCont = this.templateHelp.clone();
        tooltipCont.attr('title', this.loc.layerselection.tooltip);
        contentPanel.append(tooltipCont);

        // tool selection
        var toolContainer = this.templateTool.clone();
        toolContainer.find('span').append(this.loc.layerselection.label);
        if (this.showLayerSelection) {
            toolContainer.find('input').attr('checked', 'checked');
        }
        contentPanel.append(toolContainer);
        contentPanel.append(this.loc.layerselection.info);
        toolContainer.find('input').change(function() {
            var checkbox = jQuery(this);
            var isChecked = checkbox.is(':checked');
            me.showLayerSelection = isChecked;
    		me._enablePlugin(isChecked);
            contentPanel.empty();
            me._populateMapLayerPanel();
        });
        if (!this.showLayerSelection) {
            return;
        }
        // if layer selection = ON -> show content
        var closureMagic = function(layer) {
            return function() {
                var checkbox = jQuery(this);
                var isChecked = checkbox.is(':checked');
                layer.selected = isChecked;
                if (isChecked) {
                	me.plugin.addBaseLayer(layer);
                } else {
                	me.plugin.removeBaseLayer(layer);
                }
            };
        };
        var layers = this.getLayersList();
        for (var i = 0; i < layers.length; ++i) {

            var layer = layers[i];
            var layerContainer = this.templateTool.clone();
            layerContainer.attr('data-id', layer.getId());
            layerContainer.find('span').append(layer.getName());
            var input = layerContainer.find('input');

            if (this.config.layers.preselect &&
            	this.config.layers.preselect == layer.getId()) {
                input.attr('checked', 'checked');
                layer.selected = true;
                this.plugin.addBaseLayer(layer);
            }
            input.change(closureMagic(layer));
            contentPanel.append(layerContainer);
        }

        if (this.config.layers.promote && this.config.layers.promote.length > 0) {
            this._populateLayerPromotion(contentPanel);
        }
    },
    /**
     * @method _populateLayerPromotion
     * @private
     * Populates the layer promotion part of the map layers panel in publisher
     */
    _populateLayerPromotion : function(contentPanel) {
        var me = this;
        var sandbox = this.instance.getSandbox();
        var addRequestBuilder = sandbox.getRequestBuilder('AddMapLayerRequest');
        var removeRequestBuilder = sandbox.getRequestBuilder('RemoveMapLayerRequest');
        var closureMagic = function(layer) {
            return function() {
                var checkbox = jQuery(this);
                var isChecked = checkbox.is(':checked');
                if (isChecked) {
                	sandbox.request(me.instance, addRequestBuilder(layer.getId(), true));
                	//me.plugin.addLayer(layer);
                } else {
                	sandbox.request(me.instance, removeRequestBuilder(layer.getId()));
                	//me.plugin.removeLayer(layer);
                }
            };
        };

        for (var i = 0; i < this.config.layers.promote.length; ++i) {
            var promotion = this.config.layers.promote[i];
            var promoLayerList = this._getActualPromotionLayers(promotion.id);

            if (promoLayerList.length > 0) {
                contentPanel.append(promotion.text);
            	for (var j = 0; j < promoLayerList.length; ++j) {
            		var layer = promoLayerList[j];
                	var layerContainer = this.templateTool.clone();
		            layerContainer.attr('data-id', layer.getId());
		            layerContainer.find('span').append(layer.getName());
		            var input = layerContainer.find('input');
        			input.change(closureMagic(layer));
                	contentPanel.append(layerContainer);
                }
            }
        }
    },
    /**
     * @method _getActualPromotionLayers
     * Checks given layer list and returns any layer that is found on the system but not yet selected.
     * The returned list contains the list that we should promote.
     * @param {String[]} list - list of layer ids that we want to promote
     * @return {Oskari.mapframework.domain.WmsLayer[]/Oskari.mapframework.domain.WfsLayer[]/Oskari.mapframework.domain.VectorLayer[]/Object[]} filtered list of promoted layers
     * @private
     */
    _getActualPromotionLayers : function(list) {
        var sandbox = this.instance.getSandbox();
        var layersToPromote = [];
        for (var j = 0; j < list.length; ++j) {
            if (!sandbox.isLayerAlreadySelected(list[j])) {
            	var layer = sandbox.findMapLayerFromAllAvailable(list[j]);
	            // promo layer found in system
                if (layer) {
                	layersToPromote.push(layer);
                }
            }
        }
        return layersToPromote;
    }
});

<commit_msg>[PORTTISK-1002] promote layers go directly to baselayers when selected<commit_after>/**
 * @class Oskari.mapframework.bundle.publisher.view.PublisherLayerForm
 *
 */
Oskari.clazz.define('Oskari.mapframework.bundle.publisher.view.PublisherLayerForm',

/**
 * @method create called automatically on construction
 * @static
 */
function(localization, instance) {
	this.loc = localization;
	this.instance = instance;
	this.panel = null;
	this.plugin = null;

    this.templateHelp = jQuery('

');
    this.templateTool = jQuery('
<input type="checkbox"/>
');

    this.config = {
        layers : {
            promote : [{
                text : this.loc.layerselection.promote,
                id : [24] // , 203
            }],
            preselect : 'base_35'
        }
    };
    this.showLayerSelection = false;
}, {
	init : function() {
		if(!this.panel) {
	        this.panel = Oskari.clazz.create('Oskari.userinterface.component.AccordionPanel');
	        this.panel.setTitle(this.loc.layers.label);
		}

		this.plugin = Oskari.clazz.create('Oskari.mapframework.bundle.mapmodule.plugin.LayerSelectionPlugin');
	},
	getPanel : function() {
        this._populateMapLayerPanel();
		return this.panel;
	},
	_enablePlugin : function(blnEnabled) {
        if (blnEnabled) {
            this.plugin.startPlugin(this.instance.sandbox);
        } else {
            this.plugin.stopPlugin(this.instance.sandbox);
        }
	},
	start : function() {
        var mapModule = this.instance.sandbox.findRegisteredModuleInstance('MainMapModule');
        mapModule.registerPlugin(this.plugin);
	},
	stop : function() {
		this._enablePlugin(false);
        var mapModule = this.instance.sandbox.findRegisteredModuleInstance('MainMapModule');
        mapModule.unregisterPlugin(this.plugin);
	},
	getValues : function() {
		var values = {
		};
		if(this.showLayerSelection) {
			values.layerSelection = {
				id : 'Oskari.mapframework.bundle.mapmodule.plugin.LayerSelectionPlugin',
				config : {
				}
			}
			var pluginValues = this.plugin.getBaseLayers();
			if(pluginValues.defaultBaseLayer) {
				values.layerSelection.config.baseLayers = pluginValues.baseLayers;
				values.layerSelection.config.defaultBaseLayer = pluginValues.defaultBaseLayer;
			}
		}

		return values;
    },
	validate : function() {
		var errors = [];
		return errors;
   },
    /**
     * @method getLayersList
     * Handles the published map layer selection
     * FIXME: this is completely under construction, rights aren't managed in any
     * way etc
     */
    getLayersList : function() {
        var layers = [];
        var selectedLayers = this.instance.sandbox.findAllSelectedMapLayers();
        return selectedLayers;
        /*
        for (var i = 0; i < selectedLayers.length; ++i) {
            var layer = {
                id : selectedLayers[i].getId(),
                name : selectedLayers[i].getName(),
                opacity : selectedLayers[i].getOpacity()
            };
            layers.push(layer);
        }
        return layers;
        */
    },
    /**
     * @method _populateMapLayerPanel
     * @private
     * Populates the map layers panel in publisher
     */
    _populateMapLayerPanel : function() {

        var me = this;
        var contentPanel = this.panel.getContainer();

        // tooltip
        var tooltipCont = this.templateHelp.clone();
        tooltipCont.attr('title', this.loc.layerselection.tooltip);
        contentPanel.append(tooltipCont);

        // tool selection
        var toolContainer = this.templateTool.clone();
        toolContainer.find('span').append(this.loc.layerselection.label);
        if (this.showLayerSelection) {
            toolContainer.find('input').attr('checked', 'checked');
        }
        contentPanel.append(toolContainer);
        contentPanel.append(this.loc.layerselection.info);
        toolContainer.find('input').change(function() {
            var checkbox = jQuery(this);
            var isChecked = checkbox.is(':checked');
            me.showLayerSelection = isChecked;
    		me._enablePlugin(isChecked);
            contentPanel.empty();
            me._populateMapLayerPanel();
        });
        if (!this.showLayerSelection) {
            return;
        }
        // if layer selection = ON -> show content
        var closureMagic = function(layer) {
            return function() {
                var checkbox = jQuery(this);
                var isChecked = checkbox.is(':checked');
                layer.selected = isChecked;
                if (isChecked) {
                	me.plugin.addBaseLayer(layer);
                } else {
                	me.plugin.removeBaseLayer(layer);
                }
            };
        };
        var layers = this.getLayersList();
        for (var i = 0; i < layers.length; ++i) {

            var layer = layers[i];
            var layerContainer = this.templateTool.clone();
            layerContainer.attr('data-id', layer.getId());
            layerContainer.find('span').append(layer.getName());
            var input = layerContainer.find('input');

            if (this.config.layers.preselect &&
            	this.config.layers.preselect == layer.getId()) {
                input.attr('checked', 'checked');
                layer.selected = true;
                this.plugin.addBaseLayer(layer);
            }
            input.change(closureMagic(layer));
            contentPanel.append(layerContainer);
        }

        if (this.config.layers.promote && this.config.layers.promote.length > 0) {
            this._populateLayerPromotion(contentPanel);
        }
    },
    /**
     * @method _populateLayerPromotion
     * @private
     * Populates the layer promotion part of the map layers panel in publisher
     */
    _populateLayerPromotion : function(contentPanel) {
        var me = this;
        var sandbox = this.instance.getSandbox();
        var addRequestBuilder = sandbox.getRequestBuilder('AddMapLayerRequest');
        var removeRequestBuilder = sandbox.getRequestBuilder('RemoveMapLayerRequest');
        var closureMagic = function(layer) {
            return function() {
                var checkbox = jQuery(this);
                var isChecked = checkbox.is(':checked');
                if (isChecked) {
                	sandbox.request(me.instance, addRequestBuilder(layer.getId(), true));
                	// promoted layers go directly to baselayers
                    me.plugin.addBaseLayer(layer);
                } else {
                	sandbox.request(me.instance, removeRequestBuilder(layer.getId()));
                }
            };
        };

        for (var i = 0; i < this.config.layers.promote.length; ++i) {
            var promotion = this.config.layers.promote[i];
            var promoLayerList = this._getActualPromotionLayers(promotion.id);

            if (promoLayerList.length > 0) {
                contentPanel.append(promotion.text);
            	for (var j = 0; j < promoLayerList.length; ++j) {
            		var layer = promoLayerList[j];
                	var layerContainer = this.templateTool.clone();
		            layerContainer.attr('data-id', layer.getId());
		            layerContainer.find('span').append(layer.getName());
		            var input = layerContainer.find('input');
        			input.change(closureMagic(layer));
                	contentPanel.append(layerContainer);
                }
            }
        }
    },
    /**
     * @method _getActualPromotionLayers
     * Checks given layer list and returns any layer that is found on the system but not yet selected.
     * The returned list contains the list that we should promote.
     * @param {String[]} list - list of layer ids that we want to promote
     * @return {Oskari.mapframework.domain.WmsLayer[]/Oskari.mapframework.domain.WfsLayer[]/Oskari.mapframework.domain.VectorLayer[]/Object[]} filtered list of promoted layers
     * @private
     */
    _getActualPromotionLayers : function(list) {
        var sandbox = this.instance.getSandbox();
        var layersToPromote = [];
        for (var j = 0; j < list.length; ++j) {
            if (!sandbox.isLayerAlreadySelected(list[j])) {
            	var layer = sandbox.findMapLayerFromAllAvailable(list[j]);
	            // promo layer found in system
                if (layer) {
                	layersToPromote.push(layer);
                }
            }
        }
        return layersToPromote;
    }
});

<|endoftext|>


<commit_before>/*
 * ext-server_opensave.js
 *
 * Licensed under the Apache License, Version 2
 *
 * Copyright(c) 2010 Alexis Deveria
 *
 */

svgEditor.addExtension("server_opensave", {
	callback: function() {

		var save_svg_action = 'extensions/filesave.php';
		var save_png_action = 'extensions/filesave.php';

		// Create upload target (hidden iframe)
		var target = $('<iframe name="output_frame" src="#"/>').hide().appendTo('body');

		svgEditor.setCustomHandlers({
			save: function(win, data) {
				var svg = "<?xml version='1.0'?>\n" + data;

				var title = svgCanvas.getDocumentTitle();
				var filename = title.replace(/[^a-z0-9\.\_\-]+/gi, '_');

				var form = $('<form>').attr({
					method: 'post',
					action: save_svg_action,
					target: 'output_frame'
				})	.append('<input type="hidden" name="output_svg" value="' + encodeURI(svg) + '">')
					.append('<input type="hidden" name="filename" value="' + filename + '">')
					.appendTo('body')
					.submit().remove();
			},
			pngsave: function(win, data) {
				var issues = data.issues;

				if(!$('#export_canvas').length) {
					$('<canvas>', {id: 'export_canvas'}).hide().appendTo('body');
				}
				var c = $('#export_canvas')[0];

				c.width = svgCanvas.contentW;
				c.height = svgCanvas.contentH;
				canvg(c, data.svg);

				// Timeout to allow canvg to run a bit
				setTimeout(function() {
					var datauri = c.toDataURL('image/png');

					var uiStrings = svgEditor.uiStrings;
					var note = '';

					// Check if there's issues
					if(issues.length) {
						var pre = "\n \u2022 ";
						note += ("\n\n" + pre + issues.join(pre));
					}

					if(note.length) {
						alert(note);
					}

					var title = svgCanvas.getDocumentTitle();
					var filename = title.replace(/[^a-z0-9\.\_\-]+/gi, '_');

					var form = $('<form>').attr({
						method: 'post',
						action: save_png_action,
						target: 'output_frame'
					})	.append('<input type="hidden" name="output_png" value="' + datauri + '">')
						.append('<input type="hidden" name="filename" value="' + filename + '">')
						.appendTo('body')
						.submit().remove();
				}, 1000);

			}
		});

		// Do nothing if client support is found
		if(window.FileReader) return;

		var cancelled = false;

		// Change these to appropriate script file
		var open_svg_action = 'extensions/fileopen.php?type=load_svg';
		var import_svg_action = 'extensions/fileopen.php?type=import_svg';
		var import_img_action = 'extensions/fileopen.php?type=import_img';

		// Set up function for PHP uploader to use
		svgEditor.processFile = function(str64, type) {
			if(cancelled) {
				cancelled = false;
				return;
			}

			$('#dialog_box').hide();

			if(type != 'import_img') {
				var xmlstr = svgCanvas.Utils.decode64(str64);
			}

			switch ( type ) {
				case 'load_svg':
					svgCanvas.clear();
					svgCanvas.setSvgString(xmlstr);
					svgEditor.updateCanvas();
					break;
				case 'import_svg':
					svgCanvas.importSvgString(xmlstr);
					svgEditor.updateCanvas();
					break;
				case 'import_img':
					svgCanvas.setGoodImage(str64);
					break;
			}
		}

		// Create upload form
		var open_svg_form = $('<form>');
		open_svg_form.attr({
			enctype: 'multipart/form-data',
			method: 'post',
			action: open_svg_action,
			target: 'output_frame'
		});

		// Create import form
		var import_svg_form = open_svg_form.clone().attr('action', import_svg_action);

		// Create image form
		var import_img_form = open_svg_form.clone().attr('action', import_img_action);

		// It appears necessory to rebuild this input every time a file is
		// selected so the same file can be picked and the change event can fire.
		function rebuildInput(form) {
			form.empty();
			var inp = $('<input type="file" name="svg_file">').appendTo(form);

			function submit() {
				// This submits the form, which returns the file data using svgEditor.uploadSVG
				form.submit();

				rebuildInput(form);
				$.process_cancel("Uploading...", function() {
					cancelled = true;
					$('#dialog_box').hide();
				});
			}

			if(form[0] == open_svg_form[0]) {
				inp.change(function() {
					// This takes care of the "are you sure" dialog box
					svgEditor.openPrep(function(ok) {
						if(!ok) {
							rebuildInput(form);
							return;
						}
						submit();
					});
				});
			} else {
				inp.change(function() {
					// This submits the form, which returns the file data using svgEditor.uploadSVG
					submit();
				});
			}
		}

		// Create the input elements
		rebuildInput(open_svg_form);
		rebuildInput(import_svg_form);
		rebuildInput(import_img_form);

		// Add forms to buttons
		$("#tool_open").show().prepend(open_svg_form);
		$("#tool_import").show().prepend(import_svg_form);
		$("#tool_image").prepend(import_img_form);
	}
});

<commit_msg>Added canvg callback to opensave extension<commit_after>/*
 * ext-server_opensave.js
 *
 * Licensed under the Apache License, Version 2
 *
 * Copyright(c) 2010 Alexis Deveria
 *
 */

svgEditor.addExtension("server_opensave", {
	callback: function() {

		var save_svg_action = 'extensions/filesave.php';
		var save_png_action = 'extensions/filesave.php';

		// Create upload target (hidden iframe)
		var target = $('<iframe name="output_frame" src="#"/>').hide().appendTo('body');

		svgEditor.setCustomHandlers({
			save: function(win, data) {
				var svg = "<?xml version='1.0'?>\n" + data;

				var title = svgCanvas.getDocumentTitle();
				var filename = title.replace(/[^a-z0-9\.\_\-]+/gi, '_');

				var form = $('<form>').attr({
					method: 'post',
					action: save_svg_action,
					target: 'output_frame'
				})	.append('<input type="hidden" name="output_svg" value="' + encodeURI(svg) + '">')
					.append('<input type="hidden" name="filename" value="' + filename + '">')
					.appendTo('body')
					.submit().remove();
			},
			pngsave: function(win, data) {
				var issues = data.issues;

				if(!$('#export_canvas').length) {
					$('<canvas>', {id: 'export_canvas'}).hide().appendTo('body');
				}
				var c = $('#export_canvas')[0];

				c.width = svgCanvas.contentW;
				c.height = svgCanvas.contentH;
				canvg(c, data.svg, {renderCallback: function() {
					var datauri = c.toDataURL('image/png');

					var uiStrings = svgEditor.uiStrings;
					var note = '';

					// Check if there's issues
					if(issues.length) {
						var pre = "\n \u2022 ";
						note += ("\n\n" + pre + issues.join(pre));
					}

					if(note.length) {
						alert(note);
					}

					var title = svgCanvas.getDocumentTitle();
					var filename = title.replace(/[^a-z0-9\.\_\-]+/gi, '_');

					var form = $('<form>').attr({
						method: 'post',
						action: save_png_action,
						target: 'output_frame'
					})	.append('<input type="hidden" name="output_png" value="' + datauri + '">')
						.append('<input type="hidden" name="filename" value="' + filename + '">')
						.appendTo('body')
						.submit().remove();
				}});

			}
		});

		// Do nothing if client support is found
		if(window.FileReader) return;

		var cancelled = false;

		// Change these to appropriate script file
		var open_svg_action = 'extensions/fileopen.php?type=load_svg';
		var import_svg_action = 'extensions/fileopen.php?type=import_svg';
		var import_img_action = 'extensions/fileopen.php?type=import_img';

		// Set up function for PHP uploader to use
		svgEditor.processFile = function(str64, type) {
			if(cancelled) {
				cancelled = false;
				return;
			}

			$('#dialog_box').hide();

			if(type != 'import_img') {
				var xmlstr = svgCanvas.Utils.decode64(str64);
			}

			switch ( type ) {
				case 'load_svg':
					svgCanvas.clear();
					svgCanvas.setSvgString(xmlstr);
					svgEditor.updateCanvas();
					break;
				case 'import_svg':
					svgCanvas.importSvgString(xmlstr);
					svgEditor.updateCanvas();
					break;
				case 'import_img':
					svgCanvas.setGoodImage(str64);
					break;
			}
		}

		// Create upload form
		var open_svg_form = $('<form>');
		open_svg_form.attr({
			enctype: 'multipart/form-data',
			method: 'post',
			action: open_svg_action,
			target: 'output_frame'
		});

		// Create import form
		var import_svg_form = open_svg_form.clone().attr('action', import_svg_action);

		// Create image form
		var import_img_form = open_svg_form.clone().attr('action', import_img_action);

		// It appears necessory to rebuild this input every time a file is
		// selected so the same file can be picked and the change event can fire.
		function rebuildInput(form) {
			form.empty();
			var inp = $('<input type="file" name="svg_file">').appendTo(form);

			function submit() {
				// This submits the form, which returns the file data using svgEditor.uploadSVG
				form.submit();

				rebuildInput(form);
				$.process_cancel("Uploading...", function() {
					cancelled = true;
					$('#dialog_box').hide();
				});
			}

			if(form[0] == open_svg_form[0]) {
				inp.change(function() {
					// This takes care of the "are you sure" dialog box
					svgEditor.openPrep(function(ok) {
						if(!ok) {
							rebuildInput(form);
							return;
						}
						submit();
					});
				});
			} else {
				inp.change(function() {
					// This submits the form, which returns the file data using svgEditor.uploadSVG
					submit();
				});
			}
		}

		// Create the input elements
		rebuildInput(open_svg_form);
		rebuildInput(import_svg_form);
		rebuildInput(import_img_form);

		// Add forms to buttons
		$("#tool_open").show().prepend(open_svg_form);
		$("#tool_import").show().prepend(import_svg_form);
		$("#tool_image").prepend(import_img_form);
	}
});

<|endoftext|>


<commit_before>var util = require('util'),
  dbs = require('./dbs');

var MapDb = dbs.MapDb,
  sourceDb = dbs.sourceDb,
  IncarnationDb = dbs.IncarnationDb,
  Logger = require('./logger');

// opts:
//  id
//  sourceDb
//  map: {
//    db: ...,
//    sourceToMapSeq: ...
//  }
//  incarnations: [
//    {
//      mapToIncSeq: ...,
//      db: ...,
//      reName: ...,
//      gl: ...
//    },
//    ...
//  ]
//  [log]
//
var Syncer = function (opts) {

  var log = opts.log || new Logger();
  var id = opts.id;
  var map = opts.map;
  var sourceToMapSeq = opts.map_last_sync_seq || 0;
  var mapToIncSeqs = {}
  var sourceDb = opts.sourceDb;
  var status = 'idle';
  var incs = opts.incarnations;
  var syncingIncsById;

  this.getSourceToMapSeq = function () {
    return sourceToMapSeq;
  }

  this.getMapToIncSeq = function (incId) {
    return incs[incId].mapToIncSeq;
  }

  this.sync = function (incIds, cb) {
    log.info('syncing ' + id + ' incarnations: ' + incIds);
    syncingIncsById = {};
    incIds.forEach( function (id) {
      syncingIncsById[id] = true;
    });
    status = 'mapDb'
    syncMapDbFromSource( function (err) {
    if (err) {
      log.warn('failed to sync MapDB from source for ' + id);
      stop();
      cb(new Error('MAP_SYNC'));
      return;
    }
    status = 'incarnations';
    syncIncDbsFromMapDb( function (err) {
    if (err) {
      log.warn('failed to sync some incarnation DB\'s from MapDB for ' + id);
      stop();
      cb(new Error('INC_SYNC'));
      return;
    }
    cb();
    });});
  }

  var stop = function () {
    status = 'idle';
    syncingIncsById = null;
  }

  this.addToRunningSync = function (incId) {
    if (status === 'idle' || status === 'incarnations') {
      throw new Error('add_failed');
    }
    syncingIncsById[incId] = true;
  }

  this.isSyncingMap = function () {
    return status === 'map';
  }

  var toInt = function (str) {
    var integer = parseInt(str, 10);
    return (!isNaN(integer) && integer.toString() === str) ? integer : null;
  };

  var keysUnion = function () {
    var args = Array.prototype.slice.call(arguments);
    var i, key, obj, allKeys = {};

    for (i = 0; i < args.length; i++) {
      obj = args[i];
      for (key in obj) {
        allKeys[key] = true;
      }
    }
    return Object.keys(allKeys);
  }

  var emitFor = function (mapFnStr, changes) {
    var adjustedMap = function (doc, emits) {
      var map;
      var require; // eval should be run in a very limited context
      var emit = function (key, value) {
        var emittedKey = JSON.stringify([key, doc._id]);
        emits[emittedKey] = {
          key: key,
          id: doc._id,
          value: value
        }
      }
      eval("map = " + mapFnStr);
      map(doc);
    }

    var emitsForId, emitsByOrigin = {};
    for (var i = 0; i < changes.length; i++) {
      var doc = changes[i].doc;
      var id = changes[i].id;
      if (id[0] !== '_' && !changes[i].changes.deleted) {
        emitsForId = {};
        adjustedMap(doc, emitsForId);
        //if (Object.keys(emitsForId).length !== 0) {
          emitsByOrigin[id] = emitsForId;
        //}
      }
    }
    return emitsByOrigin;
  }

  var compareSets = function (o1, o2) {
    var res = {
      onlyIn1: [],
      onlyIn2: [],
      inBoth: []
    }
    var key;
    for (key in o1) {
      if (o2[key]) {
        res.inBoth.push(key);
      }
      else {
        res.onlyIn1.push(key);
      }
    }
    for (key in o2) {
      if (!o1[key]) {
        res.onlyIn2.push(key);
      }
    }
    return res;
  }

  var createChangeSet = function (oldDocs, newDocs) {
    var cmp = compareSets(newDocs || {}, oldDocs || {});
    var keysToAdd = cmp.onlyIn1;
    var keysToDelete = cmp.onlyIn2;
    //TODO keysToUpdate: in case of recovering from failure, we wouldn't know
    //  which of the destination's docs are already updated.
    //  Currently, we update them again to make sure, but it does change their
    //  _rev num. Instead, we could diff and skip identical docs, but we would
    //  also skip identical docs with just a rev-change. What's better?
    var keysToUpdate = cmp.inBoth;

    var changeSet = [];
    var i, key, operation, oldDoc;

    for (i = 0; i < keysToAdd.length; i++) {
      key = keysToAdd[i];
      operation = newDocs[key];
      operation._id = key;
      changeSet.push(operation);
    }
    for (i = 0; i < keysToDelete.length; i++) {
      key = keysToDelete[i];
      operation = {
        _id: key,
        _rev: oldDocs[key]._rev,
        _deleted: true
      }
      changeSet.push(operation);
    }
    for (i = 0; i < keysToUpdate.length; i++) {
      key = keysToUpdate[i];
      operation = newDocs[key];
      operation._id = key;
      operation._rev = oldDocs[key]._rev,
      changeSet.push(operation);
    }
    return changeSet;
  }

  var incKeysFromMapKeys = function (changes, groupLevel) {
    var glNumber = toInt(groupLevel);
    var incKeys = {};
    if (groupLevel === "0") {
      for (var i = 0; i < changes.length; i++) {
        if (changes[i].id[0] !== '_') {
          incKeys[JSON.stringify(null)] = true;
        }
      }
    }
    else {
      changes.forEach( function (change) {
        if (change.id[0] === '_') return;
        var mapKey = JSON.parse(change.id)[0];
        var incKey = (Array.isArray(mapKey) && glNumber !== null) ? mapKey.slice(0, glNumber) : mapKey;
        incKeys[JSON.stringify(incKey)] = true;
      });
    }
    return Object.keys(incKeys);
  }

  // opts:
  //  db
  //  viewPath
  //  queriesById
  //  rowToDbEntry
  var getViewResultsByQueryAsDictionaries = function (opts, cb) {
    var rowToDbEntry = opts.rowToDbEntry;
    opts.db.multiqueryView(opts.viewPath, opts.queriesById, function (err, resultsByQueryId) {
      if (err) { cb(err); return; }
      var valuesByOrigin = {};
      for (var id in resultsByQueryId) {
        resultsByQueryId[id].rows.forEach( function (row) {
          var entry = rowToDbEntry(row, id);
          valuesByOrigin[id] = valuesByOrigin[id] || {};
          valuesByOrigin[id][entry.docId] = entry.docContents;
        });
      }
      cb(null, valuesByOrigin);
    });
  }

  var syncMapDbFromSource = function (cb) {

    var getPastEmitsMetaByOrigin = function (changes, cb) {
      var docId, change, queriesById = {};
      for (var i = 0; i < changes.length; i++) {
        change = changes[i];
        docId = change.id;
        if (docId[0] !== '_' && !change.deleted) {
          queriesById[docId] = {key: docId};
        }
      }
      getViewResultsByQueryAsDictionaries(
        {
          db: map.db,
          viewPath: '_design/incarnate/_view/meta_by_origin',
          queriesById: queriesById,
          rowToDbEntry: function (row, idString) {
            return {
              docId: JSON.stringify(row.id),
              docContents: { _rev: row.value._rev }
            };
          }
        },
        cb
      );
    }

    // 1. calculate map-values resulting from recently updated sourceDb docs
    log.trace('syncer ' + id + ': calculating new map-values');
    var changesOpts = {
      since: sourceToMapSeq,
      include_docs: true,
      reduce: false
    }
    sourceDb.changes(changesOpts, function (err, changes) {
    if (err) {
      log.warn('syncer ' + id + ': failed to fetch changes from source');
      cb(err);
      return;
    }
    if (changes.results.length === 0) {
      cb();
      return;
    }
    var changes = changes.results;
    var emitsByOrigin = emitFor(map.fn, changes);

    // 2. get previous map-values resulting from same sourceDb docs
    log.trace('syncer ' + id + ': fetching previous map-values');
    getPastEmitsMetaByOrigin(changes, function (err, pastEmitsMetaByOrigin) {
    if (err) {
      log.warn('syncer ' + id + ': failed to fetch docs from MapDB');
      cb(err);
      return;
    }

    // 3. deduce change-set between new and previous values
    log.trace('syncer ' + id + ': submitting change-set to MapDB');
    var origins = keysUnion(emitsByOrigin, pastEmitsMetaByOrigin);
    var changeSet = [];
    var originChangeSet;
    for (var i = 0; i < origins.length; i++) {
      origin = origins[i];
      originChangeSet = createChangeSet(
        pastEmitsMetaByOrigin[origin],
        emitsByOrigin[origin]
      );
      changeSet = changeSet.concat(originChangeSet);
    }
    map.db.bulkDocs({docs: changeSet, all_or_nothing: true}, function (err, docs) {
    if (err) {
      log.warn('syncer ' + id + ': failed to submit change-set to MapDB');
      cb(err);
      return;
    }

    sourceToMapSeq = changes[changes.length - 1].seq;
    cb();
    });});});
  }

  var syncIncDbsFromMapDb = function (cb) {
    var syncsNotDone = Object.keys(syncingIncsById).length;
    var stopped = false;
    for (var incId in syncingIncsById) {
      syncInc(incId, function (err) {
        syncsNotDone--;
        if (stopped) return;
        if (err) {
          stopped = true;
          cb(err);
          return;
        }
        if (syncsNotDone === 0) {
          cb();
        }
      });
    }
  }

  var syncInc = function (incId, cb) {

    var newIncValuesByOrigin = function (queryKeys, reName, gl, cb) {
      var queriesById = gl === '0' ?
        {null: {group: true, group_level: 0}}
        :
        (function () {
          var retval = {};
          var intGl = toInt(inc.gl);
          queryKeys.forEach( function (key) {
            var parsedKey = JSON.parse(key);
            retval[key] = intGl !== null && Array.isArray(parsedKey) && parsedKey.length >= intGl ?
              {
                startkey: parsedKey,
                endkey: [].concat(parsedKey).concat([{}]),
                group: true
              } :
              {
                key: parsedKey,
                group: true
              };
            if (intGl !== null) {
              retval[key].group_level = intGl;
            }
          });
          return retval;
        }());

      getViewResultsByQueryAsDictionaries(
        {
          db: map.db,
          viewPath: '_design/incarnate/_view/' + reName,
          queriesById: queriesById,
          rowToDbEntry: function (row, qId) {
            var unstringifiedDocId, query = queriesById[qId];
            if (query.startkey !== undefined) {
              unstringifiedDocId = query.startkey;
            }
            else if (query.key !== undefined) {
              unstringifiedDocId = query.key;
            }
            else {
              unstringifiedDocId = null;
            }
            return {
              docId: qId,
              docContents: {
                key: unstringifiedDocId,
                value: row.value
              }
            };
          }
        },
        cb
      );
    }

    var pastIncValuesByOrigin = function (queryKeys, incDb, cb) {
      queriesById= {};
      queryKeys.forEach( function (key) {
        queriesById[key] = {key: key};
      });

      //TODO doc heads are enough, we only want to figure out if they already exist
      getViewResultsByQueryAsDictionaries(
        {
          db: incDb,
          viewPath: '_all_docs',
          queriesById: queriesById,
          rowToDbEntry: function (row, idString) {
            return {
              docId: row.key,
              docContents: {_rev: row.value.rev}
            };
          },
        },
        cb
      );
    }

    var inc = incs[incId];

    var logPrefix = 'syncer ' + id + ': incarnation ' + incId + ': ';

    log.trace(logPrefix + 'fetching changes from MapDB');
    map.db.changes({since: inc.mapToIncSeq}, function (err, changes) {
    log.trace('MapDB changes: ' + JSON.stringify(changes, null, ' '));
    if (err) {
      log.warn(logPrefix + 'failed to fetch changes from MapDB');
      cb(err);
      return;
    }
    if (changes.results.length === 0) {
      cb();
      return;
    }
    var queryKeys = incKeysFromMapKeys(changes.results, inc.gl);

    log.trace(logPrefix + 'fetch new values from MapDB');
    newIncValuesByOrigin(queryKeys, inc.reName, inc.gl, function (err, valuesByOrigin) {
    if (err) {
      log.warn(logPrefix + 'failed to fetch new values from MapDB');
      cb(err);
      return;
    }

    log.trace(logPrefix + 'fetch previous incarnationDB values');
    pastIncValuesByOrigin(queryKeys, inc.db, function (err, pastValuesByOrigin) {
    if (err) {
      log.warn(logPrefix + 'failed to fetch previous docs from incarnationDB');
      cb(err);
      return;
    }

    log.trace(logPrefix + 'submit change-set to incarnationDB');
    var changeSet = [], keyChangeSet;
    queryKeys.forEach( function (origin) {
      var originChangeSet = createChangeSet(pastValuesByOrigin[origin], valuesByOrigin[origin]);
      changeSet = changeSet.concat(originChangeSet);
    });

    inc.db.bulkDocs({docs: changeSet, all_or_nothing: true}, function (err) {
    if (err) {
      log.warn(logPrefix + 'failed to submit changes to incarnationDB');
      cb(err);
      return;
    }

    inc.mapToIncSeq = changes.last_seq;

    cb();
    });});});});
  }
}

var errorCodes = {
  MAP_SYNC: 0,
  INC_SYNC: 1,
  ADD: 2
}

var SyncerError = function (errCode) {
  Error.apply(this);
  this.code = errCode;
}
util.inherits(SyncerError, Error);

Syncer.errorCodes = errorCodes;
module.exports = Syncer;
<commit_msg>reading changes from source and map DB's is now done in batches to avoid eating up lots of memory<commit_after>var util = require('util'),
  dbs = require('./dbs');

var MapDb = dbs.MapDb,
  sourceDb = dbs.sourceDb,
  IncarnationDb = dbs.IncarnationDb,
  Logger = require('./logger');

// opts:
//  id
//  sourceDb
//  map: {
//    db: ...,
//    sourceToMapSeq: ...
//  }
//  incarnations: [
//    {
//      mapToIncSeq: ...,
//      db: ...,
//      reName: ...,
//      gl: ...
//    },
//    ...
//  ]
//  [log]
//
var Syncer = function (opts) {

  var log = opts.log || new Logger();
  var id = opts.id;
  var map = opts.map;
  var sourceToMapSeq = opts.map_last_sync_seq || 0;
  var mapToIncSeqs = {}
  var sourceDb = opts.sourceDb;
  var status = 'idle';
  var incs = opts.incarnations;
  var syncingIncsById;

  this.getSourceToMapSeq = function () {
    return sourceToMapSeq;
  }

  this.getMapToIncSeq = function (incId) {
    return incs[incId].mapToIncSeq;
  }

  this.sync = function (incIds, cb) {
    log.info('syncing ' + id + ' incarnations: ' + incIds);
    syncingIncsById = {};
    incIds.forEach( function (id) {
      syncingIncsById[id] = true;
    });
    status = 'mapDb'
    syncMapDbFromSource( function (err) {
    if (err) {
      log.warn('failed to sync MapDB from source for ' + id);
      stop();
      cb(new Error('MAP_SYNC'));
      return;
    }
    status = 'incarnations';
    syncIncDbsFromMapDb( function (err) {
    if (err) {
      log.warn('failed to sync some incarnation DB\'s from MapDB for ' + id);
      stop();
      cb(new Error('INC_SYNC'));
      return;
    }
    cb();
    });});
  }

  var stop = function () {
    status = 'idle';
    syncingIncsById = null;
  }

  this.addToRunningSync = function (incId) {
    if (status === 'idle' || status === 'incarnations') {
      throw new Error('add_failed');
    }
    syncingIncsById[incId] = true;
  }

  this.isSyncingMap = function () {
    return status === 'map';
  }

  var toInt = function (str) {
    var integer = parseInt(str, 10);
    return (!isNaN(integer) && integer.toString() === str) ? integer : null;
  };

  var keysUnion = function () {
    var args = Array.prototype.slice.call(arguments);
    var i, key, obj, allKeys = {};

    for (i = 0; i < args.length; i++) {
      obj = args[i];
      for (key in obj) {
        allKeys[key] = true;
      }
    }
    return Object.keys(allKeys);
  }

  var emitFor = function (mapFnStr, changes) {
    var adjustedMap = function (doc, emits) {
      var map;
      var require; // eval should be run in a very limited context
      var emit = function (key, value) {
        var emittedKey = JSON.stringify([key, doc._id]);
        emits[emittedKey] = {
          key: key,
          id: doc._id,
          value: value
        }
      }
      eval("map = " + mapFnStr);
      map(doc);
    }

    var emitsForId, emitsByOrigin = {};
    for (var i = 0; i < changes.length; i++) {
      var doc = changes[i].doc;
      var id = changes[i].id;
      if (id[0] !== '_' && !changes[i].changes.deleted) {
        emitsForId = {};
        adjustedMap(doc, emitsForId);
        //if (Object.keys(emitsForId).length !== 0) {
          emitsByOrigin[id] = emitsForId;
        //}
      }
    }
    return emitsByOrigin;
  }

  var compareSets = function (o1, o2) {
    var res = {
      onlyIn1: [],
      onlyIn2: [],
      inBoth: []
    }
    var key;
    for (key in o1) {
      if (o2[key]) {
        res.inBoth.push(key);
      }
      else {
        res.onlyIn1.push(key);
      }
    }
    for (key in o2) {
      if (!o1[key]) {
        res.onlyIn2.push(key);
      }
    }
    return res;
  }

  var createChangeSet = function (oldDocs, newDocs) {
    var cmp = compareSets(newDocs || {}, oldDocs || {});
    var keysToAdd = cmp.onlyIn1;
    var keysToDelete = cmp.onlyIn2;
    //TODO keysToUpdate: in case of recovering from failure, we wouldn't know
    //  which of the destination's docs are already updated.
    //  Currently, we update them again to make sure, but it does change their
    //  _rev num. Instead, we could diff and skip identical docs, but we would
    //  also skip identical docs with just a rev-change. What's better?
    var keysToUpdate = cmp.inBoth;

    var changeSet = [];
    var i, key, operation, oldDoc;

    for (i = 0; i < keysToAdd.length; i++) {
      key = keysToAdd[i];
      operation = newDocs[key];
      operation._id = key;
      changeSet.push(operation);
    }
    for (i = 0; i < keysToDelete.length; i++) {
      key = keysToDelete[i];
      operation = {
        _id: key,
        _rev: oldDocs[key]._rev,
        _deleted: true
      }
      changeSet.push(operation);
    }
    for (i = 0; i < keysToUpdate.length; i++) {
      key = keysToUpdate[i];
      operation = newDocs[key];
      operation._id = key;
      operation._rev = oldDocs[key]._rev,
      changeSet.push(operation);
    }
    return changeSet;
  }

  var incKeysFromMapKeys = function (changes, groupLevel) {
    var glNumber = toInt(groupLevel);
    var incKeys = {};
    if (groupLevel === "0") {
      for (var i = 0; i < changes.length; i++) {
        if (changes[i].id[0] !== '_') {
          incKeys[JSON.stringify(null)] = true;
        }
      }
    }
    else {
      changes.forEach( function (change) {
        if (change.id[0] === '_') return;
        var mapKey = JSON.parse(change.id)[0];
        var incKey = (Array.isArray(mapKey) && glNumber !== null) ? mapKey.slice(0, glNumber) : mapKey;
        incKeys[JSON.stringify(incKey)] = true;
      });
    }
    return Object.keys(incKeys);
  }

  // opts:
  //  db
  //  viewPath
  //  queriesById
  //  rowToDbEntry
  var getViewResultsByQueryAsDictionaries = function (opts, cb) {
    var rowToDbEntry = opts.rowToDbEntry;
    opts.db.multiqueryView(opts.viewPath, opts.queriesById, function (err, resultsByQueryId) {
      if (err) { cb(err); return; }
      var valuesByOrigin = {};
      for (var id in resultsByQueryId) {
        resultsByQueryId[id].rows.forEach( function (row) {
          var entry = rowToDbEntry(row, id);
          valuesByOrigin[id] = valuesByOrigin[id] || {};
          valuesByOrigin[id][entry.docId] = entry.docContents;
        });
      }
      cb(null, valuesByOrigin);
    });
  }

  var syncMapDbFromSourceChanges = function (since, limit, cb) {

    var getPastEmitsMetaByOrigin = function (changes, cb) {
      var docId, change, queriesById = {};
      for (var i = 0; i < changes.length; i++) {
        change = changes[i];
        docId = change.id;
        if (docId[0] !== '_' && !change.deleted) {
          queriesById[docId] = {key: docId};
        }
      }
      getViewResultsByQueryAsDictionaries(
        {
          db: map.db,
          viewPath: '_design/incarnate/_view/meta_by_origin',
          queriesById: queriesById,
          rowToDbEntry: function (row, idString) {
            return {
              docId: JSON.stringify(row.id),
              docContents: { _rev: row.value._rev }
            };
          }
        },
        cb
      );
    }

    // 1. calculate map-values resulting from recently updated sourceDb docs
    log.trace('syncer ' + id + ': calculating new map-values');
    var changesOpts = {
      since: since,
      limit: limit,
      include_docs: true,
      reduce: false
    }
    sourceDb.changes(changesOpts, function (err, changes) {
    if (err) {
      log.warn('syncer ' + id + ': failed to fetch changes from source');
      cb(err);
      return;
    }
    if (changes.results.length === 0) {
      cb(null, since, 0);
      return;
    }
    var changes = changes.results;
    var emitsByOrigin = emitFor(map.fn, changes);

    // 2. get previous map-values resulting from same sourceDb docs
    log.trace('syncer ' + id + ': fetching previous map-values');
    getPastEmitsMetaByOrigin(changes, function (err, pastEmitsMetaByOrigin) {
    if (err) {
      log.warn('syncer ' + id + ': failed to fetch docs from MapDB');
      cb(err);
      return;
    }

    // 3. deduce change-set between new and previous values
    log.trace('syncer ' + id + ': submitting change-set to MapDB');
    var origins = keysUnion(emitsByOrigin, pastEmitsMetaByOrigin);
    var changeSet = [];
    var originChangeSet;
    for (var i = 0; i < origins.length; i++) {
      origin = origins[i];
      originChangeSet = createChangeSet(
        pastEmitsMetaByOrigin[origin],
        emitsByOrigin[origin]
      );
      changeSet = changeSet.concat(originChangeSet);
    }
    map.db.bulkDocs({docs: changeSet, all_or_nothing: true}, function (err, docs) {
    if (err) {
      log.warn('syncer ' + id + ': failed to submit change-set to MapDB');
      cb(err);
      return;
    }

    cb(null, changes[changes.length - 1].seq, changes.length);
    });});});
  }

  var syncMapDbFromSource = function (cb) {
    var since = sourceToMapSeq;
    var limit = 3000;
    var syncCallback = function (err, lastSyncedSeq, changesSyncedNum) {
      if (err) {
        cb(err);
        return;
      }
      if (changesSyncedNum < limit) {
        sourceToMapSeq = lastSyncedSeq;
        cb();
        return;
      }
      since = lastSyncedSeq;
      syncMapDbFromSourceChanges(since, limit, syncCallback);
    }
    syncMapDbFromSourceChanges(since, limit, syncCallback);
  }

  var syncIncDbsFromMapDb = function (cb) {
    var syncsNotDone = Object.keys(syncingIncsById).length;
    var stopped = false;
    for (var incId in syncingIncsById) {
      syncIncFromMapDb(incId, function (err) {
        syncsNotDone--;
        if (stopped) return;
        if (err) {
          stopped = true;
          cb(err);
          return;
        }
        if (syncsNotDone === 0) {
          cb();
        }
      });
    }
  }

  var syncIncFromMapDb = function (incId, cb) {
    var inc = incs[incId];
    var since = inc.mapToIncSeq;
    var limit = 3000;
    var syncCallback = function (err, lastSyncedSeq, changesSyncedNum) {
      if (err) {
        cb(err);
        return;
      }
      if (changesSyncedNum < limit) {
        inc.mapToIncSeq = lastSyncedSeq;
        cb();
        return;
      }
      since = lastSyncedSeq;
      syncIncFromMapDbChanges(incId, since, limit, syncCallback);
    }
    syncIncFromMapDbChanges(incId, since, limit, syncCallback);
  }

  var syncIncFromMapDbChanges = function (incId, since, limit, cb) {

    var newIncValuesByOrigin = function (queryKeys, reName, gl, cb) {
      var queriesById = gl === '0' ?
        {null: {group: true, group_level: 0}}
        :
        (function () {
          var retval = {};
          var intGl = toInt(inc.gl);
          queryKeys.forEach( function (key) {
            var parsedKey = JSON.parse(key);
            retval[key] = intGl !== null && Array.isArray(parsedKey) && parsedKey.length >= intGl ?
              {
                startkey: parsedKey,
                endkey: [].concat(parsedKey).concat([{}]),
                group: true
              } :
              {
                key: parsedKey,
                group: true
              };
            if (intGl !== null) {
              retval[key].group_level = intGl;
            }
          });
          return retval;
        }());

      getViewResultsByQueryAsDictionaries(
        {
          db: map.db,
          viewPath: '_design/incarnate/_view/' + reName,
          queriesById: queriesById,
          rowToDbEntry: function (row, qId) {
            var unstringifiedDocId, query = queriesById[qId];
            if (query.startkey !== undefined) {
              unstringifiedDocId = query.startkey;
            }
            else if (query.key !== undefined) {
              unstringifiedDocId = query.key;
            }
            else {
              unstringifiedDocId = null;
            }
            return {
              docId: qId,
              docContents: {
                key: unstringifiedDocId,
                value: row.value
              }
            };
          }
        },
        cb
      );
    }

    var pastIncValuesByOrigin = function (queryKeys, incDb, cb) {
      queriesById= {};
      queryKeys.forEach( function (key) {
        queriesById[key] = {key: key};
      });

      //TODO doc heads are enough, we only want to figure out if they already exist
      getViewResultsByQueryAsDictionaries(
        {
          db: incDb,
          viewPath: '_all_docs',
          queriesById: queriesById,
          rowToDbEntry: function (row, idString) {
            return {
              docId: row.key,
              docContents: {_rev: row.value.rev}
            };
          },
        },
        cb
      );
    }

    var inc = incs[incId];

    var logPrefix = 'syncer ' + id + ': incarnation ' + incId + ': ';

    log.trace(logPrefix + 'fetching changes from MapDB');
    map.db.changes({since: since, limit: limit}, function (err, changes) {
    log.trace('MapDB changes: ' + JSON.stringify(changes, null, ' '));
    if (err) {
      log.warn(logPrefix + 'failed to fetch changes from MapDB');
      cb(err);
      return;
    }
    if (changes.results.length === 0) {
      cb(null, since, 0);
      return;
    }
    var queryKeys = incKeysFromMapKeys(changes.results, inc.gl);

    log.trace(logPrefix + 'fetch new values from MapDB');
    newIncValuesByOrigin(queryKeys, inc.reName, inc.gl, function (err, valuesByOrigin) {
    if (err) {
      log.warn(logPrefix + 'failed to fetch new values from MapDB');
      cb(err);
      return;
    }

    log.trace(logPrefix + 'fetch previous incarnationDB values');
    pastIncValuesByOrigin(queryKeys, inc.db, function (err, pastValuesByOrigin) {
    if (err) {
      log.warn(logPrefix + 'failed to fetch previous docs from incarnationDB');
      cb(err);
      return;
    }

    log.trace(logPrefix + 'submit change-set to incarnationDB');
    var changeSet = [], keyChangeSet;
    queryKeys.forEach( function (origin) {
      var originChangeSet = createChangeSet(pastValuesByOrigin[origin], valuesByOrigin[origin]);
      changeSet = changeSet.concat(originChangeSet);
    });

    inc.db.bulkDocs({docs: changeSet, all_or_nothing: true}, function (err) {
    if (err) {
      log.warn(logPrefix + 'failed to submit changes to incarnationDB');
      cb(err);
      return;
    }

    cb(null, changes.last_seq, changes.results.length);
    });});});});
  }
}

var errorCodes = {
  MAP_SYNC: 0,
  INC_SYNC: 1,
  ADD: 2
}

var SyncerError = function (errCode) {
  Error.apply(this);
  this.code = errCode;
}
util.inherits(SyncerError, Error);

Syncer.errorCodes = errorCodes;
module.exports = Syncer;
<|endoftext|>


<commit_before>function execute_module_group_callback(fieldObj, newValue)
{
    let obj = {
        type: 'autocomplete'
    }

    if(!isNaN(Number(newValue.value)))
    {
        let list_obj = []
        let new_value = newValue.value
        let inventory_path = '/inventory/{inventory_id}'

        list_obj.push(projPath + inventory_path + '/all_groups/')
        list_obj.push(projPath + inventory_path + '/all_hosts/')

        let additional_props = {
            api_inventory_id: new_value
        }

        obj.override_opt = {
            dynamic_properties:{
                list_obj: list_obj,
                value_field:'name',
                view_field:'name',
                url_vars: additional_props
            }
        }
    }

    return obj;
}

tabSignal.connect("openapi.schema.definition.AnsibleModule", function(obj) {
    let properties = obj.definition.properties;

    properties.inventory = {
        name: 'inventory',
        title: 'Inventory',
        description: 'specify inventory host path or comma separated host list. --inventory-file is deprecated',
        type: 'string',
        format: 'hybrid_autocomplete',
        dynamic_properties: {
            list_obj:projPath + "/inventory/",
            value_field:'id',
            view_field:'name',
        },
        __func__custom_getValue: 'inventory_hybrid_autocomplete_getValue',

    }
    properties.group = {
        name: 'group',
        title: 'Group',
        type: 'string',
        format: 'dynamic',
        default: 'all',
        parent_field: ['inventory'],
        dynamic_properties: {
            __func__callback: 'execute_module_group_callback',
            value_field:'name',
            view_field:'name',
            list_obj: []
        }
    }
})

tabSignal.connect("openapi.schema.definition.AnsiblePlaybook", function(obj) {
    let properties = obj.definition.properties;

    properties.inventory = {
        name: 'inventory',
        title: 'Inventory',
        description: 'specify inventory host path or comma separated host list. --inventory-file is deprecated',
        type: 'string',
        format: 'hybrid_autocomplete',
        dynamic_properties: {
            list_obj:projPath + "/inventory/",
            value_field:'id',
            view_field:'name',
        },
        __func__custom_getValue: 'inventory_hybrid_autocomplete_getValue',

    }

    properties.limit = {
        name: 'limit',
        title: 'Limit',
        description: "further limit selected hosts to an additional pattern",
        type: 'string',
        format: 'dynamic',
        default: 'all',
        parent_field: ['inventory'],
        dynamic_properties: {
            __func__callback: 'execute_module_group_callback',
            value_field:'name',
            view_field:'name',
            list_obj: []
        }
    }
})

tabSignal.connect("openapi.schema", function(data)
{
    window.guiSchema.path["/project/{pk}/"].schema.edit.fields.execute_view_data.format = 'null'
})

gui_project = {

    polemarchYamlForm:undefined,

    hasForm:function()
    {
        return this.model && this.model.data && this.model.data.execute_view_data != null;
    },

    executePlaybook:function(data)
    {
        var def = new $.Deferred();

        let q = {
            data_type:["project", this.url_vars.api_pk, "execute_playbook"],
            data:data,
            method:'post'
        }

        $.when(this.apiQuery(q)).done(data =>
        {
            def.resolve(data)
            vstGO(['project', this.url_vars.api_pk, 'history', data.history_id])
        }).fail(e => {
            this.showErrors(e, q.method)
            def.reject(e)
        })
        def.reject()

        return def.promise();
    },

    renderForm:function()
    {
        let thisObj = this;
        let extra_fields = $.extend(true, {}, this.model.data.execute_view_data.fields)
        for(let i in this.model.data.execute_view_data.playbooks)
        {
            let val = this.model.data.execute_view_data.playbooks[i]

            extra_fields[i] = {
                title:val.title,
                text:val.title,
                description: val.help || val.description,
                format:'formButton',
                value: i,
                class:'btn-primary',
                onclick:function(){
                    let val = thisObj.polemarchYamlForm.getValue()
                    val.playbook = this.getValue()

                    delete val.extra_vars[val.playbook]
                    val.extra_vars = JSON.stringify(val.extra_vars);

                    return thisObj.executePlaybook(val)
                }
            }
        }

        let formData = {
            title:"Deploy",
            form:{
                    'inventory' : {
                        title:'inventory',
                        required:true,
                        format:'hybrid_autocomplete',
                        dynamic_properties:{
                            list_obj: "/project/{pk}/inventory/",
                            value_field: "id",
                            view_field: "name",
                        }
                    },
                    user:{
                        title:'User',
                        description: "connect as this user (default=None)",
                        format:'string',
                        type: "string",
                    },
                    key_file: {
                        title:'Key file',
                        description: "use this file to authenticate the connection",
                        format:'secretfile',
                        type: "string",
                        dynamic_properties:{
                            list_obj: "/project/{pk}/inventory/",
                            value_field: "id",
                            view_field: "name",
                        }
                    },
                    extra_vars: {
                        title:"Execute parametrs",
                        format:'form',
                        form:extra_fields
                    }
            }
        }

        this.polemarchYamlForm = new guiElements.form(undefined, formData);
        return this.polemarchYamlForm.render();
    },
}

guiElements.form = function(opt = {}, value, parent_object)
{
    this.name = 'form'
    guiElements.base.apply(this, arguments)

    this.realElements = {};

    this.prepareFieldOptions = function(field)
    {
        if(field.enum)
        {
            field.format = "enum"
        }

        return field
    }

    this.setValue = function(value)
    {
        this.value = value
        let realElements = {};
        if(value.form)
        {
            for(let i in value.form)
            {
                let field = value.form[i]
                field.name = i

                field = this.prepareFieldOptions(field)
                let type = getFieldType(field)

                realElements[i] = new guiElements[type]($.extend(true, {}, field), field.value);
            }
        }

        this.realElements = realElements
    }

    if(opt.form && !value)
    {
        this.setValue(opt)
    }
    else
    {
        this.setValue(value)
    }

    this.insertTestValue = function(value)
    {
        this.setValue(value);
        return value;
    }

    this.getValue = function()
    {
        let valueObj = {};
        for(let element_name in this.realElements)
        {
            let element = this.realElements[element_name];
            valueObj[element_name] = element.getValue();
        }

        return this.reductionToType(valueObj);
    }

    this.getValidValue = function()
    {
        let valueObj = {};
        for(let element_name in this.realElements)
        {
            let element = this.realElements[element_name];
            valueObj[element_name] = element.getValidValue();
        }

        return this.reductionToType(valueObj);
    }
}<commit_msg>change format for detail module WIP [ci skip]<commit_after>function execute_module_group_callback(fieldObj, newValue)
{
    let obj = {
        type: 'autocomplete'
    }

    if(!isNaN(Number(newValue.value)))
    {
        let list_obj = []
        let new_value = newValue.value
        let inventory_path = '/inventory/{inventory_id}'

        list_obj.push(projPath + inventory_path + '/all_groups/')
        list_obj.push(projPath + inventory_path + '/all_hosts/')

        let additional_props = {
            api_inventory_id: new_value
        }

        obj.override_opt = {
            dynamic_properties:{
                list_obj: list_obj,
                value_field:'name',
                view_field:'name',
                url_vars: additional_props
            }
        }
    }

    return obj;
}

tabSignal.connect("openapi.schema.definition.AnsibleModule", function(obj) {
    let properties = obj.definition.properties;

    properties.inventory = {
        name: 'inventory',
        title: 'Inventory',
        description: 'specify inventory host path or comma separated host list. --inventory-file is deprecated',
        type: 'string',
        format: 'hybrid_autocomplete',
        dynamic_properties: {
            list_obj:projPath + "/inventory/",
            value_field:'id',
            view_field:'name',
        },
        __func__custom_getValue: 'inventory_hybrid_autocomplete_getValue',

    }
    properties.group = {
        name: 'group',
        title: 'Group',
        type: 'string',
        format: 'dynamic',
        default: 'all',
        parent_field: ['inventory'],
        dynamic_properties: {
            __func__callback: 'execute_module_group_callback',
            value_field:'name',
            view_field:'name',
            list_obj: []
        }
    }
})

tabSignal.connect("openapi.schema.definition.AnsiblePlaybook", function(obj) {
    let properties = obj.definition.properties;

    properties.inventory = {
        name: 'inventory',
        title: 'Inventory',
        description: 'specify inventory host path or comma separated host list. --inventory-file is deprecated',
        type: 'string',
        format: 'hybrid_autocomplete',
        dynamic_properties: {
            list_obj:projPath + "/inventory/",
            value_field:'id',
            view_field:'name',
        },
        __func__custom_getValue: 'inventory_hybrid_autocomplete_getValue',

    }

    properties.limit = {
        name: 'limit',
        title: 'Limit',
        description: "further limit selected hosts to an additional pattern",
        type: 'string',
        format: 'dynamic',
        default: 'all',
        parent_field: ['inventory'],
        dynamic_properties: {
            __func__callback: 'execute_module_group_callback',
            value_field:'name',
            view_field:'name',
            list_obj: []
        }
    }
})

tabSignal.connect("openapi.schema", function(data)
{
    window.guiSchema.path["/project/{pk}/"].schema.edit.fields.execute_view_data.format = 'null'
})

gui_project = {

    polemarchYamlForm:undefined,

    hasForm:function()
    {
        return this.model && this.model.data && this.model.data.execute_view_data != null;
    },

    executePlaybook:function(data)
    {
        var def = new $.Deferred();

        let q = {
            data_type:["project", this.url_vars.api_pk, "execute_playbook"],
            data:data,
            method:'post'
        }

        $.when(this.apiQuery(q)).done(data =>
        {
            def.resolve(data)
            vstGO(['project', this.url_vars.api_pk, 'history', data.history_id])
        }).fail(e => {
            this.showErrors(e, q.method)
            def.reject(e)
        })
        def.reject()

        return def.promise();
    },

    renderForm:function()
    {
        let thisObj = this;
        let extra_fields = $.extend(true, {}, this.model.data.execute_view_data.fields)
        for(let i in this.model.data.execute_view_data.playbooks)
        {
            let val = this.model.data.execute_view_data.playbooks[i]

            extra_fields[i] = {
                title:val.title,
                text:val.title,
                description: val.help || val.description,
                format:'formButton',
                value: i,
                class:'btn-primary',
                onclick:function(){
                    let val = thisObj.polemarchYamlForm.getValue()
                    val.playbook = this.getValue()

                    delete val.extra_vars[val.playbook]
                    val.extra_vars = JSON.stringify(val.extra_vars);

                    return thisObj.executePlaybook(val)
                }
            }
        }

        let formData = {
            title:"Deploy",
            form:{
                    'inventory' : {
                        title:'inventory',
                        required:true,
                        format:'hybrid_autocomplete',
                        dynamic_properties:{
                            list_obj: "/project/{pk}/inventory/",
                            value_field: "id",
                            view_field: "name",
                        }
                    },
                    user:{
                        title:'User',
                        description: "connect as this user (default=None)",
                        format:'string',
                        type: "string",
                    },
                    key_file: {
                        title:'Key file',
                        description: "use this file to authenticate the connection",
                        format:'secretfile',
                        type: "string",
                        dynamic_properties:{
                            list_obj: "/project/{pk}/inventory/",
                            value_field: "id",
                            view_field: "name",
                        }
                    },
                    extra_vars: {
                        title:"Execute parametrs",
                        format:'form',
                        form:extra_fields
                    }
            }
        }

        this.polemarchYamlForm = new guiElements.form(undefined, formData);
        return this.polemarchYamlForm.render();
    },
}

guiElements.form = function(opt = {}, value, parent_object)
{
    this.name = 'form'
    guiElements.base.apply(this, arguments)

    this.realElements = {};

    this.prepareFieldOptions = function(field)
    {
        if(field.enum)
        {
            field.format = "enum"
        }

        return field
    }

    this.setValue = function(value)
    {
        this.value = value
        let realElements = {};
        if(value.form)
        {
            for(let i in value.form)
            {
                let field = value.form[i]
                field.name = i

                field = this.prepareFieldOptions(field)
                let type = getFieldType(field)

                realElements[i] = new guiElements[type]($.extend(true, {}, field), field.value);
            }
        }

        this.realElements = realElements
    }

    if(opt.form && !value)
    {
        this.setValue(opt)
    }
    else
    {
        this.setValue(value)
    }

    this.insertTestValue = function(value)
    {
        this.setValue(value);
        return value;
    }

    this.getValue = function()
    {
        let valueObj = {};
        for(let element_name in this.realElements)
        {
            let element = this.realElements[element_name];
            valueObj[element_name] = element.getValue();
        }

        return this.reductionToType(valueObj);
    }

    this.getValidValue = function()
    {
        let valueObj = {};
        for(let element_name in this.realElements)
        {
            let element = this.realElements[element_name];
            valueObj[element_name] = element.getValidValue();
        }

        return this.reductionToType(valueObj);
    }
}

tabSignal.connect("openapi.schema.schema", function(obj)
{
    if (obj.path == '/project/{pk}/module/{module_id}/')
    {
        obj.value.fields.data.format = 'json'
    }
})<|endoftext|>


<commit_before>enyo.kind({
	name: "enyo.XmlpRequest",
	kind: enyo.JsonpRequest,
	published: {
		detailedErrors:true 	//if true, on a query failure, it will re-query the xml url/params
					//to get the plain-text content and return that in the format
					// of {"error":"unescaped-plain-text-content"}
	},
	callbackName: "callback",
	go: function(inParams) {
		var parts = this.url.split("?");
		var uri = parts.shift() || "";
		var args = parts.join("?").split("&");
		args.push(enyo.Ajax.objectToQuery(inParams || {}));
		this.xmlUrl = [uri, args.join("&")].join("?");
		this.url = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D'"
				+ encodeURIComponent(this.xmlUrl) + "'&format=json";
		inParams = {};
		this.inherited(arguments);
	},
	respond: function(inValue) {
		if(inValue && inValue.query) {
			inValue = inValue.query.results || undefined;
			if(!inValue && this.detailedErrors) {
				var fallbackUrl = "http://query.yahooapis.com/v1/public/yql?q=use%20%22http"
						+ "%3A%2F%2Fwww.datatables.org%2Fdata%2Fhtmlstring.xml%22%20"
						+ "as%20htmlstring%3B%20select%20*%20from%20htmlstring%20where"
						+ "%20url%3D'"+ encodeURIComponent(this.xmlUrl) + "'&format=json";
				this.fallbackTextRequest = new enyo.JsonpRequest({url:fallbackUrl,
						callbackName:"callback"});
				this.fallbackTextRequest.response(this, "fallbackTextCallback");
				this.fallbackTextRequest.go();
				return;
			}
		}
		this.inherited(arguments);
	},
	fallbackTextCallback: function(inSender, inResponse) {
		if(inResponse && inResponse.query) {
			inResponse = inResponse.query.results || undefined;
			if(inResponse && inResponse.result) {
				inResponse = inResponse.result;
				if(inResponse.indexOf("HTML Tidy for Java")>-1) {
					// plain text is within auto-generated html, so extract it
					var text = inResponse.substring(inResponse.indexOf("
")+3,
							inResponse.lastIndexOf("
"));
					inResponse = {"error":text};

				} else {
					inResponse = {"error":"Query is not in XML format"};
				}
			} else if(inResponse && !inResponse.result) {
				inResponse = {"error":"Unknown XML query failure"};
			} else if(!inResponse) {
				inResponse = {"error":"Unable to connect to server"};
			}
		}
		this.respond(inResponse);
	}
});<commit_msg>Improved error handling of non-plain-text<commit_after>enyo.kind({
	name: "enyo.XmlpRequest",
	kind: enyo.JsonpRequest,
	published: {
		detailedErrors:true 	//if true, on a query failure, it will re-query the xml url/params
					//to get the plain-text content and return that in the format
					// of {"error":"unescaped-plain-text-content"}
	},
	callbackName: "callback",
	go: function(inParams) {
		var parts = this.url.split("?");
		var uri = parts.shift() || "";
		var args = parts.join("?").split("&");
		args.push(enyo.Ajax.objectToQuery(inParams || {}));
		this.xmlUrl = [uri, args.join("&")].join("?");
		this.url = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20xml%20where%20url%3D'"
				+ encodeURIComponent(this.xmlUrl) + "'&format=json";
		inParams = {};
		this.inherited(arguments);
	},
	respond: function(inValue) {
		if(inValue && inValue.query) {
			inValue = inValue.query.results || undefined;
			if(!inValue && this.detailedErrors) {
				var fallbackUrl = "http://query.yahooapis.com/v1/public/yql?q=use%20%22http"
						+ "%3A%2F%2Fwww.datatables.org%2Fdata%2Fhtmlstring.xml%22%20"
						+ "as%20htmlstring%3B%20select%20*%20from%20htmlstring%20where"
						+ "%20url%3D'"+ encodeURIComponent(this.xmlUrl) + "'&format=json";
				this.fallbackTextRequest = new enyo.JsonpRequest({url:fallbackUrl,
						callbackName:"callback"});
				this.fallbackTextRequest.response(this, "fallbackTextCallback");
				this.fallbackTextRequest.go();
				return;
			}
		}
		this.inherited(arguments);
	},
	fallbackTextCallback: function(inSender, inResponse) {
		if(inResponse && inResponse.query) {
			inResponse = inResponse.query.results || undefined;
			if(inResponse && inResponse.result) {
				inResponse = inResponse.result;
				//plain text is within auto-generated html, so extract it
				var prefix = "\n  \n  \n  \n    \n    \n    <meta content=\"HTML Tidy for Java (vers. 26 Sep 2004), see www.w3.org\" name=\"generator\"/>\n    \n    \n    <title/>\n    \n  \n  \n  \n  \n  \n    \n    \n
"
				var suffix = "
\n    \n  \n  \n  \n\n";
				inResponse = inResponse.replace(prefix, "");
				inResponse = inResponse.replace(prefix, "");
				var iOpen = inResponse.indexOf("<");
				var iClose = inResponse.lastIndexOf(">");
				//if still contains html, we know it wasn't plain text to begin with
				if(iOpen>-1 && iClose>-1 && iClose>iOpen) {
					inResponse = {"error":inResponse};

				} else {
					inResponse = {"error":"Query is not in XML format"};
				}
			} else if(inResponse && !inResponse.result) {
				inResponse = {"error":"Unknown XML query failure"};
			} else if(!inResponse) {
				inResponse = {"error":"Unable to connect to server"};
			}
		}
		this.respond(inResponse);
	}
});<|endoftext|>


<commit_before>"use strict";
(function() {
    window.actions = [];
    var storageAvailable = false;
    if(window.localStorage) {
        storageAvailable = true;
    }
	document.querySelector(".record").addEventListener("click", function(e) {
        e.stopPropagation();
        Record();
	}, false);
	document.querySelector(".run").addEventListener("click", function(e) {
        e.stopPropagation();
        if(Record.stopRecord) Record.stopRecord();
		Run();
	}, false);

    function Record() {
        window.actions = [];
        var action, isRecording = false;

        var uniqueQuerySelector = function(el) {

            if (!el || !el.tagName) {
                throw new TypeError('Element expected');
            }

            function selectors(el) {
                var parts = [];
                var label = null;
                var title = null;
                var alt   = null;
                var name  = null;
                var value = null;

                do {
                    // IDs are unique enough
                    if (el.id) {
                        label = '#' + el.id;
                    } else {
                        // Otherwise, use tag name
                        label     = el.tagName.toLowerCase();
                        var className = el.getAttribute('class');

                        // Tag names could use classes for specificity
                        if (className && className.length) {
                            label += '.' + className.split(' ').join('.');
                        }
                    }

                    // Titles & Alt attributes are very useful for specificity and tracking
                    if (title = el.getAttribute('title')) {
                        label += '[title="' + title + '"]';
                    } else if (alt = el.getAttribute('alt')) {
                        label += '[alt="' + alt + '"]';
                    } else if (name = el.getAttribute('name')) {
                        label += '[name="' + name + '"]';
                    }

                    if (value = el.getAttribute('value')) {
                        label += '[value="' + value + '"]';
                    }

                    parts.unshift(label);
                } while (!el.id && (el = el.parentNode) && el.tagName);

                // Some selectors should have matched at least
                if (!parts.length) {
                    throw new Error('Failed to identify CSS selector');
                }

                return parts;
            }

            var selector  = selectors(el).join(' > ');
            var matches   = document.querySelectorAll(selector);

            // If selector is not unique enough (wow!), then
            // force the `nth-child` pseido selector
            if (matches.length > 1) {
                for (var i = 0; i < matches.length; i++) {
                    if (el === matches[i]) {
                        // Recalculate index based on position of el amongst siblings
                        i = [].indexOf.call(el.parentNode.children, el);

                        selector += ':nth-child(' + (i + 1) + ')';
                        break;
                    }
                }
            }

            return selector;
        };

        function clickEvent(e) {
            var selector = uniqueQuerySelector(e.target);
            Record.record(selector, e);
        }

        Record.record = function(selector, event) {
            action = {};
            var eventHash = {
                altKey: event.altKey,
                shiftKey: event.shiftKey,
                ctrlKey: event.ctrlKey,
                clientX: event.clientX,
                clientY: event.clientY,
                keyCode: event.keyCode,
                offsetX: event.offsetX,
                offsetY: event.offsetY,
                pageX: event.pageX,
                pageY: event.pageY,
                screenX: event.screenX,
                screenY: event.screenY,
                type: event.type,
                which: event.which,
                elementY: (event.pageY - event.target.offsetTop),
                elementX: (event.pageX - event.target.offsetLeft)
            };
            action[selector] = eventHash;
            actions.push(action);
        };

        Record.startRecord = function() {
            isRecording = true;
            document.querySelector("body").classList.add("recordMode");
            document.addEventListener("click", clickEvent);
        };

        Record.stopRecord = function() {
            isRecording = false;
            document.querySelector("body").classList.remove("recordMode");
            document.removeEventListener("click", clickEvent);
        };

        if(isRecording !== true) {
            Record.startRecord();
        }
    }

    function Run() {
        var runActions = JSON.parse(JSON.stringify(actions));

        if(!actions.length) {
            //TODO: Noty show no actions to run
            return;
        }

        var runManager = {

            counter: 0,

            countExceeded: false,

            highlighter: "ripple01_record-run_10effect",

            highlighterNode: "",

            init: function() {
                document.querySelector("body").classList.add("runMode");
                this.insertHighlighter();
                this.nextRun();
            },

            insertHighlighter: function() {
                var highlighter = document.getElementById(this.highlighter);
                if(!highlighter) {
                    var rule = "", style = document.createElement("style");
                    style.appendChild(document.createTextNode(""));
                    document.head.appendChild(style);
                    //din't work for some reason so commented
                    rule += "@keyframes ripple-record-run { 100%{ opacity:0; transform: scale(2.5); } }\n";
                    rule += ".ripple-record-run-animate-effect { animation: ripple-record-run 0.65s linear; -webkit-animation: ripple-record-run 0.65s linear; } \n";
                    rule += "@-webkit-keyframes ripple-record-run { 100%{opacity:0;-webkit-transform: scale(2.5);}} ";
                    style.type = 'text/css';

                    if (style.styleSheet) {
                        style.styleSheet.cssText = rule;
                    } else {
                        style.appendChild(document.createTextNode(rule));
                    }

                    var highlighter = document.createElement("div");
                    highlighter.id = this.highlighter;
                    highlighter.style.display = "none";
                    highlighter.style.position = "fixed";
                    highlighter.style.background =  "rgba(255,255,255,.075)";
                    highlighter.style.borderRadius = "50%";
                    highlighter.style.transform = "scale(0)";
                    highlighter.style['-webkit-transform'] = "scale(0)";
                    document.querySelector("body").appendChild(highlighter);
                }
                this.highlighterNode =  highlighter;
            },

            nextRun: function() {
                var item = this.queue.getItem(), selector = Object.keys(item)[0];
                this.run(selector, item[selector]);
            },

            success: function() {
                this.counter = 0;
                this.queue.pop();
                if(this.queue.isEmpty() === false) {
                    this.nextRun();
                } else {
                    this.showSuccessNotification();
                }
            },

            //TODO: convert it to generic noty
            showSuccessNotification: function() {
                var notiBox = document.getElementById("record-run-noty-box");
                if(!notiBox) {
                    var box = document.createElement("div");
                    box.id = "record-run-noty-box";
                    box.style.display = "none";
                    box.style.position = "fixed";
                    box.style.width = "200px";
                    box.style.background = "rgb(14, 186, 94)";
                    box.style.top = "20px";
                    box.style.right = "20px";
                    box.style.border = "1px solid rgb(19, 169, 53)";
                    box.style.height = "50px";
                    box.style.padding = "12px";
                    box.style.textAlign = "center";
                    box.style.color = "rgb(251, 254, 251)";
                    box.innerHTML = "Ran Successfully!";
                    document.body.appendChild(box);
                    box.style.display = "block";
                    box.style.cursor = "pointer";
                    box.addEventListener("click", function(e) {
                        box.style.display = "none";
                    });
                    notiBox = box;
                } else {
                    notiBox.style.display = "block";
                }
                setTimeout(function() {
                    notiBox.style.display = "none";
                }, 5000);
            },

            count: function() {
                this.counter++;
                if(this.counter == 5) {
                    this.countExceeded  = true;
                }
            },

            run: function(selector, event) {
                var _this = this;
                var selector = document.querySelector(selector);
                if(selector != null) {
                    selector.scrollIntoView();
                    var mEvent = document.createEvent("MouseEvent");
                    mEvent.initMouseEvent(event.type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
                    selector.dispatchEvent(mEvent);
                    this.highlightPoint(selector, event);
                    setTimeout(function() {
                        _this.success();
                    }, 1000);

                } else {
                    if(this.countExceeded === false) {
                        setTimeout(function() {
                            _this.run(selector, event);
                        }, 1000);
                        this.count();
                    } else {
                        //TODO: change to noty
                        alert("Unable to find element.");
                    }
                }
            },

            highlightPoint: function(node, event) {
                var _this = this
                if(this.highlighterNode) {
                    this.highlighterNode.style.height = this.highlighterNode.style.width = "100px";
                    this.highlighterNode.style.top = -(window.pageYOffset - node.offsetTop) + event.elementY - 50 + 'px';
                    this.highlighterNode.style.left = -(window.pageXOffset - node.offsetLeft) + event.elementX - 50 + 'px';
                    this.highlighterNode.style.display = "block";
                    this.highlighterNode.classList.add("ripple-record-run-animate-effect");
                    setTimeout(function() {
                        _this.highlighterNode.style.display = "none";
                        _this.highlighterNode.classList.remove("ripple-record-run-animate-effect");
                    }, 500);
                }
            },

            queue: {

                queue: runActions,

                pop: function() {
                    this.queue.splice(0, 1);
                },

                getItem: function() {
                    return this.queue[0] || null;
                },

                isEmpty: function() {
                    return this.queue.length ? false : true;
                }
            }
        };

        runManager.init();
    }

})();
<commit_msg>notification done<commit_after>"use strict";
(function() {
    window.actions = [];
    var storageAvailable = false;
    if(window.localStorage) {
        storageAvailable = true;
    }
	document.querySelector(".record").addEventListener("click", function(e) {
        e.stopPropagation();
        Record();
	}, false);
	document.querySelector(".run").addEventListener("click", function(e) {
        e.stopPropagation();
        if(Record.stopRecord) Record.stopRecord();
		Run();
	}, false);

    function Record() {
        window.actions = [];
        var action, isRecording = false;

        var uniqueQuerySelector = function(el) {

            if (!el || !el.tagName) {
                throw new TypeError('Element expected');
            }

            function selectors(el) {
                var parts = [];
                var label = null;
                var title = null;
                var alt   = null;
                var name  = null;
                var value = null;

                do {
                    // IDs are unique enough
                    if (el.id) {
                        label = '#' + el.id;
                    } else {
                        // Otherwise, use tag name
                        label     = el.tagName.toLowerCase();
                        var className = el.getAttribute('class');

                        // Tag names could use classes for specificity
                        if (className && className.length) {
                            label += '.' + className.split(' ').join('.');
                        }
                    }

                    // Titles & Alt attributes are very useful for specificity and tracking
                    if (title = el.getAttribute('title')) {
                        label += '[title="' + title + '"]';
                    } else if (alt = el.getAttribute('alt')) {
                        label += '[alt="' + alt + '"]';
                    } else if (name = el.getAttribute('name')) {
                        label += '[name="' + name + '"]';
                    }

                    if (value = el.getAttribute('value')) {
                        label += '[value="' + value + '"]';
                    }

                    parts.unshift(label);
                } while (!el.id && (el = el.parentNode) && el.tagName);

                // Some selectors should have matched at least
                if (!parts.length) {
                    throw new Error('Failed to identify CSS selector');
                }

                return parts;
            }

            var selector  = selectors(el).join(' > ');
            var matches   = document.querySelectorAll(selector);

            // If selector is not unique enough (wow!), then
            // force the `nth-child` pseido selector
            if (matches.length > 1) {
                for (var i = 0; i < matches.length; i++) {
                    if (el === matches[i]) {
                        // Recalculate index based on position of el amongst siblings
                        i = [].indexOf.call(el.parentNode.children, el);

                        selector += ':nth-child(' + (i + 1) + ')';
                        break;
                    }
                }
            }

            return selector;
        };

        function clickEvent(e) {
            var selector = uniqueQuerySelector(e.target);
            Record.record(selector, e);
        }

        Record.record = function(selector, event) {
            action = {};
            var eventHash = {
                altKey: event.altKey,
                shiftKey: event.shiftKey,
                ctrlKey: event.ctrlKey,
                clientX: event.clientX,
                clientY: event.clientY,
                keyCode: event.keyCode,
                offsetX: event.offsetX,
                offsetY: event.offsetY,
                pageX: event.pageX,
                pageY: event.pageY,
                screenX: event.screenX,
                screenY: event.screenY,
                type: event.type,
                which: event.which,
                elementY: (event.pageY - event.target.offsetTop),
                elementX: (event.pageX - event.target.offsetLeft)
            };
            action[selector] = eventHash;
            actions.push(action);
        };

        Record.startRecord = function() {
            isRecording = true;
            document.querySelector("body").classList.add("recordMode");
            document.addEventListener("click", clickEvent);
        };

        Record.stopRecord = function() {
            isRecording = false;
            document.querySelector("body").classList.remove("recordMode");
            document.removeEventListener("click", clickEvent);
        };

        if(isRecording !== true) {
            Record.startRecord();
        }
    }

    function Run() {
        var runActions = JSON.parse(JSON.stringify(actions));

        if(!actions.length) {
            //TODO: Noty show no actions to run
            return;
        }

        var runManager = {

            counter: 0,

            countExceeded: false,

            highlighter: "ripple01_record-run_10effect",

            highlighterNode: "",

            init: function() {
                document.querySelector("body").classList.add("runMode");
                this.insertHighlighter();
                this.nextRun();
            },

            insertHighlighter: function() {
                var highlighter = document.getElementById(this.highlighter);
                if(!highlighter) {
                    var rule = "", style = document.createElement("style");
                    style.appendChild(document.createTextNode(""));
                    document.head.appendChild(style);
                    //din't work for some reason so commented
                    rule += "@keyframes ripple-record-run { 100%{ opacity:0; transform: scale(2.5); } }\n";
                    rule += ".ripple-record-run-animate-effect { animation: ripple-record-run 0.65s linear; -webkit-animation: ripple-record-run 0.65s linear; } \n";
                    rule += "@-webkit-keyframes ripple-record-run { 100%{opacity:0;-webkit-transform: scale(2.5);}} ";
                    style.type = 'text/css';

                    if (style.styleSheet) {
                        style.styleSheet.cssText = rule;
                    } else {
                        style.appendChild(document.createTextNode(rule));
                    }

                    var highlighter = document.createElement("div");
                    highlighter.id = this.highlighter;
                    highlighter.style.display = "none";
                    highlighter.style.position = "fixed";
                    highlighter.style.background =  "rgba(255,255,255,.075)";
                    highlighter.style.borderRadius = "50%";
                    highlighter.style.transform = "scale(0)";
                    highlighter.style['-webkit-transform'] = "scale(0)";
                    document.querySelector("body").appendChild(highlighter);
                }
                this.highlighterNode =  highlighter;
            },

            nextRun: function() {
                var item = this.queue.getItem(), selector = Object.keys(item)[0];
                this.run(selector, item[selector]);
            },

            success: function() {
                this.counter = 0;
                this.queue.pop();
                if(this.queue.isEmpty() === false) {
                    this.nextRun();
                } else {
                    this.showSuccessNotification();
                }
            },

            //TODO: convert it to generic noty
            showSuccessNotification: function() {
                var notiBox = document.getElementById("record-run-noty-box");
                if(!notiBox) {
                    var boxOption = {
                        tagName: "div",
                        id: "record-run-noty-box",
                        html: "Ran Successfully!",
                        style: {
                            display: "none",
                            position: "fixed",
                            width: "200px",
                            background: "rgb(14, 186, 94)",
                            top: "20px",
                            right: "20px",
                            border: "1px solid rgb(19, 169, 53)",
                            height: "50px",
                            padding: "12px",
                            textAlign: "center",
                            color: "rgb(251, 254, 251)"
                        }
                    };
                    var box = this.createElement(boxOption);
                    document.body.appendChild(box);
                    box.style.display = "block";
                    box.style.cursor = "pointer";
                    box.addEventListener("click", function(e) {
                        box.style.display = "none";
                    });
                    notiBox = box;
                } else {
                    notiBox.style.display = "block";
                }
                setTimeout(function() {
                    notiBox.style.display = "none";
                }, 5000);
            },

            count: function() {
                this.counter++;
                if(this.counter == 5) {
                    this.countExceeded  = true;
                }
            },

            run: function(selector, event) {
                var _this = this;
                var selector = document.querySelector(selector);
                if(selector != null) {
                    selector.scrollIntoView();
                    var mEvent = document.createEvent("MouseEvent");
                    mEvent.initMouseEvent(event.type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
                    selector.dispatchEvent(mEvent);
                    this.highlightPoint(selector, event);
                    setTimeout(function() {
                        _this.success();
                    }, 1000);

                } else {
                    if(this.countExceeded === false) {
                        setTimeout(function() {
                            _this.run(selector, event);
                        }, 1000);
                        this.count();
                    } else {
                        //TODO: change to noty
                        alert("Unable to find element.");
                    }
                }
            },

            highlightPoint: function(node, event) {
                var _this = this
                if(this.highlighterNode) {
                    this.highlighterNode.style.height = this.highlighterNode.style.width = "100px";
                    this.highlighterNode.style.top = -(window.pageYOffset - node.offsetTop) + event.elementY - 50 + 'px';
                    this.highlighterNode.style.left = -(window.pageXOffset - node.offsetLeft) + event.elementX - 50 + 'px';
                    this.highlighterNode.style.display = "block";
                    this.highlighterNode.classList.add("ripple-record-run-animate-effect");
                    setTimeout(function() {
                        _this.highlighterNode.style.display = "none";
                        _this.highlighterNode.classList.remove("ripple-record-run-animate-effect");
                    }, 500);
                }
            },

            queue: {

                queue: runActions,

                pop: function() {
                    this.queue.splice(0, 1);
                },

                getItem: function() {
                    return this.queue[0] || null;
                },

                isEmpty: function() {
                    return this.queue.length ? false : true;
                }
            },

            createElement: function(options) {
                var tagName = options.tagName || "div",
                    id = options.id || "",
                    styles = options.style || {},
                    html = options.html || "";
                var box = document.createElement(tagName);
                box.id = id;
                box.innerHTML = html;
                for(var style in styles) {
                    box.style[style] = styles[style];
                }
                return box;
            }
        };

        runManager.init();
    }

})();
<|endoftext|>