diff --git a/packages/components/src/autocomplete/index.tsx b/packages/components/src/autocomplete/index.tsx index c8c354bd2f8ca..c9831ae33e179 100644 --- a/packages/components/src/autocomplete/index.tsx +++ b/packages/components/src/autocomplete/index.tsx @@ -218,101 +218,92 @@ export function useAutocomplete( { return; } - const lastTriggerPrefix = completers.reduce< string | null >( - ( lastTrigger, { triggerPrefix } ) => { - const triggerIndex = textContent.lastIndexOf( triggerPrefix ); + // Find the completer with the highest triggerPrefix index in the + // textContent. + const completer = completers.reduce< WPCompleter | null >( + ( lastTrigger, currentCompleter ) => { + const triggerIndex = textContent.lastIndexOf( + currentCompleter.triggerPrefix + ); const lastTriggerIndex = lastTrigger !== null - ? textContent.lastIndexOf( lastTrigger ) + ? textContent.lastIndexOf( lastTrigger.triggerPrefix ) : -1; return triggerIndex > lastTriggerIndex - ? triggerPrefix + ? currentCompleter : lastTrigger; }, null ); - const completer = completers.find( - ( { triggerPrefix, allowContext } ) => { - if ( triggerPrefix !== lastTriggerPrefix ) { - return false; - } + if ( ! completer ) { + if ( autocompleter ) reset(); + return; + } - const index = textContent.lastIndexOf( triggerPrefix ); + const { allowContext, triggerPrefix } = completer; + const triggerIndex = textContent.lastIndexOf( triggerPrefix ); + const textWithoutTrigger = textContent.slice( + triggerIndex + triggerPrefix.length + ); - if ( index === -1 ) { - return false; - } + const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit. + // This is a final barrier to prevent the effect from completing with + // an extremely long string, which causes the editor to slow-down + // significantly. This could happen, for example, if `matchingWhileBackspacing` + // is true and one of the "words" end up being too long. If that's the case, + // it will be caught by this guard. + if ( tooDistantFromTrigger ) return; + + const mismatch = filteredOptions.length === 0; + const wordsFromTrigger = textWithoutTrigger.split( /\s/ ); + // We need to allow the effect to run when not backspacing and if there + // was a mismatch. i.e when typing a trigger + the match string or when + // clicking in an existing trigger word on the page. We do that if we + // detect that we have one word from trigger in the current textual context. + // + // Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and + // allow the effect to run. It will run until there's a mismatch. + const hasOneTriggerWord = wordsFromTrigger.length === 1; + // This is used to allow the effect to run when backspacing and if + // "touching" a word that "belongs" to a trigger. We consider a "trigger + // word" any word up to the limit of 3 from the trigger character. + // Anything beyond that is ignored if there's a mismatch. This allows + // us to "escape" a mismatch when backspacing, but still imposing some + // sane limits. + // + // Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but + // if the user presses backspace here, it will show the completion popup again. + const matchingWhileBackspacing = + backspacing.current && wordsFromTrigger.length <= 3; + + if ( mismatch && ! ( matchingWhileBackspacing || hasOneTriggerWord ) ) { + return; + } - const textWithoutTrigger = textContent.slice( - index + triggerPrefix.length - ); + const textAfterSelection = getTextContent( + slice( record, undefined, getTextContent( record ).length ) + ); - const tooDistantFromTrigger = textWithoutTrigger.length > 50; // 50 chars seems to be a good limit. - // This is a final barrier to prevent the effect from completing with - // an extremely long string, which causes the editor to slow-down - // significantly. This could happen, for example, if `matchingWhileBackspacing` - // is true and one of the "words" end up being too long. If that's the case, - // it will be caught by this guard. - if ( tooDistantFromTrigger ) return false; - - const mismatch = filteredOptions.length === 0; - const wordsFromTrigger = textWithoutTrigger.split( /\s/ ); - // We need to allow the effect to run when not backspacing and if there - // was a mismatch. i.e when typing a trigger + the match string or when - // clicking in an existing trigger word on the page. We do that if we - // detect that we have one word from trigger in the current textual context. - // - // Ex.: "Some text @a" <-- "@a" will be detected as the trigger word and - // allow the effect to run. It will run until there's a mismatch. - const hasOneTriggerWord = wordsFromTrigger.length === 1; - // This is used to allow the effect to run when backspacing and if - // "touching" a word that "belongs" to a trigger. We consider a "trigger - // word" any word up to the limit of 3 from the trigger character. - // Anything beyond that is ignored if there's a mismatch. This allows - // us to "escape" a mismatch when backspacing, but still imposing some - // sane limits. - // - // Ex: "Some text @marcelo sekkkk" <--- "kkkk" caused a mismatch, but - // if the user presses backspace here, it will show the completion popup again. - const matchingWhileBackspacing = - backspacing.current && wordsFromTrigger.length <= 3; - - if ( - mismatch && - ! ( matchingWhileBackspacing || hasOneTriggerWord ) - ) { - return false; - } - - const textAfterSelection = getTextContent( - slice( record, undefined, getTextContent( record ).length ) - ); + if ( + allowContext && + ! allowContext( + textContent.slice( 0, triggerIndex ), + textAfterSelection + ) + ) { + return; + } - if ( - allowContext && - ! allowContext( - textContent.slice( 0, index ), - textAfterSelection - ) - ) { - return false; - } - - if ( - /^\s/.test( textWithoutTrigger ) || - /\s\s+$/.test( textWithoutTrigger ) - ) { - return false; - } - - return /[\u0000-\uFFFF]*$/.test( textWithoutTrigger ); - } - ); + if ( + /^\s/.test( textWithoutTrigger ) || + /\s\s+$/.test( textWithoutTrigger ) + ) { + return; + } - if ( ! completer ) { - if ( autocompleter ) reset(); + if ( ! /[\u0000-\uFFFF]*$/.test( textWithoutTrigger ) ) { return; }