From c778a74ea2f9187ba58e884d1fe5d209ca99de9e Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 6 Oct 2021 09:54:23 +0100 Subject: [PATCH 01/63] Implement basic mechanics and UI --- .../src/components/link-control/index.js | 64 ++++++++++++++++--- .../components/link-control/link-preview.js | 1 + .../components/link-control/search-input.js | 1 + .../src/components/link-control/style.scss | 35 ++++++---- .../block-library/src/navigation-link/edit.js | 22 +++++-- 5 files changed, 99 insertions(+), 24 deletions(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 86bc0f8e00451..7f6c53adf1f51 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -2,11 +2,12 @@ * External dependencies */ import { noop } from 'lodash'; +import classnames from 'classnames'; /** * WordPress dependencies */ -import { Button, Spinner, Notice } from '@wordpress/components'; +import { Button, Spinner, Notice, TextControl } from '@wordpress/components'; import { keyboardReturn } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { useRef, useState, useEffect } from '@wordpress/element'; @@ -119,6 +120,7 @@ function LinkControl( { noURLSuggestion = false, createSuggestionButtonText, hasRichPreviews = false, + hasTextControl = false, } ) { if ( withCreateSuggestion === undefined && createSuggestion ) { withCreateSuggestion = true; @@ -126,8 +128,12 @@ function LinkControl( { const isMounting = useRef( true ); const wrapperNode = useRef(); + const [ internalInputValue, setInternalInputValue ] = useState( - ( value && value.url ) || '' + value?.url || '' + ); + const [ internalTextValue, setInternalTextValue ] = useState( + value?.text || '' ); const currentInputValue = propInputValue || internalInputValue; const [ isEditingLink, setIsEditingLink ] = useState( @@ -154,10 +160,14 @@ function LinkControl( { return; } + const linkURLInputIndex = 1; + + // When editing, the 2nd focusable element is the Link URL input. + const whichFocusTarget = isEditingLink ? linkURLInputIndex : 0; // When switching between editable and non editable LinkControl - // move focus to the first element to avoid focus loss. + // move focus to the most appropriate element to avoid focus loss. const nextFocusTarget = - focus.focusable.find( wrapperNode.current )[ 0 ] || + focus.focusable.find( wrapperNode.current )[ whichFocusTarget ] || wrapperNode.current; nextFocusTarget.focus(); @@ -165,6 +175,16 @@ function LinkControl( { isEndingEditWithFocus.current = false; }, [ isEditingLink ] ); + /** + * If the value's `text` property changes then sync this + * back up with state. + */ + useEffect( () => { + if ( value?.text && value.text !== internalTextValue ) { + setInternalTextValue( value.text ); + } + }, [ value ] ); + /** * Cancels editing state and marks that focus may need to be restored after * the next render, if focus was within the wrapper when editing finished. @@ -182,13 +202,22 @@ function LinkControl( { ); const handleSelectSuggestion = ( updatedValue ) => { - onChange( updatedValue ); + onChange( { + ...updatedValue, + text: internalTextValue, + } ); stopEditing(); }; const handleSubmitButton = () => { - if ( currentInputValue !== value?.url ) { - onChange( { url: currentInputValue } ); + if ( + currentInputValue !== value?.url || + internalTextValue !== value?.text + ) { + onChange( { + url: currentInputValue, + text: internalTextValue, + } ); } stopEditing(); }; @@ -198,6 +227,9 @@ function LinkControl( { const showSettingsDrawer = !! settings?.length; + // Only show once a URL value has been committed. + const showTextControl = hasTextControl; + return (
-
+
+ { showTextControl && ( + + ) } + { richData?.title || + value?.text || value?.title || displayURL } diff --git a/packages/block-editor/src/components/link-control/search-input.js b/packages/block-editor/src/components/link-control/search-input.js index 0f6d16e279414..98809843105b3 100644 --- a/packages/block-editor/src/components/link-control/search-input.js +++ b/packages/block-editor/src/components/link-control/search-input.js @@ -120,6 +120,7 @@ const LinkControlSearchInput = forwardRef( return (
.components-base-control__field { + display: flex; + align-items: center; + margin: 0; + } + + .components-base-control__label { + margin-right: $grid-unit-20; + margin-bottom: 0; + } + + input[type="text"], + // Specificity overide of URLInput defaults. + &.block-editor-url-input input[type="text"].block-editor-url-input__input { @include input-control; width: calc(100% - #{$grid-unit-20*2}); display: block; padding: 11px $grid-unit-20; padding-right: ( $button-size * $block-editor-link-control-number-of-actions ); // width of reset and submit buttons - margin: $grid-unit-20; + margin: 0; position: relative; border: 1px solid $gray-300; border-radius: $radius-block-ui; } - - .components-base-control__field { - margin-bottom: 0; - } } .block-editor-link-control__search-error { @@ -61,17 +71,20 @@ $preview-image-height: 140px; * when suggestions are rendered. * * Compensate for: - * - Input margin ($grid-unit-20) * - Border (1px) * - Vertically, for the difference in height between the input (40px) and * the icon buttons. * - Horizontally, pad to the minimum of: default input padding, or the * equivalent of the vertical padding. */ - top: $grid-unit-20 + 1px + ( ( 40px - $button-size ) * 0.5 ); + top: 1px + ( ( 40px - $button-size ) * 0.5 ); right: $grid-unit-20 + 1px + min($grid-unit-10, ( 40px - $button-size ) * 0.5); } +.block-editor-link-control__search-input-wrapper.has-text-control .block-editor-link-control__search-actions { + top: 59px; // TODO: figure this out in variables! +} + .components-button .block-editor-link-control__search-submit .has-icon { margin: -1px; } diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index af9d9f02b2ff5..6e465f4a8fef6 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -224,21 +224,32 @@ export const updateNavigationLinkBlockAttributes = ( const { title = '', url = '', + text = '', opensInNewTab, id, kind: newKind = originalKind, type: newType = originalType, } = updatedValue; - const normalizedTitle = title.replace( /http(s?):\/\//gi, '' ); const normalizedURL = url.replace( /http(s?):\/\//gi, '' ); const escapeTitle = title !== '' && normalizedTitle !== normalizedURL && originalLabel !== title; - const label = escapeTitle - ? escape( title ) - : originalLabel || escape( normalizedURL ); + + let label; + + if ( text ) { + label = text; + } else if ( text === '' ) { + // If the user deliberately cleared out the link text + // then reset to default to the URL. + label = escape( normalizedURL ); + } else { + label = escapeTitle + ? escape( title ) + : originalLabel || escape( normalizedURL ); + } // In https://github.com/WordPress/gutenberg/pull/24670 we decided to use "tag" in favor of "post_tag" const type = newType === 'post_tag' ? 'tag' : newType.replace( '-', '_' ); @@ -281,9 +292,11 @@ export default function NavigationLinkEdit( { title, kind, } = attributes; + const link = { url, opensInNewTab, + text: label, }; const { saveEntityRecord } = useDispatch( coreStore ); const { @@ -646,6 +659,7 @@ export default function NavigationLinkEdit( { anchorRef={ listItemRef.current } > Date: Wed, 6 Oct 2021 09:54:59 +0100 Subject: [PATCH 02/63] Grab the selected/link text to use in Link UI --- packages/format-library/src/link/inline.js | 74 +++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 8309e5df8ce12..609318e5e8154 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -12,6 +12,7 @@ import { applyFormat, useAnchorRef, removeFormat, + slice, } from '@wordpress/rich-text'; import { __experimentalLinkControl as LinkControl, @@ -24,6 +25,57 @@ import { useSelect } from '@wordpress/data'; */ import { createLinkFormat, isValidHref } from './utils'; import { link as settings } from './index'; +/** + * External dependencies + */ +import { find } from 'lodash'; + +function getFormatBoundary( + value, + format, + startIndex = value.start, + endIndex = value.end +) { + const { formats } = value; + const newFormats = formats.slice(); + + const startFormat = find( newFormats[ startIndex ], { + type: format.type, + } ); + + if ( ! startFormat ) { + return { + start: null, + end: null, + }; + } + + const index = newFormats[ startIndex ].indexOf( startFormat ); + + // Walk "backwards" until the start/leading "edge" of the matching format. + while ( + newFormats[ startIndex ] && + newFormats[ startIndex ][ index ] === startFormat + ) { + startIndex--; + } + + endIndex++; + + // Walk "forwards" until the end/trailing "edge" of the matching format. + while ( + newFormats[ endIndex ] && + newFormats[ endIndex ][ index ] === startFormat + ) { + endIndex++; + } + + // Return the indicies of the "edges" as the boundaries. + return { + start: startIndex + 1, + end: endIndex, + }; +} function InlineLinkUI( { isActive, @@ -35,6 +87,23 @@ function InlineLinkUI( { stopAddingLink, contentRef, } ) { + let formatStart = value.start; + let formatEnd = value.end; + + // If there is no selection then manually find the boundary + // of the link. + if ( isCollapsed( value ) ) { + const boundary = getFormatBoundary( value, { + type: 'core/link', + } ); + + formatStart = boundary.start; + formatEnd = boundary.end; + } + + // Grab the text content from the link format. + const { text = null } = slice( value, formatStart, formatEnd ); + /** * Pending settings to be applied to the next link. When inserting a new * link, toggle values cannot be applied immediately, because there is not @@ -60,6 +129,7 @@ function InlineLinkUI( { type: activeAttributes.type, id: activeAttributes.id, opensInNewTab: activeAttributes.target === '_blank', + text, ...nextLinkValue, }; @@ -109,8 +179,9 @@ function InlineLinkUI( { opensInNewWindow: nextValue.opensInNewTab, } ); + const newText = nextValue?.title || nextValue.title || newUrl; + if ( isCollapsed( value ) && ! isActive ) { - const newText = nextValue.title || newUrl; const toInsert = applyFormat( create( { text: newText } ), format, @@ -193,6 +264,7 @@ function InlineLinkUI( { createSuggestion={ createPageEntity && handleCreate } withCreateSuggestion={ userCanCreatePages } createSuggestionButtonText={ createButtonText } + hasTextControl /> ); From d61170b38c48b97fa70f3a9262f5db87c9503531 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 27 Sep 2021 16:40:19 +0100 Subject: [PATCH 03/63] Try to update the existing value with the new link and text --- packages/format-library/src/link/inline.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 609318e5e8154..b6bca25614ca1 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -13,6 +13,7 @@ import { useAnchorRef, removeFormat, slice, + replace, } from '@wordpress/rich-text'; import { __experimentalLinkControl as LinkControl, @@ -179,7 +180,7 @@ function InlineLinkUI( { opensInNewWindow: nextValue.opensInNewTab, } ); - const newText = nextValue?.title || nextValue.title || newUrl; + const newText = nextValue?.text || nextValue.title || newUrl; if ( isCollapsed( value ) && ! isActive ) { const toInsert = applyFormat( @@ -190,7 +191,20 @@ function InlineLinkUI( { ); onChange( insert( value, toInsert ) ); } else { - const newValue = applyFormat( value, format ); + // Create a new RichTextValue with + // 1. the new text provided by the LinkControl. + // 2. the link format applied. + const toInsert = applyFormat( + create( { text: newText } ), + format, + 0, + newText.length + ); + + // Update the existing value replacing the + // current text with the new RichTextValue. + const newValue = replace( value, text, toInsert ); + newValue.start = newValue.end; newValue.activeFormats = []; onChange( newValue ); From 371492d4600989d83a0eaa03cc56857961c7f0ba Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 6 Oct 2021 09:55:59 +0100 Subject: [PATCH 04/63] Retain existing formats when applying new link format --- packages/format-library/src/link/inline.js | 50 ++++++++++++---------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index b6bca25614ca1..9cc8c953c858b 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -20,6 +20,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies @@ -88,22 +89,26 @@ function InlineLinkUI( { stopAddingLink, contentRef, } ) { - let formatStart = value.start; - let formatEnd = value.end; + // Default to the selection ranges on the RichTextValue object. + let textStart = value.start; + let textEnd = value.end; // If there is no selection then manually find the boundary - // of the link. + // of the selection via the active format. if ( isCollapsed( value ) ) { const boundary = getFormatBoundary( value, { type: 'core/link', } ); - formatStart = boundary.start; - formatEnd = boundary.end; + textStart = boundary.start; + textEnd = boundary.end; } + // Get a RichTextValue containing the selected text content. - // Grab the text content from the link format. - const { text = null } = slice( value, formatStart, formatEnd ); + const richLinkTextValue = slice( value, textStart, textEnd ); + + // Get the text content minus any HTML tags. + const text = stripHTML( richLinkTextValue.text ); /** * Pending settings to be applied to the next link. When inserting a new @@ -170,7 +175,7 @@ function InlineLinkUI( { } const newUrl = prependHTTP( nextValue.url ); - const format = createLinkFormat( { + const linkFormat = createLinkFormat( { url: newUrl, type: nextValue.type, id: @@ -185,25 +190,26 @@ function InlineLinkUI( { if ( isCollapsed( value ) && ! isActive ) { const toInsert = applyFormat( create( { text: newText } ), - format, + linkFormat, 0, newText.length ); onChange( insert( value, toInsert ) ); } else { - // Create a new RichTextValue with - // 1. the new text provided by the LinkControl. - // 2. the link format applied. - const toInsert = applyFormat( - create( { text: newText } ), - format, - 0, - newText.length - ); - - // Update the existing value replacing the - // current text with the new RichTextValue. - const newValue = replace( value, text, toInsert ); + // Update the **text** (only) with the new text from the Link UI. + // This action retains any formats that were currently applied to + // the text selection (eg: bold, italic...etc). + let newValue = replace( richLinkTextValue, text, newText ); + + // Apply the new Link format to this new value. + newValue = applyFormat( newValue, linkFormat, 0, newText.length ); + + // Update the full existing value replacing the + // target text with the new RichTextValue containing: + // 1. The new text content. + // 2. The new link format. + // 3. Any original formats. + newValue = replace( value, text, newValue ); newValue.start = newValue.end; newValue.activeFormats = []; From 8d1bd59048c108ac37369bef6954fe38026effe6 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 4 Oct 2021 10:43:39 +0100 Subject: [PATCH 05/63] Document scenarios for clarity --- packages/format-library/src/link/inline.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/format-library/src/link/inline.js b/packages/format-library/src/link/inline.js index 9cc8c953c858b..db4f16666755c 100644 --- a/packages/format-library/src/link/inline.js +++ b/packages/format-library/src/link/inline.js @@ -188,6 +188,7 @@ function InlineLinkUI( { const newText = nextValue?.text || nextValue.title || newUrl; if ( isCollapsed( value ) && ! isActive ) { + // Scenario: we don't have any actively selected text or formats. const toInsert = applyFormat( create( { text: newText } ), linkFormat, @@ -196,6 +197,8 @@ function InlineLinkUI( { ); onChange( insert( value, toInsert ) ); } else { + // Scenario: we have any active text selection or an active format + // Update the **text** (only) with the new text from the Link UI. // This action retains any formats that were currently applied to // the text selection (eg: bold, italic...etc). From 7e1b1fbfe66beb14a2164635b53ce894015f0321 Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 4 Oct 2021 14:39:15 +0100 Subject: [PATCH 06/63] Only show text control once a link has been committed --- packages/block-editor/src/components/link-control/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 7f6c53adf1f51..6f1b31ae99c2a 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -227,8 +227,9 @@ function LinkControl( { const showSettingsDrawer = !! settings?.length; - // Only show once a URL value has been committed. - const showTextControl = hasTextControl; + // Only show text control once a URL value has been committed. + // See https://github.com/WordPress/gutenberg/pull/33849/#issuecomment-932194927. + const showTextControl = value?.url && hasTextControl; return (
Date: Tue, 26 Oct 2021 11:12:04 +0100 Subject: [PATCH 07/63] Strip HTML from Link Preview --- .../src/components/link-control/link-preview.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/link-control/link-preview.js b/packages/block-editor/src/components/link-control/link-preview.js index 793188b9f5718..cdbf587e5f7f5 100644 --- a/packages/block-editor/src/components/link-control/link-preview.js +++ b/packages/block-editor/src/components/link-control/link-preview.js @@ -14,6 +14,7 @@ import { } from '@wordpress/components'; import { filterURLForDisplay, safeDecodeURI } from '@wordpress/url'; import { Icon, globe, info, linkOff, edit } from '@wordpress/icons'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies @@ -40,6 +41,8 @@ export default function LinkPreview( { const displayURL = ( value && filterURLForDisplay( safeDecodeURI( value.url ), 16 ) ) || ''; + const displayTitle = + richData?.title || value?.text || value?.title || displayURL; const isEmptyURL = ! value.url.length; @@ -84,10 +87,7 @@ export default function LinkPreview( { className="block-editor-link-control__search-item-title" href={ value.url } > - { richData?.title || - value?.text || - value?.title || - displayURL } + { stripHTML( displayTitle ) } { value?.url && ( From 1676091eaa3240f8a24163a138fa27446199c83e Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Mon, 4 Oct 2021 14:40:46 +0100 Subject: [PATCH 08/63] Strip HTML from link text control --- packages/block-editor/src/components/link-control/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index 6f1b31ae99c2a..e2bbdcf8d0054 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -11,7 +11,7 @@ import { Button, Spinner, Notice, TextControl } from '@wordpress/components'; import { keyboardReturn } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { useRef, useState, useEffect } from '@wordpress/element'; -import { focus } from '@wordpress/dom'; +import { focus, __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { ENTER } from '@wordpress/keycodes'; /** @@ -255,7 +255,7 @@ function LinkControl( { ) } From 9df74e84fd5fde91a3592a29ed6ad43a397d696a Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Wed, 6 Oct 2021 10:11:01 +0100 Subject: [PATCH 09/63] Allow submission of text changes via enter key --- .../src/components/link-control/index.js | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index e2bbdcf8d0054..69019eccbc22b 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -209,7 +209,7 @@ function LinkControl( { stopEditing(); }; - const handleSubmitButton = () => { + const handleSubmit = () => { if ( currentInputValue !== value?.url || internalTextValue !== value?.text @@ -222,6 +222,17 @@ function LinkControl( { stopEditing(); }; + const handleSubmitWithEnter = ( event ) => { + const { keyCode } = event; + if ( + keyCode === ENTER && + ! currentInputIsEmpty // disallow submitting empty values. + ) { + event.preventDefault(); + handleSubmit(); + } + }; + const shownUnlinkControl = onRemove && value && ! isEditingLink && ! isCreatingPage; @@ -257,6 +268,7 @@ function LinkControl( { label="Text" value={ stripHTML( internalTextValue ) } onChange={ setInternalTextValue } + onKeyDown={ handleSubmitWithEnter } /> ) } @@ -280,17 +292,7 @@ function LinkControl( { >