blob: 4126ed157fdb11a34bce0d175973d556faf2fd56 [file] [log] [blame]
/*
* Copyright (C) 2006-2019 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "Editor.h"
#if PLATFORM(IOS_FAMILY)
#import "CSSComputedStyleDeclaration.h"
#import "CSSPrimitiveValueMappings.h"
#import "CachedImage.h"
#import "DataTransfer.h"
#import "DictationCommandIOS.h"
#import "DocumentFragment.h"
#import "DocumentMarkerController.h"
#import "Editing.h"
#import "EditorClient.h"
#import "Frame.h"
#import "HTMLConverter.h"
#import "HTMLInputElement.h"
#import "HTMLNames.h"
#import "HTMLParserIdioms.h"
#import "HTMLTextAreaElement.h"
#import "Pasteboard.h"
#import "RenderBlock.h"
#import "RenderImage.h"
#import "SharedBuffer.h"
#import "StyleProperties.h"
#import "Text.h"
#import "TypingCommand.h"
#import "WAKAppKitStubs.h"
#import "WebContentReader.h"
#import "markup.h"
#import <wtf/text/StringBuilder.h>
namespace WebCore {
using namespace HTMLNames;
void Editor::showFontPanel()
{
}
void Editor::showStylesPanel()
{
}
void Editor::showColorPanel()
{
}
void Editor::setTextAlignmentForChangedBaseWritingDirection(WritingDirection direction)
{
// Note that the passed-in argument is the direction that has been changed to by
// some code or user interaction outside the scope of this function. The former
// direction is not known, nor is it required for the kind of text alignment
// changes done by this function.
//
// Rules:
// When text has no explicit alignment, set to alignment to match the writing direction.
// If the text has left or right alignment, flip left->right and right->left.
// Otherwise, do nothing.
auto selectionStyle = EditingStyle::styleAtSelectionStart(m_frame.selection().selection());
if (!selectionStyle || !selectionStyle->style())
return;
RefPtr<CSSPrimitiveValue> value = static_pointer_cast<CSSPrimitiveValue>(selectionStyle->style()->getPropertyCSSValue(CSSPropertyTextAlign));
if (!value)
return;
const char *newValue = nullptr;
TextAlignMode textAlign = *value;
switch (textAlign) {
case TextAlignMode::Start:
case TextAlignMode::End: {
switch (direction) {
case WritingDirection::Natural:
// no-op
break;
case WritingDirection::LeftToRight:
newValue = "left";
break;
case WritingDirection::RightToLeft:
newValue = "right";
break;
}
break;
}
case TextAlignMode::Left:
case TextAlignMode::WebKitLeft:
newValue = "right";
break;
case TextAlignMode::Right:
case TextAlignMode::WebKitRight:
newValue = "left";
break;
case TextAlignMode::Center:
case TextAlignMode::WebKitCenter:
case TextAlignMode::Justify:
// no-op
break;
}
if (!newValue)
return;
Element* focusedElement = m_frame.document()->focusedElement();
if (focusedElement && (is<HTMLTextAreaElement>(*focusedElement) || (is<HTMLInputElement>(*focusedElement)
&& (downcast<HTMLInputElement>(*focusedElement).isTextField()
|| downcast<HTMLInputElement>(*focusedElement).isSearchField())))) {
if (direction == WritingDirection::Natural)
return;
downcast<HTMLElement>(*focusedElement).setAttributeWithoutSynchronization(alignAttr, newValue);
m_frame.document()->updateStyleIfNeeded();
return;
}
auto style = MutableStyleProperties::create();
style->setProperty(CSSPropertyTextAlign, newValue);
applyParagraphStyle(style.ptr());
}
void Editor::removeUnchangeableStyles()
{
// This function removes styles that the user cannot modify by applying their default values.
auto editingStyle = EditingStyle::create(m_frame.document()->bodyOrFrameset());
auto defaultStyle = editingStyle->style()->mutableCopy();
// Text widgets implement background color via the UIView property. Their body element will not have one.
defaultStyle->setProperty(CSSPropertyBackgroundColor, "rgba(255, 255, 255, 0.0)");
// Remove properties that the user can modify, like font-weight.
// Also remove font-family, per HI spec.
// FIXME: it'd be nice if knowledge about which styles were unchangeable was not hard-coded here.
defaultStyle->removeProperty(CSSPropertyFontWeight);
defaultStyle->removeProperty(CSSPropertyFontStyle);
defaultStyle->removeProperty(CSSPropertyFontVariantCaps);
// FIXME: we should handle also pasted quoted text, strikethrough, etc. <rdar://problem/9255115>
defaultStyle->removeProperty(CSSPropertyTextDecoration);
defaultStyle->removeProperty(CSSPropertyWebkitTextDecorationsInEffect); // implements underline
// FIXME add EditAction::MatchStlye <rdar://problem/9156507> Undo rich text's paste & match style should say "Undo Match Style"
applyStyleToSelection(defaultStyle.ptr(), EditAction::ChangeAttributes);
}
static void getImage(Element& imageElement, RefPtr<Image>& image, CachedImage*& cachedImage)
{
auto* renderer = imageElement.renderer();
if (!is<RenderImage>(renderer))
return;
CachedImage* tentativeCachedImage = downcast<RenderImage>(*renderer).cachedImage();
if (!tentativeCachedImage || tentativeCachedImage->errorOccurred())
return;
image = tentativeCachedImage->imageForRenderer(renderer);
if (!image)
return;
cachedImage = tentativeCachedImage;
}
void Editor::writeImageToPasteboard(Pasteboard& pasteboard, Element& imageElement, const URL& url, const String& title)
{
PasteboardImage pasteboardImage;
RefPtr<Image> image;
CachedImage* cachedImage = nullptr;
getImage(imageElement, image, cachedImage);
if (!image)
return;
ASSERT(cachedImage);
auto imageSourceURL = imageElement.document().completeURL(stripLeadingAndTrailingHTMLSpaces(imageElement.imageSourceURL()));
auto pasteboardImageURL = url.isEmpty() ? imageSourceURL : url;
if (!pasteboardImageURL.isLocalFile()) {
pasteboardImage.url.url = pasteboardImageURL;
pasteboardImage.url.title = title;
}
pasteboardImage.suggestedName = imageSourceURL.lastPathComponent();
pasteboardImage.imageSize = image->size();
pasteboardImage.resourceMIMEType = pasteboard.resourceMIMEType(cachedImage->response().mimeType());
pasteboardImage.resourceData = cachedImage->resourceBuffer();
Position beforeImagePosition(&imageElement, Position::PositionIsBeforeAnchor);
Position afterImagePosition(&imageElement, Position::PositionIsAfterAnchor);
auto imageRange = Range::create(imageElement.document(), beforeImagePosition, afterImagePosition);
client()->getClientPasteboardDataForRange(imageRange.ptr(), pasteboardImage.clientTypes, pasteboardImage.clientData);
pasteboard.write(pasteboardImage);
}
void Editor::pasteWithPasteboard(Pasteboard* pasteboard, OptionSet<PasteOption> options)
{
RefPtr<Range> range = selectedRange();
bool allowPlainText = options.contains(PasteOption::AllowPlainText);
WebContentReader reader(m_frame, *range, allowPlainText);
int numberOfPasteboardItems = client()->getPasteboardItemsCount();
for (int i = 0; i < numberOfPasteboardItems; ++i) {
RefPtr<DocumentFragment> fragment = client()->documentFragmentFromDelegate(i);
if (!fragment)
continue;
reader.addFragment(fragment.releaseNonNull());
}
RefPtr<DocumentFragment> fragment = reader.fragment;
if (!fragment) {
bool chosePlainTextIgnored;
fragment = webContentFromPasteboard(*pasteboard, *range, allowPlainText, chosePlainTextIgnored);
}
if (fragment && options.contains(PasteOption::AsQuotation))
quoteFragmentForPasting(*fragment);
if (fragment && shouldInsertFragment(*fragment, range.get(), EditorInsertAction::Pasted))
pasteAsFragment(fragment.releaseNonNull(), canSmartReplaceWithPasteboard(*pasteboard), false, options.contains(PasteOption::IgnoreMailBlockquote) ? MailBlockquoteHandling::IgnoreBlockquote : MailBlockquoteHandling::RespectBlockquote);
}
void Editor::insertDictationPhrases(Vector<Vector<String>>&& dictationPhrases, RetainPtr<id> metadata)
{
if (m_frame.selection().isNone())
return;
if (dictationPhrases.isEmpty())
return;
DictationCommandIOS::create(document(), WTFMove(dictationPhrases), WTFMove(metadata))->apply();
}
void Editor::setDictationPhrasesAsChildOfElement(const Vector<Vector<String>>& dictationPhrases, RetainPtr<id> metadata, Element& element)
{
// Clear the composition.
clear();
// Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack.
// Some day we could make them Undoable, and let callers clear the Undo stack explicitly if they wish.
clearUndoRedoOperations();
m_frame.selection().clear();
element.removeChildren();
if (dictationPhrases.isEmpty()) {
client()->respondToChangedContents();
return;
}
RefPtr<Range> context = document().createRange();
context->selectNodeContents(element);
StringBuilder dictationPhrasesBuilder;
for (auto& interpretations : dictationPhrases)
dictationPhrasesBuilder.append(interpretations[0]);
element.appendChild(createFragmentFromText(*context, dictationPhrasesBuilder.toString()));
auto weakElement = makeWeakPtr(element);
// We need a layout in order to add markers below.
document().updateLayout();
if (!weakElement)
return;
if (!element.firstChild()->isTextNode()) {
// Shouldn't happen.
ASSERT(element.firstChild()->isTextNode());
return;
}
Text& textNode = downcast<Text>(*element.firstChild());
int previousDictationPhraseStart = 0;
for (auto& interpretations : dictationPhrases) {
int dictationPhraseLength = interpretations[0].length();
int dictationPhraseEnd = previousDictationPhraseStart + dictationPhraseLength;
if (interpretations.size() > 1) {
auto dictationPhraseRange = Range::create(document(), &textNode, previousDictationPhraseStart, &textNode, dictationPhraseEnd);
document().markers().addDictationPhraseWithAlternativesMarker(dictationPhraseRange, interpretations);
}
previousDictationPhraseStart = dictationPhraseEnd;
}
auto resultRange = Range::create(document(), &textNode, 0, &textNode, textNode.length());
document().markers().addDictationResultMarker(resultRange, metadata);
client()->respondToChangedContents();
}
void Editor::confirmMarkedText()
{
// FIXME: This is a hacky workaround for the keyboard calling this method too late -
// after the selection and focus have already changed. See <rdar://problem/5975559>.
Element* focused = document().focusedElement();
Node* composition = compositionNode();
if (composition && focused && focused != composition && !composition->isDescendantOrShadowDescendantOf(focused)) {
cancelComposition();
document().setFocusedElement(focused);
} else
confirmComposition();
}
void Editor::setTextAsChildOfElement(const String& text, Element& element)
{
// Clear the composition
clear();
// Clear the Undo stack, since the operations that follow are not Undoable, and will corrupt the stack.
// Some day we could make them Undoable, and let callers clear the Undo stack explicitly if they wish.
clearUndoRedoOperations();
// If the element is empty already and we're not adding text, we can early return and avoid clearing/setting
// a selection at [0, 0] and the expense involved in creation VisiblePositions.
if (!element.firstChild() && text.isEmpty())
return;
// As a side effect this function sets a caret selection after the inserted content. Much of what
// follows is more expensive if there is a selection, so clear it since it's going to change anyway.
m_frame.selection().clear();
// clear out all current children of element
element.removeChildren();
if (text.length()) {
// insert new text
// remove element from tree while doing it
// FIXME: The element we're inserting into is often the body element. It seems strange to be removing it
// (even if it is only temporary). ReplaceSelectionCommand doesn't bother doing this when it inserts
// content, why should we here?
RefPtr<Node> parent = element.parentNode();
RefPtr<Node> siblingAfter = element.nextSibling();
if (parent)
element.remove();
auto context = document().createRange();
context->selectNodeContents(element);
element.appendChild(createFragmentFromText(context, text));
// restore element to document
if (parent) {
if (siblingAfter)
parent->insertBefore(element, siblingAfter.get());
else
parent->appendChild(element);
}
}
// set the selection to the end
VisibleSelection selection;
Position pos = createLegacyEditingPosition(&element, element.countChildNodes());
VisiblePosition visiblePos(pos, VP_DEFAULT_AFFINITY);
if (visiblePos.isNull())
return;
selection.setBase(visiblePos);
selection.setExtent(visiblePos);
m_frame.selection().setSelection(selection);
client()->respondToChangedContents();
}
// If the selection is adjusted from UIKit without closing the typing, the typing command may
// have a stale selection.
void Editor::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping()
{
TypingCommand::ensureLastEditCommandHasCurrentSelectionIfOpenForMoreTyping(&m_frame, m_frame.selection().selection());
}
} // namespace WebCore
#endif // PLATFORM(IOS_FAMILY)