From 9a031bdf86e78e0ab5eb1b91c2daf37678d903b0 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 18 Apr 2023 10:30:33 +0200 Subject: [PATCH 1/3] fix(rich-text-web): apply changes on losing focus --- .../rich-text-web/CHANGELOG.md | 4 ++ .../rich-text-web/package.json | 2 +- .../rich-text-web/src/components/Editor.tsx | 62 +++++++++++++++++-- .../rich-text-web/src/package.xml | 2 +- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md index 42b975e735..118e675270 100644 --- a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md +++ b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Fixed + +- We fixed an issue when Rich Text widget not saving data when user leaves the page quickly after editing. + ## [2.1.5] - 2023-03-24 ### Fixed diff --git a/packages/pluggableWidgets/rich-text-web/package.json b/packages/pluggableWidgets/rich-text-web/package.json index e20727c211..9222980441 100644 --- a/packages/pluggableWidgets/rich-text-web/package.json +++ b/packages/pluggableWidgets/rich-text-web/package.json @@ -1,7 +1,7 @@ { "name": "rich-text-web", "widgetName": "RichText", - "version": "2.1.5", + "version": "2.1.6", "description": "Rich inline or toolbar text editing", "copyright": "© Mendix Technology BV 2023. All rights reserved.", "repository": { diff --git a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx index c8feec4c3c..0b6692d3e1 100644 --- a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx @@ -1,6 +1,6 @@ import { debounce } from "@mendix/pluggable-widgets-commons"; import { CKEditorEventPayload, CKEditorHookProps, CKEditorInstance } from "ckeditor4-react"; -import { Component, createElement } from "react"; +import { Component, createElement, createRef, RefObject } from "react"; import { RichTextContainerProps } from "../../typings/RichTextProps"; import { getCKEditorConfig } from "../utils/ckeditorConfigs"; import { MainEditor } from "./MainEditor"; @@ -34,6 +34,10 @@ export class Editor extends Component { editorScript = "widgets/ckeditor/ckeditor.js"; element: HTMLElement; lastSentValue: string | undefined; + applyChangesDebounce: () => void; + divRef: RefObject; + cancelRAF: (() => void) | undefined; + hasFocus: boolean; constructor(props: EditorProps) { super(props); @@ -42,10 +46,13 @@ export class Editor extends Component { this.element = this.props.element; this.editorKey = this.getNewKey(); this.editorHookProps = this.getNewEditorHookProps(); - this.onChange = debounce(this.onChange.bind(this), 500); + this.onChange = this.onChange.bind(this); + this.applyChangesDebounce = debounce(this.applyChangesImmediately.bind(this), 500); this.onKeyPress = this.onKeyPress.bind(this); this.onPasteContent = this.onPasteContent.bind(this); this.onDropContent = this.onDropContent.bind(this); + this.divRef = createRef(); + this.hasFocus = false; } setNewRenderProps(): void { @@ -171,19 +178,24 @@ export class Editor extends Component { } } - // onChange is wrapped in debounce, so, we always need to check - // weather we sill have editor. onChange(_event: CKEditorEventPayload<"change">): void { if (this.editor) { const editorData = this.editor.getData(); const content = this.widgetProps.sanitizeContent ? DOMPurify.sanitize(editorData) : editorData; this.lastSentValue = content; - this.widgetProps.stringAttribute.setValue(content); + this.applyChangesDebounce(); } this.widgetProps.onChange?.execute(); } + applyChangesImmediately() { + // put last seen content to the attribute if it exists + if (this.lastSentValue !== undefined) { + this.widgetProps.stringAttribute.setValue(this.lastSentValue); + } + } + addListeners(): void { if (this.editor && !this.editor.readOnly) { this.editor.on("change", this.onChange); @@ -253,6 +265,25 @@ export class Editor extends Component { this.lastSentValue = undefined; } + componentDidMount() { + this.cancelRAF = animationLoop(() => { + if (this.divRef.current && this.divRef.current.parentElement) { + const newHasFocus = this.divRef.current.parentElement.contains(document.activeElement); + if (newHasFocus !== this.hasFocus) { + this.hasFocus = newHasFocus; + if (!this.hasFocus) { + // changed from true to false, user left the element, apply changes immediately + this.applyChangesImmediately(); + } + } + } + }); + } + + componentWillUnmount() { + this.cancelRAF?.(); + } + componentDidUpdate(): void { const prevAttr = this.widgetProps.stringAttribute; const nextAttr = this.props.widgetProps.stringAttribute; @@ -266,6 +297,25 @@ export class Editor extends Component { render(): JSX.Element | null { const [key, config] = this.getRenderProps(); - return ; + return ( +
+ +
+ ); } } + +function animationLoop(callback: () => void): () => void { + let requestId: number; + + const requestFrame = () => { + requestId = window.requestAnimationFrame(() => { + callback(); + requestFrame(); + }); + }; + + requestFrame(); + + return () => window.cancelAnimationFrame(requestId); +} diff --git a/packages/pluggableWidgets/rich-text-web/src/package.xml b/packages/pluggableWidgets/rich-text-web/src/package.xml index fdd4923faa..746bc55f44 100644 --- a/packages/pluggableWidgets/rich-text-web/src/package.xml +++ b/packages/pluggableWidgets/rich-text-web/src/package.xml @@ -1,6 +1,6 @@ - + From 58d67935c2212af5ec489343b268ebe31c4a2c47 Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 18 Apr 2023 10:49:34 +0200 Subject: [PATCH 2/3] chore(rich-text-web): apply ckeditor to 4.21.0 --- .../pluggableWidgets/rich-text-web/CHANGELOG.md | 4 ++++ .../pluggableWidgets/rich-text-web/package.json | 2 +- pnpm-lock.yaml | 14 +++++++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md index 118e675270..0cf7bb5e2c 100644 --- a/packages/pluggableWidgets/rich-text-web/CHANGELOG.md +++ b/packages/pluggableWidgets/rich-text-web/CHANGELOG.md @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - We fixed an issue when Rich Text widget not saving data when user leaves the page quickly after editing. +### Security + +- Update ckeditor4 to version 4.21.0 + ## [2.1.5] - 2023-03-24 ### Fixed diff --git a/packages/pluggableWidgets/rich-text-web/package.json b/packages/pluggableWidgets/rich-text-web/package.json index 9222980441..95bfd9f570 100644 --- a/packages/pluggableWidgets/rich-text-web/package.json +++ b/packages/pluggableWidgets/rich-text-web/package.json @@ -81,7 +81,7 @@ }, "dependencies": { "@types/dompurify": "^2.4.0", - "ckeditor4": "^4.20.2", + "ckeditor4": "^4.21.0", "ckeditor4-react": "^2.1.1", "classnames": "^2.3.2", "dompurify": "^2.4.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f50ebe7849..96a0d10b2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2816,7 +2816,7 @@ importers: '@types/testing-library__jest-dom': ^5.14.1 '@types/testing-library__react-hooks': ^3.4.0 '@web-widgets/run-e2e': workspace:* - ckeditor4: ^4.20.2 + ckeditor4: ^4.21.0 ckeditor4-react: ^2.1.1 classnames: ^2.3.2 cross-env: ^7.0.3 @@ -2837,8 +2837,8 @@ importers: typescript: 4.5.4 dependencies: '@types/dompurify': 2.4.0 - ckeditor4: 4.20.2 - ckeditor4-react: 2.1.1_mupovz5e3ki4lyojjfubzas2i4 + ckeditor4: 4.21.0 + ckeditor4-react: 2.1.1_x6rnuakvswxc7klxvbsb2sczw4 classnames: 2.3.2 dompurify: 2.4.5 devDependencies: @@ -8401,20 +8401,20 @@ packages: resolution: {integrity: sha512-OAoQT/gYrHkg0qgzf6MS/rndYhq3SScLVQ3rtXQeuCE8ju7nFHg3qZ7WGA2XpFxcZzsMP6hhugXqdel5vbcC3g==} dev: false - /ckeditor4-react/2.1.1_mupovz5e3ki4lyojjfubzas2i4: + /ckeditor4-react/2.1.1_x6rnuakvswxc7klxvbsb2sczw4: resolution: {integrity: sha512-q8WGsTaigP5OG34WFZUlkPpR2zJPCav5mgB6zdq58+tY0UJwnweVAammfZQJpQHF5UqTpdjq0dlsqxae8Tg4Rw==} peerDependencies: ckeditor4: ^4.17.2 react: ^16.9 || ^17 dependencies: - ckeditor4: 4.20.2 + ckeditor4: 4.21.0 ckeditor4-integrations-common: 1.0.0 prop-types: 15.8.1 react: 17.0.2 dev: false - /ckeditor4/4.20.2: - resolution: {integrity: sha512-KRRinpHF+iBmZXdtrNEQx4fqhxN/wnCAvr7jf12YW6KyDIOY6mCQsRWZ40onlNY5eg8DxfPszWIZ4eJlB9khYw==} + /ckeditor4/4.21.0: + resolution: {integrity: sha512-OAMw68puJcrKFtsPZwIWVB/upYLgJpFw1yTuBBIhoreY+g/f0SttjQY0I/fUwxevVUHvgmRVNeJwNl8qkJPvyw==} dev: false /clamp/1.0.1: From 1366c55cfc77488f17831c011fa0f74a93b5b76c Mon Sep 17 00:00:00 2001 From: Roman Vyakhirev Date: Tue, 18 Apr 2023 11:40:52 +0200 Subject: [PATCH 3/3] fix(rich-text-web): review comments --- .../rich-text-web/src/components/Editor.tsx | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx index 0b6692d3e1..c9232561ba 100644 --- a/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx +++ b/packages/pluggableWidgets/rich-text-web/src/components/Editor.tsx @@ -1,6 +1,6 @@ import { debounce } from "@mendix/pluggable-widgets-commons"; import { CKEditorEventPayload, CKEditorHookProps, CKEditorInstance } from "ckeditor4-react"; -import { Component, createElement, createRef, RefObject } from "react"; +import { Component, createElement } from "react"; import { RichTextContainerProps } from "../../typings/RichTextProps"; import { getCKEditorConfig } from "../utils/ckeditorConfigs"; import { MainEditor } from "./MainEditor"; @@ -35,7 +35,6 @@ export class Editor extends Component { element: HTMLElement; lastSentValue: string | undefined; applyChangesDebounce: () => void; - divRef: RefObject; cancelRAF: (() => void) | undefined; hasFocus: boolean; @@ -51,7 +50,6 @@ export class Editor extends Component { this.onKeyPress = this.onKeyPress.bind(this); this.onPasteContent = this.onPasteContent.bind(this); this.onDropContent = this.onDropContent.bind(this); - this.divRef = createRef(); this.hasFocus = false; } @@ -185,14 +183,13 @@ export class Editor extends Component { this.lastSentValue = content; this.applyChangesDebounce(); } - - this.widgetProps.onChange?.execute(); } applyChangesImmediately() { // put last seen content to the attribute if it exists if (this.lastSentValue !== undefined) { this.widgetProps.stringAttribute.setValue(this.lastSentValue); + this.widgetProps.onChange?.execute(); } } @@ -267,8 +264,8 @@ export class Editor extends Component { componentDidMount() { this.cancelRAF = animationLoop(() => { - if (this.divRef.current && this.divRef.current.parentElement) { - const newHasFocus = this.divRef.current.parentElement.contains(document.activeElement); + if (this.element && this.element.parentElement) { + const newHasFocus = this.element.parentElement.contains(document.activeElement); if (newHasFocus !== this.hasFocus) { this.hasFocus = newHasFocus; if (!this.hasFocus) { @@ -297,11 +294,7 @@ export class Editor extends Component { render(): JSX.Element | null { const [key, config] = this.getRenderProps(); - return ( -
- -
- ); + return ; } }