├── .github
└── workflows
│ └── ruby.yml
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── examples
├── ObjectSpaceBrowser.rb
├── ajax.rb
├── apotomo-webhunter
│ ├── main.rb
│ └── public
│ │ ├── images
│ │ ├── bear_trap_charged.png
│ │ ├── bear_trap_snapped.png
│ │ ├── cheese.png
│ │ ├── dark_forest.jpg
│ │ └── mouse.png
│ │ ├── javascripts
│ │ ├── jquery-1.3.2.min.js
│ │ └── wee-jquery.js
│ │ └── stylesheets
│ │ └── forest.css
├── arc_challenge.rb
├── arc_challenge2.rb
├── cheese_task.rb
├── continuations.rb
├── demo.rb
├── demo
│ ├── calculator.rb
│ ├── calendar.rb
│ ├── counter.rb
│ ├── editable_counter.rb
│ ├── example.rb
│ ├── file_upload.rb
│ ├── messagebox.rb
│ ├── radio.rb
│ └── window.rb
├── hw.rb
├── i18n
│ ├── app.rb
│ └── locale
│ │ ├── de
│ │ └── app.po
│ │ └── en
│ │ └── app.po
└── pager.rb
├── lib
├── wee.rb
└── wee
│ ├── application.rb
│ ├── callback.rb
│ ├── component.rb
│ ├── decoration.rb
│ ├── dialog.rb
│ ├── external_resource.rb
│ ├── hello_world.rb
│ ├── html_brushes.rb
│ ├── html_canvas.rb
│ ├── html_document.rb
│ ├── html_writer.rb
│ ├── id_generator.rb
│ ├── jquery.rb
│ ├── jquery
│ ├── jquery-1.3.2.min.js
│ └── wee-jquery.js
│ ├── locale.rb
│ ├── lru_cache.rb
│ ├── presenter.rb
│ ├── renderer.rb
│ ├── request.rb
│ ├── response.rb
│ ├── rightjs.rb
│ ├── rightjs
│ ├── rightjs-1.5.2.min.js
│ └── wee-rightjs.js
│ ├── root_component.rb
│ ├── run.rb
│ ├── session.rb
│ ├── state.rb
│ ├── task.rb
│ └── version.rb
├── spec
└── component_spec.rb
├── test
├── bm_render.rb
├── stress
│ ├── plotter.rb
│ ├── stress_client.rb
│ ├── stress_local.rb
│ └── stress_server.rb
├── test_component.rb
├── test_html_canvas.rb
├── test_html_writer.rb
├── test_lru_cache.rb
└── test_request.rb
└── wee.gemspec
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7 |
8 | name: Ruby
9 |
10 | on:
11 | push:
12 | branches: [ master ]
13 | pull_request:
14 | branches: [ master ]
15 |
16 | jobs:
17 | test:
18 |
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v2
23 | - name: Set up Ruby
24 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
25 | # change this to (see https://github.com/ruby/setup-ruby#versioning):
26 | # uses: ruby/setup-ruby@v1
27 | uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
28 | with:
29 | ruby-version: 2.6
30 | - name: Install dependencies
31 | run: bundle install
32 | - name: Run tests
33 | run: bundle exec rake test
34 | - name: Run spec tests
35 | run: bundle exec rspec
36 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 | ## [x.y.z] - yyyy-mm-dd
8 | ### Changed
9 |
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Specify your gem's dependencies in wee.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Michael Neumann
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Wee Web Framework
2 |
3 | ## Copyright and License
4 |
5 | Copyright (c) 2004-2020 by Michael Neumann (mneumann@ntecs.de).
6 |
7 | Released under the terms of the MIT license.
8 |
9 | ## Introduction
10 |
11 | Wee is a light-weight, very high-level and modern web-framework that makes
12 | *W*eb *e*ngineering *e*asy. It mainly inherits many ideas and features from
13 | [Seaside][seaside], but was written from scratch without ever looking at the
14 | Seaside (or any other) sources. All code was developed from ideas and lots of
15 | discussions with Avi Bryant.
16 |
17 |
18 | ## Features
19 |
20 | ### Reusable components
21 |
22 | Wee has _real_ components, which are like widgets in a GUI. Once written, you
23 | can use them everywhere. They are completely independent and do not interfere
24 | with other components. Components encapsulate state, a view and actions. Of
25 | course you can use an external model or use templates for rendering.
26 |
27 | ### Backtracking
28 |
29 | See the _What is backtracking?_ section below. In short, backtracking lets the
30 | browser's back and forward-button play well together with your application.
31 |
32 | ### Clean and concise
33 |
34 | Wee is well thought out, is written in *and* supports clean and concise code.
35 | Furthermore I think most parts are now very well documented.
36 |
37 | ### Templating-independent
38 |
39 | Wee does not depend on a special templating-engine. You can use a different
40 | templating engine for each component if you want.
41 |
42 | ### Powerful programmatic HTML generation
43 |
44 | Wee ships with an easy to use and very powerful programmatic HTML-generation
45 | library. For example you can create a select list easily with this piece of
46 | code:
47 |
48 | ```ruby
49 | # select an object from these items
50 | items = [1, 2, 3, 4]
51 |
52 | # the labels shown to the user
53 | labels = items.map {|i| i.to_s}
54 |
55 | # render it
56 | r.select_list(items).labels(labels).callback {|choosen| p choosen}
57 |
58 | # render a multi-select list, with objects 2 and 4 selected
59 | r.select_list(items).multi.labels(labels).selected([2,4])
60 | ```
61 |
62 | The callback is called with the selected objects from the _items_ array. Items
63 | can be any object, even whole components:
64 |
65 | ```ruby
66 | labels = ["msg1", "msg2"]
67 | items = labels.collect {|m| MessageBox.new(m)}
68 | r.select_list(items).labels(labels).callback {|choosen| call choosen.first}
69 | ```
70 |
71 | ## Observations and Limitations
72 |
73 | * Components are thread-safe by nature as a fresh components-tree is created
74 | for each session and requests inside a session are serialized.
75 |
76 | ## What is backtracking?
77 |
78 | If you want, you can make the back-button of your browser work correctly
79 | together with your web-application. Imagine you have a simple counter
80 | application, which shows the current count and two links _inc_ and _dec_ with
81 | which you can increase or decrease the current count. Starting with an inital
82 | count of 0 you increase the counter up to 8, then click three times the back
83 | button of your browser (now displays 5). Finally you decrease by one and your
84 | counter shows what you'd have expected: 4. In contrast, traditional web
85 | applications would have shown 7, because the back button usually does not
86 | trigger a HTTP request and as such the server-side state still has a value of 8
87 | for the counter when the request to decrease comes in.
88 |
89 | The solution to this problem is to take snapshots of the components state after
90 | an action is performed and restoring the state before peforming actions. Each
91 | action generates a new state, which is indicated by a so-called _page-id_
92 | within the URL.
93 |
94 | ## Decorations
95 |
96 | Decorations are used to modify the look and behaviour of a component without
97 | modifying the components tree itself. A component can have more than one
98 | decoration. Decorations are implemented as a linked list
99 | (`Wee::Decoration#next` points to the next decoration), starting at
100 | `Wee::Component#decoration`, which either points to the next decoration in the
101 | chain, or to itself.
102 |
103 | ## The request/response cycle
104 |
105 | The request/response cycle in Wee is actually split into two separate phases.
106 |
107 | ### Render Phase
108 |
109 | The rendering phase is assumed to be side-effect free! So, you as a programmer
110 | should take care to meet this assumption. Rendering is performed by method
111 | `Wee::Component#render!`.
112 |
113 | ### Action Phase (Invoking Callbacks)
114 |
115 | Possible sources for callbacks are links (anchors) and all kinds of
116 | form-elements like submit buttons, input-fields etc. There are two different
117 | kinds of callbacks:
118 |
119 | * Input callbacks (input-fields)
120 |
121 | * Action callbacks (anchor, submit-button)
122 |
123 | The distinction between input and action callbacks is important, as action
124 | callbacks might depend on values of input-fields being assigned to instance
125 | variables of the controlling component. Hence, Wee first invokes all input
126 | callbacks before any action callback is triggered. Callback processing is
127 | performed by method `Wee::Component#process_callbacks`.
128 |
129 | The result of the action phase is an updated components state. As such, a
130 | snapshot is taken of the new state and stored under a new page-id. Then, a
131 | redirect requests is sent back to the client, including this new page-id. The
132 | client automatically follows this redirect and triggers a render phase of the
133 | new page.
134 |
135 | [seaside]: http://seaside.st/
136 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | task :default => :spec
5 |
6 | Rake::TestTask.new do |t|
7 | t.libs << "lib"
8 | t.test_files = FileList['test/test_*.rb']
9 | t.verbose = true
10 | end
11 |
--------------------------------------------------------------------------------
/examples/ObjectSpaceBrowser.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 | require 'enumerator'
3 | require_relative 'demo/messagebox'
4 |
5 | module ObjectSpaceBrowser
6 |
7 | class Klasses < Wee::Component
8 |
9 | def klasses
10 | ObjectSpace.to_enum(:each_object, Class).sort_by{|k| k.name}
11 | end
12 |
13 | def choose(klass)
14 | call Klass.new(klass)
15 | end
16 |
17 | def render(r)
18 | r.h1 "Classes"
19 |
20 | r.ul {
21 | klasses.each do |klass|
22 | r.li { r.anchor.callback_method(:choose, klass).with(klass.name) }
23 | end
24 | }
25 | end
26 | end
27 |
28 | class Klass < Wee::Component
29 |
30 | def initialize(klass)
31 | super()
32 | @klass = klass
33 | set_instances
34 | end
35 |
36 | def choose(instance)
37 | call Instance.new(instance)
38 | end
39 |
40 | ##
41 | # Fetches all instances of the klass sorted by object_id
42 |
43 | def set_instances
44 | @instances =
45 | case @klass
46 | when Symbol
47 | Symbol.all_symbols.sort_by do |s| s.to_s end
48 | else
49 | ObjectSpace.to_enum(:each_object, @klass).sort_by{|i| i.object_id}
50 | end
51 | end
52 |
53 | def render(r)
54 | instances = @instances
55 | r.h1 "Class #{@klass.name}"
56 | r.h2 "#{@instances.length} Instances"
57 |
58 | return if @instances.length.zero?
59 |
60 | r.ul {
61 | @instances.each do |instance|
62 | r.li { r.anchor.callback_method(:choose, instance).with("0x%x" % instance.object_id) }
63 | end
64 | }
65 | end
66 |
67 | end
68 |
69 | class Instance < Wee::Component
70 |
71 | def initialize(instance)
72 | super()
73 | @instance = instance
74 | end
75 |
76 | def choose(instance)
77 | call Instance.new(instance)
78 | end
79 |
80 | def back
81 | answer
82 | end
83 |
84 | def render(r)
85 | r.anchor.callback_method(:back).with("back")
86 |
87 | r.break
88 | r.h1 "Instance 0x%x of #{@instance.class.name}" % @instance.object_id
89 |
90 | case @instance
91 | when Array
92 | r.bold("array elements: ")
93 | r.break
94 | r.ul do
95 | @instance.each do |obj|
96 | r.li { render_obj(r, obj) }
97 | end
98 | end
99 | when Hash
100 | r.bold("hash elements: ")
101 | r.break
102 | r.table.border(1).with do
103 | r.table_row do
104 | r.table_data do r.bold("Key") end
105 | r.table_data do r.bold("Value") end
106 | end
107 |
108 | @instance.each_pair do |k, v|
109 | r.table_row do
110 | r.table_data { render_obj(r, k) }
111 | r.table_data { render_obj(r, v) }
112 | end
113 | end
114 | end
115 |
116 | when String, Float, Fixnum, Bignum, Numeric, Integer, Symbol
117 | r.encode_text(@instance.inspect)
118 | end
119 |
120 | return if @instance.instance_variables.empty?
121 | r.break
122 |
123 | render_instance_variables(r)
124 | end
125 |
126 | def render_instance_variables(r)
127 | r.table.border(1).with do
128 | r.table_row do
129 | r.table_data do r.bold("Instance Variable") end
130 | r.table_data do r.bold("Object") end
131 | end
132 | @instance.instance_variables.each do |var| render_ivar_row(r, var) end
133 | end
134 | end
135 |
136 | def render_ivar_row(r, var)
137 | r.table_row do
138 | r.table_data(var)
139 | r.table_data do
140 | v = @instance.instance_variable_get(var)
141 | render_obj(r, v)
142 | end
143 | end
144 | end
145 |
146 | def render_obj(r, obj)
147 | r.anchor.callback_method(:choose, obj).with do
148 | r.bold(obj.class.name)
149 | r.space
150 | r.text("(#{ obj.object_id })")
151 | r.space
152 | r.space
153 |
154 | case obj
155 | when String, Float, Integer, Symbol
156 | r.encode_text(obj.inspect)
157 | else
158 | r.encode_text(obj.inspect[0, 40] + "...")
159 | end
160 | end
161 | end
162 |
163 | end
164 |
165 | end # module ObjectSpaceBrowser
166 |
167 | if $0 == __FILE__ then
168 |
169 | OBJ = {
170 | "hello" => { [1,2,3] => [5,6,7], "test" => :super },
171 | "other" => %w(a b c d e f)
172 | }
173 |
174 | class Main < Wee::Component
175 | def initialize
176 | super
177 | add_decoration(Wee::PageDecoration.new("Hello World"))
178 | @instance = ObjectSpaceBrowser::Instance.new(OBJ)
179 | end
180 |
181 | def children() [@instance] end
182 |
183 | def render(r)
184 | r.render(@instance)
185 | end
186 | end
187 |
188 | Wee.run(Main)
189 | end
190 |
--------------------------------------------------------------------------------
/examples/ajax.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 | require 'rack'
3 |
4 | class AjaxCounter < Wee::Component
5 |
6 | #require 'wee/jquery'
7 | #def self.depends; [Wee::JQuery] end
8 |
9 | require 'wee/rightjs'
10 | def self.depends; [Wee::RightJS] end
11 |
12 | def initialize
13 | @counter = 0
14 | end
15 |
16 | def state(s)
17 | super
18 | s.add_ivar(self, :@counter, @counter)
19 | end
20 |
21 | =begin
22 | def style
23 | "div.wee-AjaxCounter a { border: 1px solid blue; padding: 5px; background-color: #ABABAB; };"
24 | end
25 |
26 | def render(r)
27 | r.render_style(self)
28 | r.div.css_class('wee-AjaxCounter').oid.with {
29 | r.anchor.update_component_on(:click) { @counter += 1 }.with(@counter.to_s)
30 | }
31 | end
32 | =end
33 |
34 | def render(r)
35 | r.anchor.oid.update_component_on(:click) { @counter += 1 }.with(@counter.to_s)
36 | end
37 |
38 | end
39 |
40 | class HelloWorld < Wee::RootComponent
41 |
42 | def self.depends; [AjaxCounter.depends] end
43 |
44 | def title
45 | 'Wee + Ajax'
46 | end
47 |
48 | def initialize
49 | @counters = (1..10).map { AjaxCounter.new }
50 | end
51 |
52 | def children() @counters end
53 |
54 | def render(r)
55 | render_hello(r)
56 | r.div.callback_on(:click) { p "refresh" }.with("Refresh")
57 | @counters.each {|c| r.render(c); r.break}
58 | end
59 |
60 | def render_hello(r)
61 | @hello ||= "Hello"
62 | r.div.id("hello").update_on(:click) {|r|
63 | @hello.reverse!
64 | render_hello(r)
65 | }.with(@hello)
66 | end
67 | end
68 |
69 | if __FILE__ == $0
70 | Wee.run HelloWorld, :mount_path => '/ajax', :print_message => true
71 | end
72 |
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/main.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift '../../lib'
2 | require 'rubygems'
3 | require 'wee'
4 |
5 | class BearTrap < Wee::Component
6 | attr_accessor :mouse
7 |
8 | def initialize(is_charged=true)
9 | @charged = is_charged
10 | add_decoration Wee::OidDecoration.new
11 | end
12 |
13 | def render(r)
14 | img = @charged ? 'charged' : 'snapped'
15 | brush = r.div.id('bear_trap').style("background: transparent url('/images/bear_trap_#{img}.png');")
16 | if @charged
17 | if @over
18 | brush.update_on(:mouseout) {|r|
19 | @over = false
20 | r.render(self)
21 | }
22 | else
23 | brush.update_on(:mouseover) {|r|
24 | @over = true
25 | @mouse.update(r)
26 | if @mouse.cheese_count >= 3
27 | @charged = false
28 | end
29 | r.render(self)
30 | r.javascript("alert('gotcha')") unless @charged
31 | }
32 | end
33 | end
34 | brush.with { r.image.src('/images/cheese.png').id('cheese') }
35 | end
36 | end
37 |
38 | class Mouse < Wee::Component
39 | attr_reader :cheese_count
40 |
41 | def initialize(cheese_count=0)
42 | @cheese_count = cheese_count
43 | end
44 |
45 | def render(r)
46 | r.image.src("/images/mouse.png").id("mouse").width(90 * (@cheese_count+1))
47 | end
48 |
49 | def update(r)
50 | @cheese_count += 1
51 | r.render(self)
52 | end
53 | end
54 |
55 | class Main < Wee::Component
56 | def initialize
57 | super
58 | add_decoration Wee::PageDecoration.new('A dark forest...', %w(/stylesheets/forest.css),
59 | %w(/javascripts/jquery-1.3.2.min.js /javascripts/wee-jquery.js))
60 | @trap = BearTrap.new(true)
61 | @mouse = Mouse.new
62 | @trap.mouse = @mouse
63 | end
64 |
65 | def children() [@trap, @mouse] end
66 |
67 | def render(r)
68 | r.div.id('forest').with {
69 | r.render @trap
70 | r.render @mouse
71 | }
72 | end
73 | end
74 |
75 | Wee.run(Main, :public_path => File.join(File.dirname(__FILE__), 'public')) if __FILE__ == $0
76 |
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/images/bear_trap_charged.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mneumann/wee/76fa838dc41db272d23b278329e6016283cdff5b/examples/apotomo-webhunter/public/images/bear_trap_charged.png
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/images/bear_trap_snapped.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mneumann/wee/76fa838dc41db272d23b278329e6016283cdff5b/examples/apotomo-webhunter/public/images/bear_trap_snapped.png
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/images/cheese.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mneumann/wee/76fa838dc41db272d23b278329e6016283cdff5b/examples/apotomo-webhunter/public/images/cheese.png
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/images/dark_forest.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mneumann/wee/76fa838dc41db272d23b278329e6016283cdff5b/examples/apotomo-webhunter/public/images/dark_forest.jpg
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/images/mouse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mneumann/wee/76fa838dc41db272d23b278329e6016283cdff5b/examples/apotomo-webhunter/public/images/mouse.png
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/javascripts/wee-jquery.js:
--------------------------------------------------------------------------------
1 | var wee = {};
2 |
3 | wee._update_elements = function(_,e) {
4 | var src = jQuery(e);
5 | var id = src.attr('id');
6 | if (id)
7 | jQuery('#'+id).replaceWith(src);
8 | else
9 | jQuery('html > body').append(src);
10 | };
11 |
12 | wee._update_callback = function(data) {
13 | jQuery(data).each(wee._update_elements);
14 | };
15 |
16 | wee.update = function(url) {
17 | jQuery.get(url, {}, wee._update_callback, 'html');
18 | return false;
19 | };
20 |
--------------------------------------------------------------------------------
/examples/apotomo-webhunter/public/stylesheets/forest.css:
--------------------------------------------------------------------------------
1 | /*
2 | all images were stolen and may be licensed.
3 | thanks to felix, the best programmer i've ever met, for helping me out with my rusty css.
4 | */
5 | body {
6 |
7 | }
8 |
9 | #forest {
10 | background: transparent url('/images/dark_forest.jpg') top left no-repeat;
11 |
12 | width: 540px;
13 | height: 450px;
14 | position: relative;
15 | }
16 |
17 | #bear_trap {
18 | position: absolute;
19 | left: 180px;
20 | top: 240px;
21 | width: 220px;
22 | height: 180px;
23 | }
24 |
25 | #cheese {
26 | padding-top: 36px;
27 | padding-left: 60px;
28 | }
29 |
30 | #mouse {
31 | padding-top: 36px;
32 | padding-left: 36px;
33 | }
--------------------------------------------------------------------------------
/examples/arc_challenge.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Implementation of the Arc Challenge using Wee.
3 | #
4 | # By Michael Neumann (mneumann@ntecs.de)
5 | #
6 | # http://onestepback.org/index.cgi/Tech/Ruby/ArcChallenge.red
7 | #
8 |
9 | require 'wee'
10 |
11 | class Page1 < Wee::Component
12 | def initialize
13 | add_decoration(Wee::FormDecoration.new)
14 | add_decoration(Wee::PageDecoration.new)
15 | end
16 |
17 | def render(r)
18 | r.text_input.callback {|text| call Page2.new(text)}
19 | r.submit_button.value('OK')
20 | end
21 | end
22 |
23 | class Page2 < Wee::Component
24 | def initialize(text)
25 | @text = text
26 | end
27 | def render(r)
28 | r.anchor.callback { call Page3.new(@text) }.with('click here')
29 | end
30 | end
31 |
32 | class Page3 < Page2
33 | def render(r)
34 | r.text 'You said: '
35 | r.text @text
36 | r.break
37 | end
38 | end
39 |
40 | Wee.run(Page1) if __FILE__ == $0
41 |
--------------------------------------------------------------------------------
/examples/arc_challenge2.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Implementation of the Arc Challenge using Wee.
3 | #
4 | # By Michael Neumann (mneumann@ntecs.de)
5 | #
6 | # http://onestepback.org/index.cgi/Tech/Ruby/ArcChallenge.red
7 | #
8 |
9 | require 'wee'
10 |
11 | class Wee::IO
12 | def initialize(component)
13 | @component = component
14 | end
15 |
16 | def ask
17 | @component.call_inline do |r|
18 | r.form do
19 | text = nil
20 | r.text_input.callback {|t| text = t}
21 | r.submit_button.callback { answer(text) }.value("Enter")
22 | end
23 | end
24 | end
25 |
26 | def pause(text)
27 | @component.call_inline {|r| r.anchor.callback { answer }.with(text) }
28 | end
29 |
30 | def tell(text)
31 | @component.call_inline {|r| r.text text.to_s }
32 | end
33 | end
34 |
35 | class ArcChallenge < Wee::Task
36 | def go
37 | io = Wee::IO.new(self)
38 | text = io.ask
39 | io.pause("click here")
40 | io.tell("You said: #{text}")
41 | end
42 | end
43 |
44 | Wee.run(ArcChallenge) if __FILE__ == $0
45 |
--------------------------------------------------------------------------------
/examples/cheese_task.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 |
3 | class CheeseTask < Wee::Task
4 | def go
5 | begin choose_cheese end until confirm_cheese
6 | inform_cheese
7 | end
8 |
9 | def choose_cheese
10 | @cheese = nil
11 | while @cheese.nil?
12 | @cheese = choose_from %w(Greyerzer Tilsiter Sbrinz), "What's your favorite Cheese?"
13 | end
14 | end
15 |
16 | def confirm_cheese
17 | confirm "Is #{@cheese} your favorite cheese?"
18 | end
19 |
20 | def inform_cheese
21 | inform "Your favorite is #{@cheese}."
22 | end
23 | end
24 |
25 | Wee.run(CheeseTask) if __FILE__ == $0
26 |
--------------------------------------------------------------------------------
/examples/continuations.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 | require_relative 'demo/messagebox'
3 |
4 | class MainPage < Wee::Component
5 |
6 | def initialize
7 | super
8 | add_decoration(Wee::PageDecoration.new("Test"))
9 | end
10 |
11 | def click
12 | if callcc Wee::MessageBox.new('Really quit?')
13 | callcc Wee::MessageBox.new('You clicked YES')
14 | else
15 | callcc Wee::MessageBox.new('You clicked Cancel')
16 | callcc Wee::MessageBox.new('super')
17 | end
18 | end
19 |
20 | def render(r)
21 | r.anchor.callback_method(:click).with('show')
22 | end
23 |
24 | end
25 |
26 | Wee.run(MainPage)
27 |
--------------------------------------------------------------------------------
/examples/demo.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 |
3 | require_relative 'demo/calculator'
4 | require_relative 'demo/counter'
5 | require_relative 'demo/calendar'
6 | require_relative 'demo/radio'
7 | require_relative 'demo/file_upload'
8 | require_relative 'arc_challenge2'
9 | require_relative 'cheese_task'
10 |
11 | class ArcChallengeWrapper < Wee::WrapperDecoration
12 | def global?; true end
13 |
14 | def render(r)
15 | r.paragraph
16 | url = "http://onestepback.org/index.cgi/Tech/Ruby/ArcChallenge.red"
17 | r.anchor.url(url).with(url)
18 | r.paragraph
19 | render_inner(r)
20 | end
21 | end
22 |
23 | class Demo < Wee::RootComponent
24 | class E < Struct.new(:component, :title, :file); end
25 |
26 | def title
27 | 'Wee Demos'
28 | end
29 |
30 | def initialize
31 | @components = []
32 | @components << E.new(Counter.new, "Counter", 'examples/demo/counter.rb')
33 | @components << E.new(Calculator.new, "Calculator", 'examples/demo/calculator.rb')
34 | @components << E.new(CustomCalendarDemo.new, "Calendar", 'examples/demo/calendar.rb')
35 | @components << E.new(RadioTest.new, "Radio Buttons", 'examples/demo/radio.rb')
36 | @components << E.new(FileUploadTest.new, "File Upload", 'examples/demo/file_upload.rb')
37 |
38 | if $cc
39 | # these components need continuations
40 | arc = ArcChallenge.new
41 | arc.add_decoration(ArcChallengeWrapper.new)
42 | @components << E.new(arc, "Arc Challenge", 'arc_challenge2.rb')
43 | @components << E.new(CheeseTask.new, "Cheese Task", 'cheese_task.rb')
44 | end
45 |
46 | @editor = Editor.new
47 |
48 | select_component(@components.first)
49 | end
50 |
51 | def children
52 | @components.map {|c| c.component} + [@editor]
53 | end
54 |
55 | def select_component(component)
56 | @editor.entry = @selected_component = component
57 | end
58 |
59 | def render(r)
60 | r.form.enctype_multipart.with do
61 | r.h1 'Wee Component Demos'
62 | r.div.style('float: left; width: 200px;').with {
63 | r.select_list(@components).
64 | labels(@components.map {|c| c.title}).
65 | selected(@selected_component).
66 | size(10).
67 | onclick_javascript("this.form.submit()").
68 | callback_method(:select_component)
69 | r.break
70 | r.checkbox.checked(@editor.visibility).
71 | onclick_javascript("this.form.submit()").
72 | callback {|bool| @editor.visibility = bool }
73 | r.space
74 | r.text "Show Sourcecode?"
75 | r.break
76 | }
77 | r.div.style('float: left; left: 20px; height: 200px; width: 600px; background: #EFEFEF; border: 1px dotted red; padding: 10px').with {
78 | r.render @selected_component.component
79 | }
80 | r.render @editor
81 | end
82 | end
83 |
84 | class Editor < Wee::Component
85 | attr_accessor :visibility
86 | attr_accessor :entry
87 |
88 | def initialize
89 | super
90 | @visibility = false
91 | @mode = :view
92 | @entry = nil
93 | end
94 |
95 | def save
96 | File.open(@entry.file, 'w+') {|f| f << @txt.lines.map {|l| l.chomp}.join("\n") + "\n" }
97 | load @entry.file
98 | @mode = :view
99 | end
100 |
101 | def render(r)
102 | return unless @visibility
103 | r.div.style('float: left; margin-top: 2em; border-top: 2px solid; width: 100%').with {
104 | if @mode == :view
105 | r.anchor.callback { @mode = :edit }.with('edit')
106 | r.pre.ondblclick_callback { @mode = :edit }.with {
107 | r.encode_text(File.read(@entry.file))
108 | }
109 | else
110 | r.form do
111 | r.anchor.callback { @mode = :view }.with('cancel'); r.space
112 | r.submit_button.callback_method(:save).value('Save!')
113 | r.break
114 | r.text_area.rows(25).cols(120).callback{|txt| @txt = txt }.with {
115 | r.encode_text(File.read(@entry.file))
116 | }
117 | end
118 | end
119 | }
120 | end
121 | end
122 |
123 | end
124 |
125 | if __FILE__ == $0
126 | if ARGV[0] == "cc"
127 | $cc = true
128 | Wee.run(Demo)
129 | else
130 | Wee.run(Demo, :use_continuations => false)
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/examples/demo/calculator.rb:
--------------------------------------------------------------------------------
1 | require_relative 'messagebox'
2 |
3 | class Calculator < Wee::Component
4 | def initialize
5 | super()
6 | @number_stack = []
7 | @input = ""
8 | end
9 |
10 | def state(s)
11 | super
12 | s.add(@number_stack)
13 | s.add(@input)
14 | end
15 |
16 | def render(r)
17 | r.ul { @number_stack.each {|num| r.li(num) } }
18 |
19 | r.text_input.value(@input).readonly
20 |
21 | r.space
22 |
23 | r.submit_button.value("Enter").callback { enter }
24 | r.submit_button.value("C").callback { clear }
25 |
26 | r.break
27 |
28 | (0..9).each {|num|
29 | r.submit_button.value(num.to_s).callback { append(num.to_s) }
30 | }
31 |
32 | r.submit_button.value(".").disabled(@input.include?(".")).callback { append(".") }
33 |
34 | ['+', '-', '*', '/'].each { |op|
35 | r.submit_button.value(op).callback { operation(op) }
36 | }
37 | end
38 |
39 | protected
40 |
41 | def enter
42 | @number_stack << @input.to_f
43 | clear()
44 | end
45 |
46 | def clear
47 | @input.replace("")
48 | end
49 |
50 | def append(str)
51 | @input << str
52 | end
53 |
54 | def operation(op)
55 | enter unless @input.empty?
56 | if @number_stack.size < 2
57 | call Wee::MessageBox.new('Stack underflow!')
58 | else
59 | r2, r1 = @number_stack.pop, @number_stack.pop
60 | @number_stack.push(r1.send(op, r2))
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/examples/demo/calendar.rb:
--------------------------------------------------------------------------------
1 | # Copyright by Kevin Howe (kh@newclear.ca)
2 |
3 | require 'date'
4 |
5 | class Date
6 | # Fetch the number of days in the given month
7 | #
8 | def days_in
9 | ((Date.new(self.year, self.month, 1) >> 1)-1).day
10 | end
11 | # Calendar represenation of a month. Consists of a
12 | # list of weeks, each week a list of 7 days, each day a Date object.
13 | # Padded with days showing for previous and next month.
14 | #
15 | def calendar
16 | # months
17 | curr_month = Date.new(self.year, self.month, 1)
18 | prev_month = (curr_month << 1)
19 | next_month = (curr_month >> 1)
20 |
21 | # previous month days
22 | prev_days = Array.new
23 | prev_in_curr = curr_month.wday
24 | ((curr_month-1)-(prev_in_curr-1)).upto(curr_month-1) { |d| prev_days << d }
25 |
26 | # current month days
27 | curr_days = Array.new
28 | curr_month.upto(next_month-1) { |d| curr_days << d }
29 |
30 | # next month days
31 | next_days = Array.new
32 | days = prev_days+curr_days
33 | weeks = (days.size.to_f/7).ceil
34 | cdays_size = weeks*7
35 | next_in_curr = (cdays_size-days.size)
36 | next_month.upto(next_month+(next_in_curr-1)) { |d| next_days << d }
37 | days += next_days
38 |
39 | # split into weeks
40 | table = Array.new
41 | days.each do |day|
42 | table << Array.new if table.size == 0 or table.last.size == 7
43 | table.last << day
44 | end
45 |
46 | table
47 | end
48 | end
49 |
50 | # Generates a browsable calendar.
51 | # Each day is linked, clicking will set the date to that particular day.
52 | #
53 | class MiniCalendar < Wee::Component
54 |
55 | # Browse mode: no answer will be given
56 | attr_accessor :browse
57 |
58 | # Holds the current chosen date
59 | attr_accessor :date
60 |
61 | # Initialize the MiniCalendar
62 | #
63 | def initialize(date=Date.today)
64 | super()
65 | @month = Date.new(date.year, date.month, 1)
66 | @day = date
67 | @browse = false
68 | end
69 |
70 | # Backtrack state
71 | #
72 | def state(s)
73 | super
74 | s.add(self)
75 | end
76 |
77 | # Set to browse-only (no answer will be given)
78 | #
79 | def browse(value=true)
80 | @browse = (value && true)
81 | self
82 | end
83 |
84 | # True if in browser-only mode
85 | #
86 | def browse?
87 | @browse
88 | end
89 |
90 | # True if the given date is the currently selected month
91 | #
92 | def current_month?(date)
93 | Date.new(date.year, date.month, 1) == @month
94 | end
95 |
96 | # True if the given date is the currently selected day
97 | #
98 | def selected_day?(date)
99 | date == @day
100 | end
101 |
102 | # Date object representing the previous month
103 | #
104 | def prev_month
105 | @month << 1
106 | end
107 |
108 | # Date object representing the next month
109 | #
110 | def next_month
111 | @month >> 1
112 | end
113 |
114 | # Previous month's abbreviation
115 | #
116 | def prev_month_abbr
117 | Date::ABBR_MONTHNAMES[prev_month.month]
118 | end
119 |
120 | # Next month's abbreviation
121 | #
122 | def next_month_abbr
123 | Date::ABBR_MONTHNAMES[next_month.month]
124 | end
125 |
126 | # String to be displayed as the month heading
127 | #
128 | def month_heading
129 | Date::MONTHNAMES[@month.month].to_s+' '+@month.year.to_s
130 | end
131 |
132 | # String to be displayed indicating the current date
133 | #
134 | def today_string
135 | date = Date.today
136 | mon_abbr = Date::ABBR_MONTHNAMES[date.month]
137 | day_abbr = Date::ABBR_DAYNAMES[date.wday]
138 | sprintf('Today is %s, %s %s %s', day_abbr, mon_abbr, date.day, date.year)
139 | end
140 |
141 | # Render a given day
142 | #
143 | def render_day(r, date)
144 | if current_month?(date)
145 | selected_day?(date) ? render_selected_day(r, date) : render_month_day(r, date)
146 | else
147 | render_other_day(r, date)
148 | end
149 | end
150 |
151 | # Render a day of the currently selected month
152 | #
153 | def render_month_day(r, date)
154 | r.table_data { r.anchor.callback { save(date) }.with(date.day) }
155 | end
156 |
157 | # Render the currently selected day
158 | #
159 | def render_selected_day(r, date)
160 | r.table_data.style('border: 1px solid black').with do
161 | r.anchor.style('font-weight: bold').callback { save(date) }.with(date.day)
162 | end
163 | end
164 |
165 | # Render days of the previous or next month
166 | #
167 | def render_other_day(r, date)
168 | r.table_data do
169 | r.anchor.style('color: silver').callback { save(date) }.with(date.day)
170 | end
171 | end
172 |
173 | # CSS styles
174 | #
175 | def render_styles(r)
176 | # ...
177 | end
178 |
179 | # Render Calender header
180 | #
181 | def render_header(r)
182 | r.table_row do
183 | r.table_header.colspan(4).with { r.encode_text(month_heading) }
184 | r.table_header { r.anchor.callback { go_prev }.with(prev_month_abbr) }
185 | r.table_header { r.anchor.callback { go_next }.with(next_month_abbr) }
186 | r.table_header { browse? ? r.space : r.anchor.callback { back }.style('color: black').with('X') }
187 | end
188 | end
189 |
190 | # Render Calendar footer
191 | #
192 | def render_footer(r)
193 | r.table_row { r.table_header.colspan(7).with { r.encode_text(today_string) } }
194 | end
195 |
196 | # Render Calendar
197 | #
198 | def render(r)
199 | render_styles(r)
200 | r.div.css_class("cal").with do
201 | r.text(sprintf('', @month, @day))
202 | r.table { r.table_row { r.table_header {
203 | r.table do
204 | render_header(r)
205 | r.table_row { Date::ABBR_DAYNAMES.each { |day| r.table_header(day) } }
206 | @month.calendar.each do |week|
207 | r.table_row do
208 | week.each { |day| render_day(r, day) }
209 | end
210 | end
211 | render_footer(r)
212 | end
213 | }}}
214 | end
215 | end
216 |
217 | # Return without changes
218 | #
219 | def back
220 | answer nil unless browse?
221 | end
222 |
223 | # Select the previous month
224 | #
225 | def go_prev
226 | @month = prev_month
227 | end
228 |
229 | # Select the next month
230 | #
231 | def go_next
232 | @month = next_month
233 | end
234 |
235 | # Save the given day
236 | #
237 | def save(day)
238 | @day = day
239 | @month = Date.new(day.year, day.month, 1)
240 | answer(day) unless browse?
241 | end
242 | end
243 |
244 | # Custom CSS styles
245 | #
246 | module StyleMixin
247 | def render_styles(r)
248 | r.style("
249 | .cal {
250 | font-size : 11px;
251 | font-family : Arial, Helvetica, sans-serif;
252 | text-align: center;
253 | }
254 | .cal a {
255 | text-decoration: none;
256 | }
257 | .cal td {
258 | font-family: Arial, Helvetica, sans-serif;
259 | font-size: 11px;
260 | border: 1px solid;
261 | background-color: #FFFFFF;
262 | vertical-align: top;
263 | text-align: center;
264 | }
265 | .cal th {
266 | font-family: Arial, Helvetica, sans-serif;
267 | font-size: 11px;
268 | font-style: normal;
269 | font-weight: bold;
270 | background-color: #BBCCFF;
271 | border: 1px solid;
272 | vertical-align: top;
273 | text-align: center;
274 | }
275 | ")
276 | end
277 | end
278 |
279 | # Calendar with custom CSS styles
280 | #
281 | class CustomCalendar < MiniCalendar
282 | include StyleMixin
283 | end
284 |
285 | # Calendar demo
286 | #
287 | class CustomCalendarDemo < Wee::Component
288 | include StyleMixin
289 |
290 | # Holds the current chosen date
291 | attr_accessor :date
292 |
293 | # Initialize with a Date object (defaults to today)
294 | #
295 | def initialize(date=Date.today)
296 | super()
297 | @date = date
298 | end
299 |
300 | # Backtrack state
301 | #
302 | def state(s)
303 | super
304 | s.add(self)
305 | end
306 |
307 | # Render calendar icon
308 | #
309 | def render_icon(r)
310 | icon = 'http://www.softcomplex.com/products/tigra_calendar/img/cal.gif'
311 | r.image.src(icon).width(16).height(16).border(0).alt('Calendar')
312 | end
313 |
314 | # Render Calendar demo
315 | #
316 | def render(r)
317 | r.text_input.value(@date.to_s).callback{|val| @date } #@date = val}
318 | r.space
319 | r.anchor.callback { calendar }.with { render_icon(r) }
320 | end
321 |
322 | # Call the calendar component
323 | #
324 | def calendar()
325 | call CustomCalendar.new(@date) do |date|
326 | set_date(date)
327 | end
328 | end
329 |
330 | def set_date(date)
331 | @date = date if date
332 | end
333 | end
334 |
--------------------------------------------------------------------------------
/examples/demo/counter.rb:
--------------------------------------------------------------------------------
1 | class Counter < Wee::Component
2 | attr_accessor :count
3 |
4 | def initialize(initial_count=0)
5 | @count = initial_count
6 | add_decoration Wee::StyleDecoration.new(self)
7 | end
8 |
9 | def state(s) super
10 | s.add_ivar(self, :@count)
11 | end
12 |
13 | def dec
14 | @count -= 1
15 | end
16 |
17 | def inc
18 | @count += 1
19 | end
20 |
21 | def style
22 | ".wee-Counter a { border: 1px dotted blue; margin: 2px; }"
23 | end
24 |
25 | def render(r)
26 | r.div.oid.css_class('wee-Counter').with {
27 | r.anchor.callback_method(:dec).with("--")
28 | r.space
29 | render_count(r)
30 | r.space
31 | r.anchor.callback_method(:inc).with("++")
32 | }
33 | end
34 |
35 | def render_count(r)
36 | r.text @count.to_s
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/examples/demo/editable_counter.rb:
--------------------------------------------------------------------------------
1 | # NEEDS: FormDecoration
2 |
3 | require_relative 'counter'
4 |
5 | class EditableCounter < Counter
6 |
7 | def initialize(initial_count=0)
8 | super
9 | @show_edit_field = false
10 | end
11 |
12 | def state(s)
13 | super
14 | s.add_ivar(self, :@show_edit_field, @show_edit_field)
15 | end
16 |
17 | def render_count(r)
18 | if @show_edit_field
19 | r.text_input.value(@count).size(6).callback {|val| self.count = val}
20 | r.submit_button.value('S').callback { submit }
21 | else
22 | r.anchor.callback { submit }.with(@count)
23 | end
24 | end
25 |
26 | def submit
27 | if @count.to_s !~ /^\d+$/
28 | call Wee::MessageBox.new("You entered an invalid counter! Please try again!")
29 | @count = 0
30 | else
31 | @show_edit_field = !@show_edit_field
32 | end
33 | @count = @count.to_i
34 | end
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/examples/demo/example.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 | require_relative 'window'
3 | require_relative 'editable_counter'
4 | require_relative 'messagebox'
5 |
6 | class RegexpValidatedInput < Wee::Component
7 |
8 | def initialize(init_value, regexp)
9 | super()
10 | @regexp = regexp
11 | self.value = init_value
12 | @error = false
13 | end
14 |
15 | def state(s)
16 | super
17 | s.add(self)
18 | end
19 |
20 | def value
21 | @value
22 | end
23 |
24 | def value=(new_value)
25 | raise unless new_value =~ @regexp
26 | @input = @value = new_value
27 | end
28 |
29 | def render(r)
30 | r.form do
31 | r.text_input.value(@input).callback {|val| self.input = val }
32 | r.text %(
Invalid input
) if @error
33 | end
34 | end
35 |
36 | def input
37 | @input
38 | end
39 |
40 | def input=(str)
41 | @input = str
42 |
43 | if @input =~ @regexp
44 | @value = str
45 | @error = false
46 | else
47 | @error = true
48 | end
49 | end
50 |
51 | end
52 |
53 | class MainPage < Wee::Component
54 | def initialize
55 | super()
56 | @counters = (1..10).map {|i|
57 | Window.new {|w|
58 | w.title = "Cnt #{ i }"
59 | w.pos_x = "200px"
60 | w.pos_y = "#{i*50}px"
61 | w << EditableCounter.new(i)
62 | }
63 | }
64 |
65 | @val_inp = RegexpValidatedInput.new('Michael Neumann', /^\w+\s+\w+$/)
66 |
67 | @arr = []
68 | @text = ""
69 |
70 | @list1 = (0..9).to_a
71 | @selected1 = []
72 | @list2 = []
73 | @selected2 = []
74 | end
75 |
76 | def children
77 | [@val_inp, *@counters]
78 | end
79 |
80 | def state(s)
81 | super
82 | s.add(@counters)
83 | state_decoration(s)
84 | s.add(@arr)
85 | s.add(@text)
86 |
87 | s.add(@list1)
88 | s.add(@selected1)
89 | s.add(@list2)
90 | s.add(@selected2)
91 | end
92 |
93 | attr_accessor :text
94 |
95 | def render(r)
96 | r.page.title("Counter Test").with do
97 |
98 | r.form do
99 | r.select_list(@list1).size(10).multiple.selected(@selected1).callback {|choosen| @selected1.replace(choosen)}
100 | r.submit_button.value('->').callback { @list2.push(*@selected1); @list1.replace(@list1-@selected1); @selected1.replace([]) }
101 | r.submit_button.value('<-').callback { @list1.push(*@selected2); @list2.replace(@list2-@selected2); @selected2.replace([]) }
102 | r.select_list(@list2).size(10).multiple.selected(@selected2).callback {|choosen| @selected2.replace(choosen)}
103 | end
104 |
105 | r.form do
106 |
107 | @counters.each { |cnt|
108 | r.render(cnt)
109 | }
110 |
111 | r.render(@val_inp)
112 |
113 | @arr.each do |a|
114 | r.text(a)
115 | r.break
116 | end
117 |
118 | end
119 |
120 | r.form do
121 | r.text_input.value(@text).callback{|val| @text = val}
122 | r.submit_button.callback{add}.value('add')
123 | end
124 |
125 | end
126 | end
127 |
128 | def add
129 | call Wee::MessageBox.new("Do you really want to add '" + @text + "'?") do |res|
130 | if res
131 | call Wee::MessageBox.new("Do you really really really want to add '" + @text + "'?") do |res2|
132 | @arr << @text if res2
133 | end
134 | end
135 | end
136 | end
137 | end
138 |
139 | Wee.run(MainPage) if __FILE__ == $0
140 |
--------------------------------------------------------------------------------
/examples/demo/file_upload.rb:
--------------------------------------------------------------------------------
1 | class FileUploadTest < Wee::Component
2 | def render(r)
3 | r.file_upload.callback {|f| call Uploaded.new(f[:tempfile]) }
4 | r.break
5 | r.submit_button.name('Upload')
6 | end
7 |
8 | class Uploaded < Wee::Component
9 | def initialize(file)
10 | super()
11 | @file = file
12 | end
13 |
14 | def render(r)
15 | r.pre { r.encode_text @file.read }
16 | r.anchor.callback_method(:answer).with('back')
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/examples/demo/messagebox.rb:
--------------------------------------------------------------------------------
1 | class Wee::MessageBox < Wee::Component
2 | def initialize(text)
3 | super()
4 | @text = text
5 | end
6 |
7 | def render(r)
8 | r.bold(@text)
9 | r.form do
10 | r.submit_button.value('OK').callback { answer true }
11 | r.space
12 | r.submit_button.value('Cancel').callback { answer false }
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/examples/demo/radio.rb:
--------------------------------------------------------------------------------
1 | class RadioTest < Wee::Component
2 | def render(r)
3 | grp1 = r.new_radio_group
4 | grp2 = r.new_radio_group
5 |
6 | r.paragraph
7 | r.text "Group1"
8 | r.text " (your choice: #{@g1})" if @g1
9 | r.break
10 |
11 | r.text "R1: "
12 | r.radio_button.group(grp1).checked(@g1.nil? || @g1 == 'R1').callback { @g1 = 'R1' }
13 | r.break
14 |
15 | r.text "R2: "
16 | r.radio_button.group(grp1).checked(@g1 == 'R2').callback { @g1 = 'R2' }
17 |
18 | r.paragraph
19 | r.text "Group2"
20 | r.text " (your choice: #{@g2})" if @g2
21 | r.break
22 |
23 | r.text "R1: "
24 | r.radio_button.group(grp2).checked(@g2.nil? || @g2 == 'R1').callback { @g2 = 'R1' }
25 | r.break
26 |
27 | r.text "R2: "
28 | r.radio_button.group(grp2).checked(@g2 == 'R2').callback { @g2 = 'R2' }
29 |
30 | r.paragraph
31 | r.submit_button.value('Submit')
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/examples/demo/window.rb:
--------------------------------------------------------------------------------
1 | class Window < Wee::Component
2 |
3 | attr_accessor :title, :pos_x, :pos_y
4 |
5 | def initialize(&block)
6 | super()
7 | @status = :normal
8 | @pos_x, @pos_y = "0px", "0px"
9 | @children = []
10 | block.call(self) if block
11 | end
12 |
13 | def <<(c)
14 | @children << c
15 | end
16 |
17 | def children() @children end
18 |
19 | def state(s)
20 | super
21 | s.add_ivar(self, :@status, @status)
22 | end
23 |
24 | def process_callbacks(callbacks)
25 | return if @status == :closed
26 | super
27 | end
28 |
29 | def render(r)
30 | return if @status == :closed
31 |
32 | r.table.cellspacing(0).style("border:solid 1px grey; position: absolute; left: #{@pos_x}; top: #{@pos_y};").with do
33 | r.table_row.style("background-color: lightblue; width: 100%").with do
34 | r.table_data.style("text-align: left; width: 66%").with(@title)
35 | r.table_data.style("text-align: right").with do
36 | if @status == :minimized
37 | r.anchor.callback{maximize}.with("^")
38 | else
39 | r.anchor.callback{minimize}.with("_")
40 | end
41 | r.space
42 | r.anchor.callback{close}.with("x")
43 | end
44 | end
45 | r.table_row do
46 | r.table_data.colspan(2).with do
47 | if @status == :normal
48 | for child in self.children do
49 | r.render(child)
50 | end
51 | end
52 | end
53 | end
54 | end
55 | end
56 |
57 | public
58 |
59 | def minimize
60 | @status = :minimized
61 | end
62 |
63 | def maximize
64 | @status = :normal
65 | end
66 |
67 | def close
68 | @status = :closed
69 | end
70 |
71 | end
72 |
--------------------------------------------------------------------------------
/examples/hw.rb:
--------------------------------------------------------------------------------
1 | require 'wee'
2 |
3 | class HelloWorld < Wee::RootComponent
4 | def render(r)
5 | r.h1 "Hello World from Wee!"
6 | end
7 | end
8 |
9 | Wee.run(HelloWorld) if __FILE__ == $0
10 |
--------------------------------------------------------------------------------
/examples/i18n/app.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift "../../lib"
2 | require 'rubygems'
3 | require 'wee'
4 | require 'wee/locale'
5 |
6 | class HelloWorld < Wee::RootComponent
7 | def render(r)
8 | r.h1 _("Hello World!")
9 | r.select_list(%w(en de)).selected(session.locale).labels(["English", "Deutsch"]).callback {|lang| session.locale = lang}
10 | r.submit_button.value(_("Set"))
11 | end
12 | end
13 |
14 | Wee::Application.load_locale("app", %w(en de), "en", :path => "locale", :type => :po)
15 |
16 | HelloWorld.run if __FILE__ == $0
17 |
--------------------------------------------------------------------------------
/examples/i18n/locale/de/app.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "POT-Creation-Date: 2010-01-05 00:27+0100\n"
11 | "PO-Revision-Date: 2010-01-05 00:27+0100\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language-Team: LANGUAGE \n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=UTF-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
18 |
19 | #: app.rb:8
20 | msgid "Hello World!"
21 | msgstr "Hallo Welt!"
22 |
23 | #: app.rb:10
24 | msgid "Set"
25 | msgstr "Einstellen"
26 |
--------------------------------------------------------------------------------
/examples/i18n/locale/en/app.po:
--------------------------------------------------------------------------------
1 | # SOME DESCRIPTIVE TITLE.
2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 | # This file is distributed under the same license as the PACKAGE package.
4 | # FIRST AUTHOR , YEAR.
5 | #
6 | #, fuzzy
7 | msgid ""
8 | msgstr ""
9 | "Project-Id-Version: PACKAGE VERSION\n"
10 | "POT-Creation-Date: 2010-01-05 00:27+0100\n"
11 | "PO-Revision-Date: 2010-01-05 00:27+0100\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language-Team: LANGUAGE \n"
14 | "MIME-Version: 1.0\n"
15 | "Content-Type: text/plain; charset=UTF-8\n"
16 | "Content-Transfer-Encoding: 8bit\n"
17 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
18 |
19 | #: app.rb:8
20 | msgid "Hello World!"
21 | msgstr ""
22 |
23 | #: app.rb:10
24 | msgid "Set"
25 | msgstr ""
26 |
--------------------------------------------------------------------------------
/examples/pager.rb:
--------------------------------------------------------------------------------
1 | class Pager < Wee::Component
2 | attr_accessor :num_entries, :entries_per_page
3 | attr_reader :current_page
4 |
5 | def initialize(num_entries=0)
6 | super()
7 | @num_entries = num_entries
8 | @current_page = 0
9 | @entries_per_page = 20
10 | yield self if block_given?
11 | end
12 |
13 | # Returns the number of pages
14 |
15 | def num_pages
16 | n, rest = @num_entries.divmod(@entries_per_page)
17 | if rest > 0 then n + 1 else n end
18 | end
19 |
20 | # Returns the index of the first entry on the current page
21 |
22 | def current_start_index
23 | @current_page * @entries_per_page
24 | end
25 |
26 | # Returns the index of the last page
27 |
28 | def last_page_index
29 | num_pages() - 1
30 | end
31 |
32 | # Go to first page
33 |
34 | def first
35 | goto(0)
36 | end
37 |
38 | # Go to last page
39 |
40 | def last
41 | goto(last_page_index())
42 | end
43 |
44 | # Go to previous page
45 |
46 | def prev
47 | goto(@current_page - 1)
48 | end
49 |
50 | # Go to next page
51 |
52 | def next
53 | goto(@current_page + 1)
54 | end
55 |
56 | # Go to page with index +page+
57 | # Note that page-indices start with zero!
58 |
59 | def goto(page)
60 | @current_page = page
61 | validate
62 | end
63 |
64 | def render(r)
65 | return if num_pages() <= 0
66 | render_arrow(r, :first, "<<", "Go to first page"); r.space(2)
67 | render_arrow(r, :prev, "<", "Go to previous page"); r.space(2)
68 | render_index(r); r.space(2)
69 | render_arrow(r, :next, ">", "Go to next page"); r.space(2)
70 | render_arrow(r, :last, ">>", "Go to last page")
71 | end
72 |
73 | private
74 |
75 | def render_arrow(r, sym, text, tooltip=text)
76 | r.anchor.callback_method(sym).tooltip(tooltip).with { r.encode_text(text) }
77 | end
78 |
79 | def render_index(r)
80 | last = last_page_index()
81 | (0 .. last).each do |i|
82 | if i == @current_page
83 | render_page_num(r, i, true)
84 | else
85 | render_page_num(r, i, false)
86 | end
87 | r.space if i < last
88 | end
89 | end
90 |
91 | def render_page_num(r, num, current)
92 | if current
93 | r.bold(num+1)
94 | else
95 | r.anchor.callback{ goto(num) }.with(num+1)
96 | end
97 | end
98 |
99 | def validate
100 | @current_page = [[0, @current_page].max, last_page_index()].min
101 | end
102 | end
103 |
--------------------------------------------------------------------------------
/lib/wee.rb:
--------------------------------------------------------------------------------
1 | require 'wee/version'
2 | require 'wee/state'
3 | require 'wee/callback'
4 |
5 | require 'wee/presenter'
6 | require 'wee/decoration'
7 | require 'wee/component'
8 | require 'wee/root_component'
9 | require 'wee/task'
10 | require 'wee/dialog'
11 |
12 | require 'wee/application'
13 | require 'wee/request'
14 | require 'wee/response'
15 | require 'wee/session'
16 |
17 | require 'wee/html_document'
18 | require 'wee/html_brushes'
19 | require 'wee/html_canvas'
20 |
21 | require 'continuation'
22 | require 'wee/hello_world'
23 | require 'wee/run'
24 |
--------------------------------------------------------------------------------
/lib/wee/application.rb:
--------------------------------------------------------------------------------
1 | require 'thread'
2 | require 'wee/id_generator'
3 | require 'wee/lru_cache'
4 |
5 | module Wee
6 |
7 | #
8 | # A Wee::Application manages all Session's of a single application. It
9 | # dispatches the request to the correct handler by examining the request.
10 | #
11 | class Application
12 |
13 | def self.for(component_class, session_class=Wee::Session, *component_args)
14 | new { session_class.new(component_class.new(*component_args)) }
15 | end
16 |
17 | class SessionCache < Wee::LRUCache
18 | def garbage_collect
19 | delete_if {|id, session| session.dead? }
20 | end
21 | end
22 |
23 | #
24 | # Creates a new application. The block, when called, must
25 | # return a new Session instance.
26 | #
27 | # Wee::Application.new { Wee::Session.new(root_component) }
28 | #
29 | def initialize(max_sessions=10_000, &block)
30 | @session_factory = block || raise(ArgumentError)
31 | @session_ids ||= Wee::IdGenerator::Secure.new
32 | @sessions = SessionCache.new(max_sessions)
33 | @mutex = Mutex.new
34 | end
35 |
36 | #
37 | # Garbage collect dead sessions
38 | #
39 | def cleanup_sessions
40 | @mutex.synchronize { @sessions.garbage_collect }
41 | end
42 |
43 | #
44 | # Handles a web request
45 | #
46 | def call(env)
47 | request = Wee::Request.new(env)
48 |
49 | if request.session_id
50 | session = @mutex.synchronize { @sessions.fetch(request.session_id) }
51 | if session and session.alive?
52 | session.call(env)
53 | else
54 | url = request.build_url(:session_id => nil, :page_id => nil)
55 | Wee::RefreshResponse.new("Invalid or expired session", url).finish
56 | end
57 | else
58 | session = new_session()
59 | url = request.build_url(:session_id => session.id, :page_id => nil)
60 | Wee::RedirectResponse.new(url).finish
61 | end
62 | end
63 |
64 | protected
65 |
66 | def new_session
67 | session = @session_factory.call
68 | session.application = self
69 | insert_session(session)
70 | return session
71 | end
72 |
73 | def insert_session(session, retries=3)
74 | retries.times do
75 | @mutex.synchronize {
76 | id = @session_ids.next
77 | if not @sessions.has_key?(id)
78 | @sessions.store(id, session)
79 | session.id = id
80 | return
81 | end
82 | }
83 | end
84 | raise
85 | end
86 |
87 | end # class Application
88 |
89 | end # module Wee
90 |
--------------------------------------------------------------------------------
/lib/wee/callback.rb:
--------------------------------------------------------------------------------
1 | module Wee
2 |
3 | class CallbackRegistry
4 | def initialize(prefix="")
5 | @prefix = prefix
6 | @next_id = 0
7 | @callbacks = {} # {callback_id1 => callback1, callback_id2 => callback2}
8 | @triggered = nil
9 | @obj_map = {} # obj => [callback_id1, callback_id2, ...]
10 | end
11 |
12 | def empty?
13 | @callbacks.empty?
14 | end
15 |
16 | def register(object, callback)
17 | id = @next_id
18 | @next_id += 1
19 | @callbacks[id] = callback
20 | (@obj_map[object] ||= []) << id
21 | return "#{@prefix}#{id}"
22 | end
23 |
24 | def unregister(object)
25 | if arr = @obj_map.delete(object)
26 | arr.each {|id| @callbacks.delete(id) }
27 | end
28 | end
29 |
30 | #
31 | # NOTE that if fields named "xxx" and "xxx.yyy" occur, the value of
32 | # @fields['xxx'] is { nil => ..., 'yyy' => ... }. This is required
33 | # to make image buttons work correctly.
34 | #
35 | def prepare_triggered(ids_and_values)
36 | @triggered = {}
37 | ids_and_values.each do |id, value|
38 | if id =~ /^#{@prefix}(\d+)([.](.*))?$/
39 | id, suffix = Integer($1), $3
40 | next unless @callbacks[id]
41 |
42 | if @triggered[id].kind_of?(Hash)
43 | @triggered[id][suffix] = value
44 | elsif suffix
45 | @triggered[id] = {nil => @triggered[id], suffix => value}
46 | else
47 | @triggered[id] = value
48 | end
49 | end
50 | end
51 | end
52 |
53 | def reset_triggered
54 | @triggered = nil
55 | end
56 |
57 | def each_triggered(object)
58 | if ary = @obj_map[object]
59 | for id in ary
60 | yield @callbacks[id], @triggered[id] if @triggered.has_key?(id)
61 | end
62 | end
63 | end
64 |
65 | def each_triggered_call_with_value(object)
66 | if ary = @obj_map[object]
67 | for id in ary
68 | @callbacks[id].call(@triggered[id]) if @triggered.has_key?(id)
69 | end
70 | end
71 | end
72 |
73 | def first_triggered(object)
74 | if ary = @obj_map[object]
75 | for id in ary
76 | return @callbacks[id] if @triggered.has_key?(id)
77 | end
78 | end
79 | return nil
80 | end
81 |
82 | end # class CallbackRegistry
83 |
84 | class Callbacks
85 | attr_reader :input_callbacks
86 | attr_reader :action_callbacks
87 |
88 | def initialize
89 | @input_callbacks = CallbackRegistry.new("")
90 | @action_callbacks = CallbackRegistry.new("a")
91 | end
92 |
93 | def unregister(object)
94 | @input_callbacks.unregister(object)
95 | @action_callbacks.unregister(object)
96 | end
97 |
98 | def with_triggered(ids_and_values)
99 | @input_callbacks.prepare_triggered(ids_and_values)
100 | @action_callbacks.prepare_triggered(ids_and_values)
101 | yield
102 | ensure
103 | @input_callbacks.reset_triggered
104 | @action_callbacks.reset_triggered
105 | end
106 |
107 | end # class Callbacks
108 |
109 | end # module Wee
110 |
--------------------------------------------------------------------------------
/lib/wee/component.rb:
--------------------------------------------------------------------------------
1 | require 'wee/presenter'
2 | require 'wee/decoration'
3 |
4 | module Wee
5 |
6 | #
7 | # The base class of all components. You should at least overwrite method
8 | # #render in your own subclasses.
9 | #
10 | class Component < Presenter
11 |
12 | #
13 | # Constructs a new instance of the component.
14 | #
15 | # Overwrite this method when you want to use it both as a root component
16 | # and as a non-root component. Here you can add neccessary decorations
17 | # when used as root component, as for example a PageDecoration or a
18 | # FormDecoration.
19 | #
20 | # By default this methods adds no decoration.
21 | #
22 | # See also class RootComponent.
23 | #
24 | def self.instanciate(*args, &block)
25 | new(*args, &block)
26 | end
27 |
28 | #
29 | # Return an array of classes onto which the current component depends.
30 | # Right now this is only used to determine the required ExternalResources.
31 | #
32 | def self.depends
33 | []
34 | end
35 |
36 | #
37 | # Initializes a newly created component.
38 | #
39 | def initialize
40 | end
41 |
42 | #
43 | # This method renders the content of the component.
44 | #
45 | # *OVERWRITE* this method in your own component classes to implement the
46 | # view. By default this method does nothing!
47 | #
48 | # [+r+]
49 | # An instance of class renderer_class()
50 | #
51 | def render(r)
52 | end
53 |
54 | #
55 | # Take snapshots of objects that should correctly be backtracked.
56 | #
57 | # Backtracking means that you can go back in time of the components' state.
58 | # Therefore it is neccessary to take snapshots of those objects that want to
59 | # participate in backtracking. Taking snapshots of the whole component tree
60 | # would be too expensive and unflexible. Note that methods
61 | # take_snapshot and restore_snapshot are called for those
62 | # objects to take the snapshot (they behave like marshal_dump and
63 | # marshal_load). Overwrite them if you want to define special
64 | # behaviour.
65 | #
66 | # By default only the decoration chain is backtracked. This is
67 | # required to correctly backtrack called components. To disable
68 | # backtracking of the decorations, change method
69 | # Component#state_decoration to a no-operation:
70 | #
71 | # def state_decoration(s)
72 | # # nothing here
73 | # end
74 | #
75 | # [+s+]
76 | # An object of class State
77 | #
78 | def state(s)
79 | state_decoration(s)
80 | for child in self.children
81 | child.decoration.state(s)
82 | end
83 | end
84 |
85 | NO_CHILDREN = [].freeze
86 | #
87 | # Return all child components.
88 | #
89 | # *OVERWRITE* this method and return all child components
90 | # collected in an array.
91 | #
92 | def children
93 | return NO_CHILDREN
94 | end
95 |
96 | #
97 | # Process and invoke all input callbacks specified for this component
98 | # and all of it's child components.
99 | #
100 | # Returns the action callback to be invoked.
101 | #
102 | def process_callbacks(callbacks)
103 | callbacks.input_callbacks.each_triggered_call_with_value(self)
104 |
105 | action_callback = nil
106 |
107 | # process callbacks of all children
108 | for child in self.children
109 | if act = child.decoration.process_callbacks(callbacks)
110 | raise "Duplicate action callback" if action_callback
111 | action_callback = act
112 | end
113 | end
114 |
115 | if act = callbacks.action_callbacks.first_triggered(self)
116 | raise "Duplicate action callback" if action_callback
117 | action_callback = act
118 | end
119 |
120 | return action_callback
121 | end
122 |
123 | def state_decoration(s)
124 | s.add_ivar(self, :@decoration, @decoration)
125 | end
126 |
127 | protected :state_decoration
128 |
129 | # -------------------------------------------------------------
130 | # Decoration Methods
131 | # -------------------------------------------------------------
132 |
133 | def decoration=(d) @decoration = d end
134 | def decoration() @decoration || self end
135 |
136 | #
137 | # Iterates over all decorations
138 | # (note that the component itself is excluded)
139 | #
140 | def each_decoration # :yields: decoration
141 | d = @decoration
142 | while d and d != self
143 | yield d
144 | d = d.next
145 | end
146 | end
147 |
148 | #
149 | # Searches a decoration in the decoration chain
150 | #
151 | def find_decoration
152 | each_decoration {|d| yield d and return d }
153 | return nil
154 | end
155 |
156 | #
157 | # Adds decoration +d+ to the decoration chain.
158 | #
159 | # A global decoration is added in front of the decoration chain, a local
160 | # decoration is added in front of all other local decorations but after all
161 | # global decorations.
162 | #
163 | # Returns: +self+
164 | #
165 | def add_decoration(d)
166 | if d.global?
167 | d.next = self.decoration
168 | self.decoration = d
169 | else
170 | last_global = nil
171 | each_decoration {|i|
172 | if i.global?
173 | last_global = i
174 | else
175 | break
176 | end
177 | }
178 | if last_global.nil?
179 | # no global decorations specified -> add in front
180 | d.next = self.decoration
181 | self.decoration = d
182 | else
183 | # add after last_global
184 | d.next = last_global.next
185 | last_global.next = d
186 | end
187 | end
188 |
189 | return self
190 | end
191 |
192 | #
193 | # Remove decoration +d+ from the decoration chain.
194 | #
195 | # Returns the removed decoration or +nil+ if it did not exist in the
196 | # decoration chain.
197 | #
198 | def remove_decoration(d)
199 | if d == self.decoration # 'd' is in front
200 | self.decoration = d.next
201 | else
202 | last_decoration = self.decoration
203 | next_decoration = nil
204 | loop do
205 | return nil if last_decoration == self or last_decoration.nil?
206 | next_decoration = last_decoration.next
207 | break if d == next_decoration
208 | last_decoration = next_decoration
209 | end
210 | last_decoration.next = d.next
211 | end
212 | d.next = nil # decoration 'd' no longer is an owner of anything!
213 | return d
214 | end
215 |
216 | #
217 | # Remove all decorations that match the block condition.
218 | #
219 | # Example (removes all decorations of class +HaloDecoration+):
220 | #
221 | # remove_decoration_if {|d| d.class == HaloDecoration}
222 | #
223 | def remove_decoration_if # :yields: decoration
224 | to_remove = []
225 | each_decoration {|d| to_remove << d if yield d}
226 | to_remove.each {|d| remove_decoration(d)}
227 | end
228 |
229 | # -------------------------------------------------------------
230 | # Call/Answer Methods
231 | # -------------------------------------------------------------
232 |
233 | #
234 | # Call another component (without using continuations). The calling
235 | # component is neither rendered nor are it's callbacks processed
236 | # until the called component answers using method #answer.
237 | #
238 | # [+component+]
239 | # The component to be called.
240 | #
241 | # [+return_callback+]
242 | # Is invoked when the called component answers.
243 | #
244 | # How it works
245 | #
246 | # The component to be called is wrapped with an AnswerDecoration and a
247 | # Delegate decoration. The latter is used to redirect to the called
248 | # component. Once the decorations are installed, we end the processing of
249 | # callbacks prematurely.
250 | #
251 | # When at a later point in time the called component invokes #answer, this
252 | # will raise a AnswerDecoration::Answer exception which is catched by the
253 | # AnswerDecoration we installed before calling this component, and as such,
254 | # whose process_callbacks method was called before we gained control.
255 | #
256 | # The AnswerDecoration then invokes the answer_callback to cleanup
257 | # the decorations we added during #call and finally passes control to the
258 | # return_callback.
259 | #
260 | def call(component, &return_callback)
261 | delegate = Delegate.new(component)
262 | answer = AnswerDecoration.new
263 | answer.answer_callback = UnwindCall.new(self, component, delegate, answer, &return_callback)
264 | add_decoration(delegate)
265 | component.add_decoration(answer)
266 | session.send_response(nil)
267 | end
268 |
269 | protected :call
270 |
271 | #
272 | # Reverts the changes made due to Component#call. Is called when
273 | # Component#call 'answers'.
274 | #
275 | class UnwindCall
276 | def initialize(calling, called, delegate, answer, &return_callback)
277 | @calling, @called, @delegate, @answer = calling, called, delegate, answer
278 | @return_callback = return_callback
279 | end
280 |
281 | def call(answ)
282 | @calling.remove_decoration(@delegate)
283 | @called.remove_decoration(@answer)
284 | @return_callback.call(*answ.args) if @return_callback
285 | end
286 | end
287 |
288 | #
289 | # Similar to method #call, but using continuations.
290 | #
291 | def callcc(component)
292 | delegate = Delegate.new(component)
293 | answer = AnswerDecoration.new
294 |
295 | add_decoration(delegate)
296 | component.add_decoration(answer)
297 |
298 | answ = Kernel.callcc {|cc|
299 | answer.answer_callback = cc
300 | session.send_response(nil)
301 | }
302 | remove_decoration(delegate)
303 | component.remove_decoration(answer)
304 |
305 | args = answ.args
306 | case args.size
307 | when 0
308 | return
309 | when 1
310 | return args.first
311 | else
312 | return *args
313 | end
314 | end
315 |
316 | protected :callcc
317 |
318 | #
319 | # Chooses one of #call or #callcc depending on whether a block is
320 | # given or not.
321 | #
322 | def call!(comp, &block)
323 | if block
324 | call comp, &block
325 | else
326 | callcc comp
327 | end
328 | end
329 |
330 | protected :call!
331 |
332 | def call_inline(&render_block)
333 | callcc BlockComponent.new(&render_block)
334 | end
335 |
336 | protected :call_inline
337 |
338 | #
339 | # Return from a called component.
340 | #
341 | # NOTE that #answer never returns.
342 | #
343 | # See #call for a detailed description of the call/answer mechanism.
344 | #
345 | def answer(*args)
346 | raise AnswerDecoration::Answer.new(args)
347 | end
348 |
349 | protected :answer
350 |
351 | end # class Component
352 |
353 | class BlockComponent < Component
354 | def initialize(&block)
355 | @block = block
356 | end
357 |
358 | def render(r)
359 | instance_exec(r, &@block)
360 | end
361 | end # class BlockComponent
362 |
363 | end # module Wee
364 |
--------------------------------------------------------------------------------
/lib/wee/decoration.rb:
--------------------------------------------------------------------------------
1 | require 'wee/presenter'
2 |
3 | module Wee
4 |
5 | #
6 | # Abstract base class of all decorations. Forwards the methods
7 | # #process_callbacks, #render! and #state to the next decoration in
8 | # the chain. Subclasses should provide special behaviour in these methods,
9 | # otherwise the decoration does not make sense.
10 | #
11 | # For example, a HeaderFooterDecoration class could draw a header and footer
12 | # around the decorations or components below itself:
13 | #
14 | # class HeaderFooterDecoration < Wee::Decoration
15 | # alias render! render_presenter!
16 | # def render(r)
17 | # r.text "header"
18 | # r.render_decoration(@next)
19 | # r.text "footer"
20 | # end
21 | # end
22 | #
23 | class Decoration < Presenter
24 |
25 | #
26 | # Points to the next decoration in the chain. A decoration is responsible for
27 | # all decorations or components "below" it (everything that follows this
28 | # decoration in the chain). In other words, it's the owner of everything
29 | # "below" itself.
30 | #
31 | attr_accessor :next
32 |
33 | #
34 | # Is this decoration a global or a local one? By default all decorations are
35 | # local unless this method is overwritten.
36 | #
37 | # A global decoration is added in front of the decoration chain, a local
38 | # decoration is added in front of all other local decorations but after all
39 | # global decorations.
40 | #
41 | def global?() false end
42 |
43 | #
44 | # Forwards method call to the next decoration in the chain.
45 | #
46 | def process_callbacks(callbacks)
47 | @next.process_callbacks(callbacks)
48 | end
49 |
50 | alias render_presenter! render!
51 | #
52 | # Forwards method call to the next decoration in the chain.
53 | #
54 | def render!(r)
55 | @next.render!(r)
56 | end
57 |
58 | #
59 | # We have to save the @next attribute to be able to correctly backtrack
60 | # calls, as method Wee::Component#call modifies it in the call to
61 | # component.remove_decoration(answer). Removing the
62 | # answer-decoration has the advantage to be able to call a component more
63 | # than once!
64 | #
65 | def state(s)
66 | @next.state(s)
67 | s.add_ivar(self, :@next, @next)
68 | end
69 |
70 | end # class Decoration
71 |
72 | #
73 | # A Wee::Delegate breaks the decoration chain and forwards the methods
74 | # #process_callbacks, #render! and #state to the corresponding *chain*
75 | # method of it's _delegate_ component (a Wee::Component).
76 | #
77 | class Delegate < Decoration
78 |
79 | def initialize(delegate)
80 | @delegate = delegate
81 | end
82 |
83 | #
84 | # Forwards method to the corresponding top-level *chain* method of the
85 | # _delegate_ component.
86 | #
87 | def process_callbacks(callbacks)
88 | @delegate.decoration.process_callbacks(callbacks)
89 | end
90 |
91 | #
92 | # Forwards method to the corresponding top-level *chain* method of the
93 | # _delegate_ component.
94 | #
95 | def render!(r)
96 | @delegate.decoration.render!(r)
97 | end
98 |
99 | #
100 | # Forwards method to the corresponding top-level *chain* method of the
101 | # _delegate_ component. We also take snapshots of all non-visible
102 | # components, thus we follow the @next decoration (via super).
103 | #
104 | def state(s)
105 | super
106 | @delegate.decoration.state(s)
107 | end
108 |
109 | end # class Delegate
110 |
111 | #
112 | # A Wee::AnswerDecoration is wrapped around a component that will call
113 | # Component#answer. This makes it possible to use such components without the
114 | # need to call them (Component#call), e.g. as child components of other
115 | # components.
116 | #
117 | class AnswerDecoration < Decoration
118 |
119 | #
120 | # Used to unwind the component call chain in Component#answer.
121 | #
122 | class Answer < Exception
123 | attr_reader :args
124 | def initialize(args) @args = args end
125 | end
126 |
127 | attr_accessor :answer_callback
128 |
129 | class Interceptor
130 | attr_accessor :action_callback, :answer_callback
131 |
132 | def initialize(action_callback, answer_callback)
133 | @action_callback, @answer_callback = action_callback, answer_callback
134 | end
135 |
136 | def call
137 | @action_callback.call
138 | rescue Answer => answer
139 | # return to the calling component
140 | @answer_callback.call(answer)
141 | end
142 | end
143 |
144 | #
145 | # When a component answers, @answer_callback.call(answer)
146 | # will be executed, where +answer+ is of class Answer which includes the
147 | # arguments passed to Component#answer.
148 | #
149 | def process_callbacks(callbacks)
150 | if action_callback = super
151 | Interceptor.new(action_callback, @answer_callback)
152 | else
153 | nil
154 | end
155 | end
156 |
157 | end # class AnswerDecoration
158 |
159 |
160 | class WrapperDecoration < Decoration
161 |
162 | alias render! render_presenter!
163 |
164 | #
165 | # Overwrite this method, and call render_inner(r)
166 | # where you want the inner content to be drawn.
167 | #
168 | def render(r)
169 | render_inner(r)
170 | end
171 |
172 | def render_inner(r)
173 | r.render_decoration(@next)
174 | end
175 |
176 | end # class WrapperDecoration
177 |
178 |
179 | #
180 | # Renders a tag with a unique "id" around the wrapped component.
181 | # Useful for components that want to update their content in-place using
182 | # AJAX.
183 | #
184 | class OidDecoration < WrapperDecoration
185 | def render(r)
186 | r.div.oid.with { render_inner(r) }
187 | end
188 | end # class OidDecoration
189 |
190 | #
191 | # Renders a CSS style for a component class.
192 | #
193 | # Only works when used together with a PageDecoration,
194 | # or an existing :styles divert location.
195 | #
196 | # The style is not rendered when in an AJAX request.
197 | # This is the desired behaviour as it is assumed that
198 | # a component is first rendered via a regular request
199 | # and then updated via AJAX requests.
200 | #
201 | # It is only rendered once for all instances of a given
202 | # component.
203 | #
204 | # A method #style must exist returning the CSS style.
205 | #
206 | class StyleDecoration < WrapperDecoration
207 | def initialize(component)
208 | @component = component
209 | end
210 |
211 | def render(r)
212 | r.render_style(@component)
213 | render_inner(r)
214 | end
215 | end # class StyleDecoration
216 |
217 | class FormDecoration < WrapperDecoration
218 |
219 | def global?() true end
220 |
221 | def render(r)
222 | r.form { render_inner(r) }
223 | end
224 |
225 | end # class FormDecoration
226 |
227 | class PageDecoration < WrapperDecoration
228 |
229 | def initialize(title='', stylesheets=[], javascripts=[])
230 | @title = title
231 | @stylesheets = stylesheets
232 | @javascripts = javascripts
233 | super()
234 | end
235 |
236 | def global?() true end
237 |
238 | def render(r)
239 | r.page.title(@title).head {
240 | @stylesheets.each {|s| r.link_css(s) }
241 | @javascripts.each {|j| r.javascript.src(j) }
242 | r.style.type('text/css').with { r.define_divert(:styles) }
243 | r.javascript.with { r.define_divert(:javascripts) }
244 | }.with {
245 | render_inner(r)
246 | }
247 | end
248 |
249 | end # class PageDecoration
250 |
251 | end # module Wee
252 |
--------------------------------------------------------------------------------
/lib/wee/dialog.rb:
--------------------------------------------------------------------------------
1 | require 'wee/component'
2 |
3 | module Wee
4 | class Dialog < Component; end
5 |
6 | #
7 | # Abstract class
8 | #
9 | class FormDialog < Dialog
10 | def initialize(caption)
11 | @caption = caption
12 | end
13 |
14 | def render(r)
15 | r.div.css_class('wee').with {
16 | render_caption(r)
17 | render_form(r)
18 | }
19 | end
20 |
21 | def render_caption(r)
22 | r.h3 @caption if @caption
23 | end
24 |
25 | def render_form(r)
26 | r.form.with {
27 | render_body(r)
28 | render_buttons(r)
29 | }
30 | end
31 |
32 | def render_body(r)
33 | end
34 |
35 | def render_buttons(r)
36 | return if buttons.empty?
37 | r.div.css_class('dialog-buttons').with {
38 | buttons.each do |title, return_value, sym, method|
39 | sym ||= title.downcase
40 | r.span.css_class("dialog-button-#{sym}").with {
41 | if method
42 | r.submit_button.callback_method(method).value(title)
43 | else
44 | r.submit_button.callback_method(:answer, return_value).value(title)
45 | end
46 | }
47 | end
48 | }
49 | end
50 |
51 | def buttons
52 | []
53 | end
54 | end # class FormDialog
55 |
56 | class MessageDialog < FormDialog
57 | def initialize(caption, *buttons)
58 | super(caption)
59 | @buttons = buttons
60 | end
61 |
62 | def buttons
63 | @buttons
64 | end
65 | end
66 |
67 | class InformDialog < FormDialog
68 | def buttons
69 | [['Ok', nil, :ok]]
70 | end
71 | end # class InformDialog
72 |
73 | class ConfirmDialog < FormDialog
74 | def buttons
75 | [['Yes', true, :yes], ['No', false, :no]]
76 | end
77 | end # class ConfirmDialog
78 |
79 | class SingleSelectionDialog < FormDialog
80 | attr_accessor :selected_item
81 |
82 | def initialize(items, caption=nil, selected_item=nil)
83 | super(caption)
84 | @items = items
85 | @selected_item = selected_item
86 | end
87 |
88 | def state(s) super
89 | s.add_ivar(self, :@selected_item)
90 | end
91 |
92 | def render_body(r)
93 | r.select_list(@items).selected(@selected_item).callback_method(:selected_item=)
94 | end
95 |
96 | def buttons
97 | [['Ok', nil, :ok, :ok], ['Cancel', nil, :cancel, :cancel]]
98 | end
99 |
100 | def ok
101 | answer @selected_item
102 | end
103 |
104 | def cancel
105 | answer nil
106 | end
107 | end # class SingleSelectionDialog
108 |
109 | class TextInputDialog < Wee::FormDialog
110 | attr_accessor :text
111 |
112 | def initialize(caption=nil, text="", size=50)
113 | super(caption)
114 | @text = text
115 | @size = size
116 | end
117 |
118 | def state(s) super
119 | s.add_ivar(self, :@text)
120 | end
121 |
122 | def render_body(r)
123 | r.text_input.size(@size).callback_method(:set_text).value(@text || "")
124 | end
125 |
126 | def set_text(text)
127 | @text = text.strip
128 | end
129 |
130 | def buttons
131 | [['Ok', nil, :ok, :ok], ['Cancel', nil, :cancel, :cancel]]
132 | end
133 |
134 | def ok
135 | answer @text
136 | end
137 |
138 | def cancel
139 | answer nil
140 | end
141 | end # class TextInputDialog
142 |
143 | class TextAreaDialog < TextInputDialog
144 | def initialize(caption=nil, text="", cols=50, rows=5)
145 | super(caption, text, cols)
146 | @rows = rows
147 | end
148 |
149 | def render_body(r)
150 | r.text_area.cols(@size).rows(@rows).callback_method(:set_text).with(@text || "")
151 | end
152 | end # class TextAreaDialog
153 |
154 | #
155 | # Extend class Component with shortcuts for the dialogs above
156 | #
157 | class Component
158 | def confirm(question, &block)
159 | call! ConfirmDialog.new(question), &block
160 | end
161 |
162 | def inform(message, &block)
163 | call! InformDialog.new(message), &block
164 | end
165 |
166 | def choose_from(items, caption=nil, selected_item=nil, &block)
167 | call! SingleSelectionDialog.new(items, caption, selected_item), &block
168 | end
169 | end # class Component
170 |
171 | end # module Wee
172 |
--------------------------------------------------------------------------------
/lib/wee/external_resource.rb:
--------------------------------------------------------------------------------
1 | module Wee
2 |
3 | class ExternalResource
4 | def initialize(mount_path=nil)
5 | @mount_path = mount_path || "/" + self.class.name.to_s.downcase.gsub("::", "_")
6 | end
7 |
8 | def install(builder)
9 | rd = resource_dir()
10 | builder.map(@mount_path) do
11 | run Rack::File.new(rd)
12 | end
13 | end
14 |
15 | def javascripts
16 | []
17 | end
18 |
19 | def stylesheets
20 | []
21 | end
22 |
23 | protected
24 |
25 | def resource_dir
26 | raise
27 | end
28 |
29 | def file_relative(_file, *subdirs)
30 | File.expand_path(File.join(File.dirname(_file), *subdirs))
31 | end
32 |
33 | def mount_path_relative(*paths)
34 | paths.map {|path| "#{@mount_path}/#{path}"}
35 | end
36 |
37 | end # class ExternalResource
38 |
39 | end
40 |
--------------------------------------------------------------------------------
/lib/wee/hello_world.rb:
--------------------------------------------------------------------------------
1 | require 'wee/root_component'
2 |
3 | class Wee::HelloWorld < Wee::RootComponent
4 | def render(r)
5 | r.text "Hello World from Wee!"
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/wee/html_brushes.rb:
--------------------------------------------------------------------------------
1 | module Wee
2 |
3 | class Brush
4 | attr_accessor :canvas, :document
5 |
6 | # This method is called right after #initialize. It's only here to
7 | # simplify the implementation of Brushes, mainly to avoid passing all those
8 | # arguments to super.
9 | #
10 | # There is a bit of redundancy with canvas and document here. It's there to
11 | # avoid method calls.
12 | #
13 | # A brush is considered to be closed, when @document is nil.
14 | #
15 | def setup(canvas, document)
16 | @canvas = canvas
17 | @document = document
18 | end
19 |
20 | def with(*args, &block)
21 | @canvas.nest(&block) if block
22 | @document = @canvas = nil
23 | end
24 |
25 | def close
26 | with if @document
27 | end
28 |
29 | def self.nesting?() true end
30 | end
31 |
32 | class Brush::GenericTextBrush < Brush
33 | def with(text)
34 | @document.text(text)
35 | @document = @canvas = nil
36 | end
37 |
38 | def self.nesting?() false end
39 | end
40 |
41 | class Brush::GenericEncodedTextBrush < Brush::GenericTextBrush
42 | def with(text)
43 | @document.encode_text(text)
44 | @document = @canvas = nil
45 | end
46 | end
47 |
48 | class Brush::GenericTagBrush < Brush
49 | def self.html_attr(attr, hash={})
50 | name = hash[:html_name] || attr
51 | if hash[:type] == :bool
52 | class_eval %{
53 | def #{ attr }(bool=true)
54 | if bool
55 | @attributes[:"#{ name }"] = nil
56 | else
57 | @attributes.delete(:"#{ name }")
58 | end
59 | self
60 | end
61 | }
62 | else
63 | class_eval %{
64 | def #{ attr }(value)
65 | if value == nil
66 | @attributes.delete(:"#{ name }")
67 | else
68 | @attributes[:"#{ name }"] = value
69 | end
70 | self
71 | end
72 | }
73 | end
74 |
75 | (hash[:aliases] || []).each do |a|
76 | class_eval "alias #{ a } #{ attr }"
77 | end
78 |
79 | (hash[:shortcuts] || {}).each_pair do |k, v|
80 | class_eval "def #{ k }() #{ attr }(#{ v.inspect }) end"
81 | end
82 | end
83 | end
84 |
85 | class Brush::GenericTagBrush < Brush
86 | html_attr :id
87 | html_attr :name # XXX
88 | html_attr :css_class, :html_name => :class
89 | html_attr :css_style, :html_name => :style, :aliases => [:style]
90 | html_attr :onclick
91 | html_attr :ondblclick
92 |
93 | def initialize(tag)
94 | super()
95 | @tag = tag
96 | @attributes = Hash.new
97 | end
98 |
99 | #
100 | # Assigns a unique DOM id
101 | #
102 | def oid
103 | id(get_oid())
104 | end
105 |
106 | #
107 | # Returns a unique DOM id for the underlying component
108 | #
109 | def get_oid
110 | "wee_#{@canvas.current_component.object_id}"
111 | end
112 |
113 | #
114 | # generic support for onXXX events
115 | #
116 |
117 | EVENTS = {:click => 'onclick'.freeze,
118 | :dblclick => 'ondblclick'.freeze,
119 | :mouseover => 'onmouseover'.freeze,
120 | :mouseout => 'onmouseout'.freeze,
121 | :change => 'onchange'.freeze}.freeze
122 |
123 | def javascript_on(event, javascript)
124 | ev = EVENTS[event]
125 | raise ArgumentError unless ev
126 | @attributes[ev] = "javascript: #{javascript};"
127 | self
128 | end
129 |
130 | def callback_on(event, &block)
131 | raise ArgumentError unless block
132 | url = @canvas.url_for_callback(block)
133 | javascript_on(event, "document.location.href='#{ url }'")
134 | self
135 | end
136 |
137 | def update_on(event, &render_block)
138 | raise ArgumentError unless render_block
139 | url = @canvas.url_for_callback(@canvas.session.render_ajax_proc(render_block, @canvas.current_component))
140 | javascript_on(event, "wee.update('#{ url }')")
141 | self
142 | end
143 |
144 | def update_component_on(event, component=nil, &callback_block)
145 | component ||= @canvas.current_component
146 |
147 | render_block = proc {|r|
148 | callback_block.call if callback_block
149 | r.render(component)
150 | }
151 |
152 | url = @canvas.url_for_callback(@canvas.session.render_ajax_proc(render_block, component))
153 | javascript_on(event, "wee.update('#{ url }')")
154 | self
155 | end
156 |
157 | def onclick_javascript(v)
158 | javascript_on(:click, v)
159 | end
160 |
161 | def onclick_callback(&block)
162 | callback_on(:click, &block)
163 | end
164 |
165 | def ondblclick_callback(&block)
166 | callback_on(:dblclick, &block)
167 | end
168 |
169 | def with(text=nil, &block)
170 | @document.start_tag(@tag, @attributes)
171 | @document.text(text) if text
172 | @canvas.nest(&block) if block
173 | @document.end_tag(@tag)
174 | @document = @canvas = nil
175 | end
176 |
177 | end
178 |
179 | class Brush::GenericSingleTagBrush < Brush::GenericTagBrush
180 | def with
181 | @document.single_tag(@tag, @attributes)
182 | @document = @canvas = nil
183 | end
184 |
185 | def self.nesting?() false end
186 | end
187 |
188 | class Brush::ImageTag < Brush::GenericSingleTagBrush
189 | HTML_TAG = 'img'.freeze
190 |
191 | html_attr :src
192 | html_attr :width
193 | html_attr :height
194 | html_attr :border
195 | html_attr :alt
196 |
197 | def initialize
198 | super(HTML_TAG)
199 | end
200 | end
201 |
202 | class Brush::JavascriptTag < Brush::GenericTagBrush
203 | HTML_TAG = 'script'.freeze
204 | HTML_TYPE = 'text/javascript'.freeze
205 |
206 | html_attr :src
207 | html_attr :type
208 |
209 | def initialize
210 | super(HTML_TAG)
211 | type(HTML_TYPE)
212 | end
213 | end
214 |
215 | class Brush::StyleTag < Brush::GenericTagBrush
216 | HTML_TAG = 'style'.freeze
217 |
218 | html_attr :type
219 |
220 | def initialize
221 | super(HTML_TAG)
222 | end
223 |
224 | def with(text=nil, &block)
225 | @document.start_tag(@tag, @attributes)
226 | @document.write("\n")
230 | @document.end_tag(@tag)
231 | @document = @canvas = nil
232 | end
233 | end
234 |
235 | #---------------------------------------------------------------------
236 | # Table
237 | #---------------------------------------------------------------------
238 |
239 | class Brush::TableTag < Brush::GenericTagBrush
240 | HTML_TAG = 'table'.freeze
241 |
242 | html_attr :cellspacing
243 | html_attr :border
244 |
245 | def initialize
246 | super(HTML_TAG)
247 | end
248 | end
249 |
250 | class Brush::TableRowTag < Brush::GenericTagBrush
251 | HTML_TAG = 'tr'.freeze
252 |
253 | html_attr :align, :shortcuts => {
254 | :align_top => :top, :align_bottom => :bottom
255 | }
256 |
257 | def initialize
258 | super(HTML_TAG)
259 | end
260 |
261 | def columns(*cols, &block)
262 | with {
263 | cols.each {|col|
264 | @canvas.table_data.with {
265 | if block
266 | block.call(col)
267 | else
268 | @canvas.text(col)
269 | end
270 | }
271 | }
272 | }
273 | end
274 |
275 | def headings(*headers, &block)
276 | with {
277 | headers.each {|header|
278 | @canvas.table_header.with {
279 | if block
280 | block.call(header)
281 | else
282 | @canvas.text(header)
283 | end
284 | }
285 | }
286 | }
287 | end
288 |
289 | def spanning_column(str, colspan)
290 | with { @canvas.table_data.col_span(colspan).with(str) }
291 | end
292 |
293 | def spacer
294 | with { @canvas.table_data { @canvas.space } }
295 | end
296 | end
297 |
298 | class Brush::TableDataTag < Brush::GenericTagBrush
299 | HTML_TAG = 'td'.freeze
300 |
301 | html_attr :colspan
302 | html_attr :align, :shortcuts => {
303 | :align_top => :top,
304 | :align_bottom => :bottom
305 | }
306 |
307 | def initialize
308 | super(HTML_TAG)
309 | end
310 | end
311 |
312 | class Brush::TableHeaderTag < Brush::GenericTagBrush
313 | HTML_TAG = 'th'.freeze
314 |
315 | html_attr :colspan
316 | html_attr :align, :shortcuts => {
317 | :align_top => :top,
318 | :align_bottom => :bottom
319 | }
320 |
321 | def initialize
322 | super(HTML_TAG)
323 | end
324 | end
325 |
326 | #---------------------------------------------------------------------
327 | # Callback Mixin
328 | #---------------------------------------------------------------------
329 |
330 | module CallbackMixin
331 |
332 | def callback_method(id, *args)
333 | @callback = self
334 | @callback_object = @canvas.current_component
335 | @callback_id = id
336 | @callback_args = args
337 | __callback()
338 | return self
339 | end
340 |
341 | def callback(&block)
342 | @callback = block
343 | __callback()
344 | return self
345 | end
346 |
347 | #
348 | # Is called when #callback_method was used.
349 | #
350 | def call(*args)
351 | args.push(*@callback_args)
352 | @callback_object.send(@callback_id, *args)
353 | end
354 |
355 | end
356 |
357 | #---------------------------------------------------------------------
358 | # Form
359 | #---------------------------------------------------------------------
360 |
361 | class Brush::FormTag < Brush::GenericTagBrush
362 | HTML_TAG = 'form'.freeze
363 | HTML_METHOD_POST = 'POST'.freeze
364 |
365 | html_attr :action
366 | html_attr :enctype
367 |
368 | #
369 | # Use this enctype when you have a FileUploadTag field.
370 | #
371 | def enctype_multipart
372 | enctype('multipart/form-data')
373 | end
374 |
375 | def initialize
376 | super(HTML_TAG)
377 | @attributes[:method] = HTML_METHOD_POST
378 | end
379 |
380 | def with(&block)
381 | # If no action was specified, use a dummy one.
382 | unless @attributes.has_key?(:action)
383 | @attributes[:action] = @canvas.build_url
384 | end
385 | super
386 | end
387 |
388 | include CallbackMixin
389 |
390 | def __callback; action(@canvas.url_for_callback(@callback)) end
391 |
392 | =begin
393 | def onsubmit_update(update_id, &block)
394 | raise ArgumentError if symbol and block
395 | url = @canvas.url_for_callback(block, :live_update)
396 | onsubmit("javascript: new Ajax.Updater('#{ update_id }', '#{ url }', {method:'get', parameters: Form.serialize(this)}); return false;")
397 | end
398 | =end
399 | end
400 |
401 | #---------------------------------------------------------------------
402 | # Form - Input
403 | #---------------------------------------------------------------------
404 |
405 | class Brush::InputTag < Brush::GenericSingleTagBrush
406 | HTML_TAG = 'input'.freeze
407 |
408 | html_attr :type
409 | html_attr :name
410 | html_attr :value
411 | html_attr :size
412 | html_attr :maxlength
413 | html_attr :src
414 | html_attr :checked, :type => :bool
415 | html_attr :disabled, :type => :bool
416 | html_attr :readonly, :type => :bool
417 |
418 | def initialize(_type)
419 | super(HTML_TAG)
420 | type(_type)
421 | end
422 |
423 | include CallbackMixin
424 |
425 | def __callback; name(@canvas.register_callback(:input, @callback)) end
426 | end
427 |
428 | class Brush::TextInputTag < Brush::InputTag
429 | HTML_TYPE = 'text'.freeze
430 |
431 | def initialize
432 | super(HTML_TYPE)
433 | end
434 | end
435 |
436 | class Brush::HiddenInputTag < Brush::InputTag
437 | HTML_TYPE = 'hidden'.freeze
438 |
439 | def initialize
440 | super(HTML_TYPE)
441 | end
442 | end
443 |
444 | class Brush::PasswordInputTag < Brush::InputTag
445 | HTML_TYPE = 'password'.freeze
446 |
447 | def initialize
448 | super(HTML_TYPE)
449 | end
450 | end
451 |
452 | class Brush::CheckboxTag < Brush::InputTag
453 | HTML_TYPE = 'checkbox'.freeze
454 |
455 | def initialize
456 | super(HTML_TYPE)
457 | end
458 |
459 | def __callback; end # do nothing
460 |
461 | def with
462 | if @callback
463 | n = @canvas.register_callback(:input, proc {|input|
464 | @callback.call(input.send(input.kind_of?(Array) ? :include? : :==, '1'))
465 | })
466 | @document.single_tag('input', :type => 'hidden', :name => n, :value => '0')
467 | name(n)
468 | value('1')
469 | end
470 | super
471 | end
472 | end
473 |
474 | #
475 | # Use a