@web-atoms/web-controls 2.1.202 → 2.1.205

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.
@@ -5,7 +5,7 @@ import IElement from "./IElement";
5
5
  type Getter = (item) => any;
6
6
 
7
7
  export interface IPropertyInfo {
8
- key?: string,
8
+ key?: string;
9
9
  getter(item): any;
10
10
  setter(item, value): void;
11
11
  }
@@ -20,15 +20,15 @@ export function getPropertyInfo(propertyPath: string[] | string | IPropertyInfo)
20
20
  item[propertyPath] = value;
21
21
  }
22
22
  }
23
- }
23
+ };
24
24
  }
25
25
  if (!Array.isArray(propertyPath)) {
26
26
  return propertyPath;
27
27
  }
28
28
  const last = propertyPath.pop();
29
- let getterIntermediate = undefined;
29
+ let getterIntermediate;
30
30
  if (propertyPath.length > 0) {
31
- getterIntermediate = function(item) {
31
+ getterIntermediate = (item) => {
32
32
  if (!item) {
33
33
  return item;
34
34
  }
@@ -39,19 +39,19 @@ export function getPropertyInfo(propertyPath: string[] | string | IPropertyInfo)
39
39
  }
40
40
  }
41
41
  return item;
42
- }
42
+ };
43
43
  }
44
- let setter = (item, value) => {
44
+ const setter = (item, value) => {
45
45
  if (item) {
46
46
  if (getterIntermediate) {
47
- item = getterIntermediate(item)
47
+ item = getterIntermediate(item);
48
48
  }
49
49
  if (item) {
50
50
  item[last] = value;
51
51
  }
52
52
  }
53
- }
54
- let getter = (item) => {
53
+ };
54
+ const getter = (item) => {
55
55
  if (!item) {
56
56
  return item;
57
57
  }
@@ -64,7 +64,7 @@ export function getPropertyInfo(propertyPath: string[] | string | IPropertyInfo)
64
64
  key: propertyPath.join(","),
65
65
  getter,
66
66
  setter
67
- }
67
+ };
68
68
  }
69
69
 
70
70
  const handlers = {};
@@ -72,7 +72,7 @@ const handlers = {};
72
72
  function registerEvent(property: IPropertyInfo, changeEvents: string[], editorValuePath: (editor) => any) {
73
73
  property.key ??= property.setter + ":" + property.getter;
74
74
  const name = `${property.key}:${changeEvents.join(",")}`;
75
- if(!handlers[name]) {
75
+ if (!handlers[name]) {
76
76
  handlers[name] = changeEvents;
77
77
  for (const iterator of changeEvents) {
78
78
  document.body.addEventListener(iterator, (e) => {
@@ -84,6 +84,7 @@ function registerEvent(property: IPropertyInfo, changeEvents: string[], editorVa
84
84
  const [ _, repeater, item] = ri;
85
85
  const oldValue = property.getter(item);
86
86
  const value = editorValuePath(e.target);
87
+ // tslint:disable-next-line: triple-equals
87
88
  if (oldValue != value) {
88
89
  property.setter(item, value);
89
90
  repeater.refreshItem(item);
@@ -94,19 +95,10 @@ function registerEvent(property: IPropertyInfo, changeEvents: string[], editorVa
94
95
  }
95
96
  }
96
97
 
97
-
98
98
  export interface IEditable extends IElement {
99
99
  propertyPath: string[] | string | IPropertyInfo;
100
100
  }
101
101
 
102
- export default function Editable(
103
- {
104
- propertyPath
105
- }: IEditable
106
- ) {
107
-
108
- }
109
-
110
102
  export function EditableInput(
111
103
  {
112
104
  propertyPath,
@@ -6,9 +6,11 @@ import XNode from "@web-atoms/core/dist/core/XNode";
6
6
  import StyleRule from "@web-atoms/core/dist/style/StyleRule";
7
7
  import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
8
8
  import CSS from "@web-atoms/core/dist/web/styles/CSS";
9
+ import AtomRepeater from "../basic/AtomRepeater";
9
10
  import AddImage, { showImageDialog } from "./commands/AddImage";
10
11
  import AddLink from "./commands/AddLink";
11
12
  import Align from "./commands/Align";
13
+ import AttachFile from "./commands/AttachFile";
12
14
  import Bold from "./commands/Bold";
13
15
  import ChangeColor from "./commands/ChangeColor";
14
16
  import ChangeFont from "./commands/ChangeFont";
@@ -39,6 +41,21 @@ const css = CSS(StyleRule()
39
41
  .child(StyleRule("iframe")
40
42
  .flexStretch()
41
43
  )
44
+ .child(StyleRule(".files")
45
+ .child(StyleRule(".file")
46
+ .flexLayout({ inline: true })
47
+ .borderColor(Colors.lightGray.withAlphaPercent(0.5))
48
+ .borderWidth(1)
49
+ .borderStyle("solid")
50
+ .borderRadius(15)
51
+ .paddingLeft(10)
52
+ .paddingRight(10)
53
+ .child(StyleRule("label")
54
+ .maxWidth(100)
55
+ .textEllipsis()
56
+ )
57
+ )
58
+ )
42
59
  .nested(StyleRule(".toolbar")
43
60
  .display("flex")
44
61
  .child(StyleRule(".command")
@@ -148,6 +165,9 @@ export default class AtomHtmlEditor extends AtomControl {
148
165
  @BindableProperty
149
166
  public version: number;
150
167
 
168
+ @BindableProperty
169
+ public files: File[];
170
+
151
171
  public editor: HTMLDivElement;
152
172
 
153
173
  public eventDocumentCreated: (e: CustomEvent<HTMLDivElement>) => void;
@@ -200,6 +220,7 @@ export default class AtomHtmlEditor extends AtomControl {
200
220
 
201
221
  protected preCreate() {
202
222
  this.version = 1;
223
+ this.files = [];
203
224
  this.runAfterInit(() => {
204
225
  this.setup();
205
226
  });
@@ -323,6 +344,7 @@ export default class AtomHtmlEditor extends AtomControl {
323
344
  <IndentMore/>
324
345
  <Separator/>
325
346
  <AddImage/>
347
+ <AttachFile/>
326
348
  <Separator/>
327
349
  <AddLink/>
328
350
  <Unlink/>
@@ -333,6 +355,19 @@ export default class AtomHtmlEditor extends AtomControl {
333
355
  }
334
356
  super.render(<div {... node.attributes}>
335
357
  { ... node.children as any[]}
358
+ <AtomRepeater
359
+ class="files"
360
+ event-item-delete={(ce) => this.files.remove(ce.detail)}
361
+ items={Bind.oneWay(() => this.files)}
362
+ itemRenderer={(file: File) => <div class="file">
363
+ <i class="ri-attachment-2"/>
364
+ <label text={file.name}/>
365
+ <i
366
+ data-click-event="item-delete"
367
+ class="ri-close-circle-fill"
368
+ />
369
+ </div>}
370
+ />
336
371
  <iframe class="editor-frame"/>
337
372
  </div>);
338
373
  }
@@ -0,0 +1,63 @@
1
+ import AtomHtmlEditor from "../AtomHtmlEditor";
2
+ import CommandButton from "./CommandButton";
3
+ import HtmlCommands from "./HtmlCommands";
4
+
5
+ function promptForFiles(
6
+ accept = "*",
7
+ multiple = true
8
+ ) {
9
+ const file = document.createElement("input");
10
+ file.type = "file";
11
+ file.multiple = multiple;
12
+ file.accept = accept;
13
+ file.style.position = "absolute";
14
+ file.style.left = "-1000px";
15
+ file.style.top = "-1000px";
16
+ document.body.append(file);
17
+ const previous = file;
18
+ return new Promise<File[]>((resolve, reject) => {
19
+ file.addEventListener("change", () => {
20
+ if (file.files.length) {
21
+ const files = Array.from(file.files);
22
+ resolve(files);
23
+ } else {
24
+ reject("cancelled");
25
+ }
26
+ if (previous) {
27
+ previous.remove();
28
+ }
29
+ });
30
+ file.dispatchEvent(new MouseEvent("click"));
31
+ });
32
+ }
33
+
34
+ export default function AttachFile({
35
+ eventInsertHtml = (source: AtomHtmlEditor, e: Event) => {
36
+ const ce = new CustomEvent("attachFile",
37
+ {
38
+ detail: {
39
+ source,
40
+ files: source.files
41
+ },
42
+ cancelable: true
43
+ });
44
+ source.element.dispatchEvent(ce);
45
+ if (ce.defaultPrevented) {
46
+ return;
47
+ }
48
+ source.app.runAsync(async () => {
49
+ const result = await promptForFiles();
50
+ source.files ??= [];
51
+ source.files.addAll(result);
52
+ })
53
+ },
54
+ insertCommand = HtmlCommands.insertImage
55
+ }) {
56
+ return CommandButton({
57
+ icon: "ri-attachment-2",
58
+ insertCommand,
59
+ disabled: false,
60
+ title: "Insert Image",
61
+ eventInsertHtml
62
+ });
63
+ }
@@ -37,7 +37,7 @@ function insert(callback: (s: AtomHtmlEditor, e: Event) => Promise<string> | str
37
37
  return async (s: AtomHtmlEditor, e: Event) => {
38
38
  let r = callback(s, e);
39
39
  if (typeof r !== "string") {
40
- if (r.then) {
40
+ if (r?.then) {
41
41
  r = await r;
42
42
  }
43
43
  }
@@ -0,0 +1,348 @@
1
+ import { AtomLoader } from "@web-atoms/core/dist/core/AtomLoader";
2
+ import { AtomUri } from "@web-atoms/core/dist/core/AtomUri";
3
+ import Bind from "@web-atoms/core/dist/core/Bind";
4
+ import { BindableProperty } from "@web-atoms/core/dist/core/BindableProperty";
5
+ import Colors from "@web-atoms/core/dist/core/Colors";
6
+ import { CancelToken, IClassOf, IDisposable } from "@web-atoms/core/dist/core/types";
7
+ import XNode from "@web-atoms/core/dist/core/XNode";
8
+ import { NavigationService } from "@web-atoms/core/dist/services/NavigationService";
9
+ import StyleRule from "@web-atoms/core/dist/style/StyleRule";
10
+ import { AtomWindowViewModel } from "@web-atoms/core/dist/view-model/AtomWindowViewModel";
11
+ import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
12
+ import PopupService, { ConfirmPopup, IPopup, PopupControl } from "@web-atoms/core/dist/web/services/PopupService";
13
+ import CSS from "@web-atoms/core/dist/web/styles/CSS";
14
+
15
+ CSS(StyleRule()
16
+ .absolutePosition({ left: 0, top: 0, bottom: 0, right: 0})
17
+ .overflow("hidden")
18
+ , "div[data-page-app=page-app]");
19
+
20
+ CSS(StyleRule()
21
+ .absolutePosition({ left: 0, top: 0, bottom: 0, right: 0})
22
+ .overflow("hidden")
23
+ .transition("transform 0.3s ease-out")
24
+ .display("grid")
25
+ .gridTemplateRows("25px 1fr auto")
26
+ .gridTemplateColumns("25px 1fr auto")
27
+ .child(StyleRule("[data-icon-button=icon-button]")
28
+ .padding(5)
29
+ )
30
+ .child(StyleRule("[data-page-element=header]")
31
+ .gridRowStart("1")
32
+ .gridColumnStart("1")
33
+ .gridColumnEnd("span 3")
34
+ .backgroundColor(Colors.silver)
35
+ )
36
+ .child(StyleRule("[data-page-element=icon]")
37
+ .gridRowStart("1")
38
+ .gridColumnStart("1")
39
+ )
40
+ .child(StyleRule("[data-page-element=title]")
41
+ .gridRowStart("1")
42
+ .gridColumnStart("2")
43
+ )
44
+ .child(StyleRule("[data-page-element=action]")
45
+ .gridRowStart("1")
46
+ .gridColumnStart("3")
47
+ )
48
+ .child(StyleRule("[data-page-element=content]")
49
+ .gridRowStart("2")
50
+ .gridColumnStart("1")
51
+ .gridColumnEnd("span 3")
52
+ )
53
+ .child(StyleRule("[data-page-element=footer]")
54
+ .gridRowStart("3")
55
+ .gridColumnStart("1")
56
+ .gridColumnEnd("span 3")
57
+ )
58
+ .transform("translate(100%,0)" as any)
59
+ .and(StyleRule("[data-page-state=ready]")
60
+ .transform("translate(0,0)" as any)
61
+ )
62
+ .and(StyleRule("[data-page-state=hidden]")
63
+ .transform("translate(-100%,0)" as any)
64
+ )
65
+ , "div[data-base-page=base-page]");
66
+
67
+ export class BasePage extends AtomControl {
68
+
69
+ public close: (result) => void;
70
+
71
+ public cancel: (error?) => void;
72
+
73
+ @BindableProperty
74
+ public closeWarning: string;
75
+
76
+ @BindableProperty
77
+ public title?: string;
78
+
79
+ public titleRenderer: () => XNode;
80
+
81
+ public iconRenderer: () => XNode;
82
+
83
+ public actionRenderer: () => XNode;
84
+
85
+ public footerRenderer: () => XNode;
86
+
87
+ public headerBackgroundRenderer: () => XNode;
88
+
89
+ public iconClass: any;
90
+
91
+ public async requestCancel() {
92
+ if (this.closeWarning) {
93
+ if (!await ConfirmPopup.showModal<boolean>({
94
+ parameters: {
95
+ message : this.closeWarning
96
+ }
97
+ })) {
98
+ return;
99
+ }
100
+ }
101
+ this.cancel();
102
+ }
103
+
104
+ protected preCreate(): void {
105
+ this.element.dataset.basePage = "base-page";
106
+ this.iconClass = "";
107
+ this.runAfterInit(() => {
108
+ if (!this.element) {
109
+ return;
110
+ }
111
+ const anyAutofocus = this.element.querySelector(`*[autofocus]`);
112
+ if (!anyAutofocus) {
113
+ const windowContent = this.element.querySelector(`[data-page-element="content"]`);
114
+ if (windowContent) {
115
+ const firstInput = windowContent.querySelector("input,button,a,textarea") as HTMLInputElement;
116
+ if (firstInput) {
117
+ firstInput.focus();
118
+ return;
119
+ }
120
+ }
121
+ return;
122
+ }
123
+ });
124
+
125
+ setTimeout((p) => {
126
+ p.dataset.pageState = "ready";
127
+ }, 10, this.element);
128
+ }
129
+
130
+ protected render(node: XNode, e?: any, creator?: any): void {
131
+ this.render = super.render;
132
+ const titleContent = this.titleRenderer?.() ?? <span
133
+ class="title-text" text={Bind.oneWay(() => this.title)}/>;
134
+ const icon = this.iconRenderer?.() ?? <i
135
+ data-icon-button="icon-button"
136
+ class={Bind.oneWay(() => this.iconClass)}
137
+ eventClick={(e1) => this.dispatchIconClickEvent(e1)}/>;
138
+ const action = this.actionRenderer?.() ?? undefined;
139
+ const footer = this.footerRenderer?.() ?? undefined;
140
+ const header = this.headerBackgroundRenderer?.() ?? <div/>;
141
+ const a = node.attributes ??= {};
142
+ a["data-page-element"] = "content";
143
+ if (header) {
144
+ header.attributes ??= {};
145
+ header.attributes["data-page-element"] = "header";
146
+ }
147
+ if (icon) {
148
+ icon.attributes ??= {};
149
+ icon.attributes["data-page-element"] = "icon";
150
+ }
151
+ if (titleContent) {
152
+ titleContent.attributes ??= {};
153
+ titleContent.attributes["data-page-element"] = "title";
154
+ }
155
+ if (action) {
156
+ action.attributes ??= {};
157
+ action.attributes["data-page-element"] = "action";
158
+ }
159
+ if (footer) {
160
+ footer.attributes ??= {};
161
+ footer.attributes["data-page-element"] = "footer";
162
+ }
163
+ super.render(<div viewModelTitle={Bind.oneWay(() => this.viewModel.title)}>
164
+ { header }
165
+ { icon }
166
+ { titleContent }
167
+ { action }
168
+ { node }
169
+ { footer }
170
+ </div>);
171
+ }
172
+
173
+ protected dispatchIconClickEvent(e: Event) {
174
+ const ce = new CustomEvent("iconClick", { bubbles: true });
175
+ e.target.dispatchEvent(ce);
176
+ }
177
+ }
178
+
179
+ export class ContentPage extends BasePage {
180
+ }
181
+
182
+ export class TabbedPage extends BasePage {
183
+
184
+ }
185
+
186
+ export class Drawer extends AtomControl {
187
+ protected preCreate(): void {
188
+ this.bindEvent(this.element, "click", (e) => this.closeDrawer());
189
+ }
190
+
191
+ protected closeDrawer() {
192
+ const ce = new CustomEvent("closeDrawer", { bubbles: true });
193
+ this.element.dispatchEvent(ce);
194
+ }
195
+ }
196
+
197
+ export default class MobileApp extends AtomControl {
198
+
199
+ public static drawer = XNode.prepare("drawer", true, true);
200
+
201
+ public drawer: () => XNode;
202
+
203
+ public drawerCancelToken: CancelToken;
204
+
205
+ public pages: BasePage[];
206
+
207
+ public selectedPage: BasePage;
208
+
209
+ public icon: any;
210
+
211
+ public async back() {
212
+ if (this.pages.length === 0) {
213
+
214
+ const drawer = this.drawer;
215
+ if (drawer) {
216
+ const drawerNode = this.drawer();
217
+ const da = drawerNode.attributes ??= {};
218
+ da["event-click"] = (de: Event) => {
219
+ de.target.dispatchEvent(new CustomEvent("closeDrawer", { bubbles: true }));
220
+ };
221
+ class DrawerPopup extends PopupControl {
222
+ protected create(): void {
223
+ this.render(<div event-close-drawer={() => cancelToken.cancel()}>
224
+ { drawerNode }
225
+ </div>);
226
+ }
227
+ }
228
+ const cancelToken = new CancelToken();
229
+ this.drawerCancelToken = cancelToken;
230
+ DrawerPopup.showControl(this.icon, { cancelToken });
231
+ }
232
+
233
+ return;
234
+ }
235
+
236
+ this.selectedPage.cancel("cancelled");
237
+ }
238
+
239
+ protected preCreate(): void {
240
+ this.drawer = null;
241
+ this.element.dataset.pageApp = "page-app";
242
+ this.pages = [];
243
+ this.selectedPage = null;
244
+ this.bindEvent(this.element, "iconClick", (e) => { this.icon = e.target; return this.back(); });
245
+ this.bindEvent(this.element, "closeDrawer", (e) => {
246
+ this.drawerCancelToken?.cancel();
247
+ this.drawerCancelToken = undefined;
248
+ });
249
+ const navigationService = this.app.resolve(NavigationService);
250
+ navigationService.registerNavigationHook(
251
+ (uri, { target, clearHistory }) => {
252
+ if (target === "app") {
253
+ return this.loadPageForReturn(uri, clearHistory);
254
+ }
255
+ }
256
+ );
257
+ }
258
+
259
+ protected async loadPageForReturn(url: AtomUri, clearHistory: boolean): Promise<any> {
260
+ const p = await this.loadPage(url, clearHistory);
261
+ try {
262
+ return await (p as any).returnPromise;
263
+ } catch (ex) {
264
+ // this will prevent warning in chrome for unhandled exception
265
+ if ((ex.message ? ex.message : ex) === "cancelled") {
266
+ // tslint:disable-next-line: no-console
267
+ console.warn(ex);
268
+ return;
269
+ }
270
+ throw ex;
271
+ }
272
+ }
273
+
274
+ protected async loadPage(url: AtomUri, clearHistory: boolean) {
275
+ const { view: page, disposables } =
276
+ await AtomLoader.loadView<BasePage>(url, this.app, false, () => new AtomWindowViewModel(this.app));
277
+ page.title = "Title";
278
+ if (url.query && url.query.title) {
279
+ page.title = url.query.title.toString();
280
+ }
281
+
282
+ page.bind(page.element, "title", [["viewModel", "title"]]);
283
+
284
+ const selectedPage = this.selectedPage;
285
+ if (selectedPage) {
286
+ selectedPage.element.dataset.pageState = "hidden";
287
+ this.pages.add(selectedPage);
288
+ }
289
+
290
+ const hasPages = !clearHistory && this.pages.length;
291
+
292
+ if (clearHistory) {
293
+ for (const iterator of this.pages) {
294
+ const e = iterator.element;
295
+ iterator.dispose();
296
+ e.remove();
297
+ }
298
+ this.pages.length = 0;
299
+ this.selectedPage = null;
300
+ }
301
+
302
+ if (!hasPages) {
303
+ page.element.dataset.pageState = "ready";
304
+ page.iconClass = "fas fa-bars";
305
+ } else {
306
+ page.iconClass = "fas fa-arrow-left";
307
+ }
308
+
309
+ this.element.appendChild(page.element);
310
+ this.selectedPage = page;
311
+
312
+ const vm = page.viewModel as AtomWindowViewModel;
313
+ const element = page.element;
314
+ return new Promise((resolve, reject) => {
315
+ const cancel: any = (error?) => {
316
+ // page.dispose();
317
+ // element.remove();
318
+ element.dataset.pageState = "";
319
+ const last = this.pages.pop();
320
+ last.element.dataset.pageState = "ready";
321
+ setTimeout(() => {
322
+ page.dispose();
323
+ element.remove();
324
+ reject(error ?? "cancelled");
325
+ this.selectedPage = last;
326
+ }, 300);
327
+ };
328
+ const close: any = (r) => {
329
+ // page.dispose();
330
+ // element.remove();
331
+ delete element.dataset.pageState;
332
+ const last = this.pages.pop();
333
+ last.element.dataset.pageState = "ready";
334
+ setTimeout(() => {
335
+ page.dispose();
336
+ element.remove();
337
+ resolve(r);
338
+ this.selectedPage = last;
339
+ }, 300);
340
+ };
341
+ vm.cancel = cancel;
342
+ page.cancel = cancel;
343
+ vm.close = close;
344
+ page.close = close;
345
+ });
346
+
347
+ }
348
+ }