/
TextEdit.ts
219 lines (190 loc) · 9.33 KB
/
TextEdit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module MarkupTools
*/
import {
BeButtonEvent, CoreTools, EventHandled, IModelApp, InputSource, ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod,
ToolAssistanceInstruction, ToolAssistanceSection,
} from "@itwin/core-frontend";
import { G, Text as MarkupText } from "@svgdotjs/svg.js";
import { MarkupApp } from "./Markup";
import { MarkupTool } from "./MarkupTool";
import { RedlineTool } from "./RedlineTool";
// cspell:ignore rbox
/** Tool to place new text notes on a Markup.
* @public
*/
export class PlaceTextTool extends RedlineTool {
public static override toolId = "Markup.Text.Place";
public static override iconSpec = "icon-text-medium";
protected override _nRequiredPoints = 1;
protected override _minPoints = 0;
protected _value!: string;
public override async onPostInstall() {
this._value = MarkupApp.props.text.startValue; // so applications can put a default string (e.g. user's initials) in the note. Can be empty
return super.onPostInstall();
}
protected override showPrompt(): void { this.provideToolAssistance(`${MarkupTool.toolKey}Text.Place.Prompts.FirstPoint`, true); }
protected override async createMarkup(svg: G, ev: BeButtonEvent, isDynamics: boolean): Promise<void> {
if (isDynamics && InputSource.Touch === ev.inputSource)
return;
const start = MarkupApp.convertVpToVb(ev.viewPoint); // starting point in viewbox coordinates
const text = new MarkupText().plain(this._value); // create a plain text element
svg.put(text); // add it to the supplied container
this.setCurrentTextStyle(text); // apply active text style
text.translate(start.x, start.y); // and position it relative to the cursor
if (isDynamics) {
svg.add(text.getOutline().attr(MarkupApp.props.text.edit.textBox).addClass(MarkupApp.textOutlineClass)); // in dynamics, draw the box around the text
} else {
await new EditTextTool(text, true).run(); // text is now positioned, open text editor
}
}
public override async onResetButtonUp(_ev: BeButtonEvent): Promise<EventHandled> {
await this.exitTool();
return EventHandled.Yes;
}
}
/** Tool for editing text. Started automatically by the place text tool and by clicking on text from the SelectTool
* @public
*/
export class EditTextTool extends MarkupTool {
public static override toolId = "Markup.Text.Edit";
public static override iconSpec = "icon-text-medium";
public editor?: HTMLTextAreaElement;
public editDiv?: HTMLDivElement;
public boxed?: G;
constructor(public text?: MarkupText | G, private _fromPlaceTool = false) { super(); }
protected override showPrompt(): void {
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, IModelApp.localization.getLocalizedString(`${MarkupTool.toolKey}Text.Edit.Prompts.FirstPoint`));
const mouseInstructions: ToolAssistanceInstruction[] = [];
const touchInstructions: ToolAssistanceInstruction[] = [];
const acceptMsg = CoreTools.translate("ElementSet.Inputs.Accept");
const rejectMsg = CoreTools.translate("ElementSet.Inputs.Exit");
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, acceptMsg, false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, acceptMsg, false, ToolAssistanceInputMethod.Mouse));
touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.TwoTouchTap, rejectMsg, false, ToolAssistanceInputMethod.Touch));
mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.RightClick, rejectMsg, false, ToolAssistanceInputMethod.Mouse));
const sections: ToolAssistanceSection[] = [];
sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
IModelApp.notifications.setToolAssistance(instructions);
}
/** Open the text editor */
public startEditor() {
let text = this.text;
if (text === undefined)
return;
if (text instanceof G) {
this.boxed = text;
text = text.children()[1] as MarkupText;
if (!(text instanceof MarkupText))
return;
this.text = text;
}
const markupDiv = this.markup.markupDiv;
const editDiv = this.editDiv = document.createElement("div"); // create a new DIV to hold the text editor
const editProps = MarkupApp.props.text.edit;
let style = editDiv.style;
style.backgroundColor = editProps.background;
style.top = style.left = "0";
style.right = style.bottom = "100%";
markupDiv.appendChild(editDiv); // add textEditor div to markup div
const divRect = markupDiv.getBoundingClientRect();
const outline = text.getOutline(); // use the outline rather than the text in case it's blank.
text.after(outline); // we have to add it to the DOM or the rbox call doesn't work.
const rbox = outline.rbox();
const bbox = outline.bbox();
outline.remove(); // take it out again.
const editor = this.editor = document.createElement("textarea");
editDiv.appendChild(editor);
editor.className = MarkupApp.textEditorClass;
editor.contentEditable = "true";
editor.spellcheck = true;
editor.wrap = "off";
// so we don't send these events to the ToolAdmin and process them by tools. We want default handling
const mouseListener = (ev: Event) => (ev.stopPropagation(), true);
(editor as any).onselectstart = editor.oncontextmenu = editor.onmousedown = editor.onmouseup = mouseListener; // enable default handling for these events
// Tab, Escape, ctrl-enter, or shift-enter all end the editor
editor.onkeydown = async (ev: KeyboardEvent) => {
if (ev.key === "Tab" || ev.key === "Escape" || (ev.key === "Enter" && (ev.shiftKey || ev.ctrlKey)))
this.exitTool(); // eslint-disable-line @typescript-eslint/no-floating-promises
ev.stopPropagation();
};
const textElStyle = window.getComputedStyle(text.node);
style = editor.style;
style.pointerEvents = "auto";
style.position = "absolute";
style.top = `${(rbox.cy - (bbox.h / 2)) - divRect.top}px`; // put the editor over the middle of the text element
style.left = `${(rbox.cx - (bbox.w / 2)) - divRect.left}px`;
style.height = editProps.size.height;
style.width = editProps.size.width;
style.resize = "both";
style.fontFamily = textElStyle.fontFamily; // set the font family and anchor to the same as the text element
style.textAnchor = textElStyle.textAnchor;
style.fontSize = editProps.fontSize; // from app.props
const parentZ = parseInt(window.getComputedStyle(markupDiv).zIndex || "0", 10);
style.zIndex = (parentZ + 200).toString();
editor.innerHTML = text.getMarkup(); // start with existing text
this.editor.focus(); // give the editor focus
// if we're started from the place text tool, select the entire current value, otherwise place the cursor at the end.
this.editor.setSelectionRange(this._fromPlaceTool ? 0 : editor.value.length, editor.value.length);
}
/** Called when EditText exits, saves the edited value into the text element */
public override async onCleanup() {
if (!this.editDiv)
return;
const text = this.text! as MarkupText;
const original = this.boxed ? this.boxed : text;
const undo = this.markup.undo;
undo.performOperation(this.keyin, () => {
const newVal = this.editor!.value;
if (newVal.trim() === "") { // if the result of the editing is blank, just delete the text element
if (!this._fromPlaceTool)
undo.onDelete(original);
original.remove(); // must do this *after* we call undo.onDelete
return;
}
let newText: G | MarkupText = text.clone();
const fontSize = text.getFontSize();
newText.createMarkup(newVal, fontSize);
if (this.boxed) {
newText = this.createBoxedText((original as G).parent() as G, newText);
newText.matrix(original.matrix());
}
original.replace(newText);
if (this._fromPlaceTool)
undo.onAdded(newText);
else
undo.onModified(newText, original);
});
const editSize = MarkupApp.props.text.edit.size;
const style = this.editor!.style;
editSize.height = style.height;
editSize.width = style.width;
this.editDiv.remove();
this.editDiv = undefined;
this.editor = undefined;
}
public override async onInstall() {
if (!await super.onInstall())
return false;
this.startEditor();
return true;
}
public override async onResetButtonUp(_ev: BeButtonEvent): Promise<EventHandled> {
await this.exitTool();
return EventHandled.Yes;
}
public override async onDataButtonUp(_ev: BeButtonEvent): Promise<EventHandled> {
await this.exitTool();
return EventHandled.Yes;
}
public override async onMouseStartDrag(_ev: BeButtonEvent): Promise<EventHandled> {
await this.exitTool();
return EventHandled.Yes;
}
}