1/*
2 * Copyright (C) 2006, 2008, 2009 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. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "DeleteButtonController.h"
28
29#include "CachedImage.h"
30#include "CSSPrimitiveValue.h"
31#include "CompositeEditCommand.h"
32#include "Document.h"
33#include "EditorClient.h"
34#include "htmlediting.h"
35#include "HTMLDivElement.h"
36#include "HTMLNames.h"
37#include "Image.h"
38#include "Node.h"
39#include "Page.h"
40#include "RemoveNodeCommand.h"
41#include "RenderBox.h"
42#include "StyleProperties.h"
43
44namespace WebCore {
45
46using namespace HTMLNames;
47
48#if ENABLE(DELETION_UI)
49
50const char* const DeleteButtonController::containerElementIdentifier = "WebKit-Editing-Delete-Container";
51const char* const DeleteButtonController::buttonElementIdentifier = "WebKit-Editing-Delete-Button";
52const char* const DeleteButtonController::outlineElementIdentifier = "WebKit-Editing-Delete-Outline";
53
54DeleteButtonController::DeleteButtonController(Frame& frame)
55    : m_frame(frame)
56    , m_wasStaticPositioned(false)
57    , m_wasAutoZIndex(false)
58    , m_disableStack(0)
59{
60}
61
62static bool isDeletableElement(const Node* node)
63{
64    if (!node || !node->isHTMLElement() || !node->inDocument() || !node->hasEditableStyle())
65        return false;
66
67    // In general we want to only draw the UI around object of a certain area, but we still keep the min width/height to
68    // make sure we don't end up with very thin or very short elements getting the UI.
69    const int minimumArea = 2500;
70    const int minimumWidth = 48;
71    const int minimumHeight = 16;
72    const unsigned minimumVisibleBorders = 1;
73
74    RenderObject* renderer = node->renderer();
75    if (!renderer || !renderer->isBox())
76        return false;
77
78    // Disallow the body element since it isn't practical to delete, and the deletion UI would be clipped.
79    if (node->hasTagName(bodyTag))
80        return false;
81
82    // Disallow elements with any overflow clip, since the deletion UI would be clipped as well. <rdar://problem/6840161>
83    if (renderer->hasOverflowClip())
84        return false;
85
86    // Disallow Mail blockquotes since the deletion UI would get in the way of editing for these.
87    if (isMailBlockquote(node))
88        return false;
89
90    RenderBox* box = toRenderBox(renderer);
91    IntRect borderBoundingBox = box->borderBoundingBox();
92    if (borderBoundingBox.width() < minimumWidth || borderBoundingBox.height() < minimumHeight)
93        return false;
94
95    if ((borderBoundingBox.width() * borderBoundingBox.height()) < minimumArea)
96        return false;
97
98    if (box->isTable())
99        return true;
100
101    if (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(iframeTag))
102        return true;
103
104    if (box->isOutOfFlowPositioned())
105        return true;
106
107    if (box->isRenderBlock() && !box->isTableCell()) {
108        const RenderStyle& style = box->style();
109
110        // Allow blocks that have background images
111        if (style.hasBackgroundImage()) {
112            for (const FillLayer* background = style.backgroundLayers(); background; background = background->next()) {
113                if (background->image() && background->image()->canRender(box, 1))
114                    return true;
115            }
116        }
117
118        // Allow blocks with a minimum number of non-transparent borders
119        unsigned visibleBorders = style.borderTop().isVisible() + style.borderBottom().isVisible() + style.borderLeft().isVisible() + style.borderRight().isVisible();
120        if (visibleBorders >= minimumVisibleBorders)
121            return true;
122
123        // Allow blocks that have a different background from it's parent
124        ContainerNode* parentNode = node->parentNode();
125        if (!parentNode)
126            return false;
127
128        auto parentRenderer = parentNode->renderer();
129        if (!parentRenderer)
130            return false;
131
132        const RenderStyle& parentStyle = parentRenderer->style();
133
134        if (box->hasBackground() && (!parentRenderer->hasBackground() || style.visitedDependentColor(CSSPropertyBackgroundColor) != parentStyle.visitedDependentColor(CSSPropertyBackgroundColor)))
135            return true;
136    }
137
138    return false;
139}
140
141static HTMLElement* enclosingDeletableElement(const VisibleSelection& selection)
142{
143    if (!selection.isContentEditable())
144        return 0;
145
146    RefPtr<Range> range = selection.toNormalizedRange();
147    if (!range)
148        return nullptr;
149
150    Node* container = range->commonAncestorContainer(ASSERT_NO_EXCEPTION);
151    ASSERT(container);
152
153    // The enclosingNodeOfType function only works on nodes that are editable
154    // and capable of having editing positions inside them (which is strange, given its name).
155    if (!container->hasEditableStyle() || editingIgnoresContent(container))
156        return nullptr;
157
158    Node* element = enclosingNodeOfType(firstPositionInNode(container), &isDeletableElement);
159    return element && element->isHTMLElement() ? toHTMLElement(element) : nullptr;
160}
161
162void DeleteButtonController::respondToChangedSelection(const VisibleSelection& oldSelection)
163{
164    if (!enabled())
165        return;
166
167    HTMLElement* oldElement = enclosingDeletableElement(oldSelection);
168    HTMLElement* newElement = enclosingDeletableElement(m_frame.selection().selection());
169    if (oldElement == newElement)
170        return;
171
172    // If the base is inside a deletable element, give the element a delete widget.
173    if (newElement)
174        show(newElement);
175    else
176        hide();
177}
178
179void DeleteButtonController::deviceScaleFactorChanged()
180{
181    if (!enabled())
182        return;
183
184    HTMLElement* currentTarget = m_target.get();
185    hide();
186
187    // Setting m_containerElement to 0 will force the deletionUI to be re-created with
188    // artwork of the appropriate resolution in show().
189    m_containerElement = 0;
190    show(currentTarget);
191}
192
193void DeleteButtonController::createDeletionUI()
194{
195    RefPtr<HTMLDivElement> container = HTMLDivElement::create(m_target->document());
196    container->setIdAttribute(containerElementIdentifier);
197
198    container->setInlineStyleProperty(CSSPropertyWebkitUserDrag, CSSValueNone);
199    container->setInlineStyleProperty(CSSPropertyWebkitUserSelect, CSSValueNone);
200    container->setInlineStyleProperty(CSSPropertyWebkitUserModify, CSSValueReadOnly);
201    container->setInlineStyleProperty(CSSPropertyVisibility, CSSValueHidden);
202    container->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
203    container->setInlineStyleProperty(CSSPropertyCursor, CSSValueDefault);
204    container->setInlineStyleProperty(CSSPropertyTop, 0, CSSPrimitiveValue::CSS_PX);
205    container->setInlineStyleProperty(CSSPropertyRight, 0, CSSPrimitiveValue::CSS_PX);
206    container->setInlineStyleProperty(CSSPropertyBottom, 0, CSSPrimitiveValue::CSS_PX);
207    container->setInlineStyleProperty(CSSPropertyLeft, 0, CSSPrimitiveValue::CSS_PX);
208
209    RefPtr<HTMLDivElement> outline = HTMLDivElement::create(m_target->document());
210    outline->setIdAttribute(outlineElementIdentifier);
211
212    const int borderWidth = 4;
213    const int borderRadius = 6;
214
215    outline->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
216    outline->setInlineStyleProperty(CSSPropertyZIndex, ASCIILiteral("-1000000"));
217    outline->setInlineStyleProperty(CSSPropertyTop, -borderWidth - m_target->renderBox()->borderTop(), CSSPrimitiveValue::CSS_PX);
218    outline->setInlineStyleProperty(CSSPropertyRight, -borderWidth - m_target->renderBox()->borderRight(), CSSPrimitiveValue::CSS_PX);
219    outline->setInlineStyleProperty(CSSPropertyBottom, -borderWidth - m_target->renderBox()->borderBottom(), CSSPrimitiveValue::CSS_PX);
220    outline->setInlineStyleProperty(CSSPropertyLeft, -borderWidth - m_target->renderBox()->borderLeft(), CSSPrimitiveValue::CSS_PX);
221    outline->setInlineStyleProperty(CSSPropertyBorderWidth, borderWidth, CSSPrimitiveValue::CSS_PX);
222    outline->setInlineStyleProperty(CSSPropertyBorderStyle, CSSValueSolid);
223    outline->setInlineStyleProperty(CSSPropertyBorderColor, ASCIILiteral("rgba(0, 0, 0, 0.6)"));
224    outline->setInlineStyleProperty(CSSPropertyBorderRadius, borderRadius, CSSPrimitiveValue::CSS_PX);
225    outline->setInlineStyleProperty(CSSPropertyVisibility, CSSValueVisible);
226
227    ExceptionCode ec = 0;
228    container->appendChild(outline.get(), ec);
229    ASSERT(!ec);
230    if (ec)
231        return;
232
233    RefPtr<DeleteButton> button = DeleteButton::create(m_target->document());
234    button->setIdAttribute(buttonElementIdentifier);
235
236    const int buttonWidth = 30;
237    const int buttonHeight = 30;
238    const int buttonBottomShadowOffset = 2;
239
240    button->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
241    button->setInlineStyleProperty(CSSPropertyZIndex, ASCIILiteral("1000000"));
242    button->setInlineStyleProperty(CSSPropertyTop, (-buttonHeight / 2) - m_target->renderBox()->borderTop() - (borderWidth / 2) + buttonBottomShadowOffset, CSSPrimitiveValue::CSS_PX);
243    button->setInlineStyleProperty(CSSPropertyLeft, (-buttonWidth / 2) - m_target->renderBox()->borderLeft() - (borderWidth / 2), CSSPrimitiveValue::CSS_PX);
244    button->setInlineStyleProperty(CSSPropertyWidth, buttonWidth, CSSPrimitiveValue::CSS_PX);
245    button->setInlineStyleProperty(CSSPropertyHeight, buttonHeight, CSSPrimitiveValue::CSS_PX);
246    button->setInlineStyleProperty(CSSPropertyVisibility, CSSValueVisible);
247
248    RefPtr<Image> buttonImage;
249    if (m_target->document().deviceScaleFactor() >= 2)
250        buttonImage = Image::loadPlatformResource("deleteButton@2x");
251    else
252        buttonImage = Image::loadPlatformResource("deleteButton");
253
254    if (buttonImage->isNull())
255        return;
256
257    button->setCachedImage(new CachedImage(buttonImage.get(), m_frame.page()->sessionID()));
258
259    container->appendChild(button.get(), ec);
260    ASSERT(!ec);
261    if (ec)
262        return;
263
264    m_containerElement = container.release();
265    m_outlineElement = outline.release();
266    m_buttonElement = button.release();
267}
268
269void DeleteButtonController::show(HTMLElement* element)
270{
271    hide();
272
273    if (!enabled() || !element || !element->inDocument() || !isDeletableElement(element))
274        return;
275
276    EditorClient* client = m_frame.editor().client();
277    if (!client || !client->shouldShowDeleteInterface(element))
278        return;
279
280    // we rely on the renderer having current information, so we should update the layout if needed
281    m_frame.document()->updateLayoutIgnorePendingStylesheets();
282
283    m_target = element;
284
285    if (!m_containerElement) {
286        createDeletionUI();
287        if (!m_containerElement) {
288            hide();
289            return;
290        }
291    }
292
293    ExceptionCode ec = 0;
294    m_target->appendChild(m_containerElement.get(), ec);
295    ASSERT(!ec);
296    if (ec) {
297        hide();
298        return;
299    }
300
301    if (m_target->renderer()->style().position() == StaticPosition) {
302        m_target->setInlineStyleProperty(CSSPropertyPosition, CSSValueRelative);
303        m_wasStaticPositioned = true;
304    }
305
306    if (m_target->renderer()->style().hasAutoZIndex()) {
307        m_target->setInlineStyleProperty(CSSPropertyZIndex, ASCIILiteral("0"));
308        m_wasAutoZIndex = true;
309    }
310}
311
312void DeleteButtonController::hide()
313{
314    m_outlineElement = 0;
315    m_buttonElement = 0;
316
317    if (m_containerElement && m_containerElement->parentNode())
318        m_containerElement->parentNode()->removeChild(m_containerElement.get(), IGNORE_EXCEPTION);
319
320    if (m_target) {
321        if (m_wasStaticPositioned)
322            m_target->setInlineStyleProperty(CSSPropertyPosition, CSSValueStatic);
323        if (m_wasAutoZIndex)
324            m_target->setInlineStyleProperty(CSSPropertyZIndex, CSSValueAuto);
325    }
326
327    m_wasStaticPositioned = false;
328    m_wasAutoZIndex = false;
329}
330
331void DeleteButtonController::enable()
332{
333#if !PLATFORM(IOS)
334    ASSERT(m_disableStack > 0);
335    if (m_disableStack > 0)
336        m_disableStack--;
337    if (enabled()) {
338        // Determining if the element is deletable currently depends on style
339        // because whether something is editable depends on style, so we need
340        // to recalculate style before calling enclosingDeletableElement.
341        m_frame.document()->updateStyleIfNeeded();
342        show(enclosingDeletableElement(m_frame.selection().selection()));
343    }
344#endif
345}
346
347void DeleteButtonController::disable()
348{
349#if !PLATFORM(IOS)
350    if (enabled())
351        hide();
352    m_disableStack++;
353#endif
354}
355
356class RemoveTargetCommand : public CompositeEditCommand {
357public:
358    static PassRefPtr<RemoveTargetCommand> create(Document& document, PassRefPtr<Node> target)
359    {
360        return adoptRef(new RemoveTargetCommand(document, target));
361    }
362
363private:
364    RemoveTargetCommand(Document& document, PassRefPtr<Node> target)
365        : CompositeEditCommand(document)
366        , m_target(target)
367    { }
368
369    void doApply()
370    {
371        removeNode(m_target);
372    }
373
374private:
375    RefPtr<Node> m_target;
376};
377
378void DeleteButtonController::deleteTarget()
379{
380    if (!enabled() || !m_target)
381        return;
382
383    hide();
384
385    // Because the deletion UI only appears when the selection is entirely
386    // within the target, we unconditionally update the selection to be
387    // a caret where the target had been.
388    Position pos = positionInParentBeforeNode(m_target.get());
389    ASSERT(m_frame.document());
390    applyCommand(RemoveTargetCommand::create(*m_frame.document(), m_target));
391    m_frame.selection().setSelection(VisiblePosition(pos));
392}
393#endif
394
395} // namespace WebCore
396