├── .gitignore ├── ruby_libs ├── base.rb ├── init.rb ├── options.rb ├── config.rb ├── compiler.rb └── installer.rb ├── src ├── style │ ├── table_ruler.js │ └── color.js ├── azuki.js ├── helpers │ ├── compatibility_helper.js │ ├── forms_helper.js │ ├── keyboard_helper.js │ └── window_helper.js ├── storage │ └── cookie.js ├── windowing │ ├── busybox.js │ ├── popinfo.js │ ├── contextual_help.js │ ├── fader.js │ └── lightbox.js ├── forms │ ├── select_search.js │ ├── textarea_extensions.js │ └── remote.js └── controller.js ├── images ├── no.gif ├── ok.gif ├── add.gif ├── add.png ├── back.png ├── close.gif ├── close.png ├── error.gif ├── feed.gif ├── file.png ├── help.gif ├── info.gif ├── job.png ├── logo.gif ├── new.png ├── open.png ├── trash.gif ├── yes.gif ├── contact.png ├── export.gif ├── loading.gif ├── loading.png ├── locked.gif ├── modify.png ├── options.gif ├── remove.png ├── saving.gif ├── saving.png ├── search.gif ├── export_16.gif ├── reminder.png ├── small_file.png ├── small_job.png ├── trash_16.gif ├── triangle.gif ├── close_small.png ├── small_contact.png └── small_reminder.png ├── config.yml.example ├── Rakefile ├── tests ├── test.css └── unit │ ├── style │ ├── table_ruler_test.html │ └── color_test.html │ ├── windowing │ ├── contextual_help_test.html │ ├── fader_test.html │ ├── busybox_test.html │ ├── lightbox_test.html │ └── popinfo_test.html │ ├── forms │ ├── textarea_extensions_test.html │ ├── remote_form_test.html │ └── select_search_test.html │ ├── controller_test.html │ ├── helpers │ ├── keyboard_helper_test.html │ ├── forms_helper_test.html │ └── window_helper_test.html │ ├── index.html │ └── storage │ └── cookie_test.html ├── css └── azuki.css ├── lib ├── sound.js ├── scriptaculous.js ├── builder.js ├── slider.js ├── unittest.js └── dragdrop.js ├── README.textile └── azukilib.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /ruby_libs/base.rb: -------------------------------------------------------------------------------- 1 | module AzukiLib 2 | end -------------------------------------------------------------------------------- /src/style/table_ruler.js: -------------------------------------------------------------------------------- 1 | /* Not implemented yet */ -------------------------------------------------------------------------------- /images/no.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/no.gif -------------------------------------------------------------------------------- /images/ok.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/ok.gif -------------------------------------------------------------------------------- /images/add.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/add.gif -------------------------------------------------------------------------------- /images/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/add.png -------------------------------------------------------------------------------- /images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/back.png -------------------------------------------------------------------------------- /images/close.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/close.gif -------------------------------------------------------------------------------- /images/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/close.png -------------------------------------------------------------------------------- /images/error.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/error.gif -------------------------------------------------------------------------------- /images/feed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/feed.gif -------------------------------------------------------------------------------- /images/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/file.png -------------------------------------------------------------------------------- /images/help.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/help.gif -------------------------------------------------------------------------------- /images/info.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/info.gif -------------------------------------------------------------------------------- /images/job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/job.png -------------------------------------------------------------------------------- /images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/logo.gif -------------------------------------------------------------------------------- /images/new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/new.png -------------------------------------------------------------------------------- /images/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/open.png -------------------------------------------------------------------------------- /images/trash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/trash.gif -------------------------------------------------------------------------------- /images/yes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/yes.gif -------------------------------------------------------------------------------- /images/contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/contact.png -------------------------------------------------------------------------------- /images/export.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/export.gif -------------------------------------------------------------------------------- /images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/loading.gif -------------------------------------------------------------------------------- /images/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/loading.png -------------------------------------------------------------------------------- /images/locked.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/locked.gif -------------------------------------------------------------------------------- /images/modify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/modify.png -------------------------------------------------------------------------------- /images/options.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/options.gif -------------------------------------------------------------------------------- /images/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/remove.png -------------------------------------------------------------------------------- /images/saving.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/saving.gif -------------------------------------------------------------------------------- /images/saving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/saving.png -------------------------------------------------------------------------------- /images/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/search.gif -------------------------------------------------------------------------------- /images/export_16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/export_16.gif -------------------------------------------------------------------------------- /images/reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/reminder.png -------------------------------------------------------------------------------- /images/small_file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/small_file.png -------------------------------------------------------------------------------- /images/small_job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/small_job.png -------------------------------------------------------------------------------- /images/trash_16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/trash_16.gif -------------------------------------------------------------------------------- /images/triangle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/triangle.gif -------------------------------------------------------------------------------- /images/close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/close_small.png -------------------------------------------------------------------------------- /images/small_contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/small_contact.png -------------------------------------------------------------------------------- /images/small_reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexyoung/Azukilib/master/images/small_reminder.png -------------------------------------------------------------------------------- /src/azuki.js: -------------------------------------------------------------------------------- 1 | var Azuki = { 2 | Version: '0.0.1', 3 | Forms: { }, 4 | Helpers: { }, 5 | Storage: { }, 6 | Style: { }, 7 | Windowing: { } 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /ruby_libs/init.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'find' 3 | require 'yaml' 4 | require 'ftools' 5 | 6 | [:options, :base, :compiler, :installer, :config].each do |name| 7 | require File.expand_path(File.dirname(__FILE__) + "/#{name}") 8 | end 9 | -------------------------------------------------------------------------------- /ruby_libs/options.rb: -------------------------------------------------------------------------------- 1 | module AzukiLib 2 | class Options 3 | @@project_name = nil 4 | 5 | class << self 6 | def image_path ; '/images/azuki/' ; end 7 | 8 | def project_name ; @@project_name ; end 9 | def project_name=(value) 10 | raise "Please specify a project with project=project_name" if value.nil? or value.empty? 11 | @@project_name = value 12 | end 13 | end 14 | end 15 | end -------------------------------------------------------------------------------- /config.yml.example: -------------------------------------------------------------------------------- 1 | project_1: 2 | project_path: ../project_1/trunk/ 3 | css_target: public/stylesheets/screen.css 4 | insert_css: true 5 | javascript_target: public/javascripts/lib/ 6 | image_target: public/images/azuki/ 7 | image_relative: /images/azuki/ 8 | project_2: 9 | project_path: ../project_2/trunk/ 10 | css_target: public/stylesheets/screen.css 11 | insert_css: true 12 | javascript_target: public/javascripts/lib/ 13 | image_target: public/images/azuki/ 14 | image_relative: /images/azuki/ -------------------------------------------------------------------------------- /src/helpers/compatibility_helper.js: -------------------------------------------------------------------------------- 1 | Azuki.Helpers.Compatibility = { 2 | /* Rather than hacking IE's png transparency, we currently use this to switch between gif and png. */ 3 | suitable_image_format: function() 4 | { 5 | if (!navigator.appVersion.match(/MSIE/)) return 'png'; 6 | 7 | try { 8 | var version = parseFloat(navigator.appVersion.split('MSIE')[1]); 9 | return (version < 7.0) ? 'gif' : 'png'; 10 | } 11 | catch (exception) { 12 | return 'png'; 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'ruby_libs/init' 2 | 3 | include AzukiLib 4 | 5 | namespace :azukilib do 6 | desc "Compiles all of the source files into azukilib.js, removing any extraneous boilerplate comments." 7 | task :compile do 8 | Compiler.generate 'azukilib.js' 9 | end 10 | 11 | desc "Installs the JavaScript, CSS and images into your project" 12 | task :install do 13 | Options.project_name = ENV['project'] 14 | 15 | Compiler.generate 'azukilib.js' 16 | Installer.install 17 | end 18 | 19 | desc "Runs the tests" 20 | task :test do 21 | end 22 | end -------------------------------------------------------------------------------- /ruby_libs/config.rb: -------------------------------------------------------------------------------- 1 | module AzukiLib 2 | class Config 3 | class << self 4 | def [](key) 5 | load_config 6 | @@config[key] 7 | end 8 | 9 | def []=(key, value) 10 | load_config 11 | @@config[key] = value 12 | end 13 | 14 | private 15 | 16 | def load_config 17 | if Options.project_name 18 | @@config ||= YAML.load(File.open('config.yml'))[Options.project_name] 19 | else 20 | @@config = {} 21 | end 22 | rescue Errno::ENOENT 23 | raise Errno::ENOENT, "Please create a config.yml file" 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /src/storage/cookie.js: -------------------------------------------------------------------------------- 1 | Azuki.Storage.Cookie = { 2 | create: function(name, value, days, path) { 3 | var expires = ''; 4 | path = typeof path == 'undefined' ? '/' : path; 5 | 6 | if (days) { 7 | var date = new Date(); 8 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 9 | expires = "; expires=" + date.toGMTString(); 10 | } 11 | 12 | if (name && value) { 13 | document.cookie = name + '=' + escape(value) + expires + '; path=' + path; 14 | } 15 | }, 16 | 17 | find: function(name) { 18 | var matches = document.cookie.match(name + '=([^;]*)'); 19 | if (matches && matches.length == 2) { 20 | return unescape(matches[1]); 21 | } 22 | }, 23 | 24 | destroy: function(name) { 25 | this.create(name, ' ', -1); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /tests/test.css: -------------------------------------------------------------------------------- 1 | body { background-color: white; font-family: "Helvetica Neue", Helvetica, sans-serif; padding: 0 4em 2em 4em; margin: 0; font-size: 14px; } 2 | 3 | h1 { font-family: "Helvetica Neue", Helvetica, sans-serif; font-weight: bold; color: #411; } 4 | h2 { color: #292; font-family: "Trebuchet MS", Helvetica, sans-serif; font-size: 2em; margin: 0; padding: 5px 0; letter-spacing: -2px; } 5 | h3 { color: #141; margin: 10px 0 4px 0; padding: 0; font-size: normal} 6 | p { margin: 10px 0 0 0; padding: 0; } 7 | h1 img { margin: 0 0 0 0; padding: 0 0 0 20px; clear: none;} 8 | a { color: #333; text-decoration: none; background-color: #ffc; } 9 | a:hover { background-color: #333; color: white; text-decoration: none; } 10 | 11 | h2.page_title { color: #ff3092; text-align: left;} 12 | 13 | #Busybox { background-color: #000; color: #fff; } 14 | #HelpContainer { background-color: #000; color: #fff; } 15 | 16 | #Logo { float: right; } -------------------------------------------------------------------------------- /src/windowing/busybox.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | Displays a Loading message when control needs to be taken away from the user. 3 | */ 4 | Azuki.Windowing.Busybox = { 5 | /* operation can be 'loading', 'saving', etc. depending on the images you've got. */ 6 | busy: function(operation) { 7 | this.remove(); 8 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 9 | 10 | new Insertion.Top(document.body, ''); 11 | Azuki.Helpers.Window.center('Busybox'); 12 | $('Busybox').show(); 13 | }, 14 | 15 | remove: function() { 16 | if ($('Busybox')) $('Busybox').remove(); 17 | }, 18 | 19 | done: function() { 20 | new Effect.Fade($('Busybox'), { afterFinish: function() { Azuki.Windowing.Busybox.remove(); }}); 21 | }, 22 | 23 | active: function() { 24 | return $('Busybox') ? true : false; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/windowing/popinfo.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | Tooltip help. 3 | 4 | Examples: 5 | 6 | Azuki.Windowing.PopInfo.display(element, 'Saved') 7 | */ 8 | 9 | Azuki.Windowing.PopInfo = { 10 | display: function(element, info, callback) { 11 | var delay = 2000; 12 | var popup_element_id = element.id + '_popup'; 13 | 14 | this.remove(popup_element_id); 15 | new Insertion.Before(element, ''); 16 | 17 | if (callback) callback(element); 18 | 19 | new Effect.Appear(popup_element_id, { duration: 0.2 }); 20 | setTimeout((function() { this.fade_popup(popup_element_id); }.bind(this)), delay); 21 | }, 22 | 23 | fade_popup: function(popup_element_id) { 24 | try { 25 | new Effect.Fade(popup_element_id); 26 | setTimeout((function() { this.remove(popup_element_id); }).bind(this), 1000); 27 | } 28 | catch (exception) { 29 | } 30 | }, 31 | 32 | remove: function(popup_element_id) { 33 | try { 34 | if ($(popup_element_id)) Element.remove(popup_element_id); 35 | } 36 | catch (exception) { 37 | 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /tests/unit/style/table_ruler_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

AzukiLib test file

17 |

18 | Tests for style/table_ruler.js 19 |

20 | 21 | 22 |
23 | 24 | 25 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/forms/select_search.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | This class allows a select box to be dynamically searched. 3 | */ 4 | Azuki.Forms.SelectSearch = Class.create(); 5 | Azuki.Forms.SelectSearch.prototype = { 6 | initialize: function(search_input, search_results) { 7 | this.search_input = $(search_input); 8 | this.search_results = $(search_results); 9 | this.items = this.read_values(); 10 | 11 | new Event.observe(this.search_input, 'click', this.clear_search_field.bindAsEventListener(this)); 12 | new Event.observe(this.search_input, 'keydown', this.search.bindAsEventListener(this)); 13 | }, 14 | 15 | clear_search_field: function() { 16 | this.search_input.value = ''; 17 | this.items.each(function(option, i) { 18 | this.search_results.options[i] = new Option(option.text, option.value); 19 | }.bind(this)); 20 | }, 21 | 22 | read_values: function() { 23 | return $A(this.search_results.options).collect(function(option) { 24 | return {value: option.value, text: option.innerHTML}; 25 | }); 26 | }, 27 | 28 | search: function() { 29 | var query = this.search_input.value; 30 | var results = this.items.findAll(function(value) { 31 | return value.text.toLowerCase().match(query.toLowerCase()); 32 | }); 33 | 34 | this.search_results.options.length = 0; 35 | 36 | results.each(function(option, i) { 37 | this.search_results.options[i] = new Option(option.text, option.value); 38 | }.bind(this)); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /tests/unit/windowing/contextual_help_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

AzukiLib test file

16 |

17 | Tests for windowing/contextual_help.js 18 |

19 | 20 | 21 |
22 | 23 |

help

24 | 25 | 26 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /tests/unit/windowing/fader_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

AzukiLib test file

16 |

17 | Tests for windowing/fader.js 18 |

19 | 20 | 21 |
22 | 23 | 24 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/unit/forms/textarea_extensions_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

AzukiLib test file

15 |

16 | Tests for forms/textarea_extensions.js 17 |

18 | 19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 29 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /tests/unit/controller_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

AzukiLib test file

15 |

16 | Tests for controller.js 17 |

18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /css/azuki.css: -------------------------------------------------------------------------------- 1 | /* Busybox */ 2 | #Busybox { position: absolute; z-index: 1000; padding: 1em !important; left: 0; top: 0; margin: 0 0 0 0 !important; width: 138px; height: 81px; position: absolute; } 3 | 4 | /* Contextual help */ 5 | a.help { margin-left: 1px; padding-left: 16px; background: url('/images/azuki/help.gif') no-repeat left; font-size: 86% !important; background-position: 0; white-space: nowrap; cursor: pointer } 6 | #HelpContainer { width: 500px; z-index: 1001;} 7 | #HelpContent { margin: 4px; padding: 10px; text-align: left; line-height: 1.2; z-index: 101; background-color: #fff; } 8 | #HelpContent h3 { text-align: center; font-size: 120% !important; border-bottom: 1px solid #ccc; margin-top: 0; margin-bottom: 10px; } 9 | 10 | /* Virtual windows */ 11 | .popinfo_container { position: absolute; z-index: 1000; padding: 1em !important; left: 0; top: 0; margin: 0 0 0 0 !important; border: 2px solid #999 !important; background-color: #ccc; text-align: center;} 12 | .popinfo_frame { display: block; position: absolute; margin: -3em 0 0 0; width: 10em; z-index: 1; font-size: 12px; } 13 | .popinfo_inner { display: block; position: absolute; z-index: 1; border: solid 1px #999; background-color: #ff8; text-align: center; margin: 0; padding: 4px; color: #000 } 14 | .popinfo_image { display: block; position: absolute; top: 23px; z-index: 1; width: 28px; height: 11px; padding: 0; margin: 0; background: transparent url('/images/azuki/triangle.gif') no-repeat; } 15 | 16 | /* Keyboard shortcuts */ 17 | div.accesskey { text-decoration: underline; display: inline; margin: 0; padding: 0;} 18 | span.accesskey { text-decoration: underline; } 19 | 20 | /* Textarea extensions */ 21 | .bigger, .smaller { cursor: pointer } 22 | -------------------------------------------------------------------------------- /tests/unit/windowing/busybox_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

AzukiLib test file

17 |

18 | Tests for windowing/busybox.js 19 |

20 | 21 | 22 |
23 | 24 | 25 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/unit/forms/remote_form_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

AzukiLib test file

15 |

16 | Tests for forms/remote_form.js 17 |

18 | 19 | 20 |
21 | 22 | 25 | 26 | 27 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/unit/windowing/lightbox_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

AzukiLib test file

17 |

18 | Tests for windowing/lightbox.js 19 |

20 | 21 | 22 |
23 | 24 |

Link to an image

25 | 26 | 27 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/forms/textarea_extensions.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | This class provides tools for making textareas more friendly. 3 | */ 4 | Azuki.Forms.TextAreaExtensions = Class.create(); 5 | Azuki.Forms.TextAreaExtensions.prototype = { 6 | initialize: function() { 7 | this._add_images(); 8 | 9 | this.bigger = $$('.bigger'); 10 | this.smaller = $$('.smaller'); 11 | 12 | // Add event observer to all more buttons 13 | this.bigger.each(function(item) { 14 | Event.observe(item, 'click', this.increase_rows.bindAsEventListener(this)); 15 | }.bind(this)); 16 | 17 | // Add event observer to all less buttons 18 | this.smaller.each(function(item) { 19 | Event.observe(item, 'click', this.decrease_rows.bindAsEventListener(this)); 20 | }.bind(this)); 21 | }, 22 | 23 | increase_rows: function(e) { 24 | var element = $(Event.element(e)); 25 | var textarea = this._get_next_textarea(element); 26 | textarea.rows += 5; 27 | }, 28 | 29 | decrease_rows: function(e) { 30 | var element = $(Event.element(e)); 31 | var textarea = this._get_next_textarea(element); 32 | 33 | if (textarea.rows >= 5) { 34 | textarea.rows -= 4; 35 | } 36 | }, 37 | 38 | /** Private methods **/ 39 | _get_next_textarea: function(element) { 40 | var children = $A(element.parentNode.parentNode.childNodes); 41 | 42 | return children.find(function(child) { return child.nodeName == 'TEXTAREA'; }); 43 | }, 44 | 45 | _add_images: function() { 46 | $$('textarea').each(function(textarea) { 47 | new Insertion.Before(textarea, 'Increase the size of this text boxDecrease the size of this text box
'); 48 | }); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /ruby_libs/compiler.rb: -------------------------------------------------------------------------------- 1 | module AzukiLib 2 | class Compiler 3 | class << self 4 | def generate(file_name, options = {}) 5 | compiler = Compiler.new file_name, options 6 | compiler.read 'src/' 7 | compiler.parse 8 | compiler.save 9 | end 10 | end 11 | 12 | attr_accessor :source 13 | 14 | def initialize(file_name = nil, options = {}) 15 | @file_name = file_name 16 | @options = options 17 | end 18 | 19 | def read(directory) 20 | @source = File.open(directory + '/azuki.js').read 21 | 22 | Find.find(directory) do |path| 23 | # Ignore dot files 24 | if path == ?. 25 | Find.prune 26 | elsif File.basename(path) != 'azuki.js' and path.match /\.js$/ 27 | @source << "\n/* START #{File.basename(path)} */\n" 28 | @source << File.open(path, 'r').read 29 | @source << "\n/* END #{File.basename(path)} */\n" 30 | end 31 | end 32 | end 33 | 34 | def save 35 | file = File.open(@file_name, 'w') 36 | file << @source 37 | file.close 38 | end 39 | 40 | def parse 41 | remove_boilerplates 42 | replace_image_urls 43 | end 44 | 45 | def remove_boilerplates 46 | # Remove /* :boilerplate: */ 47 | inside_boilerplate = false 48 | source = '' 49 | 50 | @source.split("\n").each do |line| 51 | inside_boilerplate = true if line.match %r{/\*} and line.match /:boilerplate:/ 52 | source << line + "\n" unless inside_boilerplate 53 | inside_boilerplate = false if line.match %r{\*/} 54 | end 55 | 56 | @source = source 57 | end 58 | 59 | def replace_image_urls 60 | return unless Config['image_relative'] 61 | @source.gsub! Options.image_path, Config['image_relative'] 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /tests/unit/helpers/keyboard_helper_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

AzukiLib test file

16 |

17 | Tests for helpers/keyboard_helper.js 18 |

19 | 20 | 21 |
22 | 23 |
24 | Save 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/windowing/contextual_help.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | Sets up an event that waits for clicks on links with a particular class ('help' is the default.) 3 | 4 | Windows are displayed containing help information using Ajax to load a remote page, 5 | derived from the URL in the link. 6 | */ 7 | Azuki.Windowing.ContextualHelp = Class.create(); 8 | Azuki.Windowing.ContextualHelp.prototype = { 9 | initialize: function(help_class) { 10 | this.help_class = help_class ? help_class : 'help'; 11 | 12 | Event.observe(document, 'click', function(e) { 13 | var element = $(Event.element(e)); 14 | if (!element.hasClassName(this.help_class)) return true; 15 | if (element.nodeName != 'A') return true; 16 | 17 | this.display_help(element.href); 18 | 19 | Event.stop(e); 20 | return false; 21 | }.bind(this)); 22 | }, 23 | 24 | display_help: function(url) { 25 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 26 | var faded = Azuki.Windowing.Fader.active(); 27 | 28 | if (Azuki.Windowing.Busybox.active()) return; 29 | if ($('HelpContainer')) $('HelpContainer').remove(); 30 | 31 | Azuki.Windowing.Busybox.busy('loading'); 32 | new Insertion.Top(document.body, ''); 33 | 34 | Event.observe($('HelpClose'), 'click', function() { $('HelpContainer').remove(); if (!faded) { Azuki.Windowing.Fader.remove(); } }); 35 | 36 | new Ajax.Updater('HelpContent', url, { insertion: Insertion.Bottom, onComplete: function() { 37 | Azuki.Windowing.Busybox.done(); 38 | Azuki.Helpers.Window.center('HelpContainer'); 39 | if (!faded) Azuki.Windowing.Fader.fade(); 40 | new Effect.Appear($('HelpContainer'), {duration: 0.3}); 41 | }}); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /tests/unit/helpers/forms_helper_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

AzukiLib test file

15 |

16 | Tests for helpers/forms_helper.js 17 |

18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /lib/sound.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us sound.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007 2 | 3 | // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // Based on code created by Jules Gravinese (http://www.webveteran.com/) 6 | // 7 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 8 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 9 | 10 | Sound = { 11 | tracks: {}, 12 | _enabled: true, 13 | template: 14 | new Template(''), 15 | enable: function(){ 16 | Sound._enabled = true; 17 | }, 18 | disable: function(){ 19 | Sound._enabled = false; 20 | }, 21 | play: function(url){ 22 | if(!Sound._enabled) return; 23 | var options = Object.extend({ 24 | track: 'global', url: url, replace: false 25 | }, arguments[1] || {}); 26 | 27 | if(options.replace && this.tracks[options.track]) { 28 | $R(0, this.tracks[options.track].id).each(function(id){ 29 | var sound = $('sound_'+options.track+'_'+id); 30 | sound.Stop && sound.Stop(); 31 | sound.remove(); 32 | }) 33 | this.tracks[options.track] = null; 34 | } 35 | 36 | if(!this.tracks[options.track]) 37 | this.tracks[options.track] = { id: 0 } 38 | else 39 | this.tracks[options.track].id++; 40 | 41 | options.id = this.tracks[options.track].id; 42 | $$('body')[0].insert( 43 | Prototype.Browser.IE ? new Element('bgsound',{ 44 | id: 'sound_'+options.track+'_'+options.id, 45 | src: options.url, loop: 1, autostart: true 46 | }) : Sound.template.evaluate(options)); 47 | } 48 | }; 49 | 50 | if(Prototype.Browser.Gecko && navigator.userAgent.indexOf("Win") > 0){ 51 | if(navigator.plugins && $A(navigator.plugins).detect(function(p){ return p.name.indexOf('QuickTime') != -1 })) 52 | Sound.template = new Template('') 53 | else 54 | Sound.play = function(){} 55 | } 56 | -------------------------------------------------------------------------------- /src/windowing/fader.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | This class provides tools for fading out the page. Use Azuki.Windowing.Fader.fade() to fade out, and 3 | Azuki.Windowing.Fader.remove() to remote it. Azuki.Windowing.Fader.active() returns true when currently faded. 4 | 5 | This is usually used when a dialog box requires the current context. 6 | */ 7 | Azuki.Windowing.Fader = { 8 | active: function() { 9 | return $('Fader') ? true : false; 10 | }, 11 | 12 | set_height: function() { 13 | $('Fader').setStyle({height: Azuki.Helpers.Window.page_size().height + 'px'}); 14 | }, 15 | 16 | fade: function() { 17 | new Insertion.Top(document.body, ''); 18 | Position.prepare(); 19 | Azuki.Windowing.Fader.set_height(); 20 | $('Fader').setOpacity(0); 21 | $('Fader').show(0); 22 | Azuki.Windowing.Fader.alter_selects('hidden'); 23 | Azuki.Windowing.Fader.alter_overflows('hidden'); 24 | new Effect.Opacity('Fader', {duration: 0, from: 0.7, to: 0.7}); 25 | }, 26 | 27 | alter_selects: function(visible, selector) { 28 | if (!(/MSIE/.test(navigator.userAgent) && !window.opera)) return; 29 | if (!selector) selector = 'select'; 30 | 31 | $$(selector).each(function(element) { 32 | element.setStyle({ visibility: visible }); 33 | }); 34 | }, 35 | 36 | alter_overflows: function(overflow_setting) { 37 | $$('.overflow').each(function(element) { 38 | element.setStyle({ overflow: overflow_setting }); 39 | }); 40 | }, 41 | 42 | remove: function() 43 | { 44 | if (!$('Fader')) return; 45 | 46 | new Effect.Opacity('Fader', {duration: 0.2, from: 0.7, to: 0.0, afterFinish: function() { 47 | if ($('Fader')) $('Fader').remove(); 48 | Azuki.Windowing.Fader.alter_selects('visible'); 49 | Azuki.Windowing.Fader.alter_overflows('auto'); 50 | }}); 51 | } 52 | }; 53 | 54 | Event.observe(window, 'resize', function() { 55 | if (!$('Fader')) return; 56 | Azuki.Windowing.Fader.set_height(); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/unit/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 |

AzukiLib test suite

13 | 14 |

Controller test

15 | 16 | 17 |

Forms

18 | 19 |
    20 |
  1. Remote form test
  2. 21 |
  3. Select search
  4. 22 |
  5. Textarea extensions
  6. 23 |
24 | 25 |

Helpers

26 | 27 |
    28 |
  1. Forms helper test
  2. 29 |
  3. Keyboard helper test
  4. 30 |
  5. Window helper test
  6. 31 |
32 | 33 |

Style

34 | 35 |
    36 |
  1. Color test
  2. 37 |
  3. Table ruler test
  4. 38 |
39 | 40 |

Windowing

41 | 42 |
    43 |
  1. Busybox test
  2. 44 |
  3. Contextual help test
  4. 45 |
  5. Fader test
  6. 46 |
  7. Lightbox test
  8. 47 |
  9. Popinfo test
  10. 48 |
49 | 50 | 51 | -------------------------------------------------------------------------------- /src/helpers/forms_helper.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | Disables and activates form controls. 3 | Form controls can be set as disbled in your HTML, then enabled when the DOM is ready here 4 | This prevents people from accessing forms that require JavaScript that aren't ready yet. 5 | */ 6 | Azuki.Helpers.Forms = { 7 | /* Text helpers */ 8 | set_caret_position: function(element, pos) { 9 | if (element.setSelectionRange) { 10 | element.focus(); 11 | element.setSelectionRange(pos, pos); 12 | } else if (element.createTextRange) { 13 | var range = element.createTextRange(); 14 | 15 | range.collapse(true); 16 | range.moveEnd('character', pos); 17 | range.moveStart('character', pos); 18 | range.select(); 19 | } 20 | }, 21 | 22 | get_caret_position: function(element) { 23 | if (element.setSelectionRange) { 24 | return element.selectionStart; 25 | } else if (element.createTextRange) { 26 | // The current selection 27 | var range = document.selection.createRange(); 28 | // We'll use this as a 'dummy' 29 | var stored_range = range.duplicate(); 30 | // Select all text 31 | stored_range.moveToElementText(element); 32 | // Now move 'dummy' end point to end point of original range 33 | stored_range.setEndPoint('EndToEnd', range); 34 | 35 | return stored_range.text.length - range.text.length; 36 | } 37 | }, 38 | 39 | activate_controls: function(names) { 40 | $A(document.getElementsByTagName('input')).each(function(element) { 41 | $A(names).each(function(name) { 42 | if (element.name == name) { 43 | element.disabled = false; 44 | } 45 | }); 46 | }); 47 | }, 48 | 49 | disable_on_submit: function() { 50 | $$('form').each(function(form) { 51 | var submit = $A(form.getElementsByTagName('input')).find(function(input) { return input.type == 'submit'; } ); 52 | if (!submit) return; 53 | 54 | Event.observe(form, 'submit', function(e) { 55 | submit = $(submit); 56 | if (submit && !submit.hasClassName('nodisable')) submit.disable(); 57 | }); 58 | }); 59 | } 60 | }; 61 | -------------------------------------------------------------------------------- /tests/unit/forms/select_search_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

AzukiLib test file

15 |

16 | Tests for forms/select_search.js 17 |

18 | 19 | 20 |
21 | 22 |
23 |
24 | 25 | 35 |
36 |
37 | 38 | 39 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /tests/unit/helpers/window_helper_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

AzukiLib test file

16 |

17 | Tests for helpers/window_helper.js 18 |

19 | 20 | 21 |
22 | 23 |
24 |
25 | 26 | 27 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/controller.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | This class provides tools for treating sections within your site as "Controllers". 3 | 4 | 1. For each section, set a body ID. For example, Account for the account management pages. 5 | Then define a JavaScript class called AccountController. AccountController.run will 6 | automatically be called when people visit that page, OR a class will be instantiated. 7 | 8 | 2. Define a controller called ApplicationController. This gets run before the current 9 | controller, a good place to run anything you need on every page. 10 | */ 11 | Azuki.Controller = { 12 | run: function() { 13 | if (document.body.id) Azuki.Controller.run_controller(document.body.id); 14 | Azuki.Controller.run_controller('Application'); 15 | }, 16 | 17 | run_controller: function(id) { 18 | var controller_class = id + 'Controller'; 19 | if (!window[controller_class]) return; 20 | 21 | if (eval(controller_class + '.run')) { 22 | eval(controller_class + '.run()'); 23 | } else { 24 | controller = new window[controller_class]; 25 | } 26 | } 27 | }; 28 | 29 | function init() { 30 | // quit if this function has already been called 31 | if (arguments.callee.done) return; 32 | 33 | // flag this function so we don't do the same thing twice 34 | arguments.callee.done = true; 35 | 36 | // kill the timer 37 | if (_timer) clearInterval(_timer); 38 | 39 | // do stuff 40 | Azuki.Controller.run(); 41 | } 42 | 43 | /* for Mozilla/Opera9 */ 44 | if (document.addEventListener) { 45 | document.addEventListener("DOMContentLoaded", init, false); 46 | } 47 | 48 | /* for Internet Explorer */ 49 | /*@cc_on @*/ 50 | /*@if (@_win32) 51 | document.write(" 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

AzukiLib test file

17 |

18 | Tests for windowing/popinfo.js 19 |

20 | 21 | 22 |
23 | 24 | 28 | 29 | 73 | 74 | -------------------------------------------------------------------------------- /ruby_libs/installer.rb: -------------------------------------------------------------------------------- 1 | module AzukiLib 2 | class Installer 3 | class << self 4 | def install 5 | installer = Installer.new 6 | installer.run 7 | end 8 | end 9 | 10 | def initialize 11 | end 12 | 13 | def run 14 | [:install_css, :install_js, :install_images].each do |method| 15 | send(method) if respond_to? method 16 | end 17 | end 18 | 19 | def install_css 20 | return unless AzukiLib::Config['css_target'] 21 | 22 | css_target = File.join(AzukiLib::Config['project_path'], AzukiLib::Config['css_target']) 23 | css = File.read('css/azuki.css') 24 | css =<<-CSS 25 | /* START AzukiLib */ 26 | #{css} 27 | /* END AzukiLib */ 28 | CSS 29 | 30 | # Change the relative URLs in the scripts 31 | if AzukiLib::Config['image_relative'] 32 | compiler = Compiler.new 33 | compiler.source = css 34 | css = compiler.replace_image_urls 35 | end 36 | 37 | if AzukiLib::Config['insert_css'] 38 | original_css = File.read css_target 39 | in_library = false 40 | vanilla_css = '' 41 | 42 | original_css.split("\n").each do |line| 43 | in_library = true if line.match %r{/\* START AzukiLib \*/} 44 | vanilla_css << line + "\n" unless in_library 45 | in_library = false if line.match %r{/\* END AzukiLib \*/} 46 | end 47 | 48 | target_file = File.open(css_target, 'w+') 49 | target_file << vanilla_css + css 50 | target_file.close 51 | else 52 | File.copy 'css/azuki.css', File.join(css_target, 'azuki.css') 53 | end 54 | end 55 | 56 | def install_js 57 | return unless AzukiLib::Config['javascript_target'] 58 | File.copy 'azukilib.js', File.join(AzukiLib::Config['project_path'], AzukiLib::Config['javascript_target']) 59 | end 60 | 61 | def install_images 62 | return unless AzukiLib::Config['image_target'] 63 | 64 | Find.find('images/') do |path| 65 | # Ignore dot files 66 | if path == ?. 67 | Find.prune 68 | elsif path.match /\.(png|gif)$/ 69 | target_directory = File.join(AzukiLib::Config['project_path'], AzukiLib::Config['image_target']) 70 | Dir.mkdir target_directory unless File.directory?(target_directory) 71 | File.copy path, File.join(target_directory, File.basename(path)) 72 | end 73 | end 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /src/helpers/window_helper.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | These methods provide nifty cross browser window handling code. 3 | */ 4 | Azuki.Helpers.Window = { 5 | size: function() { 6 | var width, height; 7 | 8 | if (self.innerHeight) { 9 | width = self.innerWidth; 10 | height = self.innerHeight; 11 | } else if (document.documentElement && document.documentElement.clientHeight) { 12 | // IE 6 Strict Mode 13 | width = document.documentElement.clientWidth; 14 | height = document.documentElement.clientHeight; 15 | } else if (document.body) { 16 | // IE 17 | width = document.body.clientWidth; 18 | height = document.body.clientHeight; 19 | } 20 | 21 | return {width: width, height: height}; 22 | }, 23 | 24 | page_size: function() { 25 | var x_scroll, y_scroll; 26 | var width, height; 27 | var window_size = Azuki.Helpers.Window.size(); 28 | 29 | if (window.innerHeight && window.scrollMaxY) { 30 | x_scroll = document.body.scrollWidth; 31 | y_scroll = window.innerHeight + window.scrollMaxY; 32 | } else if (document.body.scrollHeight > document.body.offsetHeight) { 33 | x_scroll = document.body.scrollWidth; 34 | y_scroll = document.body.scrollHeight; 35 | } else { 36 | x_scroll = document.body.offsetWidth; 37 | y_scroll = document.body.offsetHeight; 38 | } 39 | 40 | width = x_scroll < window_size.width ? window_size.width : x_scroll; 41 | height = y_scroll < window_size.height ? window_size.height : y_scroll; 42 | 43 | return {width: width, height: height}; 44 | }, 45 | 46 | /* Centres absolute positions elements */ 47 | center: function(element) { 48 | var options = Object.extend({update: false}, arguments[1] || {}); 49 | element = $(element); 50 | 51 | Position.prepare(); 52 | 53 | var offset_x = (Position.deltaX + Math.floor((Azuki.Helpers.Window.size().width - element.getDimensions().width) / 2)) || '0'; 54 | var offset_y = (Position.deltaY + Math.floor((Azuki.Helpers.Window.size().height - element.getDimensions().height) / 2)) || '0'; 55 | 56 | element.setStyle({ left: offset_x + 'px' }); 57 | element.setStyle({ top: offset_y + 'px' }); 58 | 59 | if (options.update) { 60 | Event.observe(window, 'resize', function() { Position.center(element); }); 61 | Event.observe(window, 'scroll', function() { Position.center(element); }); 62 | } 63 | }, 64 | 65 | center_with_margin: function(element) { 66 | element = $(element); 67 | var container = element.up(); 68 | var margin = (container.getWidth() - element.getWidth()) / 2; 69 | element.setStyle({ marginLeft: margin + 'px', marginRight: margin + 'px'}); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /lib/scriptaculous.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us scriptaculous.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007 2 | 3 | // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining 6 | // a copy of this software and associated documentation files (the 7 | // "Software"), to deal in the Software without restriction, including 8 | // without limitation the rights to use, copy, modify, merge, publish, 9 | // distribute, sublicense, and/or sell copies of the Software, and to 10 | // permit persons to whom the Software is furnished to do so, subject to 11 | // the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be 14 | // included in all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | // 24 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 25 | 26 | var Scriptaculous = { 27 | Version: '1.8.0', 28 | require: function(libraryName) { 29 | // inserting via DOM fails in Safari 2.0, so brute force approach 30 | document.write(' 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

AzukiLib test file

17 |

18 | Tests for storage/cookie.js 19 |

20 | 21 | 22 |
23 | 24 | 85 | 86 | -------------------------------------------------------------------------------- /src/windowing/lightbox.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | This class will load an image inside a box that takes the current visual context. 3 | */ 4 | Azuki.Windowing.Lightbox = Class.create(); 5 | Azuki.Windowing.Lightbox.prototype = { 6 | initialize: function(class_name) { 7 | this.class_name = typeof class_name == 'undefined' ? 'lightbox' : class_name; 8 | 9 | this.container_id = 'LightboxContainer'; 10 | this.close_id = 'LightboxClose'; 11 | this.remote_type = 'image'; 12 | 13 | this.remove_event = this.remove.bindAsEventListener(this); 14 | Event.observe(document, 'click', this.events.bind(this)); 15 | }, 16 | 17 | events: function(e) { 18 | var element = Event.element(e); 19 | this.image = null; 20 | this.link = null; 21 | 22 | if (element.nodeName == 'A' && element.down('img') && element.down('img').hasClassName(this.class_name)) { 23 | this.image = element.down('img'); 24 | this.link = element; 25 | } 26 | 27 | if (element.nodeName == 'IMG' && element.hasClassName(this.class_name)) { 28 | this.link = element.up('a'); 29 | this.image = element; 30 | } 31 | 32 | if (this.link && this.image) { 33 | this.display(e); 34 | } 35 | }, 36 | 37 | remove: function(e) { 38 | Event.stopObserving($(this.close_id), 'click', this.remove_event); 39 | $(this.container_id).remove(); 40 | Azuki.Windowing.Fader.remove(); 41 | Event.stop(e); 42 | return false; 43 | }, 44 | 45 | display: function(e) { 46 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 47 | 48 | if (Azuki.Windowing.Busybox.active()) { 49 | Event.stop(e); 50 | return false; 51 | } 52 | 53 | if ($(this.container_id)) $(this.container_id).remove(); 54 | Azuki.Windowing.Busybox.busy('loading'); 55 | new Insertion.Top(document.body, ''); 56 | 57 | Event.observe($(this.close_id), 'click', this.remove_event); 58 | 59 | switch(this.remote_type) { 60 | case 'image': 61 | var image = document.createElement('img'); 62 | 63 | image.onload = function() { 64 | $(this.container_id).appendChild(image); 65 | 66 | Azuki.Windowing.Busybox.done(); 67 | Azuki.Helpers.Window.center(this.container_id); 68 | Azuki.Windowing.Fader.fade(); 69 | new Effect.Appear(this.container_id, {duration: 0.3}); 70 | 71 | return false; 72 | }.bind(this); 73 | 74 | image.src = this.link.href; 75 | break; 76 | 77 | case 'html': 78 | new Ajax.Updater(this.container_id, this.link.href, { method: 'get', evalScripts: true, onComplete: function() { 79 | Azuki.Windowing.Busybox.done(); 80 | Azuki.Windowing.Fader.fade(); 81 | Azuki.Helpers.Window.center($(this.container_id)); 82 | 83 | $(this.container_id).show(); 84 | }.bind(this)}); 85 | break; 86 | 87 | default: 88 | break; 89 | } 90 | 91 | if (e) Event.stop(e); 92 | return false; 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | !http://dl.getdropbox.com/u/221414/blogs/logo.gif! 2 | 3 | This library provides useful JavaScript classes for dealing with: 4 | 5 | * Popup windows, lightboxes, "busy" feedback messages 6 | * Contextual help 7 | * Keyboard shortcuts 8 | * Form enhancements for searching selects, adding resizers to textareas 9 | * "Controller" abstraction, to help you encapsulate your JavaScript behaviour for sections on your site 10 | * Loading and management of "remote forms" - create/edit/delete forms hosted on your server that you want to display as a popup with Ajax 11 | 12 | It also provides pre-processor scripts for integrating the library with your project. 13 | 14 | The aims of this library are: 15 | 16 | * To unite the reusable JavaScript in my applications 17 | * To improve the encapsulation in JavaScript code 18 | * To make the relationship between the application and JavaScript more obvious 19 | * To provide a rich set of tools for enhancing web applications 20 | 21 | AzukiLib is comprised of: 22 | 23 | * JavaScript classes 24 | * A set of images for icons and feedback messages 25 | * Example CSS 26 | * Unit tests 27 | * Rake tasks for producing a single library file (with boilerplate comments removed), and installing the css, images and javascripts into your project 28 | 29 | This library depends on "Prototype":http://www.prototypejs.org/. 30 | 31 | h2. Controllers 32 | 33 | I split the code in my projects up into controllers. Controllers generally map to a Rails controller. They're automatically loaded and run by matching the document body ID to your controller: 34 | 35 | * HTML body ID set to Tasks 36 | * Azuki attempts to load TasksController and call its run method. If it's a class, it will instantiate the class instead. 37 | 38 | My Rails projects generally set the body ID to the name of the controller to make it easier to specialise generalised CSS. It's also helped me keep my JavaScript reusable across projects. 39 | 40 | h2. Usage 41 | 42 | You can use the JavaScript files in src/ in part or the entire thing as azukilib.js. Build azukilib.js like this: 43 | 44 | rake azukilib:compile 45 | 46 | The config.yml file is expected to contain various settings, including: 47 | 48 | * The location of your project which you're going to use Azuki with. 49 | * Locations for your project's CSS, images and JavaScript files. 50 | * insert_css: Add Azuki's CSS to your project's CSS file (rather than loading multiple files to cut down the number of remote requests). CSS already added by Azuki will be replaced. 51 | * image_relative: Azuki's pre-processor will replace image paths in azuki.css and azukilib.js so you can keep Azuki's example images separate to your project's images. 52 | 53 | h2. Background 54 | 55 | AzukiLib is a set of libraries used by Helicoid Limited's web applications: 56 | 57 | * "Tiktrac":http://tiktrac.com - Timesheets for businesses: 58 | * "Ebiwrite":http://ebiwrite.com - Tools for translators 59 | * "Helipad":http://pad.helicoid.net - A simple web notepad 60 | * And probably a lot more 61 | 62 | h3. History 63 | 64 | * 25 April 2009: Added to GitHub 65 | * 11 December 2007: Basic unit tests added, started work on cleaning up the API, imported into repository 66 | * 10 December 2007: Extraction from Tiktrac/Ebiwrite started 67 | * Early 2007: Library created by extracting code from Helicoid's products 68 | 69 | h2. License 70 | 71 | This project is placed in the public domain. 72 | -------------------------------------------------------------------------------- /tests/unit/style/color_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | AzukiLib test file 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

AzukiLib test file

17 |

18 | Tests for style/color.js 19 |

20 | 21 | 22 |
23 | 24 |
 
25 |
 
26 |
 
27 |
 
28 |
 
29 |
 
30 |
 
31 |
 
32 |
 
33 |
 
34 | 35 | 36 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/style/color.js: -------------------------------------------------------------------------------- 1 | Azuki.Style.Color = {}; 2 | Azuki.Style.Color.RGB = Class.create(); 3 | Azuki.Style.Color.RGB.prototype = { 4 | initialize: function(value) { 5 | this.colors = Array(0, 0, 0); 6 | 7 | switch (typeof(value)) { 8 | case 'string': 9 | this.set_from_rgb_string(value); 10 | break; 11 | 12 | case 'object': 13 | this.colors = value; 14 | break; 15 | 16 | default: 17 | break; 18 | } 19 | }, 20 | 21 | red: function() { return this.colors[0]; }, 22 | green: function() { return this.colors[1]; }, 23 | blue: function() { return this.colors[2]; }, 24 | 25 | set_red: function(red) { this.colors[0] = red; }, 26 | set_green: function(green) { this.colors[1] = green; }, 27 | set_blue: function(blue) { this.colors[2] = blue; }, 28 | 29 | /* Assumes rgb(1, 2, 3) */ 30 | set_from_rgb_string: function(value) { 31 | this.colors = $A(value.replace(/rgb\(/, '').replace(/\)/, '').split(',')).collect(function(value) { 32 | return parseInt(value, 10); 33 | }); 34 | }, 35 | 36 | to_s: function() { 37 | return 'rgb(' + this.red() + ',' + this.green() + ',' + this.blue() + ')'; 38 | }, 39 | 40 | to_hex: function() { 41 | var hex = ''; 42 | 43 | $A(this.colors).each(function(colour) { 44 | var value = this._hex_value(colour); 45 | if (value.length == 1) { value = '0' + value; } 46 | hex = hex + value; 47 | }.bind(this)); 48 | 49 | return hex; 50 | }, 51 | 52 | _hex_value: function(d) { 53 | var hex_map = '0123456789ABCDEF'; 54 | var h = hex_map.substr(d&15, 1); 55 | 56 | while (d > 15) { d >>= 4; h = hex_map.substr(d&15, 1) + h; } 57 | return h; 58 | } 59 | }; 60 | 61 | Azuki.Style.Color.Hex = Class.create(); 62 | Azuki.Style.Color.Hex.prototype = { 63 | /* Create with values like this: '#000000' */ 64 | initialize: function(value) { 65 | this.value = value; 66 | }, 67 | 68 | to_rgb: function() { 69 | this._set_rgb_values(); 70 | var rgb_array = $A([this.red, this.green, this.blue]).collect(function(color) { 71 | return this._rgb_value(color); 72 | }.bind(this)); 73 | 74 | return new Azuki.Style.Color.RGB(rgb_array); 75 | }, 76 | 77 | _rgb_value: function(hex) { 78 | return parseInt(hex, 16) || 0; 79 | }, 80 | 81 | _set_rgb_values: function() { 82 | this.red = this.value.charAt(1) + this.value.charAt(2); 83 | this.green = this.value.charAt(3) + this.value.charAt(4); 84 | this.blue = this.value.charAt(5) + this.value.charAt(6); 85 | } 86 | }; 87 | 88 | Azuki.Style.Color.Methods = { 89 | invert: function(value) { 90 | var color = new Azuki.Style.Color.RGB(value); 91 | color.set_red(255 - parseInt(color.red(), 10)); 92 | color.set_green(255 - parseInt(color.green(), 10)); 93 | color.set_blue(255 - parseInt(color.blue(), 10)); 94 | return color.to_s(); 95 | }, 96 | 97 | random: function() { 98 | var color = new Azuki.Style.Color.RGB(Array(Math.round((Math.random() * 255)), Math.round((Math.random() * 255)), Math.round((Math.random() * 255)))); 99 | return color.to_s(); 100 | } 101 | }; 102 | 103 | Azuki.Style.Color.ElementMethods = { 104 | invertColor: function(element, property) { 105 | try { 106 | element.style[property] = Azuki.Style.Color.invert(element.getStyle(property)); 107 | return true; 108 | } 109 | catch (exception) { 110 | return false; 111 | } 112 | }, 113 | 114 | randomColor: function(element, property) 115 | { 116 | try { 117 | element.style[property] = Azuki.Style.Color.random(); 118 | return true; 119 | } 120 | catch (exception) { 121 | return false; 122 | } 123 | } 124 | }; 125 | 126 | Object.extend(Azuki.Style.Color, Azuki.Style.Color.Methods); 127 | Element.addMethods(Azuki.Style.Color.ElementMethods); 128 | -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us builder.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007 2 | 3 | // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | var Builder = { 9 | NODEMAP: { 10 | AREA: 'map', 11 | CAPTION: 'table', 12 | COL: 'table', 13 | COLGROUP: 'table', 14 | LEGEND: 'fieldset', 15 | OPTGROUP: 'select', 16 | OPTION: 'select', 17 | PARAM: 'object', 18 | TBODY: 'table', 19 | TD: 'table', 20 | TFOOT: 'table', 21 | TH: 'table', 22 | THEAD: 'table', 23 | TR: 'table' 24 | }, 25 | // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken, 26 | // due to a Firefox bug 27 | node: function(elementName) { 28 | elementName = elementName.toUpperCase(); 29 | 30 | // try innerHTML approach 31 | var parentTag = this.NODEMAP[elementName] || 'div'; 32 | var parentElement = document.createElement(parentTag); 33 | try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 34 | parentElement.innerHTML = "<" + elementName + ">"; 35 | } catch(e) {} 36 | var element = parentElement.firstChild || null; 37 | 38 | // see if browser added wrapping tags 39 | if(element && (element.tagName.toUpperCase() != elementName)) 40 | element = element.getElementsByTagName(elementName)[0]; 41 | 42 | // fallback to createElement approach 43 | if(!element) element = document.createElement(elementName); 44 | 45 | // abort if nothing could be created 46 | if(!element) return; 47 | 48 | // attributes (or text) 49 | if(arguments[1]) 50 | if(this._isStringOrNumber(arguments[1]) || 51 | (arguments[1] instanceof Array) || 52 | arguments[1].tagName) { 53 | this._children(element, arguments[1]); 54 | } else { 55 | var attrs = this._attributes(arguments[1]); 56 | if(attrs.length) { 57 | try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 58 | parentElement.innerHTML = "<" +elementName + " " + 59 | attrs + ">"; 60 | } catch(e) {} 61 | element = parentElement.firstChild || null; 62 | // workaround firefox 1.0.X bug 63 | if(!element) { 64 | element = document.createElement(elementName); 65 | for(attr in arguments[1]) 66 | element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; 67 | } 68 | if(element.tagName.toUpperCase() != elementName) 69 | element = parentElement.getElementsByTagName(elementName)[0]; 70 | } 71 | } 72 | 73 | // text, or array of children 74 | if(arguments[2]) 75 | this._children(element, arguments[2]); 76 | 77 | return element; 78 | }, 79 | _text: function(text) { 80 | return document.createTextNode(text); 81 | }, 82 | 83 | ATTR_MAP: { 84 | 'className': 'class', 85 | 'htmlFor': 'for' 86 | }, 87 | 88 | _attributes: function(attributes) { 89 | var attrs = []; 90 | for(attribute in attributes) 91 | attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) + 92 | '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'"') + '"'); 93 | return attrs.join(" "); 94 | }, 95 | _children: function(element, children) { 96 | if(children.tagName) { 97 | element.appendChild(children); 98 | return; 99 | } 100 | if(typeof children=='object') { // array can hold nodes and text 101 | children.flatten().each( function(e) { 102 | if(typeof e=='object') 103 | element.appendChild(e) 104 | else 105 | if(Builder._isStringOrNumber(e)) 106 | element.appendChild(Builder._text(e)); 107 | }); 108 | } else 109 | if(Builder._isStringOrNumber(children)) 110 | element.appendChild(Builder._text(children)); 111 | }, 112 | _isStringOrNumber: function(param) { 113 | return(typeof param=='string' || typeof param=='number'); 114 | }, 115 | build: function(html) { 116 | var element = this.node('div'); 117 | $(element).update(html.strip()); 118 | return element.down(); 119 | }, 120 | dump: function(scope) { 121 | if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 122 | 123 | var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " + 124 | "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " + 125 | "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+ 126 | "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+ 127 | "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+ 128 | "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/); 129 | 130 | tags.each( function(tag){ 131 | scope[tag] = function() { 132 | return Builder.node.apply(Builder, [tag].concat($A(arguments))); 133 | } 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/forms/remote.js: -------------------------------------------------------------------------------- 1 | /* :boilerplate: 2 | Instantiate this class with your required options, then bind the add/edit/destroy events as needed. 3 | */ 4 | 5 | Azuki.Forms.Remote = Class.create(); 6 | Azuki.Forms.Remote.prototype = { 7 | initialize: function(options) { 8 | this.options = { 9 | form_id: 'RemoteForm', 10 | controller_name: '', 11 | item_name: '', 12 | method: 'post', 13 | enctype: null, 14 | ajax: false, 15 | add_close_button: true, 16 | form_visible: false, 17 | form_display_callback: false, 18 | add_completed_callback: null 19 | }; 20 | 21 | Object.extend(this.options, options || { }); 22 | 23 | this.options.edit_url = '/' + this.options.controller_name + '/edit/'; 24 | this.options.new_url = '/' + this.options.controller_name + '/new'; 25 | this.options.create_url = '/' + this.options.controller_name + '/create'; 26 | this.options.update_url = '/' + this.options.controller_name + '/update/'; 27 | this.options.destroy_url = '/' + this.options.controller_name + '/destroy/'; 28 | }, 29 | 30 | edit: function(e) { 31 | var element = Event.element(e); 32 | 33 | if (element.nodeName == 'IMG') { 34 | element = element.up('.edit_' + this.options.item_name); 35 | } 36 | 37 | if (!element) return true; 38 | 39 | if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('edit_' + this.options.item_name)) { 40 | var item_id = element.id.match(/\d+$/); 41 | 42 | this.show_form(this.options.form_id, this.options.edit_url + item_id, this.options.update_url + item_id, this.options.form_display_callback); 43 | Event.stop(e); 44 | return false; 45 | } 46 | 47 | return true; 48 | }, 49 | 50 | add: function(e) 51 | { 52 | var element = Event.element(e); 53 | 54 | if (element.nodeName == 'IMG') { 55 | element = element.up('.add_' + this.options.item_name); 56 | } 57 | 58 | if (!element) return true; 59 | 60 | if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('add_' + this.options.item_name)) { 61 | if (this.options.add_callback) this.options.add_callback(); 62 | this.show_form(this.options.form_id, this.options.new_url, this.options.create_url, this.options.form_display_callback); 63 | Event.stop(e); 64 | return false; 65 | } 66 | 67 | return true; 68 | }, 69 | 70 | destroy: function(e) 71 | { 72 | var element = Event.element(e); 73 | if ((element.nodeName == 'A' || element.nodeName == 'IMG') && element.hasClassName('destroy_' + this.options.item_name)) { 74 | var item_id = element.id.match(/\d+$/); 75 | 76 | if (confirm('Are you sure you want to delete that item?')) { 77 | window.location = this.options.destroy_url + item_id; 78 | } 79 | 80 | Event.stop(e); 81 | return false; 82 | } 83 | }, 84 | 85 | show_form: function(id, url, save_url, callback) { 86 | if (this.options.form_visible) { 87 | return; 88 | } else { 89 | this.options.form_visible = true; 90 | } 91 | 92 | Azuki.Windowing.Busybox.busy('loading'); 93 | 94 | function close_form() { 95 | if ($(id)) { 96 | $(id).remove(); 97 | Azuki.Windowing.Fader.remove(); 98 | } 99 | } 100 | 101 | function focus_field() { 102 | try { 103 | if ($(id)) $(id).focusFirstElement(); 104 | } catch (exception) { 105 | } 106 | } 107 | 108 | var fields_id = id + 'FormFields'; 109 | var cancel_id = 'Cancel' + id; 110 | var editor_html = ''; 111 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 112 | var enctype = typeof this.options.enctype == 'undefined' ? '' : 'enctype="multipart/form-data"'; 113 | 114 | close_form(); 115 | 116 | editor_html += ''; 120 | 121 | // Insert the editor HTML 122 | new Insertion.Top(document.body, editor_html); 123 | 124 | // Get the form fields from the server 125 | new Ajax.Updater($(fields_id), url, { 126 | evalScripts: true, 127 | method: 'get', 128 | onComplete: function() { 129 | Azuki.Helpers.Window.center(id); 130 | new Effect.Appear($(id), { duration: 0.3, afterFinish: function() { Azuki.Windowing.Fader.alter_selects('visible'); focus_field(); } }); 131 | Azuki.Windowing.Fader.fade(); 132 | Azuki.Windowing.Busybox.done(); 133 | 134 | if (this.options.ajax) { 135 | try { 136 | Event.observe(id, 'submit', this.ajax_submit.bindAsEventListener(this)); 137 | } catch(e) { 138 | console.log(e); 139 | } 140 | } 141 | 142 | Event.observe(cancel_id, 'click', function(e) { close_form(id); this.options.form_visible = false; Event.stop(e); return false; }.bind(this)); 143 | 144 | if (callback) callback(); 145 | }.bind(this) 146 | }); 147 | }, 148 | 149 | ajax_form_html: function() { 150 | if (!this.options.ajax) return ''; 151 | return ' onsubmit="return false" '; 152 | }, 153 | 154 | ajax_submit: function() { 155 | this.options.form_visible = false; 156 | new Ajax.Request(this.options.create_url, { onSuccess: this.options.ajax_on_succes, postBody: Form.serialize(this.options.form_id), onFailure: this.options.ajax_on_failure }); 157 | $(this.options.form_id).remove(); 158 | Azuki.Windowing.Fader.remove(); 159 | return false; 160 | } 161 | }; 162 | -------------------------------------------------------------------------------- /lib/slider.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us slider.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007 2 | 3 | // Copyright (c) 2005-2007 Marty Haught, Thomas Fuchs 4 | // 5 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 6 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 7 | 8 | if (!Control) var Control = { }; 9 | 10 | // options: 11 | // axis: 'vertical', or 'horizontal' (default) 12 | // 13 | // callbacks: 14 | // onChange(value) 15 | // onSlide(value) 16 | Control.Slider = Class.create({ 17 | initialize: function(handle, track, options) { 18 | var slider = this; 19 | 20 | if (Object.isArray(handle)) { 21 | this.handles = handle.collect( function(e) { return $(e) }); 22 | } else { 23 | this.handles = [$(handle)]; 24 | } 25 | 26 | this.track = $(track); 27 | this.options = options || { }; 28 | 29 | this.axis = this.options.axis || 'horizontal'; 30 | this.increment = this.options.increment || 1; 31 | this.step = parseInt(this.options.step || '1'); 32 | this.range = this.options.range || $R(0,1); 33 | 34 | this.value = 0; // assure backwards compat 35 | this.values = this.handles.map( function() { return 0 }); 36 | this.spans = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false; 37 | this.options.startSpan = $(this.options.startSpan || null); 38 | this.options.endSpan = $(this.options.endSpan || null); 39 | 40 | this.restricted = this.options.restricted || false; 41 | 42 | this.maximum = this.options.maximum || this.range.end; 43 | this.minimum = this.options.minimum || this.range.start; 44 | 45 | // Will be used to align the handle onto the track, if necessary 46 | this.alignX = parseInt(this.options.alignX || '0'); 47 | this.alignY = parseInt(this.options.alignY || '0'); 48 | 49 | this.trackLength = this.maximumOffset() - this.minimumOffset(); 50 | 51 | this.handleLength = this.isVertical() ? 52 | (this.handles[0].offsetHeight != 0 ? 53 | this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) : 54 | (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth : 55 | this.handles[0].style.width.replace(/px$/,"")); 56 | 57 | this.active = false; 58 | this.dragging = false; 59 | this.disabled = false; 60 | 61 | if (this.options.disabled) this.setDisabled(); 62 | 63 | // Allowed values array 64 | this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false; 65 | if (this.allowedValues) { 66 | this.minimum = this.allowedValues.min(); 67 | this.maximum = this.allowedValues.max(); 68 | } 69 | 70 | this.eventMouseDown = this.startDrag.bindAsEventListener(this); 71 | this.eventMouseUp = this.endDrag.bindAsEventListener(this); 72 | this.eventMouseMove = this.update.bindAsEventListener(this); 73 | 74 | // Initialize handles in reverse (make sure first handle is active) 75 | this.handles.each( function(h,i) { 76 | i = slider.handles.length-1-i; 77 | slider.setValue(parseFloat( 78 | (Object.isArray(slider.options.sliderValue) ? 79 | slider.options.sliderValue[i] : slider.options.sliderValue) || 80 | slider.range.start), i); 81 | h.makePositioned().observe("mousedown", slider.eventMouseDown); 82 | }); 83 | 84 | this.track.observe("mousedown", this.eventMouseDown); 85 | document.observe("mouseup", this.eventMouseUp); 86 | document.observe("mousemove", this.eventMouseMove); 87 | 88 | this.initialized = true; 89 | }, 90 | dispose: function() { 91 | var slider = this; 92 | Event.stopObserving(this.track, "mousedown", this.eventMouseDown); 93 | Event.stopObserving(document, "mouseup", this.eventMouseUp); 94 | Event.stopObserving(document, "mousemove", this.eventMouseMove); 95 | this.handles.each( function(h) { 96 | Event.stopObserving(h, "mousedown", slider.eventMouseDown); 97 | }); 98 | }, 99 | setDisabled: function(){ 100 | this.disabled = true; 101 | }, 102 | setEnabled: function(){ 103 | this.disabled = false; 104 | }, 105 | getNearestValue: function(value){ 106 | if (this.allowedValues){ 107 | if (value >= this.allowedValues.max()) return(this.allowedValues.max()); 108 | if (value <= this.allowedValues.min()) return(this.allowedValues.min()); 109 | 110 | var offset = Math.abs(this.allowedValues[0] - value); 111 | var newValue = this.allowedValues[0]; 112 | this.allowedValues.each( function(v) { 113 | var currentOffset = Math.abs(v - value); 114 | if (currentOffset <= offset){ 115 | newValue = v; 116 | offset = currentOffset; 117 | } 118 | }); 119 | return newValue; 120 | } 121 | if (value > this.range.end) return this.range.end; 122 | if (value < this.range.start) return this.range.start; 123 | return value; 124 | }, 125 | setValue: function(sliderValue, handleIdx){ 126 | if (!this.active) { 127 | this.activeHandleIdx = handleIdx || 0; 128 | this.activeHandle = this.handles[this.activeHandleIdx]; 129 | this.updateStyles(); 130 | } 131 | handleIdx = handleIdx || this.activeHandleIdx || 0; 132 | if (this.initialized && this.restricted) { 133 | if ((handleIdx>0) && (sliderValuethis.values[handleIdx+1])) 136 | sliderValue = this.values[handleIdx+1]; 137 | } 138 | sliderValue = this.getNearestValue(sliderValue); 139 | this.values[handleIdx] = sliderValue; 140 | this.value = this.values[0]; // assure backwards compat 141 | 142 | this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] = 143 | this.translateToPx(sliderValue); 144 | 145 | this.drawSpans(); 146 | if (!this.dragging || !this.event) this.updateFinished(); 147 | }, 148 | setValueBy: function(delta, handleIdx) { 149 | this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta, 150 | handleIdx || this.activeHandleIdx || 0); 151 | }, 152 | translateToPx: function(value) { 153 | return Math.round( 154 | ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) * 155 | (value - this.range.start)) + "px"; 156 | }, 157 | translateToValue: function(offset) { 158 | return ((offset/(this.trackLength-this.handleLength) * 159 | (this.range.end-this.range.start)) + this.range.start); 160 | }, 161 | getRange: function(range) { 162 | var v = this.values.sortBy(Prototype.K); 163 | range = range || 0; 164 | return $R(v[range],v[range+1]); 165 | }, 166 | minimumOffset: function(){ 167 | return(this.isVertical() ? this.alignY : this.alignX); 168 | }, 169 | maximumOffset: function(){ 170 | return(this.isVertical() ? 171 | (this.track.offsetHeight != 0 ? this.track.offsetHeight : 172 | this.track.style.height.replace(/px$/,"")) - this.alignY : 173 | (this.track.offsetWidth != 0 ? this.track.offsetWidth : 174 | this.track.style.width.replace(/px$/,"")) - this.alignX); 175 | }, 176 | isVertical: function(){ 177 | return (this.axis == 'vertical'); 178 | }, 179 | drawSpans: function() { 180 | var slider = this; 181 | if (this.spans) 182 | $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) }); 183 | if (this.options.startSpan) 184 | this.setSpan(this.options.startSpan, 185 | $R(0, this.values.length>1 ? this.getRange(0).min() : this.value )); 186 | if (this.options.endSpan) 187 | this.setSpan(this.options.endSpan, 188 | $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum)); 189 | }, 190 | setSpan: function(span, range) { 191 | if (this.isVertical()) { 192 | span.style.top = this.translateToPx(range.start); 193 | span.style.height = this.translateToPx(range.end - range.start + this.range.start); 194 | } else { 195 | span.style.left = this.translateToPx(range.start); 196 | span.style.width = this.translateToPx(range.end - range.start + this.range.start); 197 | } 198 | }, 199 | updateStyles: function() { 200 | this.handles.each( function(h){ Element.removeClassName(h, 'selected') }); 201 | Element.addClassName(this.activeHandle, 'selected'); 202 | }, 203 | startDrag: function(event) { 204 | if (Event.isLeftClick(event)) { 205 | if (!this.disabled){ 206 | this.active = true; 207 | 208 | var handle = Event.element(event); 209 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 210 | var track = handle; 211 | if (track==this.track) { 212 | var offsets = Position.cumulativeOffset(this.track); 213 | this.event = event; 214 | this.setValue(this.translateToValue( 215 | (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2) 216 | )); 217 | var offsets = Position.cumulativeOffset(this.activeHandle); 218 | this.offsetX = (pointer[0] - offsets[0]); 219 | this.offsetY = (pointer[1] - offsets[1]); 220 | } else { 221 | // find the handle (prevents issues with Safari) 222 | while((this.handles.indexOf(handle) == -1) && handle.parentNode) 223 | handle = handle.parentNode; 224 | 225 | if (this.handles.indexOf(handle)!=-1) { 226 | this.activeHandle = handle; 227 | this.activeHandleIdx = this.handles.indexOf(this.activeHandle); 228 | this.updateStyles(); 229 | 230 | var offsets = Position.cumulativeOffset(this.activeHandle); 231 | this.offsetX = (pointer[0] - offsets[0]); 232 | this.offsetY = (pointer[1] - offsets[1]); 233 | } 234 | } 235 | } 236 | Event.stop(event); 237 | } 238 | }, 239 | update: function(event) { 240 | if (this.active) { 241 | if (!this.dragging) this.dragging = true; 242 | this.draw(event); 243 | if (Prototype.Browser.WebKit) window.scrollBy(0,0); 244 | Event.stop(event); 245 | } 246 | }, 247 | draw: function(event) { 248 | var pointer = [Event.pointerX(event), Event.pointerY(event)]; 249 | var offsets = Position.cumulativeOffset(this.track); 250 | pointer[0] -= this.offsetX + offsets[0]; 251 | pointer[1] -= this.offsetY + offsets[1]; 252 | this.event = event; 253 | this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] )); 254 | if (this.initialized && this.options.onSlide) 255 | this.options.onSlide(this.values.length>1 ? this.values : this.value, this); 256 | }, 257 | endDrag: function(event) { 258 | if (this.active && this.dragging) { 259 | this.finishDrag(event, true); 260 | Event.stop(event); 261 | } 262 | this.active = false; 263 | this.dragging = false; 264 | }, 265 | finishDrag: function(event, success) { 266 | this.active = false; 267 | this.dragging = false; 268 | this.updateFinished(); 269 | }, 270 | updateFinished: function() { 271 | if (this.initialized && this.options.onChange) 272 | this.options.onChange(this.values.length>1 ? this.values : this.value, this); 273 | this.event = null; 274 | } 275 | }); 276 | -------------------------------------------------------------------------------- /lib/unittest.js: -------------------------------------------------------------------------------- 1 | // script.aculo.us unittest.js v1.8.0, Tue Nov 06 15:01:40 +0300 2007 2 | 3 | // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 4 | // (c) 2005-2007 Jon Tirsen (http://www.tirsen.com) 5 | // (c) 2005-2007 Michael Schuerig (http://www.schuerig.de/michael/) 6 | // 7 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 8 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 9 | 10 | // experimental, Firefox-only 11 | Event.simulateMouse = function(element, eventName) { 12 | var options = Object.extend({ 13 | pointerX: 0, 14 | pointerY: 0, 15 | buttons: 0, 16 | ctrlKey: false, 17 | altKey: false, 18 | shiftKey: false, 19 | metaKey: false 20 | }, arguments[2] || {}); 21 | var oEvent = document.createEvent("MouseEvents"); 22 | oEvent.initMouseEvent(eventName, true, true, document.defaultView, 23 | options.buttons, options.pointerX, options.pointerY, options.pointerX, options.pointerY, 24 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 0, $(element)); 25 | 26 | if(this.mark) Element.remove(this.mark); 27 | this.mark = document.createElement('div'); 28 | this.mark.appendChild(document.createTextNode(" ")); 29 | document.body.appendChild(this.mark); 30 | this.mark.style.position = 'absolute'; 31 | this.mark.style.top = options.pointerY + "px"; 32 | this.mark.style.left = options.pointerX + "px"; 33 | this.mark.style.width = "5px"; 34 | this.mark.style.height = "5px;"; 35 | this.mark.style.borderTop = "1px solid red;" 36 | this.mark.style.borderLeft = "1px solid red;" 37 | 38 | if(this.step) 39 | alert('['+new Date().getTime().toString()+'] '+eventName+'/'+Test.Unit.inspect(options)); 40 | 41 | $(element).dispatchEvent(oEvent); 42 | }; 43 | 44 | // Note: Due to a fix in Firefox 1.0.5/6 that probably fixed "too much", this doesn't work in 1.0.6 or DP2. 45 | // You need to downgrade to 1.0.4 for now to get this working 46 | // See https://bugzilla.mozilla.org/show_bug.cgi?id=289940 for the fix that fixed too much 47 | Event.simulateKey = function(element, eventName) { 48 | var options = Object.extend({ 49 | ctrlKey: false, 50 | altKey: false, 51 | shiftKey: false, 52 | metaKey: false, 53 | keyCode: 0, 54 | charCode: 0 55 | }, arguments[2] || {}); 56 | 57 | var oEvent = document.createEvent("KeyEvents"); 58 | oEvent.initKeyEvent(eventName, true, true, window, 59 | options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, 60 | options.keyCode, options.charCode ); 61 | $(element).dispatchEvent(oEvent); 62 | }; 63 | 64 | Event.simulateKeys = function(element, command) { 65 | for(var i=0; i' + 116 | '' + 117 | '' + 118 | '' + 119 | '
StatusTestMessage
'; 120 | this.logsummary = $('logsummary') 121 | this.loglines = $('loglines'); 122 | }, 123 | _toHTML: function(txt) { 124 | return txt.escapeHTML().replace(/\n/g,"
"); 125 | }, 126 | addLinksToResults: function(){ 127 | $$("tr.failed .nameCell").each( function(td){ // todo: limit to children of this.log 128 | td.title = "Run only this test" 129 | Event.observe(td, 'click', function(){ window.location.search = "?tests=" + td.innerHTML;}); 130 | }); 131 | $$("tr.passed .nameCell").each( function(td){ // todo: limit to children of this.log 132 | td.title = "Run all tests" 133 | Event.observe(td, 'click', function(){ window.location.search = "";}); 134 | }); 135 | } 136 | } 137 | 138 | Test.Unit.Runner = Class.create(); 139 | Test.Unit.Runner.prototype = { 140 | initialize: function(testcases) { 141 | this.options = Object.extend({ 142 | testLog: 'testlog' 143 | }, arguments[1] || {}); 144 | this.options.resultsURL = this.parseResultsURLQueryParameter(); 145 | this.options.tests = this.parseTestsQueryParameter(); 146 | if (this.options.testLog) { 147 | this.options.testLog = $(this.options.testLog) || null; 148 | } 149 | if(this.options.tests) { 150 | this.tests = []; 151 | for(var i = 0; i < this.options.tests.length; i++) { 152 | if(/^test/.test(this.options.tests[i])) { 153 | this.tests.push(new Test.Unit.Testcase(this.options.tests[i], testcases[this.options.tests[i]], testcases["setup"], testcases["teardown"])); 154 | } 155 | } 156 | } else { 157 | if (this.options.test) { 158 | this.tests = [new Test.Unit.Testcase(this.options.test, testcases[this.options.test], testcases["setup"], testcases["teardown"])]; 159 | } else { 160 | this.tests = []; 161 | for(var testcase in testcases) { 162 | if(/^test/.test(testcase)) { 163 | this.tests.push( 164 | new Test.Unit.Testcase( 165 | this.options.context ? ' -> ' + this.options.titles[testcase] : testcase, 166 | testcases[testcase], testcases["setup"], testcases["teardown"] 167 | )); 168 | } 169 | } 170 | } 171 | } 172 | this.currentTest = 0; 173 | this.logger = new Test.Unit.Logger(this.options.testLog); 174 | setTimeout(this.runTests.bind(this), 1000); 175 | }, 176 | parseResultsURLQueryParameter: function() { 177 | return window.location.search.parseQuery()["resultsURL"]; 178 | }, 179 | parseTestsQueryParameter: function(){ 180 | if (window.location.search.parseQuery()["tests"]){ 181 | return window.location.search.parseQuery()["tests"].split(','); 182 | }; 183 | }, 184 | // Returns: 185 | // "ERROR" if there was an error, 186 | // "FAILURE" if there was a failure, or 187 | // "SUCCESS" if there was neither 188 | getResult: function() { 189 | var hasFailure = false; 190 | for(var i=0;i 0) { 192 | return "ERROR"; 193 | } 194 | if (this.tests[i].failures > 0) { 195 | hasFailure = true; 196 | } 197 | } 198 | if (hasFailure) { 199 | return "FAILURE"; 200 | } else { 201 | return "SUCCESS"; 202 | } 203 | }, 204 | postResults: function() { 205 | if (this.options.resultsURL) { 206 | new Ajax.Request(this.options.resultsURL, 207 | { method: 'get', parameters: 'result=' + this.getResult(), asynchronous: false }); 208 | } 209 | }, 210 | runTests: function() { 211 | var test = this.tests[this.currentTest]; 212 | if (!test) { 213 | // finished! 214 | this.postResults(); 215 | this.logger.summary(this.summary()); 216 | return; 217 | } 218 | if(!test.isWaiting) { 219 | this.logger.start(test.name); 220 | } 221 | test.run(); 222 | if(test.isWaiting) { 223 | this.logger.message("Waiting for " + test.timeToWait + "ms"); 224 | setTimeout(this.runTests.bind(this), test.timeToWait || 1000); 225 | } else { 226 | this.logger.finish(test.status(), test.summary()); 227 | this.currentTest++; 228 | // tail recursive, hopefully the browser will skip the stackframe 229 | this.runTests(); 230 | } 231 | }, 232 | summary: function() { 233 | var assertions = 0; 234 | var failures = 0; 235 | var errors = 0; 236 | var messages = []; 237 | for(var i=0;i 0) return 'failed'; 282 | if (this.errors > 0) return 'error'; 283 | return 'passed'; 284 | }, 285 | assert: function(expression) { 286 | var message = arguments[1] || 'assert: got "' + Test.Unit.inspect(expression) + '"'; 287 | try { expression ? this.pass() : 288 | this.fail(message); } 289 | catch(e) { this.error(e); } 290 | }, 291 | assertEqual: function(expected, actual) { 292 | var message = arguments[2] || "assertEqual"; 293 | try { (expected == actual) ? this.pass() : 294 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 295 | '", actual "' + Test.Unit.inspect(actual) + '"'); } 296 | catch(e) { this.error(e); } 297 | }, 298 | assertInspect: function(expected, actual) { 299 | var message = arguments[2] || "assertInspect"; 300 | try { (expected == actual.inspect()) ? this.pass() : 301 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 302 | '", actual "' + Test.Unit.inspect(actual) + '"'); } 303 | catch(e) { this.error(e); } 304 | }, 305 | assertEnumEqual: function(expected, actual) { 306 | var message = arguments[2] || "assertEnumEqual"; 307 | try { $A(expected).length == $A(actual).length && 308 | expected.zip(actual).all(function(pair) { return pair[0] == pair[1] }) ? 309 | this.pass() : this.fail(message + ': expected ' + Test.Unit.inspect(expected) + 310 | ', actual ' + Test.Unit.inspect(actual)); } 311 | catch(e) { this.error(e); } 312 | }, 313 | assertNotEqual: function(expected, actual) { 314 | var message = arguments[2] || "assertNotEqual"; 315 | try { (expected != actual) ? this.pass() : 316 | this.fail(message + ': got "' + Test.Unit.inspect(actual) + '"'); } 317 | catch(e) { this.error(e); } 318 | }, 319 | assertIdentical: function(expected, actual) { 320 | var message = arguments[2] || "assertIdentical"; 321 | try { (expected === actual) ? this.pass() : 322 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 323 | '", actual "' + Test.Unit.inspect(actual) + '"'); } 324 | catch(e) { this.error(e); } 325 | }, 326 | assertNotIdentical: function(expected, actual) { 327 | var message = arguments[2] || "assertNotIdentical"; 328 | try { !(expected === actual) ? this.pass() : 329 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 330 | '", actual "' + Test.Unit.inspect(actual) + '"'); } 331 | catch(e) { this.error(e); } 332 | }, 333 | assertNull: function(obj) { 334 | var message = arguments[1] || 'assertNull' 335 | try { (obj==null) ? this.pass() : 336 | this.fail(message + ': got "' + Test.Unit.inspect(obj) + '"'); } 337 | catch(e) { this.error(e); } 338 | }, 339 | assertMatch: function(expected, actual) { 340 | var message = arguments[2] || 'assertMatch'; 341 | var regex = new RegExp(expected); 342 | try { (regex.exec(actual)) ? this.pass() : 343 | this.fail(message + ' : regex: "' + Test.Unit.inspect(expected) + ' did not match: ' + Test.Unit.inspect(actual) + '"'); } 344 | catch(e) { this.error(e); } 345 | }, 346 | assertHidden: function(element) { 347 | var message = arguments[1] || 'assertHidden'; 348 | this.assertEqual("none", element.style.display, message); 349 | }, 350 | assertNotNull: function(object) { 351 | var message = arguments[1] || 'assertNotNull'; 352 | this.assert(object != null, message); 353 | }, 354 | assertType: function(expected, actual) { 355 | var message = arguments[2] || 'assertType'; 356 | try { 357 | (actual.constructor == expected) ? this.pass() : 358 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 359 | '", actual "' + (actual.constructor) + '"'); } 360 | catch(e) { this.error(e); } 361 | }, 362 | assertNotOfType: function(expected, actual) { 363 | var message = arguments[2] || 'assertNotOfType'; 364 | try { 365 | (actual.constructor != expected) ? this.pass() : 366 | this.fail(message + ': expected "' + Test.Unit.inspect(expected) + 367 | '", actual "' + (actual.constructor) + '"'); } 368 | catch(e) { this.error(e); } 369 | }, 370 | assertInstanceOf: function(expected, actual) { 371 | var message = arguments[2] || 'assertInstanceOf'; 372 | try { 373 | (actual instanceof expected) ? this.pass() : 374 | this.fail(message + ": object was not an instance of the expected type"); } 375 | catch(e) { this.error(e); } 376 | }, 377 | assertNotInstanceOf: function(expected, actual) { 378 | var message = arguments[2] || 'assertNotInstanceOf'; 379 | try { 380 | !(actual instanceof expected) ? this.pass() : 381 | this.fail(message + ": object was an instance of the not expected type"); } 382 | catch(e) { this.error(e); } 383 | }, 384 | assertRespondsTo: function(method, obj) { 385 | var message = arguments[2] || 'assertRespondsTo'; 386 | try { 387 | (obj[method] && typeof obj[method] == 'function') ? this.pass() : 388 | this.fail(message + ": object doesn't respond to [" + method + "]"); } 389 | catch(e) { this.error(e); } 390 | }, 391 | assertReturnsTrue: function(method, obj) { 392 | var message = arguments[2] || 'assertReturnsTrue'; 393 | try { 394 | var m = obj[method]; 395 | if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)]; 396 | m() ? this.pass() : 397 | this.fail(message + ": method returned false"); } 398 | catch(e) { this.error(e); } 399 | }, 400 | assertReturnsFalse: function(method, obj) { 401 | var message = arguments[2] || 'assertReturnsFalse'; 402 | try { 403 | var m = obj[method]; 404 | if(!m) m = obj['is'+method.charAt(0).toUpperCase()+method.slice(1)]; 405 | !m() ? this.pass() : 406 | this.fail(message + ": method returned true"); } 407 | catch(e) { this.error(e); } 408 | }, 409 | assertRaise: function(exceptionName, method) { 410 | var message = arguments[2] || 'assertRaise'; 411 | try { 412 | method(); 413 | this.fail(message + ": exception expected but none was raised"); } 414 | catch(e) { 415 | ((exceptionName == null) || (e.name==exceptionName)) ? this.pass() : this.error(e); 416 | } 417 | }, 418 | assertElementsMatch: function() { 419 | var expressions = $A(arguments), elements = $A(expressions.shift()); 420 | if (elements.length != expressions.length) { 421 | this.fail('assertElementsMatch: size mismatch: ' + elements.length + ' elements, ' + expressions.length + ' expressions'); 422 | return false; 423 | } 424 | elements.zip(expressions).all(function(pair, index) { 425 | var element = $(pair.first()), expression = pair.last(); 426 | if (element.match(expression)) return true; 427 | this.fail('assertElementsMatch: (in index ' + index + ') expected ' + expression.inspect() + ' but got ' + element.inspect()); 428 | }.bind(this)) && this.pass(); 429 | }, 430 | assertElementMatches: function(element, expression) { 431 | this.assertElementsMatch([element], expression); 432 | }, 433 | benchmark: function(operation, iterations) { 434 | var startAt = new Date(); 435 | (iterations || 1).times(operation); 436 | var timeTaken = ((new Date())-startAt); 437 | this.info((arguments[2] || 'Operation') + ' finished ' + 438 | iterations + ' iterations in ' + (timeTaken/1000)+'s' ); 439 | return timeTaken; 440 | }, 441 | _isVisible: function(element) { 442 | element = $(element); 443 | if(!element.parentNode) return true; 444 | this.assertNotNull(element); 445 | if(element.style && Element.getStyle(element, 'display') == 'none') 446 | return false; 447 | 448 | return this._isVisible(element.parentNode); 449 | }, 450 | assertNotVisible: function(element) { 451 | this.assert(!this._isVisible(element), Test.Unit.inspect(element) + " was not hidden and didn't have a hidden parent either. " + ("" || arguments[1])); 452 | }, 453 | assertVisible: function(element) { 454 | this.assert(this._isVisible(element), Test.Unit.inspect(element) + " was not visible. " + ("" || arguments[1])); 455 | }, 456 | benchmark: function(operation, iterations) { 457 | var startAt = new Date(); 458 | (iterations || 1).times(operation); 459 | var timeTaken = ((new Date())-startAt); 460 | this.info((arguments[2] || 'Operation') + ' finished ' + 461 | iterations + ' iterations in ' + (timeTaken/1000)+'s' ); 462 | return timeTaken; 463 | } 464 | } 465 | 466 | Test.Unit.Testcase = Class.create(); 467 | Object.extend(Object.extend(Test.Unit.Testcase.prototype, Test.Unit.Assertions.prototype), { 468 | initialize: function(name, test, setup, teardown) { 469 | Test.Unit.Assertions.prototype.initialize.bind(this)(); 470 | this.name = name; 471 | 472 | if(typeof test == 'string') { 473 | test = test.gsub(/(\.should[^\(]+\()/,'#{0}this,'); 474 | test = test.gsub(/(\.should[^\(]+)\(this,\)/,'#{1}(this)'); 475 | this.test = function() { 476 | eval('with(this){'+test+'}'); 477 | } 478 | } else { 479 | this.test = test || function() {}; 480 | } 481 | 482 | this.setup = setup || function() {}; 483 | this.teardown = teardown || function() {}; 484 | this.isWaiting = false; 485 | this.timeToWait = 1000; 486 | }, 487 | wait: function(time, nextPart) { 488 | this.isWaiting = true; 489 | this.test = nextPart; 490 | this.timeToWait = time; 491 | }, 492 | run: function() { 493 | try { 494 | try { 495 | if (!this.isWaiting) this.setup.bind(this)(); 496 | this.isWaiting = false; 497 | this.test.bind(this)(); 498 | } finally { 499 | if(!this.isWaiting) { 500 | this.teardown.bind(this)(); 501 | } 502 | } 503 | } 504 | catch(e) { this.error(e); } 505 | } 506 | }); 507 | 508 | // *EXPERIMENTAL* BDD-style testing to please non-technical folk 509 | // This draws many ideas from RSpec http://rspec.rubyforge.org/ 510 | 511 | Test.setupBDDExtensionMethods = function(){ 512 | var METHODMAP = { 513 | shouldEqual: 'assertEqual', 514 | shouldNotEqual: 'assertNotEqual', 515 | shouldEqualEnum: 'assertEnumEqual', 516 | shouldBeA: 'assertType', 517 | shouldNotBeA: 'assertNotOfType', 518 | shouldBeAn: 'assertType', 519 | shouldNotBeAn: 'assertNotOfType', 520 | shouldBeNull: 'assertNull', 521 | shouldNotBeNull: 'assertNotNull', 522 | 523 | shouldBe: 'assertReturnsTrue', 524 | shouldNotBe: 'assertReturnsFalse', 525 | shouldRespondTo: 'assertRespondsTo' 526 | }; 527 | var makeAssertion = function(assertion, args, object) { 528 | this[assertion].apply(this,(args || []).concat([object])); 529 | } 530 | 531 | Test.BDDMethods = {}; 532 | $H(METHODMAP).each(function(pair) { 533 | Test.BDDMethods[pair.key] = function() { 534 | var args = $A(arguments); 535 | var scope = args.shift(); 536 | makeAssertion.apply(scope, [pair.value, args, this]); }; 537 | }); 538 | 539 | [Array.prototype, String.prototype, Number.prototype, Boolean.prototype].each( 540 | function(p){ Object.extend(p, Test.BDDMethods) } 541 | ); 542 | } 543 | 544 | Test.context = function(name, spec, log){ 545 | Test.setupBDDExtensionMethods(); 546 | 547 | var compiledSpec = {}; 548 | var titles = {}; 549 | for(specName in spec) { 550 | switch(specName){ 551 | case "setup": 552 | case "teardown": 553 | compiledSpec[specName] = spec[specName]; 554 | break; 555 | default: 556 | var testName = 'test'+specName.gsub(/\s+/,'-').camelize(); 557 | var body = spec[specName].toString().split('\n').slice(1); 558 | if(/^\{/.test(body[0])) body = body.slice(1); 559 | body.pop(); 560 | body = body.map(function(statement){ 561 | return statement.strip() 562 | }); 563 | compiledSpec[testName] = body.join('\n'); 564 | titles[testName] = specName; 565 | } 566 | } 567 | new Test.Unit.Runner(compiledSpec, { titles: titles, testLog: log || 'testlog', context: name }); 568 | }; -------------------------------------------------------------------------------- /azukilib.js: -------------------------------------------------------------------------------- 1 | var Azuki = { 2 | Version: '0.0.1', 3 | Forms: { }, 4 | Helpers: { }, 5 | Storage: { }, 6 | Style: { }, 7 | Windowing: { } 8 | }; 9 | 10 | 11 | /* START popinfo.js */ 12 | 13 | Azuki.Windowing.PopInfo = { 14 | display: function(element, info, callback) { 15 | var delay = 2000; 16 | var popup_element_id = element.id + '_popup'; 17 | 18 | this.remove(popup_element_id); 19 | new Insertion.Before(element, ''); 20 | 21 | if (callback) callback(element); 22 | 23 | new Effect.Appear(popup_element_id, { duration: 0.2 }); 24 | setTimeout((function() { this.fade_popup(popup_element_id); }.bind(this)), delay); 25 | }, 26 | 27 | fade_popup: function(popup_element_id) { 28 | try { 29 | new Effect.Fade(popup_element_id); 30 | setTimeout((function() { this.remove(popup_element_id); }).bind(this), 1000); 31 | } 32 | catch (exception) { 33 | } 34 | }, 35 | 36 | remove: function(popup_element_id) { 37 | try { 38 | if ($(popup_element_id)) Element.remove(popup_element_id); 39 | } 40 | catch (exception) { 41 | 42 | } 43 | } 44 | }; 45 | 46 | /* END popinfo.js */ 47 | 48 | /* START lightbox.js */ 49 | Azuki.Windowing.Lightbox = Class.create(); 50 | Azuki.Windowing.Lightbox.prototype = { 51 | initialize: function(class_name) { 52 | this.class_name = typeof class_name == 'undefined' ? 'lightbox' : class_name; 53 | 54 | this.container_id = 'LightboxContainer'; 55 | this.close_id = 'LightboxClose'; 56 | this.remote_type = 'image'; 57 | 58 | this.remove_event = this.remove.bindAsEventListener(this); 59 | Event.observe(document, 'click', this.events.bind(this)); 60 | }, 61 | 62 | events: function(e) { 63 | var element = Event.element(e); 64 | this.image = null; 65 | this.link = null; 66 | 67 | if (element.nodeName == 'A' && element.down('img') && element.down('img').hasClassName(this.class_name)) { 68 | this.image = element.down('img'); 69 | this.link = element; 70 | } 71 | 72 | if (element.nodeName == 'IMG' && element.hasClassName(this.class_name)) { 73 | this.link = element.up('a'); 74 | this.image = element; 75 | } 76 | 77 | if (this.link && this.image) { 78 | this.display(e); 79 | } 80 | }, 81 | 82 | remove: function(e) { 83 | Event.stopObserving($(this.close_id), 'click', this.remove_event); 84 | $(this.container_id).remove(); 85 | Azuki.Windowing.Fader.remove(); 86 | Event.stop(e); 87 | return false; 88 | }, 89 | 90 | display: function(e) { 91 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 92 | 93 | if (Azuki.Windowing.Busybox.active()) { 94 | Event.stop(e); 95 | return false; 96 | } 97 | 98 | if ($(this.container_id)) $(this.container_id).remove(); 99 | Azuki.Windowing.Busybox.busy('loading'); 100 | new Insertion.Top(document.body, ''); 101 | 102 | Event.observe($(this.close_id), 'click', this.remove_event); 103 | 104 | switch(this.remote_type) { 105 | case 'image': 106 | var image = document.createElement('img'); 107 | 108 | image.onload = function() { 109 | $(this.container_id).appendChild(image); 110 | 111 | Azuki.Windowing.Busybox.done(); 112 | Azuki.Helpers.Window.center(this.container_id); 113 | Azuki.Windowing.Fader.fade(); 114 | new Effect.Appear(this.container_id, {duration: 0.3}); 115 | 116 | return false; 117 | }.bind(this); 118 | 119 | image.src = this.link.href; 120 | break; 121 | 122 | case 'html': 123 | new Ajax.Updater(this.container_id, this.link.href, { method: 'get', evalScripts: true, onComplete: function() { 124 | Azuki.Windowing.Busybox.done(); 125 | Azuki.Windowing.Fader.fade(); 126 | Azuki.Helpers.Window.center($(this.container_id)); 127 | 128 | $(this.container_id).show(); 129 | }.bind(this)}); 130 | break; 131 | 132 | default: 133 | break; 134 | } 135 | 136 | if (e) Event.stop(e); 137 | return false; 138 | } 139 | }; 140 | 141 | /* END lightbox.js */ 142 | 143 | /* START fader.js */ 144 | Azuki.Windowing.Fader = { 145 | active: function() { 146 | return $('Fader') ? true : false; 147 | }, 148 | 149 | set_height: function() { 150 | $('Fader').setStyle({height: Azuki.Helpers.Window.page_size().height + 'px'}); 151 | }, 152 | 153 | fade: function() { 154 | new Insertion.Top(document.body, ''); 155 | Position.prepare(); 156 | Azuki.Windowing.Fader.set_height(); 157 | $('Fader').setOpacity(0); 158 | $('Fader').show(0); 159 | Azuki.Windowing.Fader.alter_selects('hidden'); 160 | Azuki.Windowing.Fader.alter_overflows('hidden'); 161 | new Effect.Opacity('Fader', {duration: 0, from: 0.7, to: 0.7}); 162 | }, 163 | 164 | alter_selects: function(visible, selector) { 165 | if (!(/MSIE/.test(navigator.userAgent) && !window.opera)) return; 166 | if (!selector) selector = 'select'; 167 | 168 | $$(selector).each(function(element) { 169 | element.setStyle({ visibility: visible }); 170 | }); 171 | }, 172 | 173 | alter_overflows: function(overflow_setting) { 174 | $$('.overflow').each(function(element) { 175 | element.setStyle({ overflow: overflow_setting }); 176 | }); 177 | }, 178 | 179 | remove: function() 180 | { 181 | if (!$('Fader')) return; 182 | 183 | new Effect.Opacity('Fader', {duration: 0.2, from: 0.7, to: 0.0, afterFinish: function() { 184 | if ($('Fader')) $('Fader').remove(); 185 | Azuki.Windowing.Fader.alter_selects('visible'); 186 | Azuki.Windowing.Fader.alter_overflows('auto'); 187 | }}); 188 | } 189 | }; 190 | 191 | Event.observe(window, 'resize', function() { 192 | if (!$('Fader')) return; 193 | Azuki.Windowing.Fader.set_height(); 194 | }); 195 | 196 | /* END fader.js */ 197 | 198 | /* START contextual_help.js */ 199 | Azuki.Windowing.ContextualHelp = Class.create(); 200 | Azuki.Windowing.ContextualHelp.prototype = { 201 | initialize: function(help_class) { 202 | this.help_class = help_class ? help_class : 'help'; 203 | 204 | Event.observe(document, 'click', function(e) { 205 | var element = $(Event.element(e)); 206 | if (!element.hasClassName(this.help_class)) return true; 207 | if (element.nodeName != 'A') return true; 208 | 209 | this.display_help(element.href); 210 | 211 | Event.stop(e); 212 | return false; 213 | }.bind(this)); 214 | }, 215 | 216 | display_help: function(url) { 217 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 218 | var faded = Azuki.Windowing.Fader.active(); 219 | 220 | if (Azuki.Windowing.Busybox.active()) return; 221 | if ($('HelpContainer')) $('HelpContainer').remove(); 222 | 223 | Azuki.Windowing.Busybox.busy('loading'); 224 | new Insertion.Top(document.body, ''); 225 | 226 | Event.observe($('HelpClose'), 'click', function() { $('HelpContainer').remove(); if (!faded) { Azuki.Windowing.Fader.remove(); } }); 227 | 228 | new Ajax.Updater('HelpContent', url, { insertion: Insertion.Bottom, onComplete: function() { 229 | Azuki.Windowing.Busybox.done(); 230 | Azuki.Helpers.Window.center('HelpContainer'); 231 | if (!faded) Azuki.Windowing.Fader.fade(); 232 | new Effect.Appear($('HelpContainer'), {duration: 0.3}); 233 | }}); 234 | } 235 | }; 236 | 237 | /* END contextual_help.js */ 238 | 239 | /* START busybox.js */ 240 | Azuki.Windowing.Busybox = { 241 | /* operation can be 'loading', 'saving', etc. depending on the images you've got. */ 242 | busy: function(operation) { 243 | this.remove(); 244 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 245 | 246 | new Insertion.Top(document.body, ''); 247 | Azuki.Helpers.Window.center('Busybox'); 248 | $('Busybox').show(); 249 | }, 250 | 251 | remove: function() { 252 | if ($('Busybox')) $('Busybox').remove(); 253 | }, 254 | 255 | done: function() { 256 | new Effect.Fade($('Busybox'), { afterFinish: function() { Azuki.Windowing.Busybox.remove(); }}); 257 | }, 258 | 259 | active: function() { 260 | return $('Busybox') ? true : false; 261 | } 262 | }; 263 | 264 | /* END busybox.js */ 265 | 266 | /* START table_ruler.js */ 267 | /* Not implemented yet */ 268 | /* END table_ruler.js */ 269 | 270 | /* START color.js */ 271 | Azuki.Style.Color = {}; 272 | Azuki.Style.Color.RGB = Class.create(); 273 | Azuki.Style.Color.RGB.prototype = { 274 | initialize: function(value) { 275 | this.colors = Array(0, 0, 0); 276 | 277 | switch (typeof(value)) { 278 | case 'string': 279 | this.set_from_rgb_string(value); 280 | break; 281 | 282 | case 'object': 283 | this.colors = value; 284 | break; 285 | 286 | default: 287 | break; 288 | } 289 | }, 290 | 291 | red: function() { return this.colors[0]; }, 292 | green: function() { return this.colors[1]; }, 293 | blue: function() { return this.colors[2]; }, 294 | 295 | set_red: function(red) { this.colors[0] = red; }, 296 | set_green: function(green) { this.colors[1] = green; }, 297 | set_blue: function(blue) { this.colors[2] = blue; }, 298 | 299 | /* Assumes rgb(1, 2, 3) */ 300 | set_from_rgb_string: function(value) { 301 | this.colors = $A(value.replace(/rgb\(/, '').replace(/\)/, '').split(',')).collect(function(value) { 302 | return parseInt(value, 10); 303 | }); 304 | }, 305 | 306 | to_s: function() { 307 | return 'rgb(' + this.red() + ',' + this.green() + ',' + this.blue() + ')'; 308 | }, 309 | 310 | to_hex: function() { 311 | var hex = ''; 312 | 313 | $A(this.colors).each(function(colour) { 314 | var value = this._hex_value(colour); 315 | if (value.length == 1) { value = '0' + value; } 316 | hex = hex + value; 317 | }.bind(this)); 318 | 319 | return hex; 320 | }, 321 | 322 | _hex_value: function(d) { 323 | var hex_map = '0123456789ABCDEF'; 324 | var h = hex_map.substr(d&15, 1); 325 | 326 | while (d > 15) { d >>= 4; h = hex_map.substr(d&15, 1) + h; } 327 | return h; 328 | } 329 | }; 330 | 331 | Azuki.Style.Color.Hex = Class.create(); 332 | Azuki.Style.Color.Hex.prototype = { 333 | /* Create with values like this: '#000000' */ 334 | initialize: function(value) { 335 | this.value = value; 336 | }, 337 | 338 | to_rgb: function() { 339 | this._set_rgb_values(); 340 | var rgb_array = $A([this.red, this.green, this.blue]).collect(function(color) { 341 | return this._rgb_value(color); 342 | }.bind(this)); 343 | 344 | return new Azuki.Style.Color.RGB(rgb_array); 345 | }, 346 | 347 | _rgb_value: function(hex) { 348 | return parseInt(hex, 16) || 0; 349 | }, 350 | 351 | _set_rgb_values: function() { 352 | this.red = this.value.charAt(1) + this.value.charAt(2); 353 | this.green = this.value.charAt(3) + this.value.charAt(4); 354 | this.blue = this.value.charAt(5) + this.value.charAt(6); 355 | } 356 | }; 357 | 358 | Azuki.Style.Color.Methods = { 359 | invert: function(value) { 360 | var color = new Azuki.Style.Color.RGB(value); 361 | color.set_red(255 - parseInt(color.red(), 10)); 362 | color.set_green(255 - parseInt(color.green(), 10)); 363 | color.set_blue(255 - parseInt(color.blue(), 10)); 364 | return color.to_s(); 365 | }, 366 | 367 | random: function() { 368 | var color = new Azuki.Style.Color.RGB(Array(Math.round((Math.random() * 255)), Math.round((Math.random() * 255)), Math.round((Math.random() * 255)))); 369 | return color.to_s(); 370 | } 371 | }; 372 | 373 | Azuki.Style.Color.ElementMethods = { 374 | invertColor: function(element, property) { 375 | try { 376 | element.style[property] = Azuki.Style.Color.invert(element.getStyle(property)); 377 | return true; 378 | } 379 | catch (exception) { 380 | return false; 381 | } 382 | }, 383 | 384 | randomColor: function(element, property) 385 | { 386 | try { 387 | element.style[property] = Azuki.Style.Color.random(); 388 | return true; 389 | } 390 | catch (exception) { 391 | return false; 392 | } 393 | } 394 | }; 395 | 396 | Object.extend(Azuki.Style.Color, Azuki.Style.Color.Methods); 397 | Element.addMethods(Azuki.Style.Color.ElementMethods); 398 | 399 | /* END color.js */ 400 | 401 | /* START cookie.js */ 402 | Azuki.Storage.Cookie = { 403 | create: function(name, value, days, path) { 404 | var expires = ''; 405 | path = typeof path == 'undefined' ? '/' : path; 406 | 407 | if (days) { 408 | var date = new Date(); 409 | date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); 410 | expires = "; expires=" + date.toGMTString(); 411 | } 412 | 413 | if (name && value) { 414 | document.cookie = name + '=' + escape(value) + expires + '; path=' + path; 415 | } 416 | }, 417 | 418 | find: function(name) { 419 | var matches = document.cookie.match(name + '=([^;]*)'); 420 | if (matches && matches.length == 2) { 421 | return unescape(matches[1]); 422 | } 423 | }, 424 | 425 | destroy: function(name) { 426 | this.create(name, ' ', -1); 427 | } 428 | }; 429 | 430 | /* END cookie.js */ 431 | 432 | /* START window_helper.js */ 433 | Azuki.Helpers.Window = { 434 | size: function() { 435 | var width, height; 436 | 437 | if (self.innerHeight) { 438 | width = self.innerWidth; 439 | height = self.innerHeight; 440 | } else if (document.documentElement && document.documentElement.clientHeight) { 441 | // IE 6 Strict Mode 442 | width = document.documentElement.clientWidth; 443 | height = document.documentElement.clientHeight; 444 | } else if (document.body) { 445 | // IE 446 | width = document.body.clientWidth; 447 | height = document.body.clientHeight; 448 | } 449 | 450 | return {width: width, height: height}; 451 | }, 452 | 453 | page_size: function() { 454 | var x_scroll, y_scroll; 455 | var width, height; 456 | var window_size = Azuki.Helpers.Window.size(); 457 | 458 | if (window.innerHeight && window.scrollMaxY) { 459 | x_scroll = document.body.scrollWidth; 460 | y_scroll = window.innerHeight + window.scrollMaxY; 461 | } else if (document.body.scrollHeight > document.body.offsetHeight) { 462 | x_scroll = document.body.scrollWidth; 463 | y_scroll = document.body.scrollHeight; 464 | } else { 465 | x_scroll = document.body.offsetWidth; 466 | y_scroll = document.body.offsetHeight; 467 | } 468 | 469 | width = x_scroll < window_size.width ? window_size.width : x_scroll; 470 | height = y_scroll < window_size.height ? window_size.height : y_scroll; 471 | 472 | return {width: width, height: height}; 473 | }, 474 | 475 | /* Centres absolute positions elements */ 476 | center: function(element) { 477 | var options = Object.extend({update: false}, arguments[1] || {}); 478 | element = $(element); 479 | 480 | Position.prepare(); 481 | 482 | var offset_x = (Position.deltaX + Math.floor((Azuki.Helpers.Window.size().width - element.getDimensions().width) / 2)) || '0'; 483 | var offset_y = (Position.deltaY + Math.floor((Azuki.Helpers.Window.size().height - element.getDimensions().height) / 2)) || '0'; 484 | 485 | element.setStyle({ left: offset_x + 'px' }); 486 | element.setStyle({ top: offset_y + 'px' }); 487 | 488 | if (options.update) { 489 | Event.observe(window, 'resize', function() { Position.center(element); }); 490 | Event.observe(window, 'scroll', function() { Position.center(element); }); 491 | } 492 | }, 493 | 494 | center_with_margin: function(element) { 495 | element = $(element); 496 | var container = element.up(); 497 | var margin = (container.getWidth() - element.getWidth()) / 2; 498 | element.setStyle({ marginLeft: margin + 'px', marginRight: margin + 'px'}); 499 | } 500 | }; 501 | 502 | /* END window_helper.js */ 503 | 504 | /* START keyboard_helper.js */ 505 | Azuki.Helpers.Keyboard = { 506 | which_meta_key: function() { 507 | var key = ''; 508 | 509 | if (navigator.platform.match(/win/i)) { 510 | key = 'alt'; 511 | } else if (navigator.platform.match(/mac/i)) { 512 | key = 'ctrl'; 513 | } else { 514 | key = 'ctrl or alt'; 515 | } 516 | 517 | return key; 518 | }, 519 | 520 | underline_accesskey: function(element) { 521 | element = $(element); 522 | var key = this.which_meta_key(); 523 | var regex = new RegExp(element.accessKey, 'i'); 524 | var match = ''; 525 | 526 | if (element.accessKey.length === 0 || element.hasClassName('noaccesskey') > 0) return; 527 | 528 | if (element.tagName == 'A' && !element.innerHTML.match(/class=.?accesskey/)) { 529 | match = element.innerHTML.match(regex); 530 | element.innerHTML = element.innerHTML.replace(regex, '' + match + ''); 531 | } else if (element.tagName == 'INPUT') { 532 | element.value = element.value + ' [' + key + '+' + String(element.accessKey).toLowerCase() + ']'; 533 | } else if (element.tagName == 'BUTTON') { 534 | var text = ''; 535 | 536 | /* Remove text nodes from the button, this assumes the button has a format like: image text */ 537 | $A(element.childNodes).each(function(child) { 538 | if (child.nodeName == '#text') { 539 | text = text + child.data; 540 | element.removeChild(child); 541 | } 542 | }); 543 | 544 | match = text.match(regex); 545 | new Insertion.Bottom(element, text.replace(regex, '' + match + '')); 546 | } 547 | }, 548 | 549 | underline_accesskeys: function() { 550 | this.underline_tag_accesskeys('a'); 551 | this.underline_tag_accesskeys('button'); 552 | this.underline_tag_accesskeys('input'); 553 | }, 554 | 555 | underline_tag_accesskeys: function(tag_name) { 556 | var key = this.which_meta_key(); 557 | $A(document.getElementsByTagName(tag_name)).each(function(element) { 558 | this.underline_accesskey(element); 559 | }.bind(this)); 560 | } 561 | }; 562 | 563 | /* END keyboard_helper.js */ 564 | 565 | /* START forms_helper.js */ 566 | Azuki.Helpers.Forms = { 567 | /* Text helpers */ 568 | set_caret_position: function(element, pos) { 569 | if (element.setSelectionRange) { 570 | element.focus(); 571 | element.setSelectionRange(pos, pos); 572 | } else if (element.createTextRange) { 573 | var range = element.createTextRange(); 574 | 575 | range.collapse(true); 576 | range.moveEnd('character', pos); 577 | range.moveStart('character', pos); 578 | range.select(); 579 | } 580 | }, 581 | 582 | get_caret_position: function(element) { 583 | if (element.setSelectionRange) { 584 | return element.selectionStart; 585 | } else if (element.createTextRange) { 586 | // The current selection 587 | var range = document.selection.createRange(); 588 | // We'll use this as a 'dummy' 589 | var stored_range = range.duplicate(); 590 | // Select all text 591 | stored_range.moveToElementText(element); 592 | // Now move 'dummy' end point to end point of original range 593 | stored_range.setEndPoint('EndToEnd', range); 594 | 595 | return stored_range.text.length - range.text.length; 596 | } 597 | }, 598 | 599 | activate_controls: function(names) { 600 | $A(document.getElementsByTagName('input')).each(function(element) { 601 | $A(names).each(function(name) { 602 | if (element.name == name) { 603 | element.disabled = false; 604 | } 605 | }); 606 | }); 607 | }, 608 | 609 | disable_on_submit: function() { 610 | $$('form').each(function(form) { 611 | var submit = $A(form.getElementsByTagName('input')).find(function(input) { return input.type == 'submit'; } ); 612 | if (!submit) return; 613 | 614 | Event.observe(form, 'submit', function(e) { 615 | submit = $(submit); 616 | if (submit && !submit.hasClassName('nodisable')) submit.disable(); 617 | }); 618 | }); 619 | } 620 | }; 621 | 622 | /* END forms_helper.js */ 623 | 624 | /* START compatibility_helper.js */ 625 | Azuki.Helpers.Compatibility = { 626 | /* Rather than hacking IE's png transparency, we currently use this to switch between gif and png. */ 627 | suitable_image_format: function() 628 | { 629 | if (!navigator.appVersion.match(/MSIE/)) return 'png'; 630 | 631 | try { 632 | var version = parseFloat(navigator.appVersion.split('MSIE')[1]); 633 | return (version < 7.0) ? 'gif' : 'png'; 634 | } 635 | catch (exception) { 636 | return 'png'; 637 | } 638 | } 639 | }; 640 | 641 | /* END compatibility_helper.js */ 642 | 643 | /* START textarea_extensions.js */ 644 | Azuki.Forms.TextAreaExtensions = Class.create(); 645 | Azuki.Forms.TextAreaExtensions.prototype = { 646 | initialize: function() { 647 | this._add_images(); 648 | 649 | this.bigger = $$('.bigger'); 650 | this.smaller = $$('.smaller'); 651 | 652 | // Add event observer to all more buttons 653 | this.bigger.each(function(item) { 654 | Event.observe(item, 'click', this.increase_rows.bindAsEventListener(this)); 655 | }.bind(this)); 656 | 657 | // Add event observer to all less buttons 658 | this.smaller.each(function(item) { 659 | Event.observe(item, 'click', this.decrease_rows.bindAsEventListener(this)); 660 | }.bind(this)); 661 | }, 662 | 663 | increase_rows: function(e) { 664 | var element = $(Event.element(e)); 665 | var textarea = this._get_next_textarea(element); 666 | textarea.rows += 5; 667 | }, 668 | 669 | decrease_rows: function(e) { 670 | var element = $(Event.element(e)); 671 | var textarea = this._get_next_textarea(element); 672 | 673 | if (textarea.rows >= 5) { 674 | textarea.rows -= 4; 675 | } 676 | }, 677 | 678 | /** Private methods **/ 679 | _get_next_textarea: function(element) { 680 | var children = $A(element.parentNode.parentNode.childNodes); 681 | 682 | return children.find(function(child) { return child.nodeName == 'TEXTAREA'; }); 683 | }, 684 | 685 | _add_images: function() { 686 | $$('textarea').each(function(textarea) { 687 | new Insertion.Before(textarea, 'Increase the size of this text boxDecrease the size of this text box
'); 688 | }); 689 | } 690 | }; 691 | 692 | /* END textarea_extensions.js */ 693 | 694 | /* START select_search.js */ 695 | Azuki.Forms.SelectSearch = Class.create(); 696 | Azuki.Forms.SelectSearch.prototype = { 697 | initialize: function(search_input, search_results) { 698 | this.search_input = $(search_input); 699 | this.search_results = $(search_results); 700 | this.items = this.read_values(); 701 | 702 | new Event.observe(this.search_input, 'click', this.clear_search_field.bindAsEventListener(this)); 703 | new Event.observe(this.search_input, 'keydown', this.search.bindAsEventListener(this)); 704 | }, 705 | 706 | clear_search_field: function() { 707 | this.search_input.value = ''; 708 | this.items.each(function(option, i) { 709 | this.search_results.options[i] = new Option(option.text, option.value); 710 | }.bind(this)); 711 | }, 712 | 713 | read_values: function() { 714 | return $A(this.search_results.options).collect(function(option) { 715 | return {value: option.value, text: option.innerHTML}; 716 | }); 717 | }, 718 | 719 | search: function() { 720 | var query = this.search_input.value; 721 | var results = this.items.findAll(function(value) { 722 | return value.text.toLowerCase().match(query.toLowerCase()); 723 | }); 724 | 725 | this.search_results.options.length = 0; 726 | 727 | results.each(function(option, i) { 728 | this.search_results.options[i] = new Option(option.text, option.value); 729 | }.bind(this)); 730 | } 731 | }; 732 | 733 | /* END select_search.js */ 734 | 735 | /* START remote.js */ 736 | 737 | Azuki.Forms.Remote = Class.create(); 738 | Azuki.Forms.Remote.prototype = { 739 | initialize: function(options) { 740 | this.options = { 741 | form_id: 'RemoteForm', 742 | controller_name: '', 743 | item_name: '', 744 | method: 'post', 745 | enctype: null, 746 | ajax: false, 747 | add_close_button: true, 748 | form_visible: false, 749 | form_display_callback: false, 750 | add_completed_callback: null 751 | }; 752 | 753 | Object.extend(this.options, options || { }); 754 | 755 | this.options.edit_url = '/' + this.options.controller_name + '/edit/'; 756 | this.options.new_url = '/' + this.options.controller_name + '/new'; 757 | this.options.create_url = '/' + this.options.controller_name + '/create'; 758 | this.options.update_url = '/' + this.options.controller_name + '/update/'; 759 | this.options.destroy_url = '/' + this.options.controller_name + '/destroy/'; 760 | }, 761 | 762 | edit: function(e) { 763 | var element = Event.element(e); 764 | 765 | if (element.nodeName == 'IMG') { 766 | element = element.up('.edit_' + this.options.item_name); 767 | } 768 | 769 | if (!element) return true; 770 | 771 | if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('edit_' + this.options.item_name)) { 772 | var item_id = element.id.match(/\d+$/); 773 | 774 | this.show_form(this.options.form_id, this.options.edit_url + item_id, this.options.update_url + item_id, this.options.form_display_callback); 775 | Event.stop(e); 776 | return false; 777 | } 778 | 779 | return true; 780 | }, 781 | 782 | add: function(e) 783 | { 784 | var element = Event.element(e); 785 | 786 | if (element.nodeName == 'IMG') { 787 | element = element.up('.add_' + this.options.item_name); 788 | } 789 | 790 | if (!element) return true; 791 | 792 | if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('add_' + this.options.item_name)) { 793 | if (this.options.add_callback) this.options.add_callback(); 794 | this.show_form(this.options.form_id, this.options.new_url, this.options.create_url, this.options.form_display_callback); 795 | Event.stop(e); 796 | return false; 797 | } 798 | 799 | return true; 800 | }, 801 | 802 | destroy: function(e) 803 | { 804 | var element = Event.element(e); 805 | if ((element.nodeName == 'A' || element.nodeName == 'IMG') && element.hasClassName('destroy_' + this.options.item_name)) { 806 | var item_id = element.id.match(/\d+$/); 807 | 808 | if (confirm('Are you sure you want to delete that item?')) { 809 | window.location = this.options.destroy_url + item_id; 810 | } 811 | 812 | Event.stop(e); 813 | return false; 814 | } 815 | }, 816 | 817 | show_form: function(id, url, save_url, callback) { 818 | if (this.options.form_visible) { 819 | return; 820 | } else { 821 | this.options.form_visible = true; 822 | } 823 | 824 | Azuki.Windowing.Busybox.busy('loading'); 825 | 826 | function close_form() { 827 | if ($(id)) { 828 | $(id).remove(); 829 | Azuki.Windowing.Fader.remove(); 830 | } 831 | } 832 | 833 | function focus_field() { 834 | try { 835 | if ($(id)) $(id).focusFirstElement(); 836 | } catch (exception) { 837 | } 838 | } 839 | 840 | var fields_id = id + 'FormFields'; 841 | var cancel_id = 'Cancel' + id; 842 | var editor_html = ''; 843 | var image_format = Azuki.Helpers.Compatibility.suitable_image_format(); 844 | var enctype = typeof this.options.enctype == 'undefined' ? '' : 'enctype="multipart/form-data"'; 845 | 846 | close_form(); 847 | 848 | editor_html += ''; 852 | 853 | // Insert the editor HTML 854 | new Insertion.Top(document.body, editor_html); 855 | 856 | // Get the form fields from the server 857 | new Ajax.Updater($(fields_id), url, { 858 | evalScripts: true, 859 | method: 'get', 860 | onComplete: function() { 861 | Azuki.Helpers.Window.center(id); 862 | new Effect.Appear($(id), { duration: 0.3, afterFinish: function() { Azuki.Windowing.Fader.alter_selects('visible'); focus_field(); } }); 863 | Azuki.Windowing.Fader.fade(); 864 | Azuki.Windowing.Busybox.done(); 865 | 866 | if (this.options.ajax) { 867 | try { 868 | Event.observe(id, 'submit', this.ajax_submit.bindAsEventListener(this)); 869 | } catch(e) { 870 | console.log(e); 871 | } 872 | } 873 | 874 | Event.observe(cancel_id, 'click', function(e) { close_form(id); this.options.form_visible = false; Event.stop(e); return false; }.bind(this)); 875 | 876 | if (callback) callback(); 877 | }.bind(this) 878 | }); 879 | }, 880 | 881 | ajax_form_html: function() { 882 | if (!this.options.ajax) return ''; 883 | return ' onsubmit="return false" '; 884 | }, 885 | 886 | ajax_submit: function() { 887 | this.options.form_visible = false; 888 | new Ajax.Request(this.options.create_url, { onSuccess: this.options.ajax_on_succes, postBody: Form.serialize(this.options.form_id), onFailure: this.options.ajax_on_failure }); 889 | $(this.options.form_id).remove(); 890 | Azuki.Windowing.Fader.remove(); 891 | return false; 892 | } 893 | }; 894 | 895 | /* END remote.js */ 896 | 897 | /* START controller.js */ 898 | Azuki.Controller = { 899 | run: function() { 900 | if (document.body.id) Azuki.Controller.run_controller(document.body.id); 901 | Azuki.Controller.run_controller('Application'); 902 | }, 903 | 904 | run_controller: function(id) { 905 | var controller_class = id + 'Controller'; 906 | if (!window[controller_class]) return; 907 | 908 | if (eval(controller_class + '.run')) { 909 | eval(controller_class + '.run()'); 910 | } else { 911 | controller = new window[controller_class]; 912 | } 913 | } 914 | }; 915 | 916 | function init() { 917 | // quit if this function has already been called 918 | if (arguments.callee.done) return; 919 | 920 | // flag this function so we don't do the same thing twice 921 | arguments.callee.done = true; 922 | 923 | // kill the timer 924 | if (_timer) clearInterval(_timer); 925 | 926 | // do stuff 927 | Azuki.Controller.run(); 928 | } 929 | 930 | /* for Mozilla/Opera9 */ 931 | if (document.addEventListener) { 932 | document.addEventListener("DOMContentLoaded", init, false); 933 | } 934 | 935 | /* for Internet Explorer */ 936 | /*@cc_on @*/ 937 | /*@if (@_win32) 938 | document.write("