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