90a1efe77a65c48d0f2eeb4e48e956fcf974ded2
[WebKit-https.git] / Source / WebCore / editing / DeleteButtonController.cpp
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
44 namespace WebCore {
45
46 using namespace HTMLNames;
47
48 #if ENABLE(DELETION_UI)
49
50 const char* const DeleteButtonController::containerElementIdentifier = "WebKit-Editing-Delete-Container";
51 const char* const DeleteButtonController::buttonElementIdentifier = "WebKit-Editing-Delete-Button";
52 const char* const DeleteButtonController::outlineElementIdentifier = "WebKit-Editing-Delete-Outline";
53
54 DeleteButtonController::DeleteButtonController(Frame& frame)
55     : m_frame(frame)
56     , m_wasStaticPositioned(false)
57     , m_wasAutoZIndex(false)
58     , m_disableStack(0)
59 {
60 }
61
62 static 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
141 static 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 && is<HTMLElement>(element) ? downcast<HTMLElement>(element) : nullptr;
160 }
161
162 void 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
179 void 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
193 void 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
269 void 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
312 void 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
331 void DeleteButtonController::enable()
332 {
333     ASSERT(m_disableStack > 0);
334     if (m_disableStack > 0)
335         m_disableStack--;
336     if (enabled()) {
337         // Determining if the element is deletable currently depends on style
338         // because whether something is editable depends on style, so we need
339         // to recalculate style before calling enclosingDeletableElement.
340         m_frame.document()->updateStyleIfNeeded();
341         show(enclosingDeletableElement(m_frame.selection().selection()));
342     }
343 }
344
345 void DeleteButtonController::disable()
346 {
347     if (enabled())
348         hide();
349     m_disableStack++;
350 }
351
352 class RemoveTargetCommand : public CompositeEditCommand {
353 public:
354     static PassRefPtr<RemoveTargetCommand> create(Document& document, PassRefPtr<Node> target)
355     {
356         return adoptRef(new RemoveTargetCommand(document, target));
357     }
358
359 private:
360     RemoveTargetCommand(Document& document, PassRefPtr<Node> target)
361         : CompositeEditCommand(document)
362         , m_target(target)
363     { }
364
365     void doApply()
366     {
367         removeNode(m_target);
368     }
369
370 private:
371     RefPtr<Node> m_target;
372 };
373
374 void DeleteButtonController::deleteTarget()
375 {
376     if (!enabled() || !m_target)
377         return;
378
379     hide();
380
381     // Because the deletion UI only appears when the selection is entirely
382     // within the target, we unconditionally update the selection to be
383     // a caret where the target had been.
384     Position pos = positionInParentBeforeNode(m_target.get());
385     ASSERT(m_frame.document());
386     applyCommand(RemoveTargetCommand::create(*m_frame.document(), m_target));
387     m_frame.selection().setSelection(VisiblePosition(pos));
388 }
389 #endif
390
391 } // namespace WebCore