remi
remi copied to clipboard
CSS box model inconsistency
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:
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:
... 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?