Home | History | Annotate | Download | only in editing
      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->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 
    147 static 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 
    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, 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 
    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(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 
    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