Skip to content

Commit e1b69e3

Browse files
committed
Add support for writingsuggestions attribute
https://bugs.webkit.org/show_bug.cgi?id=266824 rdar://114989563 Reviewed by Aditya Keerthi. Implement support for the new DOM `writingsuggstions` web API attribute, as defined by the corresponding spec PR (whatwg/html#10018). * Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml: * Source/WTF/wtf/PlatformEnable.h: * Source/WebCore/dom/Element.cpp: (WebCore::Element::isWritingSuggestionsEnabled const): * Source/WebCore/dom/Element.h: * Source/WebCore/editing/VisibleSelection.cpp: (WebCore::VisibleSelection::canEnableWritingSuggestions const): * Source/WebCore/editing/VisibleSelection.h: * Source/WebCore/html/HTMLAttributeNames.in: * Source/WebCore/html/HTMLElement.cpp: (WebCore::HTMLElement::writingsuggestions const): (WebCore::HTMLElement::setWritingsuggestions): * Source/WebCore/html/HTMLElement.h: * Source/WebCore/html/HTMLElement.idl: * Source/WebCore/html/HTMLInputElement.cpp: (WebCore::HTMLInputElement::supportsWritingSuggestions const): * Source/WebCore/html/HTMLInputElement.h: * Source/WebKit/Shared/EditorState.cpp: (WebKit::operator<<): * Source/WebKit/Shared/EditorState.h: * Source/WebKit/Shared/EditorState.serialization.in: * Source/WebKit/Shared/FocusedElementInformation.h: * Source/WebKit/Shared/FocusedElementInformation.serialization.in: * Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm: (-[WKContentView _updateTextInputTraits:]): * Source/WebKit/UIProcess/mac/WebViewImpl.mm: (WebKit::WebViewImpl::postLayoutDataForContentEditable): (WebKit::WebViewImpl::allowsInlinePredictions const): * Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm: (WebKit::WebPage::getPlatformEditorStateCommon const): * Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm: (WebKit::WebPage::focusedElementInformation): * Tools/TestWebKitAPI/SourcesCocoa.txt: * Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj: * Tools/TestWebKitAPI/Tests/WebKitCocoa/WritingSuggestions.mm: Added. (-[WritingSuggestionsWebAPIWKWebView initWithHTMLString:]): (-[WritingSuggestionsWebAPIWKWebView focusElementAndEnsureEditorStateUpdate:]): (TEST): Canonical link: https://commits.webkit.org/274912@main
1 parent 40fc435 commit e1b69e3

24 files changed

+258
-3
lines changed

Source/WTF/Scripts/Preferences/UnifiedWebPreferences.yaml

+15
Original file line numberDiff line numberDiff line change
@@ -8326,6 +8326,21 @@ WriteRichTextDataWhenCopyingOrDragging:
83268326
WebCore:
83278327
default: true
83288328

8329+
WritingSuggestionsAttributeEnabled:
8330+
type: bool
8331+
status: stable
8332+
category: dom
8333+
humanReadableName: "Writing Suggestions"
8334+
humanReadableDescription: "Enable the writingsuggestions attribute"
8335+
condition: ENABLE(WRITING_SUGGESTIONS)
8336+
defaultValue:
8337+
WebKitLegacy:
8338+
default: true
8339+
WebKit:
8340+
default: true
8341+
WebCore:
8342+
default: true
8343+
83298344
ZoomOnDoubleTapWhenRoot:
83308345
type: bool
83318346
status: internal

Source/WTF/wtf/PlatformEnable.h

+5
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,8 @@
995995
&& USE(LINEARMEDIAKIT)
996996
#define ENABLE_LINEAR_MEDIA_PLAYER 0
997997
#endif
998+
999+
#if !defined(ENABLE_WRITING_SUGGESTIONS) \
1000+
&& (PLATFORM(COCOA) && HAVE(INLINE_PREDICTIONS))
1001+
#define ENABLE_WRITING_SUGGESTIONS 1
1002+
#endif

Source/WebCore/dom/Element.cpp

+49
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,10 @@
8080
#include "HTMLScriptElement.h"
8181
#include "HTMLSelectElement.h"
8282
#include "HTMLTemplateElement.h"
83+
#include "HTMLTextAreaElement.h"
8384
#include "IdChangeInvalidation.h"
8485
#include "IdTargetObserverRegistry.h"
86+
#include "InputType.h"
8587
#include "InspectorInstrumentation.h"
8688
#include "JSDOMPromiseDeferred.h"
8789
#include "JSLazyEventListener.h"
@@ -4752,6 +4754,53 @@ bool Element::isSpellCheckingEnabled() const
47524754
return true;
47534755
}
47544756

4757+
bool Element::isWritingSuggestionsEnabled() const
4758+
{
4759+
// If none of the following conditions are true, then return `false`.
4760+
4761+
// `element` is an `input` element whose `type` attribute is in either the
4762+
// `Text`, `Search`, `URL`, `Email` state and is `mutable`.
4763+
auto isEligibleInputElement = [&] {
4764+
RefPtr input = dynamicDowncast<HTMLInputElement>(*this);
4765+
if (!input)
4766+
return false;
4767+
4768+
return !input->isDisabledFormControl() && input->supportsWritingSuggestions();
4769+
};
4770+
4771+
// `element` is a `textarea` element that is `mutable`.
4772+
auto isEligibleTextArea = [&] {
4773+
RefPtr textArea = dynamicDowncast<HTMLTextAreaElement>(*this);
4774+
if (!textArea)
4775+
return false;
4776+
4777+
return !textArea->isDisabledFormControl();
4778+
};
4779+
4780+
// `element` is an `editing host` or is `editable`.
4781+
4782+
if (!isEligibleInputElement() && !isEligibleTextArea() && !hasEditableStyle())
4783+
return false;
4784+
4785+
// If `element` has an 'inclusive ancestor' with a `writingsuggestions` content attribute that's
4786+
// not in the `default` state and the nearest such ancestor's `writingsuggestions` content attribute
4787+
// is in the `false` state, then return `false`.
4788+
4789+
for (auto* ancestor = this; ancestor; ancestor = ancestor->parentElementInComposedTree()) {
4790+
auto& value = ancestor->attributeWithoutSynchronization(HTMLNames::writingsuggestionsAttr);
4791+
4792+
if (value.isNull())
4793+
continue;
4794+
if (value.isEmpty() || equalLettersIgnoringASCIICase(value, "true"_s))
4795+
return true;
4796+
if (equalLettersIgnoringASCIICase(value, "false"_s))
4797+
return false;
4798+
}
4799+
4800+
// Otherwise, return `true`.
4801+
return true;
4802+
}
4803+
47554804
#if ASSERT_ENABLED
47564805
bool Element::fastAttributeLookupAllowed(const QualifiedName& name) const
47574806
{

Source/WebCore/dom/Element.h

+1
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,7 @@ class Element : public ContainerNode {
629629
#endif
630630

631631
bool isSpellCheckingEnabled() const;
632+
WEBCORE_EXPORT bool isWritingSuggestionsEnabled() const;
632633

633634
inline bool hasID() const;
634635
inline bool hasClass() const;

Source/WebCore/editing/VisibleSelection.cpp

+15
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,21 @@ bool VisibleSelection::isInPasswordField() const
687687
return textControl && textControl->isPasswordField();
688688
}
689689

690+
bool VisibleSelection::canEnableWritingSuggestions() const
691+
{
692+
RefPtr containerNode = start().containerNode();
693+
if (!containerNode)
694+
return false;
695+
696+
if (RefPtr element = dynamicDowncast<Element>(containerNode.get()))
697+
return element->isWritingSuggestionsEnabled();
698+
699+
if (RefPtr element = containerNode->parentElement())
700+
return element->isWritingSuggestionsEnabled();
701+
702+
return false;
703+
}
704+
690705
bool VisibleSelection::isInAutoFilledAndViewableField() const
691706
{
692707
if (RefPtr input = dynamicDowncast<HTMLInputElement>(enclosingTextFormControl(start())))

Source/WebCore/editing/VisibleSelection.h

+2
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ class VisibleSelection {
118118
WEBCORE_EXPORT bool isInPasswordField() const;
119119
WEBCORE_EXPORT bool isInAutoFilledAndViewableField() const;
120120

121+
WEBCORE_EXPORT bool canEnableWritingSuggestions() const;
122+
121123
WEBCORE_EXPORT static Position adjustPositionForEnd(const Position& currentPosition, Node* startContainerNode);
122124
WEBCORE_EXPORT static Position adjustPositionForStart(const Position& currentPosition, Node* startContainerNode);
123125

Source/WebCore/html/HTMLAttributeNames.in

+1
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ webkitdirectory
431431
webkitdropzone
432432
width
433433
wrap
434+
writingsuggestions
434435
x-apple-data-detectors
435436
x-apple-data-detectors-result
436437
x-apple-data-detectors-type

Source/WebCore/html/HTMLElement.cpp

+10
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,16 @@ void HTMLElement::setSpellcheck(bool enable)
731731
setAttributeWithoutSynchronization(spellcheckAttr, enable ? trueAtom() : falseAtom());
732732
}
733733

734+
bool HTMLElement::writingsuggestions() const
735+
{
736+
return isWritingSuggestionsEnabled();
737+
}
738+
739+
void HTMLElement::setWritingsuggestions(bool enable)
740+
{
741+
setAttributeWithoutSynchronization(writingsuggestionsAttr, enable ? trueAtom() : falseAtom());
742+
}
743+
734744
void HTMLElement::effectiveSpellcheckAttributeChanged(bool newValue)
735745
{
736746
for (auto it = descendantsOfType<HTMLElement>(*this).begin(); it;) {

Source/WebCore/html/HTMLElement.h

+3
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class HTMLElement : public StyledElement {
8484
WEBCORE_EXPORT bool spellcheck() const;
8585
WEBCORE_EXPORT void setSpellcheck(bool);
8686

87+
WEBCORE_EXPORT bool writingsuggestions() const;
88+
WEBCORE_EXPORT void setWritingsuggestions(bool);
89+
8790
WEBCORE_EXPORT bool translate() const;
8891
WEBCORE_EXPORT void setTranslate(bool);
8992

Source/WebCore/html/HTMLElement.idl

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969

7070
// Non-standard: We are the only browser to support this now that Blink dropped it (http://crbug.com/688943).
7171
[CEReactions=Needed, Reflect] attribute DOMString webkitdropzone;
72+
73+
[Conditional=WRITING_SUGGESTIONS, CEReactions=Needed] attribute boolean writingsuggestions;
7274
};
7375

7476
HTMLElement includes GlobalEventHandlers;

Source/WebCore/html/HTMLInputElement.cpp

+12
Original file line numberDiff line numberDiff line change
@@ -1039,6 +1039,18 @@ bool HTMLInputElement::isTextType() const
10391039
return m_inputType->isTextType();
10401040
}
10411041

1042+
bool HTMLInputElement::supportsWritingSuggestions() const
1043+
{
1044+
static constexpr OptionSet<InputType::Type> allowedTypes = {
1045+
InputType::Type::Text,
1046+
InputType::Type::Search,
1047+
InputType::Type::URL,
1048+
InputType::Type::Email,
1049+
};
1050+
1051+
return allowedTypes.contains(m_inputType->type());
1052+
}
1053+
10421054
void HTMLInputElement::setDefaultCheckedState(bool isDefaultChecked)
10431055
{
10441056
if (m_isDefaultChecked == isDefaultChecked)

Source/WebCore/html/HTMLInputElement.h

+1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ class HTMLInputElement : public HTMLTextFormControlElement {
157157
// isTextField && !isPasswordField.
158158
WEBCORE_EXPORT bool isText() const;
159159
bool isTextType() const;
160+
bool supportsWritingSuggestions() const;
160161
WEBCORE_EXPORT bool isEmailField() const;
161162
WEBCORE_EXPORT bool isFileUpload() const;
162163
bool isImageButton() const;

Source/WebKit/Shared/EditorState.cpp

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ TextStream& operator<<(TextStream& ts, const EditorState& editorState)
7575
ts.dumpProperty("enclosingListType", enumToUnderlyingType(editorState.postLayoutData->enclosingListType));
7676
if (editorState.postLayoutData->baseWritingDirection != WebCore::WritingDirection::Natural)
7777
ts.dumpProperty("baseWritingDirection", static_cast<uint8_t>(editorState.postLayoutData->baseWritingDirection));
78+
if (editorState.postLayoutData->canEnableWritingSuggestions)
79+
ts.dumpProperty("canEnableWritingSuggestions", editorState.postLayoutData->canEnableWritingSuggestions);
7880
#endif // PLATFORM(COCOA)
7981
#if PLATFORM(IOS_FAMILY)
8082
if (editorState.postLayoutData->markedText.length())

Source/WebKit/Shared/EditorState.h

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ struct EditorState {
9595
ListType enclosingListType { ListType::None };
9696
WebCore::WritingDirection baseWritingDirection { WebCore::WritingDirection::Natural };
9797
bool editableRootIsTransparentOrFullyClipped { false };
98+
bool canEnableWritingSuggestions { false };
9899
#endif
99100
#if PLATFORM(IOS_FAMILY)
100101
String markedText;

Source/WebKit/Shared/EditorState.serialization.in

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct WebKit::EditorState {
7070
WebKit::ListType enclosingListType;
7171
WebCore::WritingDirection baseWritingDirection;
7272
bool editableRootIsTransparentOrFullyClipped;
73+
bool canEnableWritingSuggestions;
7374
#endif
7475
#if PLATFORM(IOS_FAMILY)
7576
String markedText;

Source/WebKit/Shared/FocusedElementInformation.h

+1
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ struct FocusedElementInformation {
134134
bool hasEverBeenPasswordField { false };
135135
bool shouldSynthesizeKeyEventsForEditing { false };
136136
bool isSpellCheckingEnabled { true };
137+
bool isWritingSuggestionsEnabled { false };
137138
bool shouldAvoidResizingWhenInputViewBoundsChange { false };
138139
bool shouldAvoidScrollingWhenFocusedContentIsVisible { false };
139140
bool shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation { false };

Source/WebKit/Shared/FocusedElementInformation.serialization.in

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ enum class WebKit::InputType : uint8_t {
104104
bool hasEverBeenPasswordField;
105105
bool shouldSynthesizeKeyEventsForEditing;
106106
bool isSpellCheckingEnabled;
107+
bool isWritingSuggestionsEnabled;
107108
bool shouldAvoidResizingWhenInputViewBoundsChange;
108109
bool shouldAvoidScrollingWhenFocusedContentIsVisible;
109110
bool shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation;

Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm

+1-1
Original file line numberDiff line numberDiff line change
@@ -6915,7 +6915,7 @@ - (void)_updateTextInputTraits:(id<UITextInputTraits>)traits
69156915
privateTraits.shortcutConversionType = _focusedElementInformation.elementType == WebKit::InputType::Password ? UITextShortcutConversionTypeNo : UITextShortcutConversionTypeDefault;
69166916

69176917
#if HAVE(INLINE_PREDICTIONS)
6918-
traits.inlinePredictionType = (self.webView.configuration.allowsInlinePredictions || _page->preferences().inlinePredictionsInAllEditableElementsEnabled()) ? UITextInlinePredictionTypeDefault : UITextInlinePredictionTypeNo;
6918+
traits.inlinePredictionType = (self.webView.configuration.allowsInlinePredictions || _page->preferences().inlinePredictionsInAllEditableElementsEnabled() || _focusedElementInformation.isWritingSuggestionsEnabled) ? UITextInlinePredictionTypeDefault : UITextInlinePredictionTypeNo;
69196919
#endif
69206920

69216921
[self _updateTextInputTraitsForInteractionTintColor];

Source/WebKit/UIProcess/mac/WebViewImpl.mm

+8-2
Original file line numberDiff line numberDiff line change
@@ -3210,8 +3210,6 @@ static String commandNameForSelector(SEL selector)
32103210
std::optional<EditorState::PostLayoutData> WebViewImpl::postLayoutDataForContentEditable()
32113211
{
32123212
const EditorState& editorState = m_page->editorState();
3213-
if (!editorState.isContentEditable)
3214-
return std::nullopt;
32153213

32163214
// FIXME: It's pretty lame that we have to depend on the most recent EditorState having post layout data,
32173215
// and that we just bail if it is missing.
@@ -5195,6 +5193,14 @@ static BOOL shouldUseHighlightsForMarkedText(NSAttributedString *string)
51955193
#if HAVE(INLINE_PREDICTIONS)
51965194
bool WebViewImpl::allowsInlinePredictions() const
51975195
{
5196+
const EditorState& editorState = m_page->editorState();
5197+
5198+
if (editorState.hasPostLayoutData() && editorState.postLayoutData->canEnableWritingSuggestions)
5199+
return NSSpellChecker.isAutomaticInlineCompletionEnabled;
5200+
5201+
if (!editorState.isContentEditable)
5202+
return false;
5203+
51985204
if (!inlinePredictionsEnabled() && !m_page->preferences().inlinePredictionsInAllEditableElementsEnabled())
51995205
return false;
52005206

Source/WebKit/WebProcess/WebPage/Cocoa/WebPageCocoa.mm

+2
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,8 @@
584584
}
585585

586586
postLayoutData.baseWritingDirection = frame.editor().baseWritingDirectionForSelectionStart();
587+
588+
postLayoutData.canEnableWritingSuggestions = selection.canEnableWritingSuggestions();
587589
}
588590

589591
if (RefPtr editableRootOrFormControl = enclosingTextFormControl(selection.start()) ?: selection.rootEditableElement()) {

Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm

+2
Original file line numberDiff line numberDiff line change
@@ -3550,6 +3550,8 @@ static void handleAnimationActions(Element& element, uint32_t action)
35503550
if (htmlElement)
35513551
information.isSpellCheckingEnabled = htmlElement->spellcheck();
35523552

3553+
information.isWritingSuggestionsEnabled = focusedElement->isWritingSuggestionsEnabled();
3554+
35533555
if (RefPtr formControlElement = dynamicDowncast<HTMLFormControlElement>(focusedElement))
35543556
information.isFocusingWithValidationMessage = formControlElement->isFocusingWithValidationMessage();
35553557

Tools/TestWebKitAPI/SourcesCocoa.txt

+1
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ Tests/WebKitCocoa/WebSQLBasics.mm
353353
Tests/WebKitCocoa/WebSocket.mm
354354
Tests/WebKitCocoa/WebsiteDataStoreCustomPaths.mm
355355
Tests/WebKitCocoa/WebsitePolicies.mm
356+
Tests/WebKitCocoa/WritingSuggestions.mm
356357
Tests/WebKitCocoa/YoutubeReplacementPlugin.mm
357358
Tests/WebKitCocoa/_WKInputDelegate.mm
358359
Tests/WebKitCocoa/_WKWebAuthenticationPanel.mm

Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj

+2
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,7 @@
20132013
0711DF51226A95FB003DD2F7 /* AVFoundationSoftLinkTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AVFoundationSoftLinkTest.mm; sourceTree = "<group>"; };
20142014
07137049265320E500CA2C9A /* AudioBufferSize.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = AudioBufferSize.mm; sourceTree = "<group>"; };
20152015
0721D4582838295400A95853 /* start-offset.ts */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.typescript; path = "start-offset.ts"; sourceTree = "<group>"; };
2016+
07338E042B7433A400F949EB /* WritingSuggestions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = WritingSuggestions.mm; sourceTree = "<group>"; };
20162017
0738012E275EADAB000FA77C /* GetDisplayMediaWindowAndScreen.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = GetDisplayMediaWindowAndScreen.mm; sourceTree = "<group>"; };
20172018
0746645722FF62D000E3451A /* AccessibilityTestSupportProtocol.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AccessibilityTestSupportProtocol.h; sourceTree = "<group>"; };
20182019
0746645822FF630500E3451A /* AccessibilityTestPlugin.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AccessibilityTestPlugin.mm; sourceTree = "<group>"; };
@@ -4337,6 +4338,7 @@
43374338
9984FACA1CFFAEEE008D198C /* WKWebViewTextInput.mm */,
43384339
95A524942581A10D00461FE9 /* WKWebViewThemeColor.mm */,
43394340
953ABB3425C0D681004C8B73 /* WKWebViewUnderPageBackgroundColor.mm */,
4341+
07338E042B7433A400F949EB /* WritingSuggestions.mm */,
43404342
465A08FD280096F80028FD0E /* YoutubeReplacementPlugin.mm */,
43414343
);
43424344
name = "WebKit Cocoa";

0 commit comments

Comments
 (0)