|
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 |
|
48 namespace WebCore { |
|
49 |
|
50 using namespace HTMLNames; |
|
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->isContentEditable()) |
|
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() && style->backgroundImage()->canRender(1.0f)) |
|
116 return true; |
|
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 Node* parentNode = node->parentNode(); |
|
125 if (!parentNode) |
|
126 return false; |
|
127 |
|
128 RenderObject* parentRenderer = parentNode->renderer(); |
|
129 if (!parentRenderer) |
|
130 return false; |
|
131 |
|
132 RenderStyle* parentStyle = parentRenderer->style(); |
|
133 if (!parentStyle) |
|
134 return false; |
|
135 |
|
136 if (renderer->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 0; |
|
151 |
|
152 ExceptionCode ec = 0; |
|
153 Node* container = range->commonAncestorContainer(ec); |
|
154 ASSERT(container); |
|
155 ASSERT(ec == 0); |
|
156 |
|
157 // The enclosingNodeOfType function only works on nodes that are editable |
|
158 // (which is strange, given its name). |
|
159 if (!container->isContentEditable()) |
|
160 return 0; |
|
161 |
|
162 Node* element = enclosingNodeOfType(Position(container, 0), &isDeletableElement); |
|
163 if (!element) |
|
164 return 0; |
|
165 |
|
166 ASSERT(element->isHTMLElement()); |
|
167 return static_cast<HTMLElement*>(element); |
|
168 } |
|
169 |
|
170 void 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 |
|
187 void 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, CSSValueNone); |
|
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 |
|
259 void 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(static_cast<HTMLElement*>(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 |
|
301 void 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 |
|
321 void 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 |
|
335 void DeleteButtonController::disable() |
|
336 { |
|
337 if (enabled()) |
|
338 hide(); |
|
339 m_disableStack++; |
|
340 } |
|
341 |
|
342 void 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 |