<commit_before>odoo.define('barcodes.BarcodeEvents', function(require) {
"use strict";

var core = require('web.core');
var mixins = require('web.mixins');
var session = require('web.session');

// For IE >= 9, use this, new CustomEvent(), instead of new Event()
function CustomEvent ( event, params ) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    var evt = document.createEvent( 'CustomEvent' );
    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
    return evt;
   }
CustomEvent.prototype = window.Event.prototype;

var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
    timeout: null,
    key_pressed: {},
    buffered_key_events: [],
    // Regexp to match a barcode input and extract its payload
    // Note: to build in init() if prefix/suffix can be configured
    regexp: /(.{3,})[\n\r\t]*/,
    // By knowing the terminal character we can interpret buffered keys
    // as a barcode as soon as it's encountered (instead of waiting x ms)
    suffix: /[\n\r\t]+/,
    // Keys from a barcode scanner are usually processed as quick as possible,
    // but some scanners can use an intercharacter delay (we support <= 50 ms)
    max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,
    // To be able to receive the barcode value, an input must be focused.
    // On mobile devices, this causes the virtual keyboard to open.
    // Unfortunately it is not possible to avoid this behavior...
    // To avoid keyboard flickering at each detection of a barcode value,
    // we want to keep it open for a while (800 ms).
    inputTimeOut: 800,

    init: function() {
        mixins.PropertiesMixin.init.call(this);
        // Keep a reference of the handler functions to use when adding and removing event listeners
        this.__keydown_handler = _.bind(this.keydown_handler, this);
        this.__keyup_handler = _.bind(this.keyup_handler, this);
        this.__handler = _.bind(this.handler, this);
        // Bind event handler once the DOM is loaded
        // TODO: find a way to be active only when there are listeners on the bus
        $(_.bind(this.start, this, false));

        // Mobile device detection
        var isMobile = navigator.userAgent.match(/Android/i) ||
                       navigator.userAgent.match(/webOS/i) ||
                       navigator.userAgent.match(/iPhone/i) ||
                       navigator.userAgent.match(/iPad/i) ||
                       navigator.userAgent.match(/iPod/i) ||
                       navigator.userAgent.match(/BlackBerry/i) ||
                       navigator.userAgent.match(/Windows Phone/i);
        this.isChromeMobile = isMobile && navigator.userAgent.match(/Chrome/i);

        // Creates an input who will receive the barcode scanner value.
        if (this.isChromeMobile) {
            this.$barcodeInput = $('<input/>', {
                name: 'barcode',
                type: 'text',
                css: {
                    'position': 'fixed',
                    'top': '50%',
                    'transform': 'translateY(-50%)',
                    'z-index': '-1',
                },
            });
            // Avoid to show autocomplete for a non appearing input
            this.$barcodeInput.attr('autocomplete', 'off');
        }

        this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut);
    },

    handle_buffered_keys: function() {
        var str = this.buffered_key_events.reduce(function(memo, e) { return memo + String.fromCharCode(e.which) }, '');
        var match = str.match(this.regexp);

        if (match) {
            var barcode = match[1];

            // Send the target in case there are several barcode widgets on the same page (e.g.
            // registering the lot numbers in a stock picking)
            core.bus.trigger('barcode_scanned', barcode, this.buffered_key_events[0].target);

            // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set.
            if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true")
                $(this.buffered_key_events[0].target).trigger('barcode_scanned', barcode);
        } else {
            this.resend_buffered_keys();
        }

        this.buffered_key_events = [];
    },

    resend_buffered_keys: function() {
        var old_event, new_event;
        for(var i = 0; i < this.buffered_key_events.length; i++) {
            old_event = this.buffered_key_events[i];

            if(old_event.which !== 13) { // ignore returns
                // We do not create a 'real' keypress event through
                // eg. KeyboardEvent because there are several issues
                // with them that make them very different from
                // genuine keypresses. Chrome per example has had a
                // bug for the longest time that causes keyCode and
                // charCode to not be set for events created this way:
                // https://bugs.webkit.org/show_bug.cgi?id=16735
                var params = {
                    'bubbles': old_event.bubbles,
                    'cancelable': old_event.cancelable,
                };
                new_event = $.Event('keypress', params);
                new_event.viewArg = old_event.viewArg;
                new_event.ctrl = old_event.ctrl;
                new_event.alt = old_event.alt;
                new_event.shift = old_event.shift;
                new_event.meta = old_event.meta;
                new_event.char = old_event.char;
                new_event.key = old_event.key;
                new_event.charCode = old_event.charCode;
                new_event.keyCode = old_event.keyCode || old_event.which; // Firefox doesn't set keyCode for keypresses, only keyup/down
                new_event.which = old_event.which;
                new_event.dispatched_by_barcode_reader = true;

                $(old_event.target).trigger(new_event);
            }
        }
    },

    element_is_editable: function(element) {
        return $(element).is('input,textarea,[contenteditable="true"]');
    },

    // This checks that a keypress event is either ESC, TAB, an arrow
    // key or a function key. This is Firefox specific, in Chrom{e,ium}
    // keypress events are not fired for these types of keys, only
    // keyup/keydown.
    is_special_key: function(e) {
        if (e.key === "ArrowLeft" || e.key === "ArrowRight" ||
            e.key === "ArrowUp" || e.key === "ArrowDown" ||
            e.key === "Escape" || e.key === "Tab" ||
            e.key === "Backspace" || e.key === "Delete" ||
            e.key === "Home" || e.key === "End" ||
            e.key === "PageUp" || e.key === "PageDown" ||
            e.key === "Unidentified" || /F\d\d?/.test(e.key)) {
            return true;
        } else {
            return false;
        }
    },

    // The keydown and keyup handlers are here to disallow key
    // repeat. When preventDefault() is called on a keydown event
    // the keypress that normally follows is cancelled.
    keydown_handler: function(e){
        if (this.key_pressed[e.which]) {
            e.preventDefault();
        } else {
            this.key_pressed[e.which] = true;
        }
    },

    keyup_handler: function(e){
        this.key_pressed[e.which] = false;
    },

    handler: function(e){
        // Don't catch events we resent
        if (e.dispatched_by_barcode_reader)
            return;
        // Don't catch non-printable keys for which Firefox triggers a keypress
        if (this.is_special_key(e))
            return;
        // Don't catch keypresses which could have a UX purpose (like shortcuts)
        if (e.ctrlKey || e.metaKey || e.altKey)
            return;
        // Don't catch Return when nothing is buffered. This way users
        // can still use Return to 'click' on focused buttons or links.
        if (e.which === 13 && this.buffered_key_events.length === 0)
            return;
        // Don't catch events targeting elements that are editable because we
        // have no way of redispatching 'genuine' key events. Resent events
        // don't trigger native event handlers of elements. So this means that
        // our fake events will not appear in eg. an <input> element.
        if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true")
            return;

        // Catch and buffer the event
        this.buffered_key_events.push(e);
        e.preventDefault();
        e.stopImmediatePropagation();

        // Handle buffered keys immediately if the the keypress marks the end
        // of a barcode or after x milliseconds without a new keypress
        clearTimeout(this.timeout);
        if (String.fromCharCode(e.which).match(this.suffix)) {
            this.handle_buffered_keys();
        } else {
            this.timeout = setTimeout(_.bind(this.handle_buffered_keys, this), this.max_time_between_keys_in_ms);
        }
    },

    /**
     * Try to detect the barcode value by listening all keydown events:
     * Checks if a dom element who may contains text value has the focus.
     * If not, it's probably because these events are triggered by a barcode scanner.
     * To be able to handle this value, a focused input will be created.
     *
     * This function also has the responsibility to detect the end of the barcode value.
     * (1) In most of cases, an optional key (tab or enter) is sent to mark the end of the value.
     * So, we direclty handle the value.
     * (2) If no end key is configured, we have to calculate the delay between each keydowns.
     * 'max_time_between_keys_in_ms' depends of the device and may be configured.
     * Exceeded this timeout, we consider that the barcode value is entirely sent.
     *
     * @private
     * @param  {jQuery.Event} e keydown event
     */
    _listenBarcodeScanner: function (e) {
        if ($(':focus').not('input:text, textarea, [contenteditable], ' +
            '[type="email"], [type="number"], [type="password"], [type="tel"]')) {
            $('body').append(this.$barcodeInput);
            this.$barcodeInput.focus();
        }
        if (this.$barcodeInput.is(":focus")) {
            // Handle buffered keys immediately if the keypress marks the end
            // of a barcode or after x milliseconds without a new keypress.
            clearTimeout(this.timeout);
            // On chrome mobile, e.which only works for some special characters like ENTER or TAB.
            if (String.fromCharCode(e.which).match(this.suffix)) {
                this._handleBarcodeValue(e);
            } else {
                this.timeout = setTimeout(this._handleBarcodeValue.bind(this, e),
                    this.max_time_between_keys_in_ms);
            }
            // if the barcode input doesn't receive keydown for a while, remove it.
            this.__blurBarcodeInput();
        }
    },

    /**
     * Retrieves the barcode value from the temporary input element.
     * This checks this value and trigger it on the bus.
     *
     * @private
     * @param  {jQuery.Event} keydown event
     */
    _handleBarcodeValue: function (e) {
        var barcodeValue = this.$barcodeInput.val();
        if (barcodeValue.match(this.regexp)) {
            core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]);
            this._blurBarcodeInput();
        }
    },

    /**
     * Removes the value and focus from the barcode input.
     * If nothing happens, the focus will be lost and
     * the virtual keyboard on mobile devices will be closed.
     *
     * @private
     */
    _blurBarcodeInput: function () {
        if (this.$barcodeInput) {
            // Close the virtual keyboard on mobile browsers
            // FIXME: actually we can't prevent keyboard from opening
            this.$barcodeInput.val('').blur();
        }
    },

    start: function(prevent_key_repeat){
        // Chrome Mobile isn't triggering keypress event.
        // This is marked as Legacy in the DOM-Level-3 Standard.
        // See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types
        // This fix is only applied for Google Chrome Mobile but it should work for
        // all other cases.
        // In master, we could remove the behavior with keypress and only use keydown.
        if (this.isChromeMobile) {
            $('body').on("keydown", this._listenBarcodeScanner.bind(this));
        } else {
            $('body').bind("keypress", this.__handler);
        }
        if (prevent_key_repeat === true) {
            $('body').bind("keydown", this.__keydown_handler);
            $('body').bind('keyup', this.__keyup_handler);
        }
    },

    stop: function(){
        $('body').unbind("keypress", this.__handler);
        $('body').unbind("keydown", this.__keydown_handler);
        $('body').unbind('keyup', this.__keyup_handler);
    },
});

return {
    /** Singleton that emits barcode_scanned events on core.bus */
    BarcodeEvents: new BarcodeEvents(),
    /**
     * List of barcode prefixes that are reserved for internal purposes
     * @type Array
     */
    ReservedBarcodePrefixes: ['O-CMD'],
};

});
<commit_msg>Revert "[FIX] barcodes: let focus in number, password, tel, phone inputs"<commit_after>odoo.define('barcodes.BarcodeEvents', function(require) {
"use strict";

var core = require('web.core');
var mixins = require('web.mixins');
var session = require('web.session');

// For IE >= 9, use this, new CustomEvent(), instead of new Event()
function CustomEvent ( event, params ) {
    params = params || { bubbles: false, cancelable: false, detail: undefined };
    var evt = document.createEvent( 'CustomEvent' );
    evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
    return evt;
   }
CustomEvent.prototype = window.Event.prototype;

var BarcodeEvents = core.Class.extend(mixins.PropertiesMixin, {
    timeout: null,
    key_pressed: {},
    buffered_key_events: [],
    // Regexp to match a barcode input and extract its payload
    // Note: to build in init() if prefix/suffix can be configured
    regexp: /(.{3,})[\n\r\t]*/,
    // By knowing the terminal character we can interpret buffered keys
    // as a barcode as soon as it's encountered (instead of waiting x ms)
    suffix: /[\n\r\t]+/,
    // Keys from a barcode scanner are usually processed as quick as possible,
    // but some scanners can use an intercharacter delay (we support <= 50 ms)
    max_time_between_keys_in_ms: session.max_time_between_keys_in_ms || 55,
    // To be able to receive the barcode value, an input must be focused.
    // On mobile devices, this causes the virtual keyboard to open.
    // Unfortunately it is not possible to avoid this behavior...
    // To avoid keyboard flickering at each detection of a barcode value,
    // we want to keep it open for a while (800 ms).
    inputTimeOut: 800,

    init: function() {
        mixins.PropertiesMixin.init.call(this);
        // Keep a reference of the handler functions to use when adding and removing event listeners
        this.__keydown_handler = _.bind(this.keydown_handler, this);
        this.__keyup_handler = _.bind(this.keyup_handler, this);
        this.__handler = _.bind(this.handler, this);
        // Bind event handler once the DOM is loaded
        // TODO: find a way to be active only when there are listeners on the bus
        $(_.bind(this.start, this, false));

        // Mobile device detection
        var isMobile = navigator.userAgent.match(/Android/i) ||
                       navigator.userAgent.match(/webOS/i) ||
                       navigator.userAgent.match(/iPhone/i) ||
                       navigator.userAgent.match(/iPad/i) ||
                       navigator.userAgent.match(/iPod/i) ||
                       navigator.userAgent.match(/BlackBerry/i) ||
                       navigator.userAgent.match(/Windows Phone/i);
        this.isChromeMobile = isMobile && navigator.userAgent.match(/Chrome/i);

        // Creates an input who will receive the barcode scanner value.
        if (this.isChromeMobile) {
            this.$barcodeInput = $('<input/>', {
                name: 'barcode',
                type: 'text',
                css: {
                    'position': 'fixed',
                    'top': '50%',
                    'transform': 'translateY(-50%)',
                    'z-index': '-1',
                },
            });
            // Avoid to show autocomplete for a non appearing input
            this.$barcodeInput.attr('autocomplete', 'off');
        }

        this.__blurBarcodeInput = _.debounce(this._blurBarcodeInput, this.inputTimeOut);
    },

    handle_buffered_keys: function() {
        var str = this.buffered_key_events.reduce(function(memo, e) { return memo + String.fromCharCode(e.which) }, '');
        var match = str.match(this.regexp);

        if (match) {
            var barcode = match[1];

            // Send the target in case there are several barcode widgets on the same page (e.g.
            // registering the lot numbers in a stock picking)
            core.bus.trigger('barcode_scanned', barcode, this.buffered_key_events[0].target);

            // Dispatch a barcode_scanned DOM event to elements that have barcode_events="true" set.
            if (this.buffered_key_events[0].target.getAttribute("barcode_events") === "true")
                $(this.buffered_key_events[0].target).trigger('barcode_scanned', barcode);
        } else {
            this.resend_buffered_keys();
        }

        this.buffered_key_events = [];
    },

    resend_buffered_keys: function() {
        var old_event, new_event;
        for(var i = 0; i < this.buffered_key_events.length; i++) {
            old_event = this.buffered_key_events[i];

            if(old_event.which !== 13) { // ignore returns
                // We do not create a 'real' keypress event through
                // eg. KeyboardEvent because there are several issues
                // with them that make them very different from
                // genuine keypresses. Chrome per example has had a
                // bug for the longest time that causes keyCode and
                // charCode to not be set for events created this way:
                // https://bugs.webkit.org/show_bug.cgi?id=16735
                var params = {
                    'bubbles': old_event.bubbles,
                    'cancelable': old_event.cancelable,
                };
                new_event = $.Event('keypress', params);
                new_event.viewArg = old_event.viewArg;
                new_event.ctrl = old_event.ctrl;
                new_event.alt = old_event.alt;
                new_event.shift = old_event.shift;
                new_event.meta = old_event.meta;
                new_event.char = old_event.char;
                new_event.key = old_event.key;
                new_event.charCode = old_event.charCode;
                new_event.keyCode = old_event.keyCode || old_event.which; // Firefox doesn't set keyCode for keypresses, only keyup/down
                new_event.which = old_event.which;
                new_event.dispatched_by_barcode_reader = true;

                $(old_event.target).trigger(new_event);
            }
        }
    },

    element_is_editable: function(element) {
        return $(element).is('input,textarea,[contenteditable="true"]');
    },

    // This checks that a keypress event is either ESC, TAB, an arrow
    // key or a function key. This is Firefox specific, in Chrom{e,ium}
    // keypress events are not fired for these types of keys, only
    // keyup/keydown.
    is_special_key: function(e) {
        if (e.key === "ArrowLeft" || e.key === "ArrowRight" ||
            e.key === "ArrowUp" || e.key === "ArrowDown" ||
            e.key === "Escape" || e.key === "Tab" ||
            e.key === "Backspace" || e.key === "Delete" ||
            e.key === "Home" || e.key === "End" ||
            e.key === "PageUp" || e.key === "PageDown" ||
            e.key === "Unidentified" || /F\d\d?/.test(e.key)) {
            return true;
        } else {
            return false;
        }
    },

    // The keydown and keyup handlers are here to disallow key
    // repeat. When preventDefault() is called on a keydown event
    // the keypress that normally follows is cancelled.
    keydown_handler: function(e){
        if (this.key_pressed[e.which]) {
            e.preventDefault();
        } else {
            this.key_pressed[e.which] = true;
        }
    },

    keyup_handler: function(e){
        this.key_pressed[e.which] = false;
    },

    handler: function(e){
        // Don't catch events we resent
        if (e.dispatched_by_barcode_reader)
            return;
        // Don't catch non-printable keys for which Firefox triggers a keypress
        if (this.is_special_key(e))
            return;
        // Don't catch keypresses which could have a UX purpose (like shortcuts)
        if (e.ctrlKey || e.metaKey || e.altKey)
            return;
        // Don't catch Return when nothing is buffered. This way users
        // can still use Return to 'click' on focused buttons or links.
        if (e.which === 13 && this.buffered_key_events.length === 0)
            return;
        // Don't catch events targeting elements that are editable because we
        // have no way of redispatching 'genuine' key events. Resent events
        // don't trigger native event handlers of elements. So this means that
        // our fake events will not appear in eg. an <input> element.
        if ((this.element_is_editable(e.target) && !$(e.target).data('enableBarcode')) && e.target.getAttribute("barcode_events") !== "true")
            return;

        // Catch and buffer the event
        this.buffered_key_events.push(e);
        e.preventDefault();
        e.stopImmediatePropagation();

        // Handle buffered keys immediately if the the keypress marks the end
        // of a barcode or after x milliseconds without a new keypress
        clearTimeout(this.timeout);
        if (String.fromCharCode(e.which).match(this.suffix)) {
            this.handle_buffered_keys();
        } else {
            this.timeout = setTimeout(_.bind(this.handle_buffered_keys, this), this.max_time_between_keys_in_ms);
        }
    },

    /**
     * Try to detect the barcode value by listening all keydown events:
     * Checks if a dom element who may contains text value has the focus.
     * If not, it's probably because these events are triggered by a barcode scanner.
     * To be able to handle this value, a focused input will be created.
     *
     * This function also has the responsibility to detect the end of the barcode value.
     * (1) In most of cases, an optional key (tab or enter) is sent to mark the end of the value.
     * So, we direclty handle the value.
     * (2) If no end key is configured, we have to calculate the delay between each keydowns.
     * 'max_time_between_keys_in_ms' depends of the device and may be configured.
     * Exceeded this timeout, we consider that the barcode value is entirely sent.
     *
     * @private
     * @param  {jQuery.Event} e keydown event
     */
    _listenBarcodeScanner: function (e) {
        if (!$('input:text:focus, textarea:focus, [contenteditable]:focus').length) {
            $('body').append(this.$barcodeInput);
            this.$barcodeInput.focus();
        }
        if (this.$barcodeInput.is(":focus")) {
            // Handle buffered keys immediately if the keypress marks the end
            // of a barcode or after x milliseconds without a new keypress.
            clearTimeout(this.timeout);
            // On chrome mobile, e.which only works for some special characters like ENTER or TAB.
            if (String.fromCharCode(e.which).match(this.suffix)) {
                this._handleBarcodeValue(e);
            } else {
                this.timeout = setTimeout(this._handleBarcodeValue.bind(this, e),
                    this.max_time_between_keys_in_ms);
            }
            // if the barcode input doesn't receive keydown for a while, remove it.
            this.__blurBarcodeInput();
        }
    },

    /**
     * Retrieves the barcode value from the temporary input element.
     * This checks this value and trigger it on the bus.
     *
     * @private
     * @param  {jQuery.Event} keydown event
     */
    _handleBarcodeValue: function (e) {
        var barcodeValue = this.$barcodeInput.val();
        if (barcodeValue.match(this.regexp)) {
            core.bus.trigger('barcode_scanned', barcodeValue, $(e.target).parent()[0]);
            this._blurBarcodeInput();
        }
    },

    /**
     * Removes the value and focus from the barcode input.
     * If nothing happens, the focus will be lost and
     * the virtual keyboard on mobile devices will be closed.
     *
     * @private
     */
    _blurBarcodeInput: function () {
        if (this.$barcodeInput) {
            // Close the virtual keyboard on mobile browsers
            // FIXME: actually we can't prevent keyboard from opening
            this.$barcodeInput.val('').blur();
        }
    },

    start: function(prevent_key_repeat){
        // Chrome Mobile isn't triggering keypress event.
        // This is marked as Legacy in the DOM-Level-3 Standard.
        // See: https://www.w3.org/TR/uievents/#legacy-keyboardevent-event-types
        // This fix is only applied for Google Chrome Mobile but it should work for
        // all other cases.
        // In master, we could remove the behavior with keypress and only use keydown.
        if (this.isChromeMobile) {
            $('body').on("keydown", this._listenBarcodeScanner.bind(this));
        } else {
            $('body').bind("keypress", this.__handler);
        }
        if (prevent_key_repeat === true) {
            $('body').bind("keydown", this.__keydown_handler);
            $('body').bind('keyup', this.__keyup_handler);
        }
    },

    stop: function(){
        $('body').unbind("keypress", this.__handler);
        $('body').unbind("keydown", this.__keydown_handler);
        $('body').unbind('keyup', this.__keyup_handler);
    },
});

return {
    /** Singleton that emits barcode_scanned events on core.bus */
    BarcodeEvents: new BarcodeEvents(),
    /**
     * List of barcode prefixes that are reserved for internal purposes
     * @type Array
     */
    ReservedBarcodePrefixes: ['O-CMD'],
};

});
<|endoftext|>


<commit_before>odoo.define('im_livechat.im_livechat', function (require) {
"use strict";

var local_storage = require('web.local_storage');
var bus = require('bus.bus').bus;
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
var Widget = require('web.Widget');

var ChatWindow = require('mail.ChatWindow');

var _t = core._t;
var QWeb = core.qweb;

// Constants
var LIVECHAT_COOKIE_HISTORY = 'im_livechat_history';
var HISTORY_LIMIT = 15;

// History tracking
var page = window.location.href.replace(/^.*\/\/[^\/]+/, '');
var page_history = utils.get_cookie(LIVECHAT_COOKIE_HISTORY);
var url_history = [];
if(page_history){
    url_history = JSON.parse(page_history) || [];
}
if (!_.contains(url_history, page)) {
    url_history.push(page);
    while (url_history.length > HISTORY_LIMIT) {
        url_history.shift();
    }
    utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(url_history), 60*60*24); // 1 day cookie
}

var LivechatButton = Widget.extend({
    className:"openerp o_livechat_button hidden-print",

    events: {
        "click": "open_chat"
    },

    init: function (parent, server_url, options) {
        this._super(parent);
        this.options = _.defaults(options || {}, {
            input_placeholder: _t('Ask something ...'),
            default_username: _t("Visitor"),
            button_text: _t("Chat with one of our collaborators"),
            default_message: _t("How may I help you?"),
        });
        this.channel = null;
        this.chat_window = null;
        this.messages = [];
        this.server_url = server_url;
    },

    willStart: function () {
        var self = this;
        var cookie = utils.get_cookie('im_livechat_session');
        var ready;
        if (!cookie) {
            ready = session.rpc("/im_livechat/init", {channel_id: this.options.channel_id}).then(function (result) {
                if (!result.available_for_me) {
                    return $.Deferred().reject();
                }
                self.rule = result.rule;
            });
        } else {
            var channel = JSON.parse(cookie);
            ready = session.rpc("/mail/chat_history", {uuid: channel.uuid, limit: 100}).then(function (history) {
                self.history = history;
            });
        }
        return ready.then(this.load_qweb_template.bind(this));
    },

    start: function () {
        this.$el.text(this.options.button_text);
        var small_screen = config.device.size_class === config.device.SIZES.XS;
        if (this.history) {
            _.each(this.history.reverse(), this.add_message.bind(this));
            this.open_chat();
        } else if (!small_screen && this.rule.action === 'auto_popup') {
            var auto_popup_cookie = utils.get_cookie('im_livechat_auto_popup');
            if (!auto_popup_cookie || JSON.parse(auto_popup_cookie)) {
                this.auto_popup_timeout = setTimeout(this.open_chat.bind(this), this.rule.auto_popup_timer*1000);
            }
        }
        bus.on('notification', this, function (notifications) {
            var self = this;
            _.each(notifications, function (notification) {
                self._on_notification(notification);
            });
        });
        return this._super();
    },
    _on_notification: function(notification){
        if (this.channel && (notification[0] === this.channel.uuid)) {
            if(notification[1]._type === "history_command") { // history request
                var cookie = utils.get_cookie(LIVECHAT_COOKIE_HISTORY);
                var history = cookie ? JSON.parse(cookie) : [];
                session.rpc("/im_livechat/history", {
                    pid: this.channel.operator_pid[0],
                    channel_uuid: this.channel.uuid,
                    page_history: history,
                });
            }else{ // normal message
                this.add_message(notification[1]);
                this.render_messages();
                if (this.chat_window.folded || !this.chat_window.thread.is_at_bottom()) {
                    this.chat_window.update_unread(this.chat_window.unread_msgs+1);
                }
            }
        }
    },
    load_qweb_template: function(){
        var xml_files = ['/mail/static/src/xml/chat_window.xml',
                         '/mail/static/src/xml/thread.xml',
                         '/im_livechat/static/src/xml/im_livechat.xml'];
        var defs = _.map(xml_files, function (tmpl) {
            return session.rpc('/web/proxy/load', {path: tmpl}).then(function (xml) {
                QWeb.add_template(xml);
            });
        });
        return $.when.apply($, defs);
    },

    open_chat: _.debounce(function () {
        if (this.opening_chat) {
            return;
        }
        var self = this;
        var cookie = utils.get_cookie('im_livechat_session');
        var def;
        this.opening_chat = true;
        clearTimeout(this.auto_popup_timeout);
        if (cookie) {
            def = $.when(JSON.parse(cookie));
        } else {
            this.messages = []; // re-initialize messages cache
            def = session.rpc('/im_livechat/get_session', {
                channel_id : this.options.channel_id,
                anonymous_name : this.options.default_username,
            }, {shadow: true});
        }
        def.then(function (channel) {
            if (!channel || !channel.operator_pid) {
                alert(_t("None of our collaborators seems to be available, please try again later."));
            } else {
                self.channel = channel;
                self.open_chat_window(channel);
                self.send_welcome_message();
                self.render_messages();

                bus.add_channel(channel.uuid);
                bus.start_polling();

                utils.set_cookie('im_livechat_session', JSON.stringify(channel), 60*60);
                utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
            }
        }).always(function () {
            self.opening_chat = false;
        });
    }, 200, true),

    open_chat_window: function (channel) {
        var self = this;
        var options = {
            display_stars: false,
            placeholder: this.options.input_placeholder || "",
        };
        var is_folded = (channel.state === 'folded');
        this.chat_window = new ChatWindow(this, channel.id, channel.name, is_folded, channel.message_unread_counter, options);
        this.chat_window.appendTo($('body')).then(function () {
            self.chat_window.$el.css({right: 0, bottom: 0});
            self.$el.hide();
        });
        this.chat_window.on("close_chat_session", this, function () {
            var input_disabled = this.chat_window.$(".o_chat_composer input").prop('disabled');
            var ask_fb = !input_disabled && _.find(this.messages, function (msg) {
                return msg.id !== '_welcome';
            });
            if (ask_fb) {
                this.chat_window.toggle_fold(false);
                this.ask_feedback();
            } else {
                this.close_chat();
            }
        });
        this.chat_window.on("post_message", this, function (message) {
            self.send_message(message).fail(function (error, e) {
                e.preventDefault();
                return self.send_message(message); // try again just in case
            });
        });
        this.chat_window.on("fold_channel", this, function () {
            this.channel.state = (this.channel.state === 'open') ? 'folded' : 'open';
            utils.set_cookie('im_livechat_session', JSON.stringify(this.channel), 60*60);
        });
        this.chat_window.thread.$el.on("scroll", null, _.debounce(function () {
            if (self.chat_window.thread.is_at_bottom()) {
                self.chat_window.update_unread(0);
            }
        }, 100));
    },

    close_chat: function () {
        this.chat_window.destroy();
        utils.set_cookie('im_livechat_session', "", -1); // remove cookie
    },

    send_message: function (message) {
        var self = this;
        return session
            .rpc("/mail/chat_post", {uuid: this.channel.uuid, message_content: message.content})
            .then(function () {
                self.chat_window.thread.scroll_to();
            });
    },

    add_message: function (data, options) {
        var msg = {
            id: data.id,
            attachment_ids: data.attachment_ids,
            author_id: data.author_id,
            body: data.body,
            date: moment(time.str_to_datetime(data.date)),
            is_needaction: false,
            is_note: data.is_note,
            customer_email_data: []
        };

        // Compute displayed author name or email
        msg.displayed_author = msg.author_id && msg.author_id[1] ||
                               this.options.default_username;

        // Compute the avatar_url
        msg.avatar_src = this.server_url;
        if (msg.author_id && msg.author_id[0]) {
            msg.avatar_src += "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
        } else {
            msg.avatar_src += "/mail/static/src/img/smiley/avatar.jpg";
        }

        if (options && options.prepend) {
            this.messages.unshift(msg);
        } else {
            this.messages.push(msg);
        }
    },

    render_messages: function () {
        var should_scroll = !this.chat_window.folded && this.chat_window.thread.is_at_bottom();
        this.chat_window.render(this.messages);
        if (should_scroll) {
            this.chat_window.thread.scroll_to();
        }
    },

    send_welcome_message: function () {
        if (this.options.default_message) {
            this.add_message({
                id: '_welcome',
                attachment_ids: [],
                author_id: this.channel.operator_pid,
                body: this.options.default_message,
                channel_ids: [this.channel.id],
                date: time.datetime_to_str(new Date()),
                tracking_value_ids: [],
            }, {prepend: true});
        }
    },

    ask_feedback: function () {
        this.chat_window.$(".o_chat_composer input").prop('disabled', true);

        var feedback = new Feedback(this, this.channel.uuid);
        feedback.replace(this.chat_window.thread.$el);

        feedback.on("send_message", this, this.send_message);
        feedback.on("feedback_sent", this, this.close_chat);
    }
});

/*
 * Rating for Livechat
 *
 * This widget displays the 3 rating smileys, and a textarea to add a reason
 * (only for red smiley), and sends the user feedback to the server.
 */
var Feedback = Widget.extend({
    template: "im_livechat.FeedBack",

    events: {
        'click .o_livechat_rating_choices img': 'on_click_smiley',
        'click .o_livechat_no_feedback em': 'on_click_no_feedback',
        'click .o_rating_submit_button': 'on_click_send',
    },

    init: function (parent, channel_uuid) {
        this._super(parent);
        this.channel_uuid = channel_uuid;
        this.server_origin = session.origin;
        this.rating = undefined;
        this.dp = new concurrency.DropPrevious();
    },

    on_click_smiley: function (ev) {
        this.rating = parseInt($(ev.currentTarget).data('value'));
        this.$('.o_livechat_rating_choices img').removeClass('selected');
        this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');

        // only display textearea if bad smiley selected
        var close_chat = false;
        if (this.rating === 1) {
            this.$('.o_livechat_rating_reason').show();
        } else {
            this.$('.o_livechat_rating_reason').hide();
            close_chat = true;
        }
        this._send_feedback({close: close_chat});
    },

    on_click_no_feedback: function () {
        this.trigger("feedback_sent"); // will close the chat
    },

    on_click_send: function () {
        if (_.isNumber(this.rating)) {
            this._send_feedback({ reason: this.$('textarea').val(), close: true });
        }
    },

    _send_feedback: function (options) {
        var self = this;
        var args = {
            uuid: this.channel_uuid,
            rate: this.rating,
            reason : options.reason
        };
        this.dp.add(session.rpc('/im_livechat/feedback', args)).then(function () {
            if (options.close) {
                var content = _.str.sprintf(_t("Rating: :rating_%d"), self.rating);
                if (options.reason) {
                    content += " \n" + options.reason;
                }
                self.trigger("send_message", {content: content});
                self.trigger("feedback_sent"); // will close the chat
            }
        });
    }
});

return {
    LivechatButton: LivechatButton,
    Feedback: Feedback,
};

});
<commit_msg>[FIX] im_livechat: no duplicated messages<commit_after>odoo.define('im_livechat.im_livechat', function (require) {
"use strict";

var local_storage = require('web.local_storage');
var bus = require('bus.bus').bus;
var concurrency = require('web.concurrency');
var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var time = require('web.time');
var utils = require('web.utils');
var Widget = require('web.Widget');

var ChatWindow = require('mail.ChatWindow');

var _t = core._t;
var QWeb = core.qweb;

// Constants
var LIVECHAT_COOKIE_HISTORY = 'im_livechat_history';
var HISTORY_LIMIT = 15;

// History tracking
var page = window.location.href.replace(/^.*\/\/[^\/]+/, '');
var page_history = utils.get_cookie(LIVECHAT_COOKIE_HISTORY);
var url_history = [];
if(page_history){
    url_history = JSON.parse(page_history) || [];
}
if (!_.contains(url_history, page)) {
    url_history.push(page);
    while (url_history.length > HISTORY_LIMIT) {
        url_history.shift();
    }
    utils.set_cookie(LIVECHAT_COOKIE_HISTORY, JSON.stringify(url_history), 60*60*24); // 1 day cookie
}

var LivechatButton = Widget.extend({
    className:"openerp o_livechat_button hidden-print",

    events: {
        "click": "open_chat"
    },

    init: function (parent, server_url, options) {
        this._super(parent);
        this.options = _.defaults(options || {}, {
            input_placeholder: _t('Ask something ...'),
            default_username: _t("Visitor"),
            button_text: _t("Chat with one of our collaborators"),
            default_message: _t("How may I help you?"),
        });
        this.channel = null;
        this.chat_window = null;
        this.messages = [];
        this.server_url = server_url;
    },

    willStart: function () {
        var self = this;
        var cookie = utils.get_cookie('im_livechat_session');
        var ready;
        if (!cookie) {
            ready = session.rpc("/im_livechat/init", {channel_id: this.options.channel_id}).then(function (result) {
                if (!result.available_for_me) {
                    return $.Deferred().reject();
                }
                self.rule = result.rule;
            });
        } else {
            var channel = JSON.parse(cookie);
            ready = session.rpc("/mail/chat_history", {uuid: channel.uuid, limit: 100}).then(function (history) {
                self.history = history;
            });
        }
        return ready.then(this.load_qweb_template.bind(this));
    },

    start: function () {
        this.$el.text(this.options.button_text);
        var small_screen = config.device.size_class === config.device.SIZES.XS;
        if (this.history) {
            _.each(this.history.reverse(), this.add_message.bind(this));
            this.open_chat();
        } else if (!small_screen && this.rule.action === 'auto_popup') {
            var auto_popup_cookie = utils.get_cookie('im_livechat_auto_popup');
            if (!auto_popup_cookie || JSON.parse(auto_popup_cookie)) {
                this.auto_popup_timeout = setTimeout(this.open_chat.bind(this), this.rule.auto_popup_timer*1000);
            }
        }
        bus.on('notification', this, function (notifications) {
            var self = this;
            _.each(notifications, function (notification) {
                self._on_notification(notification);
            });
        });
        return this._super();
    },
    _on_notification: function(notification){
        if (this.channel && (notification[0] === this.channel.uuid)) {
            if(notification[1]._type === "history_command") { // history request
                var cookie = utils.get_cookie(LIVECHAT_COOKIE_HISTORY);
                var history = cookie ? JSON.parse(cookie) : [];
                session.rpc("/im_livechat/history", {
                    pid: this.channel.operator_pid[0],
                    channel_uuid: this.channel.uuid,
                    page_history: history,
                });
            }else{ // normal message
                this.add_message(notification[1]);
                this.render_messages();
                if (this.chat_window.folded || !this.chat_window.thread.is_at_bottom()) {
                    this.chat_window.update_unread(this.chat_window.unread_msgs+1);
                }
            }
        }
    },
    load_qweb_template: function(){
        var xml_files = ['/mail/static/src/xml/chat_window.xml',
                         '/mail/static/src/xml/thread.xml',
                         '/im_livechat/static/src/xml/im_livechat.xml'];
        var defs = _.map(xml_files, function (tmpl) {
            return session.rpc('/web/proxy/load', {path: tmpl}).then(function (xml) {
                QWeb.add_template(xml);
            });
        });
        return $.when.apply($, defs);
    },

    open_chat: _.debounce(function () {
        if (this.opening_chat) {
            return;
        }
        var self = this;
        var cookie = utils.get_cookie('im_livechat_session');
        var def;
        this.opening_chat = true;
        clearTimeout(this.auto_popup_timeout);
        if (cookie) {
            def = $.when(JSON.parse(cookie));
        } else {
            this.messages = []; // re-initialize messages cache
            def = session.rpc('/im_livechat/get_session', {
                channel_id : this.options.channel_id,
                anonymous_name : this.options.default_username,
            }, {shadow: true});
        }
        def.then(function (channel) {
            if (!channel || !channel.operator_pid) {
                alert(_t("None of our collaborators seems to be available, please try again later."));
            } else {
                self.channel = channel;
                self.open_chat_window(channel);
                self.send_welcome_message();
                self.render_messages();

                bus.add_channel(channel.uuid);
                bus.start_polling();

                utils.set_cookie('im_livechat_session', JSON.stringify(channel), 60*60);
                utils.set_cookie('im_livechat_auto_popup', JSON.stringify(false), 60*60);
            }
        }).always(function () {
            self.opening_chat = false;
        });
    }, 200, true),

    open_chat_window: function (channel) {
        var self = this;
        var options = {
            display_stars: false,
            placeholder: this.options.input_placeholder || "",
        };
        var is_folded = (channel.state === 'folded');
        this.chat_window = new ChatWindow(this, channel.id, channel.name, is_folded, channel.message_unread_counter, options);
        this.chat_window.appendTo($('body')).then(function () {
            self.chat_window.$el.css({right: 0, bottom: 0});
            self.$el.hide();
        });
        this.chat_window.on("close_chat_session", this, function () {
            var input_disabled = this.chat_window.$(".o_chat_composer input").prop('disabled');
            var ask_fb = !input_disabled && _.find(this.messages, function (msg) {
                return msg.id !== '_welcome';
            });
            if (ask_fb) {
                this.chat_window.toggle_fold(false);
                this.ask_feedback();
            } else {
                this.close_chat();
            }
        });
        this.chat_window.on("post_message", this, function (message) {
            self.send_message(message).fail(function (error, e) {
                e.preventDefault();
                return self.send_message(message); // try again just in case
            });
        });
        this.chat_window.on("fold_channel", this, function () {
            this.channel.state = (this.channel.state === 'open') ? 'folded' : 'open';
            utils.set_cookie('im_livechat_session', JSON.stringify(this.channel), 60*60);
        });
        this.chat_window.thread.$el.on("scroll", null, _.debounce(function () {
            if (self.chat_window.thread.is_at_bottom()) {
                self.chat_window.update_unread(0);
            }
        }, 100));
    },

    close_chat: function () {
        this.chat_window.destroy();
        utils.set_cookie('im_livechat_session', "", -1); // remove cookie
    },

    send_message: function (message) {
        var self = this;
        return session
            .rpc("/mail/chat_post", {uuid: this.channel.uuid, message_content: message.content})
            .then(function () {
                self.chat_window.thread.scroll_to();
            });
    },

    add_message: function (data, options) {
        var msg = {
            id: data.id,
            attachment_ids: data.attachment_ids,
            author_id: data.author_id,
            body: data.body,
            date: moment(time.str_to_datetime(data.date)),
            is_needaction: false,
            is_note: data.is_note,
            customer_email_data: []
        };
        var hasAlreadyMessage = _.some(this.messages, function (message) {
            return message.id === msg.id;
        });
        if (hasAlreadyMessage) {
            return;
        }
        // Compute displayed author name or email
        msg.displayed_author = msg.author_id && msg.author_id[1] ||
                               this.options.default_username;

        // Compute the avatar_url
        msg.avatar_src = this.server_url;
        if (msg.author_id && msg.author_id[0]) {
            msg.avatar_src += "/web/image/res.partner/" + msg.author_id[0] + "/image_small";
        } else {
            msg.avatar_src += "/mail/static/src/img/smiley/avatar.jpg";
        }

        if (options && options.prepend) {
            this.messages.unshift(msg);
        } else {
            this.messages.push(msg);
        }
    },

    render_messages: function () {
        var should_scroll = !this.chat_window.folded && this.chat_window.thread.is_at_bottom();
        this.chat_window.render(this.messages);
        if (should_scroll) {
            this.chat_window.thread.scroll_to();
        }
    },

    send_welcome_message: function () {
        if (this.options.default_message) {
            this.add_message({
                id: '_welcome',
                attachment_ids: [],
                author_id: this.channel.operator_pid,
                body: this.options.default_message,
                channel_ids: [this.channel.id],
                date: time.datetime_to_str(new Date()),
                tracking_value_ids: [],
            }, {prepend: true});
        }
    },

    ask_feedback: function () {
        this.chat_window.$(".o_chat_composer input").prop('disabled', true);

        var feedback = new Feedback(this, this.channel.uuid);
        feedback.replace(this.chat_window.thread.$el);

        feedback.on("send_message", this, this.send_message);
        feedback.on("feedback_sent", this, this.close_chat);
    }
});

/*
 * Rating for Livechat
 *
 * This widget displays the 3 rating smileys, and a textarea to add a reason
 * (only for red smiley), and sends the user feedback to the server.
 */
var Feedback = Widget.extend({
    template: "im_livechat.FeedBack",

    events: {
        'click .o_livechat_rating_choices img': 'on_click_smiley',
        'click .o_livechat_no_feedback em': 'on_click_no_feedback',
        'click .o_rating_submit_button': 'on_click_send',
    },

    init: function (parent, channel_uuid) {
        this._super(parent);
        this.channel_uuid = channel_uuid;
        this.server_origin = session.origin;
        this.rating = undefined;
        this.dp = new concurrency.DropPrevious();
    },

    on_click_smiley: function (ev) {
        this.rating = parseInt($(ev.currentTarget).data('value'));
        this.$('.o_livechat_rating_choices img').removeClass('selected');
        this.$('.o_livechat_rating_choices img[data-value="'+this.rating+'"]').addClass('selected');

        // only display textearea if bad smiley selected
        var close_chat = false;
        if (this.rating === 1) {
            this.$('.o_livechat_rating_reason').show();
        } else {
            this.$('.o_livechat_rating_reason').hide();
            close_chat = true;
        }
        this._send_feedback({close: close_chat});
    },

    on_click_no_feedback: function () {
        this.trigger("feedback_sent"); // will close the chat
    },

    on_click_send: function () {
        if (_.isNumber(this.rating)) {
            this._send_feedback({ reason: this.$('textarea').val(), close: true });
        }
    },

    _send_feedback: function (options) {
        var self = this;
        var args = {
            uuid: this.channel_uuid,
            rate: this.rating,
            reason : options.reason
        };
        this.dp.add(session.rpc('/im_livechat/feedback', args)).then(function () {
            if (options.close) {
                var content = _.str.sprintf(_t("Rating: :rating_%d"), self.rating);
                if (options.reason) {
                    content += " \n" + options.reason;
                }
                self.trigger("send_message", {content: content});
                self.trigger("feedback_sent"); // will close the chat
            }
        });
    }
});

return {
    LivechatButton: LivechatButton,
    Feedback: Feedback,
};

});
<|endoftext|>


<commit_before>import PropTypes from 'prop-types';
import React from 'react';
import styled from 'react-emotion';

import Datetime from 'react-datetime';
// import { Input } from '@storybook/components';

import style from './styles';

const DateInput = styled(Datetime)(style);

class DateType extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (!state || props.knob.value !== state.value) {
      return { value: props.knob.value };
    }
    return null;
  }

  handleChange = date => {
    const value = date.valueOf();
    this.setState({ value });

    this.props.onChange(value);
  };

  render() {
    const { value } = this.state;

    return (
      <DateInput
        value={value ? new Date(value) : null}
        type="date"
        onChange={this.handleChange}
        size="flex"
      />
    );
  }
}

DateType.propTypes = {
  // eslint-disable-next-line react/no-unused-prop-types
  knob: PropTypes.shape({
    name: PropTypes.string,
    value: PropTypes.number,
  }).isRequired,
  onChange: PropTypes.func.isRequired,
};

DateType.serialize = value => String(value);
DateType.deserialize = value => parseFloat(value);

export default DateType;
<commit_msg>REMOVE commented code<commit_after>import PropTypes from 'prop-types';
import React from 'react';
import styled from 'react-emotion';

import Datetime from 'react-datetime';

import style from './styles';

const DateInput = styled(Datetime)(style);

class DateType extends React.Component {
  static getDerivedStateFromProps(props, state) {
    if (!state || props.knob.value !== state.value) {
      return { value: props.knob.value };
    }
    return null;
  }

  handleChange = date => {
    const value = date.valueOf();
    this.setState({ value });

    this.props.onChange(value);
  };

  render() {
    const { value } = this.state;

    return (
      <DateInput
        value={value ? new Date(value) : null}
        type="date"
        onChange={this.handleChange}
        size="flex"
      />
    );
  }
}

DateType.propTypes = {
  // eslint-disable-next-line react/no-unused-prop-types
  knob: PropTypes.shape({
    name: PropTypes.string,
    value: PropTypes.number,
  }).isRequired,
  onChange: PropTypes.func.isRequired,
};

DateType.serialize = value => String(value);
DateType.deserialize = value => parseFloat(value);

export default DateType;
<|endoftext|>


<commit_before>odoo.define('web.datepicker', function (require) {
"use strict";

var core = require('web.core');
var field_utils = require('web.field_utils');
var time = require('web.time');
var Widget = require('web.Widget');

var _t = core._t;

var DateWidget = Widget.extend({
    template: "web.datepicker",
    type_of_date: "date",
    events: {
        'change.datetimepicker': 'changeDatetime',
        'error.datetimepicker': 'errorDatetime',
        'change .o_datepicker_input': 'changeDatetime',
        'input input': '_onInput',
        'keydown': '_onKeydown',
        'show.datetimepicker': '_onDateTimePickerShow',
    },
    /**
     * @override
     */
    init: function (parent, options) {
        this._super.apply(this, arguments);

        this.name = parent.name;
        this.options = _.extend({
            locale: moment.locale(),
            format : this.type_of_date === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(),
            minDate: moment({ y: 1900 }),
            maxDate: moment().add(200, "y"),
            useCurrent: false,
            icons: {
                time: 'fa fa-clock-o',
                date: 'fa fa-calendar',
                up: 'fa fa-chevron-up',
                down: 'fa fa-chevron-down',
                previous: 'fa fa-chevron-left',
                next: 'fa fa-chevron-right',
                today: 'fa fa-calendar-check-o',
                clear: 'fa fa-delete',
                close: 'fa fa-times'
            },
            calendarWeeks: true,
            buttons: {
                showToday: false,
                showClear: false,
                showClose: false,
            },
            widgetParent: 'body',
            keyBinds: null,
            allowInputToggle: true,
        }, options || {});

        this.__libInput = 0;
    },
    /**
     * @override
     */
    start: function () {
        this.$input = this.$('input.o_datepicker_input');
        this.__libInput++;
        this.$el.datetimepicker(this.options);
        this.__libInput--;
        this._setReadonly(false);
    },
    /**
     * @override
     */
    destroy: function () {
        this.__libInput++;
        this.$el.datetimepicker('destroy');
        this.__libInput--;
        this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * set datetime value
     */
    changeDatetime: function () {
        if (this.isValid()) {
            var oldValue = this.getValue();
            this._setValueFromUi();
            var newValue = this.getValue();
            var hasChanged = !oldValue !== !newValue;
            if (oldValue && newValue) {
                var formattedOldValue = oldValue.format(time.getLangDatetimeFormat());
                var formattedNewValue = newValue.format(time.getLangDatetimeFormat());
                if (formattedNewValue !== formattedOldValue) {
                    hasChanged = true;
                }
            }
            if (hasChanged) {
                if (this.options.warn_future) {
                    this._warnFuture(newValue);
                }
                this.trigger("datetime_changed");
            }
        }
    },
    /**
     * Library clears the wrong date format so just ignore error
     */
    errorDatetime: function (e) {
        if (!e.date.isValid()){
            this.do_warn(_t("Error"), e.date.toString());
        }
        return false;
    },
    /**
     * Focuses the datepicker input. This function must be called in order to
     * prevent 'input' events triggered by the lib to bubble up, and to cause
     * unwanted effects (like triggering 'field_changed' events)
     */
    focus: function () {
        this.__libInput++;
        this.$input.focus();
        this.__libInput--;
    },
    /**
     * @returns {Moment|false}
     */
    getValue: function () {
        var value = this.get('value');
        return value && value.clone();
    },
    /**
     * @returns {boolean}
     */
    isValid: function () {
        var value = this.$input.val();
        if (value === "") {
            return true;
        } else {
            try {
                this._parseClient(value);
                return true;
            } catch (e) {
                return false;
            }
        }
    },
    /**
     * @returns {Moment|false} value
     */
    maxDate: function (date) {
        this.__libInput++;
        this.$el.datetimepicker('maxDate', date || null);
        this.__libInput--;
    },
    /**
     * @returns {Moment|false} value
     */
    minDate: function (date) {
        this.__libInput++;
        this.$el.datetimepicker('minDate', date || null);
        this.__libInput--;
    },
    /**
     * @param {Moment|false} value
     */
    setValue: function (value) {
        this.set({'value': value});
        var formatted_value = value ? this._formatClient(value) : null;
        this.$input.val(formatted_value);
        this.__libInput++;
        this.$el.datetimepicker('date', value || null);
        this.__libInput--;
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * add a warning to communicate that a date in the future has been set
     *
     * @private
     * @param {Moment} currentDate
     */
    _warnFuture: function (currentDate) {
        if (!this.$warning) {
            this.$warning = $('<span>', {
                class: 'fa fa-exclamation-triangle o_tz_warning o_datepicker_warning',
            });
            var title = _t("This date is on the future. Make sure it is what you expected.");
            this.$warning.attr('title', title);
            this.$input.after(this.$warning);
        }
        if (currentDate && currentDate.isAfter(moment())) {
            this.$warning.show();
        } else {
            this.$warning.hide();
        }
    },

    /**
     * @private
     * @param {Moment} v
     * @returns {string}
     */
    _formatClient: function (v) {
        return field_utils.format[this.type_of_date](v, null, {timezone: false});
    },
    /**
     * @private
     * @param {string|false} v
     * @returns {Moment}
     */
    _parseClient: function (v) {
        return field_utils.parse[this.type_of_date](v, null, {timezone: false});
    },
    /**
     * @private
     * @param {boolean} readonly
     */
    _setReadonly: function (readonly) {
        this.readonly = readonly;
        this.$input.prop('readonly', this.readonly);
    },
    /**
     * set the value from the input value
     *
     * @private
     */
    _setValueFromUi: function () {
        var value = this.$input.val() || false;
        this.setValue(this._parseClient(value));
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Reacts to the datetimepicker being shown
     * Could set/verify our widget value
     * And subsequently update the datetimepicker
     *
     * @private
     */
    _onDateTimePickerShow: function () {
        if (this.$input.val().length !== 0 && this.isValid()) {
            this.$input.select();
        }
    },
    /**
     * @private
     * @param {KeyEvent} ev
     */
    _onKeydown: function (ev) {
        if (ev.which === $.ui.keyCode.ESCAPE) {
            this.__libInput++;
            this.$el.datetimepicker('hide');
            this.__libInput--;
        }
    },
    /**
     * Prevents 'input' events triggered by the library to bubble up, as they
     * might have unwanted effects (like triggering 'field_changed' events in
     * the context of field widgets)
     *
     * @private
     * @param {Event} ev
     */
    _onInput: function (ev) {
        if (this.__libInput > 0) {
            ev.stopImmediatePropagation();
        }
    },
});

var DateTimeWidget = DateWidget.extend({
    type_of_date: "datetime",
    init: function (parent, options) {
        this._super(parent, _.extend({
            buttons: {
                showToday: false,
                showClear: false,
                showClose: true,
            },
        }, options || {}));
    },
});

return {
    DateWidget: DateWidget,
    DateTimeWidget: DateTimeWidget,
};

});
<commit_msg>[FIX] web: Invalid Date warning<commit_after>odoo.define('web.datepicker', function (require) {
"use strict";

var core = require('web.core');
var field_utils = require('web.field_utils');
var time = require('web.time');
var Widget = require('web.Widget');

var _t = core._t;

var DateWidget = Widget.extend({
    template: "web.datepicker",
    type_of_date: "date",
    events: {
        'change.datetimepicker': 'changeDatetime',
        'error.datetimepicker': 'errorDatetime',
        'change .o_datepicker_input': 'changeDatetime',
        'input input': '_onInput',
        'keydown': '_onKeydown',
        'show.datetimepicker': '_onDateTimePickerShow',
    },
    /**
     * @override
     */
    init: function (parent, options) {
        this._super.apply(this, arguments);

        this.name = parent.name;
        this.options = _.extend({
            locale: moment.locale(),
            format : this.type_of_date === 'datetime' ? time.getLangDatetimeFormat() : time.getLangDateFormat(),
            minDate: moment({ y: 1900 }),
            maxDate: moment().add(200, "y"),
            useCurrent: false,
            icons: {
                time: 'fa fa-clock-o',
                date: 'fa fa-calendar',
                up: 'fa fa-chevron-up',
                down: 'fa fa-chevron-down',
                previous: 'fa fa-chevron-left',
                next: 'fa fa-chevron-right',
                today: 'fa fa-calendar-check-o',
                clear: 'fa fa-delete',
                close: 'fa fa-times'
            },
            calendarWeeks: true,
            buttons: {
                showToday: false,
                showClear: false,
                showClose: false,
            },
            widgetParent: 'body',
            keyBinds: null,
            allowInputToggle: true,
        }, options || {});

        this.__libInput = 0;
    },
    /**
     * @override
     */
    start: function () {
        this.$input = this.$('input.o_datepicker_input');
        this.__libInput++;
        this.$el.datetimepicker(this.options);
        this.__libInput--;
        this._setReadonly(false);
    },
    /**
     * @override
     */
    destroy: function () {
        this.__libInput++;
        this.$el.datetimepicker('destroy');
        this.__libInput--;
        this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * set datetime value
     */
    changeDatetime: function () {
        if (this.isValid()) {
            var oldValue = this.getValue();
            this._setValueFromUi();
            var newValue = this.getValue();
            var hasChanged = !oldValue !== !newValue;
            if (oldValue && newValue) {
                var formattedOldValue = oldValue.format(time.getLangDatetimeFormat());
                var formattedNewValue = newValue.format(time.getLangDatetimeFormat());
                if (formattedNewValue !== formattedOldValue) {
                    hasChanged = true;
                }
            }
            if (hasChanged) {
                if (this.options.warn_future) {
                    this._warnFuture(newValue);
                }
                this.trigger("datetime_changed");
            }
        }
    },
    /**
     * Library clears the wrong date format so just ignore error
     */
    errorDatetime: function (e) {
        return false;
    },
    /**
     * Focuses the datepicker input. This function must be called in order to
     * prevent 'input' events triggered by the lib to bubble up, and to cause
     * unwanted effects (like triggering 'field_changed' events)
     */
    focus: function () {
        this.__libInput++;
        this.$input.focus();
        this.__libInput--;
    },
    /**
     * @returns {Moment|false}
     */
    getValue: function () {
        var value = this.get('value');
        return value && value.clone();
    },
    /**
     * @returns {boolean}
     */
    isValid: function () {
        var value = this.$input.val();
        if (value === "") {
            return true;
        } else {
            try {
                this._parseClient(value);
                return true;
            } catch (e) {
                return false;
            }
        }
    },
    /**
     * @returns {Moment|false} value
     */
    maxDate: function (date) {
        this.__libInput++;
        this.$el.datetimepicker('maxDate', date || null);
        this.__libInput--;
    },
    /**
     * @returns {Moment|false} value
     */
    minDate: function (date) {
        this.__libInput++;
        this.$el.datetimepicker('minDate', date || null);
        this.__libInput--;
    },
    /**
     * @param {Moment|false} value
     */
    setValue: function (value) {
        this.set({'value': value});
        var formatted_value = value ? this._formatClient(value) : null;
        this.$input.val(formatted_value);
        this.__libInput++;
        this.$el.datetimepicker('date', value || null);
        this.__libInput--;
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * add a warning to communicate that a date in the future has been set
     *
     * @private
     * @param {Moment} currentDate
     */
    _warnFuture: function (currentDate) {
        if (!this.$warning) {
            this.$warning = $('<span>', {
                class: 'fa fa-exclamation-triangle o_tz_warning o_datepicker_warning',
            });
            var title = _t("This date is on the future. Make sure it is what you expected.");
            this.$warning.attr('title', title);
            this.$input.after(this.$warning);
        }
        if (currentDate && currentDate.isAfter(moment())) {
            this.$warning.show();
        } else {
            this.$warning.hide();
        }
    },

    /**
     * @private
     * @param {Moment} v
     * @returns {string}
     */
    _formatClient: function (v) {
        return field_utils.format[this.type_of_date](v, null, {timezone: false});
    },
    /**
     * @private
     * @param {string|false} v
     * @returns {Moment}
     */
    _parseClient: function (v) {
        return field_utils.parse[this.type_of_date](v, null, {timezone: false});
    },
    /**
     * @private
     * @param {boolean} readonly
     */
    _setReadonly: function (readonly) {
        this.readonly = readonly;
        this.$input.prop('readonly', this.readonly);
    },
    /**
     * set the value from the input value
     *
     * @private
     */
    _setValueFromUi: function () {
        var value = this.$input.val() || false;
        this.setValue(this._parseClient(value));
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Reacts to the datetimepicker being shown
     * Could set/verify our widget value
     * And subsequently update the datetimepicker
     *
     * @private
     */
    _onDateTimePickerShow: function () {
        if (this.$input.val().length !== 0 && this.isValid()) {
            this.$input.select();
        }
    },
    /**
     * @private
     * @param {KeyEvent} ev
     */
    _onKeydown: function (ev) {
        if (ev.which === $.ui.keyCode.ESCAPE) {
            this.__libInput++;
            this.$el.datetimepicker('hide');
            this.__libInput--;
        }
    },
    /**
     * Prevents 'input' events triggered by the library to bubble up, as they
     * might have unwanted effects (like triggering 'field_changed' events in
     * the context of field widgets)
     *
     * @private
     * @param {Event} ev
     */
    _onInput: function (ev) {
        if (this.__libInput > 0) {
            ev.stopImmediatePropagation();
        }
    },
});

var DateTimeWidget = DateWidget.extend({
    type_of_date: "datetime",
    init: function (parent, options) {
        this._super(parent, _.extend({
            buttons: {
                showToday: false,
                showClear: false,
                showClose: true,
            },
        }, options || {}));
    },
});

return {
    DateWidget: DateWidget,
    DateTimeWidget: DateTimeWidget,
};

});
<|endoftext|>


<commit_before>import React, { PropTypes } from 'react';
import { css, StyleSheet } from 'aphrodite/no-important';
import Button from '../Button';
import Glyph from '../Glyph';

function GlyphButton ({ children, glyph, glyphColor, glyphSize, position, ...props }) {
	const isDefault = position === 'default';
	const isLeft = position === 'left';
	const isRight = position === 'right';

	// determine if margin is necessary
	const glyphStyles = {};
	if (isLeft) glyphStyles.marginRight = '0.5em';
	if (isRight) glyphStyles.marginLeft = '0.5em';

	// prepare the icon
	const icon = (
		<Glyph
			className={css(classes.glyph)}
			color={glyphColor}
			name={glyph}
			size={glyphSize}
			style={glyphStyles}
		/>
	);

	// render that shit
	return (
		<Button {...props}>
			{(isDefault || isLeft) && icon}
			{children}
			{isRight && icon}
		</Button>
	);
};

// For props "glyph", "glyphColor", and "glyphSize":
// prop type validation will occur within the Glyph component, no need to
// duplicate, just pass it through.
GlyphButton.propTypes = {
	glyph: PropTypes.string,
	glyphColor: PropTypes.string,
	glyphSize: PropTypes.string,
	position: PropTypes.oneOf(['default', 'left', 'right']),
};
GlyphButton.defaultProps = {
	position: 'default', // no margin, assumes no children
};

const classes = StyleSheet.create({
	glyph: {
		display: 'inline-block',
		marginTop: '-0.1em', // fix icon alignment
		verticalAlign: 'middle',
	},
});

module.exports = GlyphButton;
<commit_msg>GlyphButton: simplify alignment with pseudo classes<commit_after>/* eslint quote-props: ["error", "as-needed"] */

import React, { PropTypes } from 'react';
import { css, StyleSheet } from 'aphrodite/no-important';
import Button from '../Button';
import Glyph from '../Glyph';

function GlyphButton ({ children, glyph, glyphColor, glyphSize, position, ...props }) {
	const isDefault = position === 'default';
	const isLeft = position === 'left';
	const isRight = position === 'right';

	// prepare the icon
	const icon = (
		<Glyph
			className={css(classes.glyph)}
			color={glyphColor}
			name={glyph}
			size={glyphSize}
		/>
	);

	// render that shit
	return (
		<Button {...props}>
			{(isDefault || isLeft) && icon}
			{children}
			{isRight && icon}
		</Button>
	);
};

// For props "glyph", "glyphColor", and "glyphSize":
// prop type validation will occur within the Glyph component, no need to
// duplicate, just pass it through.
GlyphButton.propTypes = {
	glyph: PropTypes.string,
	glyphColor: PropTypes.string,
	glyphSize: PropTypes.string,
	position: PropTypes.oneOf(['default', 'left', 'right']),
};
GlyphButton.defaultProps = {
	position: 'default', // no margin, assumes no children
};

const classes = StyleSheet.create({
	glyph: {
		display: 'inline-block',
		marginTop: '-0.125em', // fix icon alignment
		verticalAlign: 'middle',

		// add spacing
		':first-child': {
			marginRight: '0.5em',
		},
		':last-child': {
			marginLeft: '0.5em',
		},
		':only-child': {
			marginLeft: 0,
			marginRight: 0,
		},
	},
});

module.exports = GlyphButton;
<|endoftext|>