x4js 1.4.14 → 1.4.17

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.
@@ -75,6 +75,8 @@ export declare class Application<P extends ApplicationProps = ApplicationProps,
75
75
  private m_app_uid;
76
76
  private m_local_storage;
77
77
  private m_user_data;
78
+ private m_touch_time;
79
+ private m_touch_count;
78
80
  constructor(props: P);
79
81
  ApplicationCreated(): void;
80
82
  get app_name(): string;
@@ -96,5 +98,6 @@ export declare class Application<P extends ApplicationProps = ApplicationProps,
96
98
  setTitle(title: string): void;
97
99
  disableZoomWheel(): void;
98
100
  enterModal(enter: boolean): void;
101
+ enableTouchDblClick(): void;
99
102
  }
100
103
  export {};
@@ -30,8 +30,10 @@
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
31
  exports.Application = void 0;
32
32
  const base_component_1 = require("./base_component");
33
+ const component_1 = require("./component");
33
34
  const settings_1 = require("./settings");
34
35
  const tools_1 = require("./tools");
36
+ const _x4_touch_time = Symbol();
35
37
  /**
36
38
  * Represents an x4 application, which is typically a single page app.
37
39
  * You should inherit Application to define yours.
@@ -65,6 +67,8 @@ class Application extends base_component_1.BaseComponent {
65
67
  m_app_uid;
66
68
  m_local_storage;
67
69
  m_user_data;
70
+ m_touch_time;
71
+ m_touch_count;
68
72
  constructor(props) {
69
73
  console.assert(Application.self === null, 'application is a singleton');
70
74
  super(props);
@@ -74,6 +78,8 @@ class Application extends base_component_1.BaseComponent {
74
78
  let settings_name = `${this.m_app_name}.${this.m_app_version}.settings`;
75
79
  this.m_local_storage = new settings_1.Settings(settings_name);
76
80
  this.m_user_data = {};
81
+ this.m_touch_time = 0;
82
+ this.m_touch_count = 0;
77
83
  Application.self = this;
78
84
  if ('onload' in globalThis) {
79
85
  globalThis.addEventListener('load', () => {
@@ -145,6 +151,30 @@ class Application extends base_component_1.BaseComponent {
145
151
  }
146
152
  enterModal(enter) {
147
153
  }
154
+ enableTouchDblClick() {
155
+ document.addEventListener('touchstart', (ev) => {
156
+ let now = Date.now();
157
+ if ((now - this.m_touch_time) > 700) {
158
+ this.m_touch_count = 1;
159
+ }
160
+ else {
161
+ this.m_touch_count++;
162
+ }
163
+ this.m_touch_time = now;
164
+ if (this.m_touch_count == 2) {
165
+ this.m_touch_count = 0;
166
+ // dirty fake dblclick event
167
+ const tch = ev.touches[0];
168
+ let fake = { type: "dblclick" };
169
+ for (const n in tch) {
170
+ fake[n] = tch[n];
171
+ }
172
+ // ignore -> private: dirty x2
173
+ component_1.Component._dispatchEvent(fake);
174
+ ev.stopPropagation();
175
+ }
176
+ });
177
+ }
148
178
  }
149
179
  exports.Application = Application;
150
180
  ;
@@ -107,7 +107,6 @@ export declare class Component<P extends CProps<BaseComponentEventMap> = CProps<
107
107
  private static __privateEvents;
108
108
  private static __sizeObserver;
109
109
  private static __createObserver;
110
- private static __intersectionObserver;
111
110
  private static __capture;
112
111
  private static __capture_mask;
113
112
  private static __css;
package/lib/component.js CHANGED
@@ -89,7 +89,7 @@ class Component extends base_component_1.BaseComponent {
89
89
  static __privateEvents = {};
90
90
  static __sizeObserver; // resize observer
91
91
  static __createObserver; // creation observer
92
- static __intersectionObserver; // visibility observer
92
+ //private static __intersectionObserver: IntersectionObserver; // visibility observer
93
93
  static __capture = null;
94
94
  static __capture_mask = null;
95
95
  static __css = null;
@@ -101,6 +101,10 @@ class Component extends base_component_1.BaseComponent {
101
101
  uid: Component.__comp_guid++,
102
102
  inrender: false,
103
103
  };
104
+ // prepare iprops
105
+ if (this.m_props.cls) {
106
+ this.addClass(this.m_props.cls);
107
+ }
104
108
  }
105
109
  /**
106
110
  *
@@ -1042,7 +1046,8 @@ class Component extends base_component_1.BaseComponent {
1042
1046
  this.addClass('@' + (0, tools_1.pascalCase)(clsname));
1043
1047
  me = Object.getPrototypeOf(me);
1044
1048
  }
1045
- this.addClass(this.m_props.cls);
1049
+ //done in ctor now
1050
+ //this.addClass(this.m_props.cls);
1046
1051
  }
1047
1052
  /**
1048
1053
  * prepend the system class name prefix on a name if needed (if class starts with @)
package/lib/layout.js CHANGED
@@ -278,10 +278,17 @@ exports.ScrollView = ScrollView;
278
278
  // https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb
279
279
  class Masonry extends component_1.Container {
280
280
  constructor(props) {
281
+ const items = props.items;
282
+ props.items = undefined;
281
283
  super(props);
282
284
  this.setDomEvent('sizechange', () => {
283
285
  this.resizeAllItems();
284
286
  });
287
+ if (items) {
288
+ items.forEach(i => {
289
+ this.addItem(i);
290
+ });
291
+ }
285
292
  }
286
293
  resizeItem(item) {
287
294
  const style = this.getComputedStyle();
package/lib/listview.d.ts CHANGED
@@ -83,7 +83,7 @@ export interface ListViewProps<E extends ListViewEventMap = ListViewEventMap> ex
83
83
  /**
84
84
  * Standard listview class
85
85
  */
86
- export declare class ListView<T extends ListViewProps = ListViewProps, E extends ListViewEventMap = ListViewEventMap> extends VLayout<T, E> {
86
+ export declare class ListView extends VLayout<ListViewProps, ListViewEventMap> {
87
87
  protected m_selection: {
88
88
  item: ListViewItem;
89
89
  citem: Component;
@@ -94,7 +94,7 @@ export declare class ListView<T extends ListViewProps = ListViewProps, E extends
94
94
  protected m_topIndex: number;
95
95
  protected m_itemHeight: number;
96
96
  protected m_cache: Map<number, Component>;
97
- constructor(props: T);
97
+ constructor(props: ListViewProps);
98
98
  componentCreated(): void;
99
99
  render(props: ListViewProps): void;
100
100
  /**
package/lib/router.d.ts CHANGED
@@ -26,9 +26,17 @@
26
26
  * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
27
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
28
  **/
29
- export declare class Router {
29
+ import { EventSource, EvError, EventMap } from "./x4_events";
30
+ declare type RouteHandler = (params: any, path: string) => void;
31
+ interface RouterEventMap extends EventMap {
32
+ error: EvError;
33
+ }
34
+ export declare class Router extends EventSource<RouterEventMap> {
30
35
  private routes;
31
36
  constructor();
32
- get(uri: any, callback: any): void;
37
+ get(uri: string | RegExp, handler: RouteHandler): void;
33
38
  init(): void;
39
+ navigate(uri: string, notify?: boolean): void;
40
+ private _find;
34
41
  }
42
+ export {};
package/lib/router.js CHANGED
@@ -29,31 +29,111 @@
29
29
  **/
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
31
  exports.Router = void 0;
32
- class Router {
32
+ const x4_events_1 = require("./x4_events");
33
+ function parseRoute(str, loose = false) {
34
+ if (str instanceof RegExp) {
35
+ return {
36
+ keys: null,
37
+ pattern: str
38
+ };
39
+ }
40
+ const arr = str.split('/');
41
+ let keys = [];
42
+ let pattern = '';
43
+ if (arr[0] == '') {
44
+ arr.shift();
45
+ }
46
+ for (const tmp of arr) {
47
+ const c = tmp[0];
48
+ if (c === '*') {
49
+ keys.push('wild');
50
+ pattern += '/(.*)';
51
+ }
52
+ else if (c === ':') {
53
+ const o = tmp.indexOf('?', 1);
54
+ const ext = tmp.indexOf('.', 1);
55
+ keys.push(tmp.substring(1, o >= 0 ? o : ext >= 0 ? ext : tmp.length));
56
+ pattern += o >= 0 && ext < 0 ? '(?:/([^\/]+?))?' : '/([^\/]+?)';
57
+ if (ext >= 0) {
58
+ pattern += (o >= 0 ? '?' : '') + '\\' + tmp.substring(ext);
59
+ }
60
+ }
61
+ else {
62
+ pattern += '/' + tmp;
63
+ }
64
+ }
65
+ return {
66
+ keys,
67
+ pattern: new RegExp(`^${pattern}${loose ? '(?=$|\/)' : '\/?$'}`, 'i')
68
+ };
69
+ }
70
+ class Router extends x4_events_1.EventSource {
33
71
  routes;
34
72
  constructor() {
73
+ super();
35
74
  this.routes = [];
36
- }
37
- get(uri, callback) {
38
- // throw an error if the route uri already exists to avoid confilicting routes
39
- this.routes.forEach(route => {
40
- if (route.uri === uri) {
41
- throw new Error(`the uri ${route.uri} already exists`);
42
- }
43
- });
44
- this.routes.push({
45
- uri,
46
- callback
75
+ window.addEventListener('popstate', (event) => {
76
+ const url = document.location.pathname;
77
+ const found = this._find(url);
78
+ found.handlers.forEach(h => {
79
+ h(found.params, url);
80
+ });
47
81
  });
48
82
  }
83
+ get(uri, handler) {
84
+ let { keys, pattern } = parseRoute(uri);
85
+ this.routes.push({ keys, pattern, handler });
86
+ }
49
87
  init() {
50
- this.routes.some(route => {
51
- let regEx = new RegExp(`^${route.uri}$`);
52
- let path = window.location.pathname;
53
- if (path.match(regEx)) {
54
- return route.callback({ path });
88
+ this.navigate(window.location.pathname);
89
+ }
90
+ navigate(uri, notify = true) {
91
+ const found = this._find(uri);
92
+ if (!found || found.handlers.length == 0) {
93
+ //window.history.pushState({}, '', 'error')
94
+ console.log('route not found: ' + uri);
95
+ this.signal("error", (0, x4_events_1.EvError)(404, "route not found"));
96
+ return;
97
+ }
98
+ window.history.pushState({}, '', uri);
99
+ if (notify) {
100
+ found.handlers.forEach(h => {
101
+ h(found.params, uri);
102
+ });
103
+ }
104
+ }
105
+ _find(url) {
106
+ let matches = [];
107
+ let params = {};
108
+ let handlers = [];
109
+ for (const tmp of this.routes) {
110
+ if (!tmp.keys) {
111
+ matches = tmp.pattern.exec(url);
112
+ if (!matches) {
113
+ continue;
114
+ }
115
+ if (matches['groups']) {
116
+ for (const k in matches['groups']) {
117
+ params[k] = matches['groups'][k];
118
+ }
119
+ }
120
+ handlers = [...handlers, tmp.handler];
55
121
  }
56
- });
122
+ else if (tmp.keys.length > 0) {
123
+ matches = tmp.pattern.exec(url);
124
+ if (matches === null) {
125
+ continue;
126
+ }
127
+ for (let j = 0; j < tmp.keys.length;) {
128
+ params[tmp.keys[j]] = matches[++j];
129
+ }
130
+ handlers = [...handlers, tmp.handler];
131
+ }
132
+ else if (tmp.pattern.test(url)) {
133
+ handlers = [...handlers, tmp.handler];
134
+ }
135
+ }
136
+ return { params, handlers };
57
137
  }
58
138
  }
59
139
  exports.Router = Router;
package/lib/tabbar.d.ts CHANGED
@@ -48,9 +48,11 @@ export declare class TabBar extends Container<TabBarProps, TabBarEventMap> {
48
48
  private m_pages;
49
49
  private m_curPage;
50
50
  constructor(props: TabBarProps);
51
+ componentCreated(): void;
51
52
  addPage(page: ITabPage): void;
52
53
  render(): void;
53
- select(id: string): void;
54
+ select(id: string | null, notify?: boolean): boolean;
54
55
  private _select;
56
+ get selection(): Component<CProps<import("./base_component").BaseComponentEventMap>, import("./base_component").BaseComponentEventMap>;
55
57
  }
56
58
  export {};
package/lib/tabbar.js CHANGED
@@ -47,8 +47,10 @@ class TabBar extends component_1.Container {
47
47
  this.addClass('@hlayout');
48
48
  }
49
49
  this.m_props.pages?.forEach(p => this.addPage(p));
50
+ }
51
+ componentCreated() {
50
52
  if (this.m_props.default) {
51
- this.select(this.m_props.default);
53
+ this.select(this.m_props.default, true);
52
54
  }
53
55
  }
54
56
  addPage(page) {
@@ -58,28 +60,52 @@ class TabBar extends component_1.Container {
58
60
  render() {
59
61
  let buttons = [];
60
62
  this.m_pages.forEach(p => {
61
- p.btn = new button_1.Button({ cls: p === this.m_curPage ? 'selected' : '', text: p.title, icon: p.icon, click: () => this._select(p) });
63
+ p.btn = new button_1.Button({ cls: p === this.m_curPage ? 'selected' : '', text: p.title, icon: p.icon, click: () => this._select(p, true) });
62
64
  buttons.push(p.btn);
63
65
  });
64
66
  this.setContent(buttons);
65
67
  }
66
- select(id) {
67
- let page = this.m_pages.find(x => x.id === id);
68
- if (page) {
69
- this._select(page);
68
+ select(id, notify = false) {
69
+ if (!id) {
70
+ this._select(null, notify);
71
+ return true;
72
+ }
73
+ else {
74
+ let page = this.m_pages.find(x => x.id === id);
75
+ if (page) {
76
+ this._select(page, notify);
77
+ return true;
78
+ }
79
+ return false;
70
80
  }
71
81
  }
72
- _select(p) {
73
- if (this.dom && this.m_curPage && this.m_curPage.page) {
82
+ _select(p, notify) {
83
+ if (this.m_curPage == p) {
84
+ return;
85
+ }
86
+ if (!this.dom) {
87
+ this.m_props.default = p.id;
88
+ return;
89
+ }
90
+ if (this.m_curPage) {
74
91
  this.m_curPage.btn.removeClass('selected');
75
- this.m_curPage.page.hide();
92
+ if (this.m_curPage.page) {
93
+ this.m_curPage.page.hide();
94
+ }
76
95
  }
77
96
  this.m_curPage = p;
78
- this.signal('change', (0, x4_events_1.EvChange)(p ? p.id : null));
79
- if (this.dom && this.m_curPage && this.m_curPage.page) {
97
+ if (notify) {
98
+ this.signal('change', (0, x4_events_1.EvChange)(p ? p.id : null));
99
+ }
100
+ if (this.m_curPage) {
80
101
  this.m_curPage.btn.addClass('selected');
81
- this.m_curPage.page.show();
102
+ if (this.m_curPage.page) {
103
+ this.m_curPage.page.show();
104
+ }
82
105
  }
83
106
  }
107
+ get selection() {
108
+ return this.m_curPage?.page;
109
+ }
84
110
  }
85
111
  exports.TabBar = TabBar;
package/lib/x4.css CHANGED
@@ -306,6 +306,8 @@ textarea::selection {
306
306
  align-items: center;
307
307
  outline: none;
308
308
  cursor: pointer;
309
+ font-family: var(--x4-font);
310
+ font-size: var(--x4-font-size);
309
311
  height: 2rem;
310
312
  padding: 8px;
311
313
  overflow: hidden;
@@ -1691,3 +1693,21 @@ textarea::selection {
1691
1693
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
1692
1694
  grid-auto-rows: 10px;
1693
1695
  }
1696
+ .x-tab-bar {
1697
+ border-bottom: 1px solid var(--gray-600);
1698
+ background-color: var(--gray-100);
1699
+ }
1700
+ .x-tab-bar > .x-button {
1701
+ border: none;
1702
+ color: var(--gray-600);
1703
+ }
1704
+ .x-tab-bar > .x-button.selected {
1705
+ font-weight: bold;
1706
+ border-bottom: none;
1707
+ color: var(--x4-selection-color);
1708
+ background-color: transparent;
1709
+ }
1710
+ .x-tab-bar > .x-button:focus:not(.selected) {
1711
+ text-decoration: underline;
1712
+ color: black;
1713
+ }
@@ -106,6 +106,14 @@ export interface EvDrag extends BasicEvent {
106
106
  data: any;
107
107
  }
108
108
  export declare function EvDrag(element: unknown, data: any, ctx: any): EvDrag;
109
+ /**
110
+ * Errors
111
+ */
112
+ export interface EvError extends BasicEvent {
113
+ code: number;
114
+ message: string;
115
+ }
116
+ export declare function EvError(code: number, message: string): EvError;
109
117
  /**
110
118
  * this Base interface is used to describe available events & their types
111
119
  *
package/lib/x4_events.js CHANGED
@@ -28,7 +28,7 @@
28
28
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29
29
  **/
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.EventSource = exports.EvDrag = exports.EvMessage = exports.EvTimer = exports.EvContextMenu = exports.EvSelectionChange = exports.EvChange = exports.EvClick = exports.BasicEvent = void 0;
31
+ exports.EventSource = exports.EvError = exports.EvDrag = exports.EvMessage = exports.EvTimer = exports.EvContextMenu = exports.EvSelectionChange = exports.EvChange = exports.EvClick = exports.BasicEvent = void 0;
32
32
  // default stopPropagation implementation for Events
33
33
  const stopPropagation = function () {
34
34
  this.propagationStopped = true;
@@ -81,6 +81,10 @@ function EvDrag(element, data, ctx) {
81
81
  return BasicEvent({ element, data, context: ctx });
82
82
  }
83
83
  exports.EvDrag = EvDrag;
84
+ function EvError(code, message) {
85
+ return BasicEvent({ code, message });
86
+ }
87
+ exports.EvError = EvError;
84
88
  /**
85
89
  * Event emitter class
86
90
  * this class allow you to emit and handle events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "1.4.14",
3
+ "version": "1.4.17",
4
4
  "description": "X4js core files",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -34,6 +34,8 @@ import { Settings } from './settings'
34
34
  import { deferCall } from './tools'
35
35
  import { _tr } from './i18n'
36
36
 
37
+ const _x4_touch_time = Symbol( );
38
+
37
39
 
38
40
  interface ApplicationEventMap extends BaseComponentEventMap {
39
41
  message: EvMessage;
@@ -92,6 +94,9 @@ export class Application<P extends ApplicationProps = ApplicationProps, E extend
92
94
 
93
95
  private m_local_storage: Settings;
94
96
  private m_user_data: any;
97
+
98
+ private m_touch_time: number;
99
+ private m_touch_count: number;
95
100
 
96
101
  constructor( props : P ) {
97
102
  console.assert( Application.self===null, 'application is a singleton' );
@@ -104,6 +109,9 @@ export class Application<P extends ApplicationProps = ApplicationProps, E extend
104
109
  let settings_name = `${this.m_app_name}.${this.m_app_version}.settings`;
105
110
  this.m_local_storage = new Settings( settings_name );
106
111
  this.m_user_data = {};
112
+
113
+ this.m_touch_time = 0;
114
+ this.m_touch_count = 0;
107
115
 
108
116
  (Application.self as any) = this;
109
117
 
@@ -194,6 +202,36 @@ export class Application<P extends ApplicationProps = ApplicationProps, E extend
194
202
 
195
203
  public enterModal( enter: boolean ) {
196
204
  }
205
+
206
+ public enableTouchDblClick( ) {
207
+ document.addEventListener( 'touchstart', ( ev: TouchEvent ) => {
208
+
209
+ let now = Date.now( );
210
+ if( (now-this.m_touch_time) > 700 ) {
211
+ this.m_touch_count = 1;
212
+ }
213
+ else {
214
+ this.m_touch_count++;
215
+ }
216
+
217
+ this.m_touch_time = now;
218
+
219
+ if( this.m_touch_count==2 ) {
220
+ this.m_touch_count = 0;
221
+
222
+ // dirty fake dblclick event
223
+ const tch = ev.touches[0];
224
+ let fake: any = {type: "dblclick" };
225
+ for( const n in tch ) {
226
+ fake[n] = tch[n];
227
+ }
228
+
229
+ // ignore -> private: dirty x2
230
+ (Component as any)._dispatchEvent( fake );
231
+ ev.stopPropagation( );
232
+ }
233
+ });
234
+ }
197
235
  };
198
236
 
199
237
 
package/src/component.ts CHANGED
@@ -188,7 +188,7 @@ export class Component<P extends CProps<BaseComponentEventMap> = CProps<BaseComp
188
188
  private static __privateEvents: any = {};
189
189
  private static __sizeObserver: ResizeObserver; // resize observer
190
190
  private static __createObserver: MutationObserver; // creation observer
191
- private static __intersectionObserver: IntersectionObserver; // visibility observer
191
+ //private static __intersectionObserver: IntersectionObserver; // visibility observer
192
192
 
193
193
  private static __capture: ICaptureInfo = null;
194
194
  private static __capture_mask = null;
@@ -202,6 +202,11 @@ export class Component<P extends CProps<BaseComponentEventMap> = CProps<BaseComp
202
202
  dom_events: {},
203
203
  uid: Component.__comp_guid++,
204
204
  inrender: false,
205
+ };
206
+
207
+ // prepare iprops
208
+ if( this.m_props.cls ) {
209
+ this.addClass( this.m_props.cls );
205
210
  }
206
211
  }
207
212
 
@@ -1389,7 +1394,8 @@ export class Component<P extends CProps<BaseComponentEventMap> = CProps<BaseComp
1389
1394
  me = Object.getPrototypeOf(me);
1390
1395
  }
1391
1396
 
1392
- this.addClass(this.m_props.cls);
1397
+ //done in ctor now
1398
+ //this.addClass(this.m_props.cls);
1393
1399
  }
1394
1400
 
1395
1401
  /**
package/src/layout.ts CHANGED
@@ -378,11 +378,20 @@ export class ScrollView extends Component<ScrollViewProps> {
378
378
  export class Masonry extends Container {
379
379
 
380
380
  constructor(props) {
381
+ const items = props.items;
382
+ props.items = undefined;
383
+
381
384
  super(props);
382
385
 
383
386
  this.setDomEvent('sizechange', () => {
384
387
  this.resizeAllItems( );
385
388
  });
389
+
390
+ if( items ) {
391
+ items.forEach( i => {
392
+ this.addItem( i );
393
+ });
394
+ }
386
395
  }
387
396
 
388
397
  resizeItem(item: Component) {
package/src/listview.ts CHANGED
@@ -100,7 +100,7 @@ export interface ListViewProps<E extends ListViewEventMap = ListViewEventMap> ex
100
100
  * Standard listview class
101
101
  */
102
102
 
103
- export class ListView<T extends ListViewProps = ListViewProps, E extends ListViewEventMap = ListViewEventMap> extends VLayout<T,E> {
103
+ export class ListView extends VLayout<ListViewProps,ListViewEventMap> {
104
104
 
105
105
  protected m_selection: {
106
106
  item: ListViewItem;
@@ -117,7 +117,7 @@ export class ListView<T extends ListViewProps = ListViewProps, E extends ListVie
117
117
 
118
118
  protected m_cache: Map<number, Component>; // recycling elements
119
119
 
120
- constructor(props: T) {
120
+ constructor(props: ListViewProps) {
121
121
  super(props);
122
122
 
123
123
  this.setDomEvent('keydown', (e) => this._handleKey(e));
package/src/router.ts CHANGED
@@ -27,45 +27,157 @@
27
27
  * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
28
  **/
29
29
 
30
- type Callback = (request: { path: string }) => void;
30
+ import { EventSource, EvError, EventMap } from "./x4_events"
31
+
32
+ type RouteHandler = ( params: any, path: string ) => void;
33
+
34
+ interface Segment {
35
+ keys: string[],
36
+ pattern: RegExp;
37
+ }
31
38
 
32
39
  interface Route {
33
- uri: string;
34
- callback: Callback;
40
+ keys: string[],
41
+ pattern: RegExp;
42
+ handler: RouteHandler;
35
43
  }
36
44
 
37
- export class Router {
45
+ function parseRoute(str: string | RegExp, loose = false): Segment {
38
46
 
39
- private routes: Route[];
47
+ if (str instanceof RegExp) {
48
+ return {
49
+ keys: null,
50
+ pattern: str
51
+ };
52
+ }
40
53
 
41
- constructor() {
42
- this.routes = [];
54
+ const arr = str.split('/');
55
+
56
+ let keys = [];
57
+ let pattern = '';
58
+
59
+ if( arr[0]=='' ) {
60
+ arr.shift();
43
61
  }
44
62
 
45
- get(uri, callback) {
63
+ for (const tmp of arr) {
64
+ const c = tmp[0];
46
65
 
47
- // throw an error if the route uri already exists to avoid confilicting routes
48
- this.routes.forEach(route => {
49
- if (route.uri === uri) {
50
- throw new Error(`the uri ${route.uri} already exists`);
66
+ if (c === '*') {
67
+ keys.push('wild');
68
+ pattern += '/(.*)';
69
+ }
70
+ else if (c === ':') {
71
+ const o = tmp.indexOf('?', 1);
72
+ const ext = tmp.indexOf('.', 1);
73
+
74
+ keys.push(tmp.substring(1, o >= 0 ? o : ext >= 0 ? ext : tmp.length));
75
+ pattern += o >= 0 && ext < 0 ? '(?:/([^\/]+?))?' : '/([^\/]+?)';
76
+ if (ext >= 0) {
77
+ pattern += (o >= 0 ? '?' : '') + '\\' + tmp.substring(ext);
51
78
  }
52
- });
79
+ }
80
+ else {
81
+ pattern += '/' + tmp;
82
+ }
83
+ }
84
+
85
+ return {
86
+ keys,
87
+ pattern: new RegExp( `^${pattern}${loose ? '(?=$|\/)' : '\/?$'}`, 'i' )
88
+ };
89
+ }
90
+
91
+ interface RouterEventMap extends EventMap {
92
+ error: EvError;
93
+ }
94
+
95
+ export class Router extends EventSource< RouterEventMap > {
96
+
97
+ private routes: Route[];
53
98
 
54
- this.routes.push({
55
- uri,
56
- callback
99
+ constructor() {
100
+ super( );
101
+
102
+ this.routes = [];
103
+
104
+ window.addEventListener('popstate', (event) => {
105
+ const url = document.location.pathname;
106
+ const found = this._find(url);
107
+
108
+ found.handlers.forEach(h => {
109
+ h(found.params,url);
110
+ });
57
111
  });
58
112
  }
59
113
 
114
+ get(uri: string | RegExp, handler: RouteHandler ) {
115
+ let { keys, pattern } = parseRoute(uri);
116
+ this.routes.push({ keys, pattern, handler });
117
+ }
118
+
60
119
  init() {
61
- this.routes.some(route => {
120
+ this.navigate( window.location.pathname );
121
+ }
122
+
123
+ navigate( uri: string, notify = true ) {
124
+
125
+ const found = this._find( uri );
62
126
 
63
- let regEx = new RegExp(`^${route.uri}$`);
64
- let path = window.location.pathname;
127
+ if( !found || found.handlers.length==0 ) {
128
+ //window.history.pushState({}, '', 'error')
129
+ console.log( 'route not found: '+uri );
130
+ this.signal( "error", EvError( 404, "route not found" ) );
131
+ return;
132
+ }
65
133
 
66
- if (path.match(regEx)) {
67
- return route.callback({ path });
134
+ window.history.pushState({}, '', uri )
135
+
136
+ if( notify ) {
137
+ found.handlers.forEach( h => {
138
+ h( found.params, uri );
139
+ } );
140
+ }
141
+ }
142
+
143
+ private _find( url: string ): { params: any, handlers: RouteHandler[] } {
144
+
145
+ let matches = [];
146
+ let params = {};
147
+ let handlers = [];
148
+
149
+ for (const tmp of this.routes ) {
150
+ if (!tmp.keys ) {
151
+ matches = tmp.pattern.exec(url);
152
+ if (!matches) {
153
+ continue;
154
+ }
155
+
156
+ if (matches['groups']) {
157
+ for (const k in matches['groups']) {
158
+ params[k] = matches['groups'][k];
159
+ }
160
+ }
161
+
162
+ handlers = [...handlers, tmp.handler];
163
+ }
164
+ else if (tmp.keys.length > 0) {
165
+ matches = tmp.pattern.exec(url);
166
+ if (matches === null) {
167
+ continue;
168
+ }
169
+
170
+ for ( let j = 0; j < tmp.keys.length;) {
171
+ params[tmp.keys[j]] = matches[++j];
172
+ }
173
+
174
+ handlers = [...handlers, tmp.handler];
175
+ }
176
+ else if (tmp.pattern.test(url)) {
177
+ handlers = [...handlers, tmp.handler];
68
178
  }
69
- })
179
+ }
180
+
181
+ return { params, handlers };
70
182
  }
71
183
  }
package/src/tabbar.ts CHANGED
@@ -75,9 +75,12 @@ export class TabBar extends Container<TabBarProps,TabBarEventMap> {
75
75
  }
76
76
 
77
77
  this.m_props.pages?.forEach( p => this.addPage(p) );
78
+ }
79
+
80
+ componentCreated(): void {
78
81
  if( this.m_props.default ) {
79
- this.select( this.m_props.default );
80
- }
82
+ this.select( this.m_props.default, true );
83
+ }
81
84
  }
82
85
 
83
86
  addPage( page: ITabPage ) {
@@ -88,33 +91,61 @@ export class TabBar extends Container<TabBarProps,TabBarEventMap> {
88
91
  render( ) {
89
92
  let buttons = [];
90
93
  this.m_pages.forEach( p => {
91
- p.btn = new Button( { cls: p===this.m_curPage ? 'selected' : '', text: p.title, icon: p.icon, click: () => this._select(p) } );
94
+ p.btn = new Button( { cls: p===this.m_curPage ? 'selected' : '', text: p.title, icon: p.icon, click: () => this._select(p,true) } );
92
95
  buttons.push( p.btn );
93
96
  });
94
97
 
95
98
  this.setContent( buttons );
96
99
  }
97
100
 
98
- select( id: string ) {
99
- let page = this.m_pages.find( x => x.id===id );
100
- if( page ) {
101
- this._select( page );
101
+ select( id: string | null, notify = false ): boolean {
102
+ if( !id ) {
103
+ this._select( null, notify );
104
+ return true;
105
+ }
106
+ else {
107
+ let page = this.m_pages.find( x => x.id===id );
108
+ if( page ) {
109
+ this._select( page, notify );
110
+ return true;
111
+ }
112
+ return false;
102
113
  }
103
114
  }
104
115
 
105
- private _select( p: TabPage ) {
116
+ private _select( p: TabPage, notify: boolean ) {
117
+
118
+ if( this.m_curPage==p ) {
119
+ return;
120
+ }
106
121
 
107
- if( this.dom && this.m_curPage && this.m_curPage.page ) {
122
+ if( !this.dom ) {
123
+ this.m_props.default = p.id;
124
+ return;
125
+ }
126
+
127
+ if( this.m_curPage ) {
108
128
  this.m_curPage.btn.removeClass( 'selected' );
109
- this.m_curPage.page.hide( );
129
+ if( this.m_curPage.page ) {
130
+ this.m_curPage.page.hide( );
131
+ }
110
132
  }
111
133
 
112
134
  this.m_curPage = p;
113
- this.signal( 'change', EvChange(p ? p.id : null) );
114
135
 
115
- if( this.dom && this.m_curPage && this.m_curPage.page ) {
136
+ if( notify ) {
137
+ this.signal( 'change', EvChange(p ? p.id : null) );
138
+ }
139
+
140
+ if( this.m_curPage ) {
116
141
  this.m_curPage.btn.addClass( 'selected' );
117
- this.m_curPage.page.show( );
142
+ if( this.m_curPage.page ) {
143
+ this.m_curPage.page.show( );
144
+ }
118
145
  }
119
146
  }
147
+
148
+ get selection( ) {
149
+ return this.m_curPage?.page;
150
+ }
120
151
  }
package/src/x4.less CHANGED
@@ -386,6 +386,8 @@ textarea {
386
386
  align-items: center;
387
387
  outline: none;
388
388
  cursor: pointer;
389
+ font-family: var( --x4-font );
390
+ font-size: var( --x4-font-size );
389
391
 
390
392
  height: 2rem;
391
393
  padding: 8px;
@@ -2134,4 +2136,27 @@ textarea {
2134
2136
  grid-gap: 10px;
2135
2137
  grid-template-columns: repeat(auto-fill, minmax(250px,1fr));
2136
2138
  grid-auto-rows: 10px;
2139
+ }
2140
+
2141
+ .x-tab-bar {
2142
+ border-bottom: 1px solid var(--gray-600);
2143
+ background-color: var(--gray-100);
2144
+
2145
+ &> .x-button {
2146
+ border: none;
2147
+ color: var( --gray-600 );
2148
+
2149
+ &.selected {
2150
+ font-weight: bold;
2151
+ //border: 1px solid var( --x4-selection-color );
2152
+ border-bottom: none;
2153
+ color: var( --x4-selection-color );
2154
+ background-color: transparent;
2155
+ }
2156
+
2157
+ &:focus:not(.selected) {
2158
+ text-decoration: underline;
2159
+ color: black;;
2160
+ }
2161
+ }
2137
2162
  }
package/src/x4_events.ts CHANGED
@@ -168,6 +168,19 @@ export function EvDrag(element: unknown, data: any, ctx: any ) {
168
168
  return BasicEvent<EvDrag>({ element, data, context: ctx });
169
169
  }
170
170
 
171
+ /**
172
+ * Errors
173
+ */
174
+
175
+ export interface EvError extends BasicEvent {
176
+ code: number;
177
+ message: string;
178
+ }
179
+
180
+ export function EvError( code: number, message: string ) : EvError {
181
+ return BasicEvent<EvError>( {code, message} );
182
+ }
183
+
171
184
 
172
185
  /**
173
186
  * this Base interface is used to describe available events & their types