/* * 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 #include 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(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(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(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(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(next.node())->data(); if (isWS(string[0])) return next; } } else { DOMString string = static_cast(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 > 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 cmd) { cmd->setStartingSelection(endingSelection());//###? cmd->setEndingSelection(endingSelection()); cmd->setParent(this); cmd->apply(); m_cmds.append(cmd); } void CompositeEditCommandImpl::insertNodeBefore(NodeImpl *insertChild, NodeImpl *refChild) { RefPtr 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(refChild), offset); insertNodeBefore(insertChild, refChild); } else { insertNodeAfter(insertChild, refChild); } } void CompositeEditCommandImpl::appendNode(NodeImpl *parent, NodeImpl *appendChild) { RefPtr cmd = new AppendNodeCommandImpl(document(), parent, appendChild); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::removeNode(NodeImpl *removeChild) { RefPtr cmd = new RemoveNodeCommandImpl(document(), removeChild); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::removeNodeAndPrune(NodeImpl *pruneNode, NodeImpl *stopNode) { RefPtr cmd = new RemoveNodeAndPruneCommandImpl(document(), pruneNode, stopNode); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::removeNodePreservingChildren(NodeImpl *removeChild) { RefPtr cmd = new RemoveNodePreservingChildrenCommandImpl(document(), removeChild); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::splitTextNode(TextImpl *text, long offset) { RefPtr cmd = new SplitTextNodeCommandImpl(document(), text, offset); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::joinTextNodes(TextImpl *text1, TextImpl *text2) { RefPtr cmd = new JoinTextNodesCommandImpl(document(), text1, text2); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::inputText(const DOMString &text) { RefPtr cmd = new InputTextCommandImpl(document()); applyCommandToComposite(cmd); cmd->input(text); } void CompositeEditCommandImpl::insertText(TextImpl *node, long offset, const DOMString &text) { RefPtr cmd = new InsertTextCommandImpl(document(), node, offset, text); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::deleteText(TextImpl *node, long offset, long count) { RefPtr cmd = new DeleteTextCommandImpl(document(), node, offset, count); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::replaceText(TextImpl *node, long offset, long count, const DOMString &replacementText) { RefPtr deleteCommand = new DeleteTextCommandImpl(document(), node, offset, count); applyCommandToComposite(deleteCommand); RefPtr insertCommand = new InsertTextCommandImpl(document(), node, offset, replacementText); applyCommandToComposite(insertCommand); } void CompositeEditCommandImpl::deleteSelection() { if (endingSelection().state() == Selection::RANGE) { RefPtr cmd = new DeleteSelectionCommandImpl(document()); applyCommandToComposite(cmd); } } void CompositeEditCommandImpl::deleteSelection(const Selection &selection) { if (selection.state() == Selection::RANGE) { RefPtr cmd = new DeleteSelectionCommandImpl(document(), selection); applyCommandToComposite(cmd); } } void CompositeEditCommandImpl::deleteCollapsibleWhitespace() { RefPtr cmd = new DeleteCollapsibleWhitespaceCommandImpl(document()); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::deleteCollapsibleWhitespace(const Selection &selection) { RefPtr cmd = new DeleteCollapsibleWhitespaceCommandImpl(document(), selection); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::removeCSSProperty(CSSStyleDeclarationImpl *decl, int property) { RefPtr cmd = new RemoveCSSPropertyCommandImpl(document(), decl, property); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::removeNodeAttribute(ElementImpl *element, int attribute) { RefPtr cmd = new RemoveNodeAttributeCommandImpl(document(), element, attribute); applyCommandToComposite(cmd); } void CompositeEditCommandImpl::setNodeAttribute(ElementImpl *element, int attribute, const DOMString &value) { RefPtr 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 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 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 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 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 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(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(start.node()); RefPtr 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(end.node()); RefPtr 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 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 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 split = new SplitTextNodeCommandImpl(document(), static_cast(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(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(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(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(deleteStart.node()); unsigned long count = downstream.offset() - deleteStart.offset(); assert(count <= textNode->length()); if (count == textNode->length()) { #ifdef DEBUG_COMMANDS kDebug(6200) << " removeNodeAndPrune 2:"<length() <<"start:" < 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(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(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(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(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(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(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(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(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(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(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(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:
  • xxx|xxx
  • (| - caret position) // result should look like:
  • xxx
  • |xx
  • // 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(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 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
    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(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(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(pos.node()); RefPtr 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(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(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 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 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 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 cmd = new InputTextCommandImpl(document()); applyCommandToComposite(cmd); cmd->input(text); } else { EditCommandImpl *lastCommand = m_cmds.last().get(); if (lastCommand->isInputTextCommand()) { static_cast(lastCommand)->input(text); } else { RefPtr cmd = new InputTextCommandImpl(document()); applyCommandToComposite(cmd); cmd->input(text); } } typingAddedToOpenCommand(); } void TypingCommandImpl::insertNewline() { RefPtr 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(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 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(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(lastEditCommand).deleteKeyPressed(); // } else { RefPtr 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(lastEditCommand)->insertNewline(); } else { RefPtr 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(lastEditCommand)->insertText(text); } else { RefPtr 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
      or
        // 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 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 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 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 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 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