opal-ferro 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +23 -0
- data/README.md +29 -0
- data/docs/GettingStarted.md +187 -0
- data/lib/opal-ferro/version.rb +10 -2
- data/opal/opal-ferro/elements/ferro_combos.js.rb +130 -0
- data/opal/opal-ferro/elements/ferro_components.js.rb +82 -0
- data/opal/opal-ferro/elements/ferro_form.js.rb +257 -0
- data/opal/opal-ferro/elements/ferro_inline.js.rb +137 -0
- data/opal/opal-ferro/elements/ferro_misc.js.rb +60 -0
- data/opal/opal-ferro/ferro_base_element.js.rb +187 -0
- data/opal/opal-ferro/ferro_document.js.rb +59 -35
- data/opal/opal-ferro/ferro_elementary.js.rb +99 -63
- data/opal/opal-ferro/ferro_factory.js.rb +69 -44
- data/opal/opal-ferro/ferro_router.js.rb +147 -88
- data/opal/opal-ferro/ferro_sequence.js.rb +20 -9
- data/opal/opal-ferro/ferro_xhr.js.rb +132 -69
- data/opal/opal-ferro.rb +6 -4
- data/opal-ferro.gemspec +1 -1
- metadata +26 -7
- data/opal/opal-ferro/ferro_base_elements.js.rb +0 -158
- data/opal/opal-ferro/ferro_components.js.rb +0 -92
- data/opal/opal-ferro/ferro_element.js.rb +0 -110
- data/opal/opal-ferro/ferro_form_elements.js.rb +0 -160
@@ -1,36 +1,60 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
1
|
+
# Module Ferro contains all Ferro functionality.
|
2
|
+
module Ferro
|
3
|
+
# This is the entry point for any Ferro application.
|
4
|
+
# It represents the top level object of the
|
5
|
+
# Master Object Model (MOM).
|
6
|
+
# There should be only one class that inhertits
|
7
|
+
# from FerroDocument in an application.
|
8
|
+
# This class attaches itself to the DOM
|
9
|
+
# `document.body` object.
|
10
|
+
# Any existing child nodes of `document.body` are removed.
|
11
|
+
class Document
|
12
|
+
|
13
|
+
include Elementary
|
14
|
+
|
15
|
+
# Create the document and start the creation
|
16
|
+
# process (casading).
|
17
|
+
def initialize
|
18
|
+
@children = {}
|
19
|
+
creation
|
20
|
+
end
|
21
|
+
|
22
|
+
# The document doesn't have a parent so returns itself.
|
23
|
+
def parent
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns the DOM element.
|
28
|
+
def element
|
29
|
+
factory.body
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns the one and only instance of the factory.
|
33
|
+
def factory
|
34
|
+
@factory ||= Factory.new
|
35
|
+
end
|
36
|
+
|
37
|
+
# The document class is the root element.
|
38
|
+
def root
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
# The document class is a component.
|
43
|
+
def component
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the one and only instance of the router.
|
48
|
+
def router
|
49
|
+
@router ||= Router.new(method(:page404))
|
50
|
+
end
|
51
|
+
|
52
|
+
# Callback for the router when no matching routes
|
53
|
+
# can be found. Override this method to add custom
|
54
|
+
# behavior.
|
55
|
+
#
|
56
|
+
# @param [String] pathname The route that was not matched
|
57
|
+
# by the router
|
58
|
+
def page404(pathname);end
|
21
59
|
end
|
22
|
-
|
23
|
-
def root
|
24
|
-
self
|
25
|
-
end
|
26
|
-
|
27
|
-
def component
|
28
|
-
self
|
29
|
-
end
|
30
|
-
|
31
|
-
def router
|
32
|
-
@router ||= FerroRouter.new(method(:page404))
|
33
|
-
end
|
34
|
-
|
35
|
-
def page404(pathname);end
|
36
|
-
end
|
60
|
+
end
|
@@ -1,79 +1,115 @@
|
|
1
|
-
module
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
after_create style _stylize cascade
|
7
|
-
add_child forget_children remove_child method_missing destroy
|
8
|
-
value set_text html parent children element domtype options
|
9
|
-
add_states add_state update_state toggle_state state_active
|
10
|
-
]
|
11
|
-
|
12
|
-
def creation
|
13
|
-
_before_create
|
14
|
-
before_create
|
15
|
-
create
|
16
|
-
_after_create
|
17
|
-
after_create
|
18
|
-
_stylize
|
19
|
-
cascade
|
20
|
-
end
|
21
|
-
|
22
|
-
def _before_create;end
|
23
|
-
def before_create;end
|
1
|
+
module Ferro
|
2
|
+
# Module defines element creation methods and child management.
|
3
|
+
# Note that there are no private methods in Opal. Methods that
|
4
|
+
# should be private are marked in the docs with 'Internal method'.
|
5
|
+
module Elementary
|
24
6
|
|
25
|
-
|
26
|
-
|
27
|
-
|
7
|
+
# Array of reseved names, child element should not have a name
|
8
|
+
# that is included in this list
|
9
|
+
RESERVED_NAMES = %i[
|
10
|
+
initialize factory root router page404
|
11
|
+
creation _before_create before_create create _after_create
|
12
|
+
after_create style _stylize cascade
|
13
|
+
add_child forget_children remove_child method_missing destroy
|
14
|
+
value set_text html parent children element domtype options
|
15
|
+
add_states add_state update_state toggle_state state_active
|
16
|
+
]
|
28
17
|
|
29
|
-
|
30
|
-
|
18
|
+
# Create DOM element and children elements.
|
19
|
+
# Calls before- and after create hooks.
|
20
|
+
def creation
|
21
|
+
_before_create
|
22
|
+
before_create
|
23
|
+
create
|
24
|
+
_after_create
|
25
|
+
after_create
|
26
|
+
_stylize
|
27
|
+
cascade
|
28
|
+
end
|
31
29
|
|
32
|
-
|
30
|
+
# Internal method.
|
31
|
+
def _before_create;end
|
33
32
|
|
34
|
-
|
35
|
-
|
33
|
+
# Internal method.
|
34
|
+
def before_create;end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
styles.map { |k, v| "#{k}:#{v};" }.join
|
41
|
-
)
|
36
|
+
# Calls the factory to create the DOM element.
|
37
|
+
def create
|
38
|
+
@element = factory.create_element(self, @domtype, @parent, @options) if @domtype
|
42
39
|
end
|
43
|
-
end
|
44
40
|
|
45
|
-
|
41
|
+
# Internal method.
|
42
|
+
def _after_create;end
|
46
43
|
|
47
|
-
|
48
|
-
|
49
|
-
raise "Child '#{sym}' already defined" if @children.has_key?(sym)
|
50
|
-
raise "Illegal name (#{sym})" if RESERVED_NAMES.include?(sym)
|
51
|
-
@children[sym] = element_class.new(self, sym, options)
|
52
|
-
end
|
44
|
+
# Internal method.
|
45
|
+
def after_create;end
|
53
46
|
|
54
|
-
|
55
|
-
name.
|
56
|
-
|
47
|
+
# Override this method to return a Hash of styles.
|
48
|
+
# Hash-key is the CSS style name, hash-value is the CSS style value.
|
49
|
+
def style;end
|
57
50
|
|
58
|
-
|
59
|
-
|
60
|
-
|
51
|
+
# Internal method.
|
52
|
+
def _stylize
|
53
|
+
styles = style
|
61
54
|
|
62
|
-
|
63
|
-
|
64
|
-
|
55
|
+
if styles.class == Hash
|
56
|
+
set_attribute(
|
57
|
+
'style',
|
58
|
+
styles.map { |k, v| "#{k}:#{v};" }.join
|
59
|
+
)
|
60
|
+
end
|
61
|
+
end
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
63
|
+
# Override this method to continue the MOM creation process.
|
64
|
+
def cascade;end
|
65
|
+
|
66
|
+
# Add a child element.
|
67
|
+
#
|
68
|
+
# @param [String] name A unique name for the element that is not
|
69
|
+
# in RESERVED_NAMES
|
70
|
+
# @param [String] element_class Ruby class name for the new element
|
71
|
+
# @param [Hash] options Options to pass to the element. Any option key
|
72
|
+
# that is not recognized is set as an attribute on the DOM element.
|
73
|
+
# Recognized keys are:
|
74
|
+
# prepend Prepend the new element before this DOM element
|
75
|
+
# content Add the value of content as a textnode to the DOM element
|
76
|
+
def add_child(name, element_class, options = {})
|
77
|
+
sym = symbolize(name)
|
78
|
+
raise "Child '#{sym}' already defined" if @children.has_key?(sym)
|
79
|
+
raise "Illegal name (#{sym})" if RESERVED_NAMES.include?(sym)
|
80
|
+
@children[sym] = element_class.new(self, sym, options)
|
81
|
+
end
|
82
|
+
|
83
|
+
# Convert a string containing a variable name to a symbol.
|
84
|
+
def symbolize(name)
|
85
|
+
name.downcase.to_sym
|
86
|
+
end
|
87
|
+
|
88
|
+
# Remove all child elements.
|
89
|
+
def forget_children
|
90
|
+
children = {}
|
91
|
+
end
|
92
|
+
|
93
|
+
# Remove a specific child element.
|
94
|
+
#
|
95
|
+
# param [Symbol] sym The element to remove
|
96
|
+
def remove_child(sym)
|
97
|
+
@children.delete(sym)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Remove a DOM element.
|
101
|
+
def destroy
|
102
|
+
`#{parent.element}.removeChild(#{element})`
|
103
|
+
parent.remove_child(@sym)
|
104
|
+
end
|
70
105
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
106
|
+
# Getter for children.
|
107
|
+
def method_missing(method_name, *args, &block)
|
108
|
+
if @children.has_key?(method_name)
|
109
|
+
@children[method_name]
|
110
|
+
else
|
111
|
+
super
|
112
|
+
end
|
77
113
|
end
|
78
114
|
end
|
79
115
|
end
|
@@ -1,58 +1,83 @@
|
|
1
|
-
|
1
|
+
module Ferro
|
2
|
+
# Create DOM elements.
|
3
|
+
class Factory
|
2
4
|
|
3
|
-
|
5
|
+
attr_reader :body
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
# Creates the factory. Do not create a factory directly, instead
|
8
|
+
# call the 'factory' method that is available in all Ferro classes
|
9
|
+
def initialize
|
10
|
+
@body = `document.body`
|
11
|
+
`while (document.body.firstChild) {document.body.removeChild(document.body.firstChild);}`
|
12
|
+
end
|
9
13
|
|
10
|
-
|
11
|
-
#
|
12
|
-
|
14
|
+
# Create a DOM element.
|
15
|
+
#
|
16
|
+
# @param [String] target The Ruby class instance
|
17
|
+
# @param [String] type Type op DOM element to create
|
18
|
+
# @param [String] parent The Ruby parent element
|
19
|
+
# @param [Hash] options Options to pass to the element.
|
20
|
+
# See FerroElementary::add_child
|
21
|
+
# @return [String] the DOM element
|
22
|
+
def create_element(target, type, parent, options = {})
|
23
|
+
# Create element
|
24
|
+
element = `document.createElement(#{type})`
|
13
25
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
26
|
+
# Add element to DOM
|
27
|
+
if options[:prepend]
|
28
|
+
`#{parent.element}.insertBefore(#{element}, #{options[:prepend].element})`
|
29
|
+
else
|
30
|
+
`#{parent.element}.appendChild(#{element})`
|
31
|
+
end
|
20
32
|
|
21
|
-
|
22
|
-
|
33
|
+
# Add ruby class to the node
|
34
|
+
`#{element}.classList.add(#{dasherize(target.class.name)})`
|
23
35
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
36
|
+
# Add ruby superclass to the node to allow for more generic styling
|
37
|
+
if target.class.superclass != BaseElement
|
38
|
+
`#{element}.classList.add(#{dasherize(target.class.superclass.name)})`
|
39
|
+
end
|
28
40
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
# Set ruby object_id as default element id
|
42
|
+
if !options.has_key?(:id)
|
43
|
+
`#{element}.id = #{target.object_id}`
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set attributes
|
47
|
+
options.each do |name, value|
|
48
|
+
case name
|
49
|
+
when :prepend
|
50
|
+
nil
|
51
|
+
when :content
|
52
|
+
`#{element}.appendChild(document.createTextNode(#{value}))`
|
53
|
+
else
|
54
|
+
`#{element}.setAttribute(#{name}, #{value})`
|
55
|
+
end
|
43
56
|
end
|
57
|
+
|
58
|
+
element
|
44
59
|
end
|
45
60
|
|
46
|
-
|
47
|
-
|
61
|
+
# Convert a Ruby classname to a dasherized name for use with CSS.
|
62
|
+
#
|
63
|
+
# @param [String] class_name The Ruby class name
|
64
|
+
# @return [String] CSS class name
|
65
|
+
def dasherize(class_name)
|
66
|
+
return class_name if class_name !~ /[A-Z:_]/
|
67
|
+
c = class_name.to_s.gsub('::', '')
|
48
68
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
69
|
+
(c[0] + c[1..-1].gsub(/[A-Z]/){ |c| "-#{c}" }).
|
70
|
+
downcase.
|
71
|
+
gsub('_', '-')
|
72
|
+
end
|
53
73
|
|
54
|
-
|
55
|
-
|
56
|
-
|
74
|
+
# Convert a CSS classname to a camelized Ruby class name.
|
75
|
+
#
|
76
|
+
# @param [String] class_name CSS class name
|
77
|
+
# @return [String] A Ruby class name
|
78
|
+
def camelize(class_name)
|
79
|
+
return class_name if class_name !~ /-/
|
80
|
+
class_name.gsub(/(?:-|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.strip
|
81
|
+
end
|
57
82
|
end
|
58
83
|
end
|
@@ -1,118 +1,177 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
1
|
+
module Ferro
|
2
|
+
|
3
|
+
require 'native'
|
4
|
+
|
5
|
+
# Wrapper for the web browsers history API.
|
6
|
+
# Note that there are no private methods in Opal. Methods that
|
7
|
+
# should be private are marked in the docs with 'Internal method'.
|
8
|
+
class Router
|
9
|
+
|
10
|
+
# Create the router. Do not create a router directly, instead
|
11
|
+
# call the 'router' method that is available in all Ferro classes
|
12
|
+
# That method points to the router instance that is attached to
|
13
|
+
# the FerroDocument.
|
14
|
+
#
|
15
|
+
# @param [Method] page404 Method that is called when the web browser
|
16
|
+
# navigates and no route can be found. Override the
|
17
|
+
# page404 method in FerroDocument.
|
18
|
+
def initialize(page404)
|
19
|
+
@routes = []
|
20
|
+
@page404 = page404
|
21
|
+
setup_navigation_listener
|
22
|
+
end
|
10
23
|
|
11
|
-
|
12
|
-
|
13
|
-
|
24
|
+
# Add a new route to the router.
|
25
|
+
#
|
26
|
+
# Examples: when the following routes are created and the web browser
|
27
|
+
# navigates to a url matching the route, the callback method will be
|
28
|
+
# called with parameters:
|
29
|
+
# add_route('/ferro/page1', my_cb) # '/ferro/page1' => my_cb({})
|
30
|
+
# add_route('/user/:id', my_cb) # '/user/1' => my_cb({id: 1})
|
31
|
+
# add_route('/ferro', my_cb) # '/ferro?page=1' => my_cb({page: 1})
|
32
|
+
#
|
33
|
+
# @param [String] path Relative url (without protocol and host)
|
34
|
+
# @param [Method] callback Method that is called when the web browser
|
35
|
+
# navigates and the path is matched. The callback
|
36
|
+
# method should accept one parameter [Hash]
|
37
|
+
# containing the parameters found in the matched url
|
38
|
+
def add_route(path, callback)
|
39
|
+
@routes << { parts: path_to_parts(path), callback: callback }
|
40
|
+
end
|
14
41
|
|
15
|
-
|
16
|
-
|
17
|
-
|
42
|
+
# Internal method to set 'onpopstate'
|
43
|
+
def setup_navigation_listener
|
44
|
+
`window.onpopstate = function(e){#{navigated}}`
|
45
|
+
end
|
18
46
|
|
19
|
-
|
20
|
-
|
21
|
-
|
47
|
+
# Replace the current location in the web browsers history
|
48
|
+
#
|
49
|
+
# @param [String] url Relative url (without protocol and host)
|
50
|
+
def replace_state(url)
|
51
|
+
`history.replaceState(null,null,#{url})`
|
52
|
+
end
|
22
53
|
|
23
|
-
|
24
|
-
|
25
|
-
|
54
|
+
# Add a location to the web browsers history
|
55
|
+
#
|
56
|
+
# @param [String] url Relative url (without protocol and host)
|
57
|
+
def push_state(url)
|
58
|
+
`history.pushState(null,null,#{url})`
|
59
|
+
end
|
26
60
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
61
|
+
# Navigate to url
|
62
|
+
#
|
63
|
+
# @param [String] url Relative url (without protocol and host)
|
64
|
+
def go_to(url)
|
65
|
+
push_state(url)
|
66
|
+
navigated
|
67
|
+
end
|
31
68
|
|
32
|
-
|
33
|
-
|
34
|
-
|
69
|
+
# Navigate back
|
70
|
+
def go_back
|
71
|
+
`history.back()`
|
72
|
+
end
|
35
73
|
|
36
|
-
|
37
|
-
|
38
|
-
|
74
|
+
# Internal method to get the new location
|
75
|
+
def get_location
|
76
|
+
Native(`new URL(window.location.href)`)
|
77
|
+
end
|
39
78
|
|
40
|
-
|
41
|
-
path
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
79
|
+
# Internal method to split a path into components
|
80
|
+
def path_to_parts(path)
|
81
|
+
path.
|
82
|
+
downcase.
|
83
|
+
split('/').
|
84
|
+
map { |part| part.empty? ? nil : part.strip }.
|
85
|
+
compact
|
86
|
+
end
|
47
87
|
|
48
|
-
|
49
|
-
|
50
|
-
|
88
|
+
# URI decode a value
|
89
|
+
#
|
90
|
+
# @param [String] value Value to decode
|
91
|
+
def decode(value)
|
92
|
+
`decodeURI(#{value})`
|
93
|
+
end
|
51
94
|
|
52
|
-
|
53
|
-
|
54
|
-
|
95
|
+
# Internal method called when the web browser navigates
|
96
|
+
def navigated
|
97
|
+
url = get_location
|
98
|
+
@params = []
|
55
99
|
|
56
|
-
|
100
|
+
idx = match(path_to_parts(decode(url.pathname)), decode(url.search))
|
57
101
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
102
|
+
if idx
|
103
|
+
@routes[idx][:callback].call(@params)
|
104
|
+
else
|
105
|
+
@page404.call(url.pathname)
|
106
|
+
end
|
62
107
|
end
|
63
|
-
end
|
64
108
|
|
65
|
-
|
66
|
-
|
109
|
+
# Internal method to match a path to the most likely route
|
110
|
+
#
|
111
|
+
# @param [String] path Url to match
|
112
|
+
# @param [String] search Url search parameters
|
113
|
+
def match(path, search)
|
114
|
+
matches = get_matches(path)
|
67
115
|
|
68
|
-
|
69
|
-
|
116
|
+
if matches.length > 0
|
117
|
+
match = matches.sort { |m| m[1] }.first
|
70
118
|
|
71
|
-
|
72
|
-
|
119
|
+
@params = match[2]
|
120
|
+
add_search_to_params(search)
|
73
121
|
|
74
|
-
|
75
|
-
|
76
|
-
|
122
|
+
match[0]
|
123
|
+
else
|
124
|
+
nil
|
125
|
+
end
|
77
126
|
end
|
78
|
-
end
|
79
127
|
|
80
|
-
|
81
|
-
|
128
|
+
# Internal method to match a path to possible routes
|
129
|
+
#
|
130
|
+
# @param [String] path Url to match
|
131
|
+
def get_matches(path)
|
132
|
+
matches = []
|
82
133
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
134
|
+
@routes.each_with_index do |route, i|
|
135
|
+
score, pars = score_route(route[:parts], path)
|
136
|
+
matches << [i, score, pars] if score > 0
|
137
|
+
end
|
87
138
|
|
88
|
-
|
89
|
-
|
139
|
+
matches
|
140
|
+
end
|
90
141
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
142
|
+
# Internal method to add a match score
|
143
|
+
#
|
144
|
+
# @param [String] parts Parts of a route
|
145
|
+
# @param [String] path Url to match
|
146
|
+
def score_route(parts, path)
|
147
|
+
score = 0
|
148
|
+
pars = {}
|
149
|
+
|
150
|
+
if parts.length == path.length
|
151
|
+
parts.each_with_index do |part, i|
|
152
|
+
if part[0] == ':'
|
153
|
+
score += 1
|
154
|
+
pars["#{part[1..-1]}"] = path[i]
|
155
|
+
elsif part == path[i].downcase
|
156
|
+
score += 2
|
157
|
+
end
|
102
158
|
end
|
103
159
|
end
|
104
|
-
end
|
105
160
|
|
106
|
-
|
107
|
-
|
161
|
+
return score, pars
|
162
|
+
end
|
108
163
|
|
109
|
-
|
110
|
-
|
111
|
-
|
164
|
+
# Internal method to split search parameters
|
165
|
+
#
|
166
|
+
# @param [String] search Url search parameters
|
167
|
+
def add_search_to_params(search)
|
168
|
+
if !search.empty?
|
169
|
+
pars = search[1..-1].split('&')
|
112
170
|
|
113
|
-
|
114
|
-
|
115
|
-
|
171
|
+
pars.each do |par|
|
172
|
+
pair = par.split('=')
|
173
|
+
@params[ pair[0] ] = pair[1] if pair.length == 2
|
174
|
+
end
|
116
175
|
end
|
117
176
|
end
|
118
177
|
end
|