@@ -3,7 +3,6 @@ import { TextCheckerCard, TextCheckerPopupElement, TextCheckerPopupElementArgs }
33import type { TextlintMessage , TextlintResult } from "@textlint/types" ;
44import type { TextCheckerElementRectItem } from "./text-checker-store" ;
55import pDebounce from "p-debounce" ;
6- import delay from "delay" ;
76import { debug } from "./util/logger" ;
87
98const createCompositionHandler = ( ) => {
@@ -48,15 +47,58 @@ export type AttachTextAreaParams = {
4847 lintEngine : LintEngineAPI ;
4948} ;
5049
51- let textCheckerPopup : TextCheckerPopupElement ;
5250const createTextCheckerPopupElement = ( args : TextCheckerPopupElementArgs ) => {
53- if ( textCheckerPopup ) {
54- return textCheckerPopup ;
55- }
56- textCheckerPopup = new TextCheckerPopupElement ( args ) ;
51+ const textCheckerPopup = new TextCheckerPopupElement ( args ) ;
5752 document . body . append ( textCheckerPopup ) ;
5853 return textCheckerPopup ;
5954} ;
55+
56+ /**
57+ * Return true if the element in viewport
58+ * @param element
59+ */
60+ function isVisibleInViewport ( element : HTMLElement ) : boolean {
61+ const style = window . getComputedStyle ( element ) ;
62+ if ( style . display === "none" || style . visibility === "hidden" ) {
63+ return false ;
64+ }
65+ const rect = element . getBoundingClientRect ( ) ;
66+ if ( rect . height === 0 || rect . width === 0 ) {
67+ return false ;
68+ }
69+ return (
70+ rect . top >= 0 &&
71+ rect . left >= 0 &&
72+ rect . bottom <= ( window . innerHeight || document . documentElement . clientHeight ) &&
73+ rect . right <= ( window . innerWidth || document . documentElement . clientWidth )
74+ ) ;
75+ }
76+
77+ /**
78+ * Dismiss all popup
79+ *
80+ * - Update text(includes fixed)
81+ * - Scroll textarea/Scroll window
82+ * - Focus on textarea
83+ * - Click out of textarea/popup
84+ * - popup → textarea → other → dismiss
85+ * - textarea → popup → other → dismiss
86+ */
87+
88+ /**
89+ * Dismiss a single popup(500ms delay)
90+ *
91+ * - Leave from popup
92+ * - Leave from RectItem
93+ * - Focus on textarea
94+ *
95+ */
96+
97+ /**
98+ * Show popup condition
99+ * - onUpdate
100+ */
101+
60102/**
61103 * Attach text-checker component to `<textarea>` element
62104 */
@@ -73,32 +115,45 @@ export const attachToTextArea = ({
73115 debug ( "Can not attach textarea that is readonly" , textAreaElement ) ;
74116 return ( ) => { } ;
75117 }
76- const textChecker = new TextCheckerElement ( {
77- targetElement : textAreaElement ,
78- hoverPadding : 20
79- } ) ;
80- textAreaElement . before ( textChecker ) ;
81- const hoverMap = new Map < TextCheckerElementRectItem , boolean > ( ) ;
118+ if ( textAreaElement . dataset . attachedTextCheckerElement === "true" ) {
119+ debug ( "Can not attach textarea that is already attached" , textAreaElement ) ;
120+ return ( ) => { } ;
121+ }
82122 const dismissCards = ( ) => {
83- if ( ! textCheckerPopup . isHovering && hoverMap . size === 0 ) {
123+ debug ( "dismissCards" , {
124+ textCheckerPopup : textCheckerPopup . isHovering ,
125+ textChecker : textChecker . isHovering ,
126+ textCheckerF : textChecker . isFocus
127+ } ) ;
128+ if ( ! textCheckerPopup . isHovering && ! textChecker . isHovering && ! textChecker . isFocus ) {
84129 textCheckerPopup . dismissCards ( ) ;
130+ textChecker . resetHoverState ( ) ;
85131 }
86132 } ;
87133 const textCheckerPopup = createTextCheckerPopupElement ( {
88134 onLeave ( ) {
89- if ( ! textCheckerPopup . isHovering && hoverMap . size === 0 ) {
90- textCheckerPopup . dismissCards ( ) ;
91- }
135+ dismissCards ( ) ;
92136 }
93137 } ) ;
138+ const textChecker = new TextCheckerElement ( {
139+ targetElement : textAreaElement ,
140+ hoverPadding : 20 ,
141+ onLeave ( ) {
142+ dismissCards ( ) ;
143+ }
144+ } ) ;
145+ textAreaElement . before ( textChecker ) ;
94146 const compositionHandler = createCompositionHandler ( ) ;
95147 const update = pDebounce ( async ( ) => {
148+ if ( ! isVisibleInViewport ( textAreaElement ) ) {
149+ return ;
150+ }
96151 // stop lint on IME composition
97152 if ( compositionHandler . onComposition ) {
98153 return ;
99154 }
100155 // dismiss card before update annotations
101- textCheckerPopup . dismissCards ( ) ;
156+ // dismissCards();
102157 const text = textAreaElement . value ;
103158 const results = await lintEngine . lintText ( {
104159 text
@@ -120,19 +175,16 @@ export const attachToTextArea = ({
120175 messageRuleId : message . ruleId ,
121176 fixable : Boolean ( message . fix )
122177 } ;
123- const abortSignalMap = new WeakMap < TextCheckerElementRectItem , AbortController > ( ) ;
178+ let dismissTimerId : null | any = null ;
124179 return {
125180 id : `${ message . ruleId } ::${ message . line } :${ message . column } ` ,
126181 start : message . index ,
127182 end : message . index + 1 ,
128183 onMouseEnter : ( { rectItem } : { rectItem : TextCheckerElementRectItem } ) => {
129- hoverMap . set ( rectItem , true ) ;
130- const controller = abortSignalMap . get ( rectItem ) ;
131- debug ( "enter" , controller ) ;
132- if ( controller ) {
133- controller . abort ( ) ;
184+ debug ( "annotation - onMouseEnter" ) ;
185+ if ( dismissTimerId ) {
186+ clearTimeout ( dismissTimerId ) ;
134187 }
135- abortSignalMap . set ( rectItem , new AbortController ( ) ) ;
136188 textCheckerPopup . updateCard ( {
137189 card : card ,
138190 rect : {
@@ -193,18 +245,20 @@ export const attachToTextArea = ({
193245 } ,
194246 async onMouseLeave ( { rectItem } : { rectItem : TextCheckerElementRectItem } ) {
195247 try {
196- hoverMap . delete ( rectItem ) ;
197- const controller = abortSignalMap . get ( rectItem ) ;
198- debug ( "leave" , controller ) ;
199- await delay ( 500 , {
200- signal : controller ?. signal
201- } ) ;
202- if ( textCheckerPopup . isHovering || hoverMap . get ( rectItem ) ) {
203- return ;
204- }
205- textCheckerPopup . dismissCard ( card ) ;
248+ debug ( "annotation - onMouseLeave" ) ;
249+ dismissTimerId = setTimeout ( ( ) => {
250+ const isHover = textChecker . isHoverRectItem ( rectItem ) ;
251+ debug ( "dismiss" , {
252+ textCheckerPopup : textCheckerPopup . isHovering ,
253+ isRectElementHover : isHover
254+ } ) ;
255+ if ( textCheckerPopup . isHovering || isHover ) {
256+ return ;
257+ }
258+ textCheckerPopup . dismissCard ( card ) ;
259+ } , 500 ) ;
206260 } catch ( error ) {
207- debug ( "Abort Canceled " , error ) ;
261+ debug ( "Abort dismiss popup " , error ) ;
208262 }
209263 }
210264 } ;
@@ -213,34 +267,50 @@ export const attachToTextArea = ({
213267 debug ( "annotations" , annotations ) ;
214268 textChecker . updateAnnotations ( annotations ) ;
215269 } , lintingDebounceMs ) ;
216- textAreaElement . addEventListener ( "compositionstart" , compositionHandler ) ;
217- textAreaElement . addEventListener ( "compositionend" , compositionHandler ) ;
218- textAreaElement . addEventListener ( "input" , update ) ;
219- textAreaElement . addEventListener ( "focusout" , dismissCards ) ;
220- update ( ) ;
270+ // Events
221271 // when resize element, update annotation
222272 const resizeObserver = new ResizeObserver ( ( ) => {
223- debug ( "textarea resize " ) ;
273+ debug ( "ResizeObserver do update " ) ;
224274 textCheckerPopup . dismissCards ( ) ;
225275 textChecker . resetAnnotations ( ) ;
226276 update ( ) ;
227277 } ) ;
228278 resizeObserver . observe ( textAreaElement ) ;
229279 // when scroll window, update annotation
230280 const onScroll = ( ) => {
281+ textCheckerPopup . dismissCards ( ) ;
231282 textChecker . resetAnnotations ( ) ;
232283 update ( ) ;
233284 } ;
285+ const onFocus = ( ) => {
286+ textCheckerPopup . dismissCards ( ) ;
287+ update ( ) ;
288+ } ;
289+ const onBlur = ( event : FocusEvent ) => {
290+ // does not dismiss on click popup items(require tabindex)
291+ if ( event . relatedTarget === textChecker || event . relatedTarget === textCheckerPopup ) {
292+ return ;
293+ }
294+ textCheckerPopup . dismissCards ( ) ;
295+ } ;
296+ textAreaElement . addEventListener ( "compositionstart" , compositionHandler ) ;
297+ textAreaElement . addEventListener ( "compositionend" , compositionHandler ) ;
298+ textAreaElement . addEventListener ( "input" , update ) ;
299+ textAreaElement . addEventListener ( "focus" , onFocus ) ;
300+ textAreaElement . addEventListener ( "blur" , onBlur ) ;
301+ textAreaElement . addEventListener ( "focusout" , dismissCards ) ;
234302 window . addEventListener ( "scroll" , onScroll ) ;
235303 // when scroll the element, update annotation
236304 textAreaElement . addEventListener ( "scroll" , onScroll ) ;
305+ update ( ) ;
237306 return ( ) => {
238307 window . removeEventListener ( "scroll" , onScroll ) ;
239308 textAreaElement . removeEventListener ( "scroll" , onScroll ) ;
240309 textAreaElement . removeEventListener ( "compositionstart" , compositionHandler ) ;
241310 textAreaElement . removeEventListener ( "compositionend" , compositionHandler ) ;
242311 textAreaElement . removeEventListener ( "input" , update ) ;
243- textAreaElement . removeEventListener ( "blur" , dismissCards ) ;
312+ textAreaElement . removeEventListener ( "focus" , onFocus ) ;
313+ textAreaElement . removeEventListener ( "blur" , onBlur ) ;
244314 resizeObserver . disconnect ( ) ;
245315 } ;
246316} ;
0 commit comments