@itwin/core-markup
Version:
iTwin.js markup package
195 lines • 9.52 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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 { CoreTools, EventHandled, IModelApp, InputSource, ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod, } 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 {
static toolId = "Markup.Text.Place";
static iconSpec = "icon-text-medium";
_nRequiredPoints = 1;
_minPoints = 0;
_value;
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();
}
showPrompt() { this.provideToolAssistance(`${MarkupTool.toolKey}Text.Place.Prompts.FirstPoint`, true); }
async createMarkup(svg, ev, isDynamics) {
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
}
}
async onResetButtonUp(_ev) {
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 {
text;
_fromPlaceTool;
static toolId = "Markup.Text.Edit";
static iconSpec = "icon-text-medium";
editor;
editDiv;
boxed;
constructor(text, _fromPlaceTool = false) {
super();
this.text = text;
this._fromPlaceTool = _fromPlaceTool;
}
showPrompt() {
const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, IModelApp.localization.getLocalizedString(`${MarkupTool.toolKey}Text.Edit.Prompts.FirstPoint`));
const mouseInstructions = [];
const touchInstructions = [];
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 = [];
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 */
startEditor() {
let text = this.text;
if (text === undefined)
return;
if (text instanceof G) {
this.boxed = text;
text = text.children()[1];
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) => (ev.stopPropagation(), true);
editor.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) => {
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 */
async onCleanup() {
if (!this.editDiv)
return;
const text = this.text;
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 = text.clone();
const fontSize = text.getFontSize();
newText.createMarkup(newVal, fontSize);
if (this.boxed) {
newText = this.createBoxedText(original.parent(), 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;
}
async onInstall() {
if (!await super.onInstall())
return false;
this.startEditor();
return true;
}
async onResetButtonUp(_ev) {
await this.exitTool();
return EventHandled.Yes;
}
async onDataButtonUp(_ev) {
await this.exitTool();
return EventHandled.Yes;
}
async onMouseStartDrag(_ev) {
await this.exitTool();
return EventHandled.Yes;
}
}
//# sourceMappingURL=TextEdit.js.map