<commit_before>// @require Kwf.Ext4.Controller.Grid
Ext4.define('Kwf.Ext4.Controller.Grid.EditWindow', {
    mixins: {
        observable: 'Ext.util.Observable'
    },
    focusOnEditSelector: 'field',
    autoSync: true,

    form: null,
    editWindow: null,
    bindable: null,
    gridController: null,

    _addToStoreOnSave: false,
    constructor: function(config) {
        this.mixins.observable.constructor.call(this, config);
        this.init();
    },

    doSave: function()
    {
        if (!this.bindable.isValid()) {
            Ext4.Msg.alert(trlKwf('Save'),
                trlKwf("Can't save, please fill all red underlined fields correctly."));
            return false;
        }

        var row = this.bindable.getLoadedRecord();
        if (row.phantom && this._addToStoreOnSave) {
            this.gridController.grid.getStore().add(row);
        }
        if (this.autoSync) {
            var syncQueue = new Kwf.Ext4.Data.StoreSyncQueue();
            syncQueue.add(this.gridController.grid.getStore()); //sync this.gridController.grid store first
            this.bindable.save(syncQueue);         //then bindables (so bindable grid is synced second)
                                                    //bindable forms can still update the row as the sync is not yet started
            syncQueue.start({
                success: function() {
                    this.fireEvent('savesuccess');
                },
                scope: this
            });
        } else {
            this.bindable.save();
        }
        this.fireEvent('save');
    },

    init: function()
    {
        if (!this.gridController) Ext4.Error.raise('gridController config is required');
        if (!this.gridController instanceof Ext4.window.Window) Ext4.Error.raise('gridController config needs to be a Kwf.Ext4.Controller.Grid');
        if (!this.editWindow) Ext4.Error.raise('editWindow config is required');
        if (!this.editWindow instanceof Ext4.window.Window) Ext4.Error.raise('editWindow config needs to be a Ext.window.Window');
        if (!this.bindable) Ext4.Error.raise('bindable config is required');
        if (!this.bindable instanceof Ext4.window.Window) Ext4.Error.raise('bindable config needs to be a Kwf.Ext4.Controller.Bindable.Abstract');

        if (!this.form) {
            this.form = this.editWindow.down('form');
        }
        if (!this.form) Ext4.Error.raise('form config is required');
        if (!this.form instanceof Ext4.form.Panel) Ext4.Error.raise('form config needs to be a Ext.form.Panel');

        if (!this.windowSaveButton) this.windowSaveButton = this.editWindow.down('> toolbar > button#save');
        if (this.windowSaveButton && !this.windowSaveButton instanceof Ext4.button.Button) Ext4.Error.raise('windowSaveButton config needs to be a Ext.button.Button');

        if (!this.windowDeleteButton) this.windowDeleteButton = this.editWindow.down('> toolbar > button#delete');
        if (this.windowDeleteButton && !this.windowDeleteButton instanceof Ext4.button.Button) Ext4.Error.raise('windowDeleteButton config needs to be a Ext.button.Button');

        if (!this.windowCancelButton) this.windowCancelButton = this.editWindow.down('> toolbar > button#cancel');
        if (this.windowCancelButton && !this.windowCancelButton instanceof Ext4.button.Button) Ext4.Error.raise('windowCancelButton config needs to be a Ext.button.Button');

        if (!this.addButton) this.addButton = this.gridController.grid.down('button#add');
        if (this.addButton && !this.addButton instanceof Ext4.button.Button) Ext4.Error.raise('addButton config needs to be a Ext.button.Button');

        if (!this.editActionColumn) this.editActionColumn = this.gridController.grid.down('actioncolumn#edit')
        if (this.editActionColumn && !this.editActionColumn instanceof Ext4.button.Button) Ext4.Error.raise('editActionColumn config needs to be a Ext.grid.Column');

        if (this.windowSaveButton) {
            this.windowSaveButton.on('click', function() {
                if (this.doSave() !== false) {
                    this.closeWindow();
                }
            }, this);
        }
        if (this.windowDeleteButton) {
            this.windowDeleteButton.on('click', function() {
                this.gridController.onDeleteClick({
                    callback: function() {
                        this.closeWindow();
                    },
                    scope: this
                });
            }, this);
        }
        if (this.windowCancelButton) {
            this.windowCancelButton.on('click', this.onCancel, this);
        }
        this.editWindow.on('beforeclose', function() {
            this.onCancel();
            return false;
        }, this);

        this.gridController.grid.on('celldblclick', function(grid, td, cellIndex, row, tr, rowIndex, e) {
            this.openEditWindow(row);
        }, this);

        if (this.editActionColumn) {
            this.editActionColumn.on('click', function(view, cell, rowIndex, colIndex, e) {
                this.openEditWindow(this.gridController.grid.store.getAt(rowIndex));
            }, this);
        }
        if (this.addButton) {
            this.addButton.on('click', function() {
                var row = this.gridController.grid.getStore().model.create();
                this.fireEvent('add', row);
                this.openEditWindow(row);
                this._addToStoreOnSave = true;
            }, this);
        }
    },

    openEditWindow: function(row)
    {
        this._addToStoreOnSave = false;
        this.bindable.load(row);
        if (row.phantom) {
            this.editWindow.setTitle(trlKwf('Add'));
        } else {
            this.editWindow.setTitle(trlKwf('Edit'));
        }
        this.editWindow.show();
        if (this.focusOnEditSelector) {
            this.editWindow.down(this.focusOnEditSelector).focus();
        }
    },

    onCancel: function()
    {
        if (this.bindable.isDirty()) {
            Ext4.Msg.show({
                title: trlKwf('Save'),
                msg: trlKwf('Save changes?'),
                icon: Ext4.MessageBox.QUESTION,
                buttons: Ext4.Msg.YESNOCANCEL,
                fn: function(btn) {
                    if (btn == 'no') {
                        this.closeWindow();
                    } else if (btn == 'yes') {
                        if (this.doSave() !== false) {
                            this.closeWindow();
                        }
                    }
                },
                scope: this
            });
        } else {
            this.closeWindow();
        }
    },

    closeWindow: function()
    {
        this.bindable.reset();
        this.editWindow.hide();
    }
});
<commit_msg>Use better text for message-box<commit_after>// @require Kwf.Ext4.Controller.Grid
Ext4.define('Kwf.Ext4.Controller.Grid.EditWindow', {
    mixins: {
        observable: 'Ext.util.Observable'
    },
    focusOnEditSelector: 'field',
    autoSync: true,

    form: null,
    editWindow: null,
    bindable: null,
    gridController: null,

    _addToStoreOnSave: false,
    constructor: function(config) {
        this.mixins.observable.constructor.call(this, config);
        this.init();
    },

    doSave: function()
    {
        if (!this.bindable.isValid()) {
            Ext4.Msg.alert(trlKwf('Save'),
                trlKwf("Can't save, please fill all red underlined fields correctly."));
            return false;
        }

        var row = this.bindable.getLoadedRecord();
        if (row.phantom && this._addToStoreOnSave) {
            this.gridController.grid.getStore().add(row);
        }
        if (this.autoSync) {
            var syncQueue = new Kwf.Ext4.Data.StoreSyncQueue();
            syncQueue.add(this.gridController.grid.getStore()); //sync this.gridController.grid store first
            this.bindable.save(syncQueue);         //then bindables (so bindable grid is synced second)
                                                    //bindable forms can still update the row as the sync is not yet started
            syncQueue.start({
                success: function() {
                    this.fireEvent('savesuccess');
                },
                scope: this
            });
        } else {
            this.bindable.save();
        }
        this.fireEvent('save');
    },

    init: function()
    {
        if (!this.gridController) Ext4.Error.raise('gridController config is required');
        if (!this.gridController instanceof Ext4.window.Window) Ext4.Error.raise('gridController config needs to be a Kwf.Ext4.Controller.Grid');
        if (!this.editWindow) Ext4.Error.raise('editWindow config is required');
        if (!this.editWindow instanceof Ext4.window.Window) Ext4.Error.raise('editWindow config needs to be a Ext.window.Window');
        if (!this.bindable) Ext4.Error.raise('bindable config is required');
        if (!this.bindable instanceof Ext4.window.Window) Ext4.Error.raise('bindable config needs to be a Kwf.Ext4.Controller.Bindable.Abstract');

        if (!this.form) {
            this.form = this.editWindow.down('form');
        }
        if (!this.form) Ext4.Error.raise('form config is required');
        if (!this.form instanceof Ext4.form.Panel) Ext4.Error.raise('form config needs to be a Ext.form.Panel');

        if (!this.windowSaveButton) this.windowSaveButton = this.editWindow.down('> toolbar > button#save');
        if (this.windowSaveButton && !this.windowSaveButton instanceof Ext4.button.Button) Ext4.Error.raise('windowSaveButton config needs to be a Ext.button.Button');

        if (!this.windowDeleteButton) this.windowDeleteButton = this.editWindow.down('> toolbar > button#delete');
        if (this.windowDeleteButton && !this.windowDeleteButton instanceof Ext4.button.Button) Ext4.Error.raise('windowDeleteButton config needs to be a Ext.button.Button');

        if (!this.windowCancelButton) this.windowCancelButton = this.editWindow.down('> toolbar > button#cancel');
        if (this.windowCancelButton && !this.windowCancelButton instanceof Ext4.button.Button) Ext4.Error.raise('windowCancelButton config needs to be a Ext.button.Button');

        if (!this.addButton) this.addButton = this.gridController.grid.down('button#add');
        if (this.addButton && !this.addButton instanceof Ext4.button.Button) Ext4.Error.raise('addButton config needs to be a Ext.button.Button');

        if (!this.editActionColumn) this.editActionColumn = this.gridController.grid.down('actioncolumn#edit')
        if (this.editActionColumn && !this.editActionColumn instanceof Ext4.button.Button) Ext4.Error.raise('editActionColumn config needs to be a Ext.grid.Column');

        if (this.windowSaveButton) {
            this.windowSaveButton.on('click', function() {
                if (this.doSave() !== false) {
                    this.closeWindow();
                }
            }, this);
        }
        if (this.windowDeleteButton) {
            this.windowDeleteButton.on('click', function() {
                this.gridController.onDeleteClick({
                    callback: function() {
                        this.closeWindow();
                    },
                    scope: this
                });
            }, this);
        }
        if (this.windowCancelButton) {
            this.windowCancelButton.on('click', this.onCancel, this);
        }
        this.editWindow.on('beforeclose', function() {
            this.onCancel();
            return false;
        }, this);

        this.gridController.grid.on('celldblclick', function(grid, td, cellIndex, row, tr, rowIndex, e) {
            this.openEditWindow(row);
        }, this);

        if (this.editActionColumn) {
            this.editActionColumn.on('click', function(view, cell, rowIndex, colIndex, e) {
                this.openEditWindow(this.gridController.grid.store.getAt(rowIndex));
            }, this);
        }
        if (this.addButton) {
            this.addButton.on('click', function() {
                var row = this.gridController.grid.getStore().model.create();
                this.fireEvent('add', row);
                this.openEditWindow(row);
                this._addToStoreOnSave = true;
            }, this);
        }
    },

    openEditWindow: function(row)
    {
        this._addToStoreOnSave = false;
        this.bindable.load(row);
        if (row.phantom) {
            this.editWindow.setTitle(trlKwf('Add'));
        } else {
            this.editWindow.setTitle(trlKwf('Edit'));
        }
        this.editWindow.show();
        if (this.focusOnEditSelector) {
            this.editWindow.down(this.focusOnEditSelector).focus();
        }
    },

    onCancel: function()
    {
        if (this.bindable.isDirty()) {
            Ext4.Msg.show({
                title: trlKwf('Save'),
                msg: trlKwf('Do you want to save the changes?'),
                icon: Ext4.MessageBox.QUESTION,
                buttons: Ext4.Msg.YESNOCANCEL,
                fn: function(btn) {
                    if (btn == 'no') {
                        this.closeWindow();
                    } else if (btn == 'yes') {
                        if (this.doSave() !== false) {
                            this.closeWindow();
                        }
                    }
                },
                scope: this
            });
        } else {
            this.closeWindow();
        }
    },

    closeWindow: function()
    {
        this.bindable.reset();
        this.editWindow.hide();
    }
});
<|endoftext|>


<commit_before>// TO DO: clean up markup and semantic groupings of the various options...

function photo_geo_edit_meta(photo_id){

	var old_ctx = $("#edit_geo").attr("geo:context");

	// TO DO: pass down / read from PHP geo context map

	var ctx_map = {
		0: 'in a place that doesn\'t matter (not defined)',
		1: 'indoors',
		2: 'outdoors'
	};

	var html = '
';
	html += '

';
	html += '<form id="photo_geo_update_context">';

	html += '
Edit the geo context for this photo
';
	html += 'This photo was taken ';

	html += '<select id="new_geocontext">';

	for (i in ctx_map){
		html += '<option value="' + i + '"';

		if (i == old_ctx){
			html += ' selected="true" disabled="true"';
		}

		html += '>' + ctx_map[i] + '</option>';
	}

	html += '</select>';
	html += ' ';
	html += '<input type="submit" value="UPDATE" />';
	html += '</form>';

	// corrections

	// FIX ME: make me less brittle...
	var old_placename = $("#geo_placename a").html();

	html += '
';
	html += '
Edit the place name for this photo
';
	html += '
Flickr thinks this photo was taken in <q>';
	html += old_placename;
	html += '</q>. Fetch the list of alternate place names.';
	html += '
';

	html += '
';

	html += '
';
	html += '
close
';

	// http://www.ericmmartin.com/projects/simplemodal/

	$.modal(html);

	$("#photo_geo_corrections_fetch").click(function(){

		var _onsuccess = function(rsp){

			$("#photo_geo_status").html("");

			if (rsp['stat'] != 'ok'){
				$("#photo_geo_status").html("Ack!");
				return;
			}

			var current_woeid = $("#edit_geo").attr("geo:woeid");

			var html = '<form id="photo_geo_corrections_update">';

			html += 'It was really taken in ';
			html += '<select id="new_woeid">';
			html += '<option />';

			var count = rsp['places'].length;

			for (var i=0; i < count; i++){
				var pl = rsp['places'][i];

				html += '<option value="' + pl['woeid'] + '"';

				if (pl['woeid'] == current_woeid){
					html += ' disabled="true"';
				}

				html += '>' + pl['name'] + '</option>';
			}

			html += '</select>';
			html += '  ';
			html += '<input type="submit" value="UPDATE" />';
			html += '</form>';

			$("#photo_geo_corrections").append(html);

			$("#photo_geo_corrections_update").submit(function(){

				var new_woeid = $("#new_woeid");
				new_woeid = new_woeid.val();

				var _onsuccess = function(rsp){
					$("#photo_geo_status").html(rsp['stat']);
					console.log(rsp);
					// update place name in display
					// auto close window ?
				};

				var args = {
					'method': 'flickr.photos.geo.correctLocation',
					'photo_id': photo_id,
					'woeid': new_woeid
				};

				$.ajax({
					'url' : '/api/',
					'type': 'POST',
					'data': args,
					'success': _onsuccess
				});

				$("#photo_geo_corrections_update").hide();
				$("#photo_geo_status").html("Poking the Flickr API...");

				return false;
			});

		};

		var args = {
			'method': 'flickr.photos.geo.possibleCorrections',
			'photo_id': photo_id,
			'place_type': 'neighbourhood'
		};

		$.ajax({
			'url': '/api/',
			'type': 'GET',
			'data': args,
			'success': _onsuccess
		});

		$("#photo_geo_corrections_fetch").hide();
		$("#photo_geo_status").html("Fetching alternate place names...");
	});

	$("#photo_geo_update_context").submit(function(){

		var new_ctx = $("#new_geocontext");
		new_ctx = new_ctx.val();

		if (new_ctx == old_ctx){
			alert("Hey! There's nothing to update!");
			return;
		}

		var args = {
			'method': 'flickr.photos.geo.setContext',
			'photo_id': photo_id,
			'context': new_ctx
		};

		var _onsuccess = function(rsp){

			if (rsp['stat'] != 'ok'){
				$("#photo_geo_status").html("Ack! There was a problem calling the Flickr API.");
				return;
			}

			$("#edit_geo").attr("geo:context", rsp['context']);

			var str_ctx = ctx_map[rsp['context']];

			var taken = '';

			if (rsp['context'] != 0){

				var woeid = $("#edit_geo").attr("geo:woeid");
				var url = places_url + woeid + '/' + str_ctx + '/';

				taken = 'It was taken ' + str_ctx + '';
			}

			$("#geo_context").html(taken);

			var msg = '
Success! The geo context for this photo has been updated and is now ' + str_ctx + '.
';
			$("#photo_geo_status").html(msg);
		};

		$.ajax({
			'url': '/api/',
			'type': 'POST',
			'data': args,
			'success': _onsuccess
		});

		$("#photo_geo_update_context").hide();
		$("#photo_geo_status").html("Poking the Flickr API...");

		return false;
	});

}
<commit_msg>start moving event callbacks in to their own functions<commit_after>// TO DO: clean up markup and semantic groupings of the various options...

function photo_geo_edit_meta(photo_id){

	var old_ctx = $("#edit_geo").attr("geo:context");

	// TO DO: pass down / read from PHP geo context map

	var ctx_map = {
		0: 'in a place that doesn\'t matter (not defined)',
		1: 'indoors',
		2: 'outdoors'
	};

	var html = '
';
	html += '

';
	html += '<form id="photo_geo_update_context">';

	html += '
Edit the geo context for this photo
';
	html += 'This photo was taken ';

	html += '<select id="new_geocontext">';

	for (i in ctx_map){
		html += '<option value="' + i + '"';

		if (i == old_ctx){
			html += ' selected="true" disabled="true"';
		}

		html += '>' + ctx_map[i] + '</option>';
	}

	html += '</select>';
	html += ' ';
	html += '<input type="submit" value="UPDATE" />';
	html += '</form>';

	// corrections

	// FIX ME: make me less brittle...
	var old_placename = $("#geo_placename a").html();

	html += '
';
	html += '
Edit the place name for this photo
';
	html += '
Flickr thinks this photo was taken in <q>';
	html += old_placename;
	html += '</q>. Fetch the list of alternate place names.';
	html += '
';

	html += '
';

	html += '
';
	html += '
close
';

	// http://www.ericmmartin.com/projects/simplemodal/

	$.modal(html);

	$("#photo_geo_corrections_fetch").click(photo_geo_corrections_fetch);

	$("#photo_geo_update_context").submit(function(){

		var new_ctx = $("#new_geocontext");
		new_ctx = new_ctx.val();

		if (new_ctx == old_ctx){
			alert("Hey! There's nothing to update!");
			return;
		}

		var args = {
			'method': 'flickr.photos.geo.setContext',
			'photo_id': photo_id,
			'context': new_ctx
		};

		var _onsuccess = function(rsp){

			if (rsp['stat'] != 'ok'){
				$("#photo_geo_status").html("Ack! There was a problem calling the Flickr API.");
				return;
			}

			$("#edit_geo").attr("geo:context", rsp['context']);

			var str_ctx = ctx_map[rsp['context']];

			var taken = '';

			if (rsp['context'] != 0){

				var woeid = $("#edit_geo").attr("geo:woeid");
				var url = places_url + woeid + '/' + str_ctx + '/';

				taken = 'It was taken ' + str_ctx + '';
			}

			$("#geo_context").html(taken);

			var msg = '
Success! The geo context for this photo has been updated and is now ' + str_ctx + '.
';
			$("#photo_geo_status").html(msg);
		};

		$.ajax({
			'url': '/api/',
			'type': 'POST',
			'data': args,
			'success': _onsuccess
		});

		$("#photo_geo_update_context").hide();
		$("#photo_geo_status").html("Poking the Flickr API...");

		return false;
	});

}

function photo_geo_corrections_fetch(){

	 	var _onsuccess = function(rsp){

			$("#photo_geo_status").html("");

			if (rsp['stat'] != 'ok'){
				$("#photo_geo_status").html("Ack!");
				return;
			}

			var current_woeid = $("#edit_geo").attr("geo:woeid");

			var html = '<form id="photo_geo_corrections_update">';

			html += 'It was really taken in ';
			html += '<select id="new_woeid">';
			html += '<option />';

			var count = rsp['places'].length;

			for (var i=0; i < count; i++){
				var pl = rsp['places'][i];

				html += '<option value="' + pl['woeid'] + '"';

				if (pl['woeid'] == current_woeid){
					html += ' disabled="true"';
				}

				html += '>' + pl['name'] + '</option>';
			}

			html += '</select>';
			html += '  ';
			html += '<input type="submit" value="UPDATE" />';
			html += '</form>';

			$("#photo_geo_corrections").append(html);

			$("#photo_geo_corrections_update").submit(function(){

				var new_woeid = $("#new_woeid");
				new_woeid = new_woeid.val();

				var _onsuccess = function(rsp){
					$("#photo_geo_status").html(rsp['stat']);
					console.log(rsp);
					// update place name in display
					// auto close window ?
				};

				var args = {
					'method': 'flickr.photos.geo.correctLocation',
					'photo_id': photo_id,
					'woeid': new_woeid
				};

				$.ajax({
					'url' : '/api/',
					'type': 'POST',
					'data': args,
					'success': _onsuccess
				});

				$("#photo_geo_corrections_update").hide();
				$("#photo_geo_status").html("Poking the Flickr API...");

				return false;
			});

		};

		var args = {
			'method': 'flickr.photos.geo.possibleCorrections',
			'photo_id': photo_id,
			'place_type': 'neighbourhood'
		};

		$.ajax({
			'url': '/api/',
			'type': 'GET',
			'data': args,
			'success': _onsuccess
		});

		$("#photo_geo_corrections_fetch").hide();
		$("#photo_geo_status").html("Fetching alternate place names...");

}
<|endoftext|>


<commit_before>L.Control.Permalink = L.Class.extend({
	options: {
		position: L.Control.Position.BOTTOM_LEFT,
		useAnchor: true,
		useMarker: true,
		markerOptions: {},
	},

	initialize: function(layers, options) {
		L.Util.setOptions(this, options);
		this._set_urlvars();
		this._centered = false;
		this._layers = layers;
		this._marker = null;
	},

	onAdd: function(map) {
		this._container = L.DomUtil.create('div', 'leaflet-control-attribution permalink');
		L.DomEvent.disableClickPropagation(this._container);
		map.on('moveend', this._update_center, this);
		map.on('layeradd', this._update_layers, this);
		map.on('layerremove', this._update_layers, this);
		this._map = map;
		this._href = L.DomUtil.create('a', null, this._container);
		this._href.innerHTML = "Permalink";
		this._set_center(this._params);
		this._set_marker(this._params, true);
		this._update_layers();
		this._update_center();

		if (this.options.useAnchor && 'onhashchange' in window) {
			var _this = this, fn = window.onhashchange;
			window.onhashchange = function() {
				_this._set_urlvars();
				_this._set_center(_this._params, true);
				_this._set_marker(_this._params, true);
				if (fn) return fn();
			}
		}
	},

	getPosition: function() {
		return this.options.position;
	},

	getContainer: function() {
		return this._container;
	},

	_update_center: function() {
		if (!this._map) return;
		this._update_href();
	},

  _generate_url: function(params) {
    var link = L.Util.getParamString(params);
    var sep = '?';
    if (this.options.useAnchor) sep = '#';
    return this._url_base + sep + link.slice(1);
  },

  _update_href: function() {
    this.get_params();
    this._href.setAttribute('href', this._generate_url(this._params));
  },

	_update_layers: function() {
		if (!this._layers) return;

		var layer = this._layers.currentBaseLayer();
		if (layer) this._params['layer'] = layer.name;
		for(x in osm.map.control_layers._layers) {
		  if (osm.map.control_layers._layers[x].overlay)
		    if (this._map.hasLayer(osm.map.control_layers._layers[x].layer))
		      this._params['layer'] += ',' + osm.map.control_layers._layers[x].name;
		}
		this._update_href();
	},

	get_params: function () {
		var center = this._map.getCenter();
		center = this._round_point(center);
		this._params['zoom'] = this._map.getZoom();
		this._params['lat'] = center.lat;
		this._params['lon'] = center.lng;
	},

	_round_point: function(point) {
		var bounds = this._map.getBounds(), size = this._map.getSize();
		var ne = bounds.getNorthEast(), sw = bounds.getSouthWest();

		var round = function (x, p) {
			if (p == 0) return x;
			shift = 1;
			while (p < 1 && p > -1) {
				x *= 10;
				p *= 10;
				shift *= 10;
			}
			return Math.floor(x)/shift;
		}
		point.lat = round(point.lat, (ne.lat - sw.lat) / size.y);
		point.lng = round(point.lng, (ne.lng - sw.lng) / size.x);
		return point;
	},

	_set_urlvars: function()
	{
		function alter(p) {
			if (!p.mlat || !p.mlon) return p;
			p.lat = p.mlat;
			p.lon = p.mlon;
			p.marker = '1';
			delete p['mlat'];
			delete p['mlon'];
			return p;
		}

		this._url_base = window.location.href.split('#')[0];
		var ph = {};
		if (this.options.useAnchor)
			ph = alter(L.UrlUtil.queryParse(window.location.hash.slice(1)));

		var q = L.UrlUtil.query();
		if (!q) {
			this._params = ph;
			return;
		}

		var pq = alter(L.UrlUtil.queryParse(q));

		if (!this.options.useAnchor) {
			this._url_base = this._url_base.split('?')[0]
			this._params = pq;
			return;
		}

		this._params = ph;
		if (pq.lat && pq.lon && pq.zoom)
			this._params = L.Util.extend({lat: pq.lat, lon: pq.lon, zoom: pq.zoom}, this._params);
		if (pq.layer)
			this._params = L.Util.extend({layer: pq.layer}, this._params);
	},

	_parse_urlvars: function(s) {
		var p = {};
		var params = s.split('&');
		for(var i = 0; i < params.length; i++) {
			var tmp = params[i].split('=');
			if (tmp.length != 2) continue;
			p[tmp[0]] = tmp[1];
		}
		return p;
	},

	_set_center: function(params, force)
	{
		if (!force && this._centered) return;
		if (params.zoom == undefined ||
		    params.lat == undefined ||
		    params.lon == undefined) return;
		this._centered = true;
		this._map.setView(new L.LatLng(params.lat, params.lon), params.zoom);
		if (params.layer && this._layers)
			this._layers.chooseBaseLayer(params.layer);
	},

	_set_marker: function(params, center)
	{
		if (this._marker)
			this._map.removeLayer(this._marker);
		this._marker = null;
		if ((params.marker != '1' || !this._centered || !this.options.useMarker) && center) return;
		this._marker = new L.Marker(new L.LatLng(params.lat, params.lon), this.options.markerOptions);
		this._map.addLayer(this._marker);
    var popup = this._marker.bindPopup('Ссылка на маркер');
    if (!center)
      popup.openPopup();
	},

  _popup_marker: function(latlng) {
    latlng = this._round_point(latlng);
    //copy simple obj
    var params = {};
    for (i in this._params)
      params[i] = this._params[i];

    params.lat = latlng.lat;
    params.lon = latlng.lng;
    params.marker = 1;

    this._set_marker(params, false);
  }
});

L.Control.Layers.include({
	chooseBaseLayer: function(name) {
    var obj;
    var layers = new Array();
    var names = name.split(',');
		for (var n in names) {
		  for (var i in this._layers) {
			  if (!this._layers.hasOwnProperty(i))
				  continue;
			  obj = this._layers[i];
			  if (obj.name == names[n])
				  layers.push(obj.layer);
		  }
		}
		if (!layers || !layers.length)
			return;

		for (var i in this._layers) {
			if (!this._layers.hasOwnProperty(i))
				continue;
			obj = this._layers[i];
			if (this._map.hasLayer(obj.layer))
				this._map.removeLayer(obj.layer)
		}
		for (l in layers) {
		  this._map.addLayer(layers[l])
		}
		this._update();
	},

	currentBaseLayer: function() {
		for (var i in this._layers) {
			if (!this._layers.hasOwnProperty(i))
				continue;
			var obj = this._layers[i];
			if (obj.overlay) continue;
			if (!obj.overlay && this._map.hasLayer(obj.layer))
				return obj;
		}
	},
});

L.UrlUtil = {
	queryParse: function(s) {
		var p = {};
		var params = s.split('&');
		for(var i = 0; i < params.length; i++) {
			var tmp = params[i].split('=');
			if (tmp.length != 2) continue;
			p[tmp[0]] = tmp[1];
		}
		return p;
	},

	query: function() {
		var href = window.location.href.split('#')[0], idx = href.indexOf('?');
		if (idx < 0)
			return '';
		return href.slice(idx+1);
	}
};

<commit_msg>pre-commit<commit_after>L.Control.Permalink = L.Class.extend({
	options: {
		position: L.Control.Position.BOTTOM_LEFT,
		useAnchor: true,
		useMarker: true,
		markerOptions: {},
	},

	initialize: function(layers, options) {
		L.Util.setOptions(this, options);
		this._set_urlvars();
		this._centered = false;
		this._layers = layers;
		this._marker = null;
	},

	onAdd: function(map) {
		this._container = L.DomUtil.create('div', 'leaflet-control-attribution permalink');
		L.DomEvent.disableClickPropagation(this._container);
		map.on('moveend', this._update_center, this);
		map.on('layeradd', this._update_layers, this);
		map.on('layerremove', this._update_layers, this);
		this._map = map;
		this._href = L.DomUtil.create('a', null, this._container);
		this._href.innerHTML = "Permalink";
		this._set_center(this._params);
		this._set_marker(this._params, true);
		this._update_layers();
		this._update_center();

		if (this.options.useAnchor && 'onhashchange' in window) {
			var _this = this, fn = window.onhashchange;
			window.onhashchange = function() {
				_this._set_urlvars();
				_this._set_center(_this._params, true);
				_this._set_marker(_this._params, true);
				if (fn) return fn();
			}
		}
	},

	getPosition: function() {
		return this.options.position;
	},

	getContainer: function() {
		return this._container;
	},

	_update_center: function() {
		if (!this._map) return;
		this._update_href();
	},

  _generate_url: function(params) {
    var link = L.Util.getParamString(params);
    var sep = '?';
    if (this.options.useAnchor) sep = '#';
    return this._url_base + sep + link.slice(1);
  },

  _update_href: function() {
    this.get_params();
    this._href.setAttribute('href', this._generate_url(this._params));
  },

	_update_layers: function() {
		if (!this._layers) return;

		var layer = this._layers.currentBaseLayer();
		if (layer) this._params['layer'] = layer.name;
		for(x in osm.map.control_layers._layers) {
		  if (osm.map.control_layers._layers[x].overlay)
		    if (this._map.hasLayer(osm.map.control_layers._layers[x].layer))
		      this._params['layer'] += ',' + osm.map.control_layers._layers[x].name;
		}
		this._update_href();
	},

	get_params: function () {
		var center = this._map.getCenter();
		center = this._round_point(center);
		this._params['zoom'] = this._map.getZoom();
		this._params['lat'] = center.lat;
		this._params['lon'] = center.lng;
	},

	_round_point: function(point) {
		var bounds = this._map.getBounds(), size = this._map.getSize();
		var ne = bounds.getNorthEast(), sw = bounds.getSouthWest();

		var round = function (x, p) {
			if (p == 0) return x;
			shift = 1;
			while (p < 1 && p > -1) {
				x *= 10;
				p *= 10;
				shift *= 10;
			}
			return Math.floor(x)/shift;
		}
		point.lat = round(point.lat, (ne.lat - sw.lat) / size.y);
		point.lng = round(point.lng, (ne.lng - sw.lng) / size.x);
		return point;
	},

	_set_urlvars: function()
	{
		function alter(p) {
			if (!p.mlat || !p.mlon) return p;
			p.lat = p.mlat;
			p.lon = p.mlon;
			p.marker = '1';
			delete p['mlat'];
			delete p['mlon'];
			return p;
		}

		this._url_base = window.location.href.split('#')[0];
		var ph = {};
		if (this.options.useAnchor)
			ph = alter(L.UrlUtil.queryParse(window.location.hash.slice(1)));

		var q = L.UrlUtil.query();
		if (!q) {
			this._params = ph;
			return;
		}

		var pq = alter(L.UrlUtil.queryParse(q));

		if (!this.options.useAnchor) {
			this._url_base = this._url_base.split('?')[0]
			this._params = pq;
			return;
		}

		this._params = ph;
		if (pq.lat && pq.lon && pq.zoom)
			this._params = L.Util.extend({lat: pq.lat, lon: pq.lon, zoom: pq.zoom}, this._params);
		if (pq.layer)
			this._params = L.Util.extend({layer: pq.layer}, this._params);
	},

	_parse_urlvars: function(s) {
		var p = {};
		var params = s.split('&');
		for(var i = 0; i < params.length; i++) {
			var tmp = params[i].split('=');
			if (tmp.length != 2) continue;
			p[tmp[0]] = tmp[1];
		}
		return p;
	},

	_set_center: function(params, force)
	{
		if (!force && this._centered) return;
		if (params.zoom == undefined ||
		    params.lat == undefined ||
		    params.lon == undefined) return;
		this._centered = true;
		this._map.setView(new L.LatLng(params.lat, params.lon), params.zoom);
		if (params.layer && this._layers)
			this._layers.chooseBaseLayer(params.layer);
	},

	_set_marker: function(params, center)
	{
		if (this._marker)
			this._map.removeLayer(this._marker);
		this._marker = null;
		if ((params.marker != '1' || !this._centered || !this.options.useMarker) && center) return;
		this._marker = new L.Marker(new L.LatLng(params.lat, params.lon), this.options.markerOptions);
		this._map.addLayer(this._marker);
    var popup = this._marker.bindPopup('Ссылка на маркер');
    if (!center)
      popup.openPopup();
	},

  _popup_marker: function(latlng) {
    latlng = this._round_point(latlng);
    //copy simple obj
    this.get_params();
    var params = {};
    for (i in this._params)
      params[i] = this._params[i];

    params.lat = latlng.lat;
    params.lon = latlng.lng;
    params.marker = 1;

    this._set_marker(params, false);
  }
});

L.Control.Layers.include({
	chooseBaseLayer: function(name) {
    var obj;
    var layers = new Array();
    var names = name.split(',');
		for (var n in names) {
		  for (var i in this._layers) {
			  if (!this._layers.hasOwnProperty(i))
				  continue;
			  obj = this._layers[i];
			  if (obj.name == names[n])
				  layers.push(obj.layer);
		  }
		}
		if (!layers || !layers.length)
			return;

		for (var i in this._layers) {
			if (!this._layers.hasOwnProperty(i))
				continue;
			obj = this._layers[i];
			if (this._map.hasLayer(obj.layer) && !($.inArray(obj.layer, layers) === -1) ) //remove only unneeded layers
				this._map.removeLayer(obj.layer)
		}
		for (l in layers) {
      if ($.inArray(layers[l], this._layers) !== -1) // add only non-existing layers
        this._map.addLayer(layers[l])
		}
		this._update();
	},

	currentBaseLayer: function() {
		for (var i in this._layers) {
			if (!this._layers.hasOwnProperty(i))
				continue;
			var obj = this._layers[i];
			if (obj.overlay) continue;
			if (!obj.overlay && this._map.hasLayer(obj.layer))
				return obj;
		}
	},
});

L.UrlUtil = {
	queryParse: function(s) {
		var p = {};
		var params = s.split('&');
		for(var i = 0; i < params.length; i++) {
			var tmp = params[i].split('=');
			if (tmp.length != 2) continue;
			p[tmp[0]] = tmp[1];
		}
		return p;
	},

	query: function() {
		var href = window.location.href.split('#')[0], idx = href.indexOf('?');
		if (idx < 0)
			return '';
		return href.slice(idx+1);
	}
};

<|endoftext|>


<commit_before>/*global MAPJS, Color, $, _*/
/*jslint nomen: true, newcap: true, browser: true*/

MAPJS.domMediator = function (mapModel, stageElement) {
	'use strict';

	var connectorKey = function (connectorObj) {
			return 'connector_' + connectorObj.from + '_' + connectorObj.to;
		},
		svg = function (tag) {
			return document.createElementNS('http://www.w3.org/2000/svg', tag);
		},
		horizontalConnector = function (parentX, parentY, parentWidth, parentHeight,
				childX, childY, childWidth, childHeight) {
			var childHorizontalOffset = parentX < childX ? 0.1 : 0.9,
				parentHorizontalOffset = 1 - childHorizontalOffset;
			return {
				from: {
					x: parentX + parentHorizontalOffset * parentWidth,
					y: parentY + 0.5 * parentHeight
				},
				to: {
					x: childX + childHorizontalOffset * childWidth,
					y: childY + 0.5 * childHeight
				},
				controlPointOffset: 0
			};
		},
		calculateConnector = function (parent, child) {
			return calculateConnectorInner(parent.position().left, parent.position().top, parent.width(), parent.height(),
				child.position().left, child.position().top, child.width(), child.height());
		},
		calculateConnectorInner = _.memoize(function (parentX, parentY, parentWidth, parentHeight,
				childX, childY, childWidth, childHeight) {
			var tolerance = 10,
				childMid = childY + childHeight * 0.5,
				parentMid = parentY + parentHeight * 0.5,
				childHorizontalOffset;
			if (Math.abs(parentMid - childMid) + tolerance < Math.max(childHeight, parentHeight * 0.75)) {
				return horizontalConnector(parentX, parentY, parentWidth, parentHeight, childX, childY, childWidth, childHeight);
			}
			childHorizontalOffset = parentX < childX ? 0 : 1;
			return {
				from: {
					x: parentX + 0.5 * parentWidth,
					y: parentY + 0.5 * parentHeight
				},
				to: {
					x: childX + childHorizontalOffset * childWidth,
					y: childY + 0.5 * childHeight
				},
				controlPointOffset: 0.75
			};
		}, function () {
			return Array.prototype.join.call(arguments, ',');
		}),
		updateDOMConnector = function (domElement) {
			var config = {
					stroke: '#888',
					width: 1
				},
				element = $(domElement),
				shapeFrom = $('#node_' + element.attr('data-connector-from')),
				shapeTo = $('#node_' + element.attr('data-connector-to')),
				calculatedConnector = calculateConnector(shapeFrom, shapeTo),
				from = calculatedConnector.from,
				to = calculatedConnector.to,
				position = {
					left: Math.min(shapeFrom.position().left, shapeTo.position().left),
					top: Math.min(shapeFrom.position().top, shapeTo.position().top),
				},
				offset = calculatedConnector.controlPointOffset * (from.y - to.y),
				maxOffset = Math.min(shapeTo.height(), shapeFrom.height()) * 1.5,
				straightLine = false;
			position.width = Math.max(shapeFrom.position().left + shapeFrom.width(), shapeTo.position().left + shapeTo.width(), position.left + 1) - position.left;
			position.height = Math.max(shapeFrom.position().top + shapeFrom.height(), shapeTo.position().top + shapeTo.height(), position.top + 1) - position.top;
			element.css(position);
			element.empty();
			if (straightLine) {
				$(svg('line')).attr({
					x1: from.x - position.left,
					x2: to.x - position.left,
					y1: from.y - position.top,
					y2: to.y - position.top,
					style: 'stroke:' + config.stroke + ';stroke-width:' + config.width + 'px'
				}).appendTo(element);
			} else {
				offset = Math.max(-maxOffset, Math.min(maxOffset, offset));
				$(svg('path')).attr('d',
					'M' + (from.x - position.left) + ',' + (from.y - position.top) +
					'Q' + (from.x - position.left) + ',' + (to.y - offset - position.top) + ' ' + (to.x - position.left) + ',' + (to.y - position.top)
				).attr({
					fill: 'none',
					stroke: config.stroke,
					'stroke-width': config.width
				}).appendTo(element);
			}
		};

	mapModel.addEventListener('nodeSelectionChanged', function (ideaId, isSelected) {
		var node = $('#node_' + ideaId);
		if (isSelected) {
//			node.addClass('selected');
			node.focus();
		} else {
//			node.removeClass('selected');
		}
	});
	mapModel.addEventListener('nodeRemoved', function (node) {
		$('#node_' + node.id).remove();
	});
	mapModel.addEventListener('nodeMoved', function (node, reason) {
		var connectors = $('[data-connector-from=' + node.id + ']').add('[data-connector-to=' + node.id + ']');
		$('#node_' + node.id).css({
			'left': node.x + stageElement.innerWidth() / 2,
			'top': node.y + stageElement.innerHeight() / 2
		});
		_.each(connectors, updateDOMConnector);
		//onFinish: ensureSelectedNodeVisible.bind(undefined, node)
	});

	mapModel.addEventListener('connectorCreated', function (connector) {
		var	domConnector = $(svg('svg'))
			.attr({'id': connectorKey(connector), 'data-connector-from': connector.from, 'data-connector-to': connector.to})
			.addClass('connector')
			.appendTo(stageElement);
		updateDOMConnector(domConnector);
	});
	mapModel.addEventListener('connectorRemoved', function (connector) {
		$('#' + connectorKey(connector)).remove();
	});
	mapModel.addEventListener('nodeCreated', function (node) {
		var config = {
				padding: '8px'
			},
			backgroundColor = function () {
				var fromStyle =	node.attr && node.attr.style && node.attr.style.background,
					generic = MAPJS.defaultStyles[node.level === 1 ? 'root' : 'nonRoot'].background;
				return fromStyle ||  generic;
			},
			foregroundColor = function (backgroundColor) {
				var tintedBackground = Color(backgroundColor).mix(Color('#EEEEEE')).hexString();
				return MAPJS.contrastForeground(tintedBackground);
			},
			nodeDiv = $('<div>')
				.attr('tabindex', 0)
				.attr('id', 'node_' + node.id)
				.addClass('node')
				.css({
				'left': node.x + stageElement.innerWidth() / 2,
				'top': node.y + stageElement.innerHeight() / 2,
				'width': node.width,
				'height': node.height,
				'background-color': backgroundColor()
			}).appendTo(stageElement),
			textBox = $('<span>').addClass('text').text(node.title).appendTo(nodeDiv).css({
				color: foregroundColor(backgroundColor()),
				display: 'block'
			}),
			icon;
		if (node.attr && node.attr.icon) {
			icon = document.createElement('img');
			icon.src = node.attr.icon.url;
			icon.width = node.attr.icon.width;
			icon.height = node.attr.icon.height;

			if (node.attr.icon.position === 'top') {
				$(icon).css({
					'display': 'block',
					'margin-left': (node.width - icon.width) / 2,
					'margin-top': config.padding
				}).prependTo(nodeDiv);
				textBox.css({
					'display': 'block',
					'width': '100%',
					'margin-top': config.padding
				});
				nodeDiv.css('text-align', 'center');
			} else {
				nodeDiv.prepend(icon);
			}
		}
		else {
			textBox.css({
				'margin-top': (node.height - textBox.outerHeight(true)) / 2,
				'margin-left': (node.width - textBox.outerWidth(true)) / 2
			});

		}
	});
};
$.fn.domMapWidget = function (activityLog, mapModel) {
	'use strict';
	var hotkeyEventHandlers = {
			'return': 'addSiblingIdea',
			'shift+return': 'addSiblingIdeaBefore',
			'del backspace': 'removeSubIdea',
			'tab insert': 'addSubIdea',
			'left': 'selectNodeLeft',
			'up': 'selectNodeUp',
			'right': 'selectNodeRight',
			'shift+right': 'activateNodeRight',
			'shift+left': 'activateNodeLeft',
			'shift+up': 'activateNodeUp',
			'shift+down': 'activateNodeDown',
			'down': 'selectNodeDown',
			'space f2': 'editNode',
			'f': 'toggleCollapse',
			'c meta+x ctrl+x': 'cut',
			'p meta+v ctrl+v': 'paste',
			'y meta+c ctrl+c': 'copy',
			'u meta+z ctrl+z': 'undo',
			'shift+tab': 'insertIntermediate',
			'Esc 0 meta+0 ctrl+0': 'resetView',
			'r meta+shift+z ctrl+shift+z meta+y ctrl+y': 'redo',
			'meta+plus ctrl+plus z': 'scaleUp',
			'meta+minus ctrl+minus shift+z': 'scaleDown',
			'meta+up ctrl+up': 'moveUp',
			'meta+down ctrl+down': 'moveDown',
			'ctrl+shift+v meta+shift+v': 'pasteStyle',
			'Esc': 'cancelCurrentAction'
		},
		charEventHandlers = {
			'[' : 'activateChildren',
			'{'	: 'activateNodeAndChildren',
			'='	: 'activateSiblingNodes',
			'.'	: 'activateSelectedNode',
			'/' : 'toggleCollapse',
			'a' : 'openAttachment',
			'i' : 'editIcon'
		},
		actOnKeys = true;
	mapModel.addEventListener('inputEnabledChanged', function (canInput) {
		actOnKeys = canInput;
	});

	return this.each(function () {
		var element = $(this);
		MAPJS.domMediator(mapModel, element);
		_.each(hotkeyEventHandlers, function (mappedFunction, keysPressed) {
			element.keydown(keysPressed, function (event) {
				if (actOnKeys) {
					event.preventDefault();
					mapModel[mappedFunction]('keyboard');
				}
			});
		});
		element.on('keypress', function (evt) {
			if (!actOnKeys) {
				return;
			}
			if (/INPUT|TEXTAREA/.test(evt && evt.target && evt.target.tagName)) {
				return;
			}
			var unicode = evt.charCode || evt.keyCode,
				actualkey = String.fromCharCode(unicode),
				mappedFunction = charEventHandlers[actualkey];
			if (mappedFunction) {
				evt.preventDefault();
				mapModel[mappedFunction]('keyboard');
			} else if (Number(actualkey) <= 9 && Number(actualkey) >= 1) {
				evt.preventDefault();
				mapModel.activateLevel('keyboard', Number(actualkey) + 1);
			}
		});
	});
};

// + shadows
// + selected
// + default and non default backgrounds for root and children
// + multi-line text
//
// focus or selected?
// drag * drop
// drag background
// icon position
// custom connectors
// attachment - clip
// straight lines extension
// collaboration avatars
// folded
// activated
// zoom
// mouse events
// mapwidget keyboard bindings
// mapwidget mouse bindings
// hyperlinks
// remaining kinetic mediator events
// -	mapModel.addEventListener('addLinkModeToggled', function (isOn) {
// -	mapModel.addEventListener('nodeEditRequested', function (nodeId, shouldSelectAll, editingNew) {
// +	mapModel.addEventListener('nodeCreated', function (n) {
// -	mapModel.addEventListener('nodeSelectionChanged', function (ideaId, isSelected) {
// -	mapModel.addEventListener('nodeFocusRequested', function (ideaId)  {
// -	mapModel.addEventListener('nodeAttrChanged', function (n) {
// -	mapModel.addEventListener('nodeDroppableChanged', function (ideaId, isDroppable) {
// +	mapModel.addEventListener('nodeRemoved', function (n) {
// -	mapModel.addEventListener('nodeMoved', function (n, reason) {
// -	mapModel.addEventListener('nodeTitleChanged', function (n) {
// +	mapModel.addEventListener('connectorCreated', function (n) {
// -	mapModel.addEventListener('layoutChangeComplete', function () {
// +	mapModel.addEventListener('connectorRemoved', function (n) {
// -	mapModel.addEventListener('linkCreated', function (l) {
// -	mapModel.addEventListener('linkRemoved', function (l) {
// -	mapModel.addEventListener('linkAttrChanged', function (l) {
// -	mapModel.addEventListener('mapScaleChanged', function (scaleMultiplier, zoomPoint) {
// -	mapModel.addEventListener('mapViewResetRequested', function () {
// -	mapModel.addEventListener('mapMoveRequested', function (deltaX, deltaY) {
// -	mapModel.addEventListener('activatedNodesChanged', function (activatedNodes, deactivatedNodes) {
// animations
// - node removed
// - node moved (esp reason = failed)
// no more memoization on calc connector - not needed

<commit_msg>nodeTitleChanged<commit_after>/*global MAPJS, Color, $, _*/
/*jslint nomen: true, newcap: true, browser: true*/

MAPJS.domMediator = function (mapModel, stageElement) {
	'use strict';

	var connectorKey = function (connectorObj) {
			return 'connector_' + connectorObj.from + '_' + connectorObj.to;
		},
		svg = function (tag) {
			return document.createElementNS('http://www.w3.org/2000/svg', tag);
		},
		horizontalConnector = function (parentX, parentY, parentWidth, parentHeight,
				childX, childY, childWidth, childHeight) {
			var childHorizontalOffset = parentX < childX ? 0.1 : 0.9,
				parentHorizontalOffset = 1 - childHorizontalOffset;
			return {
				from: {
					x: parentX + parentHorizontalOffset * parentWidth,
					y: parentY + 0.5 * parentHeight
				},
				to: {
					x: childX + childHorizontalOffset * childWidth,
					y: childY + 0.5 * childHeight
				},
				controlPointOffset: 0
			};
		},
		calculateConnector = function (parent, child) {
			return calculateConnectorInner(parent.position().left, parent.position().top, parent.width(), parent.height(),
				child.position().left, child.position().top, child.width(), child.height());
		},
		calculateConnectorInner = _.memoize(function (parentX, parentY, parentWidth, parentHeight,
				childX, childY, childWidth, childHeight) {
			var tolerance = 10,
				childMid = childY + childHeight * 0.5,
				parentMid = parentY + parentHeight * 0.5,
				childHorizontalOffset;
			if (Math.abs(parentMid - childMid) + tolerance < Math.max(childHeight, parentHeight * 0.75)) {
				return horizontalConnector(parentX, parentY, parentWidth, parentHeight, childX, childY, childWidth, childHeight);
			}
			childHorizontalOffset = parentX < childX ? 0 : 1;
			return {
				from: {
					x: parentX + 0.5 * parentWidth,
					y: parentY + 0.5 * parentHeight
				},
				to: {
					x: childX + childHorizontalOffset * childWidth,
					y: childY + 0.5 * childHeight
				},
				controlPointOffset: 0.75
			};
		}, function () {
			return Array.prototype.join.call(arguments, ',');
		}),
		updateDOMConnector = function (domElement) {
			var config = {
					stroke: '#888',
					width: 1
				},
				element = $(domElement),
				shapeFrom = $('#node_' + element.attr('data-connector-from')),
				shapeTo = $('#node_' + element.attr('data-connector-to')),
				calculatedConnector = calculateConnector(shapeFrom, shapeTo),
				from = calculatedConnector.from,
				to = calculatedConnector.to,
				position = {
					left: Math.min(shapeFrom.position().left, shapeTo.position().left),
					top: Math.min(shapeFrom.position().top, shapeTo.position().top),
				},
				offset = calculatedConnector.controlPointOffset * (from.y - to.y),
				maxOffset = Math.min(shapeTo.height(), shapeFrom.height()) * 1.5,
				straightLine = false;
			position.width = Math.max(shapeFrom.position().left + shapeFrom.width(), shapeTo.position().left + shapeTo.width(), position.left + 1) - position.left;
			position.height = Math.max(shapeFrom.position().top + shapeFrom.height(), shapeTo.position().top + shapeTo.height(), position.top + 1) - position.top;
			element.css(position);
			element.empty();
			if (straightLine) {
				$(svg('line')).attr({
					x1: from.x - position.left,
					x2: to.x - position.left,
					y1: from.y - position.top,
					y2: to.y - position.top,
					style: 'stroke:' + config.stroke + ';stroke-width:' + config.width + 'px'
				}).appendTo(element);
			} else {
				offset = Math.max(-maxOffset, Math.min(maxOffset, offset));
				$(svg('path')).attr('d',
					'M' + (from.x - position.left) + ',' + (from.y - position.top) +
					'Q' + (from.x - position.left) + ',' + (to.y - offset - position.top) + ' ' + (to.x - position.left) + ',' + (to.y - position.top)
				).attr({
					fill: 'none',
					stroke: config.stroke,
					'stroke-width': config.width
				}).appendTo(element);
			}
		};

	mapModel.addEventListener('nodeSelectionChanged', function (ideaId, isSelected) {
		var node = $('#node_' + ideaId);
		if (isSelected) {
//			node.addClass('selected');
			node.focus();
		} else {
//			node.removeClass('selected');
		}
	});
	mapModel.addEventListener('nodeTitleChanged', function (node) {
		$('#node_' + node.id).find('.text').text(node.title);
	});
	mapModel.addEventListener('nodeRemoved', function (node) {
		$('#node_' + node.id).remove();
	});
	mapModel.addEventListener('nodeMoved', function (node, reason) {
		var connectors = $('[data-connector-from=' + node.id + ']').add('[data-connector-to=' + node.id + ']');
		$('#node_' + node.id).css({
			'left': node.x + stageElement.innerWidth() / 2,
			'top': node.y + stageElement.innerHeight() / 2
		});
		_.each(connectors, updateDOMConnector);
		//onFinish: ensureSelectedNodeVisible.bind(undefined, node)
	});

	mapModel.addEventListener('connectorCreated', function (connector) {
		var	domConnector = $(svg('svg'))
			.attr({'id': connectorKey(connector), 'data-connector-from': connector.from, 'data-connector-to': connector.to})
			.addClass('connector')
			.appendTo(stageElement);
		updateDOMConnector(domConnector);
	});
	mapModel.addEventListener('connectorRemoved', function (connector) {
		$('#' + connectorKey(connector)).remove();
	});
	mapModel.addEventListener('nodeCreated', function (node) {
		var config = {
				padding: '8px'
			},
			backgroundColor = function () {
				var fromStyle =	node.attr && node.attr.style && node.attr.style.background,
					generic = MAPJS.defaultStyles[node.level === 1 ? 'root' : 'nonRoot'].background;
				return fromStyle ||  generic;
			},
			foregroundColor = function (backgroundColor) {
				var tintedBackground = Color(backgroundColor).mix(Color('#EEEEEE')).hexString();
				return MAPJS.contrastForeground(tintedBackground);
			},
			nodeDiv = $('<div>')
				.attr('tabindex', 0)
				.attr('id', 'node_' + node.id)
				.addClass('node')
				.css({
				'left': node.x + stageElement.innerWidth() / 2,
				'top': node.y + stageElement.innerHeight() / 2,
				'width': node.width,
				'height': node.height,
				'background-color': backgroundColor()
			}).appendTo(stageElement),
			textBox = $('<span>').addClass('text').text(node.title).appendTo(nodeDiv).css({
				color: foregroundColor(backgroundColor()),
				display: 'block'
			}),
			icon;
		if (node.attr && node.attr.icon) {
			icon = document.createElement('img');
			icon.src = node.attr.icon.url;
			icon.width = node.attr.icon.width;
			icon.height = node.attr.icon.height;

			if (node.attr.icon.position === 'top') {
				$(icon).css({
					'display': 'block',
					'margin-left': (node.width - icon.width) / 2,
					'margin-top': config.padding
				}).prependTo(nodeDiv);
				textBox.css({
					'display': 'block',
					'width': '100%',
					'margin-top': config.padding
				});
				nodeDiv.css('text-align', 'center');
			} else {
				nodeDiv.prepend(icon);
			}
		}
		else {
			textBox.css({
				'margin-top': (node.height - textBox.outerHeight(true)) / 2,
				'margin-left': (node.width - textBox.outerWidth(true)) / 2
			});

		}
	});
};
$.fn.domMapWidget = function (activityLog, mapModel) {
	'use strict';
	var hotkeyEventHandlers = {
			'return': 'addSiblingIdea',
			'shift+return': 'addSiblingIdeaBefore',
			'del backspace': 'removeSubIdea',
			'tab insert': 'addSubIdea',
			'left': 'selectNodeLeft',
			'up': 'selectNodeUp',
			'right': 'selectNodeRight',
			'shift+right': 'activateNodeRight',
			'shift+left': 'activateNodeLeft',
			'shift+up': 'activateNodeUp',
			'shift+down': 'activateNodeDown',
			'down': 'selectNodeDown',
			'space f2': 'editNode',
			'f': 'toggleCollapse',
			'c meta+x ctrl+x': 'cut',
			'p meta+v ctrl+v': 'paste',
			'y meta+c ctrl+c': 'copy',
			'u meta+z ctrl+z': 'undo',
			'shift+tab': 'insertIntermediate',
			'Esc 0 meta+0 ctrl+0': 'resetView',
			'r meta+shift+z ctrl+shift+z meta+y ctrl+y': 'redo',
			'meta+plus ctrl+plus z': 'scaleUp',
			'meta+minus ctrl+minus shift+z': 'scaleDown',
			'meta+up ctrl+up': 'moveUp',
			'meta+down ctrl+down': 'moveDown',
			'ctrl+shift+v meta+shift+v': 'pasteStyle',
			'Esc': 'cancelCurrentAction'
		},
		charEventHandlers = {
			'[' : 'activateChildren',
			'{'	: 'activateNodeAndChildren',
			'='	: 'activateSiblingNodes',
			'.'	: 'activateSelectedNode',
			'/' : 'toggleCollapse',
			'a' : 'openAttachment',
			'i' : 'editIcon'
		},
		actOnKeys = true;
	mapModel.addEventListener('inputEnabledChanged', function (canInput) {
		actOnKeys = canInput;
	});

	return this.each(function () {
		var element = $(this);
		MAPJS.domMediator(mapModel, element);
		_.each(hotkeyEventHandlers, function (mappedFunction, keysPressed) {
			element.keydown(keysPressed, function (event) {
				if (actOnKeys) {
					event.preventDefault();
					mapModel[mappedFunction]('keyboard');
				}
			});
		});
		element.on('keypress', function (evt) {
			if (!actOnKeys) {
				return;
			}
			if (/INPUT|TEXTAREA/.test(evt && evt.target && evt.target.tagName)) {
				return;
			}
			var unicode = evt.charCode || evt.keyCode,
				actualkey = String.fromCharCode(unicode),
				mappedFunction = charEventHandlers[actualkey];
			if (mappedFunction) {
				evt.preventDefault();
				mapModel[mappedFunction]('keyboard');
			} else if (Number(actualkey) <= 9 && Number(actualkey) >= 1) {
				evt.preventDefault();
				mapModel.activateLevel('keyboard', Number(actualkey) + 1);
			}
		});
	});
};

// + shadows
// + selected
// + default and non default backgrounds for root and children
// + multi-line text
//
// focus or selected?
// drag * drop
// drag background
// icon position
// custom connectors
// attachment - clip
// straight lines extension
// collaboration avatars
// folded
// activated
// zoom
// mouse events
// mapwidget keyboard bindings
// mapwidget mouse bindings
// hyperlinks
// remaining kinetic mediator events
// -	mapModel.addEventListener('addLinkModeToggled', function (isOn) {
// -	mapModel.addEventListener('nodeEditRequested', function (nodeId, shouldSelectAll, editingNew) {
// +	mapModel.addEventListener('nodeCreated', function (n) {
// -	mapModel.addEventListener('nodeSelectionChanged', function (ideaId, isSelected) {
// -	mapModel.addEventListener('nodeFocusRequested', function (ideaId)  {
// -	mapModel.addEventListener('nodeAttrChanged', function (n) {
// -	mapModel.addEventListener('nodeDroppableChanged', function (ideaId, isDroppable) {
// +	mapModel.addEventListener('nodeRemoved', function (n) {
// +	mapModel.addEventListener('nodeMoved', function (n, reason) {
// +	mapModel.addEventListener('nodeTitleChanged', function (n) {
// +	mapModel.addEventListener('connectorCreated', function (n) {
// -	mapModel.addEventListener('layoutChangeComplete', function () {
// +	mapModel.addEventListener('connectorRemoved', function (n) {
// -	mapModel.addEventListener('linkCreated', function (l) {
// -	mapModel.addEventListener('linkRemoved', function (l) {
// -	mapModel.addEventListener('linkAttrChanged', function (l) {
// -	mapModel.addEventListener('mapScaleChanged', function (scaleMultiplier, zoomPoint) {
// -	mapModel.addEventListener('mapViewResetRequested', function () {
// -	mapModel.addEventListener('mapMoveRequested', function (deltaX, deltaY) {
// -	mapModel.addEventListener('activatedNodesChanged', function (activatedNodes, deactivatedNodes) {
// animations
// - node removed
// - node moved (esp reason = failed)
// no more memoization on calc connector - not needed
<|endoftext|>


<commit_before>(function(window){'use strict';

  // DO NOT USE THIS FILE DIRECTLY, IT WON'T WORK
  // THIS IS A PROJECT BASED ON A BUILD SYSTEM
  // THIS FILE IS JUST WRAPPED UP RESULTING IN
  // build/document-register-element.js
  // and its .max.js counter part

  var
    document = window.document,
    Object = window.Object
  ;

}(window));
<commit_msg>trying to fix pages<commit_after><|endoftext|>


<commit_before>import {ProseMirrorError} from "../util/error"
import {contains, browser} from "../dom"

import {posFromDOM, DOMAfterPos, DOMFromPos, coordsAtPos} from "./dompos"

// ;; Error type used to signal selection-related problems.
export class SelectionError extends ProseMirrorError {}

// Track the state of the current editor selection. Keeps the editor
// selection in sync with the DOM selection by polling for changes,
// as there is no DOM event for DOM selection changes.
export class SelectionState {
  constructor(pm, range) {
    this.pm = pm
    // The current editor selection.
    this.range = range

    this.lastNonNodePos = null

    // The timeout ID for the poller when active.
    this.polling = null
    // Track the state of the DOM selection.
    this.lastAnchorNode = this.lastHeadNode = this.lastAnchorOffset = this.lastHeadOffset = null
    // The corresponding DOM node when a node selection is active.
    this.lastNode = null

    pm.content.addEventListener("focus", () => this.receivedFocus())

    this.poller = this.poller.bind(this)
  }

  // : (Selection, boolean)
  // Set the current selection and signal an event on the editor.
  setAndSignal(range, clearLast) {
    this.set(range, clearLast)
    // :: () #path=ProseMirror#events#selectionChange
    // Indicates that the editor's selection has changed.
    this.pm.signal("selectionChange")
  }

  // : (Selection, boolean)
  // Set the current selection.
  set(range, clearLast) {
    this.pm.ensureOperation({readSelection: false})
    this.range = range
    if (!range.node) this.lastNonNodePos = null
    if (clearLast !== false) this.lastAnchorNode = null
  }

  poller() {
    if (hasFocus(this.pm)) {
      if (!this.pm.operation) this.readFromDOM()
      this.polling = setTimeout(this.poller, 100)
    } else {
      this.polling = null
    }
  }

  startPolling() {
    clearTimeout(this.polling)
    this.polling = setTimeout(this.poller, 50)
  }

  fastPoll() {
    this.startPolling()
  }

  stopPolling() {
    clearTimeout(this.polling)
    this.polling = null
  }

  // : () → bool
  // Whether the DOM selection has changed from the last known state.
  domChanged() {
    let sel = window.getSelection()
    return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
      sel.focusNode != this.lastHeadNode || sel.focusOffset != this.lastHeadOffset
  }

  // Store the current state of the DOM selection.
  storeDOMState() {
    let sel = window.getSelection()
    this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
    this.lastHeadNode = sel.focusNode; this.lastHeadOffset = sel.focusOffset
  }

  // : () → bool
  // When the DOM selection changes in a notable manner, modify the
  // current selection state to match.
  readFromDOM() {
    if (this.pm.input.composing || !hasFocus(this.pm) || !this.domChanged()) return false

    let sel = window.getSelection(), doc = this.pm.doc
    let anchor = posFromDOM(this.pm, sel.anchorNode, sel.anchorOffset)
    let head = sel.isCollapsed ? anchor : posFromDOM(this.pm, sel.focusNode, sel.focusOffset)

    let newRange = findSelectionNear(doc, head, this.range.head != null && this.range.head < head ? 1 : -1)
    if (newRange instanceof TextSelection && doc.resolve(anchor).parent.isTextblock) {
      newRange = new TextSelection(anchor, newRange.head)
    } else if (newRange instanceof NodeSelection && (anchor < newRange.from || anchor > newRange.to)) {
      // If head falls on a node, but anchor falls outside of it,
      // create a text selection between them
      let inv = anchor > newRange.to
      newRange = new TextSelection(findSelectionNear(doc, anchor, inv ? -1 : 1, true).anchor,
                                   findSelectionNear(doc, inv ? newRange.from : newRange.to, inv ? 1 : -1, true).head)
    }
    this.setAndSignal(newRange)

    if (newRange instanceof NodeSelection || newRange.head != head || newRange.anchor != anchor) {
      this.toDOM()
    } else {
      this.clearNode()
      this.storeDOMState()
    }
    return true
  }

  toDOM(takeFocus) {
    if (!hasFocus(this.pm)) {
      if (!takeFocus) return
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=921444
      else if (browser.gecko) this.pm.content.focus()
    }
    if (this.range instanceof NodeSelection)
      this.nodeToDOM()
    else
      this.rangeToDOM()
  }

  // Make changes to the DOM for a node selection.
  nodeToDOM() {
    let dom = DOMAfterPos(this.pm.content, this.range.from)
    if (dom != this.lastNode) {
      this.clearNode()
      dom.classList.add("ProseMirror-selectednode")
      this.pm.content.classList.add("ProseMirror-nodeselection")
      this.lastNode = dom
    }
    let range = document.createRange(), sel = window.getSelection()
    range.selectNode(dom)
    sel.removeAllRanges()
    sel.addRange(range)
    this.storeDOMState()
  }

  // Make changes to the DOM for a text selection.
  rangeToDOM() {
    this.clearNode()

    let anchor = DOMFromPos(this.pm.content, this.range.anchor)
    let head = DOMFromPos(this.pm.content, this.range.head)

    let sel = window.getSelection(), range = document.createRange()
    if (sel.extend) {
      range.setEnd(anchor.node, anchor.offset)
      range.collapse(false)
    } else {
      if (this.range.anchor > this.range.head) { let tmp = anchor; anchor = head; head = tmp }
      range.setEnd(head.node, head.offset)
      range.setStart(anchor.node, anchor.offset)
    }
    sel.removeAllRanges()
    sel.addRange(range)
    if (sel.extend)
      sel.extend(head.node, head.offset)
    this.storeDOMState()
  }

  // Clear all DOM statefulness of the last node selection.
  clearNode() {
    if (this.lastNode) {
      this.lastNode.classList.remove("ProseMirror-selectednode")
      this.pm.content.classList.remove("ProseMirror-nodeselection")
      this.lastNode = null
      return true
    }
  }

  receivedFocus() {
    if (this.polling == null) this.startPolling()
  }
}

// ;; An editor selection. Can be one of two selection types:
// `TextSelection` and `NodeSelection`. Both have the properties
// listed here, but also contain more information (such as the
// selected [node](#NodeSelection.node) or the
// [head](#TextSelection.head) and [anchor](#TextSelection.anchor)).
export class Selection {
  // :: number #path=Selection.prototype.from
  // The left-bound of the selection.

  // :: number #path=Selection.prototype.to
  // The right-bound of the selection.

  // :: bool #path=Selection.empty
  // True if the selection is an empty text selection (head an anchor
  // are the same).

  // :: (other: Selection) → bool #path=Selection.eq
  // Test whether the selection is the same as another selection.

  // :: (doc: Node, mapping: Mappable) → Selection #path=Selection.map
  // Map this selection through a [mappable](#Mappable) thing. `doc`
  // should be the new document, to which we are mapping.
}

// ;; A text selection represents a classical editor
// selection, with a head (the moving side) and anchor (immobile
// side), both of which point into textblock nodes. It can be empty (a
// regular cursor position).
export class TextSelection extends Selection {
  // :: (number, ?number)
  // Construct a text selection. When `head` is not given, it defaults
  // to `anchor`.
  constructor(anchor, head) {
    super()
    // :: number
    // The selection's immobile side (does not move when pressing
    // shift-arrow).
    this.anchor = anchor
    // :: number
    // The selection's mobile side (the side that moves when pressing
    // shift-arrow).
    this.head = head == null ? anchor : head
  }

  get inverted() { return this.anchor > this.head }
  get from() { return Math.min(this.head, this.anchor) }
  get to() { return Math.max(this.head, this.anchor) }
  get empty() { return this.anchor == this.head }

  eq(other) {
    return other instanceof TextSelection && other.head == this.head && other.anchor == this.anchor
  }

  map(doc, mapping) {
    let head = mapping.map(this.head).pos
    if (!doc.resolve(head).parent.isTextblock)
      return findSelectionNear(doc, head)
    let anchor = mapping.map(this.anchor).pos
    return new TextSelection(doc.resolve(anchor).parent.isTextblock ? anchor : head, head)
  }
}

// ;; A node selection is a selection that points at a
// single node. All nodes marked [selectable](#NodeType.selectable)
// can be the target of a node selection. In such an object, `from`
// and `to` point directly before and after the selected node.
export class NodeSelection extends Selection {
  // :: (number, number, Node)
  // Create a node selection. Does not verify the validity of its
  // arguments. Use `ProseMirror.setNodeSelection` for an easier,
  // error-checking way to create a node selection.
  constructor(from, to, node) {
    super()
    this.from = from
    this.to = to
    // :: Node The selected node.
    this.node = node
  }

  get empty() { return false }

  eq(other) {
    return other instanceof NodeSelection && this.from == other.from
  }

  map(doc, mapping) {
    let from = mapping.map(this.from, 1).pos
    let to = mapping.map(this.to, -1).pos
    let node = doc.nodeAt(from)
    if (node && to == from + node.nodeSize && node.type.selectable)
      return new NodeSelection(from, to, node)
    return findSelectionNear(doc, from)
  }
}

export function rangeFromDOMLoose(pm) {
  if (!hasFocus(pm)) return null
  let sel = window.getSelection()
  return new TextSelection(posFromDOM(pm, sel.anchorNode, sel.anchorOffset, true),
                           posFromDOM(pm, sel.focusNode, sel.focusOffset, true))
}

export function hasFocus(pm) {
  if (document.activeElement != pm.content) return false
  let sel = window.getSelection()
  return sel.rangeCount && contains(pm.content, sel.anchorNode)
}

// Try to find a selection inside the given node. `pos` points at the
// position where the search starts. When `text` is true, only return
// text selections.
function findSelectionIn(node, pos, index, dir, text) {
  for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) {
    let child = node.child(i)
    if (child.isTextblock) return new TextSelection(pos + dir)
    if (child.type.contains) {
      let inner = findSelectionIn(child, pos + dir, dir < 0 ? child.childCount : 0, dir, text)
      if (inner) return inner
    } else if (!text && child.type.selectable) {
      return new NodeSelection(pos - (dir < 0 ? child.nodeSize : 0),
                               pos + (dir > 0 ? child.nodeSize : 0), child)
    }
    pos += child.nodeSize * dir
  }
}

// FIXME we'll need some awareness of text direction when scanning for selections

// Create a selection which is moved relative to a position in a
// given direction. When a selection isn't found at the given position,
// walks up the document tree one level and one step in the
// desired direction.
export function findSelectionFrom(doc, pos, dir, text) {
  let $pos = doc.resolve(pos)
  let inner = $pos.parent.isTextblock ? new TextSelection(pos)
      : findSelectionIn($pos.parent, pos, $pos.index($pos.depth), dir, text)
  if (inner) return inner

  for (let depth = $pos.depth - 1; depth >= 0; depth--) {
    let found = dir < 0
        ? findSelectionIn($pos.node(depth), $pos.before(depth + 1), $pos.index(depth), dir, text)
        : findSelectionIn($pos.node(depth), $pos.after(depth + 1), $pos.index(depth) + 1, dir, text)
    if (found) return found
  }
}

export function findSelectionNear(doc, pos, bias = 1, text) {
  let result = findSelectionFrom(doc, pos, bias, text) || findSelectionFrom(doc, pos, -bias, text)
  if (!result) SelectionError("Searching for selection in invalid document " + doc)
  return result
}

// Find the selection closest to the start of the given node. `pos`,
// if given, should point at the start of the node's content.
export function findSelectionAtStart(node, pos = 0, text) {
  return findSelectionIn(node, pos, 0, 1, text)
}

// Find the selection closest to the end of the given node.
export function findSelectionAtEnd(node, pos = node.content.size, text) {
  return findSelectionIn(node, pos, node.content.size, -1, text)
}

// : (ProseMirror, number, number)
// Whether vertical position motion in a given direction
// from a position would leave a text block.
export function verticalMotionLeavesTextblock(pm, pos, dir) {
  let $pos = pm.doc.resolve(pos)
  let dom = DOMAfterPos(pm.content, $pos.before($pos.depth))
  let coords = coordsAtPos(pm, pos)
  for (let child = dom.firstChild; child; child = child.nextSibling) {
    if (child.nodeType != 1) continue
    let boxes = child.getClientRects()
    for (let i = 0; i < boxes.length; i++) {
      let box = boxes[i]
      if (dir < 0 ? box.bottom < coords.top : box.top > coords.bottom)
        return false
    }
  }
  return true
}
<commit_msg>Fix pasting of text<commit_after>import {ProseMirrorError} from "../util/error"
import {contains, browser} from "../dom"

import {posFromDOM, DOMAfterPos, DOMFromPos, coordsAtPos} from "./dompos"

// ;; Error type used to signal selection-related problems.
export class SelectionError extends ProseMirrorError {}

// Track the state of the current editor selection. Keeps the editor
// selection in sync with the DOM selection by polling for changes,
// as there is no DOM event for DOM selection changes.
export class SelectionState {
  constructor(pm, range) {
    this.pm = pm
    // The current editor selection.
    this.range = range

    this.lastNonNodePos = null

    // The timeout ID for the poller when active.
    this.polling = null
    // Track the state of the DOM selection.
    this.lastAnchorNode = this.lastHeadNode = this.lastAnchorOffset = this.lastHeadOffset = null
    // The corresponding DOM node when a node selection is active.
    this.lastNode = null

    pm.content.addEventListener("focus", () => this.receivedFocus())

    this.poller = this.poller.bind(this)
  }

  // : (Selection, boolean)
  // Set the current selection and signal an event on the editor.
  setAndSignal(range, clearLast) {
    this.set(range, clearLast)
    // :: () #path=ProseMirror#events#selectionChange
    // Indicates that the editor's selection has changed.
    this.pm.signal("selectionChange")
  }

  // : (Selection, boolean)
  // Set the current selection.
  set(range, clearLast) {
    this.pm.ensureOperation({readSelection: false})
    this.range = range
    if (!range.node) this.lastNonNodePos = null
    if (clearLast !== false) this.lastAnchorNode = null
  }

  poller() {
    if (hasFocus(this.pm)) {
      if (!this.pm.operation) this.readFromDOM()
      this.polling = setTimeout(this.poller, 100)
    } else {
      this.polling = null
    }
  }

  startPolling() {
    clearTimeout(this.polling)
    this.polling = setTimeout(this.poller, 50)
  }

  fastPoll() {
    this.startPolling()
  }

  stopPolling() {
    clearTimeout(this.polling)
    this.polling = null
  }

  // : () → bool
  // Whether the DOM selection has changed from the last known state.
  domChanged() {
    let sel = window.getSelection()
    return sel.anchorNode != this.lastAnchorNode || sel.anchorOffset != this.lastAnchorOffset ||
      sel.focusNode != this.lastHeadNode || sel.focusOffset != this.lastHeadOffset
  }

  // Store the current state of the DOM selection.
  storeDOMState() {
    let sel = window.getSelection()
    this.lastAnchorNode = sel.anchorNode; this.lastAnchorOffset = sel.anchorOffset
    this.lastHeadNode = sel.focusNode; this.lastHeadOffset = sel.focusOffset
  }

  // : () → bool
  // When the DOM selection changes in a notable manner, modify the
  // current selection state to match.
  readFromDOM() {
    if (this.pm.input.composing || !hasFocus(this.pm) || !this.domChanged()) return false

    let sel = window.getSelection(), doc = this.pm.doc
    let anchor = posFromDOM(this.pm, sel.anchorNode, sel.anchorOffset)
    let head = sel.isCollapsed ? anchor : posFromDOM(this.pm, sel.focusNode, sel.focusOffset)

    let newRange = findSelectionNear(doc, head, this.range.head != null && this.range.head < head ? 1 : -1)
    if (newRange instanceof TextSelection && doc.resolve(anchor).parent.isTextblock) {
      newRange = new TextSelection(anchor, newRange.head)
    } else if (newRange instanceof NodeSelection && (anchor < newRange.from || anchor > newRange.to)) {
      // If head falls on a node, but anchor falls outside of it,
      // create a text selection between them
      let inv = anchor > newRange.to
      newRange = new TextSelection(findSelectionNear(doc, anchor, inv ? -1 : 1, true).anchor,
                                   findSelectionNear(doc, inv ? newRange.from : newRange.to, inv ? 1 : -1, true).head)
    }
    this.setAndSignal(newRange)

    if (newRange instanceof NodeSelection || newRange.head != head || newRange.anchor != anchor) {
      this.toDOM()
    } else {
      this.clearNode()
      this.storeDOMState()
    }
    return true
  }

  toDOM(takeFocus) {
    if (!hasFocus(this.pm)) {
      if (!takeFocus) return
      // See https://bugzilla.mozilla.org/show_bug.cgi?id=921444
      else if (browser.gecko) this.pm.content.focus()
    }
    if (this.range instanceof NodeSelection)
      this.nodeToDOM()
    else
      this.rangeToDOM()
  }

  // Make changes to the DOM for a node selection.
  nodeToDOM() {
    let dom = DOMAfterPos(this.pm.content, this.range.from)
    if (dom != this.lastNode) {
      this.clearNode()
      dom.classList.add("ProseMirror-selectednode")
      this.pm.content.classList.add("ProseMirror-nodeselection")
      this.lastNode = dom
    }
    let range = document.createRange(), sel = window.getSelection()
    range.selectNode(dom)
    sel.removeAllRanges()
    sel.addRange(range)
    this.storeDOMState()
  }

  // Make changes to the DOM for a text selection.
  rangeToDOM() {
    this.clearNode()

    let anchor = DOMFromPos(this.pm.content, this.range.anchor)
    let head = DOMFromPos(this.pm.content, this.range.head)

    let sel = window.getSelection(), range = document.createRange()
    if (sel.extend) {
      range.setEnd(anchor.node, anchor.offset)
      range.collapse(false)
    } else {
      if (this.range.anchor > this.range.head) { let tmp = anchor; anchor = head; head = tmp }
      range.setEnd(head.node, head.offset)
      range.setStart(anchor.node, anchor.offset)
    }
    sel.removeAllRanges()
    sel.addRange(range)
    if (sel.extend)
      sel.extend(head.node, head.offset)
    this.storeDOMState()
  }

  // Clear all DOM statefulness of the last node selection.
  clearNode() {
    if (this.lastNode) {
      this.lastNode.classList.remove("ProseMirror-selectednode")
      this.pm.content.classList.remove("ProseMirror-nodeselection")
      this.lastNode = null
      return true
    }
  }

  receivedFocus() {
    if (this.polling == null) this.startPolling()
  }
}

// ;; An editor selection. Can be one of two selection types:
// `TextSelection` and `NodeSelection`. Both have the properties
// listed here, but also contain more information (such as the
// selected [node](#NodeSelection.node) or the
// [head](#TextSelection.head) and [anchor](#TextSelection.anchor)).
export class Selection {
  // :: number #path=Selection.prototype.from
  // The left-bound of the selection.

  // :: number #path=Selection.prototype.to
  // The right-bound of the selection.

  // :: bool #path=Selection.empty
  // True if the selection is an empty text selection (head an anchor
  // are the same).

  // :: (other: Selection) → bool #path=Selection.eq
  // Test whether the selection is the same as another selection.

  // :: (doc: Node, mapping: Mappable) → Selection #path=Selection.map
  // Map this selection through a [mappable](#Mappable) thing. `doc`
  // should be the new document, to which we are mapping.
}

// ;; A text selection represents a classical editor
// selection, with a head (the moving side) and anchor (immobile
// side), both of which point into textblock nodes. It can be empty (a
// regular cursor position).
export class TextSelection extends Selection {
  // :: (number, ?number)
  // Construct a text selection. When `head` is not given, it defaults
  // to `anchor`.
  constructor(anchor, head) {
    super()
    // :: number
    // The selection's immobile side (does not move when pressing
    // shift-arrow).
    this.anchor = anchor
    // :: number
    // The selection's mobile side (the side that moves when pressing
    // shift-arrow).
    this.head = head == null ? anchor : head
  }

  get inverted() { return this.anchor > this.head }
  get from() { return Math.min(this.head, this.anchor) }
  get to() { return Math.max(this.head, this.anchor) }
  get empty() { return this.anchor == this.head }

  eq(other) {
    return other instanceof TextSelection && other.head == this.head && other.anchor == this.anchor
  }

  map(doc, mapping) {
    let head = mapping.map(this.head).pos
    if (!doc.resolve(head).parent.isTextblock)
      return findSelectionNear(doc, head)
    let anchor = mapping.map(this.anchor).pos
    return new TextSelection(doc.resolve(anchor).parent.isTextblock ? anchor : head, head)
  }
}

// ;; A node selection is a selection that points at a
// single node. All nodes marked [selectable](#NodeType.selectable)
// can be the target of a node selection. In such an object, `from`
// and `to` point directly before and after the selected node.
export class NodeSelection extends Selection {
  // :: (number, number, Node)
  // Create a node selection. Does not verify the validity of its
  // arguments. Use `ProseMirror.setNodeSelection` for an easier,
  // error-checking way to create a node selection.
  constructor(from, to, node) {
    super()
    this.from = from
    this.to = to
    // :: Node The selected node.
    this.node = node
  }

  get empty() { return false }

  eq(other) {
    return other instanceof NodeSelection && this.from == other.from
  }

  map(doc, mapping) {
    let from = mapping.map(this.from, 1).pos
    let to = mapping.map(this.to, -1).pos
    let node = doc.nodeAt(from)
    if (node && to == from + node.nodeSize && node.type.selectable)
      return new NodeSelection(from, to, node)
    return findSelectionNear(doc, from)
  }
}

export function rangeFromDOMLoose(pm) {
  if (!hasFocus(pm)) return null
  let sel = window.getSelection()
  return new TextSelection(posFromDOM(pm, sel.anchorNode, sel.anchorOffset, true),
                           posFromDOM(pm, sel.focusNode, sel.focusOffset, true))
}

export function hasFocus(pm) {
  if (document.activeElement != pm.content) return false
  let sel = window.getSelection()
  return sel.rangeCount && contains(pm.content, sel.anchorNode)
}

// Try to find a selection inside the given node. `pos` points at the
// position where the search starts. When `text` is true, only return
// text selections.
function findSelectionIn(node, pos, index, dir, text) {
  for (let i = index - (dir > 0 ? 0 : 1); dir > 0 ? i < node.childCount : i >= 0; i += dir) {
    let child = node.child(i)
    if (child.isTextblock) return new TextSelection(pos + dir)
    if (child.type.contains) {
      let inner = findSelectionIn(child, pos + dir, dir < 0 ? child.childCount : 0, dir, text)
      if (inner) return inner
    } else if (!text && child.type.selectable) {
      return new NodeSelection(pos - (dir < 0 ? child.nodeSize : 0),
                               pos + (dir > 0 ? child.nodeSize : 0), child)
    }
    pos += child.nodeSize * dir
  }
}

// FIXME we'll need some awareness of text direction when scanning for selections

// Create a selection which is moved relative to a position in a
// given direction. When a selection isn't found at the given position,
// walks up the document tree one level and one step in the
// desired direction.
export function findSelectionFrom(doc, pos, dir, text) {
  let $pos = doc.resolve(pos)
  let inner = $pos.parent.isTextblock ? new TextSelection(pos)
      : findSelectionIn($pos.parent, pos, $pos.index($pos.depth), dir, text)
  if (inner) return inner

  for (let depth = $pos.depth - 1; depth >= 0; depth--) {
    let found = dir < 0
        ? findSelectionIn($pos.node(depth), $pos.before(depth + 1), $pos.index(depth), dir, text)
        : findSelectionIn($pos.node(depth), $pos.after(depth + 1), $pos.index(depth) + 1, dir, text)
    if (found) return found
  }
}

export function findSelectionNear(doc, pos, bias = 1, text) {
  let result = findSelectionFrom(doc, pos, bias, text) || findSelectionFrom(doc, pos, -bias, text)
  if (!result) SelectionError("Searching for selection in invalid document " + doc)
  return result
}

// Find the selection closest to the start of the given node. `pos`,
// if given, should point at the start of the node's content.
export function findSelectionAtStart(node, text) {
  return findSelectionIn(node, 0, 0, 1, text)
}

// Find the selection closest to the end of the given node.
export function findSelectionAtEnd(node, text) {
  return findSelectionIn(node, node.content.size, node.childCount, -1, text)
}

// : (ProseMirror, number, number)
// Whether vertical position motion in a given direction
// from a position would leave a text block.
export function verticalMotionLeavesTextblock(pm, pos, dir) {
  let $pos = pm.doc.resolve(pos)
  let dom = DOMAfterPos(pm.content, $pos.before($pos.depth))
  let coords = coordsAtPos(pm, pos)
  for (let child = dom.firstChild; child; child = child.nextSibling) {
    if (child.nodeType != 1) continue
    let boxes = child.getClientRects()
    for (let i = 0; i < boxes.length; i++) {
      let box = boxes[i]
      if (dir < 0 ? box.bottom < coords.top : box.top > coords.bottom)
        return false
    }
  }
  return true
}
<|endoftext|>


<commit_before>JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({
  getNumColumns: function() {
    return 4;
  },
  build: function() {
    var self = this;
    this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());

    // Input that holds the base64 string
    this.input = this.theme.getFormInputField('hidden');
    this.container.appendChild(this.input);

    // Don't show uploader if this is readonly
    if(!this.schema.readOnly && !this.schema.readonly) {
      if(!window.FileReader) throw "FileReader required for upload editor";

      if(!this.jsoneditor.options.upload) throw "Upload handler required for upload editor";

      // File uploader
      this.uploader = this.theme.getFormInputField('file');

      this.uploader.addEventListener('change',function(e) {
        e.preventDefault();
        e.stopPropagation();

        if(this.files && this.files.length) {
          var fr = new FileReader();
          fr.onload = function(evt) {
            self.preview_value = evt.target.result;
            self.refreshPreview();
            fr = null;
          };
          fr.readAsDataURL(this.files[0]);
        }
      });
    }

    this.preview = this.theme.getFormInputDescription(this.schema.description);
    this.container.appendChild(this.preview);

    this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview);
    this.container.appendChild(this.control);
  },
  refreshPreview: function() {
    if(this.last_preview === this.preview_value) return;
    this.last_preview = this.preview_value;

    this.preview.innerHTML = '';

    if(!this.preview_value) return;

    var self = this;

    var mime = this.preview_value.match(/^data:([^;,]+)[;,]/);
    if(mime) mime = mime[1];
    if(!mime) mime = 'unknown';

    var file = this.uploader.files[0];

    this.preview.innerHTML = 'Type: '+mime+', Size: '+file.size+' bytes';
    if(mime.substr(0,5)==="image") {
      this.preview.innerHTML += '
';
      var img = document.createElement('img');
      img.style.maxWidth = '100%';
      img.style.maxHeight = '100px';
      img.src = this.preview_value;
      this.preview.appendChild(img);
    }

    this.preview.innerHTML += '
';
    var uploadButton = this.getButton('Upload', 'upload', 'Upload');
    this.preview.appendChild(uploadButton);
    uploadButton.addEventListener('click',function(event) {
      event.preventDefault();

      var uploadSuccess = function(url) {
        self.setValue(url);

        if(self.parent) self.parent.onChildEditorChange(self);
        else self.jsoneditor.onChange();

        if (self.progressBar) self.preview.removeChild(self.progressBar);
      };

      var uploadError = function(error) {
      };

      var uploadProgress = function(progress) {
        if (self.progressBar) self.theme.updateProgressBar(self.progressBar, progress);
      };

      var uploadProgressUnknown = function() {
        if (self.progressBar) self.theme.updateProgressBarUnknown(self.progressBar);
      };

      if (self.theme.getProgressBar) {
        self.progressBar = self.theme.getProgressBar();
        self.preview.appendChild(self.progressBar);
      }

      self.jsoneditor.options.upload(self.path, file, uploadSuccess, uploadError, uploadProgress, uploadProgressUnknown);
    });
  },
  enable: function() {
    if(this.uploader) this.uploader.disabled = false;
    this._super();
  },
  disable: function() {
    if(this.uploader) this.uploader.disabled = true;
    this._super();
  },
  setValue: function(val) {
    if(this.value !== val) {
      this.value = val;
      this.input.value = this.value;
      this.watch_listener();
      this.jsoneditor.notifyWatchers(this.path);
    }
  },
  destroy: function() {
    if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
    if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);

    this._super();
  }
});
<commit_msg>Upload callbacks grouped in callbacks object<commit_after>JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({
  getNumColumns: function() {
    return 4;
  },
  build: function() {
    var self = this;
    this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());

    // Input that holds the base64 string
    this.input = this.theme.getFormInputField('hidden');
    this.container.appendChild(this.input);

    // Don't show uploader if this is readonly
    if(!this.schema.readOnly && !this.schema.readonly) {
      if(!window.FileReader) throw "FileReader required for upload editor";

      if(!this.jsoneditor.options.upload) throw "Upload handler required for upload editor";

      // File uploader
      this.uploader = this.theme.getFormInputField('file');

      this.uploader.addEventListener('change',function(e) {
        e.preventDefault();
        e.stopPropagation();

        if(this.files && this.files.length) {
          var fr = new FileReader();
          fr.onload = function(evt) {
            self.preview_value = evt.target.result;
            self.refreshPreview();
            fr = null;
          };
          fr.readAsDataURL(this.files[0]);
        }
      });
    }

    this.preview = this.theme.getFormInputDescription(this.schema.description);
    this.container.appendChild(this.preview);

    this.control = this.theme.getFormControl(this.label, this.uploader||this.input, this.preview);
    this.container.appendChild(this.control);
  },
  refreshPreview: function() {
    if(this.last_preview === this.preview_value) return;
    this.last_preview = this.preview_value;

    this.preview.innerHTML = '';

    if(!this.preview_value) return;

    var self = this;

    var mime = this.preview_value.match(/^data:([^;,]+)[;,]/);
    if(mime) mime = mime[1];
    if(!mime) mime = 'unknown';

    var file = this.uploader.files[0];

    this.preview.innerHTML = 'Type: '+mime+', Size: '+file.size+' bytes';
    if(mime.substr(0,5)==="image") {
      this.preview.innerHTML += '
';
      var img = document.createElement('img');
      img.style.maxWidth = '100%';
      img.style.maxHeight = '100px';
      img.src = this.preview_value;
      this.preview.appendChild(img);
    }

    this.preview.innerHTML += '
';
    var uploadButton = this.getButton('Upload', 'upload', 'Upload');
    this.preview.appendChild(uploadButton);
    uploadButton.addEventListener('click',function(event) {
      event.preventDefault();

      if (self.theme.getProgressBar) {
        self.progressBar = self.theme.getProgressBar();
        self.preview.appendChild(self.progressBar);
      }

      self.jsoneditor.options.upload(self.path, file, {
        success: function(url) {
          self.setValue(url);

          if(self.parent) self.parent.onChildEditorChange(self);
          else self.jsoneditor.onChange();

          if (self.progressBar) self.preview.removeChild(self.progressBar);
        },
        failure: function(error) {

        },
        updateProgress: function(progress) {
          if (self.progressBar) {
            if (progress) self.theme.updateProgressBar(self.progressBar, progress);
            else self.theme.updateProgressBarUnknown(self.progressBar);
          }
        }
      });
    });
  },
  enable: function() {
    if(this.uploader) this.uploader.disabled = false;
    this._super();
  },
  disable: function() {
    if(this.uploader) this.uploader.disabled = true;
    this._super();
  },
  setValue: function(val) {
    if(this.value !== val) {
      this.value = val;
      this.input.value = this.value;
      this.watch_listener();
      this.jsoneditor.notifyWatchers(this.path);
    }
  },
  destroy: function() {
    if(this.preview && this.preview.parentNode) this.preview.parentNode.removeChild(this.preview);
    if(this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
    if(this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
    if(this.uploader && this.uploader.parentNode) this.uploader.parentNode.removeChild(this.uploader);

    this._super();
  }
});
<|endoftext|>