DeleteButtonController.cpp revision 2daae5fd11344eaa88a0d92b0f6d65f8d2255c00
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 COMPUTER, 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 COMPUTER, 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 "CSSMutableStyleDeclaration.h"
31#include "CSSPrimitiveValue.h"
32#include "CSSPropertyNames.h"
33#include "CSSValueKeywords.h"
34#include "DeleteButton.h"
35#include "Document.h"
36#include "Editor.h"
37#include "Frame.h"
38#include "htmlediting.h"
39#include "HTMLDivElement.h"
40#include "HTMLNames.h"
41#include "Image.h"
42#include "Node.h"
43#include "Range.h"
44#include "RemoveNodeCommand.h"
45#include "RenderBox.h"
46#include "SelectionController.h"
47
48namespace WebCore {
49
50using namespace HTMLNames;
51
52const char* const DeleteButtonController::containerElementIdentifier = "WebKit-Editing-Delete-Container";
53const char* const DeleteButtonController::buttonElementIdentifier = "WebKit-Editing-Delete-Button";
54const char* const DeleteButtonController::outlineElementIdentifier = "WebKit-Editing-Delete-Outline";
55
56DeleteButtonController::DeleteButtonController(Frame* frame)
57    : m_frame(frame)
58    , m_wasStaticPositioned(false)
59    , m_wasAutoZIndex(false)
60    , m_disableStack(0)
61{
62}
63
64static bool isDeletableElement(const Node* node)
65{
66    if (!node || !node->isHTMLElement() || !node->inDocument() || !node->rendererIsEditable())
67        return false;
68
69    // In general we want to only draw the UI around object of a certain area, but we still keep the min width/height to
70    // make sure we don't end up with very thin or very short elements getting the UI.
71    const int minimumArea = 2500;
72    const int minimumWidth = 48;
73    const int minimumHeight = 16;
74    const unsigned minimumVisibleBorders = 1;
75
76    RenderObject* renderer = node->renderer();
77    if (!renderer || !renderer->isBox())
78        return false;
79
80    // Disallow the body element since it isn't practical to delete, and the deletion UI would be clipped.
81    if (node->hasTagName(bodyTag))
82        return false;
83
84    // Disallow elements with any overflow clip, since the deletion UI would be clipped as well. <rdar://problem/6840161>
85    if (renderer->hasOverflowClip())
86        return false;
87
88    // Disallow Mail blockquotes since the deletion UI would get in the way of editing for these.
89    if (isMailBlockquote(node))
90        return false;
91
92    RenderBox* box = toRenderBox(renderer);
93    IntRect borderBoundingBox = box->borderBoundingBox();
94    if (borderBoundingBox.width() < minimumWidth || borderBoundingBox.height() < minimumHeight)
95        return false;
96
97    if ((borderBoundingBox.width() * borderBoundingBox.height()) < minimumArea)
98        return false;
99
100    if (renderer->isTable())
101        return true;
102
103    if (node->hasTagName(ulTag) || node->hasTagName(olTag) || node->hasTagName(iframeTag))
104        return true;
105
106    if (renderer->isPositioned())
107        return true;
108
109    if (renderer->isRenderBlock() && !renderer->isTableCell()) {
110        RenderStyle* style = renderer->style();
111        if (!style)
112            return false;
113
114        // Allow blocks that have background images
115        if (style->hasBackgroundImage()) {
116            for (const FillLayer* background = style->backgroundLayers(); background; background = background->next()) {
117                if (background->image() && background->image()->canRender(1))
118                    return true;
119            }
120        }
121
122        // Allow blocks with a minimum number of non-transparent borders
123        unsigned visibleBorders = style->borderTop().isVisible() + style->borderBottom().isVisible() + style->borderLeft().isVisible() + style->borderRight().isVisible();
124        if (visibleBorders >= minimumVisibleBorders)
125            return true;
126
127        // Allow blocks that have a different background from it's parent
128        ContainerNode* parentNode = node->parentNode();
129        if (!parentNode)
130            return false;
131
132        RenderObject* parentRenderer = parentNode->renderer();
133        if (!parentRenderer)
134            return false;
135
136        RenderStyle* parentStyle = parentRenderer->style();
137        if (!parentStyle)
138            return false;
139
140        if (renderer->hasBackground() && (!parentRenderer->hasBackground() || style->visitedDependentColor(CSSPropertyBackgroundColor) != parentStyle->visitedDependentColor(CSSPropertyBackgroundColor)))
141            return true;
142    }
143
144    return false;
145}
146
147static HTMLElement* enclosingDeletableElement(const VisibleSelection& selection)
148{
149    if (!selection.isContentEditable())
150        return 0;
151
152    RefPtr<Range> range = selection.toNormalizedRange();
153    if (!range)
154        return 0;
155
156    ExceptionCode ec = 0;
157    Node* container = range->commonAncestorContainer(ec);
158    ASSERT(container);
159    ASSERT(ec == 0);
160
161    // The enclosingNodeOfType function only works on nodes that are editable
162    // (which is strange, given its name).
163    if (!container->rendererIsEditable())
164        return 0;
165
166    Node* element = enclosingNodeOfType(firstPositionInNode(container), &isDeletableElement);
167    return element && element->isHTMLElement() ? toHTMLElement(element) : 0;
168}
169
170void DeleteButtonController::respondToChangedSelection(const VisibleSelection& oldSelection)
171{
172    if (!enabled())
173        return;
174
175    HTMLElement* oldElement = enclosingDeletableElement(oldSelection);
176    HTMLElement* newElement = enclosingDeletableElement(m_frame->selection()->selection());
177    if (oldElement == newElement)
178        return;
179
180    // If the base is inside a deletable element, give the element a delete widget.
181    if (newElement)
182        show(newElement);
183    else
184        hide();
185}
186
187void DeleteButtonController::createDeletionUI()
188{
189    RefPtr<HTMLDivElement> container = HTMLDivElement::create(m_target->document());
190    container->setIdAttribute(containerElementIdentifier);
191
192    CSSMutableStyleDeclaration* style = container->getInlineStyleDecl();
193    style->setProperty(CSSPropertyWebkitUserDrag, CSSValueNone);
194    style->setProperty(CSSPropertyWebkitUserSelect, CSSValueNone);
195    style->setProperty(CSSPropertyWebkitUserModify, CSSValueReadOnly);
196    style->setProperty(CSSPropertyVisibility, CSSValueHidden);
197    style->setProperty(CSSPropertyPosition, CSSValueAbsolute);
198    style->setProperty(CSSPropertyCursor, CSSValueDefault);
199    style->setProperty(CSSPropertyTop, "0");
200    style->setProperty(CSSPropertyRight, "0");
201    style->setProperty(CSSPropertyBottom, "0");
202    style->setProperty(CSSPropertyLeft, "0");
203
204    RefPtr<HTMLDivElement> outline = HTMLDivElement::create(m_target->document());
205    outline->setIdAttribute(outlineElementIdentifier);
206
207    const int borderWidth = 4;
208    const int borderRadius = 6;
209
210    style = outline->getInlineStyleDecl();
211    style->setProperty(CSSPropertyPosition, CSSValueAbsolute);
212    style->setProperty(CSSPropertyZIndex, String::number(-1000000));
213    style->setProperty(CSSPropertyTop, String::number(-borderWidth - m_target->renderBox()->borderTop()) + "px");
214    style->setProperty(CSSPropertyRight, String::number(-borderWidth - m_target->renderBox()->borderRight()) + "px");
215    style->setProperty(CSSPropertyBottom, String::number(-borderWidth - m_target->renderBox()->borderBottom()) + "px");
216    style->setProperty(CSSPropertyLeft, String::number(-borderWidth - m_target->renderBox()->borderLeft()) + "px");
217    style->setProperty(CSSPropertyBorder, String::number(borderWidth) + "px solid rgba(0, 0, 0, 0.6)");
218    style->setProperty(CSSPropertyWebkitBorderRadius, String::number(borderRadius) + "px");
219    style->setProperty(CSSPropertyVisibility, CSSValueVisible);
220
221    ExceptionCode ec = 0;
222    container->appendChild(outline.get(), ec);
223    ASSERT(ec == 0);
224    if (ec)
225        return;
226
227    RefPtr<DeleteButton> button = DeleteButton::create(m_target->document());
228    button->setIdAttribute(buttonElementIdentifier);
229
230    const int buttonWidth = 30;
231    const int buttonHeight = 30;
232    const int buttonBottomShadowOffset = 2;
233
234    style = button->getInlineStyleDecl();
235    style->setProperty(CSSPropertyPosition, CSSValueAbsolute);
236    style->setProperty(CSSPropertyZIndex, String::number(1000000));
237    style->setProperty(CSSPropertyTop, String::number((-buttonHeight / 2) - m_target->renderBox()->borderTop() - (borderWidth / 2) + buttonBottomShadowOffset) + "px");
238    style->setProperty(CSSPropertyLeft, String::number((-buttonWidth / 2) - m_target->renderBox()->borderLeft() - (borderWidth / 2)) + "px");
239    style->setProperty(CSSPropertyWidth, String::number(buttonWidth) + "px");
240    style->setProperty(CSSPropertyHeight, String::number(buttonHeight) + "px");
241    style->setProperty(CSSPropertyVisibility, CSSValueVisible);
242
243    RefPtr<Image> buttonImage = Image::loadPlatformResource("deleteButton");
244    if (buttonImage->isNull())
245        return;
246
247    button->setCachedImage(new CachedImage(buttonImage.get()));
248
249    container->appendChild(button.get(), ec);
250    ASSERT(ec == 0);
251    if (ec)
252        return;
253
254    m_containerElement = container.release();
255    m_outlineElement = outline.release();
256    m_buttonElement = button.release();
257}
258
259void DeleteButtonController::show(HTMLElement* element)
260{
261    hide();
262
263    if (!enabled() || !element || !element->inDocument() || !isDeletableElement(element))
264        return;
265
266    if (!m_frame->editor()->shouldShowDeleteInterface(toHTMLElement(element)))
267        return;
268
269    // we rely on the renderer having current information, so we should update the layout if needed
270    m_frame->document()->updateLayoutIgnorePendingStylesheets();
271
272    m_target = element;
273
274    if (!m_containerElement) {
275        createDeletionUI();
276        if (!m_containerElement) {
277            hide();
278            return;
279        }
280    }
281
282    ExceptionCode ec = 0;
283    m_target->appendChild(m_containerElement.get(), ec);
284    ASSERT(ec == 0);
285    if (ec) {
286        hide();
287        return;
288    }
289
290    if (m_target->renderer()->style()->position() == StaticPosition) {
291        m_target->getInlineStyleDecl()->setProperty(CSSPropertyPosition, CSSValueRelative);
292        m_wasStaticPositioned = true;
293    }
294
295    if (m_target->renderer()->style()->hasAutoZIndex()) {
296        m_target->getInlineStyleDecl()->setProperty(CSSPropertyZIndex, "0");
297        m_wasAutoZIndex = true;
298    }
299}
300
301void DeleteButtonController::hide()
302{
303    m_outlineElement = 0;
304    m_buttonElement = 0;
305
306    ExceptionCode ec = 0;
307    if (m_containerElement && m_containerElement->parentNode())
308        m_containerElement->parentNode()->removeChild(m_containerElement.get(), ec);
309
310    if (m_target) {
311        if (m_wasStaticPositioned)
312            m_target->getInlineStyleDecl()->setProperty(CSSPropertyPosition, CSSValueStatic);
313        if (m_wasAutoZIndex)
314            m_target->getInlineStyleDecl()->setProperty(CSSPropertyZIndex, CSSValueAuto);
315    }
316
317    m_wasStaticPositioned = false;
318    m_wasAutoZIndex = false;
319}
320
321void DeleteButtonController::enable()
322{
323    ASSERT(m_disableStack > 0);
324    if (m_disableStack > 0)
325        m_disableStack--;
326    if (enabled()) {
327        // Determining if the element is deletable currently depends on style
328        // because whether something is editable depends on style, so we need
329        // to recalculate style before calling enclosingDeletableElement.
330        m_frame->document()->updateStyleIfNeeded();
331        show(enclosingDeletableElement(m_frame->selection()->selection()));
332    }
333}
334
335void DeleteButtonController::disable()
336{
337    if (enabled())
338        hide();
339    m_disableStack++;
340}
341
342void DeleteButtonController::deleteTarget()
343{
344    if (!enabled() || !m_target)
345        return;
346
347    RefPtr<Node> element = m_target;
348    hide();
349
350    // Because the deletion UI only appears when the selection is entirely
351    // within the target, we unconditionally update the selection to be
352    // a caret where the target had been.
353    Position pos = positionInParentBeforeNode(element.get());
354    applyCommand(RemoveNodeCommand::create(element.release()));
355    m_frame->selection()->setSelection(VisiblePosition(pos));
356}
357
358} // namespace WebCore
359