@web-atoms/web-controls 2.3.67 → 2.3.68

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.
@@ -0,0 +1,203 @@
1
+ import { App } from "@web-atoms/core/dist/App";
2
+ import Command from "@web-atoms/core/dist/core/Command";
3
+ import EventScope from "@web-atoms/core/dist/core/EventScope";
4
+ import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
5
+ import PopupService from "@web-atoms/core/dist/web/services/PopupService";
6
+
7
+ const acceptCache = {};
8
+
9
+ export const isFileType = (acceptType: string) => {
10
+
11
+ function isFileTypeFactory(accept: string): ((file: File) => boolean) {
12
+ // Accept ny file
13
+ if (!accept || accept === "*/*") {
14
+ return () => true;
15
+ }
16
+ const types = accept.split(/\,/g).map((x) => {
17
+ x = x.trim();
18
+ if (x.startsWith(".")) {
19
+ return (file: File) => file.name.endsWith(x);
20
+ }
21
+ if (x.endsWith("/*")) {
22
+ const prefix = x.substring(0, x.length - 1);
23
+ return (file: File) => file.type.startsWith(prefix);
24
+ }
25
+ return (file: File) => file.type === x;
26
+ });
27
+ return (file: File) => {
28
+ for (const iterator of types) {
29
+ if (iterator(file)) {
30
+ return true;
31
+ }
32
+ }
33
+ return false;
34
+ };
35
+ }
36
+
37
+ return acceptCache[acceptType] ??= isFileTypeFactory(acceptType);
38
+ };
39
+
40
+ export type FilesAvailableEventArgs<T = any> = CustomEvent<{ files: File[], extra: T }>;
41
+
42
+ export interface IUploadParams<T = any> {
43
+ "event-files-available"?: (ce: FilesAvailableEventArgs<T>) => any,
44
+ uploadEvent?: string;
45
+ accept?: string,
46
+ capture?: string;
47
+ multiple?: any;
48
+ /** Will enforce that the selected file matches the given accept types */
49
+ forceType?: boolean;
50
+ maxSize?: number;
51
+ upload?: boolean;
52
+ /** Extra will hold other information that will be available in upload event */
53
+ extra?: T;
54
+ authorize?: boolean;
55
+ ariaLabel?: string;
56
+ }
57
+
58
+ const requestUpload = new Command();
59
+
60
+ let previousFile: HTMLInputElement;
61
+
62
+ requestUpload.eventScope.listen((ce: CustomEvent) => {
63
+ const element = ce.target as HTMLElement;
64
+ const authorize = element.dataset.authorize;
65
+ if (authorize === "true") {
66
+ if(!App.authorize()) {
67
+ return;
68
+ }
69
+ }
70
+
71
+ const multiple = element.dataset.multiple === "true";
72
+ const extra = (element as any).extra;
73
+ const upload = element.dataset.upload === "true";
74
+ const uploadEvent = element.getAttribute("data-upload-event");
75
+
76
+ const chain: HTMLElement[] = [];
77
+ let start = element;
78
+ while(start) {
79
+ chain.push(start);
80
+ start = start.parentElement;
81
+ }
82
+
83
+ previousFile?.remove();
84
+ const file = document.createElement("input");
85
+ file.type = "file";
86
+ file.multiple = multiple;
87
+ const accept = element.dataset.accept || "*/*";
88
+ file.accept = accept;
89
+ const capture = element.dataset.capture;
90
+ if (capture) {
91
+ file.capture = capture;
92
+ }
93
+ file.style.position = "absolute";
94
+ file.style.left = "-1000px";
95
+ file.style.top = "-1000px";
96
+ document.body.append(file);
97
+ previousFile = file;
98
+ const maxSize = parseInt(element.dataset.maxSize || "0", 10);
99
+ const forceType = element.dataset.forceType === "true";
100
+
101
+ file.addEventListener("change", () => {
102
+ let files = Array.from(file.files);
103
+
104
+ let msg = "";
105
+
106
+ if (forceType || maxSize) {
107
+
108
+ const validated = [];
109
+
110
+ const checkFileType = isFileType(accept);
111
+
112
+ for (const iterator of files) {
113
+ if (maxSize && iterator.size > maxSize) {
114
+ msg += `Size of ${iterator.name} is more than ${maxSize}`;
115
+ continue;
116
+ }
117
+ if (forceType) {
118
+ if (!checkFileType(iterator)) {
119
+ msg += `${iterator.name} is invalid file.`;
120
+ continue;
121
+ }
122
+ }
123
+ validated.push(iterator);
124
+ }
125
+
126
+ files = validated;
127
+ }
128
+
129
+ file.remove();
130
+ previousFile = null;
131
+
132
+ chain.reverse();
133
+ while(chain.length) {
134
+ const root = chain.pop();
135
+ if (root.isConnected) {
136
+
137
+ const control = AtomControl.from(root);
138
+ if (msg) {
139
+ control.app.runAsync(() => PopupService.alert({ message: msg}));
140
+ if (files.length === 0) {
141
+ return;
142
+ }
143
+ }
144
+ // if (upload) {
145
+ // (window as any).uploading = true;
146
+ // control.app.runAsync(async () => {
147
+ // try {
148
+ // const afs = await UploadFilesWindow.showModal({ parameters: { files, uploadEvent}});
149
+ // root.dispatchEvent(new CustomEvent(uploadEvent, { detail: { files: afs, extra }, bubbles: true}));
150
+ // } finally {
151
+ // (window as any).uploading = false;
152
+ // }
153
+ // });
154
+ // break;
155
+ // }
156
+ root.dispatchEvent(new CustomEvent(uploadEvent, { detail: {
157
+ files,
158
+ extra
159
+ }, bubbles: true }));
160
+ break;
161
+ }
162
+ }
163
+ });
164
+
165
+ setTimeout(() => {
166
+ file.dispatchEvent(new MouseEvent("click"));
167
+ });
168
+ });
169
+
170
+
171
+ let id = 1;
172
+
173
+ export default class UploadEvent {
174
+
175
+ public static AttachUploadAction({
176
+ uploadEvent = "files-available",
177
+ accept = "*/*",
178
+ capture,
179
+ multiple = false,
180
+ forceType = true,
181
+ maxSize = 524288000,
182
+ extra,
183
+ upload = true,
184
+ authorize = true,
185
+ ariaLabel = "Upload",
186
+ ... others
187
+ }: IUploadParams) {
188
+ return {
189
+ ... others,
190
+ ... requestUpload.registerOnClick(""),
191
+ "data-upload-event": uploadEvent,
192
+ "data-accept": accept,
193
+ "data-multiple": multiple ? "true" : "false",
194
+ "data-capture": capture,
195
+ "data-upload": upload ? "true" : "false",
196
+ "data-force-type": forceType ? "true" : "false",
197
+ "data-max-size" : maxSize ? maxSize.toString() : undefined,
198
+ "data-authorize": authorize.toString(),
199
+ "aria-label": ariaLabel,
200
+ extra,
201
+ };
202
+ }
203
+ }
@@ -0,0 +1,16 @@
1
+ import { IRangeUpdate } from "./RangeEditor";
2
+
3
+ export interface IHtmlEditorCommand {
4
+ [key: string]: (e: IRangeUpdate) => void;
5
+ }
6
+
7
+
8
+ const HtmlEditorCommands = {
9
+
10
+ bold({ range, check, update}) {
11
+
12
+ }
13
+
14
+ } as IHtmlEditorCommand;
15
+
16
+ export default HtmlEditorCommands;
@@ -0,0 +1,209 @@
1
+ import XNode from "@web-atoms/core/dist/core/XNode";
2
+ import sleep from "@web-atoms/core/dist/core/sleep";
3
+ import { CancelToken } from "@web-atoms/core/dist/core/types";
4
+ import StyleRule from "@web-atoms/core/dist/style/StyleRule";
5
+ import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
6
+ import { ChildEnumerator, descendentElementIterator } from "@web-atoms/core/dist/web/core/AtomUI";
7
+ import CSS from "@web-atoms/core/dist/web/styles/CSS";
8
+ import RangeEditor, { RangeEditorCommands } from "./RangeEditor";
9
+
10
+ import "@web-atoms/data-styles/data-styles";
11
+ import { showImageDialog } from "./commands/AddImage";
12
+ import { FilesAvailableEventArgs } from "../basic/UploadEvent";
13
+
14
+ CSS(StyleRule()
15
+ .child(StyleRule("[data-element=toolbar]")
16
+ .verticalFlexLayout({})
17
+ .child(StyleRule(".toolbar")
18
+ .flexLayout({})
19
+ .nested(StyleRule(".command")
20
+ .and(StyleRule(".pressed")
21
+ .fontWeight("bold")
22
+ )
23
+ )
24
+ )
25
+ )
26
+ , "[data-inline-editor=inline-editor]");
27
+
28
+ export default class InlineHtmlEditor extends AtomControl {
29
+
30
+ /**
31
+ * Maximum undo limit
32
+ */
33
+ public undoLimit = 100;
34
+
35
+ public "event-content-changed"?: (ce: CustomEvent<string>) => any;
36
+ public "event-content-ready"?: (ce: CustomEvent<HTMLElement>) => any;
37
+ public "event-load-suggestions"?: (ce: CustomEvent<string>) => any;
38
+ public "event-files-available"?: (ce: FilesAvailableEventArgs) => any;
39
+
40
+ public editableSelector: string = ".editable";
41
+
42
+ public get htmlContent() {
43
+ return this.content;
44
+ }
45
+
46
+ public set htmlContent(v: string) {
47
+ this.content = v;
48
+ }
49
+
50
+ public get content() {
51
+ return this.editor.innerHTML;
52
+ }
53
+
54
+ public set content(value: string) {
55
+ this.editor.innerHTML = value;
56
+ setTimeout(() => this.onContentSet(), 100);
57
+ }
58
+
59
+ public set toolbar(v: () => XNode) {
60
+ this.dispose(this.toolbarElement);
61
+ this.toolbarElement.innerHTML = "";
62
+ this.render(v(), this.toolbarElement, this);
63
+ }
64
+
65
+ private version: number;
66
+
67
+ private selection: Range;
68
+
69
+ private editor: HTMLElement;
70
+
71
+ private toolbarElement: HTMLElement;
72
+
73
+ private token: CancelToken;
74
+
75
+ protected executeCommand(command: string, showUI?: boolean, value?: string) {
76
+ // restore selection...
77
+ const selection = window.getSelection();
78
+ selection.removeAllRanges();
79
+ selection.addRange(this.selection);
80
+ return document.execCommand(command, showUI, value);
81
+ // // debugger;
82
+ // // restore selection
83
+ // const selection = window.getSelection();
84
+ // selection.removeAllRanges();
85
+ // const range = this.selection;
86
+ // // document.execCommand(command, showUI, value);
87
+ // const cmd = RangeEditorCommands[command];
88
+ // if (cmd) {
89
+ // RangeEditor.updateRange({
90
+ // ... cmd,
91
+ // value,
92
+ // range,
93
+ // });
94
+ // }
95
+ // selection.addRange(range);
96
+ }
97
+
98
+ protected getStyle(name: string) {
99
+
100
+ const selection = this.selection;
101
+ if (!selection) {
102
+ return void 0;
103
+ }
104
+ const node = selection;
105
+ const e = node.startContainer.parentElement as HTMLElement;
106
+ return window.getComputedStyle(e)[name]; // const range = selection.getRangeAt(0);
107
+ // const container = range.commonAncestorContainer;
108
+ // if(container.nodeType === Node.ELEMENT_NODE) {
109
+ // return window.getComputedStyle(container as HTMLElement)[name];
110
+ // }
111
+ // return void 0;
112
+ }
113
+
114
+ protected queryCommandState(command: string) {
115
+ return document.queryCommandState(command);
116
+ // const selection = this.selection;
117
+ // if (!selection) {
118
+ // return;
119
+ // }
120
+ // const range = selection;
121
+ // const cmd = RangeEditorCommands[command];
122
+ // if (cmd) {
123
+ // return RangeEditor.checkRange({
124
+ // ... cmd,
125
+ // range,
126
+ // });
127
+ // }
128
+ }
129
+
130
+ protected onContentSet() {
131
+
132
+ const start = this.editor.querySelector(this.editableSelector) as HTMLElement;
133
+ if (start) {
134
+ start.contentEditable = "true";
135
+ } else {
136
+ (this.editor.firstElementChild as HTMLElement).contentEditable = "true";
137
+ }
138
+
139
+ this.editor.dispatchEvent(new CustomEvent("contentReady", { detail: this.editor.innerHTML, bubbles: true }));
140
+ }
141
+
142
+ protected saveSelection() {
143
+ const selection = window.getSelection();
144
+ this.selection = selection.rangeCount === 0 ? null : selection.getRangeAt(0);
145
+ }
146
+
147
+ public insertImage(s: any, e: Event) {
148
+ return showImageDialog(s, e);
149
+ }
150
+
151
+ protected preCreate(): void {
152
+ this.version = 1;
153
+ this.element.setAttribute("data-inline-editor", "inline-editor");
154
+ this.render(<div>
155
+ <div data-element="toolbar"/>
156
+ <div data-element="editor"/>
157
+ </div>);
158
+
159
+ this.editor = this.element.querySelector(`[data-element=editor]`);
160
+ this.toolbarElement = this.element.querySelector(`[data-element=toolbar]`);
161
+
162
+ this.bindEvent(this.editor, "blur", () => this.saveSelection(), void 0, true);
163
+ this.bindEvent(this.editor, "input", (e: InputEvent) => this.onContentInput(e));
164
+ this.bindEvent(this.editor, "keydown", (e: KeyboardEvent) => this.updateQueryCommand());
165
+ this.bindEvent(this.editor, "click", (e: KeyboardEvent) => this.updateQueryCommand());
166
+ this.bindEvent(this.editor, "paste", () => this.onContentInput());
167
+ this.bindEvent(this.editor, "cut", () => this.onContentInput());
168
+ this.bindEvent(this.editor, "drop", (e: DragEvent) => this.onDrop(e));
169
+ }
170
+
171
+ protected updateQueryCommand() {
172
+ this.version++;
173
+ }
174
+
175
+ protected onDrop(e: DragEvent) {
176
+ e.preventDefault();
177
+ const text = e.dataTransfer.getData("text/plain");
178
+ if (!text) {
179
+ return;
180
+ }
181
+ let last: HTMLElement = null;
182
+ for (const node of descendentElementIterator(this.element)) {
183
+ if ((node as HTMLElement).isContentEditable) {
184
+ last = node as HTMLElement;
185
+ continue;
186
+ }
187
+ }
188
+ if (last) {
189
+ last.appendChild(document.createTextNode(text));
190
+ }
191
+ }
192
+
193
+ protected contentModified() {
194
+ this.element.dispatchEvent(new CustomEvent("contentChanged", { detail: this.editor.innerHTML, bubbles: true }));
195
+ }
196
+
197
+ private onContentInput(e?: InputEvent) {
198
+ this.token?.cancel();
199
+ const token = this.token = new CancelToken();
200
+ this.app.runAsync(async () => {
201
+ await sleep(500, token, false);
202
+ if(token.cancelled) {
203
+ return;
204
+ }
205
+ this.contentModified();
206
+ });
207
+ }
208
+
209
+ }
@@ -0,0 +1,99 @@
1
+ import { ChildEnumerator, descendentElementIterator } from "@web-atoms/core/dist/web/core/AtomUI";
2
+
3
+ export const checkAnyParent = (check: (e: HTMLElement) => boolean) => (e: HTMLElement) => {
4
+ while (e) {
5
+ if (check(e)) {
6
+ return true;
7
+ }
8
+ e = e.parentElement;
9
+ }
10
+ return false;
11
+ };
12
+
13
+ export interface IRangeUpdate {
14
+ range: Range;
15
+ check: (e: HTMLElement) => boolean;
16
+ update: (e: HTMLElement, v?: any) => HTMLElement;
17
+ value?: any;
18
+ }
19
+
20
+ export interface IRangeCommand {
21
+ check: (e: HTMLElement) => boolean;
22
+ update: (e: HTMLElement, v?: any) => HTMLElement;
23
+ value?: any;
24
+ }
25
+
26
+ export default class RangeEditor {
27
+
28
+ public static updateAttribute(range: Range, name: string, value: string, anyParent: boolean = true) {
29
+ return this.updateRange({
30
+ range,
31
+ check: anyParent
32
+ ? checkAnyParent((e) => e.getAttribute(name) === value)
33
+ : (e) => e.getAttribute(name) === value,
34
+ update: (e) => (e.setAttribute(name, value), e)
35
+ })
36
+ }
37
+
38
+ public static checkRange(
39
+ {
40
+ range,
41
+ check,
42
+ }: IRangeUpdate
43
+ ) {
44
+ const root = range.startContainer.nodeType !== Node.ELEMENT_NODE
45
+ ? range.startContainer.parentElement
46
+ : range.startContainer as HTMLElement;
47
+ return check(root);
48
+ }
49
+
50
+
51
+ public static updateRange(
52
+ {
53
+ range,
54
+ check,
55
+ update,
56
+ value
57
+ }: IRangeUpdate
58
+ ) {
59
+
60
+ }
61
+
62
+ }
63
+
64
+ const updateAttribute = (name: string, value: string, anyParent = true) => ({
65
+ update: (e: HTMLElement, v = value) =>
66
+ (e.setAttribute(name, v), e),
67
+ check: anyParent
68
+ ? checkAnyParent((e: HTMLElement) => e.getAttribute(name) === value)
69
+ : (e: HTMLElement) => e.getAttribute(name) === value
70
+ });
71
+
72
+ const updateStyle = (name: keyof CSSStyleDeclaration, value: string, anyParent = true) => ({
73
+ update: (e: HTMLElement, v = value) =>
74
+ (e.style[name as any] = v, e),
75
+ check: anyParent
76
+ ? checkAnyParent((e: HTMLElement) => e.style[name as any] === value)
77
+ : (e: HTMLElement) => e.style[name as any] === value
78
+ });
79
+
80
+ export const RangeEditorCommands: Record<string, IRangeCommand> = {
81
+ bold: updateStyle("fontWeight", "bold"),
82
+ italic: updateStyle("fontStyle", "italic"),
83
+ underline: updateStyle("textDecoration", "underline"),
84
+ strikeThrough: updateStyle("textDecoration", "line-through"),
85
+ foreColor: {
86
+ check: () => false,
87
+ update: (e, value) => {
88
+ e.style.color = value;
89
+ return e;
90
+ }
91
+ },
92
+ removeFormat: {
93
+ check: () => false,
94
+ update: (e) => {
95
+ e.removeAttribute("style");
96
+ return e;
97
+ }
98
+ }
99
+ };
@@ -1,63 +1,37 @@
1
+ import XNode from "@web-atoms/core/dist/core/XNode";
1
2
  import AtomHtmlEditor from "../AtomHtmlEditor";
2
3
  import CommandButton from "./CommandButton";
3
4
  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
- }
5
+ import UploadEvent from "../../basic/UploadEvent";
6
+ import { AtomControl } from "@web-atoms/core/dist/web/controls/AtomControl";
33
7
 
34
8
  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
9
+ accept = "image/*",
10
+ maxSize = 204800,
11
+ authorize = true,
12
+ capture = null as string,
13
+ ariaLabel = "upload"
55
14
  }) {
56
- return CommandButton({
57
- icon: "ri-attachment-2",
58
- insertCommand,
59
- disabled: false,
60
- title: "Insert Image",
61
- eventInsertHtml
62
- });
15
+ // return CommandButton({
16
+ // icon: "ri-attachment-2",
17
+ // insertCommand,
18
+ // disabled: false,
19
+ // title: "Insert Image",
20
+ // eventInsertHtml
21
+ // });
22
+ return <button
23
+ title="Insert Image"
24
+ class="command"
25
+ { ... UploadEvent.AttachUploadAction({
26
+ accept,
27
+ forceType: true,
28
+ maxSize,
29
+ authorize,
30
+ capture,
31
+ multiple: false,
32
+ ariaLabel
33
+ })}
34
+ >
35
+ <i class="ri-attachment-2" />
36
+ </button>;
63
37
  }