<commit_before>/* ========================================================================
 * ZUI: selectable.js [1.5.0+]
 * http://openzui.com
 * ========================================================================
 * Copyright (c) 2016 cnezsoft.com; Licensed MIT
 * ======================================================================== */

(function($) {
    'use strict';

    var name = 'zui.selectable'; // module name

    // The selectable modal class
    var Selectable = function(element, options) {
        this.name = name;
        this.$ = $(element);
        this.id = $.zui.uuid();
        this.selectOrder = 1;
        this.selections = {};

        this.getOptions(options);
        this._init();
    };

    var isPointInner = function(x, y, a) {
        return x >= a.left && x <= (a.left + a.width) && y >= a.top && y <= (a.top + a.height);
    };

    var isIntersectArea = function(a, b) {
        var x1 = Math.max(a.left, b.left),
            y1 = Math.max(a.top, b.top),
            x2 = Math.min(a.left + a.width, b.left + b.width),
            y2 = Math.min(a.top + a.height, b.top + b.height);

        return isPointInner(x1, y1, a) && isPointInner(x2, y2, a) && isPointInner(x1, y1, b) && isPointInner(x2, y2, b);
    };

    // default options
    Selectable.DEFAULTS = {
        selector: 'li,tr,div',
        trigger: '',
        selectClass: 'active',
        rangeStyle: {
            border: '1px solid ' + ($.zui.colorset ? $.zui.colorset.primary : '#3280fc'),
            backgroundColor: $.zui.colorset ? (new $.zui.Color($.zui.colorset.primary).fade(20).toCssStr()) : 'rgba(50, 128, 252, 0.2)'
        },
        clickBehavior: 'toggle',
        ignoreVal: 3,
        listenClick: true
        // mouseButton: -1 // 0, 1, 2, -1, all, left,  right, middle
    };

    // Get and init options
    Selectable.prototype.getOptions = function(options) {
        this.options = $.extend({}, Selectable.DEFAULTS, this.$.data(), options);
    };

    Selectable.prototype.select = function(elementOrid) {
        this.toggle(elementOrid, true);
    };

    Selectable.prototype.unselect = function(elementOrid) {
        this.toggle(elementOrid, false);
    };

    Selectable.prototype.toggle = function(elementOrid, isSelect, handle) {
        var $element, id, selector = this.options.selector, that = this;
        if(elementOrid === undefined) {
            this.$.find(selector).each(function() {
                that.toggle(this, isSelect);
            });
            return;
        } else if(typeof elementOrid === 'object') {
            $element = $(elementOrid).closest(selector);
            id = $element.data('id');
        } else {
            id = elementOrid;
            $element = that.$.find('.slectable-item[data-id="' + id + '"]');
        }
        if($element && $element.length) {
            if(!id) {
                id = $.zui.uuid();
                $element.attr('data-id', id);
            }
            if(isSelect === undefined || isSelect === null) {
                isSelect = !that.selections[id];
            }
            if(!!isSelect !== !!that.selections[id]) {
                var handleResult;
                if(typeof handle === 'function') {
                    handleResult = handle(isSelect);
                }
                if(handleResult !== true) {
                    that.selections[id] = isSelect ? that.selectOrder++ : false;
                    that.callEvent(isSelect ? 'select' : 'unselect', {id: id, selections: that.selections, target: $element, selected: that.getSelectedArray()}, that);
                }
            }
            if (that.options.selectClass) {
                $element.toggleClass(that.options.selectClass, isSelect);
            }
        }
    };

    Selectable.prototype.getSelectedArray = function() {
        var selected = [];
        $.each(this.selections, function(thisId, thisIsSelected) {
            if(thisIsSelected) selected.push(thisId);
        });
        return selected;
    };

    Selectable.prototype.syncSelectionsFromClass = function() {
        var that = this;
        var $children = that.$children = that.$.find(that.options.selector);
        that.selections = {};
        $children.each(function() {
            var $item = $(this);
            that.selections[$item.data('id')] = $item.hasClass(that.options.selectClass);
        });
    };

    Selectable.prototype._init = function() {
        var options = this.options, that = this;
        var ignoreVal = options.ignoreVal;
        var isIgnoreMove = true;
        var eventNamespace = '.' + this.name + '.' + this.id;
        var startX, startY, $range, range, x, y, checkRangeCall;
        var checkFunc = typeof options.checkFunc === 'function' ? options.checkFunc : null;
        var rangeFunc = typeof options.rangeFunc === 'function' ? options.rangeFunc : null;
        var isMouseDown    = false;
        var mouseDownBackEventCall = null;
        var mouseDownEventName = 'mousedown' + eventNamespace;

        var checkRange = function() {
            if(!range) return;
            that.$children.each(function() {
                var $item = $(this);
                var offset = $item.offset();
                offset.width = $item.outerWidth();
                offset.height = $item.outerHeight();
                var isIntersect = rangeFunc ? rangeFunc.call(this, range, offset) : isIntersectArea(range, offset);
                if(checkFunc) {
                    var result = checkFunc.call(that, {
                        intersect: isIntersect,
                        target: $item,
                        range: range,
                        targetRange: offset
                    });
                    if(result === true) {
                        that.select($item);
                    } else if(result === false) {
                        that.unselect($item);
                    }
                } else {
                    if(isIntersect) {
                        that.select($item);
                    } else if(!that.multiKey) {
                        that.unselect($item);
                    }
                }
            });
        };

        var mousemove = function(e) {
            if(!isMouseDown) return;
            x = e.pageX;
            y = e.pageY;
            range = {
                width: Math.abs(x - startX),
                height: Math.abs(y - startY),
                left: x > startX ? startX : x,
                top: y > startY ? startY : y
            };

            if(isIgnoreMove && range.width < ignoreVal && range.height < ignoreVal) return;
            if(!$range) {
                $range = $('.selectable-range[data-id="' + that.id + '"]');
                if(!$range.length) {
                    $range = $('

')
                        .css($.extend({
                            zIndex: 1060,
                            position: 'absolute',
                            top: startX,
                            left: startY,
                            pointerEvents: 'none',
                        }, that.options.rangeStyle))
                        .appendTo($('body'));
                }
            }
            $range.css(range);
            clearTimeout(checkRangeCall);
            checkRangeCall = setTimeout(checkRange, 10);
            isIgnoreMove = false;
        };

        var mouseup = function(e) {
            $(document).off(eventNamespace);
            clearTimeout(mouseDownBackEventCall);
            if(!isMouseDown) return;
            isMouseDown = false;
            if($range) $range.remove();
            if(!isIgnoreMove)
            {
                if(range) {
                    clearTimeout(checkRangeCall);
                    checkRange();
                    range = null;
                }
            }
            that.callEvent('finish', {selections: that.selections, selected: that.getSelectedArray()});
            e.preventDefault();
        };

        var mousedown = function(e) {
            if(isMouseDown) {
                return mouseup(e);
            }

            var mouseButton = $.zui.getMouseButtonCode(options.mouseButton);
            if(mouseButton > -1 && e.button !== mouseButton) {
                return;
            }

            if(that.altKey || e.which === 3 || that.callEvent('start', e) === false) {
                return;
            }

            var $children = that.$children = that.$.find(options.selector);
            $children.addClass('slectable-item');

            var clickBehavior = that.multiKey ? 'multi' : options.clickBehavior;
            if(clickBehavior === 'single') {
                that.unselect();
            }
            if (options.listenClick) {
                if(clickBehavior === 'multi') {
                    that.toggle(e.target);
                } else if(clickBehavior === 'single') {
                    that.select(e.target);
                } else if(clickBehavior === 'toggle') {
                    that.toggle(e.target, null, function(isSelect) {
                        that.unselect();
                    });
                }
            }

            if(that.callEvent('startDrag', e) === false) {
                that.callEvent('finish', {selections: that.selections, selected: that.getSelectedArray()});
                return;
            }

            startX = e.pageX;
            startY = e.pageY;

            $range = null;
            isIgnoreMove = true;
            isMouseDown = true;

            $(document).on('mousemove' + eventNamespace, mousemove).on('mouseup' + eventNamespace, mouseup);
            mouseDownBackEventCall = setTimeout(function() {
                $(document).on(mouseDownEventName, mouseup);
            }, 10);
            e.preventDefault();
        };

        var $container = options.container && options.container !== 'default' ? $(options.container) : this.$;
        if(options.trigger) {
            $container.on(mouseDownEventName, options.trigger, mousedown);
        } else {
            $container.on(mouseDownEventName, mousedown);
        }

        $(document).on('keydown', function(e) {
            var code = e.keyCode;
            if(code === 17 || code == 91) that.multiKey = code;
            else if(code === 18) that.altKey = true;
        }).on('keyup', function(e) {
            that.multiKey = false;
            that.altKey = false;
        });
    };

    // Call event helper
    Selectable.prototype.callEvent = function(name, params) {
        var event = $.Event(name + '.' + this.name);
        this.$.trigger(event, params);
        var result = event.result;
        var callback = this.options[name];
        if(typeof callback === 'function') {
            result = callback.apply(this, Array.isArray(params) ? params : [params]);
        }
        return result;
    };

    // Extense jquery element
    $.fn.selectable = function(option) {
        return this.each(function() {
            var $this = $(this);
            var data = $this.data(name);
            var options = typeof option == 'object' && option;

            if(!data) $this.data(name, (data = new Selectable(this, options)));

            if(typeof option == 'string') data[option]();
        });
    };

    $.fn.selectable.Constructor = Selectable;

    // Auto call selectable after document load complete
    $(function() {
        $('[data-ride="selectable"]').selectable();
    });
}(jQuery));
<commit_msg>* typo in selectable.<commit_after>/* ========================================================================
 * ZUI: selectable.js [1.5.0+]
 * http://openzui.com
 * ========================================================================
 * Copyright (c) 2016 cnezsoft.com; Licensed MIT
 * ======================================================================== */

(function($) {
    'use strict';

    var name = 'zui.selectable'; // module name

    // The selectable modal class
    var Selectable = function(element, options) {
        this.name = name;
        this.$ = $(element);
        this.id = $.zui.uuid();
        this.selectOrder = 1;
        this.selections = {};

        this.getOptions(options);
        this._init();
    };

    var isPointInner = function(x, y, a) {
        return x >= a.left && x <= (a.left + a.width) && y >= a.top && y <= (a.top + a.height);
    };

    var isIntersectArea = function(a, b) {
        var x1 = Math.max(a.left, b.left),
            y1 = Math.max(a.top, b.top),
            x2 = Math.min(a.left + a.width, b.left + b.width),
            y2 = Math.min(a.top + a.height, b.top + b.height);

        return isPointInner(x1, y1, a) && isPointInner(x2, y2, a) && isPointInner(x1, y1, b) && isPointInner(x2, y2, b);
    };

    // default options
    Selectable.DEFAULTS = {
        selector: 'li,tr,div',
        trigger: '',
        selectClass: 'active',
        rangeStyle: {
            border: '1px solid ' + ($.zui.colorset ? $.zui.colorset.primary : '#3280fc'),
            backgroundColor: $.zui.colorset ? (new $.zui.Color($.zui.colorset.primary).fade(20).toCssStr()) : 'rgba(50, 128, 252, 0.2)'
        },
        clickBehavior: 'toggle',
        ignoreVal: 3,
        listenClick: true
        // mouseButton: -1 // 0, 1, 2, -1, all, left,  right, middle
    };

    // Get and init options
    Selectable.prototype.getOptions = function(options) {
        this.options = $.extend({}, Selectable.DEFAULTS, this.$.data(), options);
    };

    Selectable.prototype.select = function(elementOrid) {
        this.toggle(elementOrid, true);
    };

    Selectable.prototype.unselect = function(elementOrid) {
        this.toggle(elementOrid, false);
    };

    Selectable.prototype.toggle = function(elementOrid, isSelect, handle) {
        var $element, id, selector = this.options.selector, that = this;
        if(elementOrid === undefined) {
            this.$.find(selector).each(function() {
                that.toggle(this, isSelect);
            });
            return;
        } else if(typeof elementOrid === 'object') {
            $element = $(elementOrid).closest(selector);
            id = $element.data('id');
        } else {
            id = elementOrid;
            $element = that.$.find('.selectable-item[data-id="' + id + '"]');
        }
        if($element && $element.length) {
            if(!id) {
                id = $.zui.uuid();
                $element.attr('data-id', id);
            }
            if(isSelect === undefined || isSelect === null) {
                isSelect = !that.selections[id];
            }
            if(!!isSelect !== !!that.selections[id]) {
                var handleResult;
                if(typeof handle === 'function') {
                    handleResult = handle(isSelect);
                }
                if(handleResult !== true) {
                    that.selections[id] = isSelect ? that.selectOrder++ : false;
                    that.callEvent(isSelect ? 'select' : 'unselect', {id: id, selections: that.selections, target: $element, selected: that.getSelectedArray()}, that);
                }
            }
            if (that.options.selectClass) {
                $element.toggleClass(that.options.selectClass, isSelect);
            }
        }
    };

    Selectable.prototype.getSelectedArray = function() {
        var selected = [];
        $.each(this.selections, function(thisId, thisIsSelected) {
            if(thisIsSelected) selected.push(thisId);
        });
        return selected;
    };

    Selectable.prototype.syncSelectionsFromClass = function() {
        var that = this;
        var $children = that.$children = that.$.find(that.options.selector);
        that.selections = {};
        $children.each(function() {
            var $item = $(this);
            that.selections[$item.data('id')] = $item.hasClass(that.options.selectClass);
        });
    };

    Selectable.prototype._init = function() {
        var options = this.options, that = this;
        var ignoreVal = options.ignoreVal;
        var isIgnoreMove = true;
        var eventNamespace = '.' + this.name + '.' + this.id;
        var startX, startY, $range, range, x, y, checkRangeCall;
        var checkFunc = typeof options.checkFunc === 'function' ? options.checkFunc : null;
        var rangeFunc = typeof options.rangeFunc === 'function' ? options.rangeFunc : null;
        var isMouseDown    = false;
        var mouseDownBackEventCall = null;
        var mouseDownEventName = 'mousedown' + eventNamespace;

        var checkRange = function() {
            if(!range) return;
            that.$children.each(function() {
                var $item = $(this);
                var offset = $item.offset();
                offset.width = $item.outerWidth();
                offset.height = $item.outerHeight();
                var isIntersect = rangeFunc ? rangeFunc.call(this, range, offset) : isIntersectArea(range, offset);
                if(checkFunc) {
                    var result = checkFunc.call(that, {
                        intersect: isIntersect,
                        target: $item,
                        range: range,
                        targetRange: offset
                    });
                    if(result === true) {
                        that.select($item);
                    } else if(result === false) {
                        that.unselect($item);
                    }
                } else {
                    if(isIntersect) {
                        that.select($item);
                    } else if(!that.multiKey) {
                        that.unselect($item);
                    }
                }
            });
        };

        var mousemove = function(e) {
            if(!isMouseDown) return;
            x = e.pageX;
            y = e.pageY;
            range = {
                width: Math.abs(x - startX),
                height: Math.abs(y - startY),
                left: x > startX ? startX : x,
                top: y > startY ? startY : y
            };

            if(isIgnoreMove && range.width < ignoreVal && range.height < ignoreVal) return;
            if(!$range) {
                $range = $('.selectable-range[data-id="' + that.id + '"]');
                if(!$range.length) {
                    $range = $('

')
                        .css($.extend({
                            zIndex: 1060,
                            position: 'absolute',
                            top: startX,
                            left: startY,
                            pointerEvents: 'none',
                        }, that.options.rangeStyle))
                        .appendTo($('body'));
                }
            }
            $range.css(range);
            clearTimeout(checkRangeCall);
            checkRangeCall = setTimeout(checkRange, 10);
            isIgnoreMove = false;
        };

        var mouseup = function(e) {
            $(document).off(eventNamespace);
            clearTimeout(mouseDownBackEventCall);
            if(!isMouseDown) return;
            isMouseDown = false;
            if($range) $range.remove();
            if(!isIgnoreMove)
            {
                if(range) {
                    clearTimeout(checkRangeCall);
                    checkRange();
                    range = null;
                }
            }
            that.callEvent('finish', {selections: that.selections, selected: that.getSelectedArray()});
            e.preventDefault();
        };

        var mousedown = function(e) {
            if(isMouseDown) {
                return mouseup(e);
            }

            var mouseButton = $.zui.getMouseButtonCode(options.mouseButton);
            if(mouseButton > -1 && e.button !== mouseButton) {
                return;
            }

            if(that.altKey || e.which === 3 || that.callEvent('start', e) === false) {
                return;
            }

            var $children = that.$children = that.$.find(options.selector);
            $children.addClass('selectable-item');

            var clickBehavior = that.multiKey ? 'multi' : options.clickBehavior;
            if(clickBehavior === 'single') {
                that.unselect();
            }
            if (options.listenClick) {
                if(clickBehavior === 'multi') {
                    that.toggle(e.target);
                } else if(clickBehavior === 'single') {
                    that.select(e.target);
                } else if(clickBehavior === 'toggle') {
                    that.toggle(e.target, null, function(isSelect) {
                        that.unselect();
                    });
                }
            }

            if(that.callEvent('startDrag', e) === false) {
                that.callEvent('finish', {selections: that.selections, selected: that.getSelectedArray()});
                return;
            }

            startX = e.pageX;
            startY = e.pageY;

            $range = null;
            isIgnoreMove = true;
            isMouseDown = true;

            $(document).on('mousemove' + eventNamespace, mousemove).on('mouseup' + eventNamespace, mouseup);
            mouseDownBackEventCall = setTimeout(function() {
                $(document).on(mouseDownEventName, mouseup);
            }, 10);
            e.preventDefault();
        };

        var $container = options.container && options.container !== 'default' ? $(options.container) : this.$;
        if(options.trigger) {
            $container.on(mouseDownEventName, options.trigger, mousedown);
        } else {
            $container.on(mouseDownEventName, mousedown);
        }

        $(document).on('keydown', function(e) {
            var code = e.keyCode;
            if(code === 17 || code == 91) that.multiKey = code;
            else if(code === 18) that.altKey = true;
        }).on('keyup', function(e) {
            that.multiKey = false;
            that.altKey = false;
        });
    };

    // Call event helper
    Selectable.prototype.callEvent = function(name, params) {
        var event = $.Event(name + '.' + this.name);
        this.$.trigger(event, params);
        var result = event.result;
        var callback = this.options[name];
        if(typeof callback === 'function') {
            result = callback.apply(this, Array.isArray(params) ? params : [params]);
        }
        return result;
    };

    // Extense jquery element
    $.fn.selectable = function(option) {
        return this.each(function() {
            var $this = $(this);
            var data = $this.data(name);
            var options = typeof option == 'object' && option;

            if(!data) $this.data(name, (data = new Selectable(this, options)));

            if(typeof option == 'string') data[option]();
        });
    };

    $.fn.selectable.Constructor = Selectable;

    // Auto call selectable after document load complete
    $(function() {
        $('[data-ride="selectable"]').selectable();
    });
}(jQuery));
<|endoftext|>


<commit_before>import {isFunction, isUndefined, isNull} from '../helper/types.js';
import {result, assign} from '../helper/objects.js';
import {toCamelCase} from '../helper/converters.js';

/**
 * Get the value from an attribute.
 * @param {!HTMLElement} el an HTML element
 * @param {!string} attrName the name of the attribute
 * @param {!boolean} isBoolean true is the returned value should be a boolean
 * @returns {string|boolean}
 */
export function getAttValue(el, attrName, isBoolean) {
    if (isBoolean) {
        return el.hasAttribute(attrName);
    }
    return el.getAttribute(attrName);
}

/**
 * Set the value of an attribute.
 * @param {!HTMLElement} el an HTML element
 * @param {!string} attrName the name of the attribute
 * @param {!boolean} isBoolean true is the value should be a boolean
 * @param {string|boolean} value the value to set
 */
export function setAttValue(el, attrName, isBoolean, value) {
    if (isBoolean) {
        // Handle boolean value
        if (value && !el.hasAttribute(attrName)) {
            el.setAttribute(attrName, '');
        } else if (!value && el.hasAttribute(attrName)) {
            el.removeAttribute(attrName);
        }
    } else {
        // Handle none boolean value
        if ((isUndefined(value) || isNull(value)) && el.hasAttribute(attrName)) {
            // There is no value, so the attribute must be removed
            el.removeAttribute(attrName);
        } else if (!isUndefined(value) && !isNull(value) && el.getAttribute(attrName) !== value) {
            // Sync the attribute value with value
            el.setAttribute(attrName, value);
        }
    }
}

function getterFactory(attrName, isBoolean) {
    return function () {
        return getAttValue(this, attrName, isBoolean);
    };
}

function setterFactory(attrName, isBoolean, attSetter) {
    return function (value) {
        var attValue = isFunction(attSetter) ? attSetter.call(this, this, value) : value;
        return setAttValue(this, attrName, isBoolean, attValue);
    };
}

const DEFAULT_DATA = {
    bound: true,
    getterFactory,
    setterFactory,
    getAttValue: getAttValue,
    setAttValue: setAttValue
};

/**
 * The attribute builder.
 * Its goal is to provide a way to define an attribute.
 */
export class AttributeBuilder {

    /**
     * @param {!string} attrName the name of the attribute
     */
    constructor(attrName) {
        /**
         * @ignore
         */
        this.data = assign({
            attrName,
            propName: toCamelCase(attrName),
            listeners: []
        }, DEFAULT_DATA);
    }

    /**
     * To handle the attribute/property value as a boolean:
     * Attribute is present when true and missing when false.
     * @returns {AttributeBuilder} the builder
     */
    boolean() {
        this.data.boolean = true;
        return this;
    }

    /**
     * To hide the property name when using Object.keys().
     * @returns {PropertyBuilder} the builder
     */
    hidden() {
        this.data.enumerable = false;
        return this;
    }

    /**
     * To skip the link between the attribute and its property
     * @returns {AttributeBuilder} the builder
     */
    unbound() {
        this.data.bound = false;
        return this;
    }

    /**
     * To override the property name.
     * @param {!string} propName the property name
     * @returns {AttributeBuilder} the builder
     */
    property(propName) {
        this.data.propName = propName;
        return this;
    }

    /**
     * To set a default value.
     * @param {*} value the default value
     * @returns {PropertyBuilder} the builder
     */
    value(value) {
        this.data.value = value;
        return this;
    }

    /**
     * To be notified when the attribute is updated.
     * @param {function(el: HTMLElement, oldVal: string, newVal: string)} listener the listener function
     * @returns {AttributeBuilder} the builder
     */
    listen(listener) {
        this.data.listeners.push(listener);
        return this;
    }

    /**
     * Logic of the builder.
     * @param {!ElementBuilder.context.proto} proto the prototype
     * @param {!ElementBuilder.on} on the method on
     */
    build(proto, on) {
        let defaultValue = result(this.data, 'value'),
            descriptor = {
                enumerable: this.data.enumerable,
                configurable: false,
                get: this.data.getterFactory(this.data.attrName, this.data.boolean),
                set: this.data.setterFactory(this.data.attrName, this.data.boolean)
            };

        if (this.data.bound) {
            Object.defineProperty(proto, this.data.propName, descriptor);
        }

        on('after:createdCallback').invoke(el => {
            if (this.data.bound) {
                let attrValue = getAttValue(el, this.data.attrName, this.data.boolean);
                if (this.data.boolean) {
                    el[this.data.propName] = !!defaultValue ? defaultValue : attrValue;
                } else if (!isNull(attrValue) && !isUndefined(attrValue)) {
                    el[this.data.propName] = attrValue;
                } else if (!isUndefined(defaultValue)) {
                    el[this.data.propName] = defaultValue;
                }
            }
            if (this.data.listeners.length > 0) {
                let oldValue = this.data.boolean ? false : null;
                let setValue = el[this.data.propName];
                if (oldValue !== setValue) {
                    this.data.listeners.forEach(listener => listener.call(el, el, oldValue, setValue));
                }
            }
        });

        on('before:attributeChangedCallback').invoke((el, attName, oldVal, newVal) => {
            // Synchronize the attribute value with its properties
            if (attName === this.data.attrName) {
                if (this.data.bound) {
                    let newValue = this.data.boolean ? newVal === '' : newVal;
                    if (el[this.data.propName] !== newValue) {
                        el[this.data.propName] = newValue;
                    }
                }
                if (this.data.listeners.length > 0) {
                    let oldValue = this.data.boolean ? oldVal === '' : oldVal;
                    let setValue = this.data.boolean ? newVal === '' : newVal;
                    if (oldValue !== setValue) {
                        this.data.listeners.forEach(listener => listener.call(el, el, oldValue, setValue));
                    }
                }
            }
        });
    }

}

/**
 * Get a new attribute builder.
 * @param {!string} attrName the name of the attribute
 * @returns {AttributeBuilder} the attribute builder
 */
export function attribute(attrName) {
    return new AttributeBuilder(attrName);
}
<commit_msg>fix ci<commit_after>import {isFunction, isUndefined, isNull} from '../helper/types.js';
import {result, assign} from '../helper/objects.js';
import {toCamelCase} from '../helper/converters.js';

/**
 * Get the value from an attribute.
 * @param {!HTMLElement} el an HTML element
 * @param {!string} attrName the name of the attribute
 * @param {!boolean} isBoolean true is the returned value should be a boolean
 * @returns {string|boolean}
 */
export function getAttValue(el, attrName, isBoolean) {
    if (isBoolean) {
        return el.hasAttribute(attrName);
    }
    return el.getAttribute(attrName);
}

/**
 * Set the value of an attribute.
 * @param {!HTMLElement} el an HTML element
 * @param {!string} attrName the name of the attribute
 * @param {!boolean} isBoolean true is the value should be a boolean
 * @param {string|boolean} value the value to set
 */
export function setAttValue(el, attrName, isBoolean, value) {
    if (isBoolean) {
        // Handle boolean value
        if (value && !el.hasAttribute(attrName)) {
            el.setAttribute(attrName, '');
        } else if (!value && el.hasAttribute(attrName)) {
            el.removeAttribute(attrName);
        }
    } else {
        // Handle none boolean value
        if ((isUndefined(value) || isNull(value)) && el.hasAttribute(attrName)) {
            // There is no value, so the attribute must be removed
            el.removeAttribute(attrName);
        } else if (!isUndefined(value) && !isNull(value) && el.getAttribute(attrName) !== value) {
            // Sync the attribute value with value
            el.setAttribute(attrName, value);
        }
    }
}

function getterFactory(attrName, isBoolean) {
    return function () {
        return getAttValue(this, attrName, isBoolean);
    };
}

function setterFactory(attrName, isBoolean, attSetter) {
    return function (value) {
        var attValue = isFunction(attSetter) ? attSetter.call(this, this, value) : value;
        return setAttValue(this, attrName, isBoolean, attValue);
    };
}

const DEFAULT_DATA = {
    bound: true,
    getterFactory,
    setterFactory,
    getAttValue: getAttValue,
    setAttValue: setAttValue
};

/**
 * The attribute builder.
 * Its goal is to provide a way to define an attribute.
 */
export class AttributeBuilder {

    /**
     * @param {!string} attrName the name of the attribute
     */
    constructor(attrName) {
        /**
         * @ignore
         */
        this.data = assign({
            attrName,
            propName: toCamelCase(attrName),
            listeners: []
        }, DEFAULT_DATA);
    }

    /**
     * To handle the attribute/property value as a boolean:
     * Attribute is present when true and missing when false.
     * @returns {AttributeBuilder} the builder
     */
    boolean() {
        this.data.boolean = true;
        return this;
    }

    /**
     * To hide the property name when using Object.keys().
     * @returns {PropertyBuilder} the builder
     */
    hidden() {
        this.data.enumerable = false;
        return this;
    }

    /**
     * To skip the link between the attribute and its property
     * @returns {AttributeBuilder} the builder
     */
    unbound() {
        this.data.bound = false;
        return this;
    }

    /**
     * To override the property name.
     * @param {!string} propName the property name
     * @returns {AttributeBuilder} the builder
     */
    property(propName) {
        this.data.propName = propName;
        return this;
    }

    /**
     * To set a default value.
     * @param {*} value the default value
     * @returns {PropertyBuilder} the builder
     */
    value(value) {
        this.data.value = value;
        return this;
    }

    /**
     * To be notified when the attribute is updated.
     * @param {function(el: HTMLElement, oldVal: string, newVal: string)} listener the listener function
     * @returns {AttributeBuilder} the builder
     */
    listen(listener) {
        this.data.listeners.push(listener);
        return this;
    }

    /**
     * Logic of the builder.
     * @param {!ElementBuilder.context.proto} proto the prototype
     * @param {!ElementBuilder.on} on the method on
     */
    build(proto, on) {
        let data = this.data,
            defaultValue = result(this.data, 'value'),
            descriptor = {
                enumerable: this.data.enumerable,
                configurable: false,
                get: this.data.getterFactory(this.data.attrName, this.data.boolean),
                set: this.data.setterFactory(this.data.attrName, this.data.boolean)
            };

        if (data.bound) {
            Object.defineProperty(proto, data.propName, descriptor);
        }

        on('after:createdCallback').invoke(el => {
            if (data.bound) {
                let attrValue = getAttValue(el, data.attrName, data.boolean);
                if (data.boolean) {
                    el[data.propName] = !!defaultValue ? defaultValue : attrValue;
                } else if (!isNull(attrValue) && !isUndefined(attrValue)) {
                    el[data.propName] = attrValue;
                } else if (!isUndefined(defaultValue)) {
                    el[data.propName] = defaultValue;
                }
            }
            if (data.listeners.length > 0) {
                let oldValue = data.boolean ? false : null;
                let setValue = el[data.propName];
                if (oldValue !== setValue) {
                    data.listeners.forEach(listener => listener.call(el, el, oldValue, setValue));
                }
            }
        });

        on('before:attributeChangedCallback').invoke((el, attName, oldVal, newVal) => {
            // Synchronize the attribute value with its properties
            if (attName === data.attrName) {
                if (data.bound) {
                    let newValue = data.boolean ? newVal === '' : newVal;
                    if (el[data.propName] !== newValue) {
                        el[data.propName] = newValue;
                    }
                }
                if (data.listeners.length > 0) {
                    let oldValue = data.boolean ? oldVal === '' : oldVal;
                    let setValue = data.boolean ? newVal === '' : newVal;
                    if (oldValue !== setValue) {
                        data.listeners.forEach(listener => listener.call(el, el, oldValue, setValue));
                    }
                }
            }
        });
    }

}

/**
 * Get a new attribute builder.
 * @param {!string} attrName the name of the attribute
 * @returns {AttributeBuilder} the attribute builder
 */
export function attribute(attrName) {
    return new AttributeBuilder(attrName);
}
<|endoftext|>


<commit_before>import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes';

import {isAccelerator} from '../app/accelerators';

import fromCharCode from './utils/key-code';
import selection from './utils/selection';

hterm.defaultStorage = new lib.Storage.Memory();

// Provide selectAll to terminal viewport
hterm.Terminal.prototype.selectAll = function () {
  // If the cursorNode_ having hyperCaret we need to remove it
  if (this.cursorNode_.contains(this.hyperCaret)) {
    this.cursorNode_.removeChild(this.hyperCaret);
    // We need to clear the DOM range to reset anchorNode
    selection.clear(this);
    selection.all(this);
  }
};

// override double click behavior to copy
const oldMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) {
  if (e.type === 'dblclick') {
    selection.extend(this);
    console.log('[hyper+hterm] ignore double click');
    return;
  }
  return oldMouse.call(this, e);
};

function containsNonLatinCodepoints(s) {
  return /[^\u0000-\u00ff]/.test(s);
}

// hterm Unicode patch
hterm.TextAttributes.splitWidecharString = function (str) {
  const context = runes(str).reduce((ctx, rune) => {
    const code = rune.codePointAt(0);
    if (code < 128 || lib.wc.charWidth(code) === 1) {
      ctx.acc += rune;
      return ctx;
    }
    if (ctx.acc) {
      ctx.items.push({str: ctx.acc});
      ctx.acc = '';
    }
    ctx.items.push({str: rune, wcNode: true});
    return ctx;
  }, {items: [], acc: ''});
  if (context.acc) {
    context.items.push({str: context.acc});
  }
  return context.items;
};

// hterm Unicode patch
lib.wc.strWidth = function (str) {
  const chars = runes(str);
  let width = 0;
  let rv = 0;

  for (let i = 0; i < chars.length; i++) {
    const codePoint = chars[i].codePointAt(0);
    width = lib.wc.charWidth(codePoint);
    if (width < 0) {
      return -1;
    }
    rv += width * ((codePoint <= 0xffff) ? 1 : 2);
  }
  return rv;
};

// hterm Unicode patch
lib.wc.substr = function (str, start, optWidth) {
  const chars = runes(str);
  let startIndex;
  let endIndex;
  let width = 0;

  for (let i = 0; i < chars.length; i++) {
    const codePoint = chars[i].codePointAt(0);
    const charWidth = lib.wc.charWidth(codePoint);
    if ((width + charWidth) > start) {
      startIndex = i;
      break;
    }
    width += charWidth;
  }

  if (optWidth) {
    width = 0;
    for (endIndex = startIndex; endIndex < chars.length && width < optWidth; endIndex++) {
      width += lib.wc.charWidth(chars[endIndex].charCodeAt(0));
    }

    if (width > optWidth) {
      endIndex--;
    }
    return chars.slice(startIndex, endIndex).join('');
  }
  return chars.slice(startIndex).join('');
};

// MacOS emoji bar support
hterm.Keyboard.prototype.onTextInput_ = function (e) {
  if (!e.data) {
    return;
  }
  runes(e.data).forEach(this.terminal.onVTKeystroke.bind(this.terminal));
};

hterm.Terminal.IO.prototype.writeUTF8 = function (string) {
  if (this.terminal_.io !== this) {
    throw new Error('Attempt to print from inactive IO object.');
  }

  if (!containsNonLatinCodepoints(string)) {
    this.terminal_.interpret(string);
    return;
  }

  runes(string).forEach(rune => {
    this.terminal_.getTextAttributes().unicodeNode = containsNonLatinCodepoints(rune);
    this.terminal_.interpret(rune);
    this.terminal_.getTextAttributes().unicodeNode = false;
  });
};

const oldIsDefault = hterm.TextAttributes.prototype.isDefault;
hterm.TextAttributes.prototype.isDefault = function () {
  return !this.unicodeNode && oldIsDefault.call(this);
};

const oldSetFontSize = hterm.Terminal.prototype.setFontSize;
hterm.Terminal.prototype.setFontSize = function (px) {
  oldSetFontSize.call(this, px);
  const doc = this.getDocument();
  let unicodeNodeStyle = doc.getElementById('hyper-unicode-styles');
  if (!unicodeNodeStyle) {
    unicodeNodeStyle = doc.createElement('style');
    unicodeNodeStyle.setAttribute('id', 'hyper-unicode-styles');
    doc.head.appendChild(unicodeNodeStyle);
  }
  unicodeNodeStyle.innerHTML = `
    .unicode-node {
      display: inline-block;
      vertical-align: top;
      width: ${this.scrollPort_.characterSize.width}px;
    }
  `;
};

const oldCreateContainer = hterm.TextAttributes.prototype.createContainer;
hterm.TextAttributes.prototype.createContainer = function (text) {
  const container = oldCreateContainer.call(this, text);
  if (container.style && runes(text).length === 1 && containsNonLatinCodepoints(text)) {
    container.className += ' unicode-node';
  }
  return container;
};

// Do not match containers when one of them has unicode text (unicode chars need to be alone in their containers)
const oldMatchesContainer = hterm.TextAttributes.prototype.matchesContainer;
hterm.TextAttributes.prototype.matchesContainer = function (obj) {
  return oldMatchesContainer.call(this, obj) &&
    !this.unicodeNode &&
    !containsNonLatinCodepoints(obj.textContent);
};

// there's no option to turn off the size overlay
hterm.Terminal.prototype.overlaySize = function () {};

// fixing a bug in hterm where a double click triggers
// a non-collapsed selection whose text is '', and results
// in an infinite copy loop
hterm.Terminal.prototype.copySelectionToClipboard = function () {
  const text = this.getSelectionText();
  if (text) {
    this.copyStringToClipboard(text);
  }
};

let lastEventTimeStamp;
let lastEventKey;
// passthrough all the commands that are meant to control
// hyper and not the terminal itself
const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
  const modifierKeysConf = this.terminal.modifierKeys;
  if (e.timeStamp === lastEventTimeStamp && e.key === lastEventKey) {
    // Event was already processed.
    // It seems to occur after a char composition ended by Tab and cause a blur.
    // See https://github.com/zeit/hyper/issues/1341
    e.preventDefault();
    return;
  }
  lastEventTimeStamp = e.timeStamp;
  lastEventKey = e.key;

  if (e.altKey &&
      e.which !== 16 && // Ignore other modifer keys
      e.which !== 17 &&
      e.which !== 18 &&
      e.which !== 91 &&
      modifierKeysConf.altIsMeta) {
    const char = fromCharCode(e);
    this.terminal.onVTKeystroke('\x1b' + char);
    e.preventDefault();
  }

  if (e.metaKey &&
      e.code !== 'MetaLeft' &&
      e.code !== 'MetaRight' &&
      e.which !== 16 &&
      e.which !== 17 &&
      e.which !== 18 &&
      e.which !== 91 &&
      modifierKeysConf.cmdIsMeta) {
    const char = fromCharCode(e);
    this.terminal.onVTKeystroke('\x1b' + char);
    e.preventDefault();
  }

  if (e.altKey || e.metaKey || isAccelerator(e)) {
    // hterm shouldn't consume a hyper accelerator
    // // Was the hyperCaret removed for selectAll
    if (e.key === 'v' && !this.terminal.cursorNode_.contains(this.hyperCaret)) {
      this.terminal.focusHyperCaret();
    }
    return;
  }

  // Test for valid keys in order to accept clear status
  if (e.code !== 'Control' && e.key !== 'Shift' && e.code !== 'CapsLock' && e.key !== 'Dead') {
    selection.clear(this.terminal);
  }

  // Was the hyperCaret removed for selectAll
  if (!this.terminal.cursorNode_.contains(this.hyperCaret)) {
    this.terminal.focusHyperCaret();
  }
  return oldKeyDown.call(this, e);
};

// we re-implement `wipeContents` to preserve the line
// and cursor position that the client is in.
// otherwise the user ends up with a completely clear
// screen which is really strange
hterm.Terminal.prototype.clearPreserveCursorRow = function () {
  this.scrollbackRows_.length = 0;
  this.scrollPort_.resetCache();

  [this.primaryScreen_, this.alternateScreen_].forEach(screen => {
    const bottom = screen.getHeight();
    if (bottom > 0) {
      this.renumberRows_(0, bottom);

      const x = screen.cursorPosition.column;
      const y = screen.cursorPosition.row;

      if (x === 0) {
        // Empty screen, nothing to do.
        return;
      }

      // here we move the row that the user was focused on
      // to the top of the screen
      this.moveRows_(y, 1, 0);

      for (let i = 1; i < bottom; i++) {
        screen.setCursorPosition(i, 0);
        screen.clearCursorRow();
      }

      // we restore the cursor position
      screen.setCursorPosition(0, x);
    }
  });

  this.syncCursorPosition_();
  this.scrollPort_.invalidate();

  // this will avoid a bug where the `wipeContents`
  // hterm API doesn't send the scroll to the top
  this.scrollPort_.redraw_();
};

const oldOnMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) {
  // override `preventDefault` to not actually
  // prevent default when the type of event is
  // mousedown, so that we can still trigger
  // focus on the terminal when the underlying
  // VT is interested in mouse events, as is the
  // case of programs like `vtop` that allow for
  // the user to click on rows
  if (e.type === 'mousedown') {
    e.preventDefault = function () { };
  }

  return oldOnMouse.call(this, e);
};

// make background transparent to avoid transparency issues
hterm.ScrollPort.prototype.setBackgroundColor = function () {
  this.screen_.style.backgroundColor = 'transparent';
};

// will be called by the <Term/> right after the `hterm.Terminal` is instantiated
hterm.Terminal.prototype.onHyperCaret = function (caret) {
  this.hyperCaret = caret;
  let ongoingComposition = false;

  caret.addEventListener('compositionstart', () => {
    ongoingComposition = true;
  });

  // we can ignore `compositionstart` since chromium always fire it with ''
  caret.addEventListener('compositionupdate', () => {
    this.cursorNode_.style.backgroundColor = 'yellow';
    this.cursorNode_.style.borderColor = 'yellow';
  });

  // at this point the char(s) is ready
  caret.addEventListener('compositionend', () => {
    ongoingComposition = false;
    this.cursorNode_.style.backgroundColor = '';
    this.setCursorShape(this.getCursorShape());
    this.cursorNode_.style.borderColor = this.getCursorColor();
    caret.innerText = '';
  });

  // if you open the `Emoji & Symbols` (ctrl+cmd+space)
  // and select an emoji, it'll be inserted into our caret
  // and stay there until you star a compositon event.
  // to avoid that, we'll just check if there's an ongoing
  // compostion event. if there's one, we do nothing.
  // otherwise, we just remove the emoji and stop the event
  // propagation.
  // PS: this event will *not* be fired when a standard char
  // (a, b, c, 1, 2, 3, etc) is typed – only for composed
  // ones and `Emoji & Symbols`
  caret.addEventListener('input', e => {
    if (!ongoingComposition) {
      caret.innerText = '';
      e.stopPropagation();
      e.preventDefault();
    }
  });

  // we need to capture pastes, prevent them and send its contents to the terminal
  caret.addEventListener('paste', e => {
    e.stopPropagation();
    e.preventDefault();
    const text = e.clipboardData.getData('text');
    this.onVTKeystroke(text);
  });

  // here we replicate the focus/blur state of our caret on the `hterm` caret
  caret.addEventListener('focus', () => {
    this.cursorNode_.setAttribute('focus', true);
    this.restyleCursor_();
  });
  caret.addEventListener('blur', () => {
    this.cursorNode_.setAttribute('focus', false);
    this.restyleCursor_();
  });

  // this is necessary because we need to access the `document_` and the hyperCaret
  // on `hterm.Screen.prototype.syncSelectionCaret`
  this.primaryScreen_.terminal = this;
  this.alternateScreen_.terminal = this;
};

// ensure that our contenteditable caret is injected
// inside the term's cursor node and that it's focused
hterm.Terminal.prototype.focusHyperCaret = function () {
  if (!this.hyperCaret.parentNode !== this.cursorNode_) {
    this.cursorNode_.appendChild(this.hyperCaret);
  }
  this.hyperCaret.focus();
};

hterm.Screen.prototype.syncSelectionCaret = function () {
  const p = this.terminal.hyperCaret;
  const doc = this.terminal.document_;
  const win = doc.defaultView;
  const s = win.getSelection();
  const r = doc.createRange();
  r.selectNodeContents(p);
  s.removeAllRanges();
  s.addRange(r);
};

// fixes a bug in hterm, where the cursor goes back to `BLOCK`
// after the bell rings
const oldRingBell = hterm.Terminal.prototype.ringBell;
hterm.Terminal.prototype.ringBell = function () {
  oldRingBell.call(this);
  setTimeout(() => {
    this.restyleCursor_();
  }, 200);
};

// fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb
lib.colors.hexToRGB = function (arg) {
  const hex16 = lib.colors.re_.hex16;
  const hex24 = lib.colors.re_.hex24;

  function convert(hex) {
    if (hex.length === 4) {
      hex = hex.replace(hex16, (h, r, g, b) => {
        return '#' + r + r + g + g + b + b;
      });
    }
    const ary = hex.match(hex24);
    if (!ary) {
      return null;
    }

    return 'rgb(' +
      parseInt(ary[1], 16) + ', ' +
      parseInt(ary[2], 16) + ', ' +
      parseInt(ary[3], 16) +
    ')';
  }

  if (arg instanceof Array) {
    for (let i = 0; i < arg.length; i++) {
      arg[i] = convert(arg[i]);
    }
  } else {
    arg = convert(arg);
  }

  return arg;
};

// add support for cursor styles 5 and 6, fixes #270
hterm.VT.CSI[' q'] = function (parseState) {
  const arg = parseState.args[0];
  if (arg === '0' || arg === '1') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
    this.terminal.setCursorBlink(true);
  } else if (arg === '2') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
    this.terminal.setCursorBlink(false);
  } else if (arg === '3') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
    this.terminal.setCursorBlink(true);
  } else if (arg === '4') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
    this.terminal.setCursorBlink(false);
  } else if (arg === '5') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
    this.terminal.setCursorBlink(true);
  } else if (arg === '6') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
    this.terminal.setCursorBlink(false);
  } else {
    console.warn('Unknown cursor style: ' + arg);
  }
};

export default hterm;
export {lib};
<commit_msg>:bug: Focus the current tab after switch from a new one – closes #1273<commit_after>import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes';

import {isAccelerator} from '../app/accelerators';

import fromCharCode from './utils/key-code';
import selection from './utils/selection';

hterm.defaultStorage = new lib.Storage.Memory();

// Provide selectAll to terminal viewport
hterm.Terminal.prototype.selectAll = function () {
  // If the cursorNode_ having hyperCaret we need to remove it
  if (this.cursorNode_.contains(this.hyperCaret)) {
    this.cursorNode_.removeChild(this.hyperCaret);
    // We need to clear the DOM range to reset anchorNode
    selection.clear(this);
    selection.all(this);
  }
};

// override double click behavior to copy
const oldMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) {
  if (e.type === 'dblclick') {
    selection.extend(this);
    console.log('[hyper+hterm] ignore double click');
    return;
  }
  return oldMouse.call(this, e);
};

function containsNonLatinCodepoints(s) {
  return /[^\u0000-\u00ff]/.test(s);
}

// hterm Unicode patch
hterm.TextAttributes.splitWidecharString = function (str) {
  const context = runes(str).reduce((ctx, rune) => {
    const code = rune.codePointAt(0);
    if (code < 128 || lib.wc.charWidth(code) === 1) {
      ctx.acc += rune;
      return ctx;
    }
    if (ctx.acc) {
      ctx.items.push({str: ctx.acc});
      ctx.acc = '';
    }
    ctx.items.push({str: rune, wcNode: true});
    return ctx;
  }, {items: [], acc: ''});
  if (context.acc) {
    context.items.push({str: context.acc});
  }
  return context.items;
};

// hterm Unicode patch
lib.wc.strWidth = function (str) {
  const chars = runes(str);
  let width = 0;
  let rv = 0;

  for (let i = 0; i < chars.length; i++) {
    const codePoint = chars[i].codePointAt(0);
    width = lib.wc.charWidth(codePoint);
    if (width < 0) {
      return -1;
    }
    rv += width * ((codePoint <= 0xffff) ? 1 : 2);
  }
  return rv;
};

// hterm Unicode patch
lib.wc.substr = function (str, start, optWidth) {
  const chars = runes(str);
  let startIndex;
  let endIndex;
  let width = 0;

  for (let i = 0; i < chars.length; i++) {
    const codePoint = chars[i].codePointAt(0);
    const charWidth = lib.wc.charWidth(codePoint);
    if ((width + charWidth) > start) {
      startIndex = i;
      break;
    }
    width += charWidth;
  }

  if (optWidth) {
    width = 0;
    for (endIndex = startIndex; endIndex < chars.length && width < optWidth; endIndex++) {
      width += lib.wc.charWidth(chars[endIndex].charCodeAt(0));
    }

    if (width > optWidth) {
      endIndex--;
    }
    return chars.slice(startIndex, endIndex).join('');
  }
  return chars.slice(startIndex).join('');
};

// MacOS emoji bar support
hterm.Keyboard.prototype.onTextInput_ = function (e) {
  if (!e.data) {
    return;
  }
  runes(e.data).forEach(this.terminal.onVTKeystroke.bind(this.terminal));
};

hterm.Terminal.IO.prototype.writeUTF8 = function (string) {
  if (this.terminal_.io !== this) {
    throw new Error('Attempt to print from inactive IO object.');
  }

  if (!containsNonLatinCodepoints(string)) {
    this.terminal_.interpret(string);
    return;
  }

  runes(string).forEach(rune => {
    this.terminal_.getTextAttributes().unicodeNode = containsNonLatinCodepoints(rune);
    this.terminal_.interpret(rune);
    this.terminal_.getTextAttributes().unicodeNode = false;
  });
};

const oldIsDefault = hterm.TextAttributes.prototype.isDefault;
hterm.TextAttributes.prototype.isDefault = function () {
  return !this.unicodeNode && oldIsDefault.call(this);
};

const oldSetFontSize = hterm.Terminal.prototype.setFontSize;
hterm.Terminal.prototype.setFontSize = function (px) {
  oldSetFontSize.call(this, px);
  const doc = this.getDocument();
  let unicodeNodeStyle = doc.getElementById('hyper-unicode-styles');
  if (!unicodeNodeStyle) {
    unicodeNodeStyle = doc.createElement('style');
    unicodeNodeStyle.setAttribute('id', 'hyper-unicode-styles');
    doc.head.appendChild(unicodeNodeStyle);
  }
  unicodeNodeStyle.innerHTML = `
    .unicode-node {
      display: inline-block;
      vertical-align: top;
      width: ${this.scrollPort_.characterSize.width}px;
    }
  `;
};

const oldCreateContainer = hterm.TextAttributes.prototype.createContainer;
hterm.TextAttributes.prototype.createContainer = function (text) {
  const container = oldCreateContainer.call(this, text);
  if (container.style && runes(text).length === 1 && containsNonLatinCodepoints(text)) {
    container.className += ' unicode-node';
  }
  return container;
};

// Do not match containers when one of them has unicode text (unicode chars need to be alone in their containers)
const oldMatchesContainer = hterm.TextAttributes.prototype.matchesContainer;
hterm.TextAttributes.prototype.matchesContainer = function (obj) {
  return oldMatchesContainer.call(this, obj) &&
    !this.unicodeNode &&
    !containsNonLatinCodepoints(obj.textContent);
};

// there's no option to turn off the size overlay
hterm.Terminal.prototype.overlaySize = function () {};

// fixing a bug in hterm where a double click triggers
// a non-collapsed selection whose text is '', and results
// in an infinite copy loop
hterm.Terminal.prototype.copySelectionToClipboard = function () {
  const text = this.getSelectionText();
  if (text) {
    this.copyStringToClipboard(text);
  }
};

let lastEventTimeStamp;
let lastEventKey;
// passthrough all the commands that are meant to control
// hyper and not the terminal itself
const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
  const modifierKeysConf = this.terminal.modifierKeys;
  if (e.timeStamp === lastEventTimeStamp && e.key === lastEventKey) {
    // Event was already processed.
    // It seems to occur after a char composition ended by Tab and cause a blur.
    // See https://github.com/zeit/hyper/issues/1341
    e.preventDefault();
    return;
  }
  lastEventTimeStamp = e.timeStamp;
  lastEventKey = e.key;

  if (e.altKey &&
      e.which !== 16 && // Ignore other modifer keys
      e.which !== 17 &&
      e.which !== 18 &&
      e.which !== 91 &&
      modifierKeysConf.altIsMeta) {
    const char = fromCharCode(e);
    this.terminal.onVTKeystroke('\x1b' + char);
    e.preventDefault();
  }

  if (e.metaKey &&
      e.code !== 'MetaLeft' &&
      e.code !== 'MetaRight' &&
      e.which !== 16 &&
      e.which !== 17 &&
      e.which !== 18 &&
      e.which !== 91 &&
      modifierKeysConf.cmdIsMeta) {
    const char = fromCharCode(e);
    this.terminal.onVTKeystroke('\x1b' + char);
    e.preventDefault();
  }

  if (e.altKey || e.metaKey || isAccelerator(e)) {
    // hterm shouldn't consume a hyper accelerator
    // // Was the hyperCaret removed for selectAll
    if (e.key === 'v' && !this.terminal.cursorNode_.contains(this.hyperCaret)) {
      this.terminal.focusHyperCaret();
    }
    return;
  }

  // Test for valid keys in order to accept clear status
  if (e.code !== 'Control' && e.key !== 'Shift' && e.code !== 'CapsLock' && e.key !== 'Dead') {
    selection.clear(this.terminal);
  }

  // Was the hyperCaret removed for selectAll
  if (!this.terminal.cursorNode_.contains(this.hyperCaret)) {
    this.terminal.focusHyperCaret();
  }
  return oldKeyDown.call(this, e);
};

// we re-implement `wipeContents` to preserve the line
// and cursor position that the client is in.
// otherwise the user ends up with a completely clear
// screen which is really strange
hterm.Terminal.prototype.clearPreserveCursorRow = function () {
  this.scrollbackRows_.length = 0;
  this.scrollPort_.resetCache();

  [this.primaryScreen_, this.alternateScreen_].forEach(screen => {
    const bottom = screen.getHeight();
    if (bottom > 0) {
      this.renumberRows_(0, bottom);

      const x = screen.cursorPosition.column;
      const y = screen.cursorPosition.row;

      if (x === 0) {
        // Empty screen, nothing to do.
        return;
      }

      // here we move the row that the user was focused on
      // to the top of the screen
      this.moveRows_(y, 1, 0);

      for (let i = 1; i < bottom; i++) {
        screen.setCursorPosition(i, 0);
        screen.clearCursorRow();
      }

      // we restore the cursor position
      screen.setCursorPosition(0, x);
    }
  });

  this.syncCursorPosition_();
  this.scrollPort_.invalidate();

  // this will avoid a bug where the `wipeContents`
  // hterm API doesn't send the scroll to the top
  this.scrollPort_.redraw_();
};

const oldOnMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) {
  // override `preventDefault` to not actually
  // prevent default when the type of event is
  // mousedown, so that we can still trigger
  // focus on the terminal when the underlying
  // VT is interested in mouse events, as is the
  // case of programs like `vtop` that allow for
  // the user to click on rows
  if (e.type === 'mousedown') {
    e.preventDefault = function () { };
  }

  return oldOnMouse.call(this, e);
};

// make background transparent to avoid transparency issues
hterm.ScrollPort.prototype.setBackgroundColor = function () {
  this.screen_.style.backgroundColor = 'transparent';
};

// will be called by the <Term/> right after the `hterm.Terminal` is instantiated
hterm.Terminal.prototype.onHyperCaret = function (caret) {
  this.hyperCaret = caret;
  let ongoingComposition = false;

  caret.addEventListener('compositionstart', () => {
    ongoingComposition = true;
  });

  // we can ignore `compositionstart` since chromium always fire it with ''
  caret.addEventListener('compositionupdate', () => {
    this.cursorNode_.style.backgroundColor = 'yellow';
    this.cursorNode_.style.borderColor = 'yellow';
  });

  // at this point the char(s) is ready
  caret.addEventListener('compositionend', () => {
    ongoingComposition = false;
    this.cursorNode_.style.backgroundColor = '';
    this.setCursorShape(this.getCursorShape());
    this.cursorNode_.style.borderColor = this.getCursorColor();
    caret.innerText = '';
  });

  // if you open the `Emoji & Symbols` (ctrl+cmd+space)
  // and select an emoji, it'll be inserted into our caret
  // and stay there until you star a compositon event.
  // to avoid that, we'll just check if there's an ongoing
  // compostion event. if there's one, we do nothing.
  // otherwise, we just remove the emoji and stop the event
  // propagation.
  // PS: this event will *not* be fired when a standard char
  // (a, b, c, 1, 2, 3, etc) is typed – only for composed
  // ones and `Emoji & Symbols`
  caret.addEventListener('input', e => {
    if (!ongoingComposition) {
      caret.innerText = '';
      e.stopPropagation();
      e.preventDefault();
    }
  });

  // we need to capture pastes, prevent them and send its contents to the terminal
  caret.addEventListener('paste', e => {
    e.stopPropagation();
    e.preventDefault();
    const text = e.clipboardData.getData('text');
    this.onVTKeystroke(text);
  });

  // here we replicate the focus/blur state of our caret on the `hterm` caret
  caret.addEventListener('focus', () => {
    this.cursorNode_.setAttribute('focus', true);
    this.restyleCursor_();
  });
  caret.addEventListener('blur', () => {
    this.cursorNode_.setAttribute('focus', false);
    this.restyleCursor_();
  });

  // this is necessary because we need to access the `document_` and the hyperCaret
  // on `hterm.Screen.prototype.syncSelectionCaret`
  this.primaryScreen_.terminal = this;
  this.alternateScreen_.terminal = this;
};

// ensure that our contenteditable caret is injected
// inside the term's cursor node and that it's focused
hterm.Terminal.prototype.focusHyperCaret = function () {
  if (!this.hyperCaret.parentNode !== this.cursorNode_) {
    this.cursorNode_.appendChild(this.hyperCaret);
  }
  this.hyperCaret.focus();
};

hterm.Screen.prototype.syncSelectionCaret = function () {
  const p = this.terminal.hyperCaret;
  const doc = this.terminal.document_;
  const win = doc.defaultView;
  const s = win.getSelection();
  const r = doc.createRange();
  r.selectNodeContents(p);
  s.removeAllRanges();
  s.addRange(r);
};

const oldScrollPortFocus = hterm.ScrollPort.prototype.focus;
hterm.ScrollPort.prototype.focus = function () {
  oldScrollPortFocus();
  this.terminal.focusHyperCaret();
};

// fixes a bug in hterm, where the cursor goes back to `BLOCK`
// after the bell rings
const oldRingBell = hterm.Terminal.prototype.ringBell;
hterm.Terminal.prototype.ringBell = function () {
  oldRingBell.call(this);
  setTimeout(() => {
    this.restyleCursor_();
  }, 200);
};

// fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb
lib.colors.hexToRGB = function (arg) {
  const hex16 = lib.colors.re_.hex16;
  const hex24 = lib.colors.re_.hex24;

  function convert(hex) {
    if (hex.length === 4) {
      hex = hex.replace(hex16, (h, r, g, b) => {
        return '#' + r + r + g + g + b + b;
      });
    }
    const ary = hex.match(hex24);
    if (!ary) {
      return null;
    }

    return 'rgb(' +
      parseInt(ary[1], 16) + ', ' +
      parseInt(ary[2], 16) + ', ' +
      parseInt(ary[3], 16) +
    ')';
  }

  if (arg instanceof Array) {
    for (let i = 0; i < arg.length; i++) {
      arg[i] = convert(arg[i]);
    }
  } else {
    arg = convert(arg);
  }

  return arg;
};

// add support for cursor styles 5 and 6, fixes #270
hterm.VT.CSI[' q'] = function (parseState) {
  const arg = parseState.args[0];
  if (arg === '0' || arg === '1') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
    this.terminal.setCursorBlink(true);
  } else if (arg === '2') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
    this.terminal.setCursorBlink(false);
  } else if (arg === '3') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
    this.terminal.setCursorBlink(true);
  } else if (arg === '4') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
    this.terminal.setCursorBlink(false);
  } else if (arg === '5') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
    this.terminal.setCursorBlink(true);
  } else if (arg === '6') {
    this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
    this.terminal.setCursorBlink(false);
  } else {
    console.warn('Unknown cursor style: ' + arg);
  }
};

export default hterm;
export {lib};
<|endoftext|>


<commit_before>'use strict';

var gulp = require('gulp'),
  lazypipe = require('lazypipe'),
  runSequence = require('run-sequence'),
  bump = require('gulp-bump'),
  watch = require('gulp-watch'),
  karma = require('gulp-karma'),
  express = require('gulp-express'),
  exit = require('gulp-exit'),
  mocha = require('gulp-mocha'),
  git = require('gulp-git'),
  tagVersion = require('gulp-tag-version'),
  jshint = require('gulp-jshint');

function getJSHintPipe(rc) {
  return lazypipe()
    .pipe(jshint, rc || '.jshintrc')
    .pipe(jshint.reporter, 'jshint-stylish')
    .pipe(jshint.reporter, 'fail');
}

function jsSourcePipe() {
  return gulp.src('src/**/*.js');
}

function karmaPipe(action) {
  express.run({
    file: 'test/spec/server.js'
  });

  return gulp.src('test/spec/**/*.js')
    .pipe(karma({
      configFile: 'karma.conf.js',
      action: action
    })).on('error', function(err) {
      throw err;
    });
}

gulp.task('karma:watch', function() {
  return karmaPipe('watch');
});

gulp.task('karma', function() {
  return karmaPipe('run')
    .pipe(exit());
});

gulp.task('jshint', ['jshint:src', 'jshint:test', 'jshint:gulpfile']);

gulp.task('jshint:src', function() {
  return jsSourcePipe()
    .pipe(getJSHintPipe()());
});

gulp.task('jshint:test', function() {
  return gulp.src('test/**/*.js')
    .pipe(getJSHintPipe('test/.jshintrc')());
});

gulp.task('jshint:gulpfile', function() {
  return gulp.src('gulpfile.js')
    .pipe(getJSHintPipe()());
});

gulp.task('test:watch', function() {
  gulp.watch(['test/*.js', 'src/**/*.js'], ['test']);
});

gulp.task('bump', function() {
  return gulp.src('./*.json')
    .pipe(bump({ type: gulp.env.type || 'patch' }))
    .pipe(gulp.dest('./'));
});

gulp.task('bump-commit', function() {
  var version = require('./package.json').version;
  return gulp.src(['dist/*.js','./*.json'])
    .pipe(git.commit('Release v' + version));
});

gulp.task('tag', function() {
  return gulp.src('package.json')
    .pipe(tagVersion());
});

gulp.task('release', function(cb) {
  runSequence(
    'bump',
    'bump-commit',
    'tag',
    cb
  );
});

gulp.task('default', function() {
  return runSequence('jshint', 'karma');
});
<commit_msg>Remove last lingering reference to dist/<commit_after>'use strict';

var gulp = require('gulp'),
  lazypipe = require('lazypipe'),
  runSequence = require('run-sequence'),
  bump = require('gulp-bump'),
  watch = require('gulp-watch'),
  karma = require('gulp-karma'),
  express = require('gulp-express'),
  exit = require('gulp-exit'),
  mocha = require('gulp-mocha'),
  git = require('gulp-git'),
  tagVersion = require('gulp-tag-version'),
  jshint = require('gulp-jshint');

function getJSHintPipe(rc) {
  return lazypipe()
    .pipe(jshint, rc || '.jshintrc')
    .pipe(jshint.reporter, 'jshint-stylish')
    .pipe(jshint.reporter, 'fail');
}

function jsSourcePipe() {
  return gulp.src('src/**/*.js');
}

function karmaPipe(action) {
  express.run({
    file: 'test/spec/server.js'
  });

  return gulp.src('test/spec/**/*.js')
    .pipe(karma({
      configFile: 'karma.conf.js',
      action: action
    })).on('error', function(err) {
      throw err;
    });
}

gulp.task('karma:watch', function() {
  return karmaPipe('watch');
});

gulp.task('karma', function() {
  return karmaPipe('run')
    .pipe(exit());
});

gulp.task('jshint', ['jshint:src', 'jshint:test', 'jshint:gulpfile']);

gulp.task('jshint:src', function() {
  return jsSourcePipe()
    .pipe(getJSHintPipe()());
});

gulp.task('jshint:test', function() {
  return gulp.src('test/**/*.js')
    .pipe(getJSHintPipe('test/.jshintrc')());
});

gulp.task('jshint:gulpfile', function() {
  return gulp.src('gulpfile.js')
    .pipe(getJSHintPipe()());
});

gulp.task('test:watch', function() {
  gulp.watch(['test/*.js', 'src/**/*.js'], ['test']);
});

gulp.task('bump', function() {
  return gulp.src('./*.json')
    .pipe(bump({ type: gulp.env.type || 'patch' }))
    .pipe(gulp.dest('./'));
});

gulp.task('bump-commit', function() {
  var version = require('./package.json').version;
  return gulp.src(['./*.json'])
    .pipe(git.commit('Release v' + version));
});

gulp.task('tag', function() {
  return gulp.src('package.json')
    .pipe(tagVersion());
});

gulp.task('release', function(cb) {
  runSequence(
    'bump',
    'bump-commit',
    'tag',
    cb
  );
});

gulp.task('default', function() {
  return runSequence('jshint', 'karma');
});
<|endoftext|>


<commit_before>/**

  Watches files and updates compilers, renderers and handlers

  • addFile
  • removeFile
  • setup (exports)

**/

/**

  Dependencies and exports

**/

var Compiler = require("./compiler");
var Handler = require("./handler");

module.exports = setup;

/**

  Creates compilation pipeline for a file, adds route handlers and global lookup.

  @param {Router} router Parent router
  @param {File} file File information as returned by Utils#parse
  @param {String} [options.url] The base URL for the route
  @param {String} [options.type] The file type (html, js, css)

**/

function addFile(router, file, options) {

  // Stop-gap method to identify bundles in real-time
  if ("js" === options.type && !file.bundle && Utils.isIndexFile(file))
    file.bundle = true;

  options = options || {};
  options.url = options.url || "/";

  // Add to list of extensions if not present
  // !!! TODO pipeline from ext
  var pipeline;
  var source = { file: file };
  var renderer = Compiler.createRenderer(id, source, options.type, pipeline);
  var handler = Handler.createHandler(renderer);

  route = Utils.makeRoute(options.url, file, options);
  router.get(route, handler);

  // Add to assets (used in templating, as well as asset pipelines)
  if (!Utils.isHTML(options.type))
    global.shipp.assets[Utils.makeKey(route)] = route;

}

/**

  Given a file, removes corresponding route handler and global assets.

  @param {Router} router Parent router
  @param {File} file File information as returned by Utils#parse
  @param {String} [options.url] The base URL for the route
  @param {String} [options.type] The file type (html, js, css)

**/

function removeFile(router, file, options) {

  var route;

  options = options || {};
  options.url = options.url || "/";

  route = Utils.makeRoute(options.url, file, options);

  // Remove previous route
  Utils.removeRoute(router, route);

  // Delete from assets (used in templating, as well as asset pipelines)
  if (!Utils.isHTML(options.type))
    delete global.shipp.assets[Utils.makeKey(route)];

}

/**

  Creates compilation pipelines for files and add route handlers.

  @param {String} options.exts The default file extension to apply
  @param {String} options.path Directory to process
  @param {String} options.url Base url to route from: url structure = base + directory structure
  @param {Boolean} [options.bundleFolders] If true, applies folder bundling
  @param {Boolean} [options.recursive] If true, processes the directory recursively (default: true)
  @returns {Router} An express-style router

**/

function setup(options) {

  var router  = global.shipp.router(),
      ignored = [],
      type    = Array.isArray(options.exts) ? options.exts[0] : options.exts;

  // Since we can have multiple exts, we attach the type to the options object
  options.type = type;

  Utils.getFiles(options.path, options).forEach(function(file) {

    // Handle the file
    addFile(router, file, { type: type, url: options.url });

    // Remove ignored directories from watch list
    if (file.ignored) ignored = ignored.concat(file.ignored);

  });

  Utils.watch(options.path, "*", {

    chokidar: {
      ignoreInitial : true,
      ignored: ignored,
    },

    type: type,

    add: function(file) {
      addFile(router, Utils.parse(file, options.path), { type: type, url: options.url });
    },

    change: function(file) {

      var file = Utils.parse(file, options.path),
          route = Utils.makeRoute(options.url, file, { type: type });

      // Monitor metadata only for HTML-files
      // We choose not to store and diff metadata as the add/remove process is
      // extremely fast (circa 1ms).
      if ("html" === options.type) {
        removeFile(router, file, { type: type, url: options.url });
        addFile(router, file, { type: type, url: options.url });
      }

      global.shipp.emit("route:refresh", { route: route });

    },

    unlink: function(file) {
      removeFile(router, Utils.parse(file, options.path), { type: type, url: options.url });
    }

  });

  return router;

};
<commit_msg>Files: adds missing Utils dependency<commit_after>/**

  Watches files and updates compilers, renderers and handlers

  • addFile
  • removeFile
  • setup (exports)

**/

/**

  Dependencies and exports

**/

var Compiler = require("./compiler");
var Handler = require("./handler");
var Utils = require("./utils");

module.exports = setup;

/**

  Creates compilation pipeline for a file, adds route handlers and global lookup.

  @param {Router} router Parent router
  @param {File} file File information as returned by Utils#parse
  @param {String} [options.url] The base URL for the route
  @param {String} [options.type] The file type (html, js, css)

**/

function addFile(router, file, options) {

  // Stop-gap method to identify bundles in real-time
  if ("js" === options.type && !file.bundle && Utils.isIndexFile(file))
    file.bundle = true;

  options = options || {};
  options.url = options.url || "/";

  // Add to list of extensions if not present
  // !!! TODO pipeline from ext
  var pipeline;
  var source = { file: file };
  var renderer = Compiler.createRenderer(id, source, options.type, pipeline);
  var handler = Handler.createHandler(renderer);

  route = Utils.makeRoute(options.url, file, options);
  router.get(route, handler);

  // Add to assets (used in templating, as well as asset pipelines)
  if (!Utils.isHTML(options.type))
    global.shipp.assets[Utils.makeKey(route)] = route;

}

/**

  Given a file, removes corresponding route handler and global assets.

  @param {Router} router Parent router
  @param {File} file File information as returned by Utils#parse
  @param {String} [options.url] The base URL for the route
  @param {String} [options.type] The file type (html, js, css)

**/

function removeFile(router, file, options) {

  var route;

  options = options || {};
  options.url = options.url || "/";

  route = Utils.makeRoute(options.url, file, options);

  // Remove previous route
  Utils.removeRoute(router, route);

  // Delete from assets (used in templating, as well as asset pipelines)
  if (!Utils.isHTML(options.type))
    delete global.shipp.assets[Utils.makeKey(route)];

}

/**

  Creates compilation pipelines for files and add route handlers.

  @param {String} options.exts The default file extension to apply
  @param {String} options.path Directory to process
  @param {String} options.url Base url to route from: url structure = base + directory structure
  @param {Boolean} [options.bundleFolders] If true, applies folder bundling
  @param {Boolean} [options.recursive] If true, processes the directory recursively (default: true)
  @returns {Router} An express-style router

**/

function setup(options) {

  var router  = global.shipp.router(),
      ignored = [],
      type    = Array.isArray(options.exts) ? options.exts[0] : options.exts;

  // Since we can have multiple exts, we attach the type to the options object
  options.type = type;

  Utils.getFiles(options.path, options).forEach(function(file) {

    // Handle the file
    addFile(router, file, { type: type, url: options.url });

    // Remove ignored directories from watch list
    if (file.ignored) ignored = ignored.concat(file.ignored);

  });

  Utils.watch(options.path, "*", {

    chokidar: {
      ignoreInitial : true,
      ignored: ignored,
    },

    type: type,

    add: function(file) {
      addFile(router, Utils.parse(file, options.path), { type: type, url: options.url });
    },

    change: function(file) {

      var file = Utils.parse(file, options.path),
          route = Utils.makeRoute(options.url, file, { type: type });

      // Monitor metadata only for HTML-files
      // We choose not to store and diff metadata as the add/remove process is
      // extremely fast (circa 1ms).
      if ("html" === options.type) {
        removeFile(router, file, { type: type, url: options.url });
        addFile(router, file, { type: type, url: options.url });
      }

      global.shipp.emit("route:refresh", { route: route });

    },

    unlink: function(file) {
      removeFile(router, Utils.parse(file, options.path), { type: type, url: options.url });
    }

  });

  return router;

};
<|endoftext|>