DeleteButtonController.cpp 10.6 KB
Newer Older
thatcher's avatar
thatcher committed
1
/*
darin@apple.com's avatar
darin@apple.com committed
2
 * Copyright (C) 2006, 2008 Apple Inc. All rights reserved.
thatcher's avatar
thatcher committed
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#include "config.h"
#include "DeleteButtonController.h"

#include "CachedImage.h"
#include "CSSMutableStyleDeclaration.h"
#include "CSSPrimitiveValue.h"
#include "CSSPropertyNames.h"
#include "CSSValueKeywords.h"
#include "DeleteButton.h"
#include "Document.h"
#include "Editor.h"
#include "Frame.h"
#include "htmlediting.h"
#include "HTMLDivElement.h"
#include "HTMLNames.h"
#include "Image.h"
#include "Node.h"
#include "Range.h"
#include "RemoveNodeCommand.h"
#include "RenderObject.h"
#include "SelectionController.h"

namespace WebCore {

using namespace HTMLNames;

const char* const DeleteButtonController::containerElementIdentifier = "WebKit-Editing-Delete-Container";
const char* const DeleteButtonController::buttonElementIdentifier = "WebKit-Editing-Delete-Button";
const char* const DeleteButtonController::outlineElementIdentifier = "WebKit-Editing-Delete-Outline";

darin's avatar
darin committed
56 57
DeleteButtonController::DeleteButtonController(Frame* frame)
    : m_frame(frame)
thatcher's avatar
thatcher committed
58 59
    , m_wasStaticPositioned(false)
    , m_wasAutoZIndex(false)
thatcher's avatar
thatcher committed
60
    , m_disableStack(0)
thatcher's avatar
thatcher committed
61 62 63
{
}

justing's avatar
justing committed
64
static bool isDeletableElement(const Node* node)
thatcher's avatar
thatcher committed
65
{
thatcher's avatar
thatcher committed
66
    if (!node || !node->isHTMLElement() || !node->inDocument() || !node->isContentEditable())
thatcher's avatar
thatcher committed
67 68
        return false;

thatcher's avatar
thatcher committed
69 70
    const int minimumWidth = 25;
    const int minimumHeight = 25;
thatcher's avatar
thatcher committed
71 72
    const unsigned minimumVisibleBorders = 3;

thatcher's avatar
thatcher committed
73
    RenderObject* renderer = node->renderer();
thatcher's avatar
thatcher committed
74
    if (!renderer || renderer->width() < minimumWidth || renderer->height() < minimumHeight)
thatcher's avatar
thatcher committed
75 76
        return false;

thatcher's avatar
thatcher committed
77 78 79 80 81 82 83
    if (renderer->isTable())
        return true;

    if (node->hasTagName(ulTag) || node->hasTagName(olTag))
        return true;

    if (renderer->isPositioned())
thatcher's avatar
thatcher committed
84 85
        return true;

thatcher's avatar
thatcher committed
86 87
    // allow block elements (excluding table cells) that have some non-transparent borders
    if (renderer->isRenderBlock() && !renderer->isTableCell()) {
thatcher's avatar
thatcher committed
88
        RenderStyle* style = renderer->style();
thatcher's avatar
thatcher committed
89
        if (style && style->hasBorder()) {
thatcher's avatar
thatcher committed
90
            unsigned visibleBorders = style->borderTop().isVisible() + style->borderBottom().isVisible() + style->borderLeft().isVisible() + style->borderRight().isVisible();
thatcher's avatar
thatcher committed
91 92 93
            if (visibleBorders >= minimumVisibleBorders)
                return true;
        }
thatcher's avatar
thatcher committed
94 95 96 97 98 99
    }

    return false;
}

static HTMLElement* enclosingDeletableElement(const Selection& selection)
thatcher's avatar
thatcher committed
100
{
darin's avatar
darin committed
101 102
    if (!selection.isContentEditable())
        return 0;
thatcher's avatar
thatcher committed
103

darin's avatar
darin committed
104 105 106
    RefPtr<Range> range = selection.toRange();
    if (!range)
        return 0;
thatcher's avatar
thatcher committed
107

darin's avatar
darin committed
108 109 110
    ExceptionCode ec = 0;
    Node* container = range->commonAncestorContainer(ec);
    ASSERT(container);
thatcher's avatar
thatcher committed
111 112
    ASSERT(ec == 0);

thatcher's avatar
thatcher committed
113
    // The enclosingNodeOfType function only works on nodes that are editable
darin's avatar
darin committed
114 115 116 117
    // (which is strange, given its name).
    if (!container->isContentEditable())
        return 0;

justin.garcia@apple.com's avatar
justin.garcia@apple.com committed
118
    Node* element = enclosingNodeOfType(Position(container, 0), &isDeletableElement);
thatcher's avatar
thatcher committed
119
    if (!element)
darin's avatar
darin committed
120 121
        return 0;

thatcher's avatar
thatcher committed
122 123
    ASSERT(element->isHTMLElement());
    return static_cast<HTMLElement*>(element);
thatcher's avatar
thatcher committed
124
}
darin's avatar
darin committed
125 126 127

void DeleteButtonController::respondToChangedSelection(const Selection& oldSelection)
{
thatcher's avatar
thatcher committed
128 129 130
    if (!enabled())
        return;

thatcher's avatar
thatcher committed
131
    HTMLElement* oldElement = enclosingDeletableElement(oldSelection);
darin@apple.com's avatar
darin@apple.com committed
132
    HTMLElement* newElement = enclosingDeletableElement(m_frame->selection()->selection());
thatcher's avatar
thatcher committed
133
    if (oldElement == newElement)
darin's avatar
darin committed
134 135
        return;

thatcher's avatar
thatcher committed
136 137 138
    // If the base is inside a deletable element, give the element a delete widget.
    if (newElement)
        show(newElement);
darin's avatar
darin committed
139 140 141 142
    else
        hide();
}

aliceli1's avatar
aliceli1 committed
143
void DeleteButtonController::createDeletionUI()
thatcher's avatar
thatcher committed
144
{
aliceli1's avatar
aliceli1 committed
145 146
    RefPtr<HTMLDivElement> container = new HTMLDivElement(m_target->document());
    container->setId(containerElementIdentifier);
thatcher's avatar
thatcher committed
147

aliceli1's avatar
aliceli1 committed
148
    CSSMutableStyleDeclaration* style = container->getInlineStyleDecl();
149 150 151
    style->setProperty(CSSPropertyWebkitUserDrag, CSSValueNone);
    style->setProperty(CSSPropertyWebkitUserSelect, CSSValueNone);
    style->setProperty(CSSPropertyWebkitUserModify, CSSValueNone);
thatcher's avatar
thatcher committed
152

aliceli1's avatar
aliceli1 committed
153 154
    RefPtr<HTMLDivElement> outline = new HTMLDivElement(m_target->document());
    outline->setId(outlineElementIdentifier);
thatcher's avatar
thatcher committed
155 156 157 158

    const int borderWidth = 4;
    const int borderRadius = 6;

aliceli1's avatar
aliceli1 committed
159
    style = outline->getInlineStyleDecl();
160 161 162 163 164 165 166 167 168 169 170 171
    style->setProperty(CSSPropertyPosition, CSSValueAbsolute);
    style->setProperty(CSSPropertyCursor, CSSValueDefault);
    style->setProperty(CSSPropertyWebkitUserDrag, CSSValueNone);
    style->setProperty(CSSPropertyWebkitUserSelect, CSSValueNone);
    style->setProperty(CSSPropertyWebkitUserModify, CSSValueNone);
    style->setProperty(CSSPropertyZIndex, String::number(-1000000));
    style->setProperty(CSSPropertyTop, String::number(-borderWidth - m_target->renderer()->borderTop()) + "px");
    style->setProperty(CSSPropertyRight, String::number(-borderWidth - m_target->renderer()->borderRight()) + "px");
    style->setProperty(CSSPropertyBottom, String::number(-borderWidth - m_target->renderer()->borderBottom()) + "px");
    style->setProperty(CSSPropertyLeft, String::number(-borderWidth - m_target->renderer()->borderLeft()) + "px");
    style->setProperty(CSSPropertyBorder, String::number(borderWidth) + "px solid rgba(0, 0, 0, 0.6)");
    style->setProperty(CSSPropertyWebkitBorderRadius, String::number(borderRadius) + "px");
thatcher's avatar
thatcher committed
172

aliceli1's avatar
aliceli1 committed
173 174
    ExceptionCode ec = 0;
    container->appendChild(outline.get(), ec);
thatcher's avatar
thatcher committed
175
    ASSERT(ec == 0);
aliceli1's avatar
aliceli1 committed
176
    if (ec)
thatcher's avatar
thatcher committed
177 178
        return;

aliceli1's avatar
aliceli1 committed
179 180
    RefPtr<DeleteButton> button = new DeleteButton(m_target->document());
    button->setId(buttonElementIdentifier);
thatcher's avatar
thatcher committed
181 182 183

    const int buttonWidth = 30;
    const int buttonHeight = 30;
thatcher's avatar
thatcher committed
184
    const int buttonBottomShadowOffset = 2;
thatcher's avatar
thatcher committed
185

aliceli1's avatar
aliceli1 committed
186
    style = button->getInlineStyleDecl();
187 188 189 190 191 192 193 194 195 196
    style->setProperty(CSSPropertyPosition, CSSValueAbsolute);
    style->setProperty(CSSPropertyCursor, CSSValueDefault);
    style->setProperty(CSSPropertyWebkitUserDrag, CSSValueNone);
    style->setProperty(CSSPropertyWebkitUserSelect, CSSValueNone);
    style->setProperty(CSSPropertyWebkitUserModify, CSSValueNone);
    style->setProperty(CSSPropertyZIndex, String::number(1000000));
    style->setProperty(CSSPropertyTop, String::number((-buttonHeight / 2) - m_target->renderer()->borderTop() - (borderWidth / 2) + buttonBottomShadowOffset) + "px");
    style->setProperty(CSSPropertyLeft, String::number((-buttonWidth / 2) - m_target->renderer()->borderLeft() - (borderWidth / 2)) + "px");
    style->setProperty(CSSPropertyWidth, String::number(buttonWidth) + "px");
    style->setProperty(CSSPropertyHeight, String::number(buttonHeight) + "px");
thatcher's avatar
thatcher committed
197

198 199 200 201 202
    Image* buttonImage = Image::loadPlatformResource("deleteButton");
    if (buttonImage->isNull())
        return;
    
    button->setCachedImage(new CachedImage(buttonImage));
thatcher's avatar
thatcher committed
203

aliceli1's avatar
aliceli1 committed
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
    container->appendChild(button.get(), ec);
    ASSERT(ec == 0);
    if (ec)
        return;

    m_containerElement = container.release();
    m_outlineElement = outline.release();
    m_buttonElement = button.release();
}

void DeleteButtonController::show(HTMLElement* element)
{
    hide();

    if (!enabled() || !element || !element->inDocument() || !isDeletableElement(element))
        return;

    if (!m_frame->editor()->shouldShowDeleteInterface(static_cast<HTMLElement*>(element)))
        return;

    // we rely on the renderer having current information, so we should update the layout if needed
    m_frame->document()->updateLayoutIgnorePendingStylesheets();

    m_target = element;

    if (!m_containerElement) {
        createDeletionUI();
        if (!m_containerElement) {
            hide();
            return;
        }
    }

    ExceptionCode ec = 0;
    m_target->appendChild(m_containerElement.get(), ec);
thatcher's avatar
thatcher committed
239 240 241 242 243 244
    ASSERT(ec == 0);
    if (ec) {
        hide();
        return;
    }

thatcher's avatar
thatcher committed
245
    if (m_target->renderer()->style()->position() == StaticPosition) {
246
        m_target->getInlineStyleDecl()->setProperty(CSSPropertyPosition, CSSValueRelative);
thatcher's avatar
thatcher committed
247 248
        m_wasStaticPositioned = true;
    }
thatcher's avatar
thatcher committed
249

thatcher's avatar
thatcher committed
250
    if (m_target->renderer()->style()->hasAutoZIndex()) {
251
        m_target->getInlineStyleDecl()->setProperty(CSSPropertyZIndex, "0");
thatcher's avatar
thatcher committed
252
        m_wasAutoZIndex = true;
thatcher's avatar
thatcher committed
253 254 255 256 257
    }
}

void DeleteButtonController::hide()
{
thatcher's avatar
thatcher committed
258 259
    m_outlineElement = 0;
    m_buttonElement = 0;
thatcher's avatar
thatcher committed
260

thatcher's avatar
thatcher committed
261 262 263 264 265 266
    ExceptionCode ec = 0;
    if (m_containerElement && m_containerElement->parentNode())
        m_containerElement->parentNode()->removeChild(m_containerElement.get(), ec);

    if (m_target) {
        if (m_wasStaticPositioned)
267
            m_target->getInlineStyleDecl()->setProperty(CSSPropertyPosition, CSSValueStatic);
thatcher's avatar
thatcher committed
268
        if (m_wasAutoZIndex)
269
            m_target->getInlineStyleDecl()->setProperty(CSSPropertyZIndex, CSSValueAuto);
thatcher's avatar
thatcher committed
270
    }
thatcher's avatar
thatcher committed
271 272 273 274 275

    m_wasStaticPositioned = false;
    m_wasAutoZIndex = false;
}

thatcher's avatar
thatcher committed
276 277 278 279 280 281
void DeleteButtonController::enable()
{
    ASSERT(m_disableStack > 0);
    if (m_disableStack > 0)
        m_disableStack--;
    if (enabled())
darin@apple.com's avatar
darin@apple.com committed
282
        show(enclosingDeletableElement(m_frame->selection()->selection()));
thatcher's avatar
thatcher committed
283 284 285 286 287 288 289 290 291
}

void DeleteButtonController::disable()
{
    if (enabled())
        hide();
    m_disableStack++;
}

thatcher's avatar
thatcher committed
292 293
void DeleteButtonController::deleteTarget()
{
thatcher's avatar
thatcher committed
294
    if (!enabled() || !m_target)
thatcher's avatar
thatcher committed
295 296
        return;

thatcher's avatar
thatcher committed
297
    RefPtr<Node> element = m_target;
thatcher's avatar
thatcher committed
298 299
    hide();

harrison's avatar
harrison committed
300 301 302 303
    // Because the deletion UI only appears when the selection is entirely
    // within the target, we unconditionally update the selection to be
    // a caret where the target had been.
    Position pos = positionBeforeNode(element.get());
darin@apple.com's avatar
darin@apple.com committed
304
    applyCommand(RemoveNodeCommand::create(element.release()));
darin@apple.com's avatar
darin@apple.com committed
305
    m_frame->selection()->setSelection(VisiblePosition(pos));
thatcher's avatar
thatcher committed
306 307 308
}

} // namespace WebCore