remi icon indicating copy to clipboard operation
remi copied to clipboard

CSS box model inconsistency

Open mcondarelli opened this issue 7 years ago • 0 comments

Hi, I'm trying to move most of my customization from code to res/style.css. In the process I found some strange inconsistencies in display (both Firefox and Chromium behave in the same way)

My current code is:

from remi.gui import *

import regex


class ValidatorGroup(object):
    _groups = {}

    @staticmethod
    def get(name):
        if name not in ValidatorGroup._groups:
            g = ValidatorGroup(name)
            ValidatorGroup._groups[name] = g
        return ValidatorGroup._groups[name]

    @staticmethod
    def subscribe(name, client):
        g = ValidatorGroup.get(name)
        g.clients.add(client)
        return g.clear

    @staticmethod
    def notify(group, provider, ok):
        g = ValidatorGroup.get(group)
        g.update(provider, ok)

    def __init__(self, name):
        self.name = name
        self.botched = set()
        self.clear = None
        self.clients = set()

    def update(self, provider, ok):
        if ok:
            if provider in self.botched:
                self.botched.remove(provider)
        else:
            if provider not in self.botched:
                self.botched.add(provider)
        clear = not self.botched
        if clear != self.clear:
            for client in self.clients:
                client.update(clear)
            self.clear = clear


class ValidableTextInput(TextInput):
    def __init__(self, *args, validate_regex=None, validate_enforce=False, validate_group=None):
        super(ValidableTextInput, self).__init__(*args)
        self._regex = validate_regex
        self._enforce = validate_enforce
        self._group = validate_group
        self._prev = None
        self.onkeydown.connect(self.onkeydown_txt)
        self.onkeyup.connect(self.onkeyup_txt)
        # self.style.update({"border": "2px solid gray", "border-radius": "6px"})
        self._decorate(self, self.validate(self.get_text()))

    def onkeydown_txt(self, emitter, new_value, _):
        del emitter
        self._prev = new_value

    def onkeyup_txt(self, emitter, new_value, _):
        emitter.set_text(new_value)
        v = self.validate(new_value)
        self._decorate(emitter, v)

    def _decorate(self, emitter, v):
        ok = False
        if v == 0:
            # emitter.style.update({"border-color": "gray"})
            emitter.attributes['valid'] = 'partial'
        elif v < 0:
            if self._enforce:
                emitter.set_text(self._prev)
                # emitter.style.update({"border-color": "orange"})
            else:
                # emitter.style.update({"border-color": "red"})
                emitter.attributes['valid'] = 'false'
        else:
            # emitter.style.update({"border-color": "green"})
            emitter.attributes['valid'] = 'true'
            ok = True
        if self._group is not None:
            ValidatorGroup.notify(self._group, self, ok)

    def validate(self, text):
        if self._regex:
            m = regex.fullmatch(self._regex, text, partial=True)
            if m is None:
                return -1
            elif m.partial:
                return 0
            else:
                return 1
        return 1  # if no regex then match is ok by default.


class ValidatedButton(Button):
    def __init__(self, *args, validate_group=None):
        super(ValidatedButton, self).__init__(*args)
        ok = ValidatorGroup.subscribe(validate_group, self)
        self.update(ok)

    def update(self, ok):
        if ok:
            self.attributes.pop('disabled', None)
        else:
            self.attributes['disabled'] = True


if __name__ == '__main__':
    import os
    from remi import start, App

    class MyApp(App):
        def __init__(self, *args, **kwargs):
            res_path = os.path.join(os.path.dirname(__file__), 'res')
            super(MyApp, self).__init__(*args, static_file_path=res_path, **kwargs)

        def idle(self):
            # idle function called every update cycle
            pass

        def main(self):
            main_container = GridBox()

            l_piva = Label('P.IVA')
            w_piva = ValidableTextInput(validate_regex=r'\d{11}', validate_group='ana')
            l_codf = Label('Codice Fiscale')
            w_codf = ValidableTextInput(validate_regex=r'[A-Za-z]{6}\d\d[A-Za-z]\d\d[A-Za-z]\d\d\d[A-Za-z]',
                                        validate_enforce=True, validate_group='ana')
            b_ok = ValidatedButton('ok', validate_group='ana')

            main_container.define_grid(['ab', 'cd', 'ee'])
            main_container.append({'a': l_piva, 'b': w_piva, 'c': l_codf, 'd': w_codf, 'e': b_ok})
            main_container.style.update({"grid-template-columns": "1fr 2fr", "border": "5px solid yellow"})
            return main_container


    start(MyApp, address='127.0.0.1', port=8081, start_browser=False)

... and custom additions to .css are:

* MCon */

.ValidableTextInput {
    border: 2px solid gray;
    border-radius: 6px;
}
.ValidableTextInput[valid='true'] {
    border-color: green;
    /*background-color: green;*/
}
.ValidableTextInput[valid='false'] {
    border-color: red;
    /*background-color: red;*/
}
.ValidableTextInput[valid='partial'] {
    border-color: gray;
    /*background-color: orange;*/
}

.ValidatedButton {
    border: 2px solid cyan;
    border-radius: 6px
}
.ValidatedButton:disabled {
    border: 2px solid gray;
    border-radius: 6px
}

.GridBox {
    grid-auto-rows: minmax;
    grid-auto-columns: minmax;
    grid-auto-flow: row;

    margin: 5px;
}

This is the result: image notice that button border is correctly drawn, while TextInput border is apparently drawn outside the available area; If I remove the border on GridBox it goes outside the right margin of browser window.

Stranger still is that if I save the .html from browser debugger and display it opening it from file I get the following image: image ... which is correctly sized, but has no borders. Saved .html file is:

<!DOCTYPE html>
<html>
<head>
<meta content='text/html;charset=utf-8' http-equiv='Content-Type'>
                <meta content='utf-8' http-equiv='encoding'>
                <meta name="viewport" content="width=device-width, initial-scale=1.0"><link href='/res/style.css?264e4a3de97dad98de772c0a15a744b5' rel='stylesheet' />


<title>MyApp</title>

</head>
<body>
<div id="loading"><div id="loading-animation"></div></div><div id="140567701727160" class="GridBox" data-parent-widget="140567701681152" style="margin:0px;display:grid;grid-template-areas:'a b''c d''e e';grid-template-columns:1fr 2fr;border:5px solid yellow"><p id="140567701727944" class="Label" data-parent-widget="140567701727160" style="margin:0px;grid-area:a;position:static">P.IVA</p><textarea id="140567701729120" class="ValidableTextInput" rows="1" oninput="
                var elem = document.getElementById('140567701729120');
                var enter_pressed = (elem.value.indexOf('\n') > -1);
                if(enter_pressed){
                    elem.value = elem.value.split('\n').join('');
                    var params={};params['new_value']=elem.value;
                    sendCallbackParam('140567701729120','onchange',params);
                }" autocomplete="off" onchange="var params={};params['new_value']=document.getElementById('140567701729120').value;sendCallbackParam('140567701729120','onchange',params);" onkeydown="var elem=document.getElementById('140567701729120');
            var params={};params['new_value']=elem.value;params['keycode']=(event.which||event.keyCode);
            sendCallbackParam('140567701729120','onkeydown',params);" onkeyup="var elem=document.getElementById('140567701729120');
            var params={};params['new_value']=elem.value;params['keycode']=(event.which||event.keyCode);
            sendCallbackParam('140567701729120','onkeyup',params);" valid="partial" data-parent-widget="140567701727160" style="margin:0px;resize:none;grid-area:b;position:static"></textarea><p id="140567667518096" class="Label" data-parent-widget="140567701727160" style="margin:0px;grid-area:c;position:static">Codice Fiscale</p><textarea id="140567667517816" class="ValidableTextInput" rows="1" oninput="
                var elem = document.getElementById('140567667517816');
                var enter_pressed = (elem.value.indexOf('\n') > -1);
                if(enter_pressed){
                    elem.value = elem.value.split('\n').join('');
                    var params={};params['new_value']=elem.value;
                    sendCallbackParam('140567667517816','onchange',params);
                }" autocomplete="off" onchange="var params={};params['new_value']=document.getElementById('140567667517816').value;sendCallbackParam('140567667517816','onchange',params);" onkeydown="var elem=document.getElementById('140567667517816');
            var params={};params['new_value']=elem.value;params['keycode']=(event.which||event.keyCode);
            sendCallbackParam('140567667517816','onkeydown',params);" onkeyup="var elem=document.getElementById('140567667517816');
            var params={};params['new_value']=elem.value;params['keycode']=(event.which||event.keyCode);
            sendCallbackParam('140567667517816','onkeyup',params);" valid="partial" data-parent-widget="140567701727160" style="margin:0px;resize:none;grid-area:d;position:static"></textarea><button id="140567667519104" class="ValidatedButton" disabled="True" data-parent-widget="140567701727160" style="margin:0px;grid-area:e;position:static">ok</button></div>
        <script>
        // from http://stackoverflow.com/questions/5515869/string-length-in-bytes-in-javascript
        // using UTF8 strings I noticed that the javascript .length of a string returned less
        // characters than they actually were
        var pendingSendMessages = [];
        var ws = null;
        var comTimeout = null;
        var failedConnections = 0;

        function byteLength(str) {
            // returns the byte length of an utf8 string
            var s = str.length;
            for (var i=str.length-1; i>=0; i--) {
                var code = str.charCodeAt(i);
                if (code > 0x7f && code <= 0x7ff) s++;
                else if (code > 0x7ff && code <= 0xffff) s+=2;
                if (code >= 0xDC00 && code <= 0xDFFF) i--; //trail surrogate
            }
            return s;
        }

        var paramPacketize = function (ps){
            var ret = '';
            for (var pkey in ps) {
                if( ret.length>0 )ret = ret + '|';
                var pstring = pkey+'='+ps[pkey];
                var pstring_length = byteLength(pstring);
                pstring = pstring_length+'|'+pstring;
                ret = ret + pstring;
            }
            return ret;
        };

        function openSocket(){
            ws_wss = "ws";
            try{
                ws_wss = document.location.protocol.startsWith('https')?'wss':'ws';
            }catch(ex){}

            try{
                ws = new WebSocket(ws_wss + '://127.0.0.1:8081/');
                console.debug('opening websocket');
                ws.onopen = websocketOnOpen;
                ws.onmessage = websocketOnMessage;
                ws.onclose = websocketOnClose;
                ws.onerror = websocketOnError;
            }catch(ex){ws=false;alert('websocketnot supported or server unreachable');}
        }
        openSocket();

        function websocketOnMessage (evt){
            var received_msg = evt.data;

            if( received_msg[0]=='0' ){ /*show_window*/
                var index = received_msg.indexOf(',')+1;
                /*var idRootNodeWidget = received_msg.substr(0,index-1);*/
                var content = received_msg.substr(index,received_msg.length-index);

                document.body.innerHTML = '<div id="loading" style="display: none;"><div id="loading-animation"></div></div>';
                document.body.innerHTML += decodeURIComponent(content);
            }else if( received_msg[0]=='1' ){ /*update_widget*/
                var focusedElement=-1;
                var caretStart=-1;
                var caretEnd=-1;
                if (document.activeElement)
                {
                    focusedElement = document.activeElement.id;
                    try{
                        caretStart = document.activeElement.selectionStart;
                        caretEnd = document.activeElement.selectionEnd;
                    }catch(e){}
                }
                var index = received_msg.indexOf(',')+1;
                var idElem = received_msg.substr(1,index-2);
                var content = received_msg.substr(index,received_msg.length-index);

                var elem = document.getElementById(idElem);
                try{
                    elem.insertAdjacentHTML('afterend',decodeURIComponent(content));
                    elem.parentElement.removeChild(elem);
                }catch(e){
                    /*Microsoft EDGE doesn't support insertAdjacentHTML for SVGElement*/
                    var ns = document.createElementNS("http://www.w3.org/2000/svg",'tmp');
                    ns.innerHTML = decodeURIComponent(content);
                    elem.parentElement.replaceChild(ns.firstChild, elem);
                }

                var elemToFocus = document.getElementById(focusedElement);
                if( elemToFocus != null ){
                    elemToFocus.focus();
                    try{
                        elemToFocus = document.getElementById(focusedElement);
                        if(caretStart>-1 && caretEnd>-1) elemToFocus.setSelectionRange(caretStart, caretEnd);
                    }catch(e){}
                }
            }else if( received_msg[0]=='2' ){ /*javascript*/
                var content = received_msg.substr(1,received_msg.length-1);
                try{
                    eval(content);
                }catch(e){console.debug(e.message);};
            }else if( received_msg[0]=='3' ){ /*ack*/
                pendingSendMessages.shift() /*remove the oldest*/
                if(comTimeout!=null)
                    clearTimeout(comTimeout);
            }
        };

        /*this uses websockets*/
        var sendCallbackParam = function (widgetID,functionName,params /*a dictionary of name:value*/){
            var paramStr = '';
            if(params!=null) paramStr=paramPacketize(params);
            var message = encodeURIComponent(unescape('callback' + '/' + widgetID+'/'+functionName + '/' + paramStr));
            pendingSendMessages.push(message);
            if( pendingSendMessages.length < 1000 ){
                ws.send(message);
                if(comTimeout==null)
                    comTimeout = setTimeout(checkTimeout, 1000);
            }else{
                console.debug('Renewing connection, ws.readyState when trying to send was: ' + ws.readyState)
                renewConnection();
            }
        };

        /*this uses websockets*/
        var sendCallback = function (widgetID,functionName){
            sendCallbackParam(widgetID,functionName,null);
        };

        function renewConnection(){
            // ws.readyState:
            //A value of 0 indicates that the connection has not yet been established.
            //A value of 1 indicates that the connection is established and communication is possible.
            //A value of 2 indicates that the connection is going through the closing handshake.
            //A value of 3 indicates that the connection has been closed or could not be opened.
            if( ws.readyState == 1){
                try{
                    ws.close();
                }catch(err){};
            }
            else if(ws.readyState == 0){
            // Don't do anything, just wait for the connection to be stablished
            }
            else{
                openSocket();
            }
        };

        function checkTimeout(){
            if(pendingSendMessages.length>0)
                renewConnection();
        };

        function websocketOnClose(evt){
            /* websocket is closed. */
            console.debug('Connection is closed... event code: ' + evt.code + ', reason: ' + evt.reason);
            // Some explanation on this error: http://stackoverflow.com/questions/19304157/getting-the-reason-why-websockets-closed
            // In practice, on a unstable network (wifi with a lot of traffic for example) this error appears
            // Got it with Chrome saying:
            // WebSocket connection to 'ws://x.x.x.x:y/' failed: Could not decode a text frame as UTF-8.
            // WebSocket connection to 'ws://x.x.x.x:y/' failed: Invalid frame header

            try {
                document.getElementById("loading").style.display = '';
            } catch(err) {
                console.log('Error hiding loading overlay ' + err.message);
            }

            failedConnections += 1;

            console.debug('failed connections=' + failedConnections + ' queued messages=' + pendingSendMessages.length);

            if(failedConnections > 3) {

                // check if the server has been restarted - which would give it a new websocket address,
                // new state, and require a reload
                console.debug('Checking if GUI still up ' + location.href);

                var http = new XMLHttpRequest();
                http.open('HEAD', location.href);
                http.onreadystatechange = function() {
                    if (http.status == 200) {
                        // server is up but has a new websocket address, reload
                        location.reload();
                    }
                };
                http.send();

                failedConnections = 0;
            }

            if(evt.code == 1006){
                renewConnection();
            }

        };

        function websocketOnError(evt){
            /* websocket is closed. */
            /* alert('Websocket error...');*/
            console.debug('Websocket error... event code: ' + evt.code + ', reason: ' + evt.reason);
        };

        function websocketOnOpen(evt){
            if(ws.readyState == 1){
                ws.send('connected');

                try {
                    document.getElementById("loading").style.display = 'none';
                } catch(err) {
                    console.log('Error hiding loading overlay ' + err.message);
                }

                failedConnections = 0;

                while(pendingSendMessages.length>0){
                    ws.send(pendingSendMessages.shift()); /*without checking ack*/
                }
            }
            else{
                console.debug('onopen fired but the socket readyState was not 1');
            }
        };

        function uploadFile(widgetID, eventSuccess, eventFail, eventData, file){
            var url = '/';
            var xhr = new XMLHttpRequest();
            var fd = new FormData();
            xhr.open('POST', url, true);
            xhr.setRequestHeader('filename', file.name);
            xhr.setRequestHeader('listener', widgetID);
            xhr.setRequestHeader('listener_function', eventData);
            xhr.onreadystatechange = function() {
                if (xhr.readyState == 4 && xhr.status == 200) {
                    /* Every thing ok, file uploaded */
                    var params={};params['filename']=file.name;
                    sendCallbackParam(widgetID, eventSuccess,params);
                    console.log('upload success: ' + file.name);
                }else if(xhr.status == 400){
                    var params={};params['filename']=file.name;
                    sendCallbackParam(widgetID,eventFail,params);
                    console.log('upload failed: ' + file.name);
                }
            };
            fd.append('upload_file', file);
            xhr.send(fd);
        };
        </script>
</body>
</html>

What am I missing?

mcondarelli avatar Nov 01 '18 14:11 mcondarelli