Serenity Operating System
at master 459 lines 17 kB view raw
1/* 2 * Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <LibWeb/Bindings/Intrinsics.h> 8#include <LibWeb/DOM/Document.h> 9#include <LibWeb/DOM/Range.h> 10#include <LibWeb/Selection/Selection.h> 11 12namespace Web::Selection { 13 14WebIDL::ExceptionOr<JS::NonnullGCPtr<Selection>> Selection::create(JS::NonnullGCPtr<JS::Realm> realm, JS::NonnullGCPtr<DOM::Document> document) 15{ 16 return MUST_OR_THROW_OOM(realm->heap().allocate<Selection>(realm, realm, document)); 17} 18 19Selection::Selection(JS::NonnullGCPtr<JS::Realm> realm, JS::NonnullGCPtr<DOM::Document> document) 20 : PlatformObject(realm) 21 , m_document(document) 22{ 23} 24 25Selection::~Selection() = default; 26 27JS::ThrowCompletionOr<void> Selection::initialize(JS::Realm& realm) 28{ 29 MUST_OR_THROW_OOM(Base::initialize(realm)); 30 set_prototype(&Bindings::ensure_web_prototype<Bindings::SelectionPrototype>(realm, "Selection")); 31 32 return {}; 33} 34 35// https://w3c.github.io/selection-api/#dfn-empty 36bool Selection::is_empty() const 37{ 38 // Each selection can be associated with a single range. 39 // When there is no range associated with the selection, the selection is empty. 40 // The selection must be initially empty. 41 42 // NOTE: This function should not be confused with Selection.empty() which empties the selection. 43 return !m_range; 44} 45 46void Selection::visit_edges(Cell::Visitor& visitor) 47{ 48 Base::visit_edges(visitor); 49 visitor.visit(m_range); 50 visitor.visit(m_document); 51} 52 53// https://w3c.github.io/selection-api/#dfn-anchor 54JS::GCPtr<DOM::Node> Selection::anchor_node() 55{ 56 if (!m_range) 57 return nullptr; 58 if (m_direction == Direction::Forwards) 59 return m_range->start_container(); 60 return m_range->end_container(); 61} 62 63// https://w3c.github.io/selection-api/#dfn-anchor 64unsigned Selection::anchor_offset() 65{ 66 if (!m_range) 67 return 0; 68 if (m_direction == Direction::Forwards) 69 return m_range->start_offset(); 70 return m_range->end_offset(); 71} 72 73// https://w3c.github.io/selection-api/#dfn-focus 74JS::GCPtr<DOM::Node> Selection::focus_node() 75{ 76 if (!m_range) 77 return nullptr; 78 if (m_direction == Direction::Forwards) 79 return m_range->end_container(); 80 return m_range->start_container(); 81} 82 83// https://w3c.github.io/selection-api/#dfn-focus 84unsigned Selection::focus_offset() const 85{ 86 if (!m_range) 87 return 0; 88 if (m_direction == Direction::Forwards) 89 return m_range->end_offset(); 90 return m_range->start_offset(); 91} 92 93// https://w3c.github.io/selection-api/#dom-selection-iscollapsed 94bool Selection::is_collapsed() const 95{ 96 // The attribute must return true if and only if the anchor and focus are the same 97 // (including if both are null). Otherwise it must return false. 98 return const_cast<Selection*>(this)->anchor_node() == const_cast<Selection*>(this)->focus_node(); 99} 100 101// https://w3c.github.io/selection-api/#dom-selection-rangecount 102unsigned Selection::range_count() const 103{ 104 if (m_range) 105 return 1; 106 return 0; 107} 108 109DeprecatedString Selection::type() const 110{ 111 if (!m_range) 112 return "None"; 113 if (m_range->collapsed()) 114 return "Caret"; 115 return "Range"; 116} 117 118// https://w3c.github.io/selection-api/#dom-selection-getrangeat 119WebIDL::ExceptionOr<JS::GCPtr<DOM::Range>> Selection::get_range_at(unsigned index) 120{ 121 // The method must throw an IndexSizeError exception if index is not 0, or if this is empty. 122 if (index != 0 || is_empty()) 123 return WebIDL::IndexSizeError::create(realm(), "Selection.getRangeAt() on empty Selection or with invalid argument"sv); 124 125 // Otherwise, it must return a reference to (not a copy of) this's range. 126 return m_range; 127} 128 129// https://w3c.github.io/selection-api/#dom-selection-addrange 130void Selection::add_range(JS::NonnullGCPtr<DOM::Range> range) 131{ 132 // 1. If the root of the range's boundary points are not the document associated with this, abort these steps. 133 if (&range->start_container()->root() != m_document.ptr()) 134 return; 135 136 // 2. If rangeCount is not 0, abort these steps. 137 if (range_count() != 0) 138 return; 139 140 // 3. Set this's range to range by a strong reference (not by making a copy). 141 set_range(range); 142} 143 144// https://w3c.github.io/selection-api/#dom-selection-removerange 145WebIDL::ExceptionOr<void> Selection::remove_range(JS::NonnullGCPtr<DOM::Range> range) 146{ 147 // The method must make this empty by disassociating its range if this's range is range. 148 if (m_range == range) { 149 set_range(nullptr); 150 return {}; 151 } 152 153 // Otherwise, it must throw a NotFoundError. 154 return WebIDL::NotFoundError::create(realm(), "Selection.removeRange() with invalid argument"sv); 155} 156 157// https://w3c.github.io/selection-api/#dom-selection-removeallranges 158void Selection::remove_all_ranges() 159{ 160 // The method must make this empty by disassociating its range if this has an associated range. 161 set_range(nullptr); 162} 163 164// https://w3c.github.io/selection-api/#dom-selection-empty 165void Selection::empty() 166{ 167 // The method must be an alias, and behave identically, to removeAllRanges(). 168 remove_all_ranges(); 169} 170 171// https://w3c.github.io/selection-api/#dom-selection-collapse 172WebIDL::ExceptionOr<void> Selection::collapse(JS::GCPtr<DOM::Node> node, unsigned offset) 173{ 174 // 1. If node is null, this method must behave identically as removeAllRanges() and abort these steps. 175 if (!node) { 176 remove_all_ranges(); 177 return {}; 178 } 179 180 // 2. The method must throw an IndexSizeError exception if offset is longer than node's length and abort these steps. 181 if (offset > node->length()) { 182 return WebIDL::IndexSizeError::create(realm(), "Selection.collapse() with offset longer than node's length"sv); 183 } 184 185 // 3. If node's root is not the document associated with this, abort these steps. 186 if (&node->root() != m_document.ptr()) 187 return {}; 188 189 // 4. Otherwise, let newRange be a new range. 190 auto new_range = TRY(DOM::Range::create(*m_document)); 191 192 // 5. Set the start the start and the end of newRange to (node, offset). 193 TRY(new_range->set_start(*node, offset)); 194 195 // 6. Set this's range to newRange. 196 set_range(new_range); 197 198 return {}; 199} 200 201// https://w3c.github.io/selection-api/#dom-selection-setposition 202WebIDL::ExceptionOr<void> Selection::set_position(JS::GCPtr<DOM::Node> node, unsigned offset) 203{ 204 // The method must be an alias, and behave identically, to collapse(). 205 return collapse(node, offset); 206} 207 208// https://w3c.github.io/selection-api/#dom-selection-collapsetostart 209WebIDL::ExceptionOr<void> Selection::collapse_to_start() 210{ 211 // 1. The method must throw InvalidStateError exception if the this is empty. 212 if (!m_range) { 213 return WebIDL::InvalidStateError::create(realm(), "Selection.collapse_to_start() on empty range"sv); 214 } 215 216 // 2. Otherwise, it must create a new range 217 auto new_range = TRY(DOM::Range::create(*m_document)); 218 219 // 3. Set the start both its start and end to the start of this's range 220 TRY(new_range->set_start(*anchor_node(), m_range->start_offset())); 221 TRY(new_range->set_end(*anchor_node(), m_range->start_offset())); 222 223 // 4. Then set this's range to the newly-created range. 224 set_range(new_range); 225 return {}; 226} 227 228// https://w3c.github.io/selection-api/#dom-selection-collapsetoend 229WebIDL::ExceptionOr<void> Selection::collapse_to_end() 230{ 231 // 1. The method must throw InvalidStateError exception if the this is empty. 232 if (!m_range) { 233 return WebIDL::InvalidStateError::create(realm(), "Selection.collapse_to_end() on empty range"sv); 234 } 235 236 // 2. Otherwise, it must create a new range 237 auto new_range = TRY(DOM::Range::create(*m_document)); 238 239 // 3. Set the start both its start and end to the start of this's range 240 TRY(new_range->set_start(*anchor_node(), m_range->end_offset())); 241 TRY(new_range->set_end(*anchor_node(), m_range->end_offset())); 242 243 // 4. Then set this's range to the newly-created range. 244 set_range(new_range); 245 246 return {}; 247} 248 249// https://w3c.github.io/selection-api/#dom-selection-extend 250WebIDL::ExceptionOr<void> Selection::extend(JS::NonnullGCPtr<DOM::Node> node, unsigned offset) 251{ 252 // 1. If node's root is not the document associated with this, abort these steps. 253 if (&node->root() != m_document.ptr()) 254 return {}; 255 256 // 2. If this is empty, throw an InvalidStateError exception and abort these steps. 257 if (!m_range) { 258 return WebIDL::InvalidStateError::create(realm(), "Selection.extend() on empty range"sv); 259 } 260 261 // 3. Let oldAnchor and oldFocus be the this's anchor and focus, and let newFocus be the boundary point (node, offset). 262 auto& old_anchor_node = *anchor_node(); 263 auto old_anchor_offset = anchor_offset(); 264 265 auto& new_focus_node = node; 266 auto new_focus_offset = offset; 267 268 // 4. Let newRange be a new range. 269 auto new_range = TRY(DOM::Range::create(*m_document)); 270 271 // 5. If node's root is not the same as the this's range's root, set the start newRange's start and end to newFocus. 272 if (&node->root() != &m_range->start_container()->root()) { 273 TRY(new_range->set_start(new_focus_node, new_focus_offset)); 274 } 275 // 6. Otherwise, if oldAnchor is before or equal to newFocus, set the start newRange's start to oldAnchor, then set its end to newFocus. 276 else if (old_anchor_node.is_before(new_focus_node) || &old_anchor_node == new_focus_node.ptr()) { 277 TRY(new_range->set_end(new_focus_node, new_focus_offset)); 278 } 279 // 7. Otherwise, set the start newRange's start to newFocus, then set its end to oldAnchor. 280 else { 281 TRY(new_range->set_start(new_focus_node, new_focus_offset)); 282 TRY(new_range->set_end(old_anchor_node, old_anchor_offset)); 283 } 284 285 // 8. Set this's range to newRange. 286 set_range(new_range); 287 288 // 9. If newFocus is before oldAnchor, set this's direction to backwards. Otherwise, set it to forwards. 289 if (new_focus_node->is_before(old_anchor_node)) { 290 m_direction = Direction::Backwards; 291 } else { 292 m_direction = Direction::Forwards; 293 } 294 295 return {}; 296} 297 298// https://w3c.github.io/selection-api/#dom-selection-setbaseandextent 299WebIDL::ExceptionOr<void> Selection::set_base_and_extent(JS::NonnullGCPtr<DOM::Node> anchor_node, unsigned anchor_offset, JS::NonnullGCPtr<DOM::Node> focus_node, unsigned focus_offset) 300{ 301 // 1. If anchorOffset is longer than anchorNode's length or if focusOffset is longer than focusNode's length, throw an IndexSizeError exception and abort these steps. 302 if (anchor_offset > anchor_node->length()) 303 return WebIDL::IndexSizeError::create(realm(), "Anchor offset points outside of the anchor node"); 304 305 if (focus_offset > focus_node->length()) 306 return WebIDL::IndexSizeError::create(realm(), "Focus offset points outside of the focus node"); 307 308 // 2. If the roots of anchorNode or focusNode are not the document associated with this, abort these steps. 309 if (&anchor_node->root() != m_document.ptr()) 310 return {}; 311 312 if (&focus_node->root() != m_document.ptr()) 313 return {}; 314 315 // 3. Let anchor be the boundary point (anchorNode, anchorOffset) and let focus be the boundary point (focusNode, focusOffset). 316 317 // 4. Let newRange be a new range. 318 auto new_range = TRY(DOM::Range::create(*m_document)); 319 320 // 5. If anchor is before focus, set the start the newRange's start to anchor and its end to focus. Otherwise, set the start them to focus and anchor respectively. 321 auto position_of_anchor_relative_to_focus = DOM::position_of_boundary_point_relative_to_other_boundary_point(anchor_node, anchor_offset, focus_node, focus_offset); 322 if (position_of_anchor_relative_to_focus == DOM::RelativeBoundaryPointPosition::Before) { 323 TRY(new_range->set_start(anchor_node, anchor_offset)); 324 TRY(new_range->set_end(focus_node, focus_offset)); 325 } else { 326 TRY(new_range->set_start(focus_node, focus_offset)); 327 TRY(new_range->set_end(anchor_node, anchor_offset)); 328 } 329 330 // 6. Set this's range to newRange. 331 set_range(new_range); 332 333 // 7. If focus is before anchor, set this's direction to backwards. Otherwise, set it to forwards 334 // NOTE: "Otherwise" can be seen as "focus is equal to or after anchor". 335 if (position_of_anchor_relative_to_focus == DOM::RelativeBoundaryPointPosition::After) 336 m_direction = Direction::Backwards; 337 else 338 m_direction = Direction::Forwards; 339 340 return {}; 341} 342 343// https://w3c.github.io/selection-api/#dom-selection-selectallchildren 344WebIDL::ExceptionOr<void> Selection::select_all_children(JS::NonnullGCPtr<DOM::Node> node) 345{ 346 // 1. If node's root is not the document associated with this, abort these steps. 347 if (&node->root() != m_document.ptr()) 348 return {}; 349 350 // 2. Let newRange be a new range and childCount be the number of children of node. 351 auto new_range = TRY(DOM::Range::create(*m_document)); 352 auto child_count = node->child_count(); 353 354 // 3. Set newRange's start to (node, 0). 355 TRY(new_range->set_start(node, 0)); 356 357 // 4. Set newRange's end to (node, childCount). 358 TRY(new_range->set_end(node, child_count)); 359 360 // 5. Set this's range to newRange. 361 set_range(new_range); 362 363 // 6. Set this's direction to forwards. 364 m_direction = Direction::Forwards; 365 366 return {}; 367} 368 369// https://w3c.github.io/selection-api/#dom-selection-deletefromdocument 370WebIDL::ExceptionOr<void> Selection::delete_from_document() 371{ 372 // The method must invoke deleteContents() on this's range if this is not empty. 373 // Otherwise the method must do nothing. 374 if (!is_empty()) 375 return m_range->delete_contents(); 376 return {}; 377} 378 379// https://w3c.github.io/selection-api/#dom-selection-containsnode 380bool Selection::contains_node(JS::NonnullGCPtr<DOM::Node> node, bool allow_partial_containment) const 381{ 382 // The method must return false if this is empty or if node's root is not the document associated with this. 383 if (!m_range) 384 return false; 385 if (&node->root() != m_document.ptr()) 386 return false; 387 388 // Otherwise, if allowPartialContainment is false, the method must return true if and only if 389 // start of its range is before or visually equivalent to the first boundary point in the node 390 // and end of its range is after or visually equivalent to the last boundary point in the node. 391 if (!allow_partial_containment) { 392 auto start_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( 393 *m_range->start_container(), 394 m_range->start_offset(), 395 node, 396 0); 397 auto end_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( 398 *m_range->end_container(), 399 m_range->end_offset(), 400 node, 401 node->length()); 402 403 return (start_relative_position == DOM::RelativeBoundaryPointPosition::Before || start_relative_position == DOM::RelativeBoundaryPointPosition::Equal) 404 && (end_relative_position == DOM::RelativeBoundaryPointPosition::Equal || end_relative_position == DOM::RelativeBoundaryPointPosition::After); 405 } 406 407 // If allowPartialContainment is true, the method must return true if and only if 408 // start of its range is before or visually equivalent to the last boundary point in the node 409 // and end of its range is after or visually equivalent to the first boundary point in the node. 410 411 auto start_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( 412 *m_range->start_container(), 413 m_range->start_offset(), 414 node, 415 node->length()); 416 auto end_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( 417 *m_range->end_container(), 418 m_range->end_offset(), 419 node, 420 0); 421 422 return (start_relative_position == DOM::RelativeBoundaryPointPosition::Before || start_relative_position == DOM::RelativeBoundaryPointPosition::Equal) 423 && (end_relative_position == DOM::RelativeBoundaryPointPosition::Equal || end_relative_position == DOM::RelativeBoundaryPointPosition::After); 424} 425 426DeprecatedString Selection::to_deprecated_string() const 427{ 428 // FIXME: This needs more work to be compatible with other engines. 429 // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=10583 430 if (!m_range) 431 return DeprecatedString::empty(); 432 return m_range->to_deprecated_string(); 433} 434 435JS::NonnullGCPtr<DOM::Document> Selection::document() const 436{ 437 return m_document; 438} 439 440JS::GCPtr<DOM::Range> Selection::range() const 441{ 442 return m_range; 443} 444 445void Selection::set_range(JS::GCPtr<DOM::Range> range) 446{ 447 if (m_range == range) 448 return; 449 450 if (m_range) 451 m_range->set_associated_selection({}, nullptr); 452 453 m_range = range; 454 455 if (m_range) 456 m_range->set_associated_selection({}, this); 457} 458 459}