mirror of
https://bitbucket.org/smil3y/kdelibs.git
synced 2025-02-24 19:02:48 +00:00
3007 lines
102 KiB
C++
3007 lines
102 KiB
C++
/*
|
|
* Copyright (C) 2004 Apple Computer, Inc. All rights reserved.
|
|
*
|
|
* 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 "htmlediting_impl.h"
|
|
#include "editor.h"
|
|
|
|
#include "css/cssproperties.h"
|
|
#include "css/css_valueimpl.h"
|
|
#include "dom/css_value.h"
|
|
#include "html/html_elementimpl.h"
|
|
#include "html/html_imageimpl.h"
|
|
#include "rendering/render_object.h"
|
|
#include "rendering/render_style.h"
|
|
#include "rendering/render_text.h"
|
|
#include "xml/dom_docimpl.h"
|
|
#include "xml/dom_elementimpl.h"
|
|
#include "xml/dom_position.h"
|
|
#include "xml/dom_positioniterator.h"
|
|
#include "xml/dom_nodeimpl.h"
|
|
#include "xml/dom_selection.h"
|
|
#include "xml/dom_stringimpl.h"
|
|
#include "xml/dom_textimpl.h"
|
|
#include "xml/dom2_rangeimpl.h"
|
|
#include "xml/dom2_viewsimpl.h"
|
|
|
|
#include "khtml_part.h"
|
|
#include "khtmlview.h"
|
|
|
|
#include <QList>
|
|
#include <limits.h>
|
|
|
|
using DOM::AttrImpl;
|
|
using DOM::CSSPrimitiveValue;
|
|
using DOM::CSSPrimitiveValueImpl;
|
|
using DOM::CSSProperty;
|
|
using DOM::CSSStyleDeclarationImpl;
|
|
using DOM::CSSValueImpl;
|
|
using DOM::DocumentFragmentImpl;
|
|
using DOM::DocumentImpl;
|
|
using DOM::DOMString;
|
|
using DOM::DOMStringImpl;
|
|
using DOM::EditingTextImpl;
|
|
using DOM::PositionIterator;
|
|
using DOM::ElementImpl;
|
|
using DOM::HTMLElementImpl;
|
|
using DOM::HTMLImageElementImpl;
|
|
using DOM::NamedAttrMapImpl;
|
|
using DOM::Node;
|
|
using DOM::NodeImpl;
|
|
using DOM::NodeListImpl;
|
|
using DOM::Position;
|
|
using DOM::Range;
|
|
using DOM::RangeImpl;
|
|
using DOM::Selection;
|
|
using DOM::TextImpl;
|
|
using DOM::TreeWalkerImpl;
|
|
using DOM::Editor;
|
|
|
|
#define DEBUG_COMMANDS 1
|
|
|
|
namespace khtml {
|
|
|
|
|
|
static inline bool isNBSP(const QChar &c)
|
|
{
|
|
return c == QChar(0xa0);
|
|
}
|
|
|
|
static inline bool isWS(const QChar &c)
|
|
{
|
|
return c.isSpace() && c != QChar(0xa0);
|
|
}
|
|
|
|
static inline bool isWS(const DOMString &text)
|
|
{
|
|
if (text.length() != 1)
|
|
return false;
|
|
|
|
return isWS(text[0]);
|
|
}
|
|
|
|
static inline bool isWS(const Position &pos)
|
|
{
|
|
if (!pos.node())
|
|
return false;
|
|
|
|
if (!pos.node()->isTextNode())
|
|
return false;
|
|
|
|
const DOMString &string = static_cast<TextImpl *>(pos.node())->data();
|
|
return isWS(string[pos.offset()]);
|
|
}
|
|
|
|
static bool shouldPruneNode(NodeImpl *node)
|
|
{
|
|
if (!node)
|
|
return false;
|
|
|
|
RenderObject *renderer = node->renderer();
|
|
if (!renderer)
|
|
return true;
|
|
|
|
if (node->hasChildNodes())
|
|
return false;
|
|
|
|
if (node->rootEditableElement() == node)
|
|
return false;
|
|
|
|
if (renderer->isBR() || renderer->isReplaced())
|
|
return false;
|
|
|
|
if (node->isTextNode()) {
|
|
TextImpl *text = static_cast<TextImpl *>(node);
|
|
if (text->length() == 0)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
if (!node->isHTMLElement()/* && !node->isXMLElementNode()*/)
|
|
return false;
|
|
|
|
if (node->id() == ID_BODY)
|
|
return false;
|
|
|
|
if (!node->isContentEditable())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static Position leadingWhitespacePosition(const Position &pos)
|
|
{
|
|
assert(pos.notEmpty());
|
|
|
|
Selection selection(pos);
|
|
Position prev = pos.previousCharacterPosition();
|
|
if (prev != pos && prev.node()->inSameContainingBlockFlowElement(pos.node()) && prev.node()->isTextNode()) {
|
|
DOMString string = static_cast<TextImpl *>(prev.node())->data();
|
|
if (isWS(string[prev.offset()]))
|
|
return prev;
|
|
}
|
|
|
|
return Position();
|
|
}
|
|
|
|
static Position trailingWhitespacePosition(const Position &pos)
|
|
{
|
|
assert(pos.notEmpty());
|
|
|
|
if (pos.node()->isTextNode()) {
|
|
TextImpl *textNode = static_cast<TextImpl *>(pos.node());
|
|
if (pos.offset() >= (long)textNode->length()) {
|
|
Position next = pos.nextCharacterPosition();
|
|
if (next != pos && next.node()->inSameContainingBlockFlowElement(pos.node()) && next.node()->isTextNode()) {
|
|
DOMString string = static_cast<TextImpl *>(next.node())->data();
|
|
if (isWS(string[0]))
|
|
return next;
|
|
}
|
|
}
|
|
else {
|
|
DOMString string = static_cast<TextImpl *>(pos.node())->data();
|
|
if (isWS(string[pos.offset()]))
|
|
return pos;
|
|
}
|
|
}
|
|
|
|
return Position();
|
|
}
|
|
|
|
static bool textNodesAreJoinable(TextImpl *text1, TextImpl *text2)
|
|
{
|
|
assert(text1);
|
|
assert(text2);
|
|
|
|
return (text1->nextSibling() == text2);
|
|
}
|
|
|
|
static DOMString &nonBreakingSpaceString()
|
|
{
|
|
static DOMString nonBreakingSpaceString = QString(QChar(0xa0));
|
|
return nonBreakingSpaceString;
|
|
}
|
|
|
|
static DOMString &styleSpanClassString()
|
|
{
|
|
static DOMString styleSpanClassString = "khtml-style-span";
|
|
return styleSpanClassString;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// EditCommandImpl
|
|
|
|
EditCommandImpl::EditCommandImpl(DocumentImpl *document)
|
|
: SharedCommandImpl(), m_document(document), m_state(NotApplied), m_parent(0)
|
|
{
|
|
assert(m_document);
|
|
assert(m_document->part());
|
|
m_document->ref();
|
|
m_startingSelection = m_document->part()->caret();
|
|
m_endingSelection = m_startingSelection;
|
|
}
|
|
|
|
EditCommandImpl::~EditCommandImpl()
|
|
{
|
|
m_document->deref();
|
|
}
|
|
|
|
void EditCommandImpl::apply()
|
|
{
|
|
assert(m_document);
|
|
assert(m_document->part());
|
|
assert(state() == NotApplied);
|
|
|
|
doApply();
|
|
|
|
m_state = Applied;
|
|
|
|
if (!isCompositeStep())
|
|
m_document->part()->editor()->appliedEditing(this);
|
|
}
|
|
|
|
void EditCommandImpl::unapply()
|
|
{
|
|
assert(m_document);
|
|
assert(m_document->part());
|
|
assert(state() == Applied);
|
|
|
|
doUnapply();
|
|
|
|
m_state = NotApplied;
|
|
|
|
if (!isCompositeStep())
|
|
m_document->part()->editor()->unappliedEditing(this);
|
|
}
|
|
|
|
void EditCommandImpl::reapply()
|
|
{
|
|
assert(m_document);
|
|
assert(m_document->part());
|
|
assert(state() == NotApplied);
|
|
|
|
doReapply();
|
|
|
|
m_state = Applied;
|
|
|
|
if (!isCompositeStep())
|
|
m_document->part()->editor()->reappliedEditing(this);
|
|
}
|
|
|
|
void EditCommandImpl::doReapply()
|
|
{
|
|
doApply();
|
|
}
|
|
|
|
void EditCommandImpl::setStartingSelection(const Selection &s)
|
|
{
|
|
m_startingSelection = s;
|
|
EditCommandImpl *cmd = parent();
|
|
while (cmd) {
|
|
cmd->m_startingSelection = s;
|
|
cmd = cmd->parent();
|
|
}
|
|
}
|
|
|
|
void EditCommandImpl::setEndingSelection(const Selection &s)
|
|
{
|
|
m_endingSelection = s;
|
|
EditCommandImpl *cmd = parent();
|
|
while (cmd) {
|
|
cmd->m_endingSelection = s;
|
|
cmd = cmd->parent();
|
|
}
|
|
}
|
|
|
|
EditCommandImpl* EditCommandImpl::parent() const
|
|
{
|
|
return m_parent.get();
|
|
}
|
|
|
|
void EditCommandImpl::setParent(EditCommandImpl* cmd)
|
|
{
|
|
m_parent = cmd;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// CompositeEditCommandImpl
|
|
|
|
CompositeEditCommandImpl::CompositeEditCommandImpl(DocumentImpl *document)
|
|
: EditCommandImpl(document)
|
|
{
|
|
}
|
|
|
|
CompositeEditCommandImpl::~CompositeEditCommandImpl()
|
|
{
|
|
}
|
|
|
|
void CompositeEditCommandImpl::doUnapply()
|
|
{
|
|
if (m_cmds.count() == 0) {
|
|
return;
|
|
}
|
|
|
|
for (int i = m_cmds.count() - 1; i >= 0; --i)
|
|
m_cmds[i]->unapply();
|
|
|
|
setState(NotApplied);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::doReapply()
|
|
{
|
|
if (m_cmds.count() == 0) {
|
|
return;
|
|
}
|
|
QMutableListIterator<RefPtr<EditCommandImpl> > it(m_cmds);
|
|
while (it.hasNext())
|
|
it.next()->reapply();
|
|
|
|
setState(Applied);
|
|
}
|
|
|
|
//
|
|
// sugary-sweet convenience functions to help create and apply edit commands in composite commands
|
|
//
|
|
void CompositeEditCommandImpl::applyCommandToComposite(PassRefPtr<EditCommandImpl> cmd)
|
|
{
|
|
cmd->setStartingSelection(endingSelection());//###?
|
|
cmd->setEndingSelection(endingSelection());
|
|
cmd->setParent(this);
|
|
cmd->apply();
|
|
m_cmds.append(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::insertNodeBefore(NodeImpl *insertChild, NodeImpl *refChild)
|
|
{
|
|
RefPtr<InsertNodeBeforeCommandImpl> cmd = new InsertNodeBeforeCommandImpl(document(), insertChild, refChild);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::insertNodeAfter(NodeImpl *insertChild, NodeImpl *refChild)
|
|
{
|
|
if (refChild->parentNode()->lastChild() == refChild) {
|
|
appendNode(refChild->parentNode(), insertChild);
|
|
}
|
|
else {
|
|
assert(refChild->nextSibling());
|
|
insertNodeBefore(insertChild, refChild->nextSibling());
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommandImpl::insertNodeAt(NodeImpl *insertChild, NodeImpl *refChild, long offset)
|
|
{
|
|
if (refChild->hasChildNodes() || (refChild->renderer() && refChild->renderer()->isBlockFlow())) {
|
|
NodeImpl *child = refChild->firstChild();
|
|
for (long i = 0; child && i < offset; i++)
|
|
child = child->nextSibling();
|
|
if (child)
|
|
insertNodeBefore(insertChild, child);
|
|
else
|
|
appendNode(refChild, insertChild);
|
|
}
|
|
else if (refChild->caretMinOffset() >= offset) {
|
|
insertNodeBefore(insertChild, refChild);
|
|
}
|
|
else if (refChild->isTextNode() && refChild->caretMaxOffset() > offset) {
|
|
splitTextNode(static_cast<TextImpl *>(refChild), offset);
|
|
insertNodeBefore(insertChild, refChild);
|
|
}
|
|
else {
|
|
insertNodeAfter(insertChild, refChild);
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommandImpl::appendNode(NodeImpl *parent, NodeImpl *appendChild)
|
|
{
|
|
RefPtr<AppendNodeCommandImpl> cmd = new AppendNodeCommandImpl(document(), parent, appendChild);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::removeNode(NodeImpl *removeChild)
|
|
{
|
|
RefPtr<RemoveNodeCommandImpl> cmd = new RemoveNodeCommandImpl(document(), removeChild);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::removeNodeAndPrune(NodeImpl *pruneNode, NodeImpl *stopNode)
|
|
{
|
|
RefPtr<RemoveNodeAndPruneCommandImpl> cmd = new RemoveNodeAndPruneCommandImpl(document(), pruneNode, stopNode);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::removeNodePreservingChildren(NodeImpl *removeChild)
|
|
{
|
|
RefPtr<RemoveNodePreservingChildrenCommandImpl> cmd = new RemoveNodePreservingChildrenCommandImpl(document(), removeChild);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::splitTextNode(TextImpl *text, long offset)
|
|
{
|
|
RefPtr<SplitTextNodeCommandImpl> cmd = new SplitTextNodeCommandImpl(document(), text, offset);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::joinTextNodes(TextImpl *text1, TextImpl *text2)
|
|
{
|
|
RefPtr<JoinTextNodesCommandImpl> cmd = new JoinTextNodesCommandImpl(document(), text1, text2);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::inputText(const DOMString &text)
|
|
{
|
|
RefPtr<InputTextCommandImpl> cmd = new InputTextCommandImpl(document());
|
|
applyCommandToComposite(cmd);
|
|
cmd->input(text);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::insertText(TextImpl *node, long offset, const DOMString &text)
|
|
{
|
|
RefPtr<InsertTextCommandImpl> cmd = new InsertTextCommandImpl(document(), node, offset, text);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::deleteText(TextImpl *node, long offset, long count)
|
|
{
|
|
RefPtr<DeleteTextCommandImpl> cmd = new DeleteTextCommandImpl(document(), node, offset, count);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::replaceText(TextImpl *node, long offset, long count, const DOMString &replacementText)
|
|
{
|
|
RefPtr<DeleteTextCommandImpl> deleteCommand = new DeleteTextCommandImpl(document(), node, offset, count);
|
|
applyCommandToComposite(deleteCommand);
|
|
RefPtr<InsertTextCommandImpl> insertCommand = new InsertTextCommandImpl(document(), node, offset, replacementText);
|
|
applyCommandToComposite(insertCommand);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::deleteSelection()
|
|
{
|
|
if (endingSelection().state() == Selection::RANGE) {
|
|
RefPtr<DeleteSelectionCommandImpl> cmd = new DeleteSelectionCommandImpl(document());
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommandImpl::deleteSelection(const Selection &selection)
|
|
{
|
|
if (selection.state() == Selection::RANGE) {
|
|
RefPtr<DeleteSelectionCommandImpl> cmd = new DeleteSelectionCommandImpl(document(), selection);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
}
|
|
|
|
void CompositeEditCommandImpl::deleteCollapsibleWhitespace()
|
|
{
|
|
RefPtr<DeleteCollapsibleWhitespaceCommandImpl> cmd = new DeleteCollapsibleWhitespaceCommandImpl(document());
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::deleteCollapsibleWhitespace(const Selection &selection)
|
|
{
|
|
RefPtr<DeleteCollapsibleWhitespaceCommandImpl> cmd = new DeleteCollapsibleWhitespaceCommandImpl(document(), selection);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::removeCSSProperty(CSSStyleDeclarationImpl *decl, int property)
|
|
{
|
|
RefPtr<RemoveCSSPropertyCommandImpl> cmd = new RemoveCSSPropertyCommandImpl(document(), decl, property);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::removeNodeAttribute(ElementImpl *element, int attribute)
|
|
{
|
|
RefPtr<RemoveNodeAttributeCommandImpl> cmd = new RemoveNodeAttributeCommandImpl(document(), element, attribute);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
void CompositeEditCommandImpl::setNodeAttribute(ElementImpl *element, int attribute, const DOMString &value)
|
|
{
|
|
RefPtr<SetNodeAttributeCommandImpl> cmd = new SetNodeAttributeCommandImpl(document(), element, attribute, value);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
ElementImpl *CompositeEditCommandImpl::createTypingStyleElement() const
|
|
{
|
|
int exceptionCode = 0;
|
|
ElementImpl *styleElement = document()->createHTMLElement("SPAN");
|
|
// assert(exceptionCode == 0);
|
|
|
|
styleElement->setAttribute(ATTR_STYLE, document()->part()->editor()->typingStyle()->cssText().implementation());
|
|
// assert(exceptionCode == 0);
|
|
|
|
styleElement->setAttribute(ATTR_CLASS, styleSpanClassString());
|
|
assert(exceptionCode == 0);
|
|
|
|
return styleElement;
|
|
}
|
|
|
|
//==========================================================================================
|
|
// Concrete commands
|
|
//------------------------------------------------------------------------------------------
|
|
// AppendNodeCommandImpl
|
|
|
|
AppendNodeCommandImpl::AppendNodeCommandImpl(DocumentImpl *document, NodeImpl *parentNode, NodeImpl *appendChild)
|
|
: EditCommandImpl(document), m_parentNode(parentNode), m_appendChild(appendChild)
|
|
{
|
|
assert(m_parentNode);
|
|
m_parentNode->ref();
|
|
|
|
assert(m_appendChild);
|
|
m_appendChild->ref();
|
|
}
|
|
|
|
AppendNodeCommandImpl::~AppendNodeCommandImpl()
|
|
{
|
|
if (m_parentNode)
|
|
m_parentNode->deref();
|
|
if (m_appendChild)
|
|
m_appendChild->deref();
|
|
}
|
|
|
|
void AppendNodeCommandImpl::doApply()
|
|
{
|
|
assert(m_parentNode);
|
|
assert(m_appendChild);
|
|
|
|
int exceptionCode = 0;
|
|
m_parentNode->appendChild(m_appendChild, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
void AppendNodeCommandImpl::doUnapply()
|
|
{
|
|
assert(m_parentNode);
|
|
assert(m_appendChild);
|
|
assert(state() == Applied);
|
|
|
|
int exceptionCode = 0;
|
|
m_parentNode->removeChild(m_appendChild, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// ApplyStyleCommandImpl
|
|
|
|
ApplyStyleCommandImpl::ApplyStyleCommandImpl(DocumentImpl *document, CSSStyleDeclarationImpl *style)
|
|
: CompositeEditCommandImpl(document), m_style(style)
|
|
{
|
|
assert(m_style);
|
|
m_style->ref();
|
|
}
|
|
|
|
ApplyStyleCommandImpl::~ApplyStyleCommandImpl()
|
|
{
|
|
assert(m_style);
|
|
m_style->deref();
|
|
}
|
|
|
|
static bool isBlockLevelStyle(const CSSStyleDeclarationImpl* style)
|
|
{
|
|
QListIterator<CSSProperty*> it(*(style->values()));
|
|
while (it.hasNext()) {
|
|
CSSProperty *property = it.next();
|
|
switch (property->id()) {
|
|
case CSS_PROP_TEXT_ALIGN:
|
|
return true;
|
|
/*case CSS_PROP_FONT_WEIGHT:
|
|
if (strcasecmp(property->value()->cssText(), "bold") == 0)
|
|
styleChange.applyBold = true;
|
|
else
|
|
styleChange.cssStyle += property->cssText();
|
|
break;
|
|
case CSS_PROP_FONT_STYLE: {
|
|
DOMString cssText(property->value()->cssText());
|
|
if (strcasecmp(cssText, "italic") == 0 || strcasecmp(cssText, "oblique") == 0)
|
|
styleChange.applyItalic = true;
|
|
else
|
|
styleChange.cssStyle += property->cssText();
|
|
}
|
|
break;
|
|
default:
|
|
styleChange.cssStyle += property->cssText();
|
|
break;*/
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void applyStyleChangeOnTheNode(ElementImpl* element, CSSStyleDeclarationImpl* style)
|
|
{
|
|
CSSStyleDeclarationImpl *computedStyle = element->document()->defaultView()->getComputedStyle(element, 0);
|
|
assert(computedStyle);
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[change style]" << element << endl;
|
|
#endif
|
|
|
|
QListIterator<CSSProperty*> it(*(style->values()));
|
|
while ( it.hasNext() ) {
|
|
CSSProperty *property = it.next();
|
|
CSSValueImpl *computedValue = computedStyle->getPropertyCSSValue(property->id());
|
|
DOMString newValue = property->value()->cssText();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[new value]:" << property->cssText() << endl;
|
|
kDebug() << "[computedValue]:" << computedValue->cssText() << endl;
|
|
#endif
|
|
if (strcasecmp(computedValue->cssText(), newValue)) {
|
|
// we can do better and avoid parsing property
|
|
element->getInlineStyleDecls()->setProperty(property->id(), newValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ApplyStyleCommandImpl::doApply()
|
|
{
|
|
if (endingSelection().state() != Selection::RANGE)
|
|
return;
|
|
|
|
// adjust to the positions we want to use for applying style
|
|
Position start(endingSelection().start().equivalentDownstreamPosition().equivalentRangeCompliantPosition());
|
|
Position end(endingSelection().end().equivalentUpstreamPosition());
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[APPLY STYLE]" << start << end << endl;
|
|
printEnclosingBlockTree(start.node()->enclosingBlockFlowElement());
|
|
#endif
|
|
|
|
if (isBlockLevelStyle(m_style)) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[APPLY BLOCK LEVEL STYLE]" << endl;
|
|
#endif
|
|
ElementImpl *startBlock = start.node()->enclosingBlockFlowElement();
|
|
ElementImpl *endBlock = end.node()->enclosingBlockFlowElement();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << startBlock << startBlock->nodeName() << endl;
|
|
#endif
|
|
if (startBlock == endBlock && startBlock == start.node()->rootEditableElement()) {
|
|
ElementImpl* block = document()->createHTMLElement("DIV");
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[Create DIV with Style:]" << m_style->cssText() << endl;
|
|
#endif
|
|
block->setAttribute(ATTR_STYLE, m_style->cssText());
|
|
for (NodeImpl* node = startBlock->firstChild(); node; node = startBlock->firstChild()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[reparent node]" << node << node->nodeName() << endl;
|
|
#endif
|
|
removeNode(node);
|
|
appendNode(block, node);
|
|
}
|
|
appendNode(startBlock, block);
|
|
} else if (startBlock == endBlock) {
|
|
// StyleChange styleChange = computeStyleChange(Position(startBlock, 0), m_style);
|
|
// kDebug() << "[Modify block with style change:]" << styleChange.cssStyle << endl;
|
|
applyStyleChangeOnTheNode(startBlock, m_style);
|
|
// startBlock->setAttribute(ATTR_STYLE, styleChange.cssStyle);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// remove style from the selection
|
|
removeStyle(start, end);
|
|
bool splitStart = splitTextAtStartIfNeeded(start, end);
|
|
if (splitStart) {
|
|
start = endingSelection().start();
|
|
end = endingSelection().end();
|
|
}
|
|
splitTextAtEndIfNeeded(start, end);
|
|
start = endingSelection().start();
|
|
end = endingSelection().end();
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[start;end]" << start << end << endl;
|
|
#endif
|
|
if (start.node() == end.node()) {
|
|
// simple case...start and end are the same node
|
|
applyStyleIfNeeded(start.node(), end.node());
|
|
} else {
|
|
NodeImpl *node = start.node();
|
|
while (1) {
|
|
if (node->childNodeCount() == 0 && node->renderer() && node->renderer()->isInline()) {
|
|
NodeImpl *runStart = node;
|
|
while (1) {
|
|
if (runStart->parentNode() != node->parentNode() || node->isHTMLElement() || node == end.node() ||
|
|
(node->renderer() && !node->renderer()->isInline())) {
|
|
applyStyleIfNeeded(runStart, node);
|
|
break;
|
|
}
|
|
node = node->traverseNextNode();
|
|
}
|
|
}
|
|
if (node == end.node())
|
|
break;
|
|
node = node->traverseNextNode();
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// ApplyStyleCommandImpl: style-removal helpers
|
|
|
|
bool ApplyStyleCommandImpl::isHTMLStyleNode(HTMLElementImpl *elem)
|
|
{
|
|
QListIterator<CSSProperty*> it(*(style()->values()));
|
|
while (it.hasNext()) {
|
|
CSSProperty *property = it.next();
|
|
switch (property->id()) {
|
|
case CSS_PROP_FONT_WEIGHT:
|
|
if (elem->id() == ID_B)
|
|
return true;
|
|
break;
|
|
case CSS_PROP_FONT_STYLE:
|
|
if (elem->id() == ID_I)
|
|
return true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void ApplyStyleCommandImpl::removeHTMLStyleNode(HTMLElementImpl *elem)
|
|
{
|
|
// This node can be removed.
|
|
// EDIT FIXME: This does not handle the case where the node
|
|
// has attributes. But how often do people add attributes to <B> tags?
|
|
// Not so often I think.
|
|
assert(elem);
|
|
removeNodePreservingChildren(elem);
|
|
}
|
|
|
|
void ApplyStyleCommandImpl::removeCSSStyle(HTMLElementImpl *elem)
|
|
{
|
|
assert(elem);
|
|
|
|
CSSStyleDeclarationImpl *decl = elem->inlineStyleDecls();
|
|
if (!decl)
|
|
return;
|
|
|
|
QListIterator<CSSProperty*> it(*(style()->values()));
|
|
while ( it.hasNext() ) {
|
|
CSSProperty *property = it.next();
|
|
if (decl->getPropertyCSSValue(property->id()))
|
|
removeCSSProperty(decl, property->id());
|
|
}
|
|
|
|
if (elem->id() == ID_SPAN) {
|
|
// Check to see if the span is one we added to apply style.
|
|
// If it is, and there are no more attributes on the span other than our
|
|
// class marker, remove the span.
|
|
NamedAttrMapImpl *map = elem->attributes();
|
|
if (map && (map->length() == 1 || (map->length() == 2 && elem->getAttribute(ATTR_STYLE).isEmpty())) &&
|
|
elem->getAttribute(ATTR_CLASS) == styleSpanClassString())
|
|
removeNodePreservingChildren(elem);
|
|
}
|
|
}
|
|
|
|
void ApplyStyleCommandImpl::removeStyle(const Position &start, const Position &end)
|
|
{
|
|
NodeImpl *node = start.node();
|
|
while (1) {
|
|
NodeImpl *next = node->traverseNextNode();
|
|
if (node->isHTMLElement() && nodeFullySelected(node)) {
|
|
HTMLElementImpl *elem = static_cast<HTMLElementImpl *>(node);
|
|
if (isHTMLStyleNode(elem))
|
|
removeHTMLStyleNode(elem);
|
|
else
|
|
removeCSSStyle(elem);
|
|
}
|
|
if (node == end.node())
|
|
break;
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
bool ApplyStyleCommandImpl::nodeFullySelected(const NodeImpl *node) const
|
|
{
|
|
assert(node);
|
|
|
|
Position end(endingSelection().end().equivalentUpstreamPosition());
|
|
|
|
if (node == end.node())
|
|
return end.offset() >= node->caretMaxOffset();
|
|
|
|
for (NodeImpl *child = node->lastChild(); child; child = child->lastChild()) {
|
|
if (child == end.node())
|
|
return end.offset() >= child->caretMaxOffset();
|
|
}
|
|
|
|
return node == end.node() || !node->isAncestor(end.node());
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// ApplyStyleCommandImpl: style-application helpers
|
|
|
|
bool ApplyStyleCommandImpl::splitTextAtStartIfNeeded(const Position &start, const Position &end)
|
|
{
|
|
if (start.node()->isTextNode() && start.offset() > start.node()->caretMinOffset() && start.offset() < start.node()->caretMaxOffset()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[split start]" << start.offset() << start.node()->caretMinOffset() << start.node()->caretMaxOffset() << endl;
|
|
#endif
|
|
long endOffsetAdjustment = start.node() == end.node() ? start.offset() : 0;
|
|
TextImpl *text = static_cast<TextImpl *>(start.node());
|
|
RefPtr<SplitTextNodeCommandImpl> cmd = new SplitTextNodeCommandImpl(document(), text, start.offset());
|
|
applyCommandToComposite(cmd);
|
|
setEndingSelection(Selection(Position(start.node(), 0), Position(end.node(), end.offset() - endOffsetAdjustment)));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
NodeImpl *ApplyStyleCommandImpl::splitTextAtEndIfNeeded(const Position &start, const Position &end)
|
|
{
|
|
if (end.node()->isTextNode() && end.offset() > end.node()->caretMinOffset() && end.offset() < end.node()->caretMaxOffset()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[split end]" << end.offset() << end.node()->caretMinOffset() << end.node()->caretMaxOffset() << endl;
|
|
#endif
|
|
TextImpl *text = static_cast<TextImpl *>(end.node());
|
|
RefPtr<SplitTextNodeCommandImpl> cmd = new SplitTextNodeCommandImpl(document(), text, end.offset());
|
|
applyCommandToComposite(cmd);
|
|
NodeImpl *startNode = start.node() == end.node() ? cmd->node()->previousSibling() : start.node();
|
|
assert(startNode);
|
|
setEndingSelection(Selection(Position(startNode, start.offset()), Position(cmd->node()->previousSibling(), cmd->node()->previousSibling()->caretMaxOffset())));
|
|
return cmd->node()->previousSibling();
|
|
}
|
|
return end.node();
|
|
}
|
|
|
|
void ApplyStyleCommandImpl::surroundNodeRangeWithElement(NodeImpl *startNode, NodeImpl *endNode, ElementImpl *element)
|
|
{
|
|
assert(startNode);
|
|
assert(endNode);
|
|
assert(element);
|
|
|
|
NodeImpl *node = startNode;
|
|
while (1) {
|
|
NodeImpl *next = node->traverseNextNode();
|
|
if (node->childNodeCount() == 0 && node->renderer() && node->renderer()->isInline()) {
|
|
removeNode(node);
|
|
appendNode(element, node);
|
|
}
|
|
if (node == endNode)
|
|
break;
|
|
node = next;
|
|
}
|
|
}
|
|
|
|
static bool /*ApplyStyleCommandImpl::*/checkIfNewStylingNeeded(ElementImpl* element, CSSStyleDeclarationImpl *style)
|
|
{
|
|
CSSStyleDeclarationImpl *computedStyle = element->document()->defaultView()->getComputedStyle(element, 0);
|
|
assert(computedStyle);
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[check styling]" << element << endl;
|
|
#endif
|
|
|
|
QListIterator<CSSProperty*> it(*(style->values()));
|
|
while ( it.hasNext() ) {
|
|
CSSProperty *property = it.next();
|
|
CSSValueImpl *computedValue = computedStyle->getPropertyCSSValue(property->id());
|
|
DOMString newValue = property->value()->cssText();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[new value]:" << property->cssText() << endl;
|
|
kDebug() << "[computedValue]:" << computedValue->cssText() << endl;
|
|
#endif
|
|
if (strcasecmp(computedValue->cssText(), newValue))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void ApplyStyleCommandImpl::applyStyleIfNeeded(DOM::NodeImpl *startNode, DOM::NodeImpl *endNode)
|
|
{
|
|
ElementImpl *parent = Position(startNode, 0).element();
|
|
if (!checkIfNewStylingNeeded(parent, style()))
|
|
return;
|
|
ElementImpl *styleElement = 0;
|
|
if (parent->id() == ID_SPAN && parent->firstChild() == startNode && parent->lastChild() == endNode) {
|
|
styleElement = parent;
|
|
} else {
|
|
styleElement = document()->createHTMLElement("SPAN");
|
|
styleElement->setAttribute(ATTR_CLASS, styleSpanClassString());
|
|
insertNodeBefore(styleElement, startNode);
|
|
surroundNodeRangeWithElement(startNode, endNode, styleElement);
|
|
}
|
|
applyStyleChangeOnTheNode(styleElement, style());
|
|
}
|
|
|
|
bool ApplyStyleCommandImpl::currentlyHasStyle(const Position &pos, const CSSProperty *property) const
|
|
{
|
|
assert(pos.notEmpty());
|
|
kDebug() << pos << endl;
|
|
CSSStyleDeclarationImpl *decl = document()->defaultView()->getComputedStyle(pos.element(), 0);
|
|
assert(decl);
|
|
CSSValueImpl *value = decl->getPropertyCSSValue(property->id());
|
|
return strcasecmp(value->cssText(), property->value()->cssText()) == 0;
|
|
}
|
|
|
|
ApplyStyleCommandImpl::StyleChange ApplyStyleCommandImpl::computeStyleChange(const Position &insertionPoint, CSSStyleDeclarationImpl *style)
|
|
{
|
|
assert(insertionPoint.notEmpty());
|
|
assert(style);
|
|
|
|
StyleChange styleChange;
|
|
|
|
QListIterator<CSSProperty*> it(*(style->values()));
|
|
while ( it.hasNext() ) {
|
|
CSSProperty *property = it.next();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[CSS property]:" << property->cssText() << endl;
|
|
#endif
|
|
if (!currentlyHasStyle(insertionPoint, property)) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[Add to style change]" << endl;
|
|
#endif
|
|
switch (property->id()) {
|
|
case CSS_PROP_FONT_WEIGHT:
|
|
if (strcasecmp(property->value()->cssText(), "bold") == 0)
|
|
styleChange.applyBold = true;
|
|
else
|
|
styleChange.cssStyle += property->cssText();
|
|
break;
|
|
case CSS_PROP_FONT_STYLE: {
|
|
DOMString cssText(property->value()->cssText());
|
|
if (strcasecmp(cssText, "italic") == 0 || strcasecmp(cssText, "oblique") == 0)
|
|
styleChange.applyItalic = true;
|
|
else
|
|
styleChange.cssStyle += property->cssText();
|
|
}
|
|
break;
|
|
default:
|
|
styleChange.cssStyle += property->cssText();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return styleChange;
|
|
}
|
|
|
|
Position ApplyStyleCommandImpl::positionInsertionPoint(Position pos)
|
|
{
|
|
if (pos.node()->isTextNode() && (pos.offset() > 0 && pos.offset() < pos.node()->maxOffset())) {
|
|
RefPtr<SplitTextNodeCommandImpl> split = new SplitTextNodeCommandImpl(document(), static_cast<TextImpl *>(pos.node()), pos.offset());
|
|
split->apply();
|
|
pos = Position(split->node(), 0);
|
|
}
|
|
#if 0
|
|
// EDIT FIXME: If modified to work with the internals of applying style,
|
|
// this code can work to optimize cases where a style change is taking place on
|
|
// a boundary between nodes where one of the nodes has the desired style. In other
|
|
// words, it is possible for content to be merged into existing nodes rather than adding
|
|
// additional markup.
|
|
if (currentlyHasStyle(pos))
|
|
return pos;
|
|
|
|
// try next node
|
|
if (pos.offset() >= pos.node()->caretMaxOffset()) {
|
|
NodeImpl *nextNode = pos.node()->traverseNextNode();
|
|
if (nextNode) {
|
|
Position next = Position(nextNode, 0);
|
|
if (currentlyHasStyle(next))
|
|
return next;
|
|
}
|
|
}
|
|
|
|
// try previous node
|
|
if (pos.offset() <= pos.node()->caretMinOffset()) {
|
|
NodeImpl *prevNode = pos.node()->traversePreviousNode();
|
|
if (prevNode) {
|
|
Position prev = Position(prevNode, prevNode->maxOffset());
|
|
if (currentlyHasStyle(prev))
|
|
return prev;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return pos;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// DeleteCollapsibleWhitespaceCommandImpl
|
|
|
|
DeleteCollapsibleWhitespaceCommandImpl::DeleteCollapsibleWhitespaceCommandImpl(DocumentImpl *document)
|
|
: CompositeEditCommandImpl(document), m_charactersDeleted(0), m_hasSelectionToCollapse(false)
|
|
{
|
|
}
|
|
|
|
DeleteCollapsibleWhitespaceCommandImpl::DeleteCollapsibleWhitespaceCommandImpl(DocumentImpl *document, const Selection &selection)
|
|
: CompositeEditCommandImpl(document), m_charactersDeleted(0), m_selectionToCollapse(selection), m_hasSelectionToCollapse(true)
|
|
{
|
|
}
|
|
|
|
DeleteCollapsibleWhitespaceCommandImpl::~DeleteCollapsibleWhitespaceCommandImpl()
|
|
{
|
|
}
|
|
|
|
static bool shouldDeleteUpstreamPosition(const Position &pos)
|
|
{
|
|
if (!pos.node()->isTextNode())
|
|
return false;
|
|
|
|
RenderObject *renderer = pos.node()->renderer();
|
|
if (!renderer)
|
|
return true;
|
|
|
|
TextImpl *textNode = static_cast<TextImpl *>(pos.node());
|
|
if (pos.offset() >= (long)textNode->length())
|
|
return false;
|
|
|
|
if (pos.isLastRenderedPositionInEditableBlock())
|
|
return false;
|
|
|
|
if (pos.isFirstRenderedPositionOnLine() || pos.isLastRenderedPositionOnLine())
|
|
return false;
|
|
|
|
return false;
|
|
// TODO we need to match DOM - Rendered offset first
|
|
// RenderText *textRenderer = static_cast<RenderText *>(renderer);
|
|
// for (InlineTextBox *box = textRenderer->firstTextBox(); box; box = box->nextTextBox()) {
|
|
// if (pos.offset() < box->m_start) {
|
|
// return true;
|
|
// }
|
|
// if (pos.offset() >= box->m_start && pos.offset() < box->m_start + box->m_len)
|
|
// return false;
|
|
// }
|
|
//
|
|
// return true;
|
|
}
|
|
|
|
Position DeleteCollapsibleWhitespaceCommandImpl::deleteWhitespace(const Position &pos)
|
|
{
|
|
Position upstream = pos.equivalentUpstreamPosition();
|
|
Position downstream = pos.equivalentDownstreamPosition();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[pos]" << pos << endl;
|
|
kDebug() << "[upstream:downstream]" << upstream << downstream << endl;
|
|
printEnclosingBlockTree(pos.node());
|
|
#endif
|
|
|
|
bool del = shouldDeleteUpstreamPosition(upstream);
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[delete upstream]" << del << endl;
|
|
#endif
|
|
|
|
if (upstream == downstream)
|
|
return upstream;
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
PositionIterator iter(upstream);
|
|
kDebug() << "[before print]" << endl;
|
|
for (iter.next(); iter.current() != downstream; iter.next())
|
|
kDebug() << "[iterate]" << iter.current() << endl;
|
|
kDebug() << "[after print]" << endl;
|
|
#endif
|
|
|
|
PositionIterator it(upstream);
|
|
Position deleteStart = upstream;
|
|
if (!del) {
|
|
deleteStart = it.peekNext();
|
|
if (deleteStart == downstream)
|
|
return upstream;
|
|
}
|
|
|
|
Position endingPosition = upstream;
|
|
|
|
while (it.current() != downstream) {
|
|
Position next = it.peekNext();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[iterate and delete]" << next << endl;
|
|
#endif
|
|
if (next.node() != deleteStart.node()) {
|
|
// TODO assert(deleteStart.node()->isTextNode());
|
|
if (deleteStart.node()->isTextNode()) {
|
|
TextImpl *textNode = static_cast<TextImpl *>(deleteStart.node());
|
|
unsigned long count = it.current().offset() - deleteStart.offset();
|
|
if (count == textNode->length()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << " removeNodeAndPrune 1:" << textNode;
|
|
#endif
|
|
if (textNode == endingPosition.node())
|
|
endingPosition = Position(next.node(), next.node()->caretMinOffset());
|
|
removeNodeAndPrune(textNode);
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << " deleteText 1:" << textNode << "t len:" << textNode->length()<<"start:" << deleteStart.offset() << "del len:" << (it.current().offset() - deleteStart.offset());
|
|
#endif
|
|
deleteText(textNode, deleteStart.offset(), count);
|
|
}
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[not text node is not supported yet]" << endl;
|
|
#endif
|
|
}
|
|
deleteStart = next;
|
|
} else if (next == downstream) {
|
|
assert(deleteStart.node() == downstream.node());
|
|
assert(downstream.node()->isTextNode());
|
|
TextImpl *textNode = static_cast<TextImpl *>(deleteStart.node());
|
|
unsigned long count = downstream.offset() - deleteStart.offset();
|
|
assert(count <= textNode->length());
|
|
if (count == textNode->length()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << " removeNodeAndPrune 2:"<<textNode;
|
|
#endif
|
|
removeNodeAndPrune(textNode);
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << " deleteText 2:"<< textNode<< "t len:" << textNode->length() <<"start:" <<deleteStart.offset() << "del len:" << count;
|
|
#endif
|
|
deleteText(textNode, deleteStart.offset(), count);
|
|
m_charactersDeleted = count;
|
|
endingPosition = Position(downstream.node(), downstream.offset() - m_charactersDeleted);
|
|
}
|
|
}
|
|
|
|
it.setPosition(next);
|
|
}
|
|
|
|
return endingPosition;
|
|
}
|
|
|
|
void DeleteCollapsibleWhitespaceCommandImpl::doApply()
|
|
{
|
|
// If selection has not been set to a custom selection when the command was created,
|
|
// use the current ending selection.
|
|
if (!m_hasSelectionToCollapse)
|
|
m_selectionToCollapse = endingSelection();
|
|
int state = m_selectionToCollapse.state();
|
|
if (state == Selection::CARET) {
|
|
Position endPosition = deleteWhitespace(m_selectionToCollapse.start());
|
|
setEndingSelection(endPosition);
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "-----------------------------------------------------";
|
|
#endif
|
|
}
|
|
else if (state == Selection::RANGE) {
|
|
Position startPosition = deleteWhitespace(m_selectionToCollapse.start());
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "-----------------------------------------------------";
|
|
#endif
|
|
Position endPosition = m_selectionToCollapse.end();
|
|
if (m_charactersDeleted > 0 && startPosition.node() == endPosition.node()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "adjust end position by" << m_charactersDeleted;
|
|
#endif
|
|
endPosition = Position(endPosition.node(), endPosition.offset() - m_charactersDeleted);
|
|
}
|
|
endPosition = deleteWhitespace(endPosition);
|
|
setEndingSelection(Selection(startPosition, endPosition));
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "=====================================================";
|
|
#endif
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// DeleteSelectionCommandImpl
|
|
|
|
DeleteSelectionCommandImpl::DeleteSelectionCommandImpl(DocumentImpl *document)
|
|
: CompositeEditCommandImpl(document), m_hasSelectionToDelete(false)
|
|
{
|
|
}
|
|
|
|
DeleteSelectionCommandImpl::DeleteSelectionCommandImpl(DocumentImpl *document, const Selection &selection)
|
|
: CompositeEditCommandImpl(document), m_selectionToDelete(selection), m_hasSelectionToDelete(true)
|
|
{
|
|
}
|
|
|
|
DeleteSelectionCommandImpl::~DeleteSelectionCommandImpl()
|
|
{
|
|
}
|
|
|
|
void DeleteSelectionCommandImpl::joinTextNodesWithSameStyle()
|
|
{
|
|
Selection selection = endingSelection();
|
|
|
|
if (selection.state() != Selection::CARET)
|
|
return;
|
|
|
|
Position pos(selection.start());
|
|
|
|
if (!pos.node()->isTextNode())
|
|
return;
|
|
|
|
TextImpl *textNode = static_cast<TextImpl *>(pos.node());
|
|
|
|
if (pos.offset() == 0) {
|
|
PositionIterator it(pos);
|
|
Position prev = it.previous();
|
|
if (prev == pos)
|
|
return;
|
|
if (prev.node()->isTextNode()) {
|
|
TextImpl *prevTextNode = static_cast<TextImpl *>(prev.node());
|
|
if (textNodesAreJoinable(prevTextNode, textNode)) {
|
|
joinTextNodes(prevTextNode, textNode);
|
|
setEndingSelection(Position(textNode, prevTextNode->length()));
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "joinTextNodesWithSameStyle [1]";
|
|
#endif
|
|
}
|
|
}
|
|
} else if (pos.offset() == (long)textNode->length()) {
|
|
PositionIterator it(pos);
|
|
Position next = it.next();
|
|
if (next == pos)
|
|
return;
|
|
if (next.node()->isTextNode()) {
|
|
TextImpl *nextTextNode = static_cast<TextImpl *>(next.node());
|
|
if (textNodesAreJoinable(textNode, nextTextNode)) {
|
|
joinTextNodes(textNode, nextTextNode);
|
|
setEndingSelection(Position(nextTextNode, pos.offset()));
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "joinTextNodesWithSameStyle [2]";
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool DeleteSelectionCommandImpl::containsOnlyWhitespace(const Position &start, const Position &end)
|
|
{
|
|
// Returns whether the range contains only whitespace characters.
|
|
// This is inclusive of the start, but not of the end.
|
|
PositionIterator it(start);
|
|
while (!it.atEnd()) {
|
|
if (!it.current().node()->isTextNode())
|
|
return false;
|
|
const DOMString &text = static_cast<TextImpl *>(it.current().node())->data();
|
|
// EDIT FIXME: signed/unsigned mismatch
|
|
if (text.length() > INT_MAX)
|
|
return false;
|
|
if (it.current().offset() < (int)text.length() && !isWS(text[it.current().offset()]))
|
|
return false;
|
|
it.next();
|
|
if (it.current() == end)
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void DeleteSelectionCommandImpl::deleteContentInsideNode(NodeImpl *node, int startOffset, int endOffset)
|
|
{
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[Delete content inside node]" << node << startOffset << endOffset << endl;
|
|
#endif
|
|
if (node->isTextNode()) {
|
|
// check if nothing to delete
|
|
if (startOffset == endOffset)
|
|
return;
|
|
// check if node is fully covered then remove node completely
|
|
if (!startOffset && endOffset == node->maxOffset()) {
|
|
removeNodeAndPrune(node);
|
|
return;
|
|
}
|
|
// delete only substring
|
|
deleteText(static_cast<TextImpl*>(node), startOffset, endOffset - startOffset);
|
|
return;
|
|
}
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[non-text node] not supported" << endl;
|
|
#endif
|
|
}
|
|
|
|
void DeleteSelectionCommandImpl::deleteContentBeforeOffset(NodeImpl *node, int offset)
|
|
{
|
|
deleteContentInsideNode(node, 0, offset);
|
|
}
|
|
|
|
void DeleteSelectionCommandImpl::deleteContentAfterOffset(NodeImpl *node, int offset)
|
|
{
|
|
if (node->isTextNode())
|
|
deleteContentInsideNode(node, offset, node->maxOffset());
|
|
}
|
|
|
|
void DeleteSelectionCommandImpl::doApply()
|
|
{
|
|
// If selection has not been set to a custom selection when the command was created,
|
|
// use the current ending selection.
|
|
if (!m_hasSelectionToDelete)
|
|
m_selectionToDelete = endingSelection();
|
|
|
|
if (m_selectionToDelete.state() != Selection::RANGE)
|
|
return;
|
|
|
|
deleteCollapsibleWhitespace(m_selectionToDelete);
|
|
Selection selection = endingSelection();
|
|
|
|
Position upstreamStart(selection.start().equivalentUpstreamPosition());
|
|
Position downstreamStart(selection.start().equivalentDownstreamPosition());
|
|
Position upstreamEnd(selection.end().equivalentUpstreamPosition());
|
|
Position downstreamEnd(selection.end().equivalentDownstreamPosition());
|
|
|
|
NodeImpl *startBlock = upstreamStart.node()->enclosingBlockFlowElement();
|
|
NodeImpl *endBlock = downstreamEnd.node()->enclosingBlockFlowElement();
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[Delete:Start]" << upstreamStart << downstreamStart << endl;
|
|
kDebug() << "[Delete:End]" << upstreamEnd << downstreamEnd << endl;
|
|
printEnclosingBlockTree(upstreamStart.node());
|
|
#endif
|
|
if (startBlock != endBlock)
|
|
printEnclosingBlockTree(downstreamEnd.node());
|
|
|
|
if (upstreamStart == downstreamEnd)
|
|
// after collapsing whitespace, selection is empty...no work to do
|
|
return;
|
|
|
|
// remove all the nodes that are completely covered by the selection
|
|
if (upstreamStart.node() != downstreamEnd.node()) {
|
|
NodeImpl *node, *next;
|
|
for (node = upstreamStart.node()->traverseNextNode(); node && node != downstreamEnd.node(); node = next) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[traverse and delete]" << node << (node->renderer() && node->renderer()->isEditable()) << endl;
|
|
#endif
|
|
next = node->traverseNextNode();
|
|
if (node->renderer() && node->renderer()->isEditable())
|
|
removeNode(node); // removeAndPrune?
|
|
}
|
|
}
|
|
|
|
// if we have different blocks then merge content of the second into first one
|
|
if (startBlock != endBlock && startBlock->parentNode() == endBlock->parentNode()) {
|
|
NodeImpl *node = endBlock->firstChild();
|
|
while (node) {
|
|
NodeImpl *moveNode = node;
|
|
node = node->nextSibling();
|
|
removeNode(moveNode);
|
|
appendNode(startBlock, moveNode);
|
|
}
|
|
}
|
|
|
|
if (upstreamStart.node() == downstreamEnd.node())
|
|
deleteContentInsideNode(upstreamEnd.node(), upstreamStart.offset(), downstreamEnd.offset());
|
|
else {
|
|
deleteContentAfterOffset(upstreamStart.node(), upstreamStart.offset());
|
|
deleteContentBeforeOffset(downstreamEnd.node(), downstreamEnd.offset());
|
|
}
|
|
|
|
setEndingSelection(upstreamStart);
|
|
#if 0
|
|
Position endingPosition;
|
|
bool adjustEndingPositionDownstream = false;
|
|
|
|
bool onlyWhitespace = containsOnlyWhitespace(upstreamStart, downstreamEnd);
|
|
kDebug() << "[OnlyWhitespace]" << onlyWhitespace << endl;
|
|
|
|
bool startCompletelySelected = !onlyWhitespace &&
|
|
(downstreamStart.offset() <= downstreamStart.node()->caretMinOffset() &&
|
|
((downstreamStart.node() != upstreamEnd.node()) ||
|
|
(upstreamEnd.offset() >= upstreamEnd.node()->caretMaxOffset())));
|
|
|
|
bool endCompletelySelected = !onlyWhitespace &&
|
|
(upstreamEnd.offset() >= upstreamEnd.node()->caretMaxOffset() &&
|
|
((downstreamStart.node() != upstreamEnd.node()) ||
|
|
(downstreamStart.offset() <= downstreamStart.node()->caretMinOffset())));
|
|
|
|
kDebug() << "[{start:end}CompletelySelected]" << startCompletelySelected << endCompletelySelected << endl;
|
|
|
|
unsigned long startRenderedOffset = downstreamStart.renderedOffset();
|
|
|
|
bool startAtStartOfRootEditableElement = startRenderedOffset == 0 && downstreamStart.inFirstEditableInRootEditableElement();
|
|
bool startAtStartOfBlock = startAtStartOfRootEditableElement ||
|
|
(startRenderedOffset == 0 && downstreamStart.inFirstEditableInContainingEditableBlock());
|
|
bool endAtEndOfBlock = downstreamEnd.isLastRenderedPositionInEditableBlock();
|
|
|
|
kDebug() << "[startAtStartOfRootEditableElement]" << startAtStartOfRootEditableElement << endl;
|
|
kDebug() << "[startAtStartOfBlock]" << startAtStartOfBlock << endl;
|
|
kDebug() << "[endAtEndOfBlock]" << endAtEndOfBlock << endl;
|
|
|
|
NodeImpl *startBlock = upstreamStart.node()->enclosingBlockFlowElement();
|
|
NodeImpl *endBlock = downstreamEnd.node()->enclosingBlockFlowElement();
|
|
bool startBlockEndBlockAreSiblings = startBlock->parentNode() == endBlock->parentNode();
|
|
|
|
kDebug() << "[startBlockEndBlockAreSiblings]" << startBlockEndBlockAreSiblings << startBlock << endBlock << endl;
|
|
|
|
debugPosition("upstreamStart: ", upstreamStart);
|
|
debugPosition("downstreamStart: ", downstreamStart);
|
|
debugPosition("upstreamEnd: ", upstreamEnd);
|
|
debugPosition("downstreamEnd: ", downstreamEnd);
|
|
kDebug(6200) << "start selected:" << (startCompletelySelected ? "YES" : "NO");
|
|
kDebug(6200) << "at start block:" << (startAtStartOfBlock ? "YES" : "NO");
|
|
kDebug(6200) << "at start root block:"<< (startAtStartOfRootEditableElement ? "YES" : "NO");
|
|
kDebug(6200) << "at end block:"<< (endAtEndOfBlock ? "YES" : "NO");
|
|
kDebug(6200) << "only whitespace:"<< (onlyWhitespace ? "YES" : "NO");
|
|
|
|
// Determine where to put the caret after the deletion
|
|
if (startAtStartOfBlock) {
|
|
kDebug(6200) << "ending position case 1";
|
|
endingPosition = Position(startBlock, 0);
|
|
adjustEndingPositionDownstream = true;
|
|
} else if (!startCompletelySelected) {
|
|
kDebug(6200) << "ending position case 2";
|
|
endingPosition = upstreamEnd; // FIXME ??????????? upstreamStart;
|
|
if (upstreamStart.node()->id() == ID_BR && upstreamStart.offset() == 1)
|
|
adjustEndingPositionDownstream = true;
|
|
} else if (upstreamStart != downstreamStart) {
|
|
kDebug(6200) << "ending position case 3";
|
|
endingPosition = upstreamStart;
|
|
if (upstreamStart.node()->id() == ID_BR && upstreamStart.offset() == 1)
|
|
adjustEndingPositionDownstream = true;
|
|
}
|
|
|
|
//
|
|
// Figure out the whitespace conversions to do
|
|
//
|
|
if ((startAtStartOfBlock && !endAtEndOfBlock) || (!startCompletelySelected && adjustEndingPositionDownstream)) {
|
|
// convert trailing whitespace
|
|
Position trailing = trailingWhitespacePosition(downstreamEnd.equivalentDownstreamPosition());
|
|
if (trailing.notEmpty()) {
|
|
debugPosition("convertTrailingWhitespace: ", trailing);
|
|
Position collapse = trailing.nextCharacterPosition();
|
|
if (collapse != trailing)
|
|
deleteCollapsibleWhitespace(collapse);
|
|
TextImpl *textNode = static_cast<TextImpl *>(trailing.node());
|
|
replaceText(textNode, trailing.offset(), 1, nonBreakingSpaceString());
|
|
}
|
|
} else if (!startAtStartOfBlock && endAtEndOfBlock) {
|
|
// convert leading whitespace
|
|
Position leading = leadingWhitespacePosition(upstreamStart.equivalentUpstreamPosition());
|
|
if (leading.notEmpty()) {
|
|
debugPosition("convertLeadingWhitespace: ", leading);
|
|
TextImpl *textNode = static_cast<TextImpl *>(leading.node());
|
|
replaceText(textNode, leading.offset(), 1, nonBreakingSpaceString());
|
|
}
|
|
} else if (!startAtStartOfBlock && !endAtEndOfBlock) {
|
|
// convert contiguous whitespace
|
|
Position leading = leadingWhitespacePosition(upstreamStart.equivalentUpstreamPosition());
|
|
Position trailing = trailingWhitespacePosition(downstreamEnd.equivalentDownstreamPosition());
|
|
if (leading.notEmpty() && trailing.notEmpty()) {
|
|
debugPosition("convertLeadingWhitespace [contiguous]: ", leading);
|
|
TextImpl *textNode = static_cast<TextImpl *>(leading.node());
|
|
replaceText(textNode, leading.offset(), 1, nonBreakingSpaceString());
|
|
}
|
|
}
|
|
|
|
//
|
|
// Do the delete
|
|
//
|
|
NodeImpl *n = downstreamStart.node()->traverseNextNode();
|
|
kDebug() << "[n]" << n << endl;
|
|
|
|
// work on start node
|
|
if (startCompletelySelected) {
|
|
kDebug(6200) << "start node delete case 1";
|
|
removeNodeAndPrune(downstreamStart.node(), startBlock);
|
|
} else if (onlyWhitespace) {
|
|
// Selection only contains whitespace. This is really a special-case to
|
|
// handle significant whitespace that is collapsed at the end of a line,
|
|
// but also handles deleting a space in mid-line.
|
|
kDebug(6200) << "start node delete case 2";
|
|
assert(upstreamStart.node()->isTextNode());
|
|
TextImpl *text = static_cast<TextImpl *>(upstreamStart.node());
|
|
int offset = upstreamStart.offset();
|
|
// EDIT FIXME: Signed/unsigned mismatch
|
|
int length = text->length();
|
|
if (length == upstreamStart.offset())
|
|
offset--;
|
|
// FIXME ??? deleteText(text, offset, 1);
|
|
} else if (downstreamStart.node()->isTextNode()) {
|
|
kDebug(6200) << "start node delete case 3";
|
|
TextImpl *text = static_cast<TextImpl *>(downstreamStart.node());
|
|
int endOffset = text == upstreamEnd.node() ? upstreamEnd.offset() : text->length();
|
|
if (endOffset > downstreamStart.offset()) {
|
|
deleteText(text, downstreamStart.offset(), endOffset - downstreamStart.offset());
|
|
}
|
|
} else {
|
|
// we have clipped the end of a non-text element
|
|
// the offset must be 1 here. if it is, do nothing and move on.
|
|
kDebug(6200) << "start node delete case 4";
|
|
assert(downstreamStart.offset() == 1);
|
|
}
|
|
|
|
if (n && !onlyWhitespace && downstreamStart.node() != upstreamEnd.node()) {
|
|
// work on intermediate nodes
|
|
while (n && n != upstreamEnd.node()) {
|
|
NodeImpl *d = n;
|
|
n = n->traverseNextNode();
|
|
if (d->renderer() && d->renderer()->isEditable())
|
|
removeNodeAndPrune(d, startBlock);
|
|
}
|
|
if (!n)
|
|
return;
|
|
|
|
// work on end node
|
|
assert(n == upstreamEnd.node());
|
|
if (endCompletelySelected) {
|
|
removeNodeAndPrune(upstreamEnd.node(), startBlock);
|
|
}
|
|
else if (upstreamEnd.node()->isTextNode()) {
|
|
if (upstreamEnd.offset() > 0) {
|
|
TextImpl *text = static_cast<TextImpl *>(upstreamEnd.node());
|
|
deleteText(text, 0, upstreamEnd.offset());
|
|
}
|
|
}
|
|
else {
|
|
// we have clipped the beginning of a non-text element
|
|
// the offset must be 0 here. if it is, do nothing and move on.
|
|
assert(downstreamStart.offset() == 0);
|
|
}
|
|
}
|
|
|
|
// Do block merge if start and end of selection are in different blocks
|
|
// and the blocks are siblings. This is a first cut at this rule arrived
|
|
// at by doing a bunch of edits and settling on the behavior that made
|
|
// the most sense. This could change in the future as we get more
|
|
// experience with how this should behave.
|
|
if (startBlock != endBlock && startBlockEndBlockAreSiblings) {
|
|
kDebug(6200) << "merging content to start block";
|
|
NodeImpl *node = endBlock->firstChild();
|
|
while (node) {
|
|
NodeImpl *moveNode = node;
|
|
node = node->nextSibling();
|
|
removeNode(moveNode);
|
|
appendNode(startBlock, moveNode);
|
|
}
|
|
}
|
|
|
|
if (adjustEndingPositionDownstream) {
|
|
kDebug(6200) << "adjust ending position downstream";
|
|
endingPosition = endingPosition.equivalentDownstreamPosition();
|
|
}
|
|
|
|
debugPosition("ending position: ", endingPosition);
|
|
setEndingSelection(endingPosition);
|
|
|
|
kDebug(6200) << "-----------------------------------------------------";
|
|
#endif
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// DeleteTextCommandImpl
|
|
|
|
DeleteTextCommandImpl::DeleteTextCommandImpl(DocumentImpl *document, TextImpl *node, long offset, long count)
|
|
: EditCommandImpl(document), m_node(node), m_offset(offset), m_count(count)
|
|
{
|
|
assert(m_node);
|
|
assert(m_offset >= 0);
|
|
assert(m_count >= 0);
|
|
|
|
m_node->ref();
|
|
}
|
|
|
|
DeleteTextCommandImpl::~DeleteTextCommandImpl()
|
|
{
|
|
if (m_node)
|
|
m_node->deref();
|
|
}
|
|
|
|
void DeleteTextCommandImpl::doApply()
|
|
{
|
|
assert(m_node);
|
|
|
|
int exceptionCode = 0;
|
|
m_text = m_node->substringData(m_offset, m_count, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_node->deleteData(m_offset, m_count, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
void DeleteTextCommandImpl::doUnapply()
|
|
{
|
|
assert(m_node);
|
|
assert(!m_text.isEmpty());
|
|
|
|
int exceptionCode = 0;
|
|
m_node->insertData(m_offset, m_text, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// InputNewlineCommandImpl
|
|
|
|
InputNewlineCommandImpl::InputNewlineCommandImpl(DocumentImpl *document)
|
|
: CompositeEditCommandImpl(document)
|
|
{
|
|
}
|
|
|
|
InputNewlineCommandImpl::~InputNewlineCommandImpl()
|
|
{
|
|
}
|
|
|
|
void InputNewlineCommandImpl::insertNodeAfterPosition(NodeImpl *node, const Position &pos)
|
|
{
|
|
// Insert the BR after the caret position. In the case the
|
|
// position is a block, do an append. We don't want to insert
|
|
// the BR *after* the block.
|
|
Position upstream(pos.equivalentUpstreamPosition());
|
|
NodeImpl *cb = pos.node()->enclosingBlockFlowElement();
|
|
if (cb == pos.node())
|
|
appendNode(cb, node);
|
|
else
|
|
insertNodeAfter(node, pos.node());
|
|
}
|
|
|
|
void InputNewlineCommandImpl::insertNodeBeforePosition(NodeImpl *node, const Position &pos)
|
|
{
|
|
// Insert the BR after the caret position. In the case the
|
|
// position is a block, do an append. We don't want to insert
|
|
// the BR *before* the block.
|
|
Position upstream(pos.equivalentUpstreamPosition());
|
|
NodeImpl *cb = pos.node()->enclosingBlockFlowElement();
|
|
if (cb == pos.node())
|
|
appendNode(cb, node);
|
|
else
|
|
insertNodeBefore(node, pos.node());
|
|
}
|
|
|
|
void InputNewlineCommandImpl::doApply()
|
|
{
|
|
deleteSelection();
|
|
Selection selection = endingSelection();
|
|
int exceptionCode = 0;
|
|
|
|
NodeImpl *enclosingBlock = selection.start().node()->enclosingBlockFlowElement();
|
|
kDebug() << enclosingBlock->nodeName() << endl;
|
|
if (enclosingBlock->id() == ID_LI) {
|
|
// need to insert new list item or split existing one into 2
|
|
// consider example: <li>x<u>x<b>x|x</b>x</u>x</li> (| - caret position)
|
|
// result should look like: <li>x<u>x<b>x</b></u></li><li><u>|x<b>x</b></u></li>
|
|
// idea is to walk up to the li item and split and reattach correspondent nodes
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[insert new list item]" << selection << endl;
|
|
printEnclosingBlockTree(selection.start().node());
|
|
#endif
|
|
Position pos(selection.start().equivalentDownstreamPosition());
|
|
NodeImpl *node = pos.node();
|
|
bool atBlockStart = pos.atStartOfContainingEditableBlock();
|
|
bool atBlockEnd = pos.isLastRenderedPositionInEditableBlock();
|
|
// split text node into 2 if we are in the middle
|
|
if (node->isTextNode() && !atBlockStart && !atBlockEnd) {
|
|
TextImpl *textNode = static_cast<TextImpl*>(node);
|
|
TextImpl *textBeforeNode = document()->createTextNode(textNode->substringData(0, selection.start().offset(), exceptionCode));
|
|
deleteText(textNode, 0, pos.offset());
|
|
insertNodeBefore(textBeforeNode, textNode);
|
|
pos = Position(textNode, 0);
|
|
setEndingSelection(pos);
|
|
|
|
// walk up and reattach
|
|
while (true) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[handle node]" << node << endl;
|
|
printEnclosingBlockTree(enclosingBlock->parent());
|
|
#endif
|
|
NodeImpl *parent = node->parent();
|
|
// FIXME copy attributes, styles etc too
|
|
RefPtr<NodeImpl> newParent = parent->cloneNode(false);
|
|
insertNodeAfter(newParent.get(), parent);
|
|
for (NodeImpl *nextSibling = 0; node; node = nextSibling) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[reattach sibling]" << node << endl;
|
|
#endif
|
|
nextSibling = node->nextSibling();
|
|
removeNode(node);
|
|
appendNode(newParent.get(), node);
|
|
}
|
|
node = newParent.get();
|
|
if (parent == enclosingBlock)
|
|
break;
|
|
}
|
|
} else if (node->isTextNode()) {
|
|
// insert <br> node either as previous list or the next one
|
|
if (atBlockStart) {
|
|
ElementImpl *listItem = document()->createHTMLElement("LI");
|
|
insertNodeBefore(listItem, enclosingBlock);
|
|
} else {
|
|
ElementImpl *listItem = document()->createHTMLElement("LI");
|
|
insertNodeAfter(listItem, enclosingBlock);
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[result]" << endl;
|
|
printEnclosingBlockTree(enclosingBlock->parent());
|
|
#endif
|
|
// FIXME set selection after operation
|
|
return;
|
|
}
|
|
|
|
ElementImpl *breakNode = document()->createHTMLElement("BR");
|
|
// assert(exceptionCode == 0);
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[insert break]" << selection << endl;
|
|
printEnclosingBlockTree(enclosingBlock);
|
|
#endif
|
|
|
|
NodeImpl *nodeToInsert = breakNode;
|
|
// Handle the case where there is a typing style.
|
|
if (document()->part()->editor()->typingStyle()) {
|
|
int exceptionCode = 0;
|
|
ElementImpl *styleElement = createTypingStyleElement();
|
|
styleElement->appendChild(breakNode, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
nodeToInsert = styleElement;
|
|
}
|
|
|
|
Position pos(selection.start().equivalentDownstreamPosition());
|
|
bool atStart = pos.offset() <= pos.node()->caretMinOffset();
|
|
bool atEndOfBlock = pos.isLastRenderedPositionInEditableBlock();
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[pos]" << pos << atStart << atEndOfBlock << endl;
|
|
#endif
|
|
|
|
if (atEndOfBlock) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "input newline case 1";
|
|
#endif
|
|
// Insert an "extra" BR at the end of the block. This makes the "real" BR we want
|
|
// to insert appear in the rendering without any significant side effects (and no
|
|
// real worries either since you can't arrow past this extra one.
|
|
insertNodeAfterPosition(nodeToInsert, pos);
|
|
exceptionCode = 0;
|
|
ElementImpl *extraBreakNode = document()->createHTMLElement("BR");
|
|
// assert(exceptionCode == 0);
|
|
insertNodeAfter(extraBreakNode, nodeToInsert);
|
|
setEndingSelection(Position(extraBreakNode, 0));
|
|
} else if (atStart) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "input newline case 2";
|
|
#endif
|
|
// Insert node, but place the caret into index 0 of the downstream
|
|
// position. This will make the caret appear after the break, and as we know
|
|
// there is content at that location, this is OK.
|
|
insertNodeBeforePosition(nodeToInsert, pos);
|
|
setEndingSelection(Position(pos.node(), 0));
|
|
} else {
|
|
// Split a text node
|
|
// FIXME it's possible that we create empty text node now if we're at the end of text
|
|
// maybe we should handle this case specially and not create it
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug(6200) << "input newline case 3";
|
|
#endif
|
|
assert(pos.node()->isTextNode());
|
|
TextImpl *textNode = static_cast<TextImpl *>(pos.node());
|
|
TextImpl *textBeforeNode = document()->createTextNode(textNode->substringData(0, selection.start().offset(), exceptionCode));
|
|
deleteText(textNode, 0, selection.start().offset());
|
|
insertNodeBefore(textBeforeNode, textNode);
|
|
insertNodeBefore(nodeToInsert, textNode);
|
|
setEndingSelection(Position(textNode, 0));
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// InputTextCommandImpl
|
|
|
|
InputTextCommandImpl::InputTextCommandImpl(DocumentImpl *document)
|
|
: CompositeEditCommandImpl(document), m_charactersAdded(0)
|
|
{
|
|
}
|
|
|
|
InputTextCommandImpl::~InputTextCommandImpl()
|
|
{
|
|
}
|
|
|
|
void InputTextCommandImpl::doApply()
|
|
{
|
|
}
|
|
|
|
void InputTextCommandImpl::input(const DOMString &text)
|
|
{
|
|
execute(text);
|
|
}
|
|
|
|
void InputTextCommandImpl::deleteCharacter()
|
|
{
|
|
assert(state() == Applied);
|
|
|
|
Selection selection = endingSelection();
|
|
|
|
if (!selection.start().node()->isTextNode())
|
|
return;
|
|
|
|
int exceptionCode = 0;
|
|
int offset = selection.start().offset() - 1;
|
|
if (offset >= selection.start().node()->caretMinOffset()) {
|
|
TextImpl *textNode = static_cast<TextImpl *>(selection.start().node());
|
|
textNode->deleteData(offset, 1, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
selection = Selection(Position(textNode, offset));
|
|
setEndingSelection(selection);
|
|
m_charactersAdded--;
|
|
}
|
|
}
|
|
|
|
Position InputTextCommandImpl::prepareForTextInsertion(bool adjustDownstream)
|
|
{
|
|
// Prepare for text input by looking at the current position.
|
|
// It may be necessary to insert a text node to receive characters.
|
|
Selection selection = endingSelection();
|
|
assert(selection.state() == Selection::CARET);
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[prepare selection]" << selection << endl;
|
|
#endif
|
|
|
|
Position pos = selection.start();
|
|
if (adjustDownstream)
|
|
pos = pos.equivalentDownstreamPosition();
|
|
else
|
|
pos = pos.equivalentUpstreamPosition();
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[prepare position]" << pos << endl;
|
|
#endif
|
|
|
|
if (!pos.node()->isTextNode()) {
|
|
NodeImpl *textNode = document()->createEditingTextNode("");
|
|
NodeImpl *nodeToInsert = textNode;
|
|
if (document()->part()->editor()->typingStyle()) {
|
|
int exceptionCode = 0;
|
|
ElementImpl *styleElement = createTypingStyleElement();
|
|
styleElement->appendChild(textNode, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
nodeToInsert = styleElement;
|
|
}
|
|
|
|
// Now insert the node in the right place
|
|
if (pos.node()->isEditableBlock()) {
|
|
kDebug(6200) << "prepareForTextInsertion case 1";
|
|
appendNode(pos.node(), nodeToInsert);
|
|
} else if (pos.node()->id() == ID_BR && pos.offset() == 1) {
|
|
kDebug(6200) << "prepareForTextInsertion case 2";
|
|
insertNodeAfter(nodeToInsert, pos.node());
|
|
} else if (pos.node()->caretMinOffset() == pos.offset()) {
|
|
kDebug(6200) << "prepareForTextInsertion case 3";
|
|
insertNodeBefore(nodeToInsert, pos.node());
|
|
} else if (pos.node()->caretMaxOffset() == pos.offset()) {
|
|
kDebug(6200) << "prepareForTextInsertion case 4";
|
|
insertNodeAfter(nodeToInsert, pos.node());
|
|
} else
|
|
assert(false);
|
|
|
|
pos = Position(textNode, 0);
|
|
} else {
|
|
// Handle the case where there is a typing style.
|
|
if (document()->part()->editor()->typingStyle()) {
|
|
if (pos.node()->isTextNode() && pos.offset() > pos.node()->caretMinOffset() && pos.offset() < pos.node()->caretMaxOffset()) {
|
|
// Need to split current text node in order to insert a span.
|
|
TextImpl *text = static_cast<TextImpl *>(pos.node());
|
|
RefPtr<SplitTextNodeCommandImpl> cmd = new SplitTextNodeCommandImpl(document(), text, pos.offset());
|
|
applyCommandToComposite(cmd);
|
|
setEndingSelection(Position(cmd->node(), 0));
|
|
}
|
|
|
|
int exceptionCode = 0;
|
|
TextImpl *editingTextNode = document()->createEditingTextNode("");
|
|
|
|
ElementImpl *styleElement = createTypingStyleElement();
|
|
styleElement->appendChild(editingTextNode, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
NodeImpl *node = endingSelection().start().node();
|
|
if (endingSelection().start().isLastRenderedPositionOnLine())
|
|
insertNodeAfter(styleElement, node);
|
|
else
|
|
insertNodeBefore(styleElement, node);
|
|
pos = Position(editingTextNode, 0);
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
void InputTextCommandImpl::execute(const DOMString &text)
|
|
{
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[execute command]" << text << endl;
|
|
#endif
|
|
Selection selection = endingSelection();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[ending selection]" << selection << endl;
|
|
#endif
|
|
bool adjustDownstream = selection.start().isFirstRenderedPositionOnLine();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[adjust]" << adjustDownstream << endl;
|
|
#endif
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
printEnclosingBlockTree(selection.start().node());
|
|
#endif
|
|
|
|
// Delete the current selection, or collapse whitespace, as needed
|
|
if (selection.state() == Selection::RANGE)
|
|
deleteSelection();
|
|
else
|
|
deleteCollapsibleWhitespace();
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[after collapsible whitespace deletion]" << endl;
|
|
printEnclosingBlockTree(selection.start().node());
|
|
#endif
|
|
|
|
// EDIT FIXME: Need to take typing style from upstream text, if any.
|
|
|
|
// Make sure the document is set up to receive text
|
|
Position pos = prepareForTextInsertion(adjustDownstream);
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[after prepare]" << pos << endl;
|
|
#endif
|
|
|
|
TextImpl *textNode = static_cast<TextImpl *>(pos.node());
|
|
long offset = pos.offset();
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[insert at]" << textNode << offset << endl;
|
|
#endif
|
|
|
|
// This is a temporary implementation for inserting adjoining spaces
|
|
// into a document. We are working on a CSS-related whitespace solution
|
|
// that will replace this some day.
|
|
if (isWS(text))
|
|
insertSpace(textNode, offset);
|
|
else {
|
|
const DOMString &existingText = textNode->data();
|
|
if (textNode->length() >= 2 && offset >= 2 && isNBSP(existingText[offset - 1]) && !isWS(existingText[offset - 2])) {
|
|
// DOM looks like this:
|
|
// character nbsp caret
|
|
// As we are about to insert a non-whitespace character at the caret
|
|
// convert the nbsp to a regular space.
|
|
// EDIT FIXME: This needs to be improved some day to convert back only
|
|
// those nbsp's added by the editor to make rendering come out right.
|
|
replaceText(textNode, offset - 1, 1, " ");
|
|
}
|
|
insertText(textNode, offset, text);
|
|
}
|
|
setEndingSelection(Position(textNode, offset + text.length()));
|
|
m_charactersAdded += text.length();
|
|
}
|
|
|
|
void InputTextCommandImpl::insertSpace(TextImpl *textNode, unsigned long offset)
|
|
{
|
|
assert(textNode);
|
|
|
|
DOMString text(textNode->data());
|
|
|
|
// count up all spaces and newlines in front of the caret
|
|
// delete all collapsed ones
|
|
// this will work out OK since the offset we have been passed has been upstream-ized
|
|
int count = 0;
|
|
for (unsigned int i = offset; i < text.length(); i++) {
|
|
if (isWS(text[i]))
|
|
count++;
|
|
else
|
|
break;
|
|
}
|
|
if (count > 0) {
|
|
// By checking the character at the downstream position, we can
|
|
// check if there is a rendered WS at the caret
|
|
Position pos(textNode, offset);
|
|
Position downstream = pos.equivalentDownstreamPosition();
|
|
if (downstream.offset() < (long)text.length() && isWS(text[downstream.offset()]))
|
|
count--; // leave this WS in
|
|
if (count > 0)
|
|
deleteText(textNode, offset, count);
|
|
}
|
|
|
|
if (offset > 0 && offset <= text.length() - 1 && !isWS(text[offset]) && !isWS(text[offset - 1])) {
|
|
// insert a "regular" space
|
|
insertText(textNode, offset, " ");
|
|
return;
|
|
}
|
|
|
|
if (text.length() >= 2 && offset >= 2 && isNBSP(text[offset - 2]) && isNBSP(text[offset - 1])) {
|
|
// DOM looks like this:
|
|
// nbsp nbsp caret
|
|
// insert a space between the two nbsps
|
|
insertText(textNode, offset - 1, " ");
|
|
return;
|
|
}
|
|
|
|
// insert an nbsp
|
|
insertText(textNode, offset, nonBreakingSpaceString());
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// InsertNodeBeforeCommandImpl
|
|
|
|
InsertNodeBeforeCommandImpl::InsertNodeBeforeCommandImpl(DocumentImpl *document, NodeImpl *insertChild, NodeImpl *refChild)
|
|
: EditCommandImpl(document), m_insertChild(insertChild), m_refChild(refChild)
|
|
{
|
|
assert(m_insertChild);
|
|
m_insertChild->ref();
|
|
|
|
assert(m_refChild);
|
|
m_refChild->ref();
|
|
}
|
|
|
|
InsertNodeBeforeCommandImpl::~InsertNodeBeforeCommandImpl()
|
|
{
|
|
if (m_insertChild)
|
|
m_insertChild->deref();
|
|
if (m_refChild)
|
|
m_refChild->deref();
|
|
}
|
|
|
|
void InsertNodeBeforeCommandImpl::doApply()
|
|
{
|
|
assert(m_insertChild);
|
|
assert(m_refChild);
|
|
assert(m_refChild->parentNode());
|
|
|
|
int exceptionCode = 0;
|
|
m_refChild->parentNode()->insertBefore(m_insertChild, m_refChild, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
void InsertNodeBeforeCommandImpl::doUnapply()
|
|
{
|
|
assert(m_insertChild);
|
|
assert(m_refChild);
|
|
assert(m_refChild->parentNode());
|
|
|
|
int exceptionCode = 0;
|
|
m_refChild->parentNode()->removeChild(m_insertChild, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// InsertTextCommandImpl
|
|
|
|
InsertTextCommandImpl::InsertTextCommandImpl(DocumentImpl *document, TextImpl *node, long offset, const DOMString &text)
|
|
: EditCommandImpl(document), m_node(node), m_offset(offset)
|
|
{
|
|
assert(m_node);
|
|
assert(m_offset >= 0);
|
|
assert(text.length() > 0);
|
|
|
|
m_node->ref();
|
|
m_text = text.copy(); // make a copy to ensure that the string never changes
|
|
}
|
|
|
|
InsertTextCommandImpl::~InsertTextCommandImpl()
|
|
{
|
|
if (m_node)
|
|
m_node->deref();
|
|
}
|
|
|
|
void InsertTextCommandImpl::doApply()
|
|
{
|
|
assert(m_node);
|
|
assert(!m_text.isEmpty());
|
|
|
|
int exceptionCode = 0;
|
|
m_node->insertData(m_offset, m_text, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
void InsertTextCommandImpl::doUnapply()
|
|
{
|
|
assert(m_node);
|
|
assert(!m_text.isEmpty());
|
|
|
|
int exceptionCode = 0;
|
|
m_node->deleteData(m_offset, m_text.length(), exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// JoinTextNodesCommandImpl
|
|
|
|
JoinTextNodesCommandImpl::JoinTextNodesCommandImpl(DocumentImpl *document, TextImpl *text1, TextImpl *text2)
|
|
: EditCommandImpl(document), m_text1(text1), m_text2(text2)
|
|
{
|
|
assert(m_text1);
|
|
assert(m_text2);
|
|
assert(m_text1->nextSibling() == m_text2);
|
|
assert(m_text1->length() > 0);
|
|
assert(m_text2->length() > 0);
|
|
|
|
m_text1->ref();
|
|
m_text2->ref();
|
|
}
|
|
|
|
JoinTextNodesCommandImpl::~JoinTextNodesCommandImpl()
|
|
{
|
|
if (m_text1)
|
|
m_text1->deref();
|
|
if (m_text2)
|
|
m_text2->deref();
|
|
}
|
|
|
|
void JoinTextNodesCommandImpl::doApply()
|
|
{
|
|
assert(m_text1);
|
|
assert(m_text2);
|
|
assert(m_text1->nextSibling() == m_text2);
|
|
|
|
int exceptionCode = 0;
|
|
m_text2->insertData(0, m_text1->data(), exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_text2->parentNode()->removeChild(m_text1, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_offset = m_text1->length();
|
|
}
|
|
|
|
void JoinTextNodesCommandImpl::doUnapply()
|
|
{
|
|
assert(m_text2);
|
|
assert(m_offset > 0);
|
|
|
|
int exceptionCode = 0;
|
|
|
|
m_text2->deleteData(0, m_offset, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_text2->parentNode()->insertBefore(m_text1, m_text2, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
assert(m_text2->previousSibling()->isTextNode());
|
|
assert(m_text2->previousSibling() == m_text1);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// ReplaceSelectionCommandImpl
|
|
|
|
ReplaceSelectionCommandImpl::ReplaceSelectionCommandImpl(DocumentImpl *document, DOM::DocumentFragmentImpl *fragment, bool selectReplacement)
|
|
: CompositeEditCommandImpl(document), m_fragment(fragment), m_selectReplacement(selectReplacement)
|
|
{
|
|
}
|
|
|
|
ReplaceSelectionCommandImpl::~ReplaceSelectionCommandImpl()
|
|
{
|
|
}
|
|
|
|
void ReplaceSelectionCommandImpl::doApply()
|
|
{
|
|
NodeImpl *firstChild = m_fragment->firstChild();
|
|
NodeImpl *lastChild = m_fragment->lastChild();
|
|
|
|
Selection selection = endingSelection();
|
|
|
|
// Delete the current selection, or collapse whitespace, as needed
|
|
if (selection.state() == Selection::RANGE)
|
|
deleteSelection();
|
|
else
|
|
deleteCollapsibleWhitespace();
|
|
|
|
selection = endingSelection();
|
|
assert(!selection.isEmpty());
|
|
|
|
if (!firstChild) {
|
|
// Pasting something that didn't parse or was empty.
|
|
assert(!lastChild);
|
|
} else if (firstChild == lastChild && firstChild->isTextNode()) {
|
|
// Simple text paste. Treat as if the text were typed.
|
|
Position base = selection.base();
|
|
inputText(static_cast<TextImpl *>(firstChild)->data());
|
|
if (m_selectReplacement) {
|
|
setEndingSelection(Selection(base, endingSelection().extent()));
|
|
}
|
|
}
|
|
else {
|
|
// HTML fragment paste.
|
|
NodeImpl *beforeNode = firstChild;
|
|
NodeImpl *node = firstChild->nextSibling();
|
|
|
|
insertNodeAt(firstChild, selection.start().node(), selection.start().offset());
|
|
|
|
// Insert the nodes from the fragment
|
|
while (node) {
|
|
NodeImpl *next = node->nextSibling();
|
|
insertNodeAfter(node, beforeNode);
|
|
beforeNode = node;
|
|
node = next;
|
|
}
|
|
assert(beforeNode);
|
|
|
|
// Find the last leaf.
|
|
NodeImpl *lastLeaf = lastChild;
|
|
while (1) {
|
|
NodeImpl *nextChild = lastLeaf->lastChild();
|
|
if (!nextChild)
|
|
break;
|
|
lastLeaf = nextChild;
|
|
}
|
|
|
|
if (m_selectReplacement) {
|
|
// Find the first leaf.
|
|
NodeImpl *firstLeaf = firstChild;
|
|
while (1) {
|
|
NodeImpl *nextChild = firstLeaf->firstChild();
|
|
if (!nextChild)
|
|
break;
|
|
firstLeaf = nextChild;
|
|
}
|
|
// Select what was inserted.
|
|
setEndingSelection(Selection(Position(firstLeaf, firstLeaf->caretMinOffset()), Position(lastLeaf, lastLeaf->caretMaxOffset())));
|
|
} else {
|
|
// Place the cursor after what was inserted.
|
|
setEndingSelection(Position(lastLeaf, lastLeaf->caretMaxOffset()));
|
|
}
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// MoveSelectionCommandImpl
|
|
|
|
MoveSelectionCommandImpl::MoveSelectionCommandImpl(DocumentImpl *document, DOM::DocumentFragmentImpl *fragment, DOM::Position &position)
|
|
: CompositeEditCommandImpl(document), m_fragment(fragment), m_position(position)
|
|
{
|
|
}
|
|
|
|
MoveSelectionCommandImpl::~MoveSelectionCommandImpl()
|
|
{
|
|
}
|
|
|
|
void MoveSelectionCommandImpl::doApply()
|
|
{
|
|
Selection selection = endingSelection();
|
|
assert(selection.state() == Selection::RANGE);
|
|
|
|
// Update the position otherwise it may become invalid after the selection is deleted.
|
|
NodeImpl *positionNode = m_position.node();
|
|
long positionOffset = m_position.offset();
|
|
Position selectionEnd = selection.end();
|
|
long selectionEndOffset = selectionEnd.offset();
|
|
if (selectionEnd.node() == positionNode && selectionEndOffset < positionOffset) {
|
|
positionOffset -= selectionEndOffset;
|
|
Position selectionStart = selection.start();
|
|
if (selectionStart.node() == positionNode) {
|
|
positionOffset += selectionStart.offset();
|
|
}
|
|
}
|
|
|
|
deleteSelection();
|
|
|
|
setEndingSelection(Position(positionNode, positionOffset));
|
|
RefPtr<ReplaceSelectionCommandImpl> cmd = new ReplaceSelectionCommandImpl(document(), m_fragment, true);
|
|
applyCommandToComposite(cmd);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// RemoveCSSPropertyCommandImpl
|
|
|
|
RemoveCSSPropertyCommandImpl::RemoveCSSPropertyCommandImpl(DocumentImpl *document, CSSStyleDeclarationImpl *decl, int property)
|
|
: EditCommandImpl(document), m_decl(decl), m_property(property), m_important(false)
|
|
{
|
|
assert(m_decl);
|
|
m_decl->ref();
|
|
}
|
|
|
|
RemoveCSSPropertyCommandImpl::~RemoveCSSPropertyCommandImpl()
|
|
{
|
|
assert(m_decl);
|
|
m_decl->deref();
|
|
}
|
|
|
|
void RemoveCSSPropertyCommandImpl::doApply()
|
|
{
|
|
assert(m_decl);
|
|
|
|
m_oldValue = m_decl->getPropertyValue(m_property);
|
|
assert(!m_oldValue.isNull());
|
|
|
|
m_important = m_decl->getPropertyPriority(m_property);
|
|
m_decl->removeProperty(m_property);
|
|
}
|
|
|
|
void RemoveCSSPropertyCommandImpl::doUnapply()
|
|
{
|
|
assert(m_decl);
|
|
assert(!m_oldValue.isNull());
|
|
|
|
m_decl->setProperty(m_property, m_oldValue, m_important);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// RemoveNodeAttributeCommandImpl
|
|
|
|
RemoveNodeAttributeCommandImpl::RemoveNodeAttributeCommandImpl(DocumentImpl *document, ElementImpl *element, NodeImpl::Id attribute)
|
|
: EditCommandImpl(document), m_element(element), m_attribute(attribute)
|
|
{
|
|
assert(m_element);
|
|
m_element->ref();
|
|
}
|
|
|
|
RemoveNodeAttributeCommandImpl::~RemoveNodeAttributeCommandImpl()
|
|
{
|
|
assert(m_element);
|
|
m_element->deref();
|
|
}
|
|
|
|
void RemoveNodeAttributeCommandImpl::doApply()
|
|
{
|
|
assert(m_element);
|
|
|
|
m_oldValue = m_element->getAttribute(m_attribute);
|
|
assert(!m_oldValue.isNull());
|
|
|
|
int exceptionCode = 0;
|
|
m_element->removeAttribute(m_attribute, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
void RemoveNodeAttributeCommandImpl::doUnapply()
|
|
{
|
|
assert(m_element);
|
|
assert(!m_oldValue.isNull());
|
|
|
|
// int exceptionCode = 0;
|
|
m_element->setAttribute(m_attribute, m_oldValue.implementation());
|
|
// assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// RemoveNodeCommandImpl
|
|
|
|
RemoveNodeCommandImpl::RemoveNodeCommandImpl(DocumentImpl *document, NodeImpl *removeChild)
|
|
: EditCommandImpl(document), m_parent(0), m_removeChild(removeChild), m_refChild(0)
|
|
{
|
|
assert(m_removeChild);
|
|
m_removeChild->ref();
|
|
|
|
m_parent = m_removeChild->parentNode();
|
|
assert(m_parent);
|
|
m_parent->ref();
|
|
|
|
RefPtr<DOM::NodeListImpl> children = m_parent->childNodes();
|
|
for (long i = children->length() - 1; i >= 0; --i) {
|
|
NodeImpl *node = children->item(i);
|
|
if (node == m_removeChild)
|
|
break;
|
|
m_refChild = node;
|
|
}
|
|
|
|
if (m_refChild)
|
|
m_refChild->ref();
|
|
}
|
|
|
|
RemoveNodeCommandImpl::~RemoveNodeCommandImpl()
|
|
{
|
|
if (m_parent)
|
|
m_parent->deref();
|
|
if (m_removeChild)
|
|
m_removeChild->deref();
|
|
if (m_refChild)
|
|
m_refChild->deref();
|
|
}
|
|
|
|
void RemoveNodeCommandImpl::doApply()
|
|
{
|
|
assert(m_parent);
|
|
assert(m_removeChild);
|
|
|
|
int exceptionCode = 0;
|
|
m_parent->removeChild(m_removeChild, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
void RemoveNodeCommandImpl::doUnapply()
|
|
{
|
|
assert(m_parent);
|
|
assert(m_removeChild);
|
|
|
|
int exceptionCode = 0;
|
|
if (m_refChild)
|
|
m_parent->insertBefore(m_removeChild, m_refChild, exceptionCode);
|
|
else
|
|
m_parent->appendChild(m_removeChild, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// RemoveNodeAndPruneCommandImpl
|
|
|
|
RemoveNodeAndPruneCommandImpl::RemoveNodeAndPruneCommandImpl(DocumentImpl *document, NodeImpl *pruneNode, NodeImpl *stopNode)
|
|
: CompositeEditCommandImpl(document), m_pruneNode(pruneNode), m_stopNode(stopNode)
|
|
{
|
|
assert(m_pruneNode);
|
|
m_pruneNode->ref();
|
|
if (m_stopNode)
|
|
m_stopNode->ref();
|
|
}
|
|
|
|
RemoveNodeAndPruneCommandImpl::~RemoveNodeAndPruneCommandImpl()
|
|
{
|
|
m_pruneNode->deref();
|
|
if (m_stopNode)
|
|
m_stopNode->deref();
|
|
}
|
|
|
|
void RemoveNodeAndPruneCommandImpl::doApply()
|
|
{
|
|
NodeImpl *editableBlock = m_pruneNode->enclosingBlockFlowElement();
|
|
NodeImpl *pruneNode = m_pruneNode;
|
|
NodeImpl *node = pruneNode->traversePreviousNode();
|
|
removeNode(pruneNode);
|
|
while (1) {
|
|
if (node == m_stopNode || editableBlock != node->enclosingBlockFlowElement() || !shouldPruneNode(node))
|
|
break;
|
|
pruneNode = node;
|
|
node = node->traversePreviousNode();
|
|
removeNode(pruneNode);
|
|
}
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// RemoveNodePreservingChildrenCommandImpl
|
|
|
|
RemoveNodePreservingChildrenCommandImpl::RemoveNodePreservingChildrenCommandImpl(DocumentImpl *document, NodeImpl *node)
|
|
: CompositeEditCommandImpl(document), m_node(node)
|
|
{
|
|
assert(m_node);
|
|
m_node->ref();
|
|
}
|
|
|
|
RemoveNodePreservingChildrenCommandImpl::~RemoveNodePreservingChildrenCommandImpl()
|
|
{
|
|
if (m_node)
|
|
m_node->deref();
|
|
}
|
|
|
|
void RemoveNodePreservingChildrenCommandImpl::doApply()
|
|
{
|
|
RefPtr<DOM::NodeListImpl> children = node()->childNodes();
|
|
const unsigned int length = children->length();
|
|
for (unsigned int i = 0; i < length; ++i) {
|
|
NodeImpl *child = children->item(0);
|
|
removeNode(child);
|
|
insertNodeBefore(child, node());
|
|
}
|
|
removeNode(node());
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// SetNodeAttributeCommandImpl
|
|
|
|
SetNodeAttributeCommandImpl::SetNodeAttributeCommandImpl(DocumentImpl *document, ElementImpl *element, NodeImpl::Id attribute, const DOMString &value)
|
|
: EditCommandImpl(document), m_element(element), m_attribute(attribute), m_value(value)
|
|
{
|
|
assert(m_element);
|
|
m_element->ref();
|
|
assert(!m_value.isNull());
|
|
}
|
|
|
|
SetNodeAttributeCommandImpl::~SetNodeAttributeCommandImpl()
|
|
{
|
|
if (m_element)
|
|
m_element->deref();
|
|
}
|
|
|
|
void SetNodeAttributeCommandImpl::doApply()
|
|
{
|
|
assert(m_element);
|
|
assert(!m_value.isNull());
|
|
|
|
// int exceptionCode = 0;
|
|
m_oldValue = m_element->getAttribute(m_attribute);
|
|
m_element->setAttribute(m_attribute, m_value.implementation());
|
|
// assert(exceptionCode == 0);
|
|
}
|
|
|
|
void SetNodeAttributeCommandImpl::doUnapply()
|
|
{
|
|
assert(m_element);
|
|
assert(!m_oldValue.isNull());
|
|
|
|
// int exceptionCode = 0;
|
|
m_element->setAttribute(m_attribute, m_oldValue.implementation());
|
|
// assert(exceptionCode == 0);
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// SplitTextNodeCommandImpl
|
|
|
|
SplitTextNodeCommandImpl::SplitTextNodeCommandImpl(DocumentImpl *document, TextImpl *text, long offset)
|
|
: EditCommandImpl(document), m_text1(0), m_text2(text), m_offset(offset)
|
|
{
|
|
assert(m_text2);
|
|
assert(m_text2->length() > 0);
|
|
|
|
m_text2->ref();
|
|
}
|
|
|
|
SplitTextNodeCommandImpl::~SplitTextNodeCommandImpl()
|
|
{
|
|
if (m_text1)
|
|
m_text1->deref();
|
|
if (m_text2)
|
|
m_text2->deref();
|
|
}
|
|
|
|
void SplitTextNodeCommandImpl::doApply()
|
|
{
|
|
assert(m_text2);
|
|
assert(m_offset > 0);
|
|
|
|
int exceptionCode = 0;
|
|
|
|
// EDIT FIXME: This should use better smarts for figuring out which portion
|
|
// of the split to copy (based on their comparative sizes). We should also
|
|
// just use the DOM's splitText function.
|
|
|
|
if (!m_text1) {
|
|
// create only if needed.
|
|
// if reapplying, this object will already exist.
|
|
m_text1 = document()->createTextNode(m_text2->substringData(0, m_offset, exceptionCode));
|
|
assert(exceptionCode == 0);
|
|
assert(m_text1);
|
|
m_text1->ref();
|
|
}
|
|
|
|
m_text2->deleteData(0, m_offset, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_text2->parentNode()->insertBefore(m_text1, m_text2, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
assert(m_text2->previousSibling()->isTextNode());
|
|
assert(m_text2->previousSibling() == m_text1);
|
|
}
|
|
|
|
void SplitTextNodeCommandImpl::doUnapply()
|
|
{
|
|
assert(m_text1);
|
|
assert(m_text2);
|
|
|
|
assert(m_text1->nextSibling() == m_text2);
|
|
|
|
int exceptionCode = 0;
|
|
m_text2->insertData(0, m_text1->data(), exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_text2->parentNode()->removeChild(m_text1, exceptionCode);
|
|
assert(exceptionCode == 0);
|
|
|
|
m_offset = m_text1->length();
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// TypingCommandImpl
|
|
|
|
TypingCommandImpl::TypingCommandImpl(DocumentImpl *document)
|
|
: CompositeEditCommandImpl(document), m_openForMoreTyping(true)
|
|
{
|
|
}
|
|
|
|
TypingCommandImpl::~TypingCommandImpl()
|
|
{
|
|
}
|
|
|
|
void TypingCommandImpl::doApply()
|
|
{
|
|
}
|
|
|
|
void TypingCommandImpl::typingAddedToOpenCommand()
|
|
{
|
|
assert(document());
|
|
assert(document()->part());
|
|
document()->part()->editor()->appliedEditing(this);
|
|
}
|
|
|
|
void TypingCommandImpl::insertText(const DOMString &text)
|
|
{
|
|
if (document()->part()->editor()->typingStyle() || m_cmds.count() == 0) {
|
|
RefPtr<InputTextCommandImpl> cmd = new InputTextCommandImpl(document());
|
|
applyCommandToComposite(cmd);
|
|
cmd->input(text);
|
|
} else {
|
|
EditCommandImpl *lastCommand = m_cmds.last().get();
|
|
if (lastCommand->isInputTextCommand()) {
|
|
static_cast<InputTextCommandImpl*>(lastCommand)->input(text);
|
|
} else {
|
|
RefPtr<InputTextCommandImpl> cmd = new InputTextCommandImpl(document());
|
|
applyCommandToComposite(cmd);
|
|
cmd->input(text);
|
|
}
|
|
}
|
|
typingAddedToOpenCommand();
|
|
}
|
|
|
|
void TypingCommandImpl::insertNewline()
|
|
{
|
|
RefPtr<InputNewlineCommandImpl> cmd = new InputNewlineCommandImpl(document());
|
|
applyCommandToComposite(cmd);
|
|
typingAddedToOpenCommand();
|
|
}
|
|
|
|
void TypingCommandImpl::issueCommandForDeleteKey()
|
|
{
|
|
Selection selectionToDelete = endingSelection();
|
|
assert(selectionToDelete.state() != Selection::NONE);
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[selection]" << selectionToDelete << endl;
|
|
#endif
|
|
if (selectionToDelete.state() == Selection::CARET) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[caret selection]" << endl;
|
|
#endif
|
|
Position pos(selectionToDelete.start());
|
|
if (pos.inFirstEditableInRootEditableElement() && pos.offset() <= pos.node()->caretMinOffset()) {
|
|
// we're at the start of a root editable block...do nothing
|
|
return;
|
|
}
|
|
selectionToDelete = Selection(pos.previousCharacterPosition(), pos);
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[modified selection]" << selectionToDelete << endl;
|
|
#endif
|
|
}
|
|
deleteSelection(selectionToDelete);
|
|
typingAddedToOpenCommand();
|
|
}
|
|
|
|
void TypingCommandImpl::deleteKeyPressed()
|
|
{
|
|
// EDIT FIXME: The ifdef'ed out code below should be re-enabled.
|
|
// In order for this to happen, the deleteCharacter case
|
|
// needs work. Specifically, the caret-positioning code
|
|
// and whitespace-handling code in DeleteSelectionCommandImpl::doApply()
|
|
// needs to be factored out so it can be used again here.
|
|
// Until that work is done, issueCommandForDeleteKey() does the
|
|
// right thing, but less efficiently and with the cost of more
|
|
// objects.
|
|
issueCommandForDeleteKey();
|
|
#if 0
|
|
if (m_cmds.count() == 0) {
|
|
issueCommandForDeleteKey();
|
|
}
|
|
else {
|
|
EditCommand lastCommand = m_cmds.last();
|
|
if (lastCommand.commandID() == InputTextCommandID) {
|
|
InputTextCommand cmd = static_cast<InputTextCommand &>(lastCommand);
|
|
cmd.deleteCharacter();
|
|
if (cmd.charactersAdded() == 0) {
|
|
removeCommand(cmd);
|
|
}
|
|
}
|
|
else if (lastCommand.commandID() == InputNewlineCommandID) {
|
|
lastCommand.unapply();
|
|
removeCommand(lastCommand);
|
|
}
|
|
else {
|
|
issueCommandForDeleteKey();
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void TypingCommandImpl::removeCommand(const PassRefPtr<EditCommandImpl> cmd)
|
|
{
|
|
// NOTE: If the passed-in command is the last command in the
|
|
// composite, we could remove all traces of this typing command
|
|
// from the system, including the undo chain. Other editors do
|
|
// not do this, but we could.
|
|
|
|
m_cmds.removeAll(cmd);
|
|
if (m_cmds.count() == 0)
|
|
setEndingSelection(startingSelection());
|
|
else
|
|
setEndingSelection(m_cmds.last()->endingSelection());
|
|
}
|
|
|
|
static bool isOpenForMoreTypingCommand(const EditCommandImpl *command)
|
|
{
|
|
return command && command->isTypingCommand() &&
|
|
static_cast<const TypingCommandImpl*>(command)->openForMoreTyping();
|
|
}
|
|
|
|
void TypingCommandImpl::deleteKeyPressed0(DocumentImpl *document)
|
|
{
|
|
//Editor *editor = document->part()->editor();
|
|
// FIXME reenable after properly modify selection of the lastEditCommand
|
|
// if (isOpenForMoreTypingCommand(lastEditCommand)) {
|
|
// static_cast<TypingCommand &>(lastEditCommand).deleteKeyPressed();
|
|
// } else {
|
|
RefPtr<TypingCommandImpl> command = new TypingCommandImpl(document);
|
|
command->apply();
|
|
command->deleteKeyPressed();
|
|
// }
|
|
}
|
|
|
|
void TypingCommandImpl::insertNewline0(DocumentImpl *document)
|
|
{
|
|
assert(document);
|
|
Editor *ed = document->part()->editor();
|
|
assert(ed);
|
|
EditCommandImpl *lastEditCommand = ed->lastEditCommand().get();
|
|
if (isOpenForMoreTypingCommand(lastEditCommand)) {
|
|
static_cast<TypingCommandImpl*>(lastEditCommand)->insertNewline();
|
|
} else {
|
|
RefPtr<TypingCommandImpl> command = new TypingCommandImpl(document);
|
|
command->apply();
|
|
command->insertNewline();
|
|
}
|
|
}
|
|
|
|
void TypingCommandImpl::insertText0(DocumentImpl *document, const DOMString &text)
|
|
{
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[insert text]" << text << endl;
|
|
#endif
|
|
assert(document);
|
|
Editor *ed = document->part()->editor();
|
|
assert(ed);
|
|
EditCommandImpl *lastEditCommand = ed->lastEditCommand().get();
|
|
if (isOpenForMoreTypingCommand(lastEditCommand)) {
|
|
static_cast<TypingCommandImpl*>(lastEditCommand)->insertText(text);
|
|
} else {
|
|
RefPtr<TypingCommandImpl> command = new TypingCommandImpl(document);
|
|
command->apply();
|
|
command->insertText(text);
|
|
}
|
|
}
|
|
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// InsertListCommandImpl
|
|
|
|
InsertListCommandImpl::InsertListCommandImpl(DocumentImpl *document, Type type)
|
|
: CompositeEditCommandImpl(document), m_listType(type)
|
|
{
|
|
}
|
|
|
|
InsertListCommandImpl::~InsertListCommandImpl()
|
|
{
|
|
}
|
|
|
|
void InsertListCommandImpl::doApply()
|
|
{
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[make current selection/paragraph a list]" << endingSelection() << endl;
|
|
#endif
|
|
Position start = endingSelection().start();
|
|
Position end = endingSelection().end();
|
|
ElementImpl *startBlock = start.node()->enclosingBlockFlowElement();
|
|
ElementImpl *endBlock = end.node()->enclosingBlockFlowElement();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[start:end blocks]" << startBlock << endBlock << endl;
|
|
printEnclosingBlockTree(start.node());
|
|
#endif
|
|
if (startBlock == endBlock) {
|
|
if (startBlock->id() == ID_LI) {
|
|
// we already have a list item, remove it then
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[remove list item]" << endl;
|
|
#endif
|
|
NodeImpl *listBlock = startBlock->parent(); // it's either <ol> or <ul>
|
|
// we need to properly split or even remove the list leaving 2 lists:
|
|
// [listBlock->firstChild(), startBlock) and (startBlock, listBlock->lastChild()]
|
|
if (listBlock->firstChild() == listBlock->lastChild() && listBlock->firstChild() == startBlock) {
|
|
// get rid of list completely
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[remove list completely]" << endl;
|
|
#endif
|
|
removeNodePreservingChildren(listBlock);
|
|
removeNodePreservingChildren(startBlock);
|
|
} else if (!startBlock->previousSibling()) {
|
|
// move nodes from this list item before the list
|
|
NodeImpl *nextSibling;
|
|
for (NodeImpl *node = startBlock->firstChild(); node; node = nextSibling) {
|
|
nextSibling = node->nextSibling();
|
|
removeNode(node);
|
|
insertNodeBefore(node, listBlock);
|
|
}
|
|
removeNode(startBlock);
|
|
} else if (!startBlock->nextSibling()) {
|
|
// move nodes from this list item after the list
|
|
NodeImpl *nextSibling;
|
|
for (NodeImpl *node = startBlock->lastChild(); node; node = nextSibling) {
|
|
nextSibling = node->previousSibling();
|
|
removeNode(node);
|
|
insertNodeAfter(node, listBlock);
|
|
}
|
|
removeNode(startBlock);
|
|
} else {
|
|
// split list into 2 and nodes from this list item goes between lists
|
|
WTF::PassRefPtr<NodeImpl> newListBlock = listBlock->cloneNode(false);
|
|
insertNodeAfter(newListBlock.get(), listBlock);
|
|
NodeImpl *node, *nextSibling;
|
|
for (node = startBlock->nextSibling(); node; node = nextSibling) {
|
|
nextSibling = node->nextSibling();
|
|
removeNode(node);
|
|
appendNode(newListBlock.get(), node);
|
|
}
|
|
for (node = startBlock->firstChild(); node; node = nextSibling) {
|
|
nextSibling = node->nextSibling();
|
|
removeNode(node);
|
|
insertNodeBefore(node, newListBlock.get());
|
|
}
|
|
removeNode(startBlock);
|
|
}
|
|
} else {
|
|
ElementImpl *ol = document()->createHTMLElement(m_listType == OrderedList ? "OL" : "UL");
|
|
ElementImpl *li = document()->createHTMLElement("LI");
|
|
appendNode(ol, li);
|
|
NodeImpl *nextNode;
|
|
for (NodeImpl *node = startBlock->firstChild(); node; node = nextNode) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[reattach node]" << node << endl;
|
|
#endif
|
|
nextNode = node->nextSibling();
|
|
removeNode(node);
|
|
appendNode(li, node);
|
|
}
|
|
appendNode(startBlock, ol);
|
|
}
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[different blocks are not supported yet]" << endl;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void InsertListCommandImpl::insertList(DocumentImpl *document, Type type)
|
|
{
|
|
RefPtr<InsertListCommandImpl> insertCommand = new InsertListCommandImpl(document, type);
|
|
insertCommand->apply();
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
// IndentOutdentCommandImpl
|
|
|
|
IndentOutdentCommandImpl::IndentOutdentCommandImpl(DocumentImpl *document, Type type)
|
|
: CompositeEditCommandImpl(document), m_commandType(type)
|
|
{
|
|
}
|
|
|
|
IndentOutdentCommandImpl::~IndentOutdentCommandImpl()
|
|
{
|
|
}
|
|
|
|
void IndentOutdentCommandImpl::indent()
|
|
{
|
|
Selection selection = endingSelection();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[indent selection]" << selection << endl;
|
|
#endif
|
|
NodeImpl *startBlock = selection.start().node()->enclosingBlockFlowElement();
|
|
NodeImpl *endBlock = selection.end().node()->enclosingBlockFlowElement();
|
|
|
|
if (startBlock == endBlock) {
|
|
// check if selection is the list, but not fully covered
|
|
if (startBlock->id() == ID_LI && (startBlock->previousSibling() || startBlock->nextSibling())) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[modify list]" << endl;
|
|
#endif
|
|
RefPtr<NodeImpl> newList = startBlock->parent()->cloneNode(false);
|
|
insertNodeAfter(newList.get(), startBlock);
|
|
removeNode(startBlock);
|
|
appendNode(newList.get(), startBlock);
|
|
} else {
|
|
NodeImpl *blockquoteElement = document()->createHTMLElement("blockquote");
|
|
if (startBlock->id() == ID_LI) {
|
|
startBlock = startBlock->parent();
|
|
NodeImpl *parent = startBlock->parent();
|
|
removeNode(startBlock);
|
|
appendNode(parent, blockquoteElement);
|
|
appendNode(blockquoteElement, startBlock);
|
|
} else {
|
|
NodeImpl *parent = startBlock->parent();
|
|
removeNode(startBlock);
|
|
appendNode(parent, blockquoteElement);
|
|
appendNode(blockquoteElement, startBlock);
|
|
}
|
|
}
|
|
} else {
|
|
if (startBlock->id() == ID_LI && endBlock->id() == ID_LI && startBlock->parent() == endBlock->parent()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[indent some items inside list]" << endl;
|
|
#endif
|
|
RefPtr<NodeImpl> nestedList = startBlock->parent()->cloneNode(false);
|
|
insertNodeBefore(nestedList.get(), startBlock);
|
|
NodeImpl *nextNode = 0;
|
|
for (NodeImpl *node = startBlock;; node = nextNode) {
|
|
nextNode = node->nextSibling();
|
|
removeNode(node);
|
|
appendNode(nestedList.get(), node);
|
|
if (node == endBlock)
|
|
break;
|
|
}
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[blocks not from one list are not supported yet]" << endl;
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool hasPreviousListItem(NodeImpl *node)
|
|
{
|
|
while (node) {
|
|
node = node->previousSibling();
|
|
if (node && node->id() == ID_LI)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static bool hasNextListItem(NodeImpl *node)
|
|
{
|
|
while (node) {
|
|
node = node->nextSibling();
|
|
if (node && node->id() == ID_LI)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void IndentOutdentCommandImpl::outdent()
|
|
{
|
|
Selection selection = endingSelection();
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[indent selection]" << selection << endl;
|
|
#endif
|
|
NodeImpl *startBlock = selection.start().node()->enclosingBlockFlowElement();
|
|
NodeImpl *endBlock = selection.end().node()->enclosingBlockFlowElement();
|
|
|
|
if (startBlock->id() == ID_LI && endBlock->id() == ID_LI && startBlock->parent() == endBlock->parent()) {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[list items selected]" << endl;
|
|
#endif
|
|
bool firstItemSelected = !hasPreviousListItem(startBlock);
|
|
bool lastItemSelected = !hasNextListItem(endBlock);
|
|
bool listFullySelected = firstItemSelected && lastItemSelected;
|
|
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[first/last item selected]" << firstItemSelected << lastItemSelected << endl;
|
|
#endif
|
|
|
|
NodeImpl *listNode = startBlock->parent();
|
|
printEnclosingBlockTree(listNode);
|
|
bool hasParentList = listNode->parent()->id() == ID_OL || listNode->parent()->id() == ID_UL;
|
|
|
|
if (!firstItemSelected && !lastItemSelected) {
|
|
// split the list into 2 and reattach all the nodes before the first selected item to the second list
|
|
RefPtr<NodeImpl> clonedList = listNode->cloneNode(false);
|
|
NodeImpl *nextNode = 0;
|
|
for (NodeImpl *node = listNode->firstChild(); node != startBlock; node = nextNode) {
|
|
nextNode = node->nextSibling();
|
|
removeNode(node);
|
|
appendNode(clonedList.get(), node);
|
|
}
|
|
insertNodeBefore(clonedList.get(), listNode);
|
|
// so now the first item selected
|
|
firstItemSelected = true;
|
|
}
|
|
|
|
NodeImpl *nextNode = 0;
|
|
for (NodeImpl *node = firstItemSelected ? startBlock : endBlock;; node = nextNode) {
|
|
nextNode = firstItemSelected ? node->nextSibling() : node->previousSibling();
|
|
removeNode(node);
|
|
if (firstItemSelected)
|
|
insertNodeBefore(node, listNode);
|
|
else
|
|
insertNodeAfter(node, listNode);
|
|
if (!hasParentList && node->id() == ID_LI) {
|
|
insertNodeAfter(document()->createHTMLElement("BR"), node);
|
|
removeNodePreservingChildren(node);
|
|
}
|
|
if (node == (firstItemSelected ? endBlock : startBlock))
|
|
break;
|
|
}
|
|
if (listFullySelected)
|
|
removeNode(listNode);
|
|
return;
|
|
}
|
|
|
|
|
|
if (startBlock == endBlock) {
|
|
if (startBlock->id() == ID_BLOCKQUOTE) {
|
|
removeNodePreservingChildren(startBlock);
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[not the list or blockquote]" << endl;
|
|
#endif
|
|
}
|
|
} else {
|
|
#ifdef DEBUG_COMMANDS
|
|
kDebug() << "[blocks not from one list are not supported yet]" << endl;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void IndentOutdentCommandImpl::doApply()
|
|
{
|
|
if (m_commandType == Indent)
|
|
indent();
|
|
else
|
|
outdent();
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------------
|
|
|
|
} // namespace khtml
|
|
|