Class: Dommy::Range

Inherits:
Object
  • Object
show all
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

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



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/dommy/range.rb', line 257

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 "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 ————————————————-



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/dommy/range.rb', line 240

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 —————————————————



220
221
222
223
224
225
# File 'lib/dommy/range.rb', line 220

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 ————————————



185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/dommy/range.rb', line 185

def compare_boundary_points(how, other)
  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)
  else
    0
  end
end

#contains_node(node, partial = false) ⇒ Object



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

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
# File 'lib/dommy/range.rb', line 141

def delete_contents
  collect_nodes_in_range.each do |node|
    if node.respond_to?(:remove)
      node.remove
    elsif node.respond_to?(:__node__)
      node.__node__.unlink
    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.



230
231
232
# File 'lib/dommy/range.rb', line 230

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

#get_client_rectsObject



234
235
236
# File 'lib/dommy/range.rb', line 234

def get_client_rects
  []
end

#insert_node(node) ⇒ Object



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

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



200
201
202
203
204
205
206
207
# File 'lib/dommy/range.rb', line 200

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

#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).



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

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