From f63563aa93d5c51c78e6fff87108f8f454cdd316 Mon Sep 17 00:00:00 2001 From: Jonathan Adeline Date: Tue, 5 May 2026 14:05:57 +0400 Subject: [PATCH] feat: `FormulaInputField` expert mode enhancements (#4866) --- .../scss/components/formula_input_field.scss | 85 +- .../function_formula_component.scss | 6 +- .../operator_formula_component.scss | 5 +- .../formula/FormulaInputErrorContext.vue | 41 +- .../components/formula/FormulaInputField.vue | 255 ++++-- .../extensions/ArrowKeyNavigationExtension.js | 5 +- .../extensions/ContextManagementExtension.js | 195 ++--- .../extensions/FormulaClipboardHandler.js | 130 ++- .../formula/extensions/FormulaNodes.js | 57 +- .../extensions/FunctionDetectionExtension.js | 331 -------- .../extensions/FunctionDowngradeExtension.js | 158 ++++ .../FunctionHelpTooltipExtension.js | 58 +- .../extensions/GroupDetectionExtension.js | 147 ---- .../extensions/InputDetectionExtension.js | 492 +++++++++++ .../extensions/NodeSelectionExtension.js | 23 +- .../extensions/OperatorDetectionExtension.js | 209 ----- .../ParenMatchHighlightExtension.js | 97 +++ .../extensions/SmartDeletionExtension.js | 142 +++- .../extensions/ZWSManagementExtension.js | 60 +- .../components/formula/extensions/helpers.js | 200 +++++ .../modules/core/formula/parser/parser.js | 6 +- .../core/formula/tiptap/fromTipTapVisitor.js | 9 +- .../core/formula/tiptap/toTipTapVisitor.js | 184 ++-- .../core/formula/FormulaInputField.spec.js | 135 +++ .../FunctionDowngradeExtension.spec.js | 351 ++++++++ .../core/formula/ParenMatchHighlight.spec.js | 240 ++++++ .../test/unit/core/formula/helpers.spec.js | 784 ++++++++++++++++++ .../unit/core/formula/toTipTapVisitor.spec.js | 110 +++ 28 files changed, 3372 insertions(+), 1143 deletions(-) delete mode 100644 web-frontend/modules/core/components/formula/extensions/FunctionDetectionExtension.js create mode 100644 web-frontend/modules/core/components/formula/extensions/FunctionDowngradeExtension.js delete mode 100644 web-frontend/modules/core/components/formula/extensions/GroupDetectionExtension.js create mode 100644 web-frontend/modules/core/components/formula/extensions/InputDetectionExtension.js delete mode 100644 web-frontend/modules/core/components/formula/extensions/OperatorDetectionExtension.js create mode 100644 web-frontend/modules/core/components/formula/extensions/ParenMatchHighlightExtension.js create mode 100644 web-frontend/modules/core/components/formula/extensions/helpers.js create mode 100644 web-frontend/test/unit/core/formula/FormulaInputField.spec.js create mode 100644 web-frontend/test/unit/core/formula/FunctionDowngradeExtension.spec.js create mode 100644 web-frontend/test/unit/core/formula/ParenMatchHighlight.spec.js create mode 100644 web-frontend/test/unit/core/formula/helpers.spec.js diff --git a/web-frontend/modules/core/assets/scss/components/formula_input_field.scss b/web-frontend/modules/core/assets/scss/components/formula_input_field.scss index 0810e75d95..c74fbc1c28 100644 --- a/web-frontend/modules/core/assets/scss/components/formula_input_field.scss +++ b/web-frontend/modules/core/assets/scss/components/formula_input_field.scss @@ -51,7 +51,7 @@ // Atomic comma style .formula-input-field__comma, .formula-input-field__parenthesis { - color: $palette-cyan-800; + color: $palette-cyan-900; font-weight: 500; background-color: $palette-cyan-50; padding: 0 8px; @@ -65,7 +65,7 @@ // Group parenthesis style .formula-input-field__group-parenthesis { - color: $palette-purple-800; + color: $palette-purple-900; font-weight: 500; background-color: $palette-purple-50; padding: 0 8px; @@ -82,56 +82,40 @@ padding-left: 0; } -// Add margin-right when a function component is followed by another function component -.function-formula-component:has(+ .function-formula-component) { - margin-right: 4px; -} - -// Add margin-right when a function component is followed by a group parenthesis -.function-formula-component:has(+ .formula-input-field__group-parenthesis) { - margin-right: 4px; -} - -// Add margin-right when a parenthesis is followed by another parenthesis -.formula-input-field__parenthesis:has(+ .formula-input-field__parenthesis) { - margin-right: 4px; -} - -// Add margin-right when a parenthesis is followed by a comma -.formula-input-field__parenthesis:has(+ .formula-input-field__comma) { - margin-right: 4px; -} - -// Add margin-right when a comma is followed by a function component -.formula-input-field__comma:has(+ .function-formula-component) { - margin-right: 4px; +// Spacing between adjacent formula atoms (closing parens, commas, group parens) +.formula-input-field__parenthesis, +.formula-input-field__comma, +.formula-input-field__group-parenthesis { + &:has( + + :is( + .function-formula-component, + .formula-input-field__parenthesis, + .formula-input-field__comma, + .formula-input-field__group-parenthesis + ) + ) { + margin-right: 4px; + } } -// Add margin-right when a comma is followed by a parenthesis -.formula-input-field__comma:has(+ .formula-input-field__parenthesis) { +// Spacing after function components (but not before their own closing paren) +.function-formula-component:has( + + :is(.function-formula-component, .formula-input-field__group-parenthesis) +) { margin-right: 4px; } -// Add margin-left when a comma is preceded by a function component +// Spacing before commas preceded by function components .function-formula-component + .formula-input-field__comma { margin-left: 4px; } -// Add margin-right when a comma is followed by another comma -.formula-input-field__comma:has(+ .formula-input-field__comma) { - margin-right: 4px; -} - -// Add margin-right when a group parenthesis is followed by another group parenthesis -.formula-input-field__group-parenthesis:has( - + .formula-input-field__group-parenthesis -) { - margin-right: 4px; -} - -// Add margin-right when a group parenthesis is followed by a function/operator/comma -.formula-input-field__group-parenthesis:has(+ .function-formula-component), -.formula-input-field__group-parenthesis:has(+ .formula-input-field__comma) { +// Spacing between closing parens and operators, and between operators and functions +:is( + .formula-input-field__parenthesis[data-formula-closing-paren='true'], + .formula-input-field__group-parenthesis[data-group-closing-paren='true'] +):has(+ .operator-formula-component), +.operator-formula-component:has(+ .function-formula-component) { margin-right: 4px; } @@ -145,3 +129,18 @@ .formula-input-field__view-full-error { margin-left: 5px; } + +// Paren match highlight on hover (disabled for no-arg functions) +.formula-input-field__paren-highlight:not([data-no-args='true']) { + &.formula-input-field__parenthesis { + box-shadow: inset 0 0 0 1.5px $palette-cyan-800; + } + + &.formula-input-field__group-parenthesis { + box-shadow: inset 0 0 0 1.5px $palette-purple-800; + } + + &.function-formula-component .function-formula-component__name { + box-shadow: inset 0 0 0 1.5px $palette-cyan-800; + } +} diff --git a/web-frontend/modules/core/assets/scss/components/function_formula_component.scss b/web-frontend/modules/core/assets/scss/components/function_formula_component.scss index c739891d6c..479423f1a5 100644 --- a/web-frontend/modules/core/assets/scss/components/function_formula_component.scss +++ b/web-frontend/modules/core/assets/scss/components/function_formula_component.scss @@ -2,10 +2,12 @@ height: 24px; display: inline-block; vertical-align: top; + user-select: none; + caret-color: transparent; } .function-formula-component__name { - color: $palette-cyan-800; + color: $palette-cyan-900; font-weight: 500; background-color: $palette-cyan-50; padding: 0 8px; @@ -21,7 +23,7 @@ } .function-formula-component__parenthesis { - color: $palette-cyan-800; + color: $palette-cyan-900; font-weight: 500; background-color: $palette-cyan-50; padding: 0 8px; diff --git a/web-frontend/modules/core/assets/scss/components/operator_formula_component.scss b/web-frontend/modules/core/assets/scss/components/operator_formula_component.scss index 230be7de7d..f73c0f8ccb 100644 --- a/web-frontend/modules/core/assets/scss/components/operator_formula_component.scss +++ b/web-frontend/modules/core/assets/scss/components/operator_formula_component.scss @@ -2,12 +2,13 @@ display: inline-block; vertical-align: top; white-space: normal; - user-select: none; cursor: default; + user-select: none; + caret-color: transparent; } .operator-formula-component__symbol { - color: $palette-green-800; + color: $palette-green-900; font-weight: 500; background-color: $palette-green-50; padding: 0 8px; diff --git a/web-frontend/modules/core/components/formula/FormulaInputErrorContext.vue b/web-frontend/modules/core/components/formula/FormulaInputErrorContext.vue index 9b2a15daf0..39962d6595 100644 --- a/web-frontend/modules/core/components/formula/FormulaInputErrorContext.vue +++ b/web-frontend/modules/core/components/formula/FormulaInputErrorContext.vue @@ -24,23 +24,43 @@ export default { type: Object, required: true, }, + visible: { + type: Boolean, + required: true, + }, + target: { + type: Object, + default: null, + validator: (value) => value == null || value instanceof HTMLElement, + }, + }, + watch: { + visible: { + handler(isVisible) { + if (isVisible) { + this.show(this.target) + } else { + this.hide() + } + }, + }, }, methods: { show( - targetElement, - verticalPosition = 'bottom', + targetElement = null, + verticalPosition = 'top', horizontalPosition = 'left', - verticalOffset = 0, - horizontalOffset = 0, - width = null + verticalOffset = 10, + horizontalOffset = 0 ) { - // Ensure that the context's width is dynamically set - // to the targetElement's width, as it can be variable. - if (width !== null) { - this.$refs.context.$el.style.width = `${width}px` + const el = targetElement ?? this.target + if (!el || !this.$refs.context) { + return } + const { width } = el.getBoundingClientRect() + this.$refs.context.$el.style.width = `${width}px` return this.$refs.context.show( - targetElement, + el, verticalPosition, horizontalPosition, verticalOffset, @@ -49,7 +69,6 @@ export default { }, hide() { this.$refs.context.hide() - this.hideTooltip() }, }, } diff --git a/web-frontend/modules/core/components/formula/FormulaInputField.vue b/web-frontend/modules/core/components/formula/FormulaInputField.vue index 67935a3e0e..39ad54bc6e 100644 --- a/web-frontend/modules/core/components/formula/FormulaInputField.vue +++ b/web-frontend/modules/core/components/formula/FormulaInputField.vue @@ -1,6 +1,10 @@