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