@qtoggle/qui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/.eslintignore +2 -0
  2. package/.eslintrc.json +492 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
  6. package/.github/workflows/main.yml +74 -0
  7. package/.pre-commit-config.yaml +8 -0
  8. package/LICENSE.txt +177 -0
  9. package/README.md +4 -0
  10. package/font/dejavusans-bold.woff +0 -0
  11. package/font/dejavusans-bolditalic.woff +0 -0
  12. package/font/dejavusans-italic.woff +0 -0
  13. package/font/dejavusans-regular.woff +0 -0
  14. package/img/qui-icons.svg +1937 -0
  15. package/js/base/base.js +47 -0
  16. package/js/base/condition-variable.js +92 -0
  17. package/js/base/errors.js +36 -0
  18. package/js/base/i18n.js +20 -0
  19. package/js/base/mixwith.js +135 -0
  20. package/js/base/require-js-compat.js +78 -0
  21. package/js/base/signal.js +91 -0
  22. package/js/base/singleton.js +66 -0
  23. package/js/base/timer.js +126 -0
  24. package/js/config.js +184 -0
  25. package/js/forms/common-fields/check-field.js +42 -0
  26. package/js/forms/common-fields/choice-buttons-field.js +30 -0
  27. package/js/forms/common-fields/color-combo-field.js +37 -0
  28. package/js/forms/common-fields/combo-field.js +108 -0
  29. package/js/forms/common-fields/common-fields.js +23 -0
  30. package/js/forms/common-fields/composite-field.js +132 -0
  31. package/js/forms/common-fields/custom-html-field.js +51 -0
  32. package/js/forms/common-fields/email-field.js +30 -0
  33. package/js/forms/common-fields/file-picker-field.js +46 -0
  34. package/js/forms/common-fields/jquery-ui-field.js +111 -0
  35. package/js/forms/common-fields/labels-field.js +69 -0
  36. package/js/forms/common-fields/numeric-field.js +39 -0
  37. package/js/forms/common-fields/password-field.js +28 -0
  38. package/js/forms/common-fields/phone-field.js +26 -0
  39. package/js/forms/common-fields/progress-disk-field.js +69 -0
  40. package/js/forms/common-fields/push-button-field.js +138 -0
  41. package/js/forms/common-fields/slider-field.js +51 -0
  42. package/js/forms/common-fields/text-area-field.js +34 -0
  43. package/js/forms/common-fields/text-field.js +89 -0
  44. package/js/forms/common-fields/up-down-field.js +85 -0
  45. package/js/forms/common-forms/common-forms.js +16 -0
  46. package/js/forms/common-forms/options-form.js +77 -0
  47. package/js/forms/common-forms/page-form.js +115 -0
  48. package/js/forms/form-button.js +202 -0
  49. package/js/forms/form-field.js +1183 -0
  50. package/js/forms/form.js +1181 -0
  51. package/js/forms/forms.js +68 -0
  52. package/js/global-glass.js +100 -0
  53. package/js/icons/default-stock.js +173 -0
  54. package/js/icons/icon.js +64 -0
  55. package/js/icons/icons.js +16 -0
  56. package/js/icons/multi-state-sprites-icon.js +362 -0
  57. package/js/icons/stock-icon.js +219 -0
  58. package/js/icons/stock.js +98 -0
  59. package/js/icons/stocks.js +57 -0
  60. package/js/index.js +232 -0
  61. package/js/lib/jquery.longpress.js +79 -0
  62. package/js/lib/jquery.module.js +4 -0
  63. package/js/lib/logger.module.js +4 -0
  64. package/js/lib/pep.module.js +4 -0
  65. package/js/lists/common-items/common-items.js +5 -0
  66. package/js/lists/common-items/icon-label-list-item.js +86 -0
  67. package/js/lists/common-lists/common-lists.js +5 -0
  68. package/js/lists/common-lists/page-list.js +53 -0
  69. package/js/lists/list-item.js +147 -0
  70. package/js/lists/list.js +636 -0
  71. package/js/lists/lists.js +26 -0
  72. package/js/main-ui/main-ui.js +64 -0
  73. package/js/main-ui/menu-bar.js +144 -0
  74. package/js/main-ui/options-bar.js +181 -0
  75. package/js/main-ui/status.js +185 -0
  76. package/js/main-ui/top-bar.js +59 -0
  77. package/js/messages/common-message-forms/common-message-forms.js +7 -0
  78. package/js/messages/common-message-forms/confirm-message-form.js +81 -0
  79. package/js/messages/common-message-forms/simple-message-form.js +67 -0
  80. package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
  81. package/js/messages/message-form.js +107 -0
  82. package/js/messages/messages.js +21 -0
  83. package/js/messages/sticky-modal-page.js +98 -0
  84. package/js/messages/sticky-modal-progress-message.js +27 -0
  85. package/js/messages/toast.js +164 -0
  86. package/js/navigation.js +654 -0
  87. package/js/pages/breadcrumbs.js +124 -0
  88. package/js/pages/common-pages/common-pages.js +6 -0
  89. package/js/pages/common-pages/modal-progress-page.js +83 -0
  90. package/js/pages/common-pages/structured-page.js +46 -0
  91. package/js/pages/page.js +1018 -0
  92. package/js/pages/pages-context.js +154 -0
  93. package/js/pages/pages.js +252 -0
  94. package/js/pwa.js +337 -0
  95. package/js/sections/section.js +612 -0
  96. package/js/sections/sections.js +300 -0
  97. package/js/tables/common-cells/common-cells.js +7 -0
  98. package/js/tables/common-cells/icon-label-table-cell.js +68 -0
  99. package/js/tables/common-cells/push-button-table-cell.js +133 -0
  100. package/js/tables/common-cells/simple-table-cell.js +37 -0
  101. package/js/tables/common-tables/common-tables.js +5 -0
  102. package/js/tables/common-tables/page-table.js +55 -0
  103. package/js/tables/table-cell.js +198 -0
  104. package/js/tables/table-row.js +126 -0
  105. package/js/tables/table.js +492 -0
  106. package/js/tables/tables.js +36 -0
  107. package/js/theme.js +304 -0
  108. package/js/utils/ajax.js +126 -0
  109. package/js/utils/array.js +194 -0
  110. package/js/utils/colors.js +445 -0
  111. package/js/utils/cookies.js +65 -0
  112. package/js/utils/crypto.js +439 -0
  113. package/js/utils/css.js +234 -0
  114. package/js/utils/date.js +300 -0
  115. package/js/utils/files.js +27 -0
  116. package/js/utils/gestures.js +165 -0
  117. package/js/utils/html.js +76 -0
  118. package/js/utils/misc.js +81 -0
  119. package/js/utils/object.js +324 -0
  120. package/js/utils/promise.js +49 -0
  121. package/js/utils/string.js +192 -0
  122. package/js/utils/url.js +187 -0
  123. package/js/utils/utils.js +3 -0
  124. package/js/utils/visibility-manager.js +211 -0
  125. package/js/views/common-views/common-views.js +7 -0
  126. package/js/views/common-views/icon-label-view.js +210 -0
  127. package/js/views/common-views/progress-view.js +89 -0
  128. package/js/views/common-views/structured-view.js +368 -0
  129. package/js/views/view.js +467 -0
  130. package/js/views/views.js +3 -0
  131. package/js/widgets/base-widget.js +23 -0
  132. package/js/widgets/common-widgets/check-button.js +109 -0
  133. package/js/widgets/common-widgets/choice-buttons.js +322 -0
  134. package/js/widgets/common-widgets/color-combo.js +104 -0
  135. package/js/widgets/common-widgets/combo.js +645 -0
  136. package/js/widgets/common-widgets/common-widgets.js +17 -0
  137. package/js/widgets/common-widgets/email-input.js +7 -0
  138. package/js/widgets/common-widgets/file-picker.js +133 -0
  139. package/js/widgets/common-widgets/labels.js +132 -0
  140. package/js/widgets/common-widgets/numeric-input.js +49 -0
  141. package/js/widgets/common-widgets/password-input.js +91 -0
  142. package/js/widgets/common-widgets/phone-input.js +7 -0
  143. package/js/widgets/common-widgets/progress-disk.js +174 -0
  144. package/js/widgets/common-widgets/push-button.js +155 -0
  145. package/js/widgets/common-widgets/slider.js +455 -0
  146. package/js/widgets/common-widgets/text-area.js +52 -0
  147. package/js/widgets/common-widgets/text-input.js +174 -0
  148. package/js/widgets/common-widgets/up-down.js +351 -0
  149. package/js/widgets/widgets.js +57 -0
  150. package/js/window.js +557 -0
  151. package/jsdoc.conf.json +20 -0
  152. package/less/base.less +123 -0
  153. package/less/forms/common-fields.less +101 -0
  154. package/less/forms/common-forms.less +5 -0
  155. package/less/forms/form-button.less +21 -0
  156. package/less/forms/form-field.less +266 -0
  157. package/less/forms/form.less +131 -0
  158. package/less/global-glass.less +64 -0
  159. package/less/icon-label-view.less +82 -0
  160. package/less/icons.less +144 -0
  161. package/less/lists.less +105 -0
  162. package/less/main-ui.less +328 -0
  163. package/less/messages.less +189 -0
  164. package/less/no-effects.less +24 -0
  165. package/less/pages/breadcrumbs.less +98 -0
  166. package/less/pages/common-pages.less +36 -0
  167. package/less/pages/page.less +70 -0
  168. package/less/progress-view.less +51 -0
  169. package/less/stock-icons.less +43 -0
  170. package/less/structured-view.less +245 -0
  171. package/less/tables.less +84 -0
  172. package/less/theme-dark.less +133 -0
  173. package/less/theme-light.less +132 -0
  174. package/less/theme.less +419 -0
  175. package/less/visibility-manager.less +11 -0
  176. package/less/widgets/check-button.less +96 -0
  177. package/less/widgets/choice-buttons.less +160 -0
  178. package/less/widgets/color-combo.less +33 -0
  179. package/less/widgets/combo.less +230 -0
  180. package/less/widgets/common-buttons.less +120 -0
  181. package/less/widgets/common.less +24 -0
  182. package/less/widgets/input.less +258 -0
  183. package/less/widgets/labels.less +81 -0
  184. package/less/widgets/progress-disk.less +70 -0
  185. package/less/widgets/slider.less +199 -0
  186. package/less/widgets/updown.less +115 -0
  187. package/less/widgets/various.less +36 -0
  188. package/package.json +47 -0
  189. package/pyproject.toml +45 -0
  190. package/qui/__init__.py +110 -0
  191. package/qui/constants.py +1 -0
  192. package/qui/exceptions.py +2 -0
  193. package/qui/j2template.py +71 -0
  194. package/qui/settings.py +60 -0
  195. package/qui/templates/manifest.json +25 -0
  196. package/qui/templates/qui.html +126 -0
  197. package/qui/templates/service-worker.js +188 -0
  198. package/qui/web/__init__.py +0 -0
  199. package/qui/web/tornado.py +220 -0
  200. package/scripts/postinstall.sh +10 -0
  201. package/webpack/webpack-adjust-css-urls-loader.js +36 -0
  202. package/webpack/webpack-common.js +384 -0
@@ -0,0 +1,60 @@
1
+ import logging
2
+
3
+ from typing import Any
4
+
5
+ from tornado.httpserver import HTTPRequest
6
+
7
+ from qui import constants, exceptions
8
+
9
+
10
+ DEFAULT_THEME_COLOR = "#62abea"
11
+ DEFAULT_BACKGROUND_COLOR = "#444444"
12
+
13
+ DEFAULT_FRONTEND_DIR = "frontend"
14
+ DEFAULT_FRONTEND_URL_PREFIX = "frontend"
15
+
16
+ DEFAULT_STATIC_URL = "{frontend_url_prefix}/static"
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ name: str = ""
21
+ display_name: str = ""
22
+ display_short_name: str = ""
23
+ description: str = ""
24
+ version: str = ""
25
+ debug: bool = False
26
+ theme_color: str = DEFAULT_THEME_COLOR
27
+ background_color: str = DEFAULT_BACKGROUND_COLOR
28
+ frontend_dir: str = DEFAULT_FRONTEND_DIR
29
+ frontend_url_prefix: str = DEFAULT_FRONTEND_URL_PREFIX
30
+ static_url: str = DEFAULT_STATIC_URL
31
+ package_name: str = ""
32
+ enable_pwa: bool = True
33
+ extra_context: dict[str, Any] = {}
34
+ build_hash = None
35
+
36
+
37
+ def make_context(request: HTTPRequest) -> dict[str, Any]:
38
+ if not name:
39
+ raise exceptions.QUIException("QUI not configured")
40
+
41
+ base_prefix = request.headers.get(constants.BASE_PREFIX_HEADER, "/")
42
+ if not base_prefix.endswith("/"):
43
+ base_prefix += "/"
44
+
45
+ return {
46
+ "name": name,
47
+ "display_name": display_name,
48
+ "display_short_name": display_short_name,
49
+ "description": description,
50
+ "version": version,
51
+ "debug": debug,
52
+ "theme_color": theme_color,
53
+ "background_color": background_color,
54
+ "navigation_base_prefix": f"{base_prefix}{frontend_url_prefix}",
55
+ "static_url": static_url,
56
+ "enable_pwa": enable_pwa,
57
+ "themes": ["dark", "light"],
58
+ "build_hash": build_hash,
59
+ **extra_context,
60
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "lang": "en-US",
3
+ {% if display_short_name or display_name -%}"short_name": "{{ display_short_name or display_name }}",{%- endif %}
4
+ {% if display_name or display_short_name -%}"name": "{{ display_name or display_short_name }}",{%- endif %}
5
+ {% if description -%}"description": "{{ description }}",{%- endif %}
6
+ {% if version -%}"version": "{{ version }}",{%- endif %}
7
+ {% if theme_color -%}"theme_color": "{{ theme_color }}",{%- endif %}
8
+ {% if background_color -%}"background_color": "{{ background_color }}",{%- endif %}
9
+ "icons": [
10
+ {"src": "{{ static_url }}/img/launcher-icon-16.png", "sizes": "16x16", "type": "image/png"},
11
+ {"src": "{{ static_url }}/img/launcher-icon-32.png", "sizes": "32x32", "type": "image/png"},
12
+ {"src": "{{ static_url }}/img/launcher-icon-36.png", "sizes": "36x36", "type": "image/png"},
13
+ {"src": "{{ static_url }}/img/launcher-icon-48.png", "sizes": "48x48", "type": "image/png"},
14
+ {"src": "{{ static_url }}/img/launcher-icon-64.png", "sizes": "64x64", "type": "image/png"},
15
+ {"src": "{{ static_url }}/img/launcher-icon-72.png", "sizes": "72x72", "type": "image/png"},
16
+ {"src": "{{ static_url }}/img/launcher-icon-96.png", "sizes": "96x96", "type": "image/png"},
17
+ {"src": "{{ static_url }}/img/launcher-icon-144.png", "sizes": "144x144", "type": "image/png"},
18
+ {"src": "{{ static_url }}/img/launcher-icon-192.png", "sizes": "192x192", "type": "image/png"},
19
+ {"src": "{{ static_url }}/img/launcher-icon-256.png", "sizes": "256x256", "type": "image/png"},
20
+ {"src": "{{ static_url }}/img/launcher-icon-384.png", "sizes": "384x384", "type": "image/png"},
21
+ {"src": "{{ static_url }}/img/launcher-icon-512.png", "sizes": "512x512", "type": "image/png"}
22
+ ],
23
+ "start_url": ".",
24
+ "display": "standalone"
25
+ }
@@ -0,0 +1,126 @@
1
+
2
+ {% macro css_devel_file(theme, path) -%}
3
+ <link rel="stylesheet"
4
+ type="text/css"
5
+ href="{{ static_url }}/css/{{ theme }}/{{ path }}?h={{ build_hash }}"
6
+ disabled
7
+ theme="{{ theme }}"/>
8
+ {%- endmacro %}
9
+
10
+ {% macro main_script_file(params={}) -%}
11
+ <script type="module"
12
+ src="{{ static_url }}{% if debug %}/js/index.js{% else %}/{{ name }}-bundle.js{% endif %}?h={{ build_hash }}"
13
+ data-app-name="{{ name }}"
14
+ data-app-latest-version="{{ version }}"
15
+ data-app-display-name="{{ display_name }}"
16
+ data-debug="{{ debug|lower }}"
17
+ data-navigation-base-prefix="{{ navigation_base_prefix }}"
18
+ data-themes="{{ themes|join(',') }}"
19
+ data-build-hash="{{ build_hash }}"
20
+ {% for n, v in params.items() -%}
21
+ {{ n }}="{{ v }}"
22
+ {% endfor %}>
23
+ </script>
24
+ {%- endmacro %}
25
+
26
+ <!DOCTYPE HTML>
27
+
28
+ <html lang="en">
29
+ <head>
30
+ <title>{% block title %}{{ display_name }}{% endblock %}</title>
31
+
32
+ {% if enable_pwa %}
33
+ <link rel="manifest" href="{{ navigation_base_prefix }}/manifest.json?display_name={{
34
+ display_name|urlquote }}&theme_color={{
35
+ theme_color|urlquote }}&background_color={{
36
+ background_color|urlquote }}&description={{
37
+ description|urlquote }}&h={{ build_hash }}">
38
+ {% endif %}
39
+ <link rel="icon" href="{{ static_url }}/img/launcher-icon-32.png">
40
+ <link rel="apple-touch-icon" href="{{ static_url }}/img/launcher-icon-32.png">
41
+
42
+ {% block _meta %}
43
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
44
+ <meta name="viewport" content="initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
45
+ <meta name="mobile-web-app-capable" content="yes">
46
+ <meta name="apple-mobile-web-app-capable" content="yes">
47
+ <meta name="theme-color" content="{{ theme_color }}">
48
+ <meta name="apple-mobile-web-app-status-bar-style" content="{{ theme_color }}">
49
+ {% endblock %}
50
+ {% block meta %}{% endblock %}
51
+
52
+ {% block _style %}
53
+ <style>
54
+ html {
55
+ background: #444444;
56
+ }
57
+ body {
58
+ opacity: 0;
59
+ }
60
+ </style>
61
+ {% for theme in themes %}
62
+ {% if debug %}
63
+ {{ css_devel_file(theme, "base.css") }}
64
+ {{ css_devel_file(theme, "main-ui.css") }}
65
+ {{ css_devel_file(theme, "no-effects.css") }}
66
+ {{ css_devel_file(theme, "visibility-manager.css") }}
67
+ {{ css_devel_file(theme, "icons.css") }}
68
+
69
+ {{ css_devel_file(theme, "widgets/check-button.css") }}
70
+ {{ css_devel_file(theme, "widgets/choice-buttons.css") }}
71
+ {{ css_devel_file(theme, "widgets/color-combo.css") }}
72
+ {{ css_devel_file(theme, "widgets/combo.css") }}
73
+ {{ css_devel_file(theme, "widgets/common.css") }}
74
+ {{ css_devel_file(theme, "widgets/common-buttons.css") }}
75
+ {{ css_devel_file(theme, "widgets/labels.css") }}
76
+ {{ css_devel_file(theme, "widgets/progress-disk.css") }}
77
+ {{ css_devel_file(theme, "widgets/slider.css") }}
78
+ {{ css_devel_file(theme, "widgets/input.css") }}
79
+ {{ css_devel_file(theme, "widgets/updown.css") }}
80
+ {{ css_devel_file(theme, "widgets/various.css") }}
81
+
82
+ {{ css_devel_file(theme, "messages.css") }}
83
+ {{ css_devel_file(theme, "global-glass.css") }}
84
+ {{ css_devel_file(theme, "structured-view.css") }}
85
+ {{ css_devel_file(theme, "progress-view.css") }}
86
+ {{ css_devel_file(theme, "icon-label-view.css") }}
87
+
88
+ {{ css_devel_file(theme, "pages/breadcrumbs.css") }}
89
+ {{ css_devel_file(theme, "pages/common-pages.css") }}
90
+ {{ css_devel_file(theme, "pages/page.css") }}
91
+
92
+ {{ css_devel_file(theme, "forms/common-fields.css") }}
93
+ {{ css_devel_file(theme, "forms/common-forms.css") }}
94
+ {{ css_devel_file(theme, "forms/form.css") }}
95
+ {{ css_devel_file(theme, "forms/form-button.css") }}
96
+ {{ css_devel_file(theme, "forms/form-field.css") }}
97
+
98
+ {{ css_devel_file(theme, "lists.css") }}
99
+ {{ css_devel_file(theme, "tables.css") }}
100
+
101
+ {{ css_devel_file(theme, "theme.css") }}
102
+ {% else %}
103
+ <link rel="stylesheet"
104
+ type="text/css"
105
+ href="{{ static_url }}/{{ name }}-bundle-{{ theme }}.css?h={{ build_hash }}"
106
+ disabled
107
+ theme="{{ theme }}"/>
108
+ {% endif %}
109
+ {% endfor %}
110
+ {% endblock %}
111
+ {% block style %}{% endblock %}
112
+
113
+ <script type="text/javascript">
114
+ if (localStorage) {
115
+ document.documentElement.style.background = localStorage.getItem('theme.background-color') || ''
116
+ }
117
+ </script>
118
+
119
+ <noscript>This application requires JavaScript!</noscript>
120
+
121
+ {% block script %}
122
+ {{ main_script_file() }}
123
+ {% endblock %}
124
+ </head>
125
+ <body></body>
126
+ </html>
@@ -0,0 +1,188 @@
1
+ /*
2
+ * This JS file is run in a service worker context.
3
+ */
4
+
5
+ /* eslint-disable no-undef */
6
+
7
+ const MESSAGE_ACTIVATE = 'qui-activate'
8
+ const DEF_APP_NAME = 'qui-app'
9
+ const DEF_APP_VERSION = 'unknown-version'
10
+ const DEF_BUILD_HASH = 'unknown-hash'
11
+ const DEF_CACHE_URL_REGEX = '.*\\.(svg|png|gif|jpg|jpe?g|ico|woff|html|json|js|css)$'
12
+
13
+
14
+ let cacheName
15
+ let debug = false
16
+ let appName = '__app_name_placeholder__'
17
+ let appVersion = '__app_version_placeholder__'
18
+ let buildHash = '__build_hash_placeholder__'
19
+ let cacheURLRegex = '__cache_url_regex_placeholder__'
20
+ let queryArguments = null
21
+
22
+
23
+ function getQueryArgument(name, def = null) {
24
+ if (queryArguments == null) {
25
+ let queryString = self.location.search.substring(1)
26
+ queryArguments = new Map(queryString.split('&').map(function (keyValuePair) {
27
+ let splits = keyValuePair.split('=');
28
+ let key = decodeURIComponent(splits[0]);
29
+ let value = decodeURIComponent(splits[1]);
30
+ if (value.indexOf(',') >= 0) {
31
+ value = value.split(',');
32
+ }
33
+
34
+ return [key, value];
35
+ }))
36
+ }
37
+
38
+ let value = queryArguments.get(name)
39
+ if (value == null) {
40
+ value = def
41
+ }
42
+
43
+ return value
44
+ }
45
+
46
+ function setup() {
47
+ /* If webpack did not replace the placeholder... */
48
+ if (appName.startsWith('__app_name_')) {
49
+ appName = DEF_APP_NAME
50
+ }
51
+ if (appVersion.startsWith('__app_version_')) {
52
+ appVersion = DEF_APP_VERSION
53
+ }
54
+ if (buildHash.startsWith('__build_hash_')) {
55
+ buildHash = getQueryArgument('h', DEF_BUILD_HASH)
56
+ }
57
+ if (cacheURLRegex.startsWith('__cache_url_regex_')) {
58
+ cacheURLRegex = DEF_CACHE_URL_REGEX
59
+ }
60
+ if (getQueryArgument('debug') === 'true') {
61
+ debug = true
62
+ }
63
+
64
+ /* Transform into RegExp instance */
65
+ cacheURLRegex = new RegExp(cacheURLRegex, 'i')
66
+
67
+ cacheName = `${appName}-cache-${buildHash}`
68
+ logDebug(`using cache name ${cacheName}`)
69
+ }
70
+
71
+
72
+ function formatLogMessage(message) {
73
+ return 'Service Worker: ' + message
74
+ }
75
+
76
+ function logDebug(message) {
77
+ console.debug(formatLogMessage(message))
78
+ }
79
+
80
+ function logInfo(message) {
81
+ console.info(formatLogMessage(message))
82
+ }
83
+
84
+ function logWarn(message) {
85
+ console.warn(formatLogMessage(message))
86
+ }
87
+
88
+ function logError(message) {
89
+ console.error(formatLogMessage(message))
90
+ }
91
+
92
+
93
+ function sendClientMessage(message, uncontrolled = false) {
94
+ self.clients.matchAll({includeUncontrolled: uncontrolled}).then(function (clients) {
95
+ clients.forEach(function (client) {
96
+ client.postMessage(message)
97
+ })
98
+ })
99
+ }
100
+
101
+ function shouldCacheRequest(request) {
102
+ return request.url.match(cacheURLRegex) && (request.method === 'GET')
103
+ }
104
+
105
+ function shouldCacheResponse(response) {
106
+ return (response &&
107
+ response.status === 200 &&
108
+ response.type === 'basic')
109
+ }
110
+
111
+
112
+ self.addEventListener('activate', function (event) {
113
+
114
+ function deleteCache(name) {
115
+ logDebug(`deleting cache ${name}`)
116
+ caches.delete(name)
117
+ }
118
+
119
+ event.waitUntil(
120
+ /* Become available to all pages */
121
+ self.clients.claim().then(function () {
122
+ /* Clear all caches starting with our app name */
123
+ caches.keys().then(function (cacheNames) {
124
+ return Promise.all(cacheNames
125
+ .filter(name => name.startsWith(`${appName}-`))
126
+ .map(name => deleteCache(name)))
127
+ })
128
+ })
129
+ )
130
+ })
131
+
132
+ self.addEventListener('fetch', function (event) {
133
+ if (debug) { /* Don't use cache in debug mode */
134
+ return
135
+ }
136
+ if (!shouldCacheRequest(event.request)) {
137
+ return
138
+ }
139
+
140
+ let url = event.request.url
141
+ /* Ensure URL has build hash */
142
+ if (!url.includes('?h=') && !url.includes('&h=')) {
143
+ if (url.includes('?')) {
144
+ url += `&h=${buildHash}`
145
+ }
146
+ else {
147
+ url += `?h=${buildHash}`
148
+ }
149
+ }
150
+
151
+ let request = new Request(url, event.request)
152
+
153
+ event.respondWith(
154
+ caches.match(request, {ignoreSearch: false}).then(function (response) {
155
+ if (response) {
156
+ return response
157
+ }
158
+
159
+ return fetch(request.clone()).then(function (response) {
160
+ if (!shouldCacheResponse(response)) {
161
+ return response
162
+ }
163
+
164
+ let responseToCache = response.clone()
165
+ caches.open(cacheName).then(function (cache) {
166
+ cache.put(request.clone(), responseToCache)
167
+ })
168
+
169
+ return response.clone()
170
+ })
171
+ })
172
+ )
173
+ })
174
+
175
+ self.addEventListener('message', function (message) {
176
+ switch (message.data.type) {
177
+ case MESSAGE_ACTIVATE:
178
+ logInfo('received activation message')
179
+ self.skipWaiting()
180
+ break
181
+
182
+ default:
183
+ logWarn(`unexpected service worker message ${message.data.type}`)
184
+ }
185
+ })
186
+
187
+
188
+ setup()
File without changes
@@ -0,0 +1,220 @@
1
+ import logging
2
+ import os
3
+ import re
4
+
5
+ from re import Match
6
+ from typing import Any
7
+
8
+ from tornado.web import RequestHandler, StaticFileHandler, URLSpec
9
+
10
+ from qui import __file__ as qui_package_path
11
+ from qui import constants, exceptions, j2template, settings
12
+
13
+
14
+ JS_MODULE_PATH_RE = re.compile(rb"\'(\$qui|\$app|\$node|)([/.][A-Za-z0-9_./$-]+\.jsm?)\'")
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class TemplateHandler(RequestHandler):
20
+ def prepare(self) -> None:
21
+ self.set_header("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
22
+
23
+ def get_context(self, path: str = "", path_offs: int = 0) -> dict[str, Any]:
24
+ context = settings.make_context(self.request)
25
+
26
+ # If using static URL that is relative to frontend URL prefix, adjust it to a relative path matching currently
27
+ # requested frontend path
28
+ static_url = context["static_url"]
29
+ if static_url.startswith(f"{settings.frontend_url_prefix}/"):
30
+ slashes = path.count("/") + path_offs
31
+ if slashes == 0:
32
+ prefix = f"{settings.frontend_url_prefix}/"
33
+ elif slashes > 1:
34
+ prefix = "/".join([".."] * (slashes - 1)) + "/"
35
+
36
+ else:
37
+ prefix = ""
38
+
39
+ context["static_url"] = prefix + static_url[len(settings.frontend_url_prefix) + 1 :]
40
+
41
+ return context
42
+
43
+ async def render(self, template_name: str, context: dict[str, Any] | None = None) -> None:
44
+ if context is None:
45
+ context = self.get_context()
46
+
47
+ env = j2template.get_env()
48
+ template = env.get_template(template_name)
49
+ template_str = await template.render_async(**context)
50
+
51
+ await self.finish(template_str)
52
+
53
+
54
+ class JSModuleMapperStaticFileHandler(StaticFileHandler):
55
+ def __init__(self, *args, **kwargs) -> None:
56
+ self._mapping = {
57
+ b"$qui": f"/{settings.static_url}/qui/js".encode(),
58
+ b"$app": f"/{settings.static_url}/app/js".encode(),
59
+ b"$node": f"/{settings.static_url}/app/node_modules".encode(),
60
+ }
61
+
62
+ self._mapped_content: bytes | None = None
63
+
64
+ super().__init__(*args, **kwargs)
65
+
66
+ def get_content_size(self) -> int:
67
+ return len(self.get_mapped_content())
68
+
69
+ def get_content(self, abspath: str, start: int | None = None, end: int | None = None) -> bytes:
70
+ return self.get_mapped_content()
71
+
72
+ @classmethod
73
+ def get_content_version(cls, abspath: str) -> str:
74
+ return ""
75
+
76
+ def replace_path(self, match: Match[bytes]) -> bytes:
77
+ prefix = match.group(1)
78
+ path = match.group(2)
79
+
80
+ prefix = self._mapping.get(prefix, prefix)
81
+ path = path + f"?h={settings.build_hash}".encode()
82
+
83
+ if path.startswith(b"."):
84
+ base_prefix = b""
85
+ else:
86
+ base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
87
+ base_prefix = base_prefix.rstrip("/").encode()
88
+
89
+ return b"'" + base_prefix + prefix + path + b"'"
90
+
91
+ def get_mapped_content(self) -> bytes:
92
+ if self._mapped_content is None:
93
+ content = b"".join(super().get_content(self.absolute_path))
94
+
95
+ if self.absolute_path.endswith(".js"):
96
+ content = JS_MODULE_PATH_RE.sub(self.replace_path, content)
97
+
98
+ self._mapped_content = content
99
+
100
+ return self._mapped_content
101
+
102
+ def should_return_304(self) -> bool:
103
+ return False
104
+
105
+
106
+ class RedirectFrontendHandler(RequestHandler):
107
+ def get(self) -> None:
108
+ base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
109
+ if not base_prefix.endswith("/"):
110
+ base_prefix += "/"
111
+
112
+ self.redirect(f"{base_prefix}{settings.frontend_url_prefix}/")
113
+
114
+
115
+ class FrontendHandler(TemplateHandler):
116
+ async def get(self, path: str) -> None:
117
+ await self.render("index.html", self.get_context(path))
118
+
119
+
120
+ class ManifestHandler(TemplateHandler):
121
+ PARAMS = [
122
+ "display_name",
123
+ "display_short_name",
124
+ "description",
125
+ "version",
126
+ "theme_color",
127
+ "background_color",
128
+ ]
129
+
130
+ async def get(self) -> None:
131
+ context = self.get_context(path_offs=1)
132
+ for param in self.PARAMS:
133
+ value = self.get_query_argument(param, None)
134
+ if value is not None:
135
+ context[param] = value
136
+
137
+ self.set_header("Content-Type", 'application/manifest+json; charset="utf-8"')
138
+ await self.render("manifest.json", context)
139
+
140
+
141
+ class ServiceWorkerHandler(TemplateHandler):
142
+ async def get(self) -> None:
143
+ self.set_header("Content-Type", 'application/javascript; charset="utf-8"')
144
+ await self.render("service-worker.js")
145
+
146
+
147
+ def make_routing_table() -> list[URLSpec]:
148
+ frontend_dir = settings.frontend_dir
149
+ if not settings.debug: # in production mode, frontend files are found under the dist frontend subfolder
150
+ frontend_dir += "/dist"
151
+
152
+ # Look for frontend dir in all available package dirs
153
+ package = __import__(settings.package_name)
154
+ for package_dir in list(package.__path__):
155
+ frontend_path = os.path.join(package_dir, frontend_dir)
156
+ if os.path.exists(frontend_path):
157
+ break
158
+
159
+ else:
160
+ raise exceptions.QUIException(f"Cannot find frontend dir in package {settings.package_name}")
161
+
162
+ spec_list = []
163
+
164
+ static_url = settings.static_url
165
+ if not static_url.startswith("/"):
166
+ static_url = f"/{static_url}"
167
+
168
+ if settings.debug:
169
+ # In debug mode, we serve QUI static files from QUI folder
170
+ qui_path = os.path.join(os.path.dirname(qui_package_path), "..")
171
+ qui_path = os.path.abspath(qui_path)
172
+
173
+ spec_list.append(
174
+ URLSpec(
175
+ rf"^{static_url}/qui/(.*)$",
176
+ JSModuleMapperStaticFileHandler,
177
+ {"path": qui_path},
178
+ name="static-qui",
179
+ )
180
+ )
181
+
182
+ spec_list.append(
183
+ URLSpec(
184
+ rf"^{static_url}/(?:app/)?(.*)$",
185
+ JSModuleMapperStaticFileHandler,
186
+ {"path": frontend_path},
187
+ name="static-app",
188
+ )
189
+ )
190
+
191
+ else:
192
+ spec_list.append(
193
+ URLSpec(
194
+ rf"^{static_url}/(.*)$",
195
+ StaticFileHandler,
196
+ {"path": frontend_path},
197
+ name="static",
198
+ )
199
+ )
200
+
201
+ spec_list += [
202
+ URLSpec(r"^/?$", RedirectFrontendHandler, name="redirect-frontend"),
203
+ URLSpec(
204
+ rf"^/{settings.frontend_url_prefix}/service-worker.js$",
205
+ ServiceWorkerHandler,
206
+ name="service-worker",
207
+ ),
208
+ URLSpec(
209
+ rf"^/{settings.frontend_url_prefix}/manifest.json$",
210
+ ManifestHandler,
211
+ name="manifest",
212
+ ),
213
+ URLSpec(
214
+ rf"^/{settings.frontend_url_prefix}(?P<path>.*)",
215
+ FrontendHandler,
216
+ name="frontend",
217
+ ),
218
+ ]
219
+
220
+ return spec_list
@@ -0,0 +1,10 @@
1
+ #!/bin/sh
2
+
3
+ # Create symlinks to libs
4
+ echo "Creating symlinks to libs"
5
+ cd js/lib
6
+ ln -sf ../../../../jquery/dist/jquery.js .
7
+ ln -sf ../../../../jquery-mousewheel/jquery.mousewheel.js .
8
+ ln -sf ../../../../jquery-ui-dist/jquery-ui.js
9
+ ln -sf ../../../../js-logger/src/logger.js
10
+ ln -sf ../../../../pepjs/dist/pep.js
@@ -0,0 +1,36 @@
1
+
2
+ const loaderUtils = require('loader-utils')
3
+
4
+
5
+ module.exports = function (source) {
6
+ const options = loaderUtils.getOptions(this)
7
+ const appLessPath = options.appLessPath
8
+ const quiLessPath = options.quiLessPath
9
+
10
+ let filename
11
+ if (this._module.resource.startsWith(appLessPath)) {
12
+ filename = this._module.resource.slice(appLessPath.length)
13
+ }
14
+ else if (this._module.resource.startsWith(quiLessPath)) {
15
+ filename = this._module.resource.slice(quiLessPath.length)
16
+ }
17
+ else { /* Cannot identify source file root path */
18
+ return source
19
+ }
20
+
21
+ let dirsInPath = (filename.match(/\//g) || []).length - 1
22
+ if (dirsInPath < 0) {
23
+ return source
24
+ }
25
+
26
+ dirsInPath += options.additionalDirs || 0
27
+
28
+ /* Generate a sequence of "../" as long as the number of dirs in path */
29
+ let relPath = [...Array(dirsInPath).keys()].map(() => '../').join('')
30
+ if (!relPath) {
31
+ return source
32
+ }
33
+
34
+ /* Adjust any url('...') by prefixing with the relative path, unless we deal with absolute paths */
35
+ return source.replace(new RegExp('url\\(([\'"])?(?!http)(./)?([^/].*)', 'g'), 'url($1' + relPath + '$3')
36
+ }