├── config ├── deploy.rb ├── database.yml ├── environments │ ├── production.rb │ ├── development.rb │ └── test.rb ├── routes.rb ├── boot.rb └── environment.rb ├── public ├── favicon.ico ├── images │ ├── logo.gif │ └── rails.png ├── robots.txt ├── javascripts │ ├── application.js │ ├── controls.js │ ├── dragdrop.js │ └── effects.js ├── stylesheets │ └── all.css ├── dispatch.cgi ├── dispatch.rb ├── dispatch.fcgi ├── 500.html ├── 404.html └── .htaccess ├── app ├── views │ ├── kite │ │ ├── _divider.rhtml │ │ ├── view.rhtml │ │ ├── _task.rhtml │ │ ├── by_tag.rhtml │ │ ├── index.rhtml │ │ └── _kite.rhtml │ ├── search │ │ ├── _result.rhtml │ │ ├── _form.rhtml │ │ └── index.rhtml │ ├── layouts │ │ ├── _footer.rhtml │ │ ├── _header.rhtml │ │ └── standard.rhtml │ └── home │ │ ├── index.rhtml │ │ └── _examples.rhtml ├── helpers │ ├── home_helper.rb │ ├── search_helper.rb │ ├── kite_helper.rb │ └── application_helper.rb ├── controllers │ ├── home_controller.rb │ ├── application.rb │ ├── kite_controller.rb │ └── search_controller.rb └── models │ ├── kite.rb │ ├── task.rb │ └── kite_parser.rb ├── .gitignore ├── vendor └── plugins │ └── acts_as_taggable_on_steroids │ ├── lib │ ├── tag_counts_extension.rb │ ├── tagging.rb │ ├── tag.rb │ └── acts_as_taggable.rb │ ├── test │ ├── fixtures │ │ ├── user.rb │ │ ├── users.yml │ │ ├── post.rb │ │ ├── photo.rb │ │ ├── tags.yml │ │ ├── photos.yml │ │ ├── posts.yml │ │ └── taggings.yml │ ├── database.yml │ ├── tagging_test.rb │ ├── schema.rb │ ├── abstract_unit.rb │ ├── tag_test.rb │ └── acts_as_taggable_test.rb │ ├── init.rb │ ├── CHANGELOG │ ├── Rakefile │ ├── MIT-LICENSE │ └── README ├── script ├── about ├── plugin ├── runner ├── server ├── console ├── destroy ├── generate ├── breakpointer ├── process │ ├── reaper │ ├── spawner │ └── inspector └── performance │ ├── profiler │ └── benchmarker ├── test ├── fixtures │ ├── kites.yml │ └── tasks.yml ├── unit │ ├── kite_test.rb │ └── task_test.rb ├── functional │ ├── home_controller_test.rb │ ├── kite_controller_test.rb │ └── search_controller_test.rb └── test_helper.rb ├── doc └── README_FOR_APP ├── README.textile ├── Rakefile ├── db ├── migrate │ ├── 002_create_tasks.rb │ ├── 001_create_kites.rb │ └── 003_add_taggable.rb └── schema.rb └── lib ├── kites ├── metacritic.kite ├── play.kite ├── boots.kite ├── hmv.kite ├── virgin.kite ├── amazon.kite └── currency.kite └── tasks ├── kite.rake └── capistrano.rake /config/deploy.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/kite/_divider.rhtml: -------------------------------------------------------------------------------- 1 |
<%= h task.kite.name %> <%= h task.name %> query-------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file -------------------------------------------------------------------------------- /script/about: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/about' -------------------------------------------------------------------------------- /script/plugin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/plugin' -------------------------------------------------------------------------------- /script/runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/runner' -------------------------------------------------------------------------------- /script/server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/server' -------------------------------------------------------------------------------- /script/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/console' -------------------------------------------------------------------------------- /script/destroy: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/destroy' -------------------------------------------------------------------------------- /script/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/generate' -------------------------------------------------------------------------------- /app/views/layouts/_footer.rhtml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /script/breakpointer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../config/boot' 3 | require 'commands/breakpointer' -------------------------------------------------------------------------------- /script/process/reaper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/reaper' 4 | -------------------------------------------------------------------------------- /script/process/spawner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require File.dirname(__FILE__) + '/../../config/boot' 3 | require 'commands/process/spawner' 4 | -------------------------------------------------------------------------------- /test/fixtures/kites.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | one: 3 | id: 1 4 | two: 5 | id: 2 6 | -------------------------------------------------------------------------------- /test/fixtures/tasks.yml: -------------------------------------------------------------------------------- 1 | # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html 2 | one: 3 | id: 1 4 | two: 5 | id: 2 6 | -------------------------------------------------------------------------------- /app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | layout 'standard' 3 | 4 | def index 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/home/index.rhtml: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'search/form' %> 2 |
Top kites: <%= top_tasks %>
3 | <%= render :partial => 'examples' %> -------------------------------------------------------------------------------- /app/views/kite/by_tag.rhtml: -------------------------------------------------------------------------------- 1 |
2 | <%= link_to 'search', search_form_url %> | <%= link_to 'list', kites_url %>
--------------------------------------------------------------------------------
/app/views/search/_form.rhtml:
--------------------------------------------------------------------------------
1 | <% form_tag search_form_url do -%>
2 | Look up: <%= text_field_tag 'q', params[:q] %>
3 | <%= submit_tag 'Search' %>
4 | <% end -%>
5 |
--------------------------------------------------------------------------------
/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | jonathan:
2 | id: 1
3 | name: Jonathan
4 |
5 | sam:
6 | id: 2
7 | name: Sam
8 |
--------------------------------------------------------------------------------
/vendor/plugins/acts_as_taggable_on_steroids/lib/tagging.rb:
--------------------------------------------------------------------------------
1 | class Tagging < ActiveRecord::Base
2 | belongs_to :tag
3 | belongs_to :taggable, :polymorphic => true
4 | end
5 |
--------------------------------------------------------------------------------
/public/javascripts/application.js:
--------------------------------------------------------------------------------
1 | // Place your application-specific JavaScript functions and classes here
2 | // This file is automatically included by javascript_include_tag :defaults
3 |
--------------------------------------------------------------------------------
/app/helpers/kite_helper.rb:
--------------------------------------------------------------------------------
1 | module KiteHelper
2 | def tag_links(kite)
3 | kite.tags.collect do |tag|
4 | link_to(tag.name, kites_by_tag_url(:tag => tag.name))
5 | end.join
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/doc/README_FOR_APP:
--------------------------------------------------------------------------------
1 | Use this README file to introduce your application and point to useful places in the API for learning more.
2 | Run "rake appdoc" to generate API documentation for your models and controllers.
--------------------------------------------------------------------------------
/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/post.rb:
--------------------------------------------------------------------------------
1 | class Post < ActiveRecord::Base
2 | acts_as_taggable
3 |
4 | belongs_to :user
5 |
6 | validates_presence_of :text
7 | end
8 |
--------------------------------------------------------------------------------
/vendor/plugins/acts_as_taggable_on_steroids/init.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/lib/acts_as_taggable'
2 |
3 | require File.dirname(__FILE__) + '/lib/tagging'
4 | require File.dirname(__FILE__) + '/lib/tag'
5 |
--------------------------------------------------------------------------------
/vendor/plugins/acts_as_taggable_on_steroids/test/fixtures/photo.rb:
--------------------------------------------------------------------------------
1 | class Photo < ActiveRecord::Base
2 | acts_as_taggable
3 |
4 | belongs_to :user
5 | end
6 |
7 | class SpecialPhoto < Photo
8 | end
9 |
--------------------------------------------------------------------------------
/app/views/kite/_kite.rhtml:
--------------------------------------------------------------------------------
1 | <%= h(kite.description) %>
4 | 5 | <%= render :partial => 'task', :collection => kite.tasks %> 6 | 7 |Tags: <%= tag_links(kite) %>
8 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/CHANGELOG: -------------------------------------------------------------------------------- 1 | [21 Feb 2007] 2 | 3 | * Use scoping instead of TagCountsExtension [Michael Schuerig] 4 | 5 | [7 Jan 2007] 6 | 7 | * Add :match_all to find_tagged_with [Michael Sheakoski] 8 | -------------------------------------------------------------------------------- /test/unit/kite_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class KiteTest < Test::Unit::TestCase 4 | fixtures :kites 5 | 6 | # Replace this with your real tests. 7 | def test_truth 8 | assert true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/unit/task_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../test_helper' 2 | 3 | class TaskTest < Test::Unit::TestCase 4 | fixtures :tasks 5 | 6 | # Replace this with your real tests. 7 | def test_truth 8 | assert true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /vendor/plugins/acts_as_taggable_on_steroids/test/database.yml: -------------------------------------------------------------------------------- 1 | mysql: 2 | :adapter: mysql 3 | :host: localhost 4 | :username: rails 5 | :password: 6 | :database: rails_plugin_test 7 | 8 | sqlite3: 9 | :adapter: sqlite3 10 | :database: ':memory:' 11 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h3. Kite 2 | 3 | Kite is a mobile web search experiment that uses a DSL to define small scrapers, called 'Kites', that can find product information from web pages. 4 | 5 | Kite was originally released in March 2007, and has since been discontinued and open sourced. 6 | -------------------------------------------------------------------------------- /app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | # Methods added to this helper will be available to all templates in the application. 2 | module ApplicationHelper 3 | def top_tasks 4 | Task.find(:all, :limit => 5).collect { |task| "#{task.kite.name} #{task.name}" }.join(', ') 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/views/search/index.rhtml: -------------------------------------------------------------------------------- 1 | <%= render :partial => 'search/form' %> 2 | <% if @results.size == 0 -%> 3 |There were no matches for your query.
4 | <% else -%> 5 |We've been notified about this issue and we'll take a look at it shortly.
28 |You may have mistyped the address or the page may have moved.
28 |/gi, "");
592 | },
593 | createEditField: function() {
594 | var text;
595 | if(this.options.loadTextURL) {
596 | text = this.options.loadingText;
597 | } else {
598 | text = this.getText();
599 | }
600 |
601 | var obj = this;
602 |
603 | if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
604 | this.options.textarea = false;
605 | var textField = document.createElement("input");
606 | textField.obj = this;
607 | textField.type = "text";
608 | textField.name = this.options.paramName;
609 | textField.value = text;
610 | textField.style.backgroundColor = this.options.highlightcolor;
611 | textField.className = 'editor_field';
612 | var size = this.options.size || this.options.cols || 0;
613 | if (size != 0) textField.size = size;
614 | if (this.options.submitOnBlur)
615 | textField.onblur = this.onSubmit.bind(this);
616 | this.editField = textField;
617 | } else {
618 | this.options.textarea = true;
619 | var textArea = document.createElement("textarea");
620 | textArea.obj = this;
621 | textArea.name = this.options.paramName;
622 | textArea.value = this.convertHTMLLineBreaks(text);
623 | textArea.rows = this.options.rows;
624 | textArea.cols = this.options.cols || 40;
625 | textArea.className = 'editor_field';
626 | if (this.options.submitOnBlur)
627 | textArea.onblur = this.onSubmit.bind(this);
628 | this.editField = textArea;
629 | }
630 |
631 | if(this.options.loadTextURL) {
632 | this.loadExternalText();
633 | }
634 | this.form.appendChild(this.editField);
635 | },
636 | getText: function() {
637 | return this.element.innerHTML;
638 | },
639 | loadExternalText: function() {
640 | Element.addClassName(this.form, this.options.loadingClassName);
641 | this.editField.disabled = true;
642 | new Ajax.Request(
643 | this.options.loadTextURL,
644 | Object.extend({
645 | asynchronous: true,
646 | onComplete: this.onLoadedExternalText.bind(this)
647 | }, this.options.ajaxOptions)
648 | );
649 | },
650 | onLoadedExternalText: function(transport) {
651 | Element.removeClassName(this.form, this.options.loadingClassName);
652 | this.editField.disabled = false;
653 | this.editField.value = transport.responseText.stripTags();
654 | Field.scrollFreeActivate(this.editField);
655 | },
656 | onclickCancel: function() {
657 | this.onComplete();
658 | this.leaveEditMode();
659 | return false;
660 | },
661 | onFailure: function(transport) {
662 | this.options.onFailure(transport);
663 | if (this.oldInnerHTML) {
664 | this.element.innerHTML = this.oldInnerHTML;
665 | this.oldInnerHTML = null;
666 | }
667 | return false;
668 | },
669 | onSubmit: function() {
670 | // onLoading resets these so we need to save them away for the Ajax call
671 | var form = this.form;
672 | var value = this.editField.value;
673 |
674 | // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
675 | // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
676 | // to be displayed indefinitely
677 | this.onLoading();
678 |
679 | if (this.options.evalScripts) {
680 | new Ajax.Request(
681 | this.url, Object.extend({
682 | parameters: this.options.callback(form, value),
683 | onComplete: this.onComplete.bind(this),
684 | onFailure: this.onFailure.bind(this),
685 | asynchronous:true,
686 | evalScripts:true
687 | }, this.options.ajaxOptions));
688 | } else {
689 | new Ajax.Updater(
690 | { success: this.element,
691 | // don't update on failure (this could be an option)
692 | failure: null },
693 | this.url, Object.extend({
694 | parameters: this.options.callback(form, value),
695 | onComplete: this.onComplete.bind(this),
696 | onFailure: this.onFailure.bind(this)
697 | }, this.options.ajaxOptions));
698 | }
699 | // stop the event to avoid a page refresh in Safari
700 | if (arguments.length > 1) {
701 | Event.stop(arguments[0]);
702 | }
703 | return false;
704 | },
705 | onLoading: function() {
706 | this.saving = true;
707 | this.removeForm();
708 | this.leaveHover();
709 | this.showSaving();
710 | },
711 | showSaving: function() {
712 | this.oldInnerHTML = this.element.innerHTML;
713 | this.element.innerHTML = this.options.savingText;
714 | Element.addClassName(this.element, this.options.savingClassName);
715 | this.element.style.backgroundColor = this.originalBackground;
716 | Element.show(this.element);
717 | },
718 | removeForm: function() {
719 | if(this.form) {
720 | if (this.form.parentNode) Element.remove(this.form);
721 | this.form = null;
722 | }
723 | },
724 | enterHover: function() {
725 | if (this.saving) return;
726 | this.element.style.backgroundColor = this.options.highlightcolor;
727 | if (this.effect) {
728 | this.effect.cancel();
729 | }
730 | Element.addClassName(this.element, this.options.hoverClassName)
731 | },
732 | leaveHover: function() {
733 | if (this.options.backgroundColor) {
734 | this.element.style.backgroundColor = this.oldBackground;
735 | }
736 | Element.removeClassName(this.element, this.options.hoverClassName)
737 | if (this.saving) return;
738 | this.effect = new Effect.Highlight(this.element, {
739 | startcolor: this.options.highlightcolor,
740 | endcolor: this.options.highlightendcolor,
741 | restorecolor: this.originalBackground
742 | });
743 | },
744 | leaveEditMode: function() {
745 | Element.removeClassName(this.element, this.options.savingClassName);
746 | this.removeForm();
747 | this.leaveHover();
748 | this.element.style.backgroundColor = this.originalBackground;
749 | Element.show(this.element);
750 | if (this.options.externalControl) {
751 | Element.show(this.options.externalControl);
752 | }
753 | this.editing = false;
754 | this.saving = false;
755 | this.oldInnerHTML = null;
756 | this.onLeaveEditMode();
757 | },
758 | onComplete: function(transport) {
759 | this.leaveEditMode();
760 | this.options.onComplete.bind(this)(transport, this.element);
761 | },
762 | onEnterEditMode: function() {},
763 | onLeaveEditMode: function() {},
764 | dispose: function() {
765 | if (this.oldInnerHTML) {
766 | this.element.innerHTML = this.oldInnerHTML;
767 | }
768 | this.leaveEditMode();
769 | Event.stopObserving(this.element, 'click', this.onclickListener);
770 | Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
771 | Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
772 | if (this.options.externalControl) {
773 | Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
774 | Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
775 | Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
776 | }
777 | }
778 | };
779 |
780 | Ajax.InPlaceCollectionEditor = Class.create();
781 | Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
782 | Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
783 | createEditField: function() {
784 | if (!this.cached_selectTag) {
785 | var selectTag = document.createElement("select");
786 | var collection = this.options.collection || [];
787 | var optionTag;
788 | collection.each(function(e,i) {
789 | optionTag = document.createElement("option");
790 | optionTag.value = (e instanceof Array) ? e[0] : e;
791 | if((typeof this.options.value == 'undefined') &&
792 | ((e instanceof Array) ? this.element.innerHTML == e[1] : e == optionTag.value)) optionTag.selected = true;
793 | if(this.options.value==optionTag.value) optionTag.selected = true;
794 | optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
795 | selectTag.appendChild(optionTag);
796 | }.bind(this));
797 | this.cached_selectTag = selectTag;
798 | }
799 |
800 | this.editField = this.cached_selectTag;
801 | if(this.options.loadTextURL) this.loadExternalText();
802 | this.form.appendChild(this.editField);
803 | this.options.callback = function(form, value) {
804 | return "value=" + encodeURIComponent(value);
805 | }
806 | }
807 | });
808 |
809 | // Delayed observer, like Form.Element.Observer,
810 | // but waits for delay after last key input
811 | // Ideal for live-search fields
812 |
813 | Form.Element.DelayedObserver = Class.create();
814 | Form.Element.DelayedObserver.prototype = {
815 | initialize: function(element, delay, callback) {
816 | this.delay = delay || 0.5;
817 | this.element = $(element);
818 | this.callback = callback;
819 | this.timer = null;
820 | this.lastValue = $F(this.element);
821 | Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
822 | },
823 | delayedListener: function(event) {
824 | if(this.lastValue == $F(this.element)) return;
825 | if(this.timer) clearTimeout(this.timer);
826 | this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
827 | this.lastValue = $F(this.element);
828 | },
829 | onTimerEvent: function() {
830 | this.timer = null;
831 | this.callback(this.element, $F(this.element));
832 | }
833 | };
834 |
--------------------------------------------------------------------------------
/public/javascripts/dragdrop.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 | // (c) 2005, 2006 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
3 | //
4 | // script.aculo.us is freely distributable under the terms of an MIT-style license.
5 | // For details, see the script.aculo.us web site: http://script.aculo.us/
6 |
7 | if(typeof Effect == 'undefined')
8 | throw("dragdrop.js requires including script.aculo.us' effects.js library");
9 |
10 | var Droppables = {
11 | drops: [],
12 |
13 | remove: function(element) {
14 | this.drops = this.drops.reject(function(d) { return d.element==$(element) });
15 | },
16 |
17 | add: function(element) {
18 | element = $(element);
19 | var options = Object.extend({
20 | greedy: true,
21 | hoverclass: null,
22 | tree: false
23 | }, arguments[1] || {});
24 |
25 | // cache containers
26 | if(options.containment) {
27 | options._containers = [];
28 | var containment = options.containment;
29 | if((typeof containment == 'object') &&
30 | (containment.constructor == Array)) {
31 | containment.each( function(c) { options._containers.push($(c)) });
32 | } else {
33 | options._containers.push($(containment));
34 | }
35 | }
36 |
37 | if(options.accept) options.accept = [options.accept].flatten();
38 |
39 | Element.makePositioned(element); // fix IE
40 | options.element = element;
41 |
42 | this.drops.push(options);
43 | },
44 |
45 | findDeepestChild: function(drops) {
46 | deepest = drops[0];
47 |
48 | for (i = 1; i < drops.length; ++i)
49 | if (Element.isParent(drops[i].element, deepest.element))
50 | deepest = drops[i];
51 |
52 | return deepest;
53 | },
54 |
55 | isContained: function(element, drop) {
56 | var containmentNode;
57 | if(drop.tree) {
58 | containmentNode = element.treeNode;
59 | } else {
60 | containmentNode = element.parentNode;
61 | }
62 | return drop._containers.detect(function(c) { return containmentNode == c });
63 | },
64 |
65 | isAffected: function(point, element, drop) {
66 | return (
67 | (drop.element!=element) &&
68 | ((!drop._containers) ||
69 | this.isContained(element, drop)) &&
70 | ((!drop.accept) ||
71 | (Element.classNames(element).detect(
72 | function(v) { return drop.accept.include(v) } ) )) &&
73 | Position.within(drop.element, point[0], point[1]) );
74 | },
75 |
76 | deactivate: function(drop) {
77 | if(drop.hoverclass)
78 | Element.removeClassName(drop.element, drop.hoverclass);
79 | this.last_active = null;
80 | },
81 |
82 | activate: function(drop) {
83 | if(drop.hoverclass)
84 | Element.addClassName(drop.element, drop.hoverclass);
85 | this.last_active = drop;
86 | },
87 |
88 | show: function(point, element) {
89 | if(!this.drops.length) return;
90 | var affected = [];
91 |
92 | if(this.last_active) this.deactivate(this.last_active);
93 | this.drops.each( function(drop) {
94 | if(Droppables.isAffected(point, element, drop))
95 | affected.push(drop);
96 | });
97 |
98 | if(affected.length>0) {
99 | drop = Droppables.findDeepestChild(affected);
100 | Position.within(drop.element, point[0], point[1]);
101 | if(drop.onHover)
102 | drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
103 |
104 | Droppables.activate(drop);
105 | }
106 | },
107 |
108 | fire: function(event, element) {
109 | if(!this.last_active) return;
110 | Position.prepare();
111 |
112 | if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
113 | if (this.last_active.onDrop)
114 | this.last_active.onDrop(element, this.last_active.element, event);
115 | },
116 |
117 | reset: function() {
118 | if(this.last_active)
119 | this.deactivate(this.last_active);
120 | }
121 | }
122 |
123 | var Draggables = {
124 | drags: [],
125 | observers: [],
126 |
127 | register: function(draggable) {
128 | if(this.drags.length == 0) {
129 | this.eventMouseUp = this.endDrag.bindAsEventListener(this);
130 | this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
131 | this.eventKeypress = this.keyPress.bindAsEventListener(this);
132 |
133 | Event.observe(document, "mouseup", this.eventMouseUp);
134 | Event.observe(document, "mousemove", this.eventMouseMove);
135 | Event.observe(document, "keypress", this.eventKeypress);
136 | }
137 | this.drags.push(draggable);
138 | },
139 |
140 | unregister: function(draggable) {
141 | this.drags = this.drags.reject(function(d) { return d==draggable });
142 | if(this.drags.length == 0) {
143 | Event.stopObserving(document, "mouseup", this.eventMouseUp);
144 | Event.stopObserving(document, "mousemove", this.eventMouseMove);
145 | Event.stopObserving(document, "keypress", this.eventKeypress);
146 | }
147 | },
148 |
149 | activate: function(draggable) {
150 | if(draggable.options.delay) {
151 | this._timeout = setTimeout(function() {
152 | Draggables._timeout = null;
153 | window.focus();
154 | Draggables.activeDraggable = draggable;
155 | }.bind(this), draggable.options.delay);
156 | } else {
157 | window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
158 | this.activeDraggable = draggable;
159 | }
160 | },
161 |
162 | deactivate: function() {
163 | this.activeDraggable = null;
164 | },
165 |
166 | updateDrag: function(event) {
167 | if(!this.activeDraggable) return;
168 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
169 | // Mozilla-based browsers fire successive mousemove events with
170 | // the same coordinates, prevent needless redrawing (moz bug?)
171 | if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
172 | this._lastPointer = pointer;
173 |
174 | this.activeDraggable.updateDrag(event, pointer);
175 | },
176 |
177 | endDrag: function(event) {
178 | if(this._timeout) {
179 | clearTimeout(this._timeout);
180 | this._timeout = null;
181 | }
182 | if(!this.activeDraggable) return;
183 | this._lastPointer = null;
184 | this.activeDraggable.endDrag(event);
185 | this.activeDraggable = null;
186 | },
187 |
188 | keyPress: function(event) {
189 | if(this.activeDraggable)
190 | this.activeDraggable.keyPress(event);
191 | },
192 |
193 | addObserver: function(observer) {
194 | this.observers.push(observer);
195 | this._cacheObserverCallbacks();
196 | },
197 |
198 | removeObserver: function(element) { // element instead of observer fixes mem leaks
199 | this.observers = this.observers.reject( function(o) { return o.element==element });
200 | this._cacheObserverCallbacks();
201 | },
202 |
203 | notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
204 | if(this[eventName+'Count'] > 0)
205 | this.observers.each( function(o) {
206 | if(o[eventName]) o[eventName](eventName, draggable, event);
207 | });
208 | if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
209 | },
210 |
211 | _cacheObserverCallbacks: function() {
212 | ['onStart','onEnd','onDrag'].each( function(eventName) {
213 | Draggables[eventName+'Count'] = Draggables.observers.select(
214 | function(o) { return o[eventName]; }
215 | ).length;
216 | });
217 | }
218 | }
219 |
220 | /*--------------------------------------------------------------------------*/
221 |
222 | var Draggable = Class.create();
223 | Draggable._dragging = {};
224 |
225 | Draggable.prototype = {
226 | initialize: function(element) {
227 | var defaults = {
228 | handle: false,
229 | reverteffect: function(element, top_offset, left_offset) {
230 | var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
231 | new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
232 | queue: {scope:'_draggable', position:'end'}
233 | });
234 | },
235 | endeffect: function(element) {
236 | var toOpacity = typeof element._opacity == 'number' ? element._opacity : 1.0;
237 | new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
238 | queue: {scope:'_draggable', position:'end'},
239 | afterFinish: function(){
240 | Draggable._dragging[element] = false
241 | }
242 | });
243 | },
244 | zindex: 1000,
245 | revert: false,
246 | scroll: false,
247 | scrollSensitivity: 20,
248 | scrollSpeed: 15,
249 | snap: false, // false, or xy or [x,y] or function(x,y){ return [x,y] }
250 | delay: 0
251 | };
252 |
253 | if(!arguments[1] || typeof arguments[1].endeffect == 'undefined')
254 | Object.extend(defaults, {
255 | starteffect: function(element) {
256 | element._opacity = Element.getOpacity(element);
257 | Draggable._dragging[element] = true;
258 | new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
259 | }
260 | });
261 |
262 | var options = Object.extend(defaults, arguments[1] || {});
263 |
264 | this.element = $(element);
265 |
266 | if(options.handle && (typeof options.handle == 'string'))
267 | this.handle = this.element.down('.'+options.handle, 0);
268 |
269 | if(!this.handle) this.handle = $(options.handle);
270 | if(!this.handle) this.handle = this.element;
271 |
272 | if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
273 | options.scroll = $(options.scroll);
274 | this._isScrollChild = Element.childOf(this.element, options.scroll);
275 | }
276 |
277 | Element.makePositioned(this.element); // fix IE
278 |
279 | this.delta = this.currentDelta();
280 | this.options = options;
281 | this.dragging = false;
282 |
283 | this.eventMouseDown = this.initDrag.bindAsEventListener(this);
284 | Event.observe(this.handle, "mousedown", this.eventMouseDown);
285 |
286 | Draggables.register(this);
287 | },
288 |
289 | destroy: function() {
290 | Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
291 | Draggables.unregister(this);
292 | },
293 |
294 | currentDelta: function() {
295 | return([
296 | parseInt(Element.getStyle(this.element,'left') || '0'),
297 | parseInt(Element.getStyle(this.element,'top') || '0')]);
298 | },
299 |
300 | initDrag: function(event) {
301 | if(typeof Draggable._dragging[this.element] != 'undefined' &&
302 | Draggable._dragging[this.element]) return;
303 | if(Event.isLeftClick(event)) {
304 | // abort on form elements, fixes a Firefox issue
305 | var src = Event.element(event);
306 | if(src.tagName && (
307 | src.tagName=='INPUT' ||
308 | src.tagName=='SELECT' ||
309 | src.tagName=='OPTION' ||
310 | src.tagName=='BUTTON' ||
311 | src.tagName=='TEXTAREA')) return;
312 |
313 | var pointer = [Event.pointerX(event), Event.pointerY(event)];
314 | var pos = Position.cumulativeOffset(this.element);
315 | this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
316 |
317 | Draggables.activate(this);
318 | Event.stop(event);
319 | }
320 | },
321 |
322 | startDrag: function(event) {
323 | this.dragging = true;
324 |
325 | if(this.options.zindex) {
326 | this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
327 | this.element.style.zIndex = this.options.zindex;
328 | }
329 |
330 | if(this.options.ghosting) {
331 | this._clone = this.element.cloneNode(true);
332 | Position.absolutize(this.element);
333 | this.element.parentNode.insertBefore(this._clone, this.element);
334 | }
335 |
336 | if(this.options.scroll) {
337 | if (this.options.scroll == window) {
338 | var where = this._getWindowScroll(this.options.scroll);
339 | this.originalScrollLeft = where.left;
340 | this.originalScrollTop = where.top;
341 | } else {
342 | this.originalScrollLeft = this.options.scroll.scrollLeft;
343 | this.originalScrollTop = this.options.scroll.scrollTop;
344 | }
345 | }
346 |
347 | Draggables.notify('onStart', this, event);
348 |
349 | if(this.options.starteffect) this.options.starteffect(this.element);
350 | },
351 |
352 | updateDrag: function(event, pointer) {
353 | if(!this.dragging) this.startDrag(event);
354 | Position.prepare();
355 | Droppables.show(pointer, this.element);
356 | Draggables.notify('onDrag', this, event);
357 |
358 | this.draw(pointer);
359 | if(this.options.change) this.options.change(this);
360 |
361 | if(this.options.scroll) {
362 | this.stopScrolling();
363 |
364 | var p;
365 | if (this.options.scroll == window) {
366 | with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
367 | } else {
368 | p = Position.page(this.options.scroll);
369 | p[0] += this.options.scroll.scrollLeft + Position.deltaX;
370 | p[1] += this.options.scroll.scrollTop + Position.deltaY;
371 | p.push(p[0]+this.options.scroll.offsetWidth);
372 | p.push(p[1]+this.options.scroll.offsetHeight);
373 | }
374 | var speed = [0,0];
375 | if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
376 | if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
377 | if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
378 | if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
379 | this.startScrolling(speed);
380 | }
381 |
382 | // fix AppleWebKit rendering
383 | if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
384 |
385 | Event.stop(event);
386 | },
387 |
388 | finishDrag: function(event, success) {
389 | this.dragging = false;
390 |
391 | if(this.options.ghosting) {
392 | Position.relativize(this.element);
393 | Element.remove(this._clone);
394 | this._clone = null;
395 | }
396 |
397 | if(success) Droppables.fire(event, this.element);
398 | Draggables.notify('onEnd', this, event);
399 |
400 | var revert = this.options.revert;
401 | if(revert && typeof revert == 'function') revert = revert(this.element);
402 |
403 | var d = this.currentDelta();
404 | if(revert && this.options.reverteffect) {
405 | this.options.reverteffect(this.element,
406 | d[1]-this.delta[1], d[0]-this.delta[0]);
407 | } else {
408 | this.delta = d;
409 | }
410 |
411 | if(this.options.zindex)
412 | this.element.style.zIndex = this.originalZ;
413 |
414 | if(this.options.endeffect)
415 | this.options.endeffect(this.element);
416 |
417 | Draggables.deactivate(this);
418 | Droppables.reset();
419 | },
420 |
421 | keyPress: function(event) {
422 | if(event.keyCode!=Event.KEY_ESC) return;
423 | this.finishDrag(event, false);
424 | Event.stop(event);
425 | },
426 |
427 | endDrag: function(event) {
428 | if(!this.dragging) return;
429 | this.stopScrolling();
430 | this.finishDrag(event, true);
431 | Event.stop(event);
432 | },
433 |
434 | draw: function(point) {
435 | var pos = Position.cumulativeOffset(this.element);
436 | if(this.options.ghosting) {
437 | var r = Position.realOffset(this.element);
438 | pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
439 | }
440 |
441 | var d = this.currentDelta();
442 | pos[0] -= d[0]; pos[1] -= d[1];
443 |
444 | if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
445 | pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
446 | pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
447 | }
448 |
449 | var p = [0,1].map(function(i){
450 | return (point[i]-pos[i]-this.offset[i])
451 | }.bind(this));
452 |
453 | if(this.options.snap) {
454 | if(typeof this.options.snap == 'function') {
455 | p = this.options.snap(p[0],p[1],this);
456 | } else {
457 | if(this.options.snap instanceof Array) {
458 | p = p.map( function(v, i) {
459 | return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
460 | } else {
461 | p = p.map( function(v) {
462 | return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
463 | }
464 | }}
465 |
466 | var style = this.element.style;
467 | if((!this.options.constraint) || (this.options.constraint=='horizontal'))
468 | style.left = p[0] + "px";
469 | if((!this.options.constraint) || (this.options.constraint=='vertical'))
470 | style.top = p[1] + "px";
471 |
472 | if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
473 | },
474 |
475 | stopScrolling: function() {
476 | if(this.scrollInterval) {
477 | clearInterval(this.scrollInterval);
478 | this.scrollInterval = null;
479 | Draggables._lastScrollPointer = null;
480 | }
481 | },
482 |
483 | startScrolling: function(speed) {
484 | if(!(speed[0] || speed[1])) return;
485 | this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
486 | this.lastScrolled = new Date();
487 | this.scrollInterval = setInterval(this.scroll.bind(this), 10);
488 | },
489 |
490 | scroll: function() {
491 | var current = new Date();
492 | var delta = current - this.lastScrolled;
493 | this.lastScrolled = current;
494 | if(this.options.scroll == window) {
495 | with (this._getWindowScroll(this.options.scroll)) {
496 | if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
497 | var d = delta / 1000;
498 | this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
499 | }
500 | }
501 | } else {
502 | this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
503 | this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
504 | }
505 |
506 | Position.prepare();
507 | Droppables.show(Draggables._lastPointer, this.element);
508 | Draggables.notify('onDrag', this);
509 | if (this._isScrollChild) {
510 | Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
511 | Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
512 | Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
513 | if (Draggables._lastScrollPointer[0] < 0)
514 | Draggables._lastScrollPointer[0] = 0;
515 | if (Draggables._lastScrollPointer[1] < 0)
516 | Draggables._lastScrollPointer[1] = 0;
517 | this.draw(Draggables._lastScrollPointer);
518 | }
519 |
520 | if(this.options.change) this.options.change(this);
521 | },
522 |
523 | _getWindowScroll: function(w) {
524 | var T, L, W, H;
525 | with (w.document) {
526 | if (w.document.documentElement && documentElement.scrollTop) {
527 | T = documentElement.scrollTop;
528 | L = documentElement.scrollLeft;
529 | } else if (w.document.body) {
530 | T = body.scrollTop;
531 | L = body.scrollLeft;
532 | }
533 | if (w.innerWidth) {
534 | W = w.innerWidth;
535 | H = w.innerHeight;
536 | } else if (w.document.documentElement && documentElement.clientWidth) {
537 | W = documentElement.clientWidth;
538 | H = documentElement.clientHeight;
539 | } else {
540 | W = body.offsetWidth;
541 | H = body.offsetHeight
542 | }
543 | }
544 | return { top: T, left: L, width: W, height: H };
545 | }
546 | }
547 |
548 | /*--------------------------------------------------------------------------*/
549 |
550 | var SortableObserver = Class.create();
551 | SortableObserver.prototype = {
552 | initialize: function(element, observer) {
553 | this.element = $(element);
554 | this.observer = observer;
555 | this.lastValue = Sortable.serialize(this.element);
556 | },
557 |
558 | onStart: function() {
559 | this.lastValue = Sortable.serialize(this.element);
560 | },
561 |
562 | onEnd: function() {
563 | Sortable.unmark();
564 | if(this.lastValue != Sortable.serialize(this.element))
565 | this.observer(this.element)
566 | }
567 | }
568 |
569 | var Sortable = {
570 | SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,
571 |
572 | sortables: {},
573 |
574 | _findRootElement: function(element) {
575 | while (element.tagName != "BODY") {
576 | if(element.id && Sortable.sortables[element.id]) return element;
577 | element = element.parentNode;
578 | }
579 | },
580 |
581 | options: function(element) {
582 | element = Sortable._findRootElement($(element));
583 | if(!element) return;
584 | return Sortable.sortables[element.id];
585 | },
586 |
587 | destroy: function(element){
588 | var s = Sortable.options(element);
589 |
590 | if(s) {
591 | Draggables.removeObserver(s.element);
592 | s.droppables.each(function(d){ Droppables.remove(d) });
593 | s.draggables.invoke('destroy');
594 |
595 | delete Sortable.sortables[s.element.id];
596 | }
597 | },
598 |
599 | create: function(element) {
600 | element = $(element);
601 | var options = Object.extend({
602 | element: element,
603 | tag: 'li', // assumes li children, override with tag: 'tagname'
604 | dropOnEmpty: false,
605 | tree: false,
606 | treeTag: 'ul',
607 | overlap: 'vertical', // one of 'vertical', 'horizontal'
608 | constraint: 'vertical', // one of 'vertical', 'horizontal', false
609 | containment: element, // also takes array of elements (or id's); or false
610 | handle: false, // or a CSS class
611 | only: false,
612 | delay: 0,
613 | hoverclass: null,
614 | ghosting: false,
615 | scroll: false,
616 | scrollSensitivity: 20,
617 | scrollSpeed: 15,
618 | format: this.SERIALIZE_RULE,
619 | onChange: Prototype.emptyFunction,
620 | onUpdate: Prototype.emptyFunction
621 | }, arguments[1] || {});
622 |
623 | // clear any old sortable with same element
624 | this.destroy(element);
625 |
626 | // build options for the draggables
627 | var options_for_draggable = {
628 | revert: true,
629 | scroll: options.scroll,
630 | scrollSpeed: options.scrollSpeed,
631 | scrollSensitivity: options.scrollSensitivity,
632 | delay: options.delay,
633 | ghosting: options.ghosting,
634 | constraint: options.constraint,
635 | handle: options.handle };
636 |
637 | if(options.starteffect)
638 | options_for_draggable.starteffect = options.starteffect;
639 |
640 | if(options.reverteffect)
641 | options_for_draggable.reverteffect = options.reverteffect;
642 | else
643 | if(options.ghosting) options_for_draggable.reverteffect = function(element) {
644 | element.style.top = 0;
645 | element.style.left = 0;
646 | };
647 |
648 | if(options.endeffect)
649 | options_for_draggable.endeffect = options.endeffect;
650 |
651 | if(options.zindex)
652 | options_for_draggable.zindex = options.zindex;
653 |
654 | // build options for the droppables
655 | var options_for_droppable = {
656 | overlap: options.overlap,
657 | containment: options.containment,
658 | tree: options.tree,
659 | hoverclass: options.hoverclass,
660 | onHover: Sortable.onHover
661 | }
662 |
663 | var options_for_tree = {
664 | onHover: Sortable.onEmptyHover,
665 | overlap: options.overlap,
666 | containment: options.containment,
667 | hoverclass: options.hoverclass
668 | }
669 |
670 | // fix for gecko engine
671 | Element.cleanWhitespace(element);
672 |
673 | options.draggables = [];
674 | options.droppables = [];
675 |
676 | // drop on empty handling
677 | if(options.dropOnEmpty || options.tree) {
678 | Droppables.add(element, options_for_tree);
679 | options.droppables.push(element);
680 | }
681 |
682 | (this.findElements(element, options) || []).each( function(e) {
683 | // handles are per-draggable
684 | var handle = options.handle ?
685 | $(e).down('.'+options.handle,0) : e;
686 | options.draggables.push(
687 | new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
688 | Droppables.add(e, options_for_droppable);
689 | if(options.tree) e.treeNode = element;
690 | options.droppables.push(e);
691 | });
692 |
693 | if(options.tree) {
694 | (Sortable.findTreeElements(element, options) || []).each( function(e) {
695 | Droppables.add(e, options_for_tree);
696 | e.treeNode = element;
697 | options.droppables.push(e);
698 | });
699 | }
700 |
701 | // keep reference
702 | this.sortables[element.id] = options;
703 |
704 | // for onupdate
705 | Draggables.addObserver(new SortableObserver(element, options.onUpdate));
706 |
707 | },
708 |
709 | // return all suitable-for-sortable elements in a guaranteed order
710 | findElements: function(element, options) {
711 | return Element.findChildren(
712 | element, options.only, options.tree ? true : false, options.tag);
713 | },
714 |
715 | findTreeElements: function(element, options) {
716 | return Element.findChildren(
717 | element, options.only, options.tree ? true : false, options.treeTag);
718 | },
719 |
720 | onHover: function(element, dropon, overlap) {
721 | if(Element.isParent(dropon, element)) return;
722 |
723 | if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
724 | return;
725 | } else if(overlap>0.5) {
726 | Sortable.mark(dropon, 'before');
727 | if(dropon.previousSibling != element) {
728 | var oldParentNode = element.parentNode;
729 | element.style.visibility = "hidden"; // fix gecko rendering
730 | dropon.parentNode.insertBefore(element, dropon);
731 | if(dropon.parentNode!=oldParentNode)
732 | Sortable.options(oldParentNode).onChange(element);
733 | Sortable.options(dropon.parentNode).onChange(element);
734 | }
735 | } else {
736 | Sortable.mark(dropon, 'after');
737 | var nextElement = dropon.nextSibling || null;
738 | if(nextElement != element) {
739 | var oldParentNode = element.parentNode;
740 | element.style.visibility = "hidden"; // fix gecko rendering
741 | dropon.parentNode.insertBefore(element, nextElement);
742 | if(dropon.parentNode!=oldParentNode)
743 | Sortable.options(oldParentNode).onChange(element);
744 | Sortable.options(dropon.parentNode).onChange(element);
745 | }
746 | }
747 | },
748 |
749 | onEmptyHover: function(element, dropon, overlap) {
750 | var oldParentNode = element.parentNode;
751 | var droponOptions = Sortable.options(dropon);
752 |
753 | if(!Element.isParent(dropon, element)) {
754 | var index;
755 |
756 | var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
757 | var child = null;
758 |
759 | if(children) {
760 | var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
761 |
762 | for (index = 0; index < children.length; index += 1) {
763 | if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
764 | offset -= Element.offsetSize (children[index], droponOptions.overlap);
765 | } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
766 | child = index + 1 < children.length ? children[index + 1] : null;
767 | break;
768 | } else {
769 | child = children[index];
770 | break;
771 | }
772 | }
773 | }
774 |
775 | dropon.insertBefore(element, child);
776 |
777 | Sortable.options(oldParentNode).onChange(element);
778 | droponOptions.onChange(element);
779 | }
780 | },
781 |
782 | unmark: function() {
783 | if(Sortable._marker) Sortable._marker.hide();
784 | },
785 |
786 | mark: function(dropon, position) {
787 | // mark on ghosting only
788 | var sortable = Sortable.options(dropon.parentNode);
789 | if(sortable && !sortable.ghosting) return;
790 |
791 | if(!Sortable._marker) {
792 | Sortable._marker =
793 | ($('dropmarker') || Element.extend(document.createElement('DIV'))).
794 | hide().addClassName('dropmarker').setStyle({position:'absolute'});
795 | document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
796 | }
797 | var offsets = Position.cumulativeOffset(dropon);
798 | Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});
799 |
800 | if(position=='after')
801 | if(sortable.overlap == 'horizontal')
802 | Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
803 | else
804 | Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});
805 |
806 | Sortable._marker.show();
807 | },
808 |
809 | _tree: function(element, options, parent) {
810 | var children = Sortable.findElements(element, options) || [];
811 |
812 | for (var i = 0; i < children.length; ++i) {
813 | var match = children[i].id.match(options.format);
814 |
815 | if (!match) continue;
816 |
817 | var child = {
818 | id: encodeURIComponent(match ? match[1] : null),
819 | element: element,
820 | parent: parent,
821 | children: [],
822 | position: parent.children.length,
823 | container: $(children[i]).down(options.treeTag)
824 | }
825 |
826 | /* Get the element containing the children and recurse over it */
827 | if (child.container)
828 | this._tree(child.container, options, child)
829 |
830 | parent.children.push (child);
831 | }
832 |
833 | return parent;
834 | },
835 |
836 | tree: function(element) {
837 | element = $(element);
838 | var sortableOptions = this.options(element);
839 | var options = Object.extend({
840 | tag: sortableOptions.tag,
841 | treeTag: sortableOptions.treeTag,
842 | only: sortableOptions.only,
843 | name: element.id,
844 | format: sortableOptions.format
845 | }, arguments[1] || {});
846 |
847 | var root = {
848 | id: null,
849 | parent: null,
850 | children: [],
851 | container: element,
852 | position: 0
853 | }
854 |
855 | return Sortable._tree(element, options, root);
856 | },
857 |
858 | /* Construct a [i] index for a particular node */
859 | _constructIndex: function(node) {
860 | var index = '';
861 | do {
862 | if (node.id) index = '[' + node.position + ']' + index;
863 | } while ((node = node.parent) != null);
864 | return index;
865 | },
866 |
867 | sequence: function(element) {
868 | element = $(element);
869 | var options = Object.extend(this.options(element), arguments[1] || {});
870 |
871 | return $(this.findElements(element, options) || []).map( function(item) {
872 | return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
873 | });
874 | },
875 |
876 | setSequence: function(element, new_sequence) {
877 | element = $(element);
878 | var options = Object.extend(this.options(element), arguments[2] || {});
879 |
880 | var nodeMap = {};
881 | this.findElements(element, options).each( function(n) {
882 | if (n.id.match(options.format))
883 | nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
884 | n.parentNode.removeChild(n);
885 | });
886 |
887 | new_sequence.each(function(ident) {
888 | var n = nodeMap[ident];
889 | if (n) {
890 | n[1].appendChild(n[0]);
891 | delete nodeMap[ident];
892 | }
893 | });
894 | },
895 |
896 | serialize: function(element) {
897 | element = $(element);
898 | var options = Object.extend(Sortable.options(element), arguments[1] || {});
899 | var name = encodeURIComponent(
900 | (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
901 |
902 | if (options.tree) {
903 | return Sortable.tree(element, arguments[1]).children.map( function (item) {
904 | return [name + Sortable._constructIndex(item) + "[id]=" +
905 | encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
906 | }).flatten().join('&');
907 | } else {
908 | return Sortable.sequence(element, arguments[1]).map( function(item) {
909 | return name + "[]=" + encodeURIComponent(item);
910 | }).join('&');
911 | }
912 | }
913 | }
914 |
915 | // Returns true if child is contained within element
916 | Element.isParent = function(child, element) {
917 | if (!child.parentNode || child == element) return false;
918 | if (child.parentNode == element) return true;
919 | return Element.isParent(child.parentNode, element);
920 | }
921 |
922 | Element.findChildren = function(element, only, recursive, tagName) {
923 | if(!element.hasChildNodes()) return null;
924 | tagName = tagName.toUpperCase();
925 | if(only) only = [only].flatten();
926 | var elements = [];
927 | $A(element.childNodes).each( function(e) {
928 | if(e.tagName && e.tagName.toUpperCase()==tagName &&
929 | (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
930 | elements.push(e);
931 | if(recursive) {
932 | var grandchildren = Element.findChildren(e, only, recursive, tagName);
933 | if(grandchildren) elements.push(grandchildren);
934 | }
935 | });
936 |
937 | return (elements.length>0 ? elements.flatten() : []);
938 | }
939 |
940 | Element.offsetSize = function (element, type) {
941 | return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
942 | }
943 |
--------------------------------------------------------------------------------
/public/javascripts/effects.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2005, 2006 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2 | // Contributors:
3 | // Justin Palmer (http://encytemedia.com/)
4 | // Mark Pilgrim (http://diveintomark.org/)
5 | // Martin Bialasinki
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 | // converts rgb() and #xxx to #xxxxxx format,
11 | // returns self (or first argument) if not convertable
12 | String.prototype.parseColor = function() {
13 | var color = '#';
14 | if(this.slice(0,4) == 'rgb(') {
15 | var cols = this.slice(4,this.length-1).split(',');
16 | var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
17 | } else {
18 | if(this.slice(0,1) == '#') {
19 | if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
20 | if(this.length==7) color = this.toLowerCase();
21 | }
22 | }
23 | return(color.length==7 ? color : (arguments[0] || this));
24 | }
25 |
26 | /*--------------------------------------------------------------------------*/
27 |
28 | Element.collectTextNodes = function(element) {
29 | return $A($(element).childNodes).collect( function(node) {
30 | return (node.nodeType==3 ? node.nodeValue :
31 | (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
32 | }).flatten().join('');
33 | }
34 |
35 | Element.collectTextNodesIgnoreClass = function(element, className) {
36 | return $A($(element).childNodes).collect( function(node) {
37 | return (node.nodeType==3 ? node.nodeValue :
38 | ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
39 | Element.collectTextNodesIgnoreClass(node, className) : ''));
40 | }).flatten().join('');
41 | }
42 |
43 | Element.setContentZoom = function(element, percent) {
44 | element = $(element);
45 | element.setStyle({fontSize: (percent/100) + 'em'});
46 | if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
47 | return element;
48 | }
49 |
50 | Element.getOpacity = function(element){
51 | element = $(element);
52 | var opacity;
53 | if (opacity = element.getStyle('opacity'))
54 | return parseFloat(opacity);
55 | if (opacity = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
56 | if(opacity[1]) return parseFloat(opacity[1]) / 100;
57 | return 1.0;
58 | }
59 |
60 | Element.setOpacity = function(element, value){
61 | element= $(element);
62 | if (value == 1){
63 | element.setStyle({ opacity:
64 | (/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
65 | 0.999999 : 1.0 });
66 | if(/MSIE/.test(navigator.userAgent) && !window.opera)
67 | element.setStyle({filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
68 | } else {
69 | if(value < 0.00001) value = 0;
70 | element.setStyle({opacity: value});
71 | if(/MSIE/.test(navigator.userAgent) && !window.opera)
72 | element.setStyle(
73 | { filter: element.getStyle('filter').replace(/alpha\([^\)]*\)/gi,'') +
74 | 'alpha(opacity='+value*100+')' });
75 | }
76 | return element;
77 | }
78 |
79 | Element.getInlineOpacity = function(element){
80 | return $(element).style.opacity || '';
81 | }
82 |
83 | Element.forceRerendering = function(element) {
84 | try {
85 | element = $(element);
86 | var n = document.createTextNode(' ');
87 | element.appendChild(n);
88 | element.removeChild(n);
89 | } catch(e) { }
90 | };
91 |
92 | /*--------------------------------------------------------------------------*/
93 |
94 | Array.prototype.call = function() {
95 | var args = arguments;
96 | this.each(function(f){ f.apply(this, args) });
97 | }
98 |
99 | /*--------------------------------------------------------------------------*/
100 |
101 | var Effect = {
102 | _elementDoesNotExistError: {
103 | name: 'ElementDoesNotExistError',
104 | message: 'The specified DOM element does not exist, but is required for this effect to operate'
105 | },
106 | tagifyText: function(element) {
107 | if(typeof Builder == 'undefined')
108 | throw("Effect.tagifyText requires including script.aculo.us' builder.js library");
109 |
110 | var tagifyStyle = 'position:relative';
111 | if(/MSIE/.test(navigator.userAgent) && !window.opera) tagifyStyle += ';zoom:1';
112 |
113 | element = $(element);
114 | $A(element.childNodes).each( function(child) {
115 | if(child.nodeType==3) {
116 | child.nodeValue.toArray().each( function(character) {
117 | element.insertBefore(
118 | Builder.node('span',{style: tagifyStyle},
119 | character == ' ' ? String.fromCharCode(160) : character),
120 | child);
121 | });
122 | Element.remove(child);
123 | }
124 | });
125 | },
126 | multiple: function(element, effect) {
127 | var elements;
128 | if(((typeof element == 'object') ||
129 | (typeof element == 'function')) &&
130 | (element.length))
131 | elements = element;
132 | else
133 | elements = $(element).childNodes;
134 |
135 | var options = Object.extend({
136 | speed: 0.1,
137 | delay: 0.0
138 | }, arguments[2] || {});
139 | var masterDelay = options.delay;
140 |
141 | $A(elements).each( function(element, index) {
142 | new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
143 | });
144 | },
145 | PAIRS: {
146 | 'slide': ['SlideDown','SlideUp'],
147 | 'blind': ['BlindDown','BlindUp'],
148 | 'appear': ['Appear','Fade']
149 | },
150 | toggle: function(element, effect) {
151 | element = $(element);
152 | effect = (effect || 'appear').toLowerCase();
153 | var options = Object.extend({
154 | queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
155 | }, arguments[2] || {});
156 | Effect[element.visible() ?
157 | Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
158 | }
159 | };
160 |
161 | var Effect2 = Effect; // deprecated
162 |
163 | /* ------------- transitions ------------- */
164 |
165 | Effect.Transitions = {
166 | linear: Prototype.K,
167 | sinoidal: function(pos) {
168 | return (-Math.cos(pos*Math.PI)/2) + 0.5;
169 | },
170 | reverse: function(pos) {
171 | return 1-pos;
172 | },
173 | flicker: function(pos) {
174 | return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
175 | },
176 | wobble: function(pos) {
177 | return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
178 | },
179 | pulse: function(pos, pulses) {
180 | pulses = pulses || 5;
181 | return (
182 | Math.round((pos % (1/pulses)) * pulses) == 0 ?
183 | ((pos * pulses * 2) - Math.floor(pos * pulses * 2)) :
184 | 1 - ((pos * pulses * 2) - Math.floor(pos * pulses * 2))
185 | );
186 | },
187 | none: function(pos) {
188 | return 0;
189 | },
190 | full: function(pos) {
191 | return 1;
192 | }
193 | };
194 |
195 | /* ------------- core effects ------------- */
196 |
197 | Effect.ScopedQueue = Class.create();
198 | Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
199 | initialize: function() {
200 | this.effects = [];
201 | this.interval = null;
202 | },
203 | _each: function(iterator) {
204 | this.effects._each(iterator);
205 | },
206 | add: function(effect) {
207 | var timestamp = new Date().getTime();
208 |
209 | var position = (typeof effect.options.queue == 'string') ?
210 | effect.options.queue : effect.options.queue.position;
211 |
212 | switch(position) {
213 | case 'front':
214 | // move unstarted effects after this effect
215 | this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
216 | e.startOn += effect.finishOn;
217 | e.finishOn += effect.finishOn;
218 | });
219 | break;
220 | case 'with-last':
221 | timestamp = this.effects.pluck('startOn').max() || timestamp;
222 | break;
223 | case 'end':
224 | // start effect after last queued effect has finished
225 | timestamp = this.effects.pluck('finishOn').max() || timestamp;
226 | break;
227 | }
228 |
229 | effect.startOn += timestamp;
230 | effect.finishOn += timestamp;
231 |
232 | if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
233 | this.effects.push(effect);
234 |
235 | if(!this.interval)
236 | this.interval = setInterval(this.loop.bind(this), 40);
237 | },
238 | remove: function(effect) {
239 | this.effects = this.effects.reject(function(e) { return e==effect });
240 | if(this.effects.length == 0) {
241 | clearInterval(this.interval);
242 | this.interval = null;
243 | }
244 | },
245 | loop: function() {
246 | var timePos = new Date().getTime();
247 | this.effects.invoke('loop', timePos);
248 | }
249 | });
250 |
251 | Effect.Queues = {
252 | instances: $H(),
253 | get: function(queueName) {
254 | if(typeof queueName != 'string') return queueName;
255 |
256 | if(!this.instances[queueName])
257 | this.instances[queueName] = new Effect.ScopedQueue();
258 |
259 | return this.instances[queueName];
260 | }
261 | }
262 | Effect.Queue = Effect.Queues.get('global');
263 |
264 | Effect.DefaultOptions = {
265 | transition: Effect.Transitions.sinoidal,
266 | duration: 1.0, // seconds
267 | fps: 25.0, // max. 25fps due to Effect.Queue implementation
268 | sync: false, // true for combining
269 | from: 0.0,
270 | to: 1.0,
271 | delay: 0.0,
272 | queue: 'parallel'
273 | }
274 |
275 | Effect.Base = function() {};
276 | Effect.Base.prototype = {
277 | position: null,
278 | start: function(options) {
279 | this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
280 | this.currentFrame = 0;
281 | this.state = 'idle';
282 | this.startOn = this.options.delay*1000;
283 | this.finishOn = this.startOn + (this.options.duration*1000);
284 | this.event('beforeStart');
285 | if(!this.options.sync)
286 | Effect.Queues.get(typeof this.options.queue == 'string' ?
287 | 'global' : this.options.queue.scope).add(this);
288 | },
289 | loop: function(timePos) {
290 | if(timePos >= this.startOn) {
291 | if(timePos >= this.finishOn) {
292 | this.render(1.0);
293 | this.cancel();
294 | this.event('beforeFinish');
295 | if(this.finish) this.finish();
296 | this.event('afterFinish');
297 | return;
298 | }
299 | var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
300 | var frame = Math.round(pos * this.options.fps * this.options.duration);
301 | if(frame > this.currentFrame) {
302 | this.render(pos);
303 | this.currentFrame = frame;
304 | }
305 | }
306 | },
307 | render: function(pos) {
308 | if(this.state == 'idle') {
309 | this.state = 'running';
310 | this.event('beforeSetup');
311 | if(this.setup) this.setup();
312 | this.event('afterSetup');
313 | }
314 | if(this.state == 'running') {
315 | if(this.options.transition) pos = this.options.transition(pos);
316 | pos *= (this.options.to-this.options.from);
317 | pos += this.options.from;
318 | this.position = pos;
319 | this.event('beforeUpdate');
320 | if(this.update) this.update(pos);
321 | this.event('afterUpdate');
322 | }
323 | },
324 | cancel: function() {
325 | if(!this.options.sync)
326 | Effect.Queues.get(typeof this.options.queue == 'string' ?
327 | 'global' : this.options.queue.scope).remove(this);
328 | this.state = 'finished';
329 | },
330 | event: function(eventName) {
331 | if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
332 | if(this.options[eventName]) this.options[eventName](this);
333 | },
334 | inspect: function() {
335 | return '#