Protect frames during style and layout changes
[WebKit-https.git] / Source / WebCore / editing / ios / EditorIOS.mm
1 /*
2  * Copyright (C) 2006-2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "Editor.h"
28
29 #if PLATFORM(IOS_FAMILY)
30
31 #import "CSSComputedStyleDeclaration.h"
32 #import "CSSPrimitiveValueMappings.h"
33 #import "CachedImage.h"
34 #import "DataTransfer.h"
35 #import "DictationCommandIOS.h"
36 #import "DocumentFragment.h"
37 #import "DocumentMarkerController.h"
38 #import "Editing.h"
39 #import "EditorClient.h"
40 #import "Frame.h"
41 #import "HTMLConverter.h"
42 #import "HTMLInputElement.h"
43 #import "HTMLNames.h"
44 #import "HTMLParserIdioms.h"
45 #import "HTMLTextAreaElement.h"
46 #import "Pasteboard.h"
47 #import "RenderBlock.h"
48 #import "RenderImage.h"
49 #import "SharedBuffer.h"
50 #import "StyleProperties.h"
51 #import "Text.h"
52 #import "TypingCommand.h"
53 #import "WAKAppKitStubs.h"
54 #import "WebContentReader.h"
55 #import "markup.h"
56 #import <wtf/text/StringBuilder.h>
57
58 namespace WebCore {
59
60 using namespace HTMLNames;
61
62 void Editor::showFontPanel()
63 {
64 }
65
66 void Editor::showStylesPanel()
67 {
68 }
69
70 void Editor::showColorPanel()
71 {
72 }
73
74 void Editor::setTextAlignmentForChangedBaseWritingDirection(WritingDirection direction)
75 {
76     // Note that the passed-in argument is the direction that has been changed to by
77     // some code or user interaction outside the scope of this function. The former
78     // direction is not known, nor is it required for the kind of text alignment
79     // changes done by this function.
80     //
81     // Rules:
82     // When text has no explicit alignment, set to alignment to match the writing direction.
83     // If the text has left or right alignment, flip left->right and right->left. 
84     // Otherwise, do nothing.
85
86     auto selectionStyle = EditingStyle::styleAtSelectionStart(m_frame.selection().selection());
87     if (!selectionStyle || !selectionStyle->style())
88          return;
89
90     RefPtr<CSSPrimitiveValue> value = static_pointer_cast<CSSPrimitiveValue>(selectionStyle->style()->getPropertyCSSValue(CSSPropertyTextAlign));
91     if (!value)
92         return;
93         
94     const char *newValue = nullptr;
95     TextAlignMode textAlign = *value;
96     switch (textAlign) {
97     case TextAlignMode::Start:
98     case TextAlignMode::End: {
99         switch (direction) {
100         case WritingDirection::Natural:
101             // no-op
102             break;
103         case WritingDirection::LeftToRight:
104             newValue = "left";
105             break;
106         case WritingDirection::RightToLeft:
107             newValue = "right";
108             break;
109         }
110         break;
111     }
112     case TextAlignMode::Left:
113     case TextAlignMode::WebKitLeft:
114         newValue = "right";
115         break;
116     case TextAlignMode::Right:
117     case TextAlignMode::WebKitRight:
118         newValue = "left";
119         break;
120     case TextAlignMode::Center:
121     case TextAlignMode::WebKitCenter:
122     case TextAlignMode::Justify:
123         // no-op
124         break;
125     }
126
127     if (!newValue)
128         return;
129
130     Element* focusedElement = m_frame.document()->focusedElement();
131     if (focusedElement && (is<HTMLTextAreaElement>(*focusedElement) || (is<HTMLInputElement>(*focusedElement)
132         && (downcast<HTMLInputElement>(*focusedElement).isTextField()
133             || downcast<HTMLInputElement>(*focusedElement).isSearchField())))) {
134         if (direction == WritingDirection::Natural)
135             return;
136         downcast<HTMLElement>(*focusedElement).setAttributeWithoutSynchronization(alignAttr, newValue);
137         m_frame.document()->updateStyleIfNeeded();
138         return;
139     }
140
141     auto style = MutableStyleProperties::create();
142     style->setProperty(CSSPropertyTextAlign, newValue);
143     applyParagraphStyle(style.ptr());
144 }
145
146 void Editor::removeUnchangeableStyles()
147 {
148     // This function removes styles that the user cannot modify by applying their default values.
149     
150     auto editingStyle = EditingStyle::create(m_frame.document()->bodyOrFrameset());
151     auto defaultStyle = editingStyle->style()->mutableCopy();
152     
153     // Text widgets implement background color via the UIView property. Their body element will not have one.
154     defaultStyle->setProperty(CSSPropertyBackgroundColor, "rgba(255, 255, 255, 0.0)");
155     
156     // Remove properties that the user can modify, like font-weight. 
157     // Also remove font-family, per HI spec.
158     // FIXME: it'd be nice if knowledge about which styles were unchangeable was not hard-coded here.
159     defaultStyle->removeProperty(CSSPropertyFontWeight);
160     defaultStyle->removeProperty(CSSPropertyFontStyle);
161     defaultStyle->removeProperty(CSSPropertyFontVariantCaps);
162     // FIXME: we should handle also pasted quoted text, strikethrough, etc. <rdar://problem/9255115>
163     defaultStyle->removeProperty(CSSPropertyTextDecoration);
164     defaultStyle->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); // implements underline
165
166     // FIXME add EditAction::MatchStlye <rdar://problem/9156507> Undo rich text's paste & match style should say "Undo Match Style"
167     applyStyleToSelection(defaultStyle.ptr(), EditAction::ChangeAttributes);
168 }
169
170 static void getImage(Element& imageElement, RefPtr<Image>& image, CachedImage*& cachedImage)
171 {
172     auto* renderer = imageElement.renderer();
173     if (!is<RenderImage>(renderer))
174         return;
175
176     CachedImage* tentativeCachedImage = downcast<RenderImage>(*renderer).cachedImage();
177     if (!tentativeCachedImage || tentativeCachedImage->errorOccurred())
178         return;
179
180     image = tentativeCachedImage->imageForRenderer(renderer);
181     if (!image)
182         return;
183     
184     cachedImage = tentativeCachedImage;
185 }
186
187 void Editor::writeImageToPasteboard(Pasteboard& pasteboard, Element& imageElement, const URL& url, const String& title)
188 {
189     PasteboardImage pasteboardImage;
190
191     RefPtr<Image> image;
192     CachedImage* cachedImage = nullptr;
193     getImage(imageElement, image, cachedImage);
194     if (!image)
195         return;
196     ASSERT(cachedImage);
197
198     auto imageSourceURL = imageElement.document().completeURL(stripLeadingAndTrailingHTMLSpaces(imageElement.imageSourceURL()));
199
200     auto pasteboardImageURL = url.isEmpty() ? imageSourceURL : url;
201     if (!pasteboardImageURL.isLocalFile()) {
202         pasteboardImage.url.url = pasteboardImageURL;
203         pasteboardImage.url.title = title;
204     }
205     pasteboardImage.suggestedName = imageSourceURL.lastPathComponent();
206     pasteboardImage.imageSize = image->size();
207     pasteboardImage.resourceMIMEType = pasteboard.resourceMIMEType(cachedImage->response().mimeType());
208     pasteboardImage.resourceData = cachedImage->resourceBuffer();
209
210     Position beforeImagePosition(&imageElement, Position::PositionIsBeforeAnchor);
211     Position afterImagePosition(&imageElement, Position::PositionIsAfterAnchor);
212     auto imageRange = Range::create(imageElement.document(), beforeImagePosition, afterImagePosition);
213     client()->getClientPasteboardDataForRange(imageRange.ptr(), pasteboardImage.clientTypes, pasteboardImage.clientData);
214
215     pasteboard.write(pasteboardImage);
216 }
217
218 void Editor::pasteWithPasteboard(Pasteboard* pasteboard, OptionSet<PasteOption> options)
219 {
220     RefPtr<Range> range = selectedRange();
221     bool allowPlainText = options.contains(PasteOption::AllowPlainText);
222     WebContentReader reader(m_frame, *range, allowPlainText);
223     int numberOfPasteboardItems = client()->getPasteboardItemsCount();
224     for (int i = 0; i < numberOfPasteboardItems; ++i) {
225         RefPtr<DocumentFragment> fragment = client()->documentFragmentFromDelegate(i);
226         if (!fragment)
227             continue;
228         reader.addFragment(fragment.releaseNonNull());
229     }
230
231     RefPtr<DocumentFragment> fragment = reader.fragment;
232     if (!fragment) {
233         bool chosePlainTextIgnored;
234         fragment = webContentFromPasteboard(*pasteboard, *range, allowPlainText, chosePlainTextIgnored);
235     }
236
237     if (fragment && options.contains(PasteOption::AsQuotation))
238         quoteFragmentForPasting(*fragment);
239
240     if (fragment && shouldInsertFragment(*fragment, range.get(), EditorInsertAction::Pasted))
241         pasteAsFragment(fragment.releaseNonNull(), canSmartReplaceWithPasteboard(*pasteboard), false, options.contains(PasteOption::IgnoreMailBlockquote) ? MailBlockquoteHandling::IgnoreBlockquote : MailBlockquoteHandling::RespectBlockquote);
242 }
243
244 void Editor::insertDictationPhrases(Vector<Vector<String>>&& dictationPhrases, RetainPtr<id> metadata)
245 {
246     if (m_frame.selection().isNone())
247         return;
248
249     if (dictationPhrases.isEmpty())
250         return;
251
252     DictationCommandIOS::create(document(), WTFMove(dictationPhrases), WTFMove(metadata))->apply();
253 }
254
255 void Editor::setDictationPhrasesAsChildOfElement(const Vector<Vector<String>>& dictationPhrases, RetainPtr<id> metadata, Element& element)
256 {
257     // Clear the composition.
258     clear();
259
260     // Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack.
261     // Some day we could make them Undoable, and let callers clear the Undo stack explicitly if they wish.
262     clearUndoRedoOperations();
263
264     m_frame.selection().clear();
265
266     element.removeChildren();
267
268     if (dictationPhrases.isEmpty()) {
269         client()->respondToChangedContents();
270         return;
271     }
272
273     RefPtr<Range> context = document().createRange();
274     context->selectNodeContents(element);
275
276     StringBuilder dictationPhrasesBuilder;
277     for (auto& interpretations : dictationPhrases)
278         dictationPhrasesBuilder.append(interpretations[0]);
279
280     element.appendChild(createFragmentFromText(*context, dictationPhrasesBuilder.toString()));
281
282     auto weakElement = makeWeakPtr(element);
283
284     // We need a layout in order to add markers below.
285     document().updateLayout();
286
287     if (!weakElement)
288         return;
289
290     if (!element.firstChild()->isTextNode()) {
291         // Shouldn't happen.
292         ASSERT(element.firstChild()->isTextNode());
293         return;
294     }
295
296     Text& textNode = downcast<Text>(*element.firstChild());
297     int previousDictationPhraseStart = 0;
298     for (auto& interpretations : dictationPhrases) {
299         int dictationPhraseLength = interpretations[0].length();
300         int dictationPhraseEnd = previousDictationPhraseStart + dictationPhraseLength;
301         if (interpretations.size() > 1) {
302             auto dictationPhraseRange = Range::create(document(), &textNode, previousDictationPhraseStart, &textNode, dictationPhraseEnd);
303             document().markers().addDictationPhraseWithAlternativesMarker(dictationPhraseRange, interpretations);
304         }
305         previousDictationPhraseStart = dictationPhraseEnd;
306     }
307
308     auto resultRange = Range::create(document(), &textNode, 0, &textNode, textNode.length());
309     document().markers().addDictationResultMarker(resultRange, metadata);
310
311     client()->respondToChangedContents();
312 }
313
314 void Editor::confirmMarkedText()
315 {
316     // FIXME: This is a hacky workaround for the keyboard calling this method too late -
317     // after the selection and focus have already changed. See <rdar://problem/5975559>.
318     Element* focused = document().focusedElement();
319     Node* composition = compositionNode();
320     if (composition && focused && focused != composition && !composition->isDescendantOrShadowDescendantOf(focused)) {
321         cancelComposition();
322         document().setFocusedElement(focused);
323     } else
324         confirmComposition();
325 }
326
327 void Editor::setTextAsChildOfElement(const String& text, Element& element)
328 {
329     // Clear the composition
330     clear();
331
332     // Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack.
333     // Some day we could make them Undoable, and let callers clear the Undo stack explicitly if they wish.
334     clearUndoRedoOperations();
335
336     // If the element is empty already and we're not adding text, we can early return and avoid clearing/setting
337     // a selection at [0, 0] and the expense involved in creation VisiblePositions.
338     if (!element.firstChild() && text.isEmpty())
339         return;
340
341     // As a side effect this function sets a caret selection after the inserted content. Much of what
342     // follows is more expensive if there is a selection, so clear it since it's going to change anyway.
343     m_frame.selection().clear();
344
345     // clear out all current children of element
346     element.removeChildren();
347
348     if (text.length()) {
349         // insert new text
350         // remove element from tree while doing it
351         // FIXME: The element we're inserting into is often the body element. It seems strange to be removing it
352         // (even if it is only temporary). ReplaceSelectionCommand doesn't bother doing this when it inserts
353         // content, why should we here?
354         RefPtr<Node> parent = element.parentNode();
355         RefPtr<Node> siblingAfter = element.nextSibling();
356         if (parent)
357             element.remove();
358
359         auto context = document().createRange();
360         context->selectNodeContents(element);
361         element.appendChild(createFragmentFromText(context, text));
362
363         // restore element to document
364         if (parent) {
365             if (siblingAfter)
366                 parent->insertBefore(element, siblingAfter.get());
367             else
368                 parent->appendChild(element);
369         }
370     }
371
372     // set the selection to the end
373     VisibleSelection selection;
374
375     Position pos = createLegacyEditingPosition(&element, element.countChildNodes());
376
377     VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY);
378     if (visiblePos.isNull())
379         return;
380
381     selection.setBase(visiblePos);
382     selection.setExtent(visiblePos);
383
384     m_frame.selection().setSelection(selection);
385
386     client()->respondToChangedContents();
387 }
388
389 // If the selection is adjusted from UIKit without closing the typing, the typing command may
390 // have a stale selection.
391 void Editor::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping()
392 {
393     TypingCommand::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping(&m_frame, m_frame.selection().selection());
394 }
395
396 } // namespace WebCore
397
398 #endif // PLATFORM(IOS_FAMILY)