<commit_before>/*jslint white: true vars: true browser: true todo: true */
/*jshint camelcase:true, plusplus:true, forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, devel:true, maxerr:100, white:false, onevar:false */
/*global noty:true jQuery:true $:true sprintf:true Roundizzle:true */

/* jQuery Simulate Drag-n-Drop Plugin 1.0
 * http://github.com/j-ulrich/jquery-simulate-ext
 *
 * Copyright (c) 2012 Jochen Ulrich
 * Licensed under the MIT license (MIT-LICENSE.txt).
 */

;(function($, undefined) {
	"use strict";

	// Based on the findCenter function from jquery.simulate.js
	function findCenter( elem ) {
		var offset,
			jDocument = $( elem.ownerDocument );
		elem = $( elem );
		if (elem[0] === document) {
			offset = {left: 0, top: 0};
		}
		else {
			offset = elem.offset();
		}

		return {
			x: offset.left + elem.outerWidth() / 2 - jDocument.scrollLeft(),
			y: offset.top + elem.outerHeight() / 2 - jDocument.scrollTop()
		};
	}

	function dragFinished(target, options) {
		$(target).trigger({type: "simulate-drag"});
		if ($.isFunction(options.callback)) {
			options.callback.apply(target);
		}
	}

	function interpolatedEvents(target, start, drag, options) {
		var self = this;
		var interpolOptions = options.interpolation;
		var dragDistance = Math.sqrt(Math.pow(drag.x,2) + Math.pow(drag.y,2)); // sqrt(a^2 + b^2)
		var stepWidth, stepCount, stepVector;

		if (interpolOptions.stepWidth) {
			stepWidth = parseInt(interpolOptions.stepWidth);
			stepCount = Math.floor(dragDistance / stepWidth)-1;
			var stepScale = stepWidth / dragDistance;
			stepVector = {x: drag.x*stepScale, y: drag.y*stepScale };
		}
		else {
			stepCount = parseInt(interpolOptions.stepCount);
			stepWidth = dragDistance / (stepCount+1);
			stepVector = {x: drag.x/(stepCount+1), y: drag.y/(stepCount+1)};
		}

		var coords = $.extend({},start);

		function interpolationStep() {
			coords.x += stepVector.x;
			coords.y += stepVector.y;
			var effectiveCoords = {x: coords.x, y: coords.y};
			if (interpolOptions.shaky) {
				var amplitude = parseInt(interpolOptions.shaky) || 3;
				effectiveCoords.x += Math.floor(Math.random()*(2*amplitude+1)-amplitude);
				effectiveCoords.y += Math.floor(Math.random()*(2*amplitude+1)-amplitude);
			}
			self.simulateEvent( target, "mousemove", {clientX: Math.round(effectiveCoords.x), clientY: Math.round(effectiveCoords.y)});
		}

		function stepAndSleep() {
			var timeElapsed = now() - lastTime; // Work-around for Firefox "bug": setTimeout can fire before the timeout
			if (timeElapsed >= stepDelay) {
				if (i < stepCount) {
					interpolationStep();
					i += 1;
					setTimeout(stepAndSleep, stepDelay);
				}
				else {
					self.simulateEvent( target, "mousemove", {clientX: start.x+drag.x, clientY: start.y+drag.y});
					dragFinished(target, options);
				}
			}
			else {
				setTimeout(stepAndSleep, stepDelay - timeElapsed);
			}

		}

		if ( (!interpolOptions.stepDelay && !interpolOptions.duration) || (interpolOptions.stepDelay <= 0) || (interpolOptions.duration <= 0) ) {
			// Trigger as fast as possible
			for (var i=0; i < stepCount; i+=1) {
				interpolationStep();
			}
			self.simulateEvent( target, "mousemove", {clientX: start.x+drag.x, clientY: start.y+drag.y});
			dragFinished(target, options);
		}
		else {
			var stepDelay = parseInt(interpolOptions.stepDelay) || Math.round(parseInt(interpolOptions.duration) / (stepCount+1));
			var i = 0;
			var now = Date.now || function() { return new Date().getTime() },
				lastTime = now();

			setTimeout(stepAndSleep, stepDelay);
		}

	}

	$.extend( $.simulate.prototype, {

		simulateDrag: function() {
			var target = this.target,
				options = $.extend({
					dx: 0,
					dy: 0,
					dragTarget: undefined,
					interpolation: {
						stepWidth: 0,
						stepCount: 0,
						stepDelay: 0,
						duration: 0,
						shaky: 0
					},
					callback: undefined
				},	this.options),
				start = findCenter( target ),
				end = (options.dragTarget)? findCenter(options.dragTarget) : undefined,
				x = Math.round( start.x ),
				y = Math.round( start.y ),
				dx = Math.round( options.dx || ((end)? (end.x - start.x) : 0) ),
				dy = Math.round( options.dy || ((end)? (end.y - start.y) : 0) ),
				coord = { clientX: x, clientY: y };

			if ($.simulate.activeDrag && $.simulate.activeDrag.dragElement === target) {
				// We just continue to move the dragged element
				$.simulate.activeDrag.dragDistance.x += dx;
				$.simulate.activeDrag.dragDistance.y += dy;
			}
			else {
				// We start a new drag
				this.simulateEvent( target, "mousedown", coord );
				$.extend($.simulate, {
					activeDrag: {
						dragElement: target,
						dragStart: { x: x, y: y },
						dragDistance: { x: dx, y: dy }
					}
				});
				$(target).add(document).one('mouseup', function() {
					$.simulate.activeDrag = undefined;
				});
			}

			if (dx !== 0 || dy !== 0) {
				coord = { clientX: x + dx, clientY: y + dy };
				if ( options.interpolation && (options.interpolation.stepCount || options.interpolation.stepWidth) ) {
					interpolatedEvents.apply(this, [target, {x: x, y: y}, {x: dx, y: dy}, options]);
				}
				else {
					this.simulateEvent( target, "mousemove", coord );
					dragFinished(target, options);
				}
			}
			else {
				dragFinished(target, options);
			}
		},

		/**
		 * Simulates a drop.
		 *
		 * The position where the drop occurs is determined in the following way:
		 * 1.) If there is an active drag with a distance dx != 0 and dy != 0, the drop occurs
		 * at the end position of that drag.
		 * 2.) If there is no active drag or the distance of the active drag is 0 (i.e. dx == 0 and
		 * dy == 0), then the drop occurs at the center of the target given to the drop. In this case,
		 * the mouse is moved onto the center of the target before the drop is simulated.
		 * In both cases, an active drag will be ended.
		 */
		simulateDrop: function() {
			var target = this.target,
				activeDrag = $.simulate.activeDrag,
				options = $.extend({
					callback: undefined
				}, this.options),
				center = findCenter( target ),
				x = Math.round( center.x ),
				y = Math.round( center.y ),
				coord = { clientX: x, clientY: y };

			if (activeDrag && (activeDrag.dragDistance.x !== 0 || activeDrag.dragDistance.y !== 0)) {
				// We already moved the mouse during the drag so we just simulate the drop on the end position
				x = activeDrag.dragStart.x + activeDrag.dragDistance.x;
				y = activeDrag.dragStart.y + activeDrag.dragDistance.y;
				coord = { clientX: x, clientY: y };
			}
			else {
				// Else we assume the drop should happen on target, so we move there
				this.simulateEvent( target, "mousemove", coord );
			}

			this.simulateEvent( target, "mouseup", coord );
			this.simulateEvent( target, "click", coord );
			$.simulate.activeDrag = undefined;
			$(target).trigger({type: "simulate-drop"});
			if ($.isFunction(options.callback)) {
				options.callback.apply(target);
			}
		},

		simulateDragNDrop: function() {
			var target = this.target,
				options = $.extend({
					dragTarget: undefined,
					dropTarget: undefined
				}, this.options),
				dropTarget = options.dropTarget || target;
/*
				dx = (options.dropTarget)? 0 : (options.dx || 0),
				dy = (options.dropTarget)? 0 : (options.dy || 0),
				dragDistance = { dx: dx, dy: dy };

			$.extend(options, dragDistance);
*/
			$(target).simulate( "drag", $.extend({},options,{
				dragTarget: options.dragTarget || options.dropTarget,
				callback: function() {
					$(dropTarget).simulate( "drop", options );
				}
			}));

		}
	});

}(jQuery));<commit_msg>Improves sanitization of shaky option.<commit_after>/*jslint white: true vars: true browser: true todo: true */
/*jshint camelcase:true, plusplus:true, forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, devel:true, maxerr:100, white:false, onevar:false */
/*global noty:true jQuery:true $:true sprintf:true Roundizzle:true */

/* jQuery Simulate Drag-n-Drop Plugin 1.0
 * http://github.com/j-ulrich/jquery-simulate-ext
 *
 * Copyright (c) 2012 Jochen Ulrich
 * Licensed under the MIT license (MIT-LICENSE.txt).
 */

;(function($, undefined) {
	"use strict";

	// Based on the findCenter function from jquery.simulate.js
	function findCenter( elem ) {
		var offset,
			jDocument = $( elem.ownerDocument );
		elem = $( elem );
		if (elem[0] === document) {
			offset = {left: 0, top: 0};
		}
		else {
			offset = elem.offset();
		}

		return {
			x: offset.left + elem.outerWidth() / 2 - jDocument.scrollLeft(),
			y: offset.top + elem.outerHeight() / 2 - jDocument.scrollTop()
		};
	}

	function dragFinished(target, options) {
		$(target).trigger({type: "simulate-drag"});
		if ($.isFunction(options.callback)) {
			options.callback.apply(target);
		}
	}

	function interpolatedEvents(target, start, drag, options) {
		var self = this;
		var interpolOptions = options.interpolation;
		var dragDistance = Math.sqrt(Math.pow(drag.x,2) + Math.pow(drag.y,2)); // sqrt(a^2 + b^2)
		var stepWidth, stepCount, stepVector;

		if (interpolOptions.stepWidth) {
			stepWidth = parseInt(interpolOptions.stepWidth);
			stepCount = Math.floor(dragDistance / stepWidth)-1;
			var stepScale = stepWidth / dragDistance;
			stepVector = {x: drag.x*stepScale, y: drag.y*stepScale };
		}
		else {
			stepCount = parseInt(interpolOptions.stepCount);
			stepWidth = dragDistance / (stepCount+1);
			stepVector = {x: drag.x/(stepCount+1), y: drag.y/(stepCount+1)};
		}

		var coords = $.extend({},start);

		function interpolationStep() {
			coords.x += stepVector.x;
			coords.y += stepVector.y;
			var effectiveCoords = {x: coords.x, y: coords.y};
			if (interpolOptions.shaky && (interpolOptions.shaky === true || !isNaN(parseInt(interpolOptions.shaky)) )) {
				var amplitude = (interpolOptions.shaky === true)? 3 : parseInt(interpolOptions.shaky);
				effectiveCoords.x += Math.floor(Math.random()*(2*amplitude+1)-amplitude);
				effectiveCoords.y += Math.floor(Math.random()*(2*amplitude+1)-amplitude);
			}
			self.simulateEvent( target, "mousemove", {clientX: Math.round(effectiveCoords.x), clientY: Math.round(effectiveCoords.y)});
		}

		function stepAndSleep() {
			var timeElapsed = now() - lastTime; // Work-around for Firefox "bug": setTimeout can fire before the timeout
			if (timeElapsed >= stepDelay) {
				if (i < stepCount) {
					interpolationStep();
					i += 1;
					setTimeout(stepAndSleep, stepDelay);
				}
				else {
					self.simulateEvent( target, "mousemove", {clientX: start.x+drag.x, clientY: start.y+drag.y});
					dragFinished(target, options);
				}
			}
			else {
				setTimeout(stepAndSleep, stepDelay - timeElapsed);
			}

		}

		if ( (!interpolOptions.stepDelay && !interpolOptions.duration) || (interpolOptions.stepDelay <= 0) || (interpolOptions.duration <= 0) ) {
			// Trigger as fast as possible
			for (var i=0; i < stepCount; i+=1) {
				interpolationStep();
			}
			self.simulateEvent( target, "mousemove", {clientX: start.x+drag.x, clientY: start.y+drag.y});
			dragFinished(target, options);
		}
		else {
			var stepDelay = parseInt(interpolOptions.stepDelay) || Math.round(parseInt(interpolOptions.duration) / (stepCount+1));
			var i = 0;
			var now = Date.now || function() { return new Date().getTime() },
				lastTime = now();

			setTimeout(stepAndSleep, stepDelay);
		}

	}

	$.extend( $.simulate.prototype, {

		simulateDrag: function() {
			var target = this.target,
				options = $.extend({
					dx: 0,
					dy: 0,
					dragTarget: undefined,
					interpolation: {
						stepWidth: 0,
						stepCount: 0,
						stepDelay: 0,
						duration: 0,
						shaky: 0
					},
					callback: undefined
				},	this.options),
				start = findCenter( target ),
				end = (options.dragTarget)? findCenter(options.dragTarget) : undefined,
				x = Math.round( start.x ),
				y = Math.round( start.y ),
				dx = Math.round( options.dx || ((end)? (end.x - start.x) : 0) ),
				dy = Math.round( options.dy || ((end)? (end.y - start.y) : 0) ),
				coord = { clientX: x, clientY: y };

			if ($.simulate.activeDrag && $.simulate.activeDrag.dragElement === target) {
				// We just continue to move the dragged element
				$.simulate.activeDrag.dragDistance.x += dx;
				$.simulate.activeDrag.dragDistance.y += dy;
			}
			else {
				// We start a new drag
				this.simulateEvent( target, "mousedown", coord );
				$.extend($.simulate, {
					activeDrag: {
						dragElement: target,
						dragStart: { x: x, y: y },
						dragDistance: { x: dx, y: dy }
					}
				});
				$(target).add(document).one('mouseup', function() {
					$.simulate.activeDrag = undefined;
				});
			}

			if (dx !== 0 || dy !== 0) {
				coord = { clientX: x + dx, clientY: y + dy };
				if ( options.interpolation && (options.interpolation.stepCount || options.interpolation.stepWidth) ) {
					interpolatedEvents.apply(this, [target, {x: x, y: y}, {x: dx, y: dy}, options]);
				}
				else {
					this.simulateEvent( target, "mousemove", coord );
					dragFinished(target, options);
				}
			}
			else {
				dragFinished(target, options);
			}
		},

		/**
		 * Simulates a drop.
		 *
		 * The position where the drop occurs is determined in the following way:
		 * 1.) If there is an active drag with a distance dx != 0 and dy != 0, the drop occurs
		 * at the end position of that drag.
		 * 2.) If there is no active drag or the distance of the active drag is 0 (i.e. dx == 0 and
		 * dy == 0), then the drop occurs at the center of the target given to the drop. In this case,
		 * the mouse is moved onto the center of the target before the drop is simulated.
		 * In both cases, an active drag will be ended.
		 */
		simulateDrop: function() {
			var target = this.target,
				activeDrag = $.simulate.activeDrag,
				options = $.extend({
					callback: undefined
				}, this.options),
				center = findCenter( target ),
				x = Math.round( center.x ),
				y = Math.round( center.y ),
				coord = { clientX: x, clientY: y };

			if (activeDrag && (activeDrag.dragDistance.x !== 0 || activeDrag.dragDistance.y !== 0)) {
				// We already moved the mouse during the drag so we just simulate the drop on the end position
				x = activeDrag.dragStart.x + activeDrag.dragDistance.x;
				y = activeDrag.dragStart.y + activeDrag.dragDistance.y;
				coord = { clientX: x, clientY: y };
			}
			else {
				// Else we assume the drop should happen on target, so we move there
				this.simulateEvent( target, "mousemove", coord );
			}

			this.simulateEvent( target, "mouseup", coord );
			this.simulateEvent( target, "click", coord );
			$.simulate.activeDrag = undefined;
			$(target).trigger({type: "simulate-drop"});
			if ($.isFunction(options.callback)) {
				options.callback.apply(target);
			}
		},

		simulateDragNDrop: function() {
			var target = this.target,
				options = $.extend({
					dragTarget: undefined,
					dropTarget: undefined
				}, this.options),
				dropTarget = options.dropTarget || target;
/*
				dx = (options.dropTarget)? 0 : (options.dx || 0),
				dy = (options.dropTarget)? 0 : (options.dy || 0),
				dragDistance = { dx: dx, dy: dy };

			$.extend(options, dragDistance);
*/
			$(target).simulate( "drag", $.extend({},options,{
				dragTarget: options.dragTarget || options.dropTarget,
				callback: function() {
					$(dropTarget).simulate( "drop", options );
				}
			}));

		}
	});

}(jQuery));<|endoftext|>


<commit_before>/**
 * jquery.textareaHighlighter.js 0.3.1
 * jQuery plugin for highlighting text in textarea.
 *
 * alexandre.kirillov@gmail.com
 * MIT license. http://opensource.org/licenses/MIT
 */
;(function ( $, window, document, undefined ) {
    "use strict";

    /**
     *
     * PLUGIN CORE
     *
     */
    var pluginName = "textareaHighlighter",
        defaults = {
            matches: [
                // {'matchClass': '', 'match': []}
            ],
            maxlength: -1,
            maxlengthWarning: '',
            maxlengthElement: null,
            isCustomeCss: false,
            debug: false,
            typingDelay: 30
        };

    // constructor
    function Plugin ( element, options ) {
        this.isInited  = false;
        this.element   = element;
        this.$element  = $(this.element);
        this.settings  = $.extend( {}, defaults, this.$element.data(), options );
        this._defaults = defaults;
        this._name     = pluginName;

        this.style = {};
        this.$wrapDiv       = $(document.createElement('div')).addClass('textarea-highlighter-wrap');
        this.$backgroundDiv = $(document.createElement('div'));

        this.init();
    }

    Plugin.prototype = {
        init: function() {
            var _this          = this,
                $this          = this.$element,
                style          = this.style,
                settings       = this.settings,
                $wrapDiv       = this.$wrapDiv,
                $backgroundDiv = this.$backgroundDiv;

            _this.updateStyle();

            $this
                .data('highlighterTimerId', -1)
                // Bind events
                .on('scroll.textarea.highlighter', function(){
                    $backgroundDiv.scrollTop( $this.scrollTop() );
                })
                // ORIGINAL EVENTS
                .on('textarea.highlighter.destroy', function(){
                    _this.destroy();
                })
                .on('textarea.highlighter.updateStyle', function(){
                    _this.updateStyle();
                })
                .on('textarea.highlighter.change', function(){
                    _this.change({});
                })
                // debug mode toggle
                .on('textarea.highlighter.debug.on', function(){
                    _this.debugModeOn();
                })
                .on('textarea.highlighter.debug.off', function(){
                    _this.debugModeOff();
                });

            if (browser.msie) {
                // IE
                $this.on('input.textarea.highlighter keyup.textarea.highlighter', function(e){
                    _this.change(e);
                });
            }
            else {
                // Modern browsers
                $this.on('input.textarea.highlighter', function(e){
                    _this.change(e);
                });
            }

            // insert backgroundDiv
            $this.wrap( $wrapDiv ).before( $backgroundDiv );
            // do initial check for input
            _this.change({});
        },
        updateStyle: function(){
            var _this    = this,
                $this    = this.$element,
                settings = this.settings,
                style    = this.style;

            // textarea style
            style = {
                // background   : this.$element.css('background'),
                paddingTop   : parseInt( $this.css('padding-top'), 10 ),
                paddingRight : parseInt( $this.css('padding-right'), 10 ),
                paddingBottom: parseInt( $this.css('padding-bottom'), 10 ),
                paddingLeft  : parseInt( $this.css('padding-left'), 10 ),
            };

            // Hack for firefox, some how width needs to be 2px smallet then the textarea
            // and padding-left needs to be added 1px
            if( browser.firefox ){
                style.paddingRight += 1;
                style.paddingLeft += 1;
            }
            if( browser.iphone ){
                style.paddingRight += 3;
                style.paddingLeft += 3;
            }

            this.$wrapDiv.css({
                'position': 'relative',
                'margin'  : 0
            });
            this.$backgroundDiv.addClass('background-div').addClass( $this.attr('class') ).css({
                'height'        : '100%',
                'color'         : ( settings.debug ) ? '#f00' : 'transparent',
                // 'background'    : ( settings.debug ) ? '#fee' : style.background,
                // 'padding-top'   : style.paddingTop,
                // 'padding-right' : style.paddingRight,
                // 'padding-bottom': style.paddingBottom,
                // 'padding-left'  : style.paddingLeft,
                'position'      : 'absolute',
                'margin'        : '0'
            });
            if (! settings.isCustomeCss) {
                this.$backgroundDiv.css({
                    'padding-top'   : style.paddingTop,
                    'padding-right' : style.paddingRight,
                    'padding-bottom': style.paddingBottom,
                    'padding-left'  : style.paddingLeft,
                    'box-sizing'    : 'border-box',
                    'border-color'  : 'transparent'
                });
            }

            $this.css({
                'color'     : ( settings.debug ) ? 'rgba(0,0,0,0.5)' : 'inherit',
                'position'  : 'relative',
                'background': 'transparent'
            });
        },
        change: function(e){
            var _this = this,
                settings = _this.settings;
            // if arrow keys, don't do anything
            if (/(37|38|39|40)/.test(e.keyCode)) { return true; }

            // check for last update, this is for performace
            if (_this.$element.data('highlighterTimerId') !== -1) {
                clearTimeout( _this.$element.data('highlighterTimerId') );
                _this.$element.data('highlighterTimerId', -1);
            }

            // id for set timeout
            var changeId = setTimeout(function(){
                var tmpMaxLengthObj = _this.checkMaxLength();
                var tmpMatchedObj = _this.checkMatchWords( tmpMaxLengthObj.notOverMaxText );

                // update background div content
                _this.$backgroundDiv.html( tmpMatchedObj.txt + tmpMaxLengthObj.overMaxText );
                // trigger update event
                _this.$element.trigger('textarea.highlighter.update', {'textList': tmpMatchedObj.matchedList});

                // if not initialize execution the trigger an initialization complete event
                if (!_this.isInited) {
                    _this.isInited = true;
                    _this.$element.trigger('textarea.highlighter.init.complete');
                }
            }, settings.typingDelay);
            // set setTimeout id
            _this.$element.data('highlighterTimerId', changeId);
        },
        // wrap matched text with an HTML element
        getWrapedText: function( text, matchedText, matchClass ){
            return text.replace( new RegExp( helper.escapeRegExp( matchedText ), 'g'), this.getTextInSpan( matchClass, matchedText ) );
        },
        getTextInSpan: function( className, text ){
            return ''+ text +'';
        },

        checkMaxLength: function(){
            var _this = this,
                textareaText = _this.$element.val(),
                settings = _this.settings,
                maxSize = 0,
                overMaxText = '',
                notOverMaxText = '';

            if (0 < settings.maxlength) {
                // check for max length
                if ( settings.maxlength < textareaText.length) {
                    // get text that was over max length
                    var matchText = textareaText.slice( settings.maxlength, settings.maxlength + textareaText.length - 1 );
                    // wrap matched text with an HTML element for highlighting
                    overMaxText = _this.getTextInSpan( settings.maxlengthWarning, matchText );
                }
                // update text max length
                if (settings.maxlengthElement !== null) {
                    maxSize = settings.maxlength - textareaText.length;

                    if (maxSize < 0) {
                        // add max length warning class
                        if (! settings.maxlengthElement.hasClass( settings.maxlengthWarning )) {
                            settings.maxlengthElement.addClass( settings.maxlengthWarning );
                        }
                    }
                    else {
                        // remove max length warning class
                        if (settings.maxlengthElement.hasClass( settings.maxlengthWarning )) {
                            settings.maxlengthElement.removeClass( settings.maxlengthWarning );
                        }
                    }
                    // update max length
                    settings.maxlengthElement.text( maxSize );
                }
                // set text that wasn't over max length
                notOverMaxText = textareaText.slice( 0, settings.maxlength );
            }
            else {
                // max length wasn't set so use input text without any extra settings
                notOverMaxText = textareaText;
            }

            return {
                'overMaxText': overMaxText,
                'notOverMaxText': notOverMaxText
            };
        },

        checkMatchWords: function( notOverMaxText ){
            var settings = this.settings;
            var i = 0, imax = 0, j = 0, jmax = 0,
                matchesList = [], matchTextList = [],
                matchText = '', matchClass = '';

            // check for matching words
            for (i = 0, imax = settings.matches.length; i < imax; i++) {

                // check if match match is a RegExp
                if (settings.matches[i].match instanceof RegExp) {
                    // set matched words array
                    matchesList = helper.getUniqueArray( notOverMaxText.match( settings.matches[i].match ) || [] );
                }
                else {
                    // copy words array
                    matchesList = settings.matches[i].match;
                }

                for (j = 0, jmax = matchesList.length; j < jmax; j++) {
                    // get word to match
                    matchText = matchesList[j];
                    matchClass = '';

                    // check if word exists in input text
                    if (notOverMaxText.indexOf( matchText ) !== -1){
                        matchTextList.push( matchText );

                        // check for match class name
                        if (settings.matches[i].hasOwnProperty('matchClass')) {
                            matchClass = settings.matches[i].matchClass;
                        }
                        // update replaced text
                        notOverMaxText = this.getWrapedText( notOverMaxText, matchText, matchClass );
                    }
                }
            }

            return {
                'txt': notOverMaxText,
                'matchedList': matchTextList
            };
        },

        // Destroy plugin in settings & extra elements that were added
        destroy: function(){
            $.data( this.element, "plugin_" + pluginName, false );
            this.$backgroundDiv.remove();
            this.$element
                .off('scroll.textarea.highlighter')
                .off('input.textarea.highlighter keyup.textarea.highlighter')
                .off('textarea.highlighter.destroy')
                .off('textarea.highlighter.update')
                .off('textarea.highlighter.change')
                .css({
                    'color'     : '',
                    'position'  : '',
                    'background': ''
                })
                .unwrap();
        },
        // Turn on debug mode
        debugModeOn: function(){
            this.settings.debug = true;
            this.$backgroundDiv.css({ 'color': '#f00' });
            this.$element.css({ 'color': 'rgba(0,0,0,0.5)' });
        },
        // Turn off debug mode
        debugModeOff: function(){
            this.settings.debug = false;
            this.$backgroundDiv.css({ 'color': 'transparent' });
            this.$element.css({ 'color': 'inherit' });
        }
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[ pluginName ] = function ( options ) {
        return this.each(function(i) {
            if ( ! $.data( this, "plugin_" + pluginName ) ) {
                $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
            }
        });
    };

    /**
     *
     * HELPER FUNCTIONS
     *
     */
    var helper = {
        escapeRegExp: function(str){
            return str.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
        },
        getUniqueArray: function( array ){
            return array.filter(function(elem, pos, self) {
                if ( elem === '' ) {
                    return false;
                }
                return self.indexOf(elem) === pos;
            });
        }
    };
    // get curretn bworser type
    var browser = (function(){
        var userAgent = navigator.userAgent,
            msie    = /(msie|trident)/i.test( userAgent ),
            chrome  = /chrome/i.test( userAgent ),
            firefox = /firefox/i.test( userAgent ),
            safari  = /safari/i.test( userAgent ) && !chrome,
            iphone  = /iphone/i.test( userAgent );

        if( msie ){ return { msie: true }; }
        if( chrome ){ return { chrome: true }; }
        if( firefox ){ return { firefox: true }; }
        if( iphone ){ return { iphone: true }; }
        if( safari ){ return { safari: true }; }

        return {
            msie   : false,
            chrome : false,
            firefox: false,
            safari : false,
            iphone : false
        };
    }());

})( jQuery, window, document );<commit_msg>fix: escape text so that text with HTML tags get correctly highlighted & some refactoring<commit_after>/**
 * jquery.textareaHighlighter.js 0.3.1
 * jQuery plugin for highlighting text in textarea.
 *
 * alexandre.kirillov@gmail.com
 * MIT license. http://opensource.org/licenses/MIT
 */
;(function ( $, window, document, undefined ) {
    "use strict";

    /**
     *
     * PLUGIN CORE
     *
     */
    var pluginName = "textareaHighlighter",
        defaults = {
            matches: [
                // {'matchClass': '', 'match': []}
            ],
            maxlength: -1,
            maxlengthWarning: '',
            maxlengthElement: null,
            isCustomeCss: false,
            debug: false,
            typingDelay: 30
        };

    // constructor
    function Plugin ( element, options ) {
        this.isInited  = false;
        this.element   = element;
        this.$element  = $(this.element);
        this.settings  = $.extend( {}, defaults, this.$element.data(), options );
        this._defaults = defaults;
        this._name     = pluginName;

        this.style = {};
        this.$wrapDiv       = $(document.createElement('div')).addClass('textarea-highlighter-wrap');
        this.$backgroundDiv = $(document.createElement('div'));

        this.init();
    }

    Plugin.prototype = {
        init: function() {
            var _this          = this,
                $this          = this.$element,
                style          = this.style,
                settings       = this.settings,
                $wrapDiv       = this.$wrapDiv,
                $backgroundDiv = this.$backgroundDiv;

            _this.updateStyle();

            $this
                .data('highlighterTimerId', -1)
                // Bind events
                .on('scroll.textarea.highlighter', function(){
                    $backgroundDiv.scrollTop( $this.scrollTop() );
                })
                // ORIGINAL EVENTS
                .on('textarea.highlighter.destroy', function(){
                    _this.destroy();
                })
                .on('textarea.highlighter.updateStyle', function(){
                    _this.updateStyle();
                })
                .on('textarea.highlighter.change', function(){
                    _this.change({});
                })
                // debug mode toggle
                .on('textarea.highlighter.debug.on', function(){
                    _this.debugModeOn();
                })
                .on('textarea.highlighter.debug.off', function(){
                    _this.debugModeOff();
                });

            if (browser.msie) {
                // IE
                $this.on('input.textarea.highlighter keyup.textarea.highlighter', function(e){
                    _this.change(e);
                });
            }
            else {
                // Modern browsers
                $this.on('input.textarea.highlighter', function(e){
                    _this.change(e);
                });
            }

            // insert backgroundDiv
            $this.wrap( $wrapDiv ).before( $backgroundDiv );
            // do initial check for input
            _this.change({});
        },
        updateStyle: function(){
            var _this    = this,
                $this    = this.$element,
                settings = this.settings,
                style    = this.style;

            // textarea style
            style = {
                // background   : this.$element.css('background'),
                paddingTop   : parseInt( $this.css('padding-top'), 10 ),
                paddingRight : parseInt( $this.css('padding-right'), 10 ),
                paddingBottom: parseInt( $this.css('padding-bottom'), 10 ),
                paddingLeft  : parseInt( $this.css('padding-left'), 10 ),
            };

            // Hack for firefox, some how width needs to be 2px smallet then the textarea
            // and padding-left needs to be added 1px
            if( browser.firefox ){
                style.paddingRight += 1;
                style.paddingLeft += 1;
            }
            if( browser.iphone ){
                style.paddingRight += 3;
                style.paddingLeft += 3;
            }

            this.$wrapDiv.css({
                'position': 'relative',
                'margin'  : 0
            });
            this.$backgroundDiv.addClass('background-div').addClass( $this.attr('class') ).css({
                'height'        : '100%',
                'color'         : ( settings.debug ) ? '#f00' : 'transparent',
                // 'background'    : ( settings.debug ) ? '#fee' : style.background,
                // 'padding-top'   : style.paddingTop,
                // 'padding-right' : style.paddingRight,
                // 'padding-bottom': style.paddingBottom,
                // 'padding-left'  : style.paddingLeft,
                'position'      : 'absolute',
                'margin'        : '0'
            });
            if (! settings.isCustomeCss) {
                this.$backgroundDiv.css({
                    'padding-top'   : style.paddingTop,
                    'padding-right' : style.paddingRight,
                    'padding-bottom': style.paddingBottom,
                    'padding-left'  : style.paddingLeft,
                    'box-sizing'    : 'border-box',
                    'border-color'  : 'transparent'
                });
            }

            $this.css({
                'color'     : ( settings.debug ) ? 'rgba(0,0,0,0.5)' : 'inherit',
                'position'  : 'relative',
                'background': 'transparent'
            });
        },
        change: function(e){
            var _this = this,
                settings = _this.settings;
            // if arrow keys, don't do anything
            if (/(37|38|39|40)/.test(e.keyCode)) { return true; }

            // check for last update, this is for performace
            if (_this.$element.data('highlighterTimerId') !== -1) {
                clearTimeout( _this.$element.data('highlighterTimerId') );
                _this.$element.data('highlighterTimerId', -1);
            }

            // id for set timeout
            var changeId = setTimeout(function(){
                var tmpMaxLengthObj = _this.checkMaxLength();
                var tmpMatchedObj = _this.checkMatchWords( tmpMaxLengthObj.notOverMaxText );

                // update background div content
                _this.$backgroundDiv.html( tmpMatchedObj.txt + tmpMaxLengthObj.overMaxText );
                // trigger update event
                _this.$element.trigger('textarea.highlighter.update', {'textList': tmpMatchedObj.matchedList});

                // if not initialize execution the trigger an initialization complete event
                if (!_this.isInited) {
                    _this.isInited = true;
                    _this.$element.trigger('textarea.highlighter.init.complete');
                }
            }, settings.typingDelay);
            // set setTimeout id
            _this.$element.data('highlighterTimerId', changeId);
        },
        // wrap matched text with an HTML element
        getWrapedText: function( text, matchedText, matchClass ){
            return text.replace( new RegExp( helper.escapeRegExp( matchedText ), 'g'), this.getTextInSpan( matchClass, matchedText ) );
        },
        getTextInSpan: function( className, text ){
            return ''+ text +'';
        },

        checkMaxLength: function(){
            var _this = this,
                textareaText = _this.$element.val(),
                settings = _this.settings,
                maxSize = 0,
                overMaxText = '',
                notOverMaxText = '';

            if (0 < settings.maxlength) {
                // check for max length
                if ( settings.maxlength < textareaText.length) {
                    // get text that was over max length
                    var matchText = textareaText.slice( settings.maxlength, settings.maxlength + textareaText.length - 1 );
                    // wrap matched text with an HTML element for highlighting
                    overMaxText = _this.getTextInSpan( settings.maxlengthWarning, matchText );
                }
                // update text max length
                if (settings.maxlengthElement !== null) {
                    maxSize = settings.maxlength - textareaText.length;

                    if (maxSize < 0) {
                        // add max length warning class
                        if (! settings.maxlengthElement.hasClass( settings.maxlengthWarning )) {
                            settings.maxlengthElement.addClass( settings.maxlengthWarning );
                        }
                    }
                    else {
                        // remove max length warning class
                        if (settings.maxlengthElement.hasClass( settings.maxlengthWarning )) {
                            settings.maxlengthElement.removeClass( settings.maxlengthWarning );
                        }
                    }
                    // update max length
                    settings.maxlengthElement.text( maxSize );
                }
                // set text that wasn't over max length
                notOverMaxText = textareaText.slice( 0, settings.maxlength );
            }
            else {
                // max length wasn't set so use input text without any extra settings
                notOverMaxText = textareaText;
            }

            return {
                'overMaxText': helper.escapeHTML( overMaxText ),
                'notOverMaxText': helper.escapeHTML( notOverMaxText )
            };
        },

        checkMatchWords: function( escapedTargetText ){
            var settings = this.settings;
            var i = 0, imax = 0, j = 0, jmax = 0,
                matchesList = [], matchTextList = [],
                matchText = '', matchTextEscape = '', matchClass = '';

            // check for matching words
            for (i = 0, imax = settings.matches.length; i < imax; i++) {

                // check if match match is a RegExp
                if (settings.matches[i].match instanceof RegExp) {
                    // set matched words array
                    matchesList = helper.getUniqueArray( escapedTargetText.match( settings.matches[i].match ) || [] );
                }
                else {
                    // copy words array
                    matchesList = settings.matches[i].match;
                }

                for (j = 0, jmax = matchesList.length; j < jmax; j++) {
                    // get word to match
                    matchText = matchesList[j];
                    matchTextEscape = helper.escapeHTML( matchText );
                    matchClass = '';

                    // check if word exists in input text
                    if (escapedTargetText.indexOf( matchTextEscape ) !== -1){
                        matchTextList.push( matchText );

                        // check for match class name
                        if (settings.matches[i].hasOwnProperty('matchClass')) {
                            matchClass = settings.matches[i].matchClass;
                        }
                        // update replaced text
                        escapedTargetText = this.getWrapedText( escapedTargetText, matchTextEscape, matchClass );
                    }
                }
            }

            return {
                'txt': notOverMaxText,
                'matchedList': matchTextList
            };
        },

        // Destroy plugin in settings & extra elements that were added
        destroy: function(){
            $.data( this.element, "plugin_" + pluginName, false );
            this.$backgroundDiv.remove();
            this.$element
                .off('scroll.textarea.highlighter')
                .off('input.textarea.highlighter keyup.textarea.highlighter')
                .off('textarea.highlighter.destroy')
                .off('textarea.highlighter.update')
                .off('textarea.highlighter.change')
                .css({
                    'color'     : '',
                    'position'  : '',
                    'background': ''
                })
                .unwrap();
        },
        // Turn on debug mode
        debugModeOn: function(){
            this.settings.debug = true;
            this.$backgroundDiv.css({ 'color': '#f00' });
            this.$element.css({ 'color': 'rgba(0,0,0,0.5)' });
        },
        // Turn off debug mode
        debugModeOff: function(){
            this.settings.debug = false;
            this.$backgroundDiv.css({ 'color': 'transparent' });
            this.$element.css({ 'color': 'inherit' });
        }
    };

    // A really lightweight plugin wrapper around the constructor,
    // preventing against multiple instantiations
    $.fn[ pluginName ] = function ( options ) {
        return this.each(function(i) {
            if ( ! $.data( this, "plugin_" + pluginName ) ) {
                $.data( this, "plugin_" + pluginName, new Plugin( this, options ) );
            }
        });
    };

    /**
     *
     * HELPER FUNCTIONS
     *
     */
    var helper = {
        escapeHTML: function(str){
            return $( document.createElement('div') ).text(str).html();
        },
        escapeRegExp: function(str){
            return str.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
        },
        getUniqueArray: function( array ){
            return array.filter(function(elem, pos, self) {
                if ( elem === '' ) {
                    return false;
                }
                return self.indexOf(elem) === pos;
            });
        }
    };
    // get curretn bworser type
    var browser = (function(){
        var userAgent = navigator.userAgent,
            msie    = /(msie|trident)/i.test( userAgent ),
            chrome  = /chrome/i.test( userAgent ),
            firefox = /firefox/i.test( userAgent ),
            safari  = /safari/i.test( userAgent ) && !chrome,
            iphone  = /iphone/i.test( userAgent );

        if( msie ){ return { msie: true }; }
        if( chrome ){ return { chrome: true }; }
        if( firefox ){ return { firefox: true }; }
        if( iphone ){ return { iphone: true }; }
        if( safari ){ return { safari: true }; }

        return {
            msie   : false,
            chrome : false,
            firefox: false,
            safari : false,
            iphone : false
        };
    }());

})( jQuery, window, document );<|endoftext|>


<commit_before>define(['jquery', 'underscore', 'backbone', 'model_search_item'], function($, _, Backbone, Models) {
	Models.SearchResults = Backbone.Collection.extend({
		model: Models.SearchItem,
		parse: function(response, options) {
			return response.result.results;
		}
	});
	return Models;
});<commit_msg>Fetch on init.<commit_after>define(['jquery', 'underscore', 'backbone', 'model_search_item'], function($, _, Backbone, Models) {
	Models.SearchResults = Backbone.Collection.extend({
		model: Models.SearchItem,
		initialize: function() {
			this.fetch();
		}
		parse: function(response, options) {
			return response.result.results;
		}
	});
	return Models;
});<|endoftext|>


<commit_before>/* exported SlidingPanel */

// Copyright 2015 Endless Mobile, Inc.

const EosKnowledgePrivate = imports.gi.EosKnowledgePrivate;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

const Knowledge = imports.app.knowledge;

const PanelFrame = new Knowledge.Class({
    Name: 'PanelFrame',
    Extends: Gtk.Bin,

    _init: function (panel, params={}) {
        this.parent(params);
        this.panel = panel;
    },

    vfunc_size_allocate: function (allocation) {
        this.set_allocation(allocation);
        if (!this.get_child())
            return;

        let shadow_margin = EosKnowledgePrivate.style_context_get_custom_int(this.panel.get_style_context(),
                                                                             'shadow-margin');

        // Remove the extra space given by the SlidingPanelOverlay
        allocation.width -= 2 * shadow_margin;
        allocation.height -= 2 * shadow_margin;
        allocation.x += shadow_margin;
        allocation.y += shadow_margin;
        this.get_child().size_allocate(allocation);
    },
});

/**
 * Class: SlidingPanel
 * A widget which can slide its contents visible or out of sight.
 */
const SlidingPanel = new Knowledge.Class({
    Name: 'SlidingPanel',
    Extends: Gtk.Stack,

    Properties: {
        /**
         * Property: hide-direction
         * Direction to slide out of sight when hiding.
         *
         * We could make this mutable, but right now there's no need so keeping
         * construct only to keep the code simpler
         */
        'hide-direction': GObject.ParamSpec.enum('hide-direction',
            'Hide Direction', 'Hide Direction',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            Gtk.PositionType, Gtk.PositionType.TOP),
        /**
         * Property: panel-widget
         * The widget that slides in and out.
         */
        'panel-widget': GObject.ParamSpec.object('panel-widget',
            'Panel widget', 'Panel widget',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
            Gtk.Widget),
        /**
         * Property: reveal-panel
         * Whether the container should reveal the child.
         */
        'reveal-panel': GObject.ParamSpec.boolean('reveal-panel',
            'Reveal Panel', 'Reveal Panel',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
            false),
        /**
         * Property: panel-revealed
         * True if the panel is fully revealed or animating to the hidden state.
         */
        'panel-revealed': GObject.ParamSpec.boolean('panel-revealed',
            'Panel Revealed', 'Panel Revealed',
            GObject.ParamFlags.READABLE, false),
        /**
         * Property: hide-when-invisible
         *
         * True if the panel should hide entirely when not animating or showing
         * the widget.
         */
        'hide-when-invisible': GObject.ParamSpec.boolean('hide-when-invisible',
            'Hide When Invisible', 'Hide When Invisible',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            false),
    },
    StyleProperties: {
        // These custom style properties are specifically for when using the
        // slidingPanel in a slidingPanelOverlay. Fill percentage will control
        // how much of the side the panel is on to fill.
        'fill-percentage': GObject.ParamSpec.float('fill-percentage',
            'Fill percentage', 'Fill percentage',  GObject.ParamFlags.READABLE,
            0, 1, 1),
        // Shadow margin will add extra drawable area on the slidingPanel in a
        // slidingPanelOverlay. Useful for drawing a box-shadow.
        'shadow-margin': GObject.ParamSpec.int('shadow-margin', 'Shadow Margin',
            'Shadow Margin', GObject.ParamFlags.READABLE, 0, GLib.MAXINT32, 0),
    },

    _init: function (props={}) {
        this._panel_frame = new PanelFrame(this, {
            visible: true,
        });
        this._transparent_frame = new Gtk.Frame({
            visible: true,
        });

        this.parent(props);

        if (this.hide_when_invisible)
            this.no_show_all = true;

        switch(this.hide_direction) {
            case Gtk.PositionType.TOP:
                this.transition_type = Gtk.StackTransitionType.SLIDE_UP_DOWN;
                this.add(this._panel_frame);
                this.add(this._transparent_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_TOP);
                break;
            case Gtk.PositionType.RIGHT:
                this.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
                this.add(this._transparent_frame);
                this.add(this._panel_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_RIGHT);
                break;
            case Gtk.PositionType.BOTTOM:
                this.transition_type = Gtk.StackTransitionType.SLIDE_UP_DOWN;
                this.add(this._transparent_frame);
                this.add(this._panel_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_BOTTOM);
                break;
            case Gtk.PositionType.LEFT:
                this.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
                this.add(this._panel_frame);
                this.add(this._transparent_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_LEFT);
                break;
            default:
                logError(new Error('Warning: unhandled hide_direction value ' + this.hide_direction));
                break;
        }

        this.set_visible_child(this._reveal_panel ? this._panel_frame : this._transparent_frame);

        this.connect('notify::transition-running', () => {
            if (this.transition_running)
                return;
            if (!this.reveal_panel && this.hide_when_invisible)
                this.visible = false;
            this.notify('panel-revealed');
        });
    },

    get panel_widget () {
        return this._panel_frame.get_child();
    },

    set panel_widget (v) {
        if (this._panel_frame.get_child())
            this._panel_frame.remove(this._panel_frame.get_child());
        if (v)
            this._panel_frame.add(v);
    },

    get panel_revealed () {
        if (this.transition_running)
            return !this.reveal_panel;
        return this.reveal_panel;
    },

    get reveal_panel () {
        return this._reveal_panel;
    },

    set reveal_panel (v) {
        if (v === this._reveal_panel)
            return;
        this._reveal_panel = v;
        if (v && this.hide_when_invisible)
            this.visible = true;
        if (this.get_children().length > 0)
            this.set_visible_child(v ? this._panel_frame : this._transparent_frame);
    },
});
<commit_msg>SlidingPanel: fixed hide_when_invisible impl<commit_after>/* exported SlidingPanel */

// Copyright 2015 Endless Mobile, Inc.

const EosKnowledgePrivate = imports.gi.EosKnowledgePrivate;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

const Knowledge = imports.app.knowledge;

const PanelFrame = new Knowledge.Class({
    Name: 'PanelFrame',
    Extends: Gtk.Bin,

    _init: function (panel, params={}) {
        this.parent(params);
        this.panel = panel;
    },

    vfunc_size_allocate: function (allocation) {
        this.set_allocation(allocation);
        if (!this.get_child())
            return;

        let shadow_margin = EosKnowledgePrivate.style_context_get_custom_int(this.panel.get_style_context(),
                                                                             'shadow-margin');

        // Remove the extra space given by the SlidingPanelOverlay
        allocation.width -= 2 * shadow_margin;
        allocation.height -= 2 * shadow_margin;
        allocation.x += shadow_margin;
        allocation.y += shadow_margin;
        this.get_child().size_allocate(allocation);
    },
});

/**
 * Class: SlidingPanel
 * A widget which can slide its contents visible or out of sight.
 */
const SlidingPanel = new Knowledge.Class({
    Name: 'SlidingPanel',
    Extends: Gtk.Stack,

    Properties: {
        /**
         * Property: hide-direction
         * Direction to slide out of sight when hiding.
         *
         * We could make this mutable, but right now there's no need so keeping
         * construct only to keep the code simpler
         */
        'hide-direction': GObject.ParamSpec.enum('hide-direction',
            'Hide Direction', 'Hide Direction',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            Gtk.PositionType, Gtk.PositionType.TOP),
        /**
         * Property: panel-widget
         * The widget that slides in and out.
         */
        'panel-widget': GObject.ParamSpec.object('panel-widget',
            'Panel widget', 'Panel widget',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
            Gtk.Widget),
        /**
         * Property: reveal-panel
         * Whether the container should reveal the child.
         */
        'reveal-panel': GObject.ParamSpec.boolean('reveal-panel',
            'Reveal Panel', 'Reveal Panel',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
            false),
        /**
         * Property: panel-revealed
         * True if the panel is fully revealed or animating to the hidden state.
         */
        'panel-revealed': GObject.ParamSpec.boolean('panel-revealed',
            'Panel Revealed', 'Panel Revealed',
            GObject.ParamFlags.READABLE, false),
        /**
         * Property: hide-when-invisible
         *
         * True if the panel should hide entirely when not animating or showing
         * the widget.
         */
        'hide-when-invisible': GObject.ParamSpec.boolean('hide-when-invisible',
            'Hide When Invisible', 'Hide When Invisible',
            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
            false),
    },
    StyleProperties: {
        // These custom style properties are specifically for when using the
        // slidingPanel in a slidingPanelOverlay. Fill percentage will control
        // how much of the side the panel is on to fill.
        'fill-percentage': GObject.ParamSpec.float('fill-percentage',
            'Fill percentage', 'Fill percentage',  GObject.ParamFlags.READABLE,
            0, 1, 1),
        // Shadow margin will add extra drawable area on the slidingPanel in a
        // slidingPanelOverlay. Useful for drawing a box-shadow.
        'shadow-margin': GObject.ParamSpec.int('shadow-margin', 'Shadow Margin',
            'Shadow Margin', GObject.ParamFlags.READABLE, 0, GLib.MAXINT32, 0),
    },

    _init: function (props={}) {
        this._panel_frame = new PanelFrame(this, {
            visible: true,
        });
        this._transparent_frame = new Gtk.Frame({
            visible: true,
        });

        this.parent(props);

        if (this.hide_when_invisible)
            this.no_show_all = true;

        switch(this.hide_direction) {
            case Gtk.PositionType.TOP:
                this.transition_type = Gtk.StackTransitionType.SLIDE_UP_DOWN;
                this.add(this._panel_frame);
                this.add(this._transparent_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_TOP);
                break;
            case Gtk.PositionType.RIGHT:
                this.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
                this.add(this._transparent_frame);
                this.add(this._panel_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_RIGHT);
                break;
            case Gtk.PositionType.BOTTOM:
                this.transition_type = Gtk.StackTransitionType.SLIDE_UP_DOWN;
                this.add(this._transparent_frame);
                this.add(this._panel_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_BOTTOM);
                break;
            case Gtk.PositionType.LEFT:
                this.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT;
                this.add(this._panel_frame);
                this.add(this._transparent_frame);
                this.get_style_context().add_class(Gtk.STYLE_CLASS_LEFT);
                break;
            default:
                logError(new Error('Warning: unhandled hide_direction value ' + this.hide_direction));
                break;
        }

        this.set_visible_child(this._reveal_panel ? this._panel_frame : this._transparent_frame);

        this.connect('notify::transition-running', this._eval_visibility.bind(this));
    },

    get panel_widget () {
        return this._panel_frame.get_child();
    },

    set panel_widget (v) {
        if (this._panel_frame.get_child())
            this._panel_frame.remove(this._panel_frame.get_child());
        if (v)
            this._panel_frame.add(v);
    },

    get panel_revealed () {
        if (this.transition_running)
            return !this.reveal_panel;
        return this.reveal_panel;
    },

    get reveal_panel () {
        return this._reveal_panel;
    },

    set reveal_panel (v) {
        if (v === this._reveal_panel)
            return;
        this._reveal_panel = v;
        if (v && this.hide_when_invisible)
            this.visible = true;
        if (this.get_children().length > 0)
            this.set_visible_child(v ? this._panel_frame : this._transparent_frame);

        /* Make sure hide_when_invisible works if GTK animations are disabled */
        this._eval_visibility();
    },

    _eval_visibility: function () {
        if (this.transition_running)
            return;

        if (!this.reveal_panel && this.hide_when_invisible)
            this.visible = false;

        this.notify('panel-revealed');
    },
});
<|endoftext|>


<commit_before>'use strict';

define(['bitcoinjs-lib', 'util/djbec', 'util/encryption', 'util/protocol', 'backend/channels/peer', 'backend/channels/utils', 'backend/port', 'sjcl'],
function (Bitcoin, Curve25519, Encryption, Protocol, Peer, ChannelUtils, Port) {

  var convert = Bitcoin.convert;
  var bufToArray = function(obj) {return Array.prototype.slice.call(obj, 0);};

  /************************************
   * Channel
   */
  function Channel(transport, name) {
      var self = this;
      var client = transport.getClient();
      this.callbacks = {};
      this.chatLog = [];
      this.requested = [];
      this.lastRequest = 0;
      // max messages in the log
      this.maxChatLog = 200;

      // requests coming from peers
      this.peerRequests = [];

      // Set transport session key
      this.transport = transport;
      //this.prepareSession(transport.getSessionKey());
      this.newSession();

      this.name = name;

      // hash channel name
      var channelHash = ChannelUtils.hashChannelName(name);
      this.channelHash = channelHash;

      // Subscribe to channel updates
      if (this.subscribed !== channelHash) {
        this._onChannelData = function(_data) {self.onChannelData(_data);};
        if (client.handler_map["chan.update." + channelHash]) {
            // update callback
            client.handler_map["chan.update." + channelHash] = this._onChannelData;
        } else {
            this.channelSubscribe(function(err, data){
                if (!err) {
                    self.subscribed = channelHash;
                    // Internal log only for shout messages for now
                    self.addCallback('Shout', function(_data) {self.onChatMessage(_data);});
                    self.addCallback('Pair', function(_data) {self.onPairMessage(_data);});
                    self.addCallback('publicKeyRequest', function(_data) {self.onPublicKeyRequest(_data);});

                    // now tell listeners we'resubscribed
                    self.triggerCallbacks('subscribed', {});
                }
                console.log("[catchan] channel subscribed", name);
            }, this._onChannelData);
        }
      }
  }

  /**
   * Get the session from the transport
   */
  Channel.prototype.prepareSession = function(sessionKey) {
    // Set keys
    var priv = sessionKey.priv;
    var ecPriv = Encryption.adaptPrivateKey(priv);

    this.pub = Curve25519.ecDH(ecPriv);
    this.priv = ecPriv;

    // Setup peer details
    var newMe = new Peer(this.pub.toByteArrayUnsigned());

    this.comms = newMe;

    // Just relink so interface can be updated
    /*this.transport.comms.pubKeyHex = newMe.pubKeyHex;
    this.transport.comms.fingerprint = newMe.fingerprint;
    this.transport.comms.name = newMe.name;
    this.transport.comms.pubKey = newMe.pubKey;*/

    // Set some identity variables
    this.fingerprint = newMe.fingerprint;
  };

  /**
   * Initialize a new session with a new cloak
   */
  Channel.prototype.newSession = function() {
    // For now this will get changed in transport
    // and propagated back to all channels
    var sessionKey = new Bitcoin.ECKey();
    sessionKey.compressed = true;
    this.prepareSession(sessionKey);
  };

  /**
   * Disconnect the channel
   * Will be unusable afterwards and *must* be discarded.
   */
  Channel.prototype.disconnect = function() {
      this.removeAllCallbacks();
      this.channelUnsubscribe(function(){});
      this.requested = [];
      this.lastRequest = 0;
  };

  /*
   * Get peer from the fingerprint
   */
  Channel.prototype.getPeer = function(fingerprint, discover) {
      if (fingerprint === this.fingerprint) {
          return this.comms;
      }
      for(var idx=0; idx<this.transport.peers.length; idx++) {
          var peer = this.transport.peers[idx];
          if (peer.fingerprint === fingerprint) {
              peer.updateChannel(this);
              return peer;
          }
      }
      if (fingerprint.length !== 40) {
          // bad peers
          var pubKeyBytes = convert.stringToBytes(fingerprint);
          while(pubKeyBytes.length<32) { pubKeyBytes.push(6); }
          pubKeyBytes = pubKeyBytes.slice(32);

          console.log("[catchan] bad peer detected, dropping lookup", fingerprint);

          var newPeer = this.transport.addPeer(pubKeyBytes, fingerprint, this);
          newPeer.name = 'troll: ' + newPeer.name;
          newPeer.troll = true;
          return newPeer;
      }
      // unknown, request public key
      if (discover) {
          console.log("[catchan] request pubKey", fingerprint);
          this.requestPublicKey(fingerprint);
      }
      // create a dummy entry in the peers table, we can update it later
      return this.transport.addPeer(null, fingerprint, this);
  };

  /**
   * Request the public key from the given fingerprint
   */
  Channel.prototype.requestPublicKey = function(fingerprint) {
      if (fingerprint === this.fingerprint) {
          throw new Error("Requesting my own public key");
      }
      if (this.requested.indexOf(fingerprint) > -1) {
          console.log("[catchan] dropping request since already requested");
          return;
      }
      this.requested.push(fingerprint);
      // Prepare request
      var data = Protocol.PublicKeyRequestMsg(fingerprint);

      // Send encrypted
      this.postEncrypted(data, function(err, data){
          //console.log("announcement posted", err, data)
      }, true);
  };

  /**
   * Callback for a public key request
   */
  Channel.prototype.onPublicKeyRequest = function(data) {
      var now = Date.now();
      if (data.text[this.fingerprint] && (now - this.lastRequest) > 100) {
          console.log("[catchan] answering to pubKey request", this.fingerprint);
          this.sendOpening();
          this.lastRequest = now;
      }
  };

  /**
   * Callback for a public key being received
   */
  Channel.prototype.onPublicKey = function(data) {
      var first = Object.keys(data.text)[0];

      var pubKeyB64 = data.text[first].message;
      var pubKey = convert.base64ToBytes(pubKeyB64);

      var fingerprint = Encryption.genFingerprint(pubKey);
      if (fingerprint === this.fingerprint) {
          return;
      }
      this.transport.addPeer(pubKey, fingerprint, this);
      this.startPairing(fingerprint, pubKey);
  };

  // Send opening messages when joining a channel
  Channel.prototype.sendOpening = function() {
      // Send announcement
      var data = Protocol.PublicKeyMsg(this.fingerprint, this.pub);

      // Send encrypted
      this.postEncrypted(data, function(err, data){
          //console.log("announcement posted", err, data)
      });
  };

  // Subscribe to the channel
  Channel.prototype.channelSubscribe = function(callback, update_cb) {
      var client = this.transport.getClient();
      client.chan_subscribe("b", this.channelHash, callback, update_cb);
  };

  // Unsubscribe from the channel
  Channel.prototype.channelUnsubscribe = function(callback, update_cb) {
      var client = this.transport.getClient();
      if (client.connected) {
          client.chan_unsubscribe("b", this.channelHash, callback, update_cb);
      }
  };

  // Post to given channel
  Channel.prototype.post = function(data, callback) {
      var client = this.transport.getClient();
      data.sender = this.fingerprint;
      if (client.connected) {
          client.chan_post("b", this.channelHash, data, callback);
      }
  };

  /**
   * Post data to a public key
   */
  Channel.prototype.postDH = function(otherKey, data, callback) {
      data.sender = this.fingerprint;
      // prepare the message
      var msg = {
          type: 'postDH',
          fingerprint: this.fingerprint,
          channelName: this.name,
          otherKey: otherKey,
          privKey: this.priv.toByteArrayUnsigned(),
          pubKey: this.pub.toByteArrayUnsigned(),
          data: data
      };
      // send
      var self = this;
      this.transport.crypto.post(msg, function(err, encrypted) {
          if (!err) {
              self.post(encrypted, callback);
          }
      });
  };

  /**
   * Post data encrypted for the channel
   */
  Channel.prototype.postEncrypted = function(data, callback, hiding) {
      // prepare the message
      var msg = {
          type: 'postEncrypted',
          fingerprint: this.fingerprint,
          channelName: this.name,
          hiding: hiding,
          data: data
      };

      // send
      var self = this;
      this.transport.crypto.post(msg, function(err, encrypted) {
          if (!err) {
              self.post(encrypted, callback);
          }
      });

  };

  // Callback for data received on channel
  Channel.prototype.addCallback = function(type, callback) {
      if (!this.callbacks.hasOwnProperty(type)) {
          this.callbacks[type] = [callback];
      } else {
          this.callbacks[type].push(callback);
      }
      return callback;
  };

  /**
   * Trigger all callbacks of the given type
   */
  Channel.prototype.triggerCallbacks = function(type, data) {
      // channel messages don't have sender
      if (data.sender && type !== 'publicKey') {
          data.peer = this.getPeer(data.sender, true);
      } else if (!data.sender && ['subscribed', 'publicKeyRequest'].indexOf(type) === -1) {
          console.log("[catchan] message with no sender", type, data);
      }
      if (this.callbacks.hasOwnProperty(type)) {
          this.callbacks[type].forEach(function(cb) {cb(data);});
      }
  };

  /**
   * Remove a callback
   */
  Channel.prototype.removeCallback = function(type, callback) {
      if (this.callbacks.hasOwnProperty(type)) {
          var cbArr = this.callbacks[type];
          if (cbArr.indexOf(callback) !== -1) {
              cbArr.splice(cbArr.indexOf(callback), 1);
          }
      }
  };

  /**
   * Remove all callbacks
   */
  Channel.prototype.removeAllCallbacks = function() {
      // tell listeners we're being unsubscribed
      if (this.callbacks.unsubscribed && this.callbacks.unsubscribed.length) {
          this.callbacks.unsubscribed.forEach(function(callback) {
              callback({});
          });
      }
      // Now delete all callbacks
      this.callbacks = {};
  };

  /**
   * Channel worker data arriving
   */
  Channel.prototype.onWorkerData = function(message) {
      if (message.peer) {
          var pubKey = message.peer.pubKey;
          var fingerprint = message.peer.fingerprint;
          message.data.peer = this.transport.addPeer(pubKey, fingerprint, this);
      }
      switch(message.type) {
          case 'beacon':
              this.onReceiveBeacon(message.data);
              break;
          case 'publicKey':
              this.onPublicKey(message.data);
              // continue to trigger callbacks...
          case 'personal':
          case 'channel':
              this.triggerCallbacks(message.data.type, message.data);
              break;
          default:
              console.log("Unknown message type on channel! " + message.type);
              break;
      }
  };

  /**
   * Channel data arriving
   */
  Channel.prototype.onChannelData = function(message) {
      if (!this.transport.channelWorker) {
          console.log("[catchan] Receiving data on dead transport!");
          return;
      }
      // Prepare the message
      var msg = {
          type: 'channelData',
          channelName: this.name,
          channelPriv: this.priv.toByteArrayUnsigned(),
          scanKey: this.transport.getScanKey(),
          message: message
      };
      // serialize private key
      msg.scanKey.priv = msg.scanKey.priv.toByteArrayUnsigned();

      // Send to worker
      this.transport.channelWorker.postMessage(msg);
  };

  Channel.prototype.onChatMessage = function(data) {
      var chatLog;
      if (data.metadata && data.metadata.whisper) {
          chatLog = data.peer.chatLog;
          chatLog.dirty = true;
      } else {
          chatLog = this.chatLog;
      }
      // Insert new
      if (chatLog.length > this.maxChatLog) {
          chatLog.pop();
      }
      chatLog.splice(0,0,data);
  };

  /**
   * Beacons and pairing
   */
  Channel.prototype.sendPairing = function(nick, peer, address, callback) {
      var signKey = this.transport.getSignKey();

      var scanKey = this.transport.getScanKey();

      var msg = Protocol.PairMsg(nick, signKey, scanKey.pub, address);
      this.postDH(peer.pubKey, msg, callback);
  };

  Channel.prototype.onPairMessage = function(data) {
      this.peerRequests.push(data);
  };

  Channel.prototype.checkPairMessage = function(decoded) {
      var idKey = decoded.body.pub;
      var keys = bufToArray(Bitcoin.base58check.decode(idKey.substr(3)).payload);
      var data = decoded.body;
      var toCheck = data.address+data.nick+data.pub;

      return Curve25519.checksig(decoded.body.sig, toCheck, keys.slice(32));
  };

  Channel.prototype.sendBeacon = function(beaconKey, callback) {
      var signKey = this.transport.getSignKey();
      var work = {type: 'buildBeacon', pubKey: this.pub.toByteArrayUnsigned(), signKey: signKey};
      var self = this;
      this.transport.crypto.post(work, function(err, beacon) {
          if (!err) {
              self.postDH(beaconKey, beacon, callback);
          }
      });
  };

  /**
   * Trigger callbacks for the peer and peer.contact
   * @private
   */
  Channel.prototype.onContactAvailable = function(peer) {
      Port.post('contacts', {type: 'contact', peer: peer});
  };

  Channel.prototype.onReceiveBeacon = function(decoded) {
      var self = this;
      // Find out which contact this is
      var valid = false;
      var identity = this.transport.identity;
      identity.contacts.contacts.forEach(function(contact) {
           var idKey = contact.findIdentityKey();
           if (idKey) {
               var keys = bufToArray(Bitcoin.base58check.decode(idKey.data.substr(3)).payload);
               var signKey = Bitcoin.convert.bytesToString(keys.slice(32));
               if (signKey === decoded.body.pub) {
                   var toCheck = decoded.body.ephem+decoded.body.pub;
                   if (Curve25519.checksig(decoded.body.sig, toCheck, keys.slice(32))) {
                       decoded.body.nick = contact.data.name;
                       decoded.peer.nick = contact.data.name;
                       decoded.peer.contact = contact;
                       self.onContactAvailable(decoded.peer, contact);
                       valid = true;
                   } else {
                       console.log("checking!");
                   }
               }
           }
      });
      if (valid) {
          this.peerRequests.push(decoded);
      }
      this.triggerCallbacks('Beacon', decoded);
      return decoded;
  };

  Channel.prototype.startPairing = function(fingerprint, pubKey) {
     console.log('[catchan] stored pubkey', fingerprint);
  };

  return Channel;
});
<commit_msg>Remove uneeded line breaking all lobby communications on some systems.<commit_after>'use strict';

define(['bitcoinjs-lib', 'util/djbec', 'util/encryption', 'util/protocol', 'backend/channels/peer', 'backend/channels/utils', 'backend/port', 'sjcl'],
function (Bitcoin, Curve25519, Encryption, Protocol, Peer, ChannelUtils, Port) {

  var convert = Bitcoin.convert;
  var bufToArray = function(obj) {return Array.prototype.slice.call(obj, 0);};

  /************************************
   * Channel
   */
  function Channel(transport, name) {
      var self = this;
      var client = transport.getClient();
      this.callbacks = {};
      this.chatLog = [];
      this.requested = [];
      this.lastRequest = 0;
      // max messages in the log
      this.maxChatLog = 200;

      // requests coming from peers
      this.peerRequests = [];

      // Set transport session key
      this.transport = transport;
      //this.prepareSession(transport.getSessionKey());
      this.newSession();

      this.name = name;

      // hash channel name
      var channelHash = ChannelUtils.hashChannelName(name);
      this.channelHash = channelHash;

      // Subscribe to channel updates
      if (this.subscribed !== channelHash) {
        this._onChannelData = function(_data) {self.onChannelData(_data);};
        if (client.handler_map["chan.update." + channelHash]) {
            // update callback
            client.handler_map["chan.update." + channelHash] = this._onChannelData;
        } else {
            this.channelSubscribe(function(err, data){
                if (!err) {
                    self.subscribed = channelHash;
                    // Internal log only for shout messages for now
                    self.addCallback('Shout', function(_data) {self.onChatMessage(_data);});
                    self.addCallback('Pair', function(_data) {self.onPairMessage(_data);});
                    self.addCallback('publicKeyRequest', function(_data) {self.onPublicKeyRequest(_data);});

                    // now tell listeners we'resubscribed
                    self.triggerCallbacks('subscribed', {});
                }
                console.log("[catchan] channel subscribed", name);
            }, this._onChannelData);
        }
      }
  }

  /**
   * Get the session from the transport
   */
  Channel.prototype.prepareSession = function(sessionKey) {
    // Set keys
    var priv = sessionKey.priv;
    var ecPriv = Encryption.adaptPrivateKey(priv);

    this.pub = Curve25519.ecDH(ecPriv);
    this.priv = ecPriv;

    // Setup peer details
    var newMe = new Peer(this.pub.toByteArrayUnsigned());

    this.comms = newMe;

    // Just relink so interface can be updated
    /*this.transport.comms.pubKeyHex = newMe.pubKeyHex;
    this.transport.comms.fingerprint = newMe.fingerprint;
    this.transport.comms.name = newMe.name;
    this.transport.comms.pubKey = newMe.pubKey;*/

    // Set some identity variables
    this.fingerprint = newMe.fingerprint;
  };

  /**
   * Initialize a new session with a new cloak
   */
  Channel.prototype.newSession = function() {
    // For now this will get changed in transport
    // and propagated back to all channels
    var sessionKey = new Bitcoin.ECKey();
    sessionKey.compressed = true;
    this.prepareSession(sessionKey);
  };

  /**
   * Disconnect the channel
   * Will be unusable afterwards and *must* be discarded.
   */
  Channel.prototype.disconnect = function() {
      this.removeAllCallbacks();
      this.channelUnsubscribe(function(){});
      this.requested = [];
      this.lastRequest = 0;
  };

  /*
   * Get peer from the fingerprint
   */
  Channel.prototype.getPeer = function(fingerprint, discover) {
      if (fingerprint === this.fingerprint) {
          return this.comms;
      }
      for(var idx=0; idx<this.transport.peers.length; idx++) {
          var peer = this.transport.peers[idx];
          if (peer.fingerprint === fingerprint) {
              peer.updateChannel(this);
              return peer;
          }
      }
      if (fingerprint.length !== 40) {
          // bad peers
          var pubKeyBytes = convert.stringToBytes(fingerprint);
          while(pubKeyBytes.length<32) { pubKeyBytes.push(6); }
          pubKeyBytes = pubKeyBytes.slice(32);

          console.log("[catchan] bad peer detected, dropping lookup", fingerprint);

          var newPeer = this.transport.addPeer(pubKeyBytes, fingerprint, this);
          newPeer.name = 'troll: ' + newPeer.name;
          newPeer.troll = true;
          return newPeer;
      }
      // unknown, request public key
      if (discover) {
          console.log("[catchan] request pubKey", fingerprint);
          this.requestPublicKey(fingerprint);
      }
      // create a dummy entry in the peers table, we can update it later
      return this.transport.addPeer(null, fingerprint, this);
  };

  /**
   * Request the public key from the given fingerprint
   */
  Channel.prototype.requestPublicKey = function(fingerprint) {
      if (fingerprint === this.fingerprint) {
          throw new Error("Requesting my own public key");
      }
      if (this.requested.indexOf(fingerprint) > -1) {
          console.log("[catchan] dropping request since already requested");
          return;
      }
      this.requested.push(fingerprint);
      // Prepare request
      var data = Protocol.PublicKeyRequestMsg(fingerprint);

      // Send encrypted
      this.postEncrypted(data, function(err, data){
          //console.log("announcement posted", err, data)
      }, true);
  };

  /**
   * Callback for a public key request
   */
  Channel.prototype.onPublicKeyRequest = function(data) {
      var now = Date.now();
      if (data.text[this.fingerprint] && (now - this.lastRequest) > 100) {
          console.log("[catchan] answering to pubKey request", this.fingerprint);
          this.sendOpening();
          this.lastRequest = now;
      }
  };

  /**
   * Callback for a public key being received
   */
  Channel.prototype.onPublicKey = function(data) {
      var first = Object.keys(data.text)[0];

      var pubKeyB64 = data.text[first].message;
      var pubKey = convert.base64ToBytes(pubKeyB64);

      var fingerprint = Encryption.genFingerprint(pubKey);
      if (fingerprint === this.fingerprint) {
          return;
      }
      this.transport.addPeer(pubKey, fingerprint, this);
      this.startPairing(fingerprint, pubKey);
  };

  // Send opening messages when joining a channel
  Channel.prototype.sendOpening = function() {
      // Send announcement
      var data = Protocol.PublicKeyMsg(this.fingerprint, this.pub);

      // Send encrypted
      this.postEncrypted(data, function(err, data){
          //console.log("announcement posted", err, data)
      });
  };

  // Subscribe to the channel
  Channel.prototype.channelSubscribe = function(callback, update_cb) {
      var client = this.transport.getClient();
      client.chan_subscribe("b", this.channelHash, callback, update_cb);
  };

  // Unsubscribe from the channel
  Channel.prototype.channelUnsubscribe = function(callback, update_cb) {
      var client = this.transport.getClient();
      if (client.connected) {
          client.chan_unsubscribe("b", this.channelHash, callback, update_cb);
      }
  };

  // Post to given channel
  Channel.prototype.post = function(data, callback) {
      var client = this.transport.getClient();
      if (client.connected) {
          client.chan_post("b", this.channelHash, data, callback);
      }
  };

  /**
   * Post data to a public key
   */
  Channel.prototype.postDH = function(otherKey, data, callback) {
      data.sender = this.fingerprint;
      // prepare the message
      var msg = {
          type: 'postDH',
          fingerprint: this.fingerprint,
          channelName: this.name,
          otherKey: otherKey,
          privKey: this.priv.toByteArrayUnsigned(),
          pubKey: this.pub.toByteArrayUnsigned(),
          data: data
      };
      // send
      var self = this;
      this.transport.crypto.post(msg, function(err, encrypted) {
          if (!err) {
              self.post(encrypted, callback);
          }
      });
  };

  /**
   * Post data encrypted for the channel
   */
  Channel.prototype.postEncrypted = function(data, callback, hiding) {
      // prepare the message
      var msg = {
          type: 'postEncrypted',
          fingerprint: this.fingerprint,
          channelName: this.name,
          hiding: hiding,
          data: data
      };

      // send
      var self = this;
      this.transport.crypto.post(msg, function(err, encrypted) {
          if (!err) {
              self.post(encrypted, callback);
          }
      });

  };

  // Callback for data received on channel
  Channel.prototype.addCallback = function(type, callback) {
      if (!this.callbacks.hasOwnProperty(type)) {
          this.callbacks[type] = [callback];
      } else {
          this.callbacks[type].push(callback);
      }
      return callback;
  };

  /**
   * Trigger all callbacks of the given type
   */
  Channel.prototype.triggerCallbacks = function(type, data) {
      // channel messages don't have sender
      if (data.sender && type !== 'publicKey') {
          data.peer = this.getPeer(data.sender, true);
      } else if (!data.sender && ['subscribed', 'publicKeyRequest'].indexOf(type) === -1) {
          console.log("[catchan] message with no sender", type, data);
      }
      if (this.callbacks.hasOwnProperty(type)) {
          this.callbacks[type].forEach(function(cb) {cb(data);});
      }
  };

  /**
   * Remove a callback
   */
  Channel.prototype.removeCallback = function(type, callback) {
      if (this.callbacks.hasOwnProperty(type)) {
          var cbArr = this.callbacks[type];
          if (cbArr.indexOf(callback) !== -1) {
              cbArr.splice(cbArr.indexOf(callback), 1);
          }
      }
  };

  /**
   * Remove all callbacks
   */
  Channel.prototype.removeAllCallbacks = function() {
      // tell listeners we're being unsubscribed
      if (this.callbacks.unsubscribed && this.callbacks.unsubscribed.length) {
          this.callbacks.unsubscribed.forEach(function(callback) {
              callback({});
          });
      }
      // Now delete all callbacks
      this.callbacks = {};
  };

  /**
   * Channel worker data arriving
   */
  Channel.prototype.onWorkerData = function(message) {
      if (message.peer) {
          var pubKey = message.peer.pubKey;
          var fingerprint = message.peer.fingerprint;
          message.data.peer = this.transport.addPeer(pubKey, fingerprint, this);
      }
      switch(message.type) {
          case 'beacon':
              this.onReceiveBeacon(message.data);
              break;
          case 'publicKey':
              this.onPublicKey(message.data);
              // continue to trigger callbacks...
          case 'personal':
          case 'channel':
              this.triggerCallbacks(message.data.type, message.data);
              break;
          default:
              console.log("Unknown message type on channel! " + message.type);
              break;
      }
  };

  /**
   * Channel data arriving
   */
  Channel.prototype.onChannelData = function(message) {
      if (!this.transport.channelWorker) {
          console.log("[catchan] Receiving data on dead transport!");
          return;
      }
      // Prepare the message
      var msg = {
          type: 'channelData',
          channelName: this.name,
          channelPriv: this.priv.toByteArrayUnsigned(),
          scanKey: this.transport.getScanKey(),
          message: message
      };
      // serialize private key
      msg.scanKey.priv = msg.scanKey.priv.toByteArrayUnsigned();

      // Send to worker
      this.transport.channelWorker.postMessage(msg);
  };

  Channel.prototype.onChatMessage = function(data) {
      var chatLog;
      if (data.metadata && data.metadata.whisper) {
          chatLog = data.peer.chatLog;
          chatLog.dirty = true;
      } else {
          chatLog = this.chatLog;
      }
      // Insert new
      if (chatLog.length > this.maxChatLog) {
          chatLog.pop();
      }
      chatLog.splice(0,0,data);
  };

  /**
   * Beacons and pairing
   */
  Channel.prototype.sendPairing = function(nick, peer, address, callback) {
      var signKey = this.transport.getSignKey();

      var scanKey = this.transport.getScanKey();

      var msg = Protocol.PairMsg(nick, signKey, scanKey.pub, address);
      this.postDH(peer.pubKey, msg, callback);
  };

  Channel.prototype.onPairMessage = function(data) {
      this.peerRequests.push(data);
  };

  Channel.prototype.checkPairMessage = function(decoded) {
      var idKey = decoded.body.pub;
      var keys = bufToArray(Bitcoin.base58check.decode(idKey.substr(3)).payload);
      var data = decoded.body;
      var toCheck = data.address+data.nick+data.pub;

      return Curve25519.checksig(decoded.body.sig, toCheck, keys.slice(32));
  };

  Channel.prototype.sendBeacon = function(beaconKey, callback) {
      var signKey = this.transport.getSignKey();
      var work = {type: 'buildBeacon', pubKey: this.pub.toByteArrayUnsigned(), signKey: signKey};
      var self = this;
      this.transport.crypto.post(work, function(err, beacon) {
          if (!err) {
              self.postDH(beaconKey, beacon, callback);
          }
      });
  };

  /**
   * Trigger callbacks for the peer and peer.contact
   * @private
   */
  Channel.prototype.onContactAvailable = function(peer) {
      Port.post('contacts', {type: 'contact', peer: peer});
  };

  Channel.prototype.onReceiveBeacon = function(decoded) {
      var self = this;
      // Find out which contact this is
      var valid = false;
      var identity = this.transport.identity;
      identity.contacts.contacts.forEach(function(contact) {
           var idKey = contact.findIdentityKey();
           if (idKey) {
               var keys = bufToArray(Bitcoin.base58check.decode(idKey.data.substr(3)).payload);
               var signKey = Bitcoin.convert.bytesToString(keys.slice(32));
               if (signKey === decoded.body.pub) {
                   var toCheck = decoded.body.ephem+decoded.body.pub;
                   if (Curve25519.checksig(decoded.body.sig, toCheck, keys.slice(32))) {
                       decoded.body.nick = contact.data.name;
                       decoded.peer.nick = contact.data.name;
                       decoded.peer.contact = contact;
                       self.onContactAvailable(decoded.peer, contact);
                       valid = true;
                   } else {
                       console.log("checking!");
                   }
               }
           }
      });
      if (valid) {
          this.peerRequests.push(decoded);
      }
      this.triggerCallbacks('Beacon', decoded);
      return decoded;
  };

  Channel.prototype.startPairing = function(fingerprint, pubKey) {
     console.log('[catchan] stored pubkey', fingerprint);
  };

  return Channel;
});
<|endoftext|>