Class: Dommy::Range

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods
Defined in:
lib/dommy/range.rb

Overview

‘Range` — a span between two boundary points in the DOM, used by text-editing / highlighting / selection logic.

Dommy has no layout, so methods that return pixel rectangles (‘getBoundingClientRect`, `getClientRects`) return zeroed values. All non-layout operations (selectNode, extractContents, cloneContents, surroundContents, deleteContents, toString, collapse, compareBoundaryPoints, intersectsNode, containsNode) work against the actual DOM tree.

Spec: dom.spec.whatwg.org/#interface-range

Constant Summary collapse

START_TO_START =

compareBoundaryPoints ‘how` constants.

0
START_TO_END =
1
END_TO_END =
2
END_TO_START =
3

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Constructor Details

#initialize(document) ⇒ Range

Returns a new instance of Range.



24
25
26
27
28
29
30
31
# File 'lib/dommy/range.rb', line 24

def initialize(document)
  @document = document
  # Default: collapsed at start of the document
  @start_container = document
  @start_offset = 0
  @end_container = document
  @end_offset = 0
end

Instance Attribute Details

#end_containerObject (readonly)

Returns the value of attribute end_container.



22
23
24
# File 'lib/dommy/range.rb', line 22

def end_container
  @end_container
end

#end_offsetObject (readonly)

Returns the value of attribute end_offset.



22
23
24
# File 'lib/dommy/range.rb', line 22

def end_offset
  @end_offset
end

#start_containerObject (readonly)

Returns the value of attribute start_container.



22
23
24
# File 'lib/dommy/range.rb', line 22

def start_container
  @start_container
end

#start_offsetObject (readonly)

Returns the value of attribute start_offset.



22
23
24
# File 'lib/dommy/range.rb', line 22

def start_offset
  @start_offset
end

Instance Method Details

#__js_call__(method, args) ⇒ Object



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/dommy/range.rb', line 301

def __js_call__(method, args)
  case method
  when "setStart"
    set_start(args[0], args[1])
  when "setEnd"
    set_end(args[0], args[1])
  when "setStartBefore"
    set_start_before(args[0])
  when "setStartAfter"
    set_start_after(args[0])
  when "setEndBefore"
    set_end_before(args[0])
  when "setEndAfter"
    set_end_after(args[0])
  when "collapse"
    collapse(args[0])
  when "selectNode"
    select_node(args[0])
  when "selectNodeContents"
    select_node_contents(args[0])
  when "toString"
    to_s
  when "cloneContents"
    clone_contents
  when "extractContents"
    extract_contents
  when "deleteContents"
    delete_contents
  when "surroundContents"
    surround_contents(args[0])
  when "insertNode"
    insert_node(args[0])
  when "compareBoundaryPoints"
    compare_boundary_points(args[0], args[1])
  when "intersectsNode"
    intersects_node(args[0])
  when "comparePoint"
    compare_point(args[0], args[1])
  when "isPointInRange"
    is_point_in_range(args[0], args[1])
  when "containsNode"
    contains_node(args[0], args[1])
  when "cloneRange"
    clone_range
  when "detach"
    nil
  when "getBoundingClientRect"
    get_bounding_client_rect
  when "getClientRects"
    get_client_rects
  end
end

#__js_get__(key) ⇒ Object

— JS bridge ————————————————-



277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/dommy/range.rb', line 277

def __js_get__(key)
  case key
  when "startContainer"
    @start_container
  when "startOffset"
    @start_offset
  when "endContainer"
    @end_container
  when "endOffset"
    @end_offset
  when "collapsed"
    collapsed?
  when "commonAncestorContainer"
    common_ancestor_container
  end
end

#clone_contentsObject

cloneContents — returns a DocumentFragment with a deep clone of the range contents. Range is left unchanged.



122
123
124
125
126
127
128
129
130
131
# File 'lib/dommy/range.rb', line 122

def clone_contents
  fragment = @document.create_document_fragment
  contents = collect_nodes_in_range
  contents.each do |node|
    clone = clone_wrapped(node)
    fragment.append_child(clone) if clone
  end

  fragment
end

#clone_rangeObject

— Cloning —————————————————



257
258
259
260
261
262
# File 'lib/dommy/range.rb', line 257

def clone_range
  r = Range.new(@document)
  r.set_start(@start_container, @start_offset)
  r.set_end(@end_container, @end_offset)
  r
end

#collapse(to_start = false) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
# File 'lib/dommy/range.rb', line 84

def collapse(to_start = false)
  if to_start
    @end_container = @start_container
    @end_offset = @start_offset
  else
    @start_container = @end_container
    @start_offset = @end_offset
  end

  nil
end

#collapsed?Boolean Also known as: collapsed

Returns:

  • (Boolean)


33
34
35
# File 'lib/dommy/range.rb', line 33

def collapsed?
  @start_container.equal?(@end_container) && @start_offset == @end_offset
end

#common_ancestor_containerObject



39
40
41
42
43
44
45
46
# File 'lib/dommy/range.rb', line 39

def common_ancestor_container
  # Find the lowest (deepest) common ancestor of start_container
  # and end_container. Walk from start_container up and return the
  # first node also present in end_container's ancestor chain.
  starts = ancestor_chain(@start_container)
  ends_set = ancestor_chain(@end_container)
  starts.find { |a| ends_set.any? { |e| e.equal?(a) } } || @document
end

#compare_boundary_points(how, other) ⇒ Object

— Ordering / containment ————————————



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/dommy/range.rb', line 187

def compare_boundary_points(how, other)
  # `how` must be one of the four named constants, else NotSupportedError.
  unless [START_TO_START, START_TO_END, END_TO_END, END_TO_START].include?(how)
    raise DOMException::NotSupportedError, "invalid comparison type: #{how}"
  end
  # The two ranges must share a root.
  unless same_root?(other.start_container)
    raise DOMException::WrongDocumentError, "the two Ranges are in different trees"
  end

  case how
  when START_TO_START
    compare_points(@start_container, @start_offset, other.start_container, other.start_offset)
  when START_TO_END
    compare_points(@end_container, @end_offset, other.start_container, other.start_offset)
  when END_TO_END
    compare_points(@end_container, @end_offset, other.end_container, other.end_offset)
  when END_TO_START
    compare_points(@start_container, @start_offset, other.end_container, other.end_offset)
  end
end

#compare_point(node, offset) ⇒ Object

WHATWG Range.comparePoint(node, offset): -1 if (node, offset) is before the range, 0 if inside, 1 if after. offset is a WebIDL unsigned long (so -1 wraps to a huge value > length → IndexSizeError).



221
222
223
224
225
226
227
228
229
230
231
# File 'lib/dommy/range.rb', line 221

def compare_point(node, offset)
  off = unsigned_long(offset)
  raise DOMException::WrongDocumentError, "node is in a different tree" unless same_root?(node)
  raise DOMException::InvalidNodeTypeError, "node is a doctype" if doctype?(node)
  raise DOMException::IndexSizeError, "offset is greater than node length" if off > length_of(node)

  return -1 if compare_points(node, off, @start_container, @start_offset) < 0
  return 1 if compare_points(node, off, @end_container, @end_offset) > 0

  0
end

#contains_node(node, partial = false) ⇒ Object



246
247
248
249
250
251
252
253
# File 'lib/dommy/range.rb', line 246

def contains_node(node, partial = false)
  if partial
    intersects_node(node)
  else
    # node must be wholly inside the range
    !before?(node) && !after?(node) && fully_inside?(node)
  end
end

#delete_contentsObject



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/dommy/range.rb', line 141

def delete_contents
  collect_nodes_in_range.each do |node|
    if node.respond_to?(:__dommy_backend_node__)
      # Fire a childList removal record (with correct sibling context) on the
      # node's parent, like any other tree removal.
      @document.remove_node_with_notify(node.__dommy_backend_node__)
    elsif node.respond_to?(:remove)
      node.remove
    end
  end

  collapse(true)
  nil
end

#extract_contentsObject

extractContents — like cloneContents but also removes the extracted nodes from the document.



135
136
137
138
139
# File 'lib/dommy/range.rb', line 135

def extract_contents
  fragment = clone_contents
  delete_contents
  fragment
end

#get_bounding_client_rectObject

— Layout stubs ———————————————- No layout engine; return zeroed rects so callers don’t crash.



267
268
269
# File 'lib/dommy/range.rb', line 267

def get_bounding_client_rect
  DOMRect.new(x: 0, y: 0, width: 0, height: 0)
end

#get_client_rectsObject



271
272
273
# File 'lib/dommy/range.rb', line 271

def get_client_rects
  []
end

#insert_node(node) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/dommy/range.rb', line 167

def insert_node(node)
  # Insert at the range start. For text-node containers we split;
  # for element containers we insert at child index.
  sc = @start_container
  if text_node?(sc)
    # Splitting is out of spec-perfect scope; insert before/after
    # the text node based on offset.
    parent = parent_of(sc)
    idx = child_index_of(parent, sc)
    idx += 1 if @start_offset >= length_of(sc)
    insert_into_parent_at(parent, idx, node)
  else
    insert_into_parent_at(sc, @start_offset, node)
  end

  nil
end

#intersects_node(node) ⇒ Object



209
210
211
212
213
214
215
216
# File 'lib/dommy/range.rb', line 209

def intersects_node(node)
  # Range and node intersect iff node's "position relative to range"
  # is not entirely before or entirely after.
  return false if before?(node)
  return false if after?(node)

  true
end

#is_point_in_range(node, offset) ⇒ Object

WHATWG Range.isPointInRange(node, offset): true iff the point lies within the range (inclusive). A different root returns false (no throw).



235
236
237
238
239
240
241
242
243
244
# File 'lib/dommy/range.rb', line 235

def is_point_in_range(node, offset)
  return false unless same_root?(node)

  off = unsigned_long(offset)
  raise DOMException::InvalidNodeTypeError, "node is a doctype" if doctype?(node)
  raise DOMException::IndexSizeError, "offset is greater than node length" if off > length_of(node)

  compare_points(node, off, @start_container, @start_offset) >= 0 &&
    compare_points(node, off, @end_container, @end_offset) <= 0
end

#select_node(node) ⇒ Object



96
97
98
99
100
101
102
103
104
# File 'lib/dommy/range.rb', line 96

def select_node(node)
  parent = parent_of(node)
  idx = child_index_of(parent, node)
  @start_container = parent
  @start_offset = idx
  @end_container = parent
  @end_offset = idx + 1
  nil
end

#select_node_contents(node) ⇒ Object



106
107
108
109
110
111
112
# File 'lib/dommy/range.rb', line 106

def select_node_contents(node)
  @start_container = node
  @start_offset = 0
  @end_container = node
  @end_offset = length_of(node)
  nil
end

#set_end(node, offset) ⇒ Object



57
58
59
60
61
62
# File 'lib/dommy/range.rb', line 57

def set_end(node, offset)
  @end_container = node
  @end_offset = offset.to_i
  collapse_to_end if compare_points(@start_container, @start_offset, @end_container, @end_offset) > 0
  nil
end

#set_end_after(node) ⇒ Object



79
80
81
82
# File 'lib/dommy/range.rb', line 79

def set_end_after(node)
  parent = parent_of(node)
  set_end(parent, child_index_of(parent, node) + 1)
end

#set_end_before(node) ⇒ Object



74
75
76
77
# File 'lib/dommy/range.rb', line 74

def set_end_before(node)
  parent = parent_of(node)
  set_end(parent, child_index_of(parent, node))
end

#set_start(node, offset) ⇒ Object

— Boundary setters ——————————————–



50
51
52
53
54
55
# File 'lib/dommy/range.rb', line 50

def set_start(node, offset)
  @start_container = node
  @start_offset = offset.to_i
  collapse_to_start if compare_points(@start_container, @start_offset, @end_container, @end_offset) > 0
  nil
end

#set_start_after(node) ⇒ Object



69
70
71
72
# File 'lib/dommy/range.rb', line 69

def set_start_after(node)
  parent = parent_of(node)
  set_start(parent, child_index_of(parent, node) + 1)
end

#set_start_before(node) ⇒ Object



64
65
66
67
# File 'lib/dommy/range.rb', line 64

def set_start_before(node)
  parent = parent_of(node)
  set_start(parent, child_index_of(parent, node))
end

#surround_contents(new_parent) ⇒ Object

surroundContents(newParent) — wraps the range contents in newParent (which must be an element).



158
159
160
161
162
163
164
165
# File 'lib/dommy/range.rb', line 158

def surround_contents(new_parent)
  contents = extract_contents
  new_parent.append_child(contents)
  # Insert new_parent at the (now-collapsed) range start.
  insert_node(new_parent)
  select_node(new_parent)
  nil
end

#to_sObject

— Content extraction —————————————-



116
117
118
# File 'lib/dommy/range.rb', line 116

def to_s
  Internal::RangeTextSerializer.new(self).serialize
end