1 use webcore::value::Reference;
2 use webcore::try_from::TryInto;
3 use webapi::node::{INode, Node};
4 use webapi::dom_exception::{IndexSizeError, NotFoundError, InvalidStateError};
5 
6 /// Possible values are:
7 ///
8 /// * `None`: No selection has currently been made.
9 /// * `Caret`: The selection is collapsed (i.e. the caret is placed on some text, but no
10 /// range has been selected).
11 /// * `Range`: A range has been selected.
12 ///
13 /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/type)
14 #[derive(Copy, Clone, PartialEq, Eq, Debug)]
15 pub enum SelectionType {
16     None,
17     Caret,
18     Range
19 }
20 
21 /// Represents the range of text selected by the user or the current position of the caret. To
22 /// obtain a Selection object for examination or modification, call
23 /// [Window.get_selection()](struct.Window.html#method.get_selection).
24 ///
25 /// A user may make a selection from left to right (in document order) or right to left (reverse of
26 /// document order). The anchor is where the user began the selection and the focus is where the
27 /// user ends the selection. If you make a selection with a desktop mouse, the anchor is placed
28 /// where you pressed the mouse button and the focus is placed where you released the mouse button.
29 /// Anchor and focus should not be confused with the start and end positions of a selection, since
30 /// anchor can be placed before the focus or vice versa, depending on the direction you made your
31 /// selection.
32 ///
33 /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection)
34 // https://w3c.github.io/selection-api/#selection-interface
35 #[derive(Clone, Debug, PartialEq, Eq, ReferenceType)]
36 #[reference(instance_of = "Selection")]
37 pub struct Selection(Reference);
38 
39 impl Selection {
40     /// Returns the [Node](struct.Node.html) in which the selection begins.
41     ///
42     /// A user may make a selection from left to right (in document order) or right to left
43     /// (reverse of document order). The anchor is where the user began the selection. This can be
44     /// visualized by holding the Shift key and pressing the arrow keys on your keyboard. The
45     /// selection's anchor does not move, but the selection's focus, the other end of the
46     /// selection, does move.
47     ///
48     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/anchorNode)
anchor_node(&self) -> Option<Node>49     pub fn anchor_node(&self) -> Option<Node> {
50         js! (
51             return @{self}.anchorNode;
52         ).try_into().unwrap()
53     }
54 
55     /// Returns the number of characters that the selection's anchor is offset within the
56     /// [anchor_node](struct.Selection.html#method.anchor_node).
57     ///
58     /// This number is zero-based. If the selection begins with the first character in the
59     /// [anchor_node](struct.Selection.html#method.anchor_node), 0 is returned.
60     ///
61     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/anchorOffset)
anchor_offset(&self) -> u3262     pub fn anchor_offset(&self) -> u32 {
63         js! (
64             return @{self}.anchorOffset;
65         ).try_into().unwrap()
66     }
67 
68     /// Returns the [Node](struct.Node.html) in which the selection ends.
69     ///
70     /// A user may make a selection from left to right (in document order) or right to left
71     /// (reverse of document order). The focus is where the user ended the selection. This can be
72     /// visualized by holding the Shift key and pressing the arrow keys on your keyboard to modify
73     /// the current selection. The selection's focus moves, but the selection's anchor, the other
74     /// end of the selection, does not move.
75     ///
76     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/focusNode)
focus_node(&self) -> Option<Node>77     pub fn focus_node(&self) -> Option<Node> {
78         js! (
79             return @{self}.focusNode;
80         ).try_into().unwrap()
81     }
82 
83     /// Returns the number of characters that the selection's anchor is offset within the
84     /// [focus_node](struct.Selection.html#method.focus_node).
85     ///
86     /// This number is zero-based. If the selection begins with the first character in the
87     /// [focus_node](struct.Selection.html#method.focus_node), 0 is returned.
88     ///
89     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/focusOffset)
focus_offset(&self) -> u3290     pub fn focus_offset(&self) -> u32 {
91         js! (
92             return @{self}.focusOffset;
93         ).try_into().unwrap()
94     }
95 
96     /// Returns a boolean which indicates whether or not there is currently any text selected; That
97     /// is to say that the selection's start and end points are at the same position in the
98     /// content.
99     ///
100     /// Keep in mind that a collapsed selection may still have one (or more, in Gecko)
101     /// [Range](struct.Range.html)s, so [range_count](struct.Selection.html#method.range_count) may
102     /// not be zero. In that scenario, calling a [Selection](struct.Selection.html) object's
103     /// [get_range_at](struct.Selection.html#method.get_range_at) method may return a
104     /// [Range](struct.Range.html) object which is collapsed.
105     ///
106     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/isCollapsed)
is_collapsed(&self) -> bool107     pub fn is_collapsed(&self) -> bool {
108         js! (
109             return @{self}.isCollapsed;
110         ).try_into().unwrap()
111     }
112 
113     /// Returns the number of ranges in the selection.
114     ///
115     /// Before the user has clicked a freshly loaded page, the
116     /// [range_count](struct.Selection.html#method.range_count) is 0. After the user
117     /// clicks on the page, [range_count](struct.Selection.html#method.range_count) even if no
118     /// selection is visible.
119     ///
120     /// A user can normally only select one range at a time, so the
121     /// [range_count](struct.Selection.html#method.range_count) will usually be 1.
122     /// Scripting can be used to make the selection contain more than 1 range.
123     ///
124     /// Gecko browsers allow multiple selections across table cells.
125     ///
126     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/rangeCount)
range_count(&self) -> u32127     pub fn range_count(&self) -> u32 {
128         js! (
129             return @{self}.rangeCount;
130         ).try_into().unwrap()
131     }
132 
133     /// Returns the type of the current selection.
134     ///
135     /// Possible values are:
136     ///
137     /// * `None`: No selection has currently been made.
138     /// * `Caret`: The selection is collapsed (i.e. the caret is placed on some text, but no
139     /// range has been selected).
140     /// * `Range`: A range has been selected.
141     ///
142     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/type)
kind(&self) -> SelectionType143     pub fn kind(&self) -> SelectionType {
144         let selection: String = js! (
145             return @{self}.type;
146         ).try_into().unwrap();
147 
148         match selection.as_ref() {
149             "None" => SelectionType::None,
150             "Caret" => SelectionType::Caret,
151             "Range" => SelectionType::Range,
152             _ => panic!("Selection Type invalid!"),
153         }
154     }
155 
156     /// Returns a [Range](struct.Range.html) object representing one of the ranges currently
157     /// selected.
158     ///
159     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt)
get_range_at(&self, index: u32) -> Result<Range, IndexSizeError>160     pub fn get_range_at(&self, index: u32) -> Result<Range, IndexSizeError> {
161         js_try! (
162             return @{self}.getRangeAt(@{index});
163         ).unwrap()
164     }
165 
166     /// Adds a [Range](struct.Range.html) to the [Selection](struct.Selection.html).
167     ///
168     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/addRange)
add_range(&self, range: &Range)169     pub fn add_range(&self, range: &Range) {
170         js! { @(no_return)
171             @{self}.addRange(@{range});
172         };
173     }
174 
175     /// Removes a [Range](struct.Range.html) from the [Selection](struct.Selection.html).
176     ///
177     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/removeRange)
remove_range(&self, range: &Range) -> Result<(), NotFoundError>178     pub fn remove_range(&self, range: &Range) -> Result<(), NotFoundError> {
179         js_try! ( @(no_return)
180             @{self}.removeRange(@{range});
181         ).unwrap()
182     }
183 
184     /// Removes all ranges from the [Selection](struct.Selection.html), leaving the
185     /// [anchor_node](struct.Selection.html#method.anchor_node) and
186     /// [focus_node](struct.Selection.html#method.focus_node) properties equal to
187     /// `None` and leaving nothing selected.
188     ///
189     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/removeRange)
remove_all_ranges(&self)190     pub fn remove_all_ranges(&self) {
191         js! { @(no_return)
192             @{self}.removeAllRanges();
193         };
194     }
195 
196     /// Collapses the [Selection](struct.Selection.html) to a single point. The document is not
197     /// modified. If the content is focused or editable, the caret will blink there.
198     ///
199     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/collapse)
collapse<N: INode>(&self, node: &N)200     pub fn collapse<N: INode>(&self, node: &N) {
201         js! { @(no_return)
202             @{self}.collapse(@{node.as_ref()});
203         }
204     }
205 
206     /// Collapses the [Selection](struct.Selection.html) to a single point. The document is not
207     /// modified. If the content is focused or editable, the caret will blink there.
208     ///
209     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/collapse)
collapse_with_offset<N: INode>(&self, node: &N, offset: Option<u32>) -> Result<(), IndexSizeError>210     pub fn collapse_with_offset<N: INode>(&self, node: &N, offset: Option<u32>) -> Result<(), IndexSizeError> {
211         js_try! ( @(no_return)
212             @{self}.collapse(@{node.as_ref()}, @{offset});
213         ).unwrap()
214     }
215 
216     /// Collapses the [Selection](struct.Selection.html) to the start of the first range in the
217     /// selection. If the content is focused or editable, the caret will blink there.
218     ///
219     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/collapseToStart)
collapse_to_start(&self) -> Result<(), InvalidStateError>220     pub fn collapse_to_start(&self) -> Result<(), InvalidStateError> {
221         js_try! ( @(no_return)
222             @{self}.collapseToStart();
223         ).unwrap()
224     }
225 
226     /// Collapses the [Selection](struct.Selection.html) to the end of the last range in the
227     /// selection. If the content is focused or editable, the caret will blink there.
228     ///
229     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/collapseToEnd)
collapse_to_end(&self) -> Result<(), InvalidStateError>230     pub fn collapse_to_end(&self) -> Result<(), InvalidStateError> {
231         js_try! ( @(no_return)
232             @{self}.collapseToEnd();
233         ).unwrap()
234     }
235 
236     /// Moves the focus of the selection to a specified point. The anchor of the selection does not
237     /// move. The selection will be from the anchor node to the new focus regardless of direction.
238     ///
239     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/extend)
extend<N: INode>(&self, node: &N, offset: Option<u32>) -> Result<(), InvalidStateError>240     pub fn extend<N: INode>(&self, node: &N, offset: Option<u32>) -> Result<(), InvalidStateError> {
241         js_try! ( @(no_return)
242             @{self}.extend(@{node.as_ref()}, @{offset});
243         ).unwrap()
244     }
245 
246     /// Sets the selection to be a range including all or parts of the two specified
247     /// [Node](struct.Node.html)s, and any content located between them.
248     ///
249     /// [(Javascript
250     /// docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/setBaseAndExtent)
set_base_and_extent<N: INode, M: INode>(&self, anchor_node: &N, anchor_offset: u32, focus_node: &M, focus_offset: u32) -> Result<(), IndexSizeError>251     pub fn set_base_and_extent<N: INode, M: INode>(&self, anchor_node: &N, anchor_offset: u32, focus_node: &M, focus_offset: u32) -> Result<(), IndexSizeError> {
252         js_try! ( @(no_return)
253             @{self}.setBaseAndExtent(@{anchor_node.as_ref()}, @{anchor_offset}, @{focus_node.as_ref()}, @{focus_offset});
254         ).unwrap()
255     }
256 
257     /// Adds all the children of the specified [Node](struct.Node.html) to the selection. Previous
258     /// selection is lost.
259     ///
260     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/selectAllChildren)
select_all_children<N: INode>(&self, node: &N)261     pub fn select_all_children<N: INode>(&self, node: &N) {
262         js! { @(no_return)
263             @{self}.selectAllChildren(@{node.as_ref()});
264         };
265     }
266 
267     /// Deletes the actual text being represented by the [Selection](struct.Selection.html) from
268     /// the document's DOM.
269     ///
270     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/deleteFromDocument)
delete_from_document(&self)271     pub fn delete_from_document(&self) {
272         js! { @(no_return)
273             @{self}.deleteFromDocument();
274         };
275     }
276 
277     /// Indicates if the entire node is part of the selection.
278     ///
279     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/containsNode)
contains_whole<N: INode>(&self, node: &N) -> bool280     pub fn contains_whole<N: INode>(&self, node: &N) -> bool {
281         js! (
282             return @{self}.containsNode(@{node.as_ref()}, false);
283         ).try_into().unwrap()
284     }
285 
286     /// Indicates if atleast some of the node is part of the selection.
287     ///
288     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Selection/containsNode)
contains_part_of<N: INode>(&self, node: &N) -> bool289     pub fn contains_part_of<N: INode>(&self, node: &N) -> bool {
290         js! (
291             return @{self}.containsNode(@{node.as_ref()}, true);
292         ).try_into().unwrap()
293     }
294 }
295 
296 /// The Range interface represents a fragment of a document that can contain nodes and parts of
297 /// text nodes.
298 ///
299 /// A range can be created using the [create_range()](struct.Document.html#method.create_range) method
300 /// of the Document object. Range objects can also be retrieved by using the
301 /// [get_range_at()](struct.Selection.html#method.get_range_at) method of the [Selection](struct.Selection.html)
302 /// object or the [caret_range_from_point()](struct.Document.html#method.caret_range_from_point) method of
303 /// the [Document](struct.Document.html] object.
304 // https://dom.spec.whatwg.org/#range
305 #[derive(Clone, Debug, PartialEq, Eq, ReferenceType)]
306 #[reference(instance_of = "Range")]
307 pub struct Range(Reference);
308 
309 impl Range {
310     /// Returns a boolean indicating whether the range's start and end points are at the same
311     /// position.
312     ///
313     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Range/collapsed)
collapsed(&self) -> bool314     pub fn collapsed(&self) -> bool {
315         js! (
316             return @{self}.collapsed;
317         ).try_into().unwrap()
318     }
319 
320     /// Returns the deepest [Node](struct.Node.html) that contains the startContainer and
321     /// endContainer nodes.
322     ///
323     /// [(Javascript
324     /// docs)](https://developer.mozilla.org/en-US/docs/Web/API/Range/commonAncestorContainer)
common_ancestor_container(&self) -> Node325     pub fn common_ancestor_container(&self) -> Node {
326         js! (
327             return @{self}.commonAncestorContainer;
328         ).try_into().unwrap()
329     }
330 
331     /// Returns the [Node](struct.Node.html) within which the [Range](struct.Range.html) ends.
332     ///
333     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Range/endContainer)
end_container(&self) -> Node334     pub fn end_container(&self) -> Node {
335         js! (
336             return @{self}.endContainer;
337         ).try_into().unwrap()
338     }
339 
340     /// Returns a number representing where in the endContainer the [Range](struct.Range.html) ends.
341     ///
342     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Range/endOffset)
end_offset(&self) -> u32343     pub fn end_offset(&self) -> u32 {
344         js! (
345             return @{self}.endOffset;
346         ).try_into().unwrap()
347     }
348 
349     /// Returns the [Node](struct.Node.html) within which the [Range](struct.Range.html) starts.
350     ///
351     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Range/startContainer)
start_container(&self) -> Node352     pub fn start_container(&self) -> Node {
353         js! (
354             return @{self}.startContainer;
355         ).try_into().unwrap()
356     }
357 
358     /// Returns a number representing where in the startContainer the [Range](struct.Range.html) starts.
359     ///
360     /// [(Javascript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Range/startOffset)
start_offset(&self) -> u32361     pub fn start_offset(&self) -> u32 {
362         js! (
363             return @{self}.startOffset;
364         ).try_into().unwrap()
365     }
366 }
367 
368 #[cfg(all(test, feature = "web_test"))]
369 mod tests {
370     use super::*;
371     use webapi::document::document;
372     use webapi::window::window;
373 
div() -> Node374     fn div() -> Node {
375         let node = js!(
376             return document.createElement("div");
377         ).try_into().unwrap();
378         document().body().unwrap().append_child(&node);
379         node
380     }
381 
text(text: &str) -> Node382     fn text(text: &str) -> Node {
383         js!(
384             return new Text(@{text});
385         ).try_into().unwrap()
386     }
387 
selection() -> Selection388     fn selection() -> Selection {
389         window().get_selection().unwrap()
390     }
391 
392     #[test]
test_set_base_and_extent()393     fn test_set_base_and_extent() {
394         let parent = div();
395         parent.append_child(&text("ab"));
396 
397         assert!(selection().set_base_and_extent(&parent, 0, &parent, 0).is_ok());
398     }
399 
400     #[test]
test_anchor()401     fn test_anchor() {
402         let parent = div();
403         parent.append_child(&text("ab"));
404         assert!(selection().set_base_and_extent(&parent, 0, &parent, 0).is_ok());
405         assert_eq!(selection().anchor_node().unwrap().as_ref(), parent.as_ref());
406         assert_eq!(selection().anchor_offset(), 0);
407     }
408 
409     #[test]
test_focus()410     fn test_focus() {
411         let parent = div();
412         parent.append_child(&text("ab"));
413         assert!(selection().set_base_and_extent(&parent, 0, &parent, 0).is_ok());
414         assert_eq!(selection().focus_node().unwrap().as_ref(), parent.as_ref());
415         assert_eq!(selection().focus_offset(), 0);
416     }
417 
418     #[test]
test_is_collapsed()419     fn test_is_collapsed() {
420         let parent = div();
421         parent.append_child(&text("ab"));
422         assert!(selection().set_base_and_extent(&parent, 0, &parent, 0).is_ok());
423         assert!(selection().is_collapsed());
424     }
425 
426     #[test]
test_contains_part_of()427     fn test_contains_part_of() {
428         let parent = div();
429         parent.append_child(&text("ab"));
430         assert!(selection().set_base_and_extent(&parent, 0, &parent, 0).is_ok());
431         assert!(selection().contains_part_of(&parent));
432     }
433 
434     #[test]
test_contains_whole()435     fn test_contains_whole() {
436         let parent = div();
437         let text_node = text("ab");
438         parent.append_child(&text_node);
439         selection().select_all_children(&parent);
440         assert!(selection().contains_whole(&text_node));
441     }
442 }
443