From 80bfa1381f349ab1177f46e6eba77659ed486938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Wed, 28 May 2025 11:52:14 +0200 Subject: [PATCH 1/2] chore: initial commit From 86fdc681cdcf05cb426a1097fb4e016c9018a82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesus=20Manuel=20Pi=C3=B1eiro=20Cid?= Date: Wed, 28 May 2025 16:05:53 +0200 Subject: [PATCH 2/2] fix: mantain text sizing and introduce text layouts --- code/CHANGELOG.md | 4 + .../sdk/src/actions/text-tool/constants.ts | 6 + .../sdk/src/actions/text-tool/text-tool.ts | 11 +- .../sdk/src/actions/text-tool/types.ts | 8 +- code/packages/sdk/src/nodes/text/text.ts | 325 +++++++++++++++--- .../plugins/stage-panning/stage-panning.ts | 2 +- .../docs/main/changelog/prerelease/019.0.mdx | 4 + 7 files changed, 303 insertions(+), 57 deletions(-) diff --git a/code/CHANGELOG.md b/code/CHANGELOG.md index f29300182..85b5680e3 100644 --- a/code/CHANGELOG.md +++ b/code/CHANGELOG.md @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#288](https://github.com/InditexTech/weavejs/issues/288) Provide a tool for erasing elements +### Fixed + +- [#290](https://github.com/InditexTech/weavejs/issues/290) Maintain text node size when changed + ## [0.18.0] - 2025-05-27 ### Changed diff --git a/code/packages/sdk/src/actions/text-tool/constants.ts b/code/packages/sdk/src/actions/text-tool/constants.ts index ecdad367f..756e88b3d 100644 --- a/code/packages/sdk/src/actions/text-tool/constants.ts +++ b/code/packages/sdk/src/actions/text-tool/constants.ts @@ -9,3 +9,9 @@ export const TEXT_TOOL_STATE = { ['ADDING']: 'adding', ['FINISHED']: 'finished', } as const; + +export const TEXT_LAYOUT = { + ['AUTO_ALL']: 'auto-all', + ['AUTO_HEIGHT']: 'auto-height', + ['FIXED']: 'fixed', +} as const; diff --git a/code/packages/sdk/src/actions/text-tool/text-tool.ts b/code/packages/sdk/src/actions/text-tool/text-tool.ts index efdd032e8..01c65eed0 100644 --- a/code/packages/sdk/src/actions/text-tool/text-tool.ts +++ b/code/packages/sdk/src/actions/text-tool/text-tool.ts @@ -6,7 +6,11 @@ import { v4 as uuidv4 } from 'uuid'; import { WeaveAction } from '@/actions/action'; import { type Vector2d } from 'konva/lib/types'; import { type WeaveTextToolActionState } from './types'; -import { TEXT_TOOL_ACTION_NAME, TEXT_TOOL_STATE } from './constants'; +import { + TEXT_LAYOUT, + TEXT_TOOL_ACTION_NAME, + TEXT_TOOL_STATE, +} from './constants'; import Konva from 'konva'; import { WeaveNodesSelectionPlugin } from '@/plugins/nodes-selection/nodes-selection'; import type { WeaveTextNode } from '@/nodes/text/text'; @@ -103,8 +107,9 @@ export class WeaveTextToolAction extends WeaveAction { const node = nodeHandler.create(this.textId, { x: this.clickPoint?.x ?? 0, y: this.clickPoint?.y ?? 0, - text: 'Your text here...', - width: 300, + text: '', + layout: TEXT_LAYOUT.AUTO_ALL, + width: 10, fontSize: 20, fontFamily: 'Arial, sans-serif', fill: '#000000ff', diff --git a/code/packages/sdk/src/actions/text-tool/types.ts b/code/packages/sdk/src/actions/text-tool/types.ts index dfd5c2a57..b2b301c65 100644 --- a/code/packages/sdk/src/actions/text-tool/types.ts +++ b/code/packages/sdk/src/actions/text-tool/types.ts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: Apache-2.0 -import { TEXT_TOOL_STATE } from "./constants"; +import { TEXT_TOOL_STATE, TEXT_LAYOUT } from './constants'; export type WeaveTextToolActionStateKeys = keyof typeof TEXT_TOOL_STATE; -export type WeaveTextToolActionState = (typeof TEXT_TOOL_STATE)[WeaveTextToolActionStateKeys]; +export type WeaveTextToolActionState = + (typeof TEXT_TOOL_STATE)[WeaveTextToolActionStateKeys]; + +export type WeaveTextLayoutKeys = keyof typeof TEXT_LAYOUT; +export type WeaveTextLayout = (typeof TEXT_LAYOUT)[WeaveTextLayoutKeys]; diff --git a/code/packages/sdk/src/nodes/text/text.ts b/code/packages/sdk/src/nodes/text/text.ts index fd2dce0b9..d1738b386 100644 --- a/code/packages/sdk/src/nodes/text/text.ts +++ b/code/packages/sdk/src/nodes/text/text.ts @@ -14,6 +14,7 @@ import { WeaveNodesSelectionPlugin } from '@/plugins/nodes-selection/nodes-selec import { resetScale } from '@/utils'; import { WEAVE_TEXT_NODE_TYPE } from './constants'; import { SELECTION_TOOL_ACTION_NAME } from '@/actions/selection-tool/constants'; +import { TEXT_LAYOUT } from '@/actions/text-tool/constants'; export class WeaveTextNode extends WeaveNode { protected nodeType: string = WEAVE_TEXT_NODE_TYPE; @@ -40,6 +41,26 @@ export class WeaveTextNode extends WeaveNode { this.setupDefaultNodeEvents(text); + text.on('transform', (e) => { + const node = e.target; + + if (this.isSelecting() && this.isNodeSelected(node)) { + const nodeHandler = + this.instance.getNodeHandler(WEAVE_TEXT_NODE_TYPE); + const serializedNode = nodeHandler.serialize( + node as WeaveElementInstance + ); + this.instance.updateNode({ + ...serializedNode, + props: { + ...serializedNode.props, + layout: TEXT_LAYOUT.FIXED, + }, + }); + e.cancelBubble = true; + } + }); + window.addEventListener('keypress', (e) => { if ( e.key === 'Enter' && @@ -144,28 +165,29 @@ export class WeaveTextNode extends WeaveNode { } private resizeTextAreaDOM(textNode: Konva.Text) { - const textArea = document.getElementById( - textNode.id() + const textAreaContainer = document.getElementById( + `${textNode.id()}_container` ) as HTMLTextAreaElement | null; - if (!textArea) { + if (!textAreaContainer) { return; } const stage = this.instance.getStage(); - const textPosition = textNode.absolutePosition(); + const containerRect = stage.container().getBoundingClientRect(); + const textPosition = textNode.getClientRect(); const position: Vector2d = { - x: stage.container().offsetLeft + textPosition.x, - y: stage.container().offsetTop + textPosition.y, + x: textPosition.x, + y: textPosition.y, }; - textArea.style.top = position.y + 'px'; - textArea.style.left = position.x + 'px'; - textArea.style.width = + textAreaContainer.style.top = -containerRect.y + position.y + 'px'; + textAreaContainer.style.left = -containerRect.x + position.x + 'px'; + textAreaContainer.style.width = textNode.getWidth() * textNode.getAbsoluteScale().x + 'px'; - textArea.style.height = + textAreaContainer.style.height = textNode.getHeight() * textNode.getAbsoluteScale().x + 'px'; - textArea.style.fontSize = + textAreaContainer.style.fontSize = textNode.fontSize() * textNode.getAbsoluteScale().x + 'px'; } @@ -185,13 +207,56 @@ export class WeaveTextNode extends WeaveNode { this.resizeTextAreaDOM(textNode); }; + private textAreaDomResizer( + textArea: HTMLTextAreaElement, + textNode: Konva.Text + ) { + textArea.style.height = 'auto'; + textArea.style.height = + textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + textArea.rows = textArea.value.split('\n').length; + } + + private textAreaRenderedWidth( + textArea: HTMLTextAreaElement, + textNode: Konva.Text + ) { + let maxWidth = 0; + const lines = textArea.value.split('\n'); + for (const line of lines) { + const textSize = textNode.measureSize(line); + if (textSize.width > maxWidth) { + maxWidth = textSize.width; + } + } + return maxWidth; + } + private createTextAreaDOM(textNode: Konva.Text, position: Vector2d) { const stage = this.instance.getStage(); // create textarea and style it + const textAreaSuperContainer = document.createElement('div'); + textAreaSuperContainer.id = `${textNode.id()}_supercontainer`; + textAreaSuperContainer.style.position = 'absolute'; + textAreaSuperContainer.style.top = '0px'; + textAreaSuperContainer.style.left = '0px'; + textAreaSuperContainer.style.bottom = '0px'; + textAreaSuperContainer.style.right = '0px'; + textAreaSuperContainer.style.overflow = 'hidden'; + textAreaSuperContainer.style.pointerEvents = 'none'; + + const textAreaContainer = document.createElement('div'); + textAreaContainer.id = `${textNode.id()}_container`; + textAreaContainer.style.pointerEvents = 'none'; const textArea = document.createElement('textarea'); textArea.id = textNode.id(); - stage.container().appendChild(textArea); + textArea.style.pointerEvents = 'none'; + textAreaContainer.appendChild(textArea); + textAreaSuperContainer.appendChild(textAreaContainer); + stage.container().appendChild(textAreaSuperContainer); + + const containerRect = stage.container().getBoundingClientRect(); this.instance.addEventListener( 'onZoomChange', @@ -209,52 +274,180 @@ export class WeaveTextNode extends WeaveNode { // and sometimes it is hard to make it 100% the same. But we will try... textArea.value = textNode.text(); textArea.id = textNode.id(); - textArea.style.position = 'fixed'; - textArea.style.top = position.y + 'px'; - textArea.style.left = position.x + 'px'; - textArea.style.width = - (textNode.width() - textNode.padding() * 2) * - textNode.getAbsoluteScale().x + - 'px'; - textArea.style.height = - (textNode.height() - textNode.padding() * 2) * - textNode.getAbsoluteScale().x + - 2 + - 'px'; + textAreaContainer.style.overflow = 'hidden'; + textAreaContainer.style.display = 'flex'; + textAreaContainer.style.justifyContent = 'start'; + if (textNode.getAttrs().verticalAlign === 'top') { + textAreaContainer.style.alignItems = 'start'; + } + if (textNode.getAttrs().verticalAlign === 'middle') { + textAreaContainer.style.alignItems = 'center'; + } + if (textNode.getAttrs().verticalAlign === 'bottom') { + textAreaContainer.style.alignItems = 'end'; + } + textAreaContainer.style.position = 'absolute'; + textAreaContainer.style.top = -containerRect.y + position.y + 'px'; + textAreaContainer.style.left = -containerRect.x + position.x + 'px'; + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + textAreaContainer.style.width = textArea.scrollWidth + 'px'; + textAreaContainer.style.height = + (textNode.height() - textNode.padding() * 2) * + textNode.getAbsoluteScale().x + + 2 + + 'px'; + } + if (textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT) { + textAreaContainer.style.width = + (textNode.width() - textNode.padding() * 2) * + textNode.getAbsoluteScale().x + + 'px'; + textAreaContainer.style.height = + (textNode.height() - textNode.padding() * 2) * + textNode.getAbsoluteScale().x + + 2 + + 'px'; + } + if (textNode.getAttrs().layout === TEXT_LAYOUT.FIXED) { + textAreaContainer.style.width = + (textNode.width() - textNode.padding() * 2) * + textNode.getAbsoluteScale().x + + 'px'; + textAreaContainer.style.height = + (textNode.height() - textNode.padding() * 2) * + textNode.getAbsoluteScale().x + + 2 + + 'px'; + } textArea.style.fontSize = textNode.fontSize() * textNode.getAbsoluteScale().x + 'px'; - textArea.style.border = 'solid 1px #1e40af'; + textArea.rows = textNode.text().split('\n').length; + textAreaContainer.style.border = 'solid 1px #1e40af'; + textArea.style.width = '100%'; textArea.style.minHeight = 'auto'; textArea.style.margin = '0px'; textArea.style.overflow = 'hidden'; textArea.style.background = 'transparent'; + textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.resize = 'none'; textArea.style.lineHeight = `${textNode.lineHeight()}`; textArea.style.fontFamily = textNode.fontFamily(); - textArea.style.transformOrigin = 'left top'; + textAreaContainer.style.transformOrigin = 'left top'; textArea.style.textAlign = textNode.align(); textArea.style.color = `${textNode.fill()}`; textArea.onfocus = () => { - textArea.style.height = 'auto'; - textArea.style.height = - textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; - textArea.setSelectionRange(textArea.value.length, textArea.value.length); + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + const textAreaWidth = this.textAreaRenderedWidth(textArea, textNode); + textAreaContainer.style.width = + (textAreaWidth + 3.2) * textNode.getAbsoluteScale().x + 'px'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + textAreaContainer.style.height = + textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + textArea.style.width = 'auto'; + } + this.textAreaDomResizer(textArea, textNode); }; textArea.onkeydown = () => { - textArea.style.height = 'auto'; - textArea.style.height = - textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + const textAreaWidth = this.textAreaRenderedWidth(textArea, textNode); + textAreaContainer.style.width = + (textAreaWidth + 3.2) * textNode.getAbsoluteScale().x + 'px'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + textAreaContainer.style.height = + textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + } + this.textAreaDomResizer(textArea, textNode); }; textArea.onkeyup = () => { - textArea.style.height = 'auto'; - textArea.style.height = - textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + const textAreaWidth = this.textAreaRenderedWidth(textArea, textNode); + textAreaContainer.style.width = + (textAreaWidth + 3.2) * textNode.getAbsoluteScale().x + 'px'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + textAreaContainer.style.height = + textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + } + this.textAreaDomResizer(textArea, textNode); }; textArea.oninput = () => { - textArea.style.height = 'auto'; - textArea.style.height = - textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + const textAreaWidth = this.textAreaRenderedWidth(textArea, textNode); + textAreaContainer.style.width = + (textAreaWidth + 3.2) * textNode.getAbsoluteScale().x + 'px'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + textAreaContainer.style.height = + textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + } + this.textAreaDomResizer(textArea, textNode); }; const rotation = textNode.rotation(); let transform = ''; @@ -273,12 +466,22 @@ export class WeaveTextNode extends WeaveNode { const handleKeyDown = (e: any) => { e.stopPropagation(); if (textArea && textNode && e.key === 'Escape') { - textNode.width( - parseFloat(textArea.style.width) * (1 / textNode.getAbsoluteScale().x) - ); - textNode.height( - (textArea.scrollHeight + 1.6) * (1 / textNode.getAbsoluteScale().x) - ); + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + const textAreaWidth = this.textAreaRenderedWidth(textArea, textNode); + textNode.width(textAreaWidth + 3.2); + } + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL + ) { + textNode.height( + (textArea.scrollHeight + 1.6) * (1 / textNode.getAbsoluteScale().x) + ); + } textNode.text(textArea.value); this.updateNode(textNode); this.removeTextAreaDOM(textNode); @@ -299,12 +502,16 @@ export class WeaveTextNode extends WeaveNode { const handleKeyUp = () => { textNode.text(textArea.value); if (textArea && textNode) { - // textNode.text(textArea.value); - textArea.style.width = - textNode.width() * textNode.getAbsoluteScale().x + 'px'; - textArea.style.height = 'auto'; - textArea.style.height = - textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + if ( + !textNode.getAttrs().layout || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_ALL || + textNode.getAttrs().layout === TEXT_LAYOUT.AUTO_HEIGHT + ) { + textAreaContainer.style.height = 'auto'; + textAreaContainer.style.height = + textArea.scrollHeight + 1.6 * textNode.getAbsoluteScale().x + 'px'; + } + this.textAreaDomResizer(textArea, textNode); } }; @@ -347,6 +554,22 @@ export class WeaveTextNode extends WeaveNode { delete window.weaveTextEditing[textNode.id()]; + const textAreaSuperContainer = document.getElementById( + `${textNode.id()}_supercontainer` + ) as HTMLTextAreaElement | null; + + if (textAreaSuperContainer) { + textAreaSuperContainer.remove(); + } + + const textAreaContainer = document.getElementById( + `${textNode.id()}_container` + ) as HTMLTextAreaElement | null; + + if (textAreaContainer) { + textAreaContainer.remove(); + } + const textArea = document.getElementById( textNode.id() ) as HTMLTextAreaElement | null; diff --git a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts index dfc7beff0..d7169e710 100644 --- a/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts +++ b/code/packages/sdk/src/plugins/stage-panning/stage-panning.ts @@ -217,7 +217,7 @@ export class WeaveStagePanningPlugin extends WeavePlugin { stage.x(stage.x() - e.deltaX); stage.y(stage.y() - e.deltaY); - this.instance.emit('stageMove', undefined); + this.instance.emit('onStageMove', undefined); }); } diff --git a/docs/content/docs/main/changelog/prerelease/019.0.mdx b/docs/content/docs/main/changelog/prerelease/019.0.mdx index 0f1e60e8e..4ec665bb5 100644 --- a/docs/content/docs/main/changelog/prerelease/019.0.mdx +++ b/docs/content/docs/main/changelog/prerelease/019.0.mdx @@ -10,3 +10,7 @@ description: Improvements to Azure Web PubSub store ### Added - [#288](https://github.com/InditexTech/weavejs/issues/288) Provide a tool for erasing elements + +### Fixed + +- [#290](https://github.com/InditexTech/weavejs/issues/290) Maintain text node size when changed