UNPKG

@itwin/core-markup

Version:
195 lines • 9.52 kB
/*--------------------------------------------------------------------------------------------- * 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