From cf0a752e284f744dbaac5d2f52415014f5651951 Mon Sep 17 00:00:00 2001 From: Wolfgang Fahl Date: Thu, 8 Sep 2022 09:11:05 +0200 Subject: [PATCH] moves javascript code from main.html to justpy_core.js and uses Context class to generate html and javascript code to improve #479 --- jpcore/template.py | 81 ++++++++-- justpy/__init__.py | 2 +- justpy/templates/js/justpy_core.js | 249 ++++++++++++++++++++++++++++- justpy/templates/main.html | 233 +-------------------------- tests/test_template.py | 31 ++-- 5 files changed, 344 insertions(+), 252 deletions(-) diff --git a/jpcore/template.py b/jpcore/template.py index bcd487af..f00c8b36 100644 --- a/jpcore/template.py +++ b/jpcore/template.py @@ -3,8 +3,6 @@ @author: wf """ - - class Context: """ legacy context handler, encapsulates context @@ -19,19 +17,87 @@ def __init__(self, context_dict: dict): """ self.context_dict = context_dict self.page_options = PageOptions(context_dict.get("page_options", {})) + self.use_websockets_js=self.context_dict.get("use_websockets","true") + self.page_id_js=context_dict["page_id"] + self.title_js=self.get_js_option("title", "JustPy") + self.redirect_js=self.get_js_option("redirect","") + self.display_url_js=self.get_js_option("display_url","") + justpy_dict_js=str(self.context_dict.get("justpy_dict","[]")) + self.justpy_dict_js=justpy_dict_js.replace('', '') + + def get_js_option(self,key,default_value): + """ + get the page_option with the given key as javascript using the + default value in case the value is not set or none + """ + js_option=self.page_options.page_options_dict.get(key,default_value) + if js_option is None: + js_option=default_value + return js_option + + def as_html_lines(self,indent:str=" "): + """ + generate the html lines for justpy to work + """ + html=self.as_script_src("justpy_core") + html+=f"""{indent}\n{self.as_script_srcs(indent)}" + html+=f"{indent}\n" + return html + + def as_script_src(self,file_name:str,indent:str=" "): + src= f"{indent}\n" + return src + + def as_script_srcs(self,indent:str=" "): + """ + generate a list of javascript files to be imported + """ + srcs = "" + for file_name in [ + "event_handler", + "html_component", + "quasar_component", + "chartjp", + "aggrid", + "iframejp", + "deckgl", + "altairjp", + "plotlyjp", + "bokehjp", + "katexjp", + "editorjp", + ]: + srcs +=self.as_script_src(file_name,indent) + return srcs + + def as_javascript_setup(self,indent): + """ + generate the java script setup code + """ + js=f"{indent} justpy_core.setup();" + return js - def as_javascript(self): + def as_javascript_constructor(self,indent:str=" "): """ generate my initial JavaScript """ - title = self.page_options.get_title() debug = str(self.page_options.get_debug()).lower() page_ready = str(self.page_options.get_page_ready()).lower() result_ready = str(self.page_options.get_result_ready()).lower() reload_interval_ms = self.page_options.get_reload_interval_ms() - javascript = f"""let justpy_core=new JustpyCore( + javascript = f"""{indent}let justpy_core=new JustpyCore( this, // window - '{title}', // title + page_id, // page_id + '{self.title_js}', // title + use_websockets, // use_websockets + '{self.redirect_js}', // redirect + '{self.display_url_js}', // display_url {page_ready}, // page_ready {result_ready}, // result_ready {reload_interval_ms}, // reload_interval @@ -49,9 +115,6 @@ def __init__(self, page_options_dict: dict): self.page_options_dict = page_options_dict self.events = page_options_dict["events"] - def get_title(self): - return self.page_options_dict.get("title", "JustPy") - def get_debug(self): return self.page_options_dict.get("debug", False) diff --git a/justpy/__init__.py b/justpy/__init__.py index 0a93beca..82fbe557 100644 --- a/justpy/__init__.py +++ b/justpy/__init__.py @@ -1,4 +1,4 @@ """JustPy is an object-oriented, component based, high-level Python Web Framework that requires no front-end programming""" from .justpy import * -__version__ = "0.7.2" +__version__ = "0.8.0" diff --git a/justpy/templates/js/justpy_core.js b/justpy/templates/js/justpy_core.js index 0b33672a..165ccaf9 100644 --- a/justpy/templates/js/justpy_core.js +++ b/justpy/templates/js/justpy_core.js @@ -6,7 +6,14 @@ * Legacy global variables */ var comp_dict = {}; // Hold components for running methods + var websocket_id = ''; var websocket_ready = false; + var web_socket_closed = false; + + // the global app object + var app1=null; // will be initialized with a Vue component in justpy_core.setup() + var msg=null; // declare msg - beware aggrid also does this! + var socket=null; // @TODO make configurable and put into object oriented part let reload_timeout = 2000; @@ -78,9 +85,17 @@ class JustpyCore { // create a JustpyCore instance - constructor(window,title,page_ready,result_ready,reload_interval_ms,debug) { + constructor(window,page_id,title,use_websockets,redirect,display_url,page_ready,result_ready,reload_interval_ms,debug) { this.window=window; + this.page_id=page_id; this.setTitle(title); + this.use_websockets=use_websockets; + if (redirect) { + location.href = redirect; + } + if (display_url) { + window.history.pushState("", "", display_url); + } this.page_ready=page_ready; this.result_ready=result_ready; this.reload_interval_ms=reload_interval_ms; @@ -93,4 +108,236 @@ class JustpyCore { this.title=title; // pass } + + // setup the core functionality + setup() { + if (this.use_websockets) { + this.setupWebSocket(); + } else { + this.setupNoWebSocket(); + } + this.createApp1(); + } + + // create the global app1 Vue object + createApp1() { + app1 = new Vue({ + el: '#components', + data: { + justpyComponents: justpyComponents + }, + render: function (h) { + var comps = []; + for (var i = 0; i < this.justpyComponents.length; i++) { + if (this.justpyComponents[i].show) { + comps.push(h(this.justpyComponents[i].vue_type, { + props: { + jp_props: this.justpyComponents[i] + } + })) + } + } + return h('div', {}, comps); + } + }); + } + + // prepare WebSocket handling + setupWebSocket() { + console.log(location.protocol + ' Domain: ' + document.domain); + if (location.protocol === 'https:') { + var protocol_string = 'wss://' + } else { + protocol_string = 'ws://' + } + var ws_url = protocol_string + document.domain; + if (location.port) { + ws_url += ':' + location.port; + } + socket = new WebSocket(ws_url); + + socket.addEventListener('open', function (event) { + console.log('Websocket opened'); + socket.send(JSON.stringify({'type': 'connect', 'page_id': page_id})); + }); + + socket.addEventListener('error', function (event) { + reload_site(); + }); + + socket.addEventListener('close', function (event) { + console.log('Websocket closed'); + web_socket_closed = true; + reload_site() + }); + + socket.addEventListener('message', function (event) { + msg = JSON.parse(event.data); + if (justpy_core.debug) { + console.log('Message received from server ', msg); + console.log(event); + } + let e = {}; + switch (msg.type) { + case 'page_update': + if (msg.page_options.redirect) { + location.href = msg.page_options.redirect; + break; + } + if (msg.page_options.open) { + window.open(msg.page_options.open, '_blank'); + } + if (msg.page_options.display_url !== null) + window.history.pushState("", "", msg.page_options.display_url); + document.title = msg.page_options.title; + if (msg.page_options.favicon) { + var link = document.querySelector("link[rel*='icon']") || document.createElement('link'); + link.type = 'image/x-icon'; + link.rel = 'shortcut icon'; + if (msg.page_options.favicon.startsWith('http')) { + link.href = msg.page_options.favicon; + } else { + link.href = '{{ url_for(options.static_name, path='/') }}' + msg.page_options.favicon; + } + document.getElementsByTagName('head')[0].appendChild(link); + } + + app1.justpyComponents = msg.data; + + break; + case 'page_mode_update': + Quasar.Dark.set(msg.dark); + break; + case 'websocket_update': + websocket_id = msg.data; + websocket_ready = true; + if (justpy_core.page_ready) { + e = { + 'event_type': 'page_ready', + 'visibility': document.visibilityState, + 'page_id': page_id, + 'websocket_id': websocket_id + }; + send_to_server(e, 'page_event', false); + } + break; + case 'component_update': + // update just specific component on the page + comp_replace(msg.data, app1.justpyComponents); + break; + case 'run_javascript': + // let js_result = eval(msg.data); + let js_result; + + function eval_success() { + e = { + 'event_type': 'result_ready', + 'visibility': document.visibilityState, + 'page_id': page_id, + 'websocket_id': websocket_id, + 'request_id': msg.request_id, + 'result': js_result //JSON.stringify(js_result) + }; + if (justpy_core.result_ready) { + if (msg.send) send_to_server(e, 'page_event', false); + } + } + + const jsPromise = (new Promise(function () { + js_result = eval(msg.data); + })).then(eval_success()); + + break; + case 'run_method': + // await websocket.send_json({'type': 'run_method', 'data': command, 'id': self.id}) + eval('comp_dict[' + msg.id + '].' + msg.data); + break; + case 'draw_crosshair': + // Remove previous crosshairs + for (let i = 0; i < crosshairs.length; i++) { + crosshairs[i].destroy(); + } + crosshairs = []; + for (let i = 0; i < msg.data.length; i++) { + const m = msg.data[i]; + const chart_index = document.getElementById(m.id.toString()).getAttribute('data-highcharts-chart'); + const chart = Highcharts.charts[chart_index]; + const point = chart.series[m.series].data[m.point]; + draw_crosshair(chart, chart.series[m.series], point); + } + break; + case 'select_point': + for (let i = 0; i < msg.data.length; i++) { + const m = msg.data[i]; + const chart_index = document.getElementById(m.id.toString()).getAttribute('data-highcharts-chart'); + const chart = Highcharts.charts[chart_index]; + chart.series[m.series].data[m.point].select(); + } + break; + case 'chart_update': + const chart = cached_graph_def['chart' + msg.id]; + chart.update(msg.data); + break; + case 'tooltip_update': + // https://github.com/highcharts/highcharts/issues/6824 + var chart_on = cached_graph_def['chart' + msg.id]; + + if (chart_on.options.tooltip.split) { + chart_on.tooltip.tt.attr({ + text: msg.data[0] + }); + + var j = 1; + for (let i = 0; i < chart_on.tooltip.chart.series.length; i++) { + if (chart_on.tooltip.chart.series[i].visible && + (chart_on.tooltip.chart.series[i].tt != null)) { + chart_on.tooltip.chart.series[i].tt.attr({ + text: msg.data[j++] + }); + + } + } + + } else { + chart_on.tooltip.label.attr({ + text: msg.data + }); + } + break; + default: { + + } + } + }); + } + + setupNoWebSocket() { + window.addEventListener('beforeunload', function (event) { + e = { + 'event_type': 'beforeunload', + 'page_id': page_id, + }; + send_to_server(e); + }); + } + + // setup the reload interval + setupReloadInterval() { + if (this.reload_interval_ms >0) { + setInterval(function () { + $.ajax({ + type: "POST", + url: "/zzz_justpy_ajax", + data: JSON.stringify({ + 'type': 'event', + 'event_data': {'event_type': 'page_update', 'page_id': this.page_id} + }), + success: function (msg) { + if (msg) app1.justpyComponents = msg.data; + }, + dataType: 'json' + }); + }, this.reload_interval_ms); + } + } } \ No newline at end of file diff --git a/justpy/templates/main.html b/justpy/templates/main.html index d63ac391..4d8d03c4 100644 --- a/justpy/templates/main.html +++ b/justpy/templates/main.html @@ -1,25 +1,8 @@ {%- for file_name in options.component_file_list %} {%- endfor %} - -{%- for file_name in ["justpy_core","event_handler","html_component","quasar_component", - "chartjp","aggrid","iframejp","deckgl","altairjp","plotlyjp","bokehjp","katexjp","editorjp"] %} - -{%- endfor %} - \ No newline at end of file diff --git a/tests/test_template.py b/tests/test_template.py index 9b915c7c..24a3e0d0 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -6,17 +6,14 @@ from tests.basetest import Basetest from jpcore.template import Context - class TestTemplate(Basetest): """ Tests template handling """ - - def test_javascript(self): - """ - test javascript generation - """ - context_dict = { + + def setUp(self, debug=False, profile=True): + Basetest.setUp(self, debug=debug, profile=profile) + self.context_dict = { "html": "", "justpy_dict": '[{"attrs": {}, "id": null, "vue_type": "html_component", ' '"show": true, "events": [], "event_modifiers": {}, "classes": ' @@ -61,14 +58,30 @@ def test_javascript(self): "request": None, "use_websockets": "true", } - context_obj = Context(context_dict) - js = context_obj.as_javascript() + self.context_obj = Context(self.context_dict) + + def test_html(self): + """ + test html generation + """ + html=self.context_obj.as_html_lines() + print(html) + + def test_javascript(self): + """ + test javascript generation + """ + js = self.context_obj.as_javascript_constructor() debug = True if debug: print(js) for param in [ "window", + "page_id", "title", + "use_websockets", + "redirect", + "display_url", "page_ready", "result_ready", "reload_interval",