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 (style->hasBackground() && (!parentStyle->hasBackground() || style->backgroundColor() != parentStyle->backgroundColor())) 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 = new HTMLDivElement(divTag, m_target->document()); 190 container->setAttribute(container->idAttributeName(), 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 = new HTMLDivElement(divTag, m_target->document()); 205 outline->setAttribute(outline->idAttributeName(), 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 = new DeleteButton(m_target->document()); 228 button->setAttribute(button->idAttributeName(), 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 359