@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.
- package/dist/basic/UploadEvent.d.ts +38 -0
- package/dist/basic/UploadEvent.d.ts.map +1 -0
- package/dist/basic/UploadEvent.js +168 -0
- package/dist/basic/UploadEvent.js.map +1 -0
- package/dist/html-editor/HtmlEditorCommands.d.ts +7 -0
- package/dist/html-editor/HtmlEditorCommands.d.ts.map +1 -0
- package/dist/html-editor/HtmlEditorCommands.js +19 -0
- package/dist/html-editor/HtmlEditorCommands.js.map +1 -0
- package/dist/html-editor/InlineHtmlEditor.d.ts +37 -0
- package/dist/html-editor/InlineHtmlEditor.d.ts.map +1 -0
- package/dist/html-editor/InlineHtmlEditor.js +149 -0
- package/dist/html-editor/InlineHtmlEditor.js.map +1 -0
- package/dist/html-editor/RangeEditor.d.ts +19 -0
- package/dist/html-editor/RangeEditor.d.ts.map +1 -0
- package/dist/html-editor/RangeEditor.js +71 -0
- package/dist/html-editor/RangeEditor.js.map +1 -0
- package/dist/html-editor/commands/AttachFile.d.ts +6 -4
- package/dist/html-editor/commands/AttachFile.d.ts.map +1 -1
- package/dist/html-editor/commands/AttachFile.js +24 -59
- package/dist/html-editor/commands/AttachFile.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/src/basic/UploadEvent.tsx +203 -0
- package/src/html-editor/HtmlEditorCommands.tsx +16 -0
- package/src/html-editor/InlineHtmlEditor.tsx +209 -0
- package/src/html-editor/RangeEditor.tsx +99 -0
- package/src/html-editor/commands/AttachFile.tsx +30 -56
|
@@ -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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
}
|