It should be possible to open a page preview by clicking on it
[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 "RenderTable.h"
43 #include "RenderTableCell.h"
44 #include "StyleProperties.h"
45
46 namespace WebCore {
47
48 using namespace HTMLNames;
49
50 #if ENABLE(DELETION_UI)
51
52 const char* const DeleteButtonController::containerElementIdentifier = "WebKit-Editing-Delete-Container";
53 const char* const DeleteButtonController::buttonElementIdentifier = "WebKit-Editing-Delete-Button";
54 const char* const DeleteButtonController::outlineElementIdentifier = "WebKit-Editing-Delete-Outline";
55
56 DeleteButtonController::DeleteButtonController(Frame& frame)
57     : m_frame(frame)
58     , m_wasStaticPositioned(false)
59     , m_wasAutoZIndex(false)
60     , m_disableStack(0)
61 {
62 }
63
64 static bool isDeletableElement(const Node* node)
65 {
66     if (!node || !node->isHTMLElement() || !node->inDocument() || !node->hasEditableStyle())
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 (!is<RenderBox>(renderer))
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 = downcast<RenderBox>(*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 (is<RenderTable>(box))
101         return true;
102
103     if (is<HTMLUListElement>(*node) || is<HTMLOListElement>(*node) || is<HTMLIFrameElement>(*node))
104         return true;
105
106     if (box.isOutOfFlowPositioned())
107         return true;
108
109     if (is<RenderBlock>(box) && !is<RenderTableCell>(box)) {
110         const RenderStyle& style = box.style();
111
112         // Allow blocks that have background images
113         if (style.hasBackgroundImage()) {
114             for (const FillLayer* background = style.backgroundLayers(); background; background = background->next()) {
115                 if (background->image() && background->image()->canRender(&box, 1))
116                     return true;
117             }
118         }
119
120         // Allow blocks with a minimum number of non-transparent borders
121         unsigned visibleBorders = style.borderTop().isVisible() + style.borderBottom().isVisible() + style.borderLeft().isVisible() + style.borderRight().isVisible();
122         if (visibleBorders >= minimumVisibleBorders)
123             return true;
124
125         // Allow blocks that have a different background from it's parent
126         ContainerNode* parentNode = node->parentNode();
127         if (!parentNode)
128             return false;
129
130         auto* parentRenderer = parentNode->renderer();
131         if (!parentRenderer)
132             return false;
133
134         const RenderStyle& parentStyle = parentRenderer->style();
135
136         if (box.hasBackground() && (!parentRenderer->hasBackground() || style.visitedDependentColor(CSSPropertyBackgroundColor) != parentStyle.visitedDependentColor(CSSPropertyBackgroundColor)))
137             return true;
138     }
139
140     return false;
141 }
142
143 static HTMLElement* enclosingDeletableElement(const VisibleSelection& selection)
144 {
145     if (!selection.isContentEditable())
146         return 0;
147
148     RefPtr<Range> range = selection.toNormalizedRange();
149     if (!range)
150         return nullptr;
151
152     Node* container = range->commonAncestorContainer(ASSERT_NO_EXCEPTION);
153     ASSERT(container);
154
155     // The enclosingNodeOfType function only works on nodes that are editable
156     // and capable of having editing positions inside them (which is strange, given its name).
157     if (!container->hasEditableStyle() || editingIgnoresContent(container))
158         return nullptr;
159
160     Node* element = enclosingNodeOfType(firstPositionInNode(container), &isDeletableElement);
161     return is<HTMLElement>(element) ? downcast<HTMLElement>(element) : nullptr;
162 }
163
164 void DeleteButtonController::respondToChangedSelection(const VisibleSelection& oldSelection)
165 {
166     if (!enabled())
167         return;
168
169     HTMLElement* oldElement = enclosingDeletableElement(oldSelection);
170     HTMLElement* newElement = enclosingDeletableElement(m_frame.selection().selection());
171     if (oldElement == newElement)
172         return;
173
174     // If the base is inside a deletable element, give the element a delete widget.
175     if (newElement)
176         show(newElement);
177     else
178         hide();
179 }
180
181 void DeleteButtonController::deviceScaleFactorChanged()
182 {
183     if (!enabled())
184         return;
185     
186     HTMLElement* currentTarget = m_target.get();
187     hide();
188
189     // Setting m_containerElement to 0 will force the deletionUI to be re-created with
190     // artwork of the appropriate resolution in show().
191     m_containerElement = 0;
192     show(currentTarget);
193 }
194
195 void DeleteButtonController::createDeletionUI()
196 {
197     RefPtr<HTMLDivElement> container = HTMLDivElement::create(m_target->document());
198     container->setIdAttribute(containerElementIdentifier);
199
200     container->setInlineStyleProperty(CSSPropertyWebkitUserDrag, CSSValueNone);
201     container->setInlineStyleProperty(CSSPropertyWebkitUserSelect, CSSValueNone);
202     container->setInlineStyleProperty(CSSPropertyWebkitUserModify, CSSValueReadOnly);
203     container->setInlineStyleProperty(CSSPropertyVisibility, CSSValueHidden);
204     container->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
205     container->setInlineStyleProperty(CSSPropertyCursor, CSSValueDefault);
206     container->setInlineStyleProperty(CSSPropertyTop, 0, CSSPrimitiveValue::CSS_PX);
207     container->setInlineStyleProperty(CSSPropertyRight, 0, CSSPrimitiveValue::CSS_PX);
208     container->setInlineStyleProperty(CSSPropertyBottom, 0, CSSPrimitiveValue::CSS_PX);
209     container->setInlineStyleProperty(CSSPropertyLeft, 0, CSSPrimitiveValue::CSS_PX);
210
211     RefPtr<HTMLDivElement> outline = HTMLDivElement::create(m_target->document());
212     outline->setIdAttribute(outlineElementIdentifier);
213
214     const int borderWidth = 4;
215     const int borderRadius = 6;
216
217     outline->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
218     outline->setInlineStyleProperty(CSSPropertyZIndex, ASCIILiteral("-1000000"));
219     outline->setInlineStyleProperty(CSSPropertyTop, -borderWidth - m_target->renderBox()->borderTop(), CSSPrimitiveValue::CSS_PX);
220     outline->setInlineStyleProperty(CSSPropertyRight, -borderWidth - m_target->renderBox()->borderRight(), CSSPrimitiveValue::CSS_PX);
221     outline->setInlineStyleProperty(CSSPropertyBottom, -borderWidth - m_target->renderBox()->borderBottom(), CSSPrimitiveValue::CSS_PX);
222     outline->setInlineStyleProperty(CSSPropertyLeft, -borderWidth - m_target->renderBox()->borderLeft(), CSSPrimitiveValue::CSS_PX);
223     outline->setInlineStyleProperty(CSSPropertyBorderWidth, borderWidth, CSSPrimitiveValue::CSS_PX);
224     outline->setInlineStyleProperty(CSSPropertyBorderStyle, CSSValueSolid);
225     outline->setInlineStyleProperty(CSSPropertyBorderColor, ASCIILiteral("rgba(0, 0, 0, 0.6)"));
226     outline->setInlineStyleProperty(CSSPropertyBorderRadius, borderRadius, CSSPrimitiveValue::CSS_PX);
227     outline->setInlineStyleProperty(CSSPropertyVisibility, CSSValueVisible);
228
229     ExceptionCode ec = 0;
230     container->appendChild(outline.get(), ec);
231     ASSERT(!ec);
232     if (ec)
233         return;
234
235     RefPtr<DeleteButton> button = DeleteButton::create(m_target->document());
236     button->setIdAttribute(buttonElementIdentifier);
237
238     const int buttonWidth = 30;
239     const int buttonHeight = 30;
240     const int buttonBottomShadowOffset = 2;
241
242     button->setInlineStyleProperty(CSSPropertyPosition, CSSValueAbsolute);
243     button->setInlineStyleProperty(CSSPropertyZIndex, ASCIILiteral("1000000"));
244     button->setInlineStyleProperty(CSSPropertyTop, (-buttonHeight / 2) - m_target->renderBox()->borderTop() - (borderWidth / 2) + buttonBottomShadowOffset, CSSPrimitiveValue::CSS_PX);
245     button->setInlineStyleProperty(CSSPropertyLeft, (-buttonWidth / 2) - m_target->renderBox()->borderLeft() - (borderWidth / 2), CSSPrimitiveValue::CSS_PX);
246     button->setInlineStyleProperty(CSSPropertyWidth, buttonWidth, CSSPrimitiveValue::CSS_PX);
247     button->setInlineStyleProperty(CSSPropertyHeight, buttonHeight, CSSPrimitiveValue::CSS_PX);
248     button->setInlineStyleProperty(CSSPropertyVisibility, CSSValueVisible);
249
250     RefPtr<Image> buttonImage;
251     if (m_target->document().deviceScaleFactor() >= 2)
252         buttonImage = Image::loadPlatformResource("deleteButton@2x");
253     else
254         buttonImage = Image::loadPlatformResource("deleteButton");
255
256     if (buttonImage->isNull())
257         return;
258
259     button->setCachedImage(new CachedImage(buttonImage.get(), m_frame.page()->sessionID()));
260
261     container->appendChild(button.get(), ec);
262     ASSERT(!ec);
263     if (ec)
264         return;
265
266     m_containerElement = container.release();
267     m_outlineElement = outline.release();
268     m_buttonElement = button.release();
269 }
270
271 void DeleteButtonController::show(HTMLElement* element)
272 {
273     hide();
274
275     if (!enabled() || !element || !element->inDocument() || !isDeletableElement(element))
276         return;
277
278     EditorClient* client = m_frame.editor().client();
279     if (!client || !client->shouldShowDeleteInterface(element))
280         return;
281
282     // we rely on the renderer having current information, so we should update the layout if needed
283     m_frame.document()->updateLayoutIgnorePendingStylesheets();
284
285     m_target = element;
286
287     if (!m_containerElement) {
288         createDeletionUI();
289         if (!m_containerElement) {
290             hide();
291             return;
292         }
293     }
294
295     ExceptionCode ec = 0;
296     m_target->appendChild(m_containerElement.get(), ec);
297     ASSERT(!ec);
298     if (ec) {
299         hide();
300         return;
301     }
302
303     if (m_target->renderer()->style().position() == StaticPosition) {
304         m_target->setInlineStyleProperty(CSSPropertyPosition, CSSValueRelative);
305         m_wasStaticPositioned = true;
306     }
307
308     if (m_target->renderer()->style().hasAutoZIndex()) {
309         m_target->setInlineStyleProperty(CSSPropertyZIndex, ASCIILiteral("0"));
310         m_wasAutoZIndex = true;
311     }
312 }
313
314 void DeleteButtonController::hide()
315 {
316     m_outlineElement = 0;
317     m_buttonElement = 0;
318
319     if (m_containerElement && m_containerElement->parentNode())
320         m_containerElement->parentNode()->removeChild(m_containerElement.get(), IGNORE_EXCEPTION);
321
322     if (m_target) {
323         if (m_wasStaticPositioned)
324             m_target->setInlineStyleProperty(CSSPropertyPosition, CSSValueStatic);
325         if (m_wasAutoZIndex)
326             m_target->setInlineStyleProperty(CSSPropertyZIndex, CSSValueAuto);
327     }
328
329     m_wasStaticPositioned = false;
330     m_wasAutoZIndex = false;
331 }
332
333 void DeleteButtonController::enable()
334 {
335     ASSERT(m_disableStack > 0);
336     if (m_disableStack > 0)
337         m_disableStack--;
338     if (enabled()) {
339         // Determining if the element is deletable currently depends on style
340         // because whether something is editable depends on style, so we need
341         // to recalculate style before calling enclosingDeletableElement.
342         m_frame.document()->updateStyleIfNeeded();
343         show(enclosingDeletableElement(m_frame.selection().selection()));
344     }
345 }
346
347 void DeleteButtonController::disable()
348 {
349     if (enabled())
350         hide();
351     m_disableStack++;
352 }
353
354 class RemoveTargetCommand : public CompositeEditCommand {
355 public:
356     static PassRefPtr<RemoveTargetCommand> create(Document& document, PassRefPtr<Node> target)
357     {
358         return adoptRef(new RemoveTargetCommand(document, target));
359     }
360
361 private:
362     RemoveTargetCommand(Document& document, PassRefPtr<Node> target)
363         : CompositeEditCommand(document)
364         , m_target(target)
365     { }
366
367     void doApply()
368     {
369         removeNode(m_target);
370     }
371
372 private:
373     RefPtr<Node> m_target;
374 };
375
376 void DeleteButtonController::deleteTarget()
377 {
378     if (!enabled() || !m_target)
379         return;
380
381     hide();
382
383     // Because the deletion UI only appears when the selection is entirely
384     // within the target, we unconditionally update the selection to be
385     // a caret where the target had been.
386     Position pos = positionInParentBeforeNode(m_target.get());
387     ASSERT(m_frame.document());
388     applyCommand(RemoveTargetCommand::create(*m_frame.document(), m_target));
389     m_frame.selection().setSelection(VisiblePosition(pos));
390 }
391 #endif
392
393 } // namespace WebCore