├── README ├── static ├── img │ ├── spinner.gif │ ├── Screenshot.png │ └── example_image.jpeg ├── README.rst ├── alto_edit │ ├── js │ │ ├── interface.js │ │ ├── image_client_jp2.js │ │ ├── seg_edit.js │ │ ├── alto_view.js │ │ └── effects.js │ └── index.html └── example.xml ├── json_alto ├── README.rst ├── src │ └── simple_get_response.rb └── web.rb ├── cropper ├── README.rst ├── web.rb └── simple_get_response.rb └── README.rst /README: -------------------------------------------------------------------------------- 1 | placeholder template 2 | -------------------------------------------------------------------------------- /static/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KBNLresearch/alto-editor/HEAD/static/img/spinner.gif -------------------------------------------------------------------------------- /static/img/Screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KBNLresearch/alto-editor/HEAD/static/img/Screenshot.png -------------------------------------------------------------------------------- /static/img/example_image.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KBNLresearch/alto-editor/HEAD/static/img/example_image.jpeg -------------------------------------------------------------------------------- /json_alto/README.rst: -------------------------------------------------------------------------------- 1 | ALTO to JSON converter 2 | --------- 3 | 4 | Simple converter for ALTO to JSON 5 | 6 | To run the alto_edit software out of the box you must first: 7 | 8 | 1) Install ruby and the following gems + their dependencies: 9 | 10 | sinatra >= 1.1.2 (sudo gem install sinatra) 11 | 12 | hpricot >= 0.8.2 13 | 14 | json >= 1.4.3 15 | 16 | 2) Run the following command 17 | 18 | $ ruby -rubygems web.rb -p3002 19 | 20 | 21 | -------------------------------------------------------------------------------- /cropper/README.rst: -------------------------------------------------------------------------------- 1 | Cropper 2 | --------- 3 | 4 | Allows for zooming and cropping images 5 | 6 | To run the alto_edit software out of the box you must first: 7 | 8 | 0) Prereq imagemagick dev lib: 9 | 10 | sudo apt-get install libmagick9-dev 11 | 12 | 1) Install ruby and the following gems: 13 | 14 | sinatra >= 1.1.2 (sudo gem install sinatra) 15 | 16 | rmagick >= 2.13.1 17 | 18 | 2) Run the following command 19 | 20 | $ ruby -rubygems web.rb -p3000 21 | 22 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Alto Edit 2 | ----------- 3 | 4 | Demo: http://altoedit.kbresearch.nl 5 | 6 | Browser based post correction tool for Alto XML files: 7 | 8 | This repos contains three parts: 9 | 10 | cropper --> ruby sinatra based cropper using image magick gem 11 | 12 | json_alto --> ruby sinatra based converter for alto xml files to JSON 13 | 14 | static --> client website, the actual tool 15 | 16 | 17 | Basic setup (for details see the separate subdirs): 18 | 19 | 1. Run the cropper on port 3000 20 | 21 | 2. Run json_alto on port 3002 22 | 23 | 3. On Apache symlink the static dir to /var/www with a name of your choosing 24 | 25 | 26 | .. image:: https://github.com/impactcentre/alto-editor/raw/master/static/img/Screenshot.png 27 | -------------------------------------------------------------------------------- /static/README.rst: -------------------------------------------------------------------------------- 1 | Alto Edit 2 | ---------- 3 | 4 | This website is designed as a demo and is not configurable. To use it generically for your own project some code review is required 5 | 6 | To run this alto editor website, first follow the instructions on starting up the cropper and json_alto services 7 | 8 | Next, to get this demo up and running: 9 | 10 | 1) Install apache 11 | 12 | 2) symlink this folder (static) to your public path (/var/www), using a name of your choosing 13 | 14 | 3) surf to http://localhost/static/alto_edit/ 15 | 16 | All remote references are currently hardcoded, so to change them some code review is required: 17 | 18 | All instances of localhost:3000 refer to the cropper service 19 | 20 | All instances of localhost:3002 refer to the json_alto converter 21 | 22 | 23 | There is a same xml file (alto), which is referred to, this is not dynamically retrieved, but this xml file *is* however passed to the json_alto converter service. 24 | 25 | The same goes for the example jpeg file, under the images folder. It is statically retrieved, but it *is* however passed to the cropper service. 26 | 27 | NOTE: 28 | The image_client_jp2.js was written to be used as a client for a dynamic JPEG 2000 service. The cropper is just a dummy for and should not be used with large jpeg files. 29 | -------------------------------------------------------------------------------- /cropper/web.rb: -------------------------------------------------------------------------------- 1 | # cropper/web.rb: Simple cropper for image magick 2 | # For details see: http://opendatachallenge.kbresearch.nl/ 3 | # Copyright (C) 2011 R. van der Ark, Koninklijke Bibliotheek - National Library of the Netherlands 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | #!/usr/bin/ruby 18 | 19 | require 'sinatra' 20 | require 'RMagick' 21 | require 'simple_get_response' 22 | 23 | 24 | 25 | get '/' do 26 | get = SimpleGetResponse.new(params[:url] || params[:id]) 27 | if get.success? 28 | img = Magick::Image.from_blob(get.body).first 29 | img.scale!(params[:s].to_f) 30 | img.crop!(params[:x].to_i, params[:y].to_i, params[:w].to_i, params[:h].to_i) 31 | content_type img.mime_type 32 | return img.to_blob 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /cropper/simple_get_response.rb: -------------------------------------------------------------------------------- 1 | # simple_get_response.rb: Wrapper for net/http 2 | # For details see: http://opendatachallenge.kbresearch.nl/ 3 | # Copyright (C) 2011 R. van der Ark, Koninklijke Bibliotheek - National Library of the Netherlands 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | require "net/http" 19 | require "uri" 20 | 21 | class SimpleGetResponse 22 | attr_accessor :response, :http, :url 23 | def initialize(set_url) 24 | self.url = URI.parse(set_url) 25 | self.http = Net::HTTP.new(url.host, url.port) 26 | self.http.read_timeout = 30 27 | self.http.open_timeout = 30 28 | get_response(5) 29 | end 30 | 31 | def get_response(tries = 1) 32 | (0..tries).each do |i| 33 | success = true 34 | begin 35 | self.response = http.start {|http| 36 | http.request_get(url.request_uri) rescue success = false 37 | } 38 | break if success 39 | rescue 40 | puts "Try #{i} on #{self.url.request_uri}: #{$!}" 41 | sleep 1 42 | end 43 | end 44 | 45 | if response.kind_of?(Net::HTTPRedirection) 46 | initialize(redirect_url) 47 | end 48 | end 49 | 50 | def body 51 | return self.response.body 52 | end 53 | 54 | def success? 55 | case self.response 56 | when Net::HTTPSuccess 57 | return true 58 | else 59 | return false 60 | end 61 | end 62 | 63 | def redirect_url 64 | if response['location'].nil? 65 | response.body.match(/]+)\">/i)[1] 66 | else 67 | response['location'] 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /json_alto/src/simple_get_response.rb: -------------------------------------------------------------------------------- 1 | # simple_get_response.rb: Wrapper for net/http 2 | # For details see: http://opendatachallenge.kbresearch.nl/ 3 | # Copyright (C) 2011 R. van der Ark, Koninklijke Bibliotheek - National Library of the Netherlands 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | require "net/http" 19 | require "uri" 20 | 21 | class SimpleGetResponse 22 | attr_accessor :response, :http, :url 23 | def initialize(set_url) 24 | self.url = URI.parse(set_url) 25 | self.http = Net::HTTP.new(url.host, url.port) 26 | self.http.read_timeout = 30 27 | self.http.open_timeout = 30 28 | get_response(5) 29 | end 30 | 31 | def get_response(tries = 1) 32 | (0..tries).each do |i| 33 | success = true 34 | begin 35 | self.response = http.start {|http| 36 | http.request_get(url.request_uri) rescue success = false 37 | } 38 | break if success 39 | rescue 40 | puts "Try #{i} on #{self.url.request_uri}: #{$!}" 41 | sleep 1 42 | end 43 | end 44 | 45 | if response.kind_of?(Net::HTTPRedirection) 46 | initialize(redirect_url) 47 | end 48 | end 49 | 50 | def body 51 | return self.response.body 52 | end 53 | 54 | def success? 55 | case self.response 56 | when Net::HTTPSuccess 57 | return true 58 | else 59 | return false 60 | end 61 | end 62 | 63 | def redirect_url 64 | if response['location'].nil? 65 | response.body.match(/]+)\">/i)[1] 66 | else 67 | response['location'] 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /static/alto_edit/js/interface.js: -------------------------------------------------------------------------------- 1 | var image_client = null; 2 | var viewer = null; 3 | 4 | // Array Remove - By John Resig (MIT Licensed) 5 | Array.prototype.remove = function(from, to) { 6 | var rest = this.slice((to || from) + 1 || this.length); 7 | this.length = from < 0 ? this.length + from : from; 8 | return this.push.apply(this, rest); 9 | }; 10 | 11 | document.onkeydown = function(e) { 12 | var code, shift, ctrl = null; 13 | if(!e) code = window.event.keyCode; else code = e.keyCode || e.which; 14 | if(!e) ctrl = window.event.ctrlKey; else ctrl = e.ctrlKey; 15 | if(!e) shift = window.event.shiftKey; else shift = e.shiftKey; 16 | 17 | if(code == 83 && ctrl == 1) { 18 | viewer.saveCurrentUpdate(); 19 | if(shift == 1) { 20 | if(confirm("Weet u zeker dat u deze pagina definitief wilt opslaan en verdergaan? Wijzigingen zijn hierna niet meer mogelijk")) 21 | $('update_form').insert(new Element("input", {"name": "finalize", "value": "true", "type": "hidden"})); 22 | else 23 | return false; 24 | } 25 | $('image_container').hide(); 26 | $('ocr_container').hide(); 27 | $('spinner_div').show(); 28 | 29 | $('update_form').submit(); 30 | return false; 31 | } 32 | } 33 | 34 | function altoFrom(version) { 35 | json_alto("reinitAltoText", version); 36 | } 37 | 38 | 39 | function confirmChanges() { 40 | viewer.saveCurrentUpdate(); 41 | var i = $$('#update_form input').length - 1; 42 | if(i > 0) { 43 | if(confirm("De laatste wijzigingen zijn niet opgeslagen, wilt u eerste de wijzigingen opslaan?")) { 44 | $('image_container').hide(); $('ocr_container').hide(); $('spinner_div').show(); 45 | $('update_form').submit(); 46 | } else { 47 | $('image_container').hide(); $('ocr_container').hide(); $('spinner_div').show(); 48 | return true; 49 | } 50 | } else { 51 | $('image_container').hide(); $('ocr_container').hide(); $('spinner_div').show(); 52 | return true; 53 | } 54 | } 55 | 56 | function leadingZero(x) { 57 | var y = "" + x; 58 | var retStr = ""; 59 | for(var i = 0; i < 4 - y.length; ++i) 60 | retStr += "0"; 61 | return retStr + y; 62 | } 63 | 64 | function viewport() { 65 | if(document.body.clientWidth && document.body.clientHeight) 66 | return {width: document.body.clientWidth, height: document.body.clientHeight}; 67 | else 68 | return {width: 950, height: 600}; 69 | } 70 | 71 | function scaleWindows() { 72 | var dims = viewport(); 73 | [['image_container', 1.9], ['ocr_container', 2.5]].each(function(x) { 74 | $(x[0]).style.width = (dims.width / x[1]) + "px"; 75 | $(x[0]).style.height = (dims.height - 170) + "px"; 76 | if(image_client && x[0] == 'image_container') 77 | image_client.updateDims(); 78 | }); 79 | } 80 | 81 | function preventDefault(e) { 82 | if (e.preventDefault) e.preventDefault(); 83 | return false; 84 | } 85 | 86 | -------------------------------------------------------------------------------- /static/alto_edit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 100 | 101 | 102 | 103 |
104 | 105 | 106 |
107 | 108 |
109 | 110 | 111 | 112 | 113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 |
121 | 122 | 123 | -------------------------------------------------------------------------------- /json_alto/web.rb: -------------------------------------------------------------------------------- 1 | # json_alto/web.rb: Simple converter from ALTO XML to JSON 2 | # For details see: http://opendatachallenge.kbresearch.nl/ 3 | # Copyright (C) 2011 R. van der Ark, Koninklijke Bibliotheek - National Library of the Netherlands 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | 18 | require "rubygems" 19 | require "sinatra" 20 | require "hpricot" 21 | require "json" 22 | require "src/simple_get_response" 23 | 24 | def correct_hyphenation(doc) 25 | #(/\-\s*$/, "-") 26 | textlines = (doc/"TextLine") 27 | (textlines/"String").remove_attr("SUBS_CONTENT") 28 | (textlines/"String").remove_attr("SUBS_TYPE") 29 | textlines.each_with_index do |textline, i| 30 | break if i == textlines.length - 1 31 | last_in_line = (textline/"String").last 32 | next_one = (textlines[i+1]/"String").first 33 | 34 | if last_in_line.attributes["CONTENT"] =~ /\-\s*$/ 35 | last_in_line.set_attribute("CONTENT", last_in_line.attributes["CONTENT"].sub(/\-\s*$/, "")) 36 | last_in_line.set_attribute("SUBS_CONTENT", last_in_line.attributes["CONTENT"] + next_one.attributes["CONTENT"]) 37 | last_in_line.set_attribute("SUBS_TYPE", "HypPart1") 38 | next_one.set_attribute("SUBS_CONTENT", last_in_line.attributes["CONTENT"] + next_one.attributes["CONTENT"]) 39 | next_one.set_attribute("SUBS_TYPE", "HypPart2") 40 | if (textline/'HYP').length == 0 41 | est_hyp_width = (last_in_line.attributes["WIDTH"].to_i / (last_in_line.attributes["CONTENT"].length + 1.5)).to_i 42 | hpos = last_in_line.attributes["WIDTH"].to_i + last_in_line.attributes["HPOS"].to_i - est_hyp_width 43 | vpos = last_in_line.attributes["VPOS"].to_i + (last_in_line.attributes["HEIGHT"].to_i / 2).to_i 44 | last_in_line.after(%()) 45 | end 46 | end 47 | end 48 | end 49 | 50 | def correct_whitespaces(doc) 51 | (doc/"SP").remove 52 | id_it = 1 53 | (doc/"TextLine").each do |textline| 54 | strings = (textline/'String') 55 | (0..strings.length-2).each do |i| 56 | hpos = strings[i].attributes["HPOS"].to_i + strings[i].attributes["WIDTH"].to_i 57 | width = strings[i+1].attributes["HPOS"].to_i - hpos 58 | vpos = ((strings[i].attributes["VPOS"].to_i + strings[i+1].attributes["VPOS"].to_i).to_f / 2.0).to_i 59 | id = id_it 60 | strings[i].after(%()) 61 | id_it += 1 62 | end 63 | end 64 | end 65 | 66 | 67 | 68 | get "/" do 69 | content_type "text/javascript", 'charset' => 'utf-8' 70 | get = SimpleGetResponse.new(params[:url]) 71 | timestamp = params[:timestamp] ? params[:timestamp].to_i : Time.now.to_i 72 | if get.success? 73 | doc = Hpricot.XML(get.body) 74 | alto = { 75 | :identifier => (doc/'//fileName').first.innerText, 76 | :page_width => (doc/'//Page').first.attributes["WIDTH"].to_i, 77 | :page_height => (doc/'//Page').first.attributes["HEIGHT"].to_i, 78 | :blocks => (doc/'//TextBlock').map do |tb| 79 | { 80 | :id => tb.attributes["ID"], 81 | :hpos => tb.attributes["HPOS"].to_i, 82 | :vpos => tb.attributes["VPOS"].to_i, 83 | :width => tb.attributes["WIDTH"].to_i, 84 | :height => tb.attributes["HEIGHT"].to_i, 85 | :lines => (tb/'//TextLine').map do |tl| 86 | { 87 | :id => tl.attributes["ID"], 88 | :hpos => tl.attributes["HPOS"].to_i, 89 | :vpos => tl.attributes["VPOS"].to_i, 90 | :width => tl.attributes["WIDTH"].to_i, 91 | :height => tl.attributes["HEIGHT"].to_i, 92 | :strings => (tl/'//String').map do |s| 93 | { 94 | :id => s.attributes["ID"], 95 | :hpos => s.attributes["HPOS"].to_i, 96 | :vpos => s.attributes["VPOS"].to_i, 97 | :height => s.attributes["HEIGHT"].to_i, 98 | :width => s.attributes["WIDTH"].to_i, 99 | :content => s.attributes["CONTENT"], 100 | :wc => s.attributes["WC"], 101 | :updated => false 102 | } 103 | end 104 | } 105 | end 106 | } 107 | end 108 | } 109 | 110 | return "#{params[:callback]}(#{JSON alto});" if params[:callback] 111 | return JSON alto 112 | end 113 | end 114 | 115 | post "/" do 116 | content_type :xml 117 | if params[:url] 118 | get = SimpleGetResponse.new(params[:url]) 119 | if get.success? 120 | doc = Hpricot.XML(get.body) 121 | (params[:insert] || []).each do |key, values| 122 | (block_id, line_id, word_id) = key.split(":") 123 | block = (doc/"//TextBlock[@ID=#{block_id}]").first 124 | line = (block/"/TextLine[@ID=#{line_id}]").first 125 | after = (line/"/String[@ID=#{values["after"].gsub(/.+:/, "")}]").first 126 | after.after(%()) 127 | end 128 | 129 | (params[:update] || []).each do |key, values| 130 | if values["delete"] 131 | (block_id, line_id, word_id) = key.split(":") 132 | block = (doc/"//TextBlock[@ID=#{block_id}]").first 133 | line = (block/"/TextLine[@ID=#{line_id}]").first 134 | string = (line/"/String[@ID=#{word_id}]").first 135 | deleted_node = (line/"/String[@ID=#{string.attributes["ID"]}]") 136 | 137 | deleted_node.after("") 138 | deleted_node.remove 139 | end 140 | end 141 | 142 | 143 | 144 | 145 | 146 | updates = params[:update] 147 | (updates||[]).each do |key, update| 148 | (block_id, line_id, word_id) = key.split(":") 149 | block = (doc/"//TextBlock[@ID=#{block_id}]").first 150 | line = (block/"/TextLine[@ID=#{line_id}]").first 151 | string = (line/"/String[@ID=#{word_id}]").first 152 | if string && string.has_attribute?("SUBS_CONTENT") 153 | remainder = string.attributes["SUBS_CONTENT"].sub(string.attributes["CONTENT"], "") 154 | string.set_attribute("SUBS_CONTENT", update["content"] + remainder) if string.attributes["SUBS_TYPE"] == "HypPart1" 155 | string.set_attribute("SUBS_CONTENT", remainder + update["content"]) if string.attributes["SUBS_TYPE"] == "HypPart2" 156 | end 157 | string.set_attribute("CONTENT", update["content"]) if string 158 | string.set_attribute("HPOS", update["hpos"]) if string && update["hpos"] 159 | string.set_attribute("WIDTH", update["width"]) if string && update["width"] 160 | end 161 | 162 | # Fourth run: normalize white-spaces 163 | correct_whitespaces(doc) 164 | # Fifth run: correct Hyphenation 165 | correct_hyphenation(doc) 166 | 167 | x = "" 168 | doc.output(x) 169 | return x 170 | end 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /static/alto_edit/js/image_client_jp2.js: -------------------------------------------------------------------------------- 1 | /* 2 | * image_client_jp2.js: Client for an image service which can zoom and crop via HTTP GET 3 | * For details see: http://opendatachallenge.kbresearch.nl/ 4 | * Copyright (C) 2011 R. van der Ark, Koninklijke Bibliotheek - National Library of the Netherlands 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | var ImageClient = Class.create({ 20 | initialize: function(container_id, urn, fullImageDims, words, spinner_src, zc, altUrl, usefullUrl) { 21 | this.imgUrl = (altUrl ? altUrl : "http://imageviewer.kb.nl/ImagingService/imagingService"); 22 | this.container = $(container_id); 23 | if(usefullUrl) { 24 | this.settings = { 25 | colour: "89c5e7", 26 | id: urn, 27 | r: 0, 28 | s: 0.1, 29 | x: 0, 30 | y: 0, 31 | w: parseInt(this.container.style.width), 32 | h: parseInt(this.container.style.height), 33 | useresolver: "false" 34 | } 35 | } else { 36 | this.settings = { 37 | colour: "89c5e7", 38 | coords: urn + ":alto", 39 | words: (words ? words : ""), 40 | id: urn + ":image", 41 | r: 0, 42 | s: 0.1, 43 | x: 0, 44 | y: 0, 45 | w: parseInt(this.container.style.width), 46 | h: parseInt(this.container.style.height) 47 | } 48 | } 49 | 50 | 51 | this.spinner = new Element("img", { 52 | "src": (spinner_src ? spinner_src : ""), 53 | "style": "margin-top: " + parseInt(this.container.style.height) / 2 + "; margin-left: " + parseInt(this.container.style.width) / 2 54 | }); 55 | this.container.insert(this.spinner); 56 | this.fullImageDims = fullImageDims; 57 | this.settings.s = parseInt(this.container.style.height) / this.fullImageDims.h; 58 | this.zoomCorrection = (zc ? zc : 1.0); 59 | this.locked = false; 60 | this.bufferedImage = null; 61 | this.defineCallbacks(); 62 | this.overlays = []; 63 | }, 64 | 65 | updateDims: function() { 66 | this.settings.w = parseInt(this.container.style.width); 67 | this.settings.h = parseInt(this.container.style.height) 68 | this.render(); 69 | }, 70 | 71 | render: function() { 72 | var _this = this; 73 | if(!this.locked) { 74 | this.bufferedImage = new Image(); 75 | this.settings.s *= this.zoomCorrection; 76 | this.bufferedImage.src = this.imgUrl + "?" + Object.toQueryString(this.settings); 77 | this.settings.s /= this.zoomCorrection; 78 | this.locked = true; 79 | this.spinner.show(); 80 | window.setTimeout(function() { _this.render() }, 5); 81 | } else { 82 | if(this.bufferedImage.complete) { 83 | this.spinner.hide(); 84 | this.renderImage(); 85 | this.locked = false; 86 | } else { 87 | window.setTimeout(function() { _this.render() }, 5); 88 | } 89 | } 90 | }, 91 | renderImage: function() { 92 | this.container.style.backgroundImage = "url(" + this.bufferedImage.src + ")"; 93 | this.container.style.backgroundPosition = "0 0"; 94 | this.renderOverlays(); 95 | }, 96 | zoomIn: function() { 97 | this.setZoom(this.settings.s * 1.1); 98 | this.setPosition(this.settings.x, this.settings.y); 99 | this.render(); 100 | }, 101 | zoomOut: function() { 102 | this.setZoom(this.settings.s * 0.9); 103 | this.setPosition(this.settings.x, this.settings.y); 104 | this.render(); 105 | }, 106 | zoomBack: function(isVertical) { 107 | if(isVertical) 108 | this.settings.s = parseInt(this.container.style.width) / this.fullImageDims.w; 109 | else 110 | this.settings.s = parseInt(this.container.style.height) / this.fullImageDims.h; 111 | this.setPosition(0,0); 112 | this.render(); 113 | }, 114 | setPosition: function(x, y) { 115 | this.settings.x = x + parseInt(this.container.style.width) > (this.fullImageDims.w * this.settings.s) ? (this.fullImageDims.w * this.settings.s) - parseInt(this.container.style.width) : x; 116 | this.settings.y = y + parseInt(this.container.style.height) > (this.fullImageDims.h * this.settings.s) ? (this.fullImageDims.h * this.settings.s) - parseInt(this.container.style.height) : y; 117 | this.settings.x = this.settings.x < 0 ? 0 : this.settings.x; 118 | this.settings.y = this.settings.y < 0 ? 0 : this.settings.y; 119 | }, 120 | setZoom: function(z) { 121 | this.settings.s = z; 122 | }, 123 | getZoom: function() { 124 | return this.settings.s; 125 | }, 126 | getTop: function() { 127 | return this.settings.y; 128 | }, 129 | getLeft: function() { 130 | return this.settings.x; 131 | }, 132 | renderAreaBox: function(elem) { 133 | if( 134 | parseInt(elem.style.top) > this.container.offsetTop + parseInt(this.container.style.height) || 135 | parseInt(elem.style.top) + parseInt(elem.style.height) < this.container.offsetTop || 136 | parseInt(elem.style.left) > this.container.offsetLeft + parseInt(this.container.style.width) || 137 | parseInt(elem.style.left) + parseInt(elem.style.width) < this.container.offsetLeft 138 | ) 139 | elem.hide(); 140 | else { 141 | var wdiff = (parseInt(elem.style.left) + parseInt(elem.style.width)) - (this.container.offsetLeft + parseInt(this.container.style.width)); 142 | var hdiff = (parseInt(elem.style.top) + parseInt(elem.style.height)) - (this.container.offsetTop + parseInt(this.container.style.height)); 143 | if(wdiff > 0) elem.style.width = parseInt(elem.style.width) - wdiff; 144 | if(hdiff > 0) elem.style.height = parseInt(elem.style.height) - hdiff; 145 | var tdiff = parseInt(elem.style.top) - this.container.offsetTop; 146 | if(tdiff < 0) { 147 | elem.style.top = this.container.offsetTop; 148 | elem.style.height = parseInt(elem.style.height) + tdiff; 149 | } 150 | var ldiff = parseInt(elem.style.left) - this.container.offsetLeft; 151 | if(ldiff < 0) { 152 | elem.style.left = this.container.offsetLeft; 153 | elem.style.width = parseInt(elem.style.width) + ldiff; 154 | } 155 | elem.show(); 156 | } 157 | }, 158 | hideOverlays: function() { 159 | this.overlays.each(function(o) { o.div.hide()}); 160 | }, 161 | renderOverlays: function() { 162 | var _this = this; 163 | this.overlays.each(function(hl) { 164 | hl.div.style.width = hl.w * _this.settings.s; 165 | hl.div.style.height = hl.h * _this.settings.s; 166 | hl.div.style.left = (hl.x * _this.settings.s) + _this.container.offsetLeft - _this.settings.x; 167 | hl.div.style.top = (hl.y * _this.settings.s) + _this.container.offsetTop - _this.settings.y; 168 | _this.renderAreaBox(hl.div); 169 | }); 170 | }, 171 | addOverlay: function(x, y, w, h, c, mouseover, id, callback) { 172 | var hlDiv = new Element("div", { 173 | "id": id ? id : "", 174 | "style": 175 | "position: absolute; background-color: " + c + "; opacity: 0.2; line-height: 0px; filter: alpha(opacity=20);" 176 | }); 177 | hlDiv.insert(" "); 178 | this.container.insert(hlDiv); 179 | hlDiv.hide(); 180 | if(callback) hlDiv.observe("click", callback); 181 | if(mouseover) { 182 | if(Prototype.Browser.IE) hlDiv.style.filter = "alpha(opacity=0)"; 183 | else hlDiv.style.backgroundColor = "transparent"; 184 | 185 | hlDiv.observe("mouseover", function(e) { 186 | if(Prototype.Browser.IE) this.style.filter = "alpha(opacity=20)"; 187 | else this.style.backgroundColor = c; 188 | this.style.cursor = "pointer"; 189 | }); 190 | hlDiv.observe("mouseout", function(e) { 191 | if(Prototype.Browser.IE) this.style.filter = "alpha(opacity=0)"; 192 | else this.style.backgroundColor = "transparent"; 193 | }); 194 | } 195 | var hl = {x: x, y: y, w: w, h: h, div: hlDiv}; 196 | this.overlays.push(hl); 197 | this.renderOverlays(); 198 | return hl; 199 | }, 200 | dropOverlay: function(hl) { 201 | hl.div.remove(); 202 | this.overlays.splice(this.overlays.indexOf(hl), 1); 203 | }, 204 | defineCallbacks: function() { 205 | this.lastPosition = null; 206 | var _this = this; 207 | this.container.observe("mousedown", function(e) { _this.mouseDown(e); }); 208 | this.container.observe("mousemove", function(e) { _this.mouseMove(e); }); 209 | this.container.observe("mouseup", function(e) { _this.mouseUp(e); }); 210 | var mousewheelevt=(/Firefox/i.test(navigator.userAgent))? "DOMMouseScroll" : "mousewheel"; 211 | if(Prototype.Browser.IE || Prototype.Browser.Opera) 212 | document.attachEvent("on"+mousewheelevt, function(e){ _this.mouseScroll(e) }); 213 | else 214 | document.addEventListener(mousewheelevt, function(e){ _this.mouseScroll(e) }, false); 215 | }, 216 | mouseScroll: function(e) { 217 | if(e.clientX > this.container.offsetLeft && e.clientY > this.container.offsetTop && 218 | e.clientX < this.container.offsetLeft + parseInt(this.container.style.width) && 219 | e.clientY < this.container.offsetTop + parseInt(this.container.style.height) && !this.locked) { 220 | var evt = window.event || e; 221 | var delta = evt.detail ? -(evt.detail) : evt.wheelDelta; 222 | if(delta < 0) 223 | this.zoomOut(); 224 | else 225 | this.zoomIn(); 226 | } 227 | }, 228 | mouseDown: function(e) { 229 | if(Prototype.Browser.IE) { e.returnValue = false; } else { e.preventDefault(); } 230 | this.container.style.cursor = "move"; 231 | this.lastPosition = {x: e.clientX, y: e.clientY}; 232 | }, 233 | mouseMove: function(e) { 234 | if(Prototype.Browser.IE) { e.returnValue = false; } else { e.preventDefault(); } 235 | if(this.lastPosition) { 236 | this.hideOverlays(); 237 | var movement = {x: this.lastPosition.x - e.clientX, y: this.lastPosition.y - e.clientY}; 238 | this.container.style.backgroundPosition = -(movement.x) + " " + -(movement.y); 239 | } 240 | }, 241 | mouseUp: function(e) { 242 | if(Prototype.Browser.IE) { e.returnValue = false; } else { e.preventDefault(); } 243 | if(this.lastPosition) { 244 | var movement = {x: this.lastPosition.x - e.clientX, y: this.lastPosition.y - e.clientY}; 245 | this.renderOverlays(); 246 | if(movement.x != 0 || movement.y != 0) { 247 | 248 | this.setPosition(this.settings.x + movement.x, this.settings.y + movement.y); 249 | this.render(); 250 | } 251 | this.container.style.cursor = "default"; 252 | this.lastPosition = null; 253 | } 254 | } 255 | }); 256 | 257 | -------------------------------------------------------------------------------- /static/alto_edit/js/seg_edit.js: -------------------------------------------------------------------------------- 1 | var ieKeyCode = 0; 2 | 3 | function delKeyPressedIE(e) { 4 | if(!e) 5 | e = window.event; 6 | ieKeyCode = e.keyCode; 7 | } 8 | 9 | 10 | var SegEdit = Class.create({ 11 | initialize: function(altoLine, imageClient, lineDiv, altoViewer) { 12 | 13 | var _this = this 14 | var vp = viewport(); 15 | this.altoViewer = altoViewer; 16 | this.deletions = []; 17 | this.resizings = []; 18 | this.insertions = []; 19 | this.contentUpdates = []; 20 | this.origDims = {}; 21 | this.resizeMode = "none"; 22 | this.selectedRect = null; 23 | this.lastX = 0; 24 | this.lineDiv = lineDiv; 25 | this.splittingSegment = null; 26 | this.wordPart = 0; 27 | this.altoLine = altoLine; 28 | 29 | var max = 0; 30 | lineDiv.childElements().each(function(s) { 31 | if(s.id.match(/appendedword/)) { 32 | var current = parseInt(s.id.replace(/.+_/, "")); 33 | max = current >= max ? (current+1) : max; 34 | } 35 | }); 36 | this.appendedCount = max; 37 | 38 | this.shade = new Element("div", { 39 | "style": "position: absolute; top:0;left:0;width:" + vp.width + "px;height:" + vp.height + "px; background-color: black; opacity: 0.6; filter: alpha(opacity=60);" 40 | }); 41 | this.container = new Element("div", { 42 | "style": "position: absolute; top: 50; left: 50; width:" + (vp.width-100)+"px;height:"+(vp.height-100)+"px;background-color: white;" 43 | }); 44 | 45 | this.delButton = new Element("button", { 46 | "disabled": true, 47 | "style": "margin-left: 50px; margin-top: 50px" 48 | }); 49 | 50 | this.delButton.insert("Verwijder segment"); 51 | this.delButton.observe("click", function(e) { 52 | _this.deleteSegment(); 53 | }); 54 | 55 | this.container.insert(this.delButton); 56 | 57 | this.delAllButton = new Element("button"); 58 | this.delAllButton.insert("Verwijder alle lege segmenten"); 59 | this.delAllButton.observe("click", function(e) { 60 | if(confirm("Weet u zeker dat u alle lege segmenten wilt verwijderen?")) 61 | _this.deleteEmptySegments(); 62 | }); 63 | this.container.insert(this.delAllButton); 64 | 65 | var imgSettings = { 66 | x: altoLine.hpos, 67 | y: altoLine.vpos, 68 | w: altoLine.width, 69 | h: altoLine.height, 70 | s: imageClient.zoomCorrection, 71 | coords: imageClient.settings.coords, 72 | id: imageClient.settings.id 73 | }; 74 | this.img = new Element("div", { 75 | "style": "background-image: url("+ imageClient.imgUrl + "?" + Object.toQueryString(imgSettings) +");" + 76 | "width:" + altoLine.width + "px;" + "height:" + altoLine.height + "px;" + 77 | "margin-top: 100px; margin-left: 50px" 78 | }); 79 | 80 | if(parseInt(this.img.style.width) > (parseInt(this.container.style.width) - 200)) { 81 | this.container.style.width = (parseInt(this.img.style.width) + 200) + "px"; 82 | this.shade.style.width = (parseInt(this.container.style.width) + 100) + "px"; 83 | } 84 | 85 | this.splitterDiv = new Element("div", { 86 | "style": "position: relative; width: 0px; height: " + (parseInt(this.img.style.height) - 4) + "px; border-left: 2px solid black" 87 | }); 88 | this.splitterDiv.insert(" "); 89 | 90 | this.img.observe("mousemove", function(e) { 91 | if(_this.selectedRect && _this.resizeMode != "none") 92 | return _this.mousemoveOnSelectedRect(_this.selectedRect, e); 93 | }); 94 | this.img.observe("mouseup", function(e) { 95 | if(_this.selectedRect && _this.resizeMode != "none") 96 | _this.resizeMode = "none"; 97 | }); 98 | 99 | this.word_container = new Element("div", { 100 | "style": "width: " + this.img.style.width + "; height: 100px; margin-top: 15px; margin-left: 50px;" 101 | }); 102 | 103 | this.img.insert(" "); 104 | this.shade.insert(" "); 105 | this.container.insert(this.img); 106 | this.container.insert(this.word_container); 107 | $$("body")[0].insert(this.shade); 108 | $$("body")[0].insert(this.container); 109 | this.rects = []; 110 | var _this = this; 111 | altoLine.strings.each(function(word) { 112 | var span = lineDiv.childElements().detect(function(e) { return e.id.replace(/.*:/, "") == word.id}); 113 | if(span) { 114 | var color = span.innerHTML == " " || span.innerHTML == " " ? "red" : "green"; 115 | _this.appendRect(word, span.innerHTML, color); 116 | } 117 | }); 118 | this.saveButton = new Element("button"); 119 | this.saveButton.observe("click", function() { 120 | _this.saveToAltoViewer(); 121 | _this.terminate(); 122 | }); 123 | this.cancelButton = new Element("button", {"style": "margin-left: 50px"}); 124 | this.cancelButton.observe("click", function() { 125 | _this.terminate(); 126 | }); 127 | this.saveButton.insert("Ok"); 128 | this.cancelButton.insert("Annuleren"); 129 | this.container.insert(this.cancelButton); 130 | this.container.insert(this.saveButton); 131 | document.onkeyup = function(e) { 132 | var code = null; 133 | if(!e) code = window.event.keyCode; 134 | else code = e.keyCode || e.which; 135 | if(code == 46) _this.deleteSegment(); 136 | } 137 | }, 138 | 139 | deleteEmptySegments: function() { 140 | var emptyRects = this.rects.select(function(r) { return r.style.borderColor.match(/red/) }); 141 | var _this = this; 142 | emptyRects.each(function(r) { 143 | _this.selectedRect = r; 144 | _this.deleteSegment(true); 145 | }); 146 | }, 147 | 148 | deleteSegment: function(noConfirm) { 149 | if(this.selectedRect && this.selectedRect.style.borderColor.match(/red/) && (noConfirm || confirm("Weet u zeker dat u dit segement wilt verwijderen?"))) { 150 | this.deletions.push(this.lineDiv.ancestors()[0].id + ":" + this.lineDiv.id + ":" + this.selectedRect.id.replace(":seg", "")); 151 | this.rects.remove(this.rects.indexOf(this.selectedRect)); 152 | this.selectedRect.remove(); 153 | this.selectedRect = null; 154 | this.delButton.disabled = true; 155 | } 156 | }, 157 | 158 | appendRect: function(word, wordContent, color) { 159 | var _this = this; 160 | var rect = new Element("div", { 161 | "style": "background-color: " + color + "; opacity: 0.2; line-height: 0px; filter: alpha(opacity=20);" + 162 | "height: " + (Prototype.Browser.IE ? parseInt(this.img.style.height) : parseInt(this.img.style.height) - 4) + "px; width: " + word.width + "px;" + "position: absolute;" + 163 | "top: " + this.img.offsetTop + "px; left: " + (this.img.offsetLeft + (word.hpos - this.altoLine.hpos)) + "px; " + 164 | "border: 2px solid " + color + ";cursor:pointer", 165 | "id": word.id + ":seg" 166 | }); 167 | rect.observe("click", function(e) { 168 | _this.rectOnClick(this, e, false, color == "red"); 169 | }); 170 | rect.insert(" "); 171 | this.img.insert(rect); 172 | this.rects.push(rect); 173 | this.origDims[rect.id] = {left: parseInt(rect.style.left), width: parseInt(rect.style.width)}; 174 | 175 | var word_span = new Element("div", { 176 | "style": "position: absolute; left: " + rect.style.left, 177 | "id": word.id + ":segwrd" 178 | }); 179 | word_span.insert(wordContent); 180 | this.word_container.insert(word_span); 181 | rect.observe("mouseover", function(e) { 182 | word_span.style.color = "green"; 183 | }); 184 | rect.observe("mouseout", function(e) { 185 | word_span.style.color = "black"; 186 | }) 187 | this.addSplitButtons(word.id, word_span); 188 | }, 189 | 190 | addSplitButtons: function(word_id, word_span) { 191 | word_span.innerHTML = word_span.innerHTML.replace(/\s+/g, " "); 192 | var i = 0; 193 | var _this = this; 194 | word_span.childElements().each(function(b) { 195 | b.addClassName("button_" + i); 196 | b.observe("click", function(e) { 197 | _this.splitSegment(word_id + ":seg", this); 198 | }); 199 | i++; 200 | }); 201 | }, 202 | 203 | splitSegment: function(segId, btn) { 204 | this.rectOnClick($(segId), null, true); 205 | this.splittingSegment = segId; 206 | this.wordPart = parseInt(btn.readAttribute("class").replace("button_", "")); 207 | $(this.splittingSegment).insert(this.splitterDiv); 208 | }, 209 | 210 | splitSegmentAt: function(hpos) { 211 | var segDiv1 = $(this.splittingSegment); 212 | var adjustedWidth = (hpos - parseInt(segDiv1.style.left)) - (Prototype.Browser.IE ? 0 : 2); 213 | var newWidth = parseInt(segDiv1.style.width) - adjustedWidth - (Prototype.Browser.IE ? 0 : 2); 214 | segDiv1.style.width = adjustedWidth + "px"; 215 | var leftContent = ""; 216 | var wordSpan = $(this.splittingSegment.replace(":seg", ":segwrd")); 217 | var updatedSegmentId = this.splittingSegment.replace(":seg", ""); 218 | wordSpan.childElements().each(function(btn) { btn.stopObserving("click"); btn.remove(); }); 219 | var rightContent = wordSpan.innerHTML; 220 | for(var i = 0; i < this.wordPart + 1; ++i) { 221 | leftContent += rightContent.replace(/ .+/, " "); 222 | rightContent = rightContent.replace(/^.+? /, ""); 223 | } 224 | rightContent = rightContent.replace(/ /g, " "); 225 | wordSpan.innerHTML = leftContent.strip(); 226 | this.addSplitButtons(updatedSegmentId, wordSpan); 227 | this.wordPart = null; 228 | this.splittingSegment = null; 229 | this.splitterDiv.remove(); 230 | 231 | this.rectOnClick(segDiv1); 232 | this.appendRect({ 233 | "id": "appendedword_" + this.lineDiv.id + "_" + this.appendedCount, 234 | "hpos": hpos + this.altoLine.hpos - parseInt(this.img.style.marginLeft), 235 | "width": newWidth }, rightContent, "green"); 236 | 237 | this.saveResizing(segDiv1); 238 | this.insertions.push({ 239 | "id": "appendedword_" + this.lineDiv.id + "_" + this.appendedCount, 240 | "hpos": hpos + 2 + this.altoLine.hpos - parseInt(this.img.style.marginLeft), 241 | "width": newWidth, 242 | "vpos": this.altoLine.vpos, 243 | "height": this.altoLine.height, 244 | "content": rightContent 245 | }); 246 | this.contentUpdates.push({ 247 | "id": updatedSegmentId, 248 | "content": leftContent.strip() 249 | }); 250 | this.appendedCount++; 251 | }, 252 | 253 | rectOnClick: function(rect, e, suppressOpacityChange, mayDelete) { 254 | if(this.splittingSegment) { 255 | var absX = e.clientX - this.img.cumulativeOffset()[0] + parseInt(this.img.style.marginLeft) + document.body.scrollLeft; 256 | this.splitSegmentAt(absX + (Prototype.Browser.IE ? 3 : 0)); 257 | } else { 258 | var _this = this; 259 | this.rects.each(function(r) { 260 | var span = $(r.id.replace(":seg", ":segwrd")) 261 | var color = span.innerHTML == " " || span.innerHTML == " " ? "red" : "green"; 262 | 263 | r.style.border = "2px solid " + color; 264 | r.style.opacity = "0.2"; 265 | r.style.filter = "alpha(opacity=20)"; 266 | r.style.backgroundColor = color; 267 | r.style.cursor = "pointer"; 268 | r.stopObserving("mousemove"); 269 | r.stopObserving("mouseup"); 270 | r.stopObserving("mousedown"); 271 | }); 272 | rect.style.border = "2px solid " + rect.style.backgroundColor; 273 | if(suppressOpacityChange) { 274 | rect.style.opacity = "0.6"; 275 | rect.style.filter = "alpha(opacity=60)"; 276 | } else { 277 | rect.style.opacity = "1.0"; 278 | rect.style.filter = "alpha(opacity=100)"; 279 | rect.style.backgroundColor = "transparent"; 280 | } 281 | this.selectedRect = rect; 282 | rect.observe("mousemove", function(e) { return _this.mousemoveOnSelectedRect(rect, e); }); 283 | rect.observe("mousedown", function(e) { _this.mousedownOnSelectedRect(rect, e); }); 284 | rect.observe("mouseup", function(e) { _this.mouseupOnSelectedRect(rect, e); }); 285 | if(mayDelete) 286 | this.delButton.disabled = false; 287 | else 288 | this.delButton.disabled = true; 289 | } 290 | }, 291 | 292 | delKeyPressed: function(keyCode) { 293 | if(keyCode == 46) 294 | this.deleteSegment(); 295 | }, 296 | 297 | mousemoveOnSelectedRect: function(rect, e) { 298 | var margin = 15; 299 | var relX = e.clientX - rect.cumulativeOffset()[0] + document.body.scrollLeft; 300 | var absX = e.clientX - this.img.cumulativeOffset()[0] + parseInt(this.img.style.marginLeft) + document.body.scrollLeft; 301 | var movement = (relX - this.lastX); 302 | if(this.splittingSegment != null) { 303 | this.splitterDiv.style.left = relX + "px"; 304 | } else if(this.resizeMode == "none") { 305 | if(relX < margin) { 306 | rect.style.cursor = "col-resize"; 307 | } else if(relX > parseInt(rect.style.width) - margin){ 308 | rect.style.cursor = "col-resize"; 309 | } else { 310 | rect.style.cursor = "pointer"; 311 | } 312 | } else if(movement != 0) { 313 | this.img.childElements().each(function(r) { if(r != rect) r.hide()}); 314 | if(this.resizeMode == "w-resize") { 315 | var oldX = parseInt(rect.style.left); 316 | rect.style.left = absX + "px"; 317 | rect.style.width = (parseInt(rect.style.width) + (oldX - parseInt(rect.style.left))) + "px"; 318 | $(rect.id.replace(":seg", ":segwrd")).style.left = rect.style.left; 319 | } else if(this.resizeMode == "e-resize") { 320 | rect.style.width = (parseInt(rect.style.width) + movement) + "px"; 321 | var resizingId = this.lineDiv.ancestors()[0].id + ":" + this.lineDiv.id + ":" + rect.id.replace(":seg", ""); 322 | } 323 | this.img.childElements().each(function(r) { if(r != rect) r.show()}); 324 | } 325 | 326 | this.lastX = relX; 327 | return preventDefault(e); 328 | }, 329 | 330 | mousedownOnSelectedRect: function(rect, e) { 331 | var margin = 15; 332 | var relX = e.clientX - rect.cumulativeOffset()[0] + document.body.scrollLeft; 333 | if(relX < margin) { 334 | this.resizeMode = "w-resize"; 335 | } else if(relX > parseInt(rect.style.width) - margin){ 336 | this.resizeMode = "e-resize"; 337 | } else { 338 | this.resizeMode = "pointer"; 339 | } 340 | this.lastX = relX; 341 | }, 342 | 343 | saveResizing: function(rect) { 344 | var _this = this; 345 | var idPart1 = this.lineDiv.ancestors()[0].id + ":" + this.lineDiv.id + ":"; 346 | var resizing = this.resizings.detect(function(r) { return r.id.replace(idPart1, "") + ":seg" == rect.id}); 347 | if(resizing) { 348 | resizing.width = parseInt(rect.style.width) - this.origDims[rect.id].width + 2; 349 | resizing.left = parseInt(rect.style.left) - this.origDims[rect.id].left + 2; 350 | } else { 351 | this.resizings.push({ 352 | "id": idPart1 + rect.id.replace(":seg", ""), 353 | "width": parseInt(rect.style.width) - this.origDims[rect.id].width + 2, 354 | "left": parseInt(rect.style.left) - this.origDims[rect.id].left + 2 355 | }); 356 | } 357 | }, 358 | 359 | mouseupOnSelectedRect: function(rect, e) { 360 | if(this.selectedRect && !this.splittingSegment) { 361 | this.saveResizing(this.selectedRect); 362 | } 363 | this.resizeMode = "none"; 364 | }, 365 | 366 | saveToAltoViewer: function() { 367 | this.altoViewer.updateSegments(this.deletions, this.resizings, this.insertions, this.contentUpdates, this.lineDiv); 368 | }, 369 | 370 | terminate: function() { 371 | this.altoViewer.lockLineClick = false; 372 | this.rects.each(function(r) { 373 | r.stopObserving("click"); 374 | r.stopObserving("mouseout"); 375 | r.stopObserving("mouseover"); 376 | r.stopObserving("mousemove"); 377 | r.stopObserving("mousedown"); 378 | r.stopObserving("mouseup"); 379 | r.remove(); 380 | }); 381 | this.delButton.remove(); 382 | this.img.stopObserving("mousemove"); 383 | this.img.stopObserving("mouseup"); 384 | this.img.remove(); 385 | this.word_container.remove(); 386 | this.cancelButton.stopObserving("click"); 387 | this.saveButton.stopObserving("click"); 388 | this.cancelButton.remove(); 389 | this.saveButton.remove(); 390 | this.container.remove(); 391 | this.shade.remove(); 392 | document.onkeyup = null; 393 | } 394 | }); 395 | -------------------------------------------------------------------------------- /static/alto_edit/js/alto_view.js: -------------------------------------------------------------------------------- 1 | /* 2 | * alto_view.js: Viewer and editor for ALTO represented as JSON 3 | * For details see: http://opendatachallenge.kbresearch.nl/ 4 | * Copyright (C) 2011 R. van der Ark, Koninklijke Bibliotheek - National Library of the Netherlands 5 | * 6 | * This program is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | var AltoView = Class.create({ 21 | initialize: function(alto, image_client, text_container, words, zc) { 22 | this.alto = alto; 23 | this.words = (words ? words.toLowerCase().split(" ") : []); 24 | this.imageClient = image_client; 25 | this.imageContainer = this.imageClient.container; 26 | this.textContainer = $(text_container); 27 | this.lineHighlight = null; 28 | this.blockAreas = []; 29 | this.updates = []; 30 | this.readorder = []; 31 | this.current_edit = null; 32 | this.lockLineClick = false; 33 | this.broadestLine = 0; 34 | this.renderText(); 35 | }, 36 | 37 | renderText: function() { 38 | var _this = this; 39 | this.alto.blocks.each(function(textBlock) { 40 | _this.renderTextBlock(textBlock); 41 | _this.blockAreas.push(_this.imageClient.addOverlay( 42 | textBlock.hpos, 43 | textBlock.vpos, 44 | textBlock.width, 45 | textBlock.height, 46 | "red", 47 | true, 48 | textBlock.id + "_box", 49 | function(e) { 50 | _this.textContainer.scrollTop = $(this.id.replace("_box", "")).offsetTop - (Prototype.Browser.IE ? 0 : _this.textContainer.offsetTop); 51 | new Effect.Highlight(this.id.replace("_box", ""), { startcolor: '#ffff99',endcolor: '#ffffff' }); 52 | _this.imageClient.setPosition(textBlock.hpos * _this.imageClient.getZoom(), (textBlock.vpos * _this.imageClient.getZoom())); 53 | _this.imageClient.render(); 54 | } 55 | )); 56 | }); 57 | }, 58 | 59 | renderTextBlock: function(textBlock) { 60 | var _this = this; 61 | var textDiv = new Element("div", {"id": textBlock.id}); 62 | textBlock.lines.each(function(textLine) { 63 | var lineDiv = new Element("div", {"id": textLine.id, "class": "altoLineDiv"}); 64 | lineDiv.observe("mouseover", function(e) { 65 | this.style.backgroundColor = "#dfd"; 66 | this.style.cursor = "pointer"; 67 | _this.highlightTextLine(textLine); 68 | }); 69 | lineDiv.observe("mousemove", function(e) { 70 | $$('.seg_corrector').each(Element.hide); 71 | if(e.clientX > (this.cumulativeOffset()[0] + this.getWidth() - 40)) 72 | $(this.id + ":corrector1").show(); 73 | }); 74 | _this.appendSegLink(lineDiv); 75 | textLine.strings.each(function(word) { 76 | if(!word["delete"]) { 77 | _this.readorder.push(textBlock.id + ":" + textLine.id + ":" + word.id); 78 | var span = new Element("span", {"id": textBlock.id + ":" + textLine.id + ":" + word.id}); 79 | span.insert(word.content); 80 | if(word.updated) 81 | span.style.color = "green"; 82 | 83 | lineDiv.insert(span); 84 | lineDiv.insert(" "); 85 | span.observe("click", function(e) { 86 | _this.editWord(this); 87 | }); 88 | span.observe("mouseover", function(e) { 89 | this.style.backgroundColor = "#ddf"; 90 | this.style.cursor = "pointer"; 91 | _this.highlightWord(word); 92 | }); 93 | span.observe("mouseout", function(e) { 94 | this.style.backgroundColor = "transparent"; 95 | }); 96 | } 97 | }); 98 | lineDiv.observe("click", function(e) { 99 | _this.zoomToLineAt(textLine, textBlock, _this.scrolledLinePos(this)); 100 | _this.highlightTextLine(textLine); 101 | if(!this.childElements().detect(function(e) { return e.id.match(/_input$/); }) && !_this.lockLineClick) 102 | _this.editWord(this.childElements()[0]); 103 | }); 104 | 105 | lineDiv.observe("mouseout", function(e) { 106 | this.style.backgroundColor = "transparent"; 107 | }); 108 | _this.broadestLine = _this.broadestLine > textLine.width ? _this.broadestLine : textLine.width; 109 | textDiv.insert(lineDiv); 110 | if(!_this.validateSegments(lineDiv)) 111 | _this.appendCorrectSegLink(lineDiv); 112 | 113 | 114 | }); 115 | this.textContainer.insert(textDiv); 116 | this.textContainer.insert(new Element("br")); 117 | }, 118 | 119 | findLine: function(lineId) { 120 | for(i = 0; i < this.alto.blocks.length; ++i) { 121 | var curLine = this.alto.blocks[i].lines.detect(function(l) { return l.id == lineId; }); 122 | if(curLine) return curLine; 123 | }; 124 | return null; 125 | }, 126 | 127 | findWord: function(wordId) { 128 | wordId = wordId.replace(/.*:/, ""); 129 | for(i = 0; i < this.alto.blocks.length; ++i) { 130 | for(j = 0; j < this.alto.blocks[i].lines.length; ++j) { 131 | var curWord = this.alto.blocks[i].lines[j].strings.detect(function(w) { return w.id == wordId; }); 132 | if(curWord) return curWord; 133 | } 134 | }; 135 | return null; 136 | }, 137 | 138 | scrolledLinePos: function(textDiv) { 139 | return textDiv.offsetTop - this.scrollPosition(); 140 | }, 141 | 142 | scrollPosition: function() { 143 | return this.textContainer.scrollTop + (Prototype.Browser.IE ? 0 : this.textContainer.offsetTop); 144 | }, 145 | 146 | zoomToLineAt: function(textLine, textBlock, atPosition) { 147 | var newZoom = 1.0 - ((this.broadestLine + 60) / this.alto.page_width); 148 | 149 | if(newZoom < 0.5) 150 | newZoom = 0.5; 151 | if(newZoom * textLine.width > parseInt(this.imageContainer.style.width) + 60) { 152 | newZoom *= parseInt(this.imageContainer.style.width) / (newZoom * (this.broadestLine +60)); 153 | } 154 | this.imageClient.setZoom(newZoom); //1.0 - (textLine.width / this.alto.page_width)); 155 | this.imageClient.setPosition((textLine.hpos * this.imageClient.getZoom())-20, (textLine.vpos * this.imageClient.getZoom()) - atPosition); 156 | this.imageClient.render(); 157 | }, 158 | 159 | highlightTextLine: function(textLine) { 160 | if(this.lineHighlight) this.imageClient.dropOverlay(this.lineHighlight); 161 | this.lineHighlight = this.imageClient.addOverlay(textLine.hpos, textLine.vpos, textLine.width, textLine.height, "blue"); 162 | }, 163 | 164 | highlightWord: function(wrd, setColor) { 165 | if(this.wordHighlight) this.imageClient.dropOverlay(this.wordHighlight); 166 | this.wordHighlight = this.imageClient.addOverlay(wrd.hpos, wrd.vpos, wrd.width, wrd.height, (setColor ? setColor : "blue")); 167 | }, 168 | 169 | appendCorrectSegLink: function(lineDiv) { 170 | var correctLink = new Element("a", { 171 | "style": "color: red; font-weight: bold; margin-right: 40px; text-decoration: underline", 172 | "id": lineDiv.id + ":corrector" 173 | }); 174 | 175 | correctLink.insert("!!!"); 176 | lineDiv.insert(correctLink); 177 | var _this = this; 178 | correctLink.observe("click", function(e) { 179 | _this.saveCurrentUpdate(); 180 | _this.lockLineClick = true; 181 | new SegEdit(_this.findLine(lineDiv.id), _this.imageClient, lineDiv, _this); 182 | }); 183 | 184 | }, 185 | 186 | appendSegLink: function(lineDiv) { 187 | var correctLink = new Element("a", { 188 | "style": "float: right; color: blue; font-weight: bold; margin-right: 3px; text-decoration: underline", 189 | "class": "seg_corrector", 190 | "id": lineDiv.id + ":corrector1" 191 | }); 192 | 193 | correctLink.insert("..."); 194 | lineDiv.insert(correctLink); 195 | var _this = this; 196 | correctLink.observe("click", function(e) { 197 | _this.saveCurrentUpdate(); 198 | _this.lockLineClick = true; 199 | new SegEdit(_this.findLine(lineDiv.id), _this.imageClient, lineDiv, _this); 200 | }); 201 | correctLink.hide(); 202 | }, 203 | 204 | updateSegments: function(deletions, resizings, insertions, contentUpdates, lineDiv) { 205 | var _this = this; 206 | var altoLine = this.findLine(lineDiv.id); 207 | 208 | insertions.each(function(insert) { 209 | var spans = lineDiv.childElements(); 210 | for(var i = 0; i < spans.length; ++i) { 211 | var word = _this.findWord(spans[i].id); 212 | var nextWord = (i == spans.length - 1 ? null : _this.findWord(spans[i+1].id)) 213 | if((word && nextWord && word.hpos < insert.hpos && nextWord.hpos > insert.hpos) || (word && !nextWord && word.hpos < insert.hpos)) { 214 | var newSpan = new Element("span", {"id": lineDiv.ancestors()[0].id + ":" + lineDiv.id + ":" + insert.id, "style": "color: green"}); 215 | newSpan.insert(insert.content); 216 | spans[i].insert({after: newSpan}); 217 | newSpan.insert({before: " "}); 218 | altoLine.strings.splice(i,0, insert); 219 | _this.readorder.splice(_this.readorder.indexOf(spans[i].id)+1, 0, newSpan.id); 220 | newSpan.observe("click", function(e) { 221 | _this.editWord(this); 222 | }); 223 | 224 | newSpan.observe("mouseover", function(e) { 225 | this.style.backgroundColor = "#ddf"; 226 | this.style.cursor = "pointer"; 227 | _this.highlightWord(insert); 228 | }); 229 | 230 | newSpan.observe("mouseout", function(e) { 231 | this.style.backgroundColor = "transparent"; 232 | }); 233 | 234 | _this.insertUpdateField("insert", newSpan.id, "after", spans[i].id); 235 | _this.insertUpdateField("insert", newSpan.id, "width", insert.width); 236 | _this.insertUpdateField("insert", newSpan.id, "hpos", insert.hpos); 237 | _this.insertUpdateField("insert", newSpan.id, "vpos", insert.vpos); 238 | _this.insertUpdateField("insert", newSpan.id, "height", insert.height); 239 | _this.insertUpdateField("insert", newSpan.id, "content", insert.content); 240 | break; 241 | } 242 | } 243 | }); 244 | 245 | contentUpdates.each(function(cu) { 246 | $(lineDiv.ancestors()[0].id + ":" + lineDiv.id + ":" + cu.id).innerHTML = cu.content; 247 | _this.insertUpdateField("update", lineDiv.ancestors()[0].id + ":" + lineDiv.id + ":" + cu.id, "content", cu.content); 248 | }); 249 | 250 | resizings.each(function(resize) { 251 | var word = _this.findWord(resize.id); 252 | if(word) { 253 | word.hpos += resize.left; 254 | word.width += resize.width; 255 | _this.insertUpdateField("update", resize.id, "width", word.width); 256 | _this.insertUpdateField("update", resize.id, "hpos", word.hpos); 257 | } 258 | }) 259 | 260 | deletions.each(function(d) { 261 | $(d).remove(); 262 | _this.readorder.remove(_this.readorder.indexOf(d)); 263 | _this.insertUpdateField("update", d, "delete", "true"); 264 | }); 265 | if(this.validateSegments(lineDiv) && $(lineDiv.id + ":corrector")) 266 | $(lineDiv.id + ":corrector").remove(); 267 | }, 268 | 269 | insertUpdateField: function(action, id, key, value) { 270 | var existing = $$("#update_form input").detect(function(inp) { 271 | return inp.getAttribute("name") == action + "[" + id + "][" + key + "]"; 272 | }); 273 | 274 | if(existing) { 275 | existing.value = value; 276 | } else { 277 | $('update_form').insert(new Element("input", { 278 | "name": action + "[" + id + "][" + key + "]", 279 | "value": value, 280 | "type": "hidden" 281 | })); 282 | } 283 | }, 284 | 285 | validateSegments: function(lineDiv) { 286 | var spans = lineDiv.childElements(); 287 | for(var x = 0; x < spans.length; x++) { 288 | if(spans[x].innerHTML == " " || spans[x].innerHTML == " " || spans[x].innerHTML.match(/[^\s]+\s+[^\s]+/) ) { 289 | return false; 290 | } 291 | } 292 | return true; 293 | }, 294 | 295 | saveCurrentUpdate: function() { 296 | if(this.current_edit) { 297 | var span = $(this.current_edit.input.name); 298 | var lineDiv = this.current_edit.input.ancestors()[0]; 299 | this.current_edit.input.stopObserving("keypress"); 300 | if(this.current_edit.input.value == "") 301 | this.current_edit.input.value = " "; 302 | 303 | if(this.current_edit.input.value != span.innerHTML) { 304 | this.updates[this.current_edit.input.name] = this.current_edit.input.value; 305 | 306 | this.insertUpdateField("update", this.current_edit.input.name, "content", this.current_edit.input.value); 307 | 308 | span.style.color = "green"; 309 | } 310 | span.innerHTML = this.current_edit.input.value; 311 | span.show(); 312 | 313 | if(this.validateSegments(lineDiv)) { 314 | if($(lineDiv.id + ":corrector")) 315 | $(lineDiv.id + ":corrector").remove(); 316 | } else if(!$(lineDiv.id + ":corrector")) 317 | this.appendCorrectSegLink(lineDiv); 318 | 319 | this.current_edit.input.stopObserving("mouseover"); 320 | this.current_edit.input.stopObserving("keydown"); 321 | this.current_edit.input.remove(); 322 | this.current_edit = null; 323 | } 324 | return true; 325 | }, 326 | 327 | editWord: function(elem) { 328 | if(this.saveCurrentUpdate()) { 329 | var input = new Element("input", {"type": "text", "value": elem.innerHTML.replace(" ", ""), "name": elem.id, "style": "width: " + (10*elem.innerHTML.length) + "px; color: " + elem.style.color, "id": elem.id + "_input"}); 330 | this.highlightWord(this.findWord(elem.id), "#2A2"); 331 | this.current_edit = {"input": input} 332 | elem.hide(); 333 | elem.insert({after: input}); 334 | 335 | var _this = this; 336 | input.observe("mouseover", function(e) { 337 | _this.highlightWord(_this.findWord(this.name)); 338 | }); 339 | input.observe("keydown", function(e) { 340 | var k = e.keyCode || e.which; 341 | if(k == 9) { 342 | _this.saveCurrentUpdate(); 343 | if(e.shiftKey == 1) _this.editPrevNext(this.name, -1); 344 | else _this.editPrevNext(this.name, 1); 345 | return preventDefault(e); 346 | } else if(k == 13) { 347 | _this.saveCurrentUpdate(); 348 | return preventDefault(e); 349 | } else if(k == 38) { 350 | _this.editUpDown(this.ancestors()[0].id, -1); 351 | return preventDefault(e); 352 | } else if(k == 40) { 353 | _this.editUpDown(this.ancestors()[0].id, 1); 354 | return preventDefault(e); 355 | } else if(k == 39 || k == 37) { 356 | var caretPos = null; 357 | if(document.selection) { 358 | var r = document.selection.createRange(); 359 | r.moveEnd('character', this.value.length); 360 | caretPos = this.value.lastIndexOf(r.text); 361 | } else 362 | caretPos = this.selectionStart; 363 | 364 | if(caretPos == 0 && k == 37) { 365 | _this.editPrevNext(this.name, -1); 366 | return preventDefault(e); 367 | } else if(caretPos == this.value.length && k == 39) { 368 | _this.editPrevNext(this.name, 1); 369 | return preventDefault(e); 370 | } 371 | } 372 | }); 373 | window.setTimeout("$('" + elem.id + "_input').focus(); $('" + elem.id + "_input').select()", 50); 374 | } 375 | }, 376 | 377 | editPrevNext: function(word_id, direction) { 378 | var i = 0; 379 | for(i = 0; i < this.readorder.length; ++i) 380 | if(this.readorder[i] == word_id) 381 | break; 382 | 383 | if(this.readorder[i+direction]) { 384 | this.editWord($(this.readorder[i+direction])); 385 | var editDiv = $(this.readorder[i+direction]).ancestors()[0]; 386 | if($(word_id).ancestors()[0] != editDiv) 387 | this.zoomToLineAt(this.findLine(editDiv.id), null, this.scrolledLinePos(editDiv)); 388 | } 389 | }, 390 | 391 | editUpDown: function(div_id, direction) { 392 | var lineDivIds = $$("#" + this.textContainer.id + " div.altoLineDiv").map(function(d) { 393 | return d.id 394 | }); 395 | 396 | var curIndex = lineDivIds.indexOf(div_id); 397 | if(lineDivIds[curIndex + direction]) { 398 | var editDiv = $(lineDivIds[curIndex + direction]); 399 | this.editWord(editDiv.childElements()[1]); 400 | this.zoomToLineAt(this.findLine(editDiv.id), null, this.scrolledLinePos(editDiv)); 401 | } 402 | } 403 | }); 404 | -------------------------------------------------------------------------------- /static/example.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pixel 5 | 6 | http://localhost/alto_edit_pub/img/example_image.jpeg 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 2010-01-23T11:51:05 19 | Planman Technologies India Pvt. Ltd. 20 | ABBYY Fine Reader Engine 9.0 21 | 22 | ABBYY FineReader 9.0, Russia 23 | FineReader 9.0 24 | 9.0 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | -------------------------------------------------------------------------------- /static/alto_edit/js/effects.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005-2008 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 (Prototype.Browser.WebKit) window.scrollBy(0,0); 47 | return element; 48 | }; 49 | 50 | Element.getInlineOpacity = function(element){ 51 | return $(element).style.opacity || ''; 52 | }; 53 | 54 | Element.forceRerendering = function(element) { 55 | try { 56 | element = $(element); 57 | var n = document.createTextNode(' '); 58 | element.appendChild(n); 59 | element.removeChild(n); 60 | } catch(e) { } 61 | }; 62 | 63 | /*--------------------------------------------------------------------------*/ 64 | 65 | var Effect = { 66 | _elementDoesNotExistError: { 67 | name: 'ElementDoesNotExistError', 68 | message: 'The specified DOM element does not exist, but is required for this effect to operate' 69 | }, 70 | Transitions: { 71 | linear: Prototype.K, 72 | sinoidal: function(pos) { 73 | return (-Math.cos(pos*Math.PI)/2) + .5; 74 | }, 75 | reverse: function(pos) { 76 | return 1-pos; 77 | }, 78 | flicker: function(pos) { 79 | var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4; 80 | return pos > 1 ? 1 : pos; 81 | }, 82 | wobble: function(pos) { 83 | return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5; 84 | }, 85 | pulse: function(pos, pulses) { 86 | return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5; 87 | }, 88 | spring: function(pos) { 89 | return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6)); 90 | }, 91 | none: function(pos) { 92 | return 0; 93 | }, 94 | full: function(pos) { 95 | return 1; 96 | } 97 | }, 98 | DefaultOptions: { 99 | duration: 1.0, // seconds 100 | fps: 100, // 100= assume 66fps max. 101 | sync: false, // true for combining 102 | from: 0.0, 103 | to: 1.0, 104 | delay: 0.0, 105 | queue: 'parallel' 106 | }, 107 | tagifyText: function(element) { 108 | var tagifyStyle = 'position:relative'; 109 | if (Prototype.Browser.IE) tagifyStyle += ';zoom:1'; 110 | 111 | element = $(element); 112 | $A(element.childNodes).each( function(child) { 113 | if (child.nodeType==3) { 114 | child.nodeValue.toArray().each( function(character) { 115 | element.insertBefore( 116 | new Element('span', {style: tagifyStyle}).update( 117 | character == ' ' ? String.fromCharCode(160) : character), 118 | child); 119 | }); 120 | Element.remove(child); 121 | } 122 | }); 123 | }, 124 | multiple: function(element, effect) { 125 | var elements; 126 | if (((typeof element == 'object') || 127 | Object.isFunction(element)) && 128 | (element.length)) 129 | elements = element; 130 | else 131 | elements = $(element).childNodes; 132 | 133 | var options = Object.extend({ 134 | speed: 0.1, 135 | delay: 0.0 136 | }, arguments[2] || { }); 137 | var masterDelay = options.delay; 138 | 139 | $A(elements).each( function(element, index) { 140 | new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay })); 141 | }); 142 | }, 143 | PAIRS: { 144 | 'slide': ['SlideDown','SlideUp'], 145 | 'blind': ['BlindDown','BlindUp'], 146 | 'appear': ['Appear','Fade'] 147 | }, 148 | toggle: function(element, effect) { 149 | element = $(element); 150 | effect = (effect || 'appear').toLowerCase(); 151 | var options = Object.extend({ 152 | queue: { position:'end', scope:(element.id || 'global'), limit: 1 } 153 | }, arguments[2] || { }); 154 | Effect[element.visible() ? 155 | Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options); 156 | } 157 | }; 158 | 159 | Effect.DefaultOptions.transition = Effect.Transitions.sinoidal; 160 | 161 | /* ------------- core effects ------------- */ 162 | 163 | Effect.ScopedQueue = Class.create(Enumerable, { 164 | initialize: function() { 165 | this.effects = []; 166 | this.interval = null; 167 | }, 168 | _each: function(iterator) { 169 | this.effects._each(iterator); 170 | }, 171 | add: function(effect) { 172 | var timestamp = new Date().getTime(); 173 | 174 | var position = Object.isString(effect.options.queue) ? 175 | effect.options.queue : effect.options.queue.position; 176 | 177 | switch(position) { 178 | case 'front': 179 | // move unstarted effects after this effect 180 | this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) { 181 | e.startOn += effect.finishOn; 182 | e.finishOn += effect.finishOn; 183 | }); 184 | break; 185 | case 'with-last': 186 | timestamp = this.effects.pluck('startOn').max() || timestamp; 187 | break; 188 | case 'end': 189 | // start effect after last queued effect has finished 190 | timestamp = this.effects.pluck('finishOn').max() || timestamp; 191 | break; 192 | } 193 | 194 | effect.startOn += timestamp; 195 | effect.finishOn += timestamp; 196 | 197 | if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)) 198 | this.effects.push(effect); 199 | 200 | if (!this.interval) 201 | this.interval = setInterval(this.loop.bind(this), 15); 202 | }, 203 | remove: function(effect) { 204 | this.effects = this.effects.reject(function(e) { return e==effect }); 205 | if (this.effects.length == 0) { 206 | clearInterval(this.interval); 207 | this.interval = null; 208 | } 209 | }, 210 | loop: function() { 211 | var timePos = new Date().getTime(); 212 | for(var i=0, len=this.effects.length;i= this.startOn) { 279 | if (timePos >= this.finishOn) { 280 | this.render(1.0); 281 | this.cancel(); 282 | this.event('beforeFinish'); 283 | if (this.finish) this.finish(); 284 | this.event('afterFinish'); 285 | return; 286 | } 287 | var pos = (timePos - this.startOn) / this.totalTime, 288 | frame = (pos * this.totalFrames).round(); 289 | if (frame > this.currentFrame) { 290 | this.render(pos); 291 | this.currentFrame = frame; 292 | } 293 | } 294 | }, 295 | cancel: function() { 296 | if (!this.options.sync) 297 | Effect.Queues.get(Object.isString(this.options.queue) ? 298 | 'global' : this.options.queue.scope).remove(this); 299 | this.state = 'finished'; 300 | }, 301 | event: function(eventName) { 302 | if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this); 303 | if (this.options[eventName]) this.options[eventName](this); 304 | }, 305 | inspect: function() { 306 | var data = $H(); 307 | for(property in this) 308 | if (!Object.isFunction(this[property])) data.set(property, this[property]); 309 | return '#'; 310 | } 311 | }); 312 | 313 | Effect.Parallel = Class.create(Effect.Base, { 314 | initialize: function(effects) { 315 | this.effects = effects || []; 316 | this.start(arguments[1]); 317 | }, 318 | update: function(position) { 319 | this.effects.invoke('render', position); 320 | }, 321 | finish: function(position) { 322 | this.effects.each( function(effect) { 323 | effect.render(1.0); 324 | effect.cancel(); 325 | effect.event('beforeFinish'); 326 | if (effect.finish) effect.finish(position); 327 | effect.event('afterFinish'); 328 | }); 329 | } 330 | }); 331 | 332 | Effect.Tween = Class.create(Effect.Base, { 333 | initialize: function(object, from, to) { 334 | object = Object.isString(object) ? $(object) : object; 335 | var args = $A(arguments), method = args.last(), 336 | options = args.length == 5 ? args[3] : null; 337 | this.method = Object.isFunction(method) ? method.bind(object) : 338 | Object.isFunction(object[method]) ? object[method].bind(object) : 339 | function(value) { object[method] = value }; 340 | this.start(Object.extend({ from: from, to: to }, options || { })); 341 | }, 342 | update: function(position) { 343 | this.method(position); 344 | } 345 | }); 346 | 347 | Effect.Event = Class.create(Effect.Base, { 348 | initialize: function() { 349 | this.start(Object.extend({ duration: 0 }, arguments[0] || { })); 350 | }, 351 | update: Prototype.emptyFunction 352 | }); 353 | 354 | Effect.Opacity = Class.create(Effect.Base, { 355 | initialize: function(element) { 356 | this.element = $(element); 357 | if (!this.element) throw(Effect._elementDoesNotExistError); 358 | // make this work on IE on elements without 'layout' 359 | if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) 360 | this.element.setStyle({zoom: 1}); 361 | var options = Object.extend({ 362 | from: this.element.getOpacity() || 0.0, 363 | to: 1.0 364 | }, arguments[1] || { }); 365 | this.start(options); 366 | }, 367 | update: function(position) { 368 | this.element.setOpacity(position); 369 | } 370 | }); 371 | 372 | Effect.Move = Class.create(Effect.Base, { 373 | initialize: function(element) { 374 | this.element = $(element); 375 | if (!this.element) throw(Effect._elementDoesNotExistError); 376 | var options = Object.extend({ 377 | x: 0, 378 | y: 0, 379 | mode: 'relative' 380 | }, arguments[1] || { }); 381 | this.start(options); 382 | }, 383 | setup: function() { 384 | this.element.makePositioned(); 385 | this.originalLeft = parseFloat(this.element.getStyle('left') || '0'); 386 | this.originalTop = parseFloat(this.element.getStyle('top') || '0'); 387 | if (this.options.mode == 'absolute') { 388 | this.options.x = this.options.x - this.originalLeft; 389 | this.options.y = this.options.y - this.originalTop; 390 | } 391 | }, 392 | update: function(position) { 393 | this.element.setStyle({ 394 | left: (this.options.x * position + this.originalLeft).round() + 'px', 395 | top: (this.options.y * position + this.originalTop).round() + 'px' 396 | }); 397 | } 398 | }); 399 | 400 | // for backwards compatibility 401 | Effect.MoveBy = function(element, toTop, toLeft) { 402 | return new Effect.Move(element, 403 | Object.extend({ x: toLeft, y: toTop }, arguments[3] || { })); 404 | }; 405 | 406 | Effect.Scale = Class.create(Effect.Base, { 407 | initialize: function(element, percent) { 408 | this.element = $(element); 409 | if (!this.element) throw(Effect._elementDoesNotExistError); 410 | var options = Object.extend({ 411 | scaleX: true, 412 | scaleY: true, 413 | scaleContent: true, 414 | scaleFromCenter: false, 415 | scaleMode: 'box', // 'box' or 'contents' or { } with provided values 416 | scaleFrom: 100.0, 417 | scaleTo: percent 418 | }, arguments[2] || { }); 419 | this.start(options); 420 | }, 421 | setup: function() { 422 | this.restoreAfterFinish = this.options.restoreAfterFinish || false; 423 | this.elementPositioning = this.element.getStyle('position'); 424 | 425 | this.originalStyle = { }; 426 | ['top','left','width','height','fontSize'].each( function(k) { 427 | this.originalStyle[k] = this.element.style[k]; 428 | }.bind(this)); 429 | 430 | this.originalTop = this.element.offsetTop; 431 | this.originalLeft = this.element.offsetLeft; 432 | 433 | var fontSize = this.element.getStyle('font-size') || '100%'; 434 | ['em','px','%','pt'].each( function(fontSizeType) { 435 | if (fontSize.indexOf(fontSizeType)>0) { 436 | this.fontSize = parseFloat(fontSize); 437 | this.fontSizeType = fontSizeType; 438 | } 439 | }.bind(this)); 440 | 441 | this.factor = (this.options.scaleTo - this.options.scaleFrom)/100; 442 | 443 | this.dims = null; 444 | if (this.options.scaleMode=='box') 445 | this.dims = [this.element.offsetHeight, this.element.offsetWidth]; 446 | if (/^content/.test(this.options.scaleMode)) 447 | this.dims = [this.element.scrollHeight, this.element.scrollWidth]; 448 | if (!this.dims) 449 | this.dims = [this.options.scaleMode.originalHeight, 450 | this.options.scaleMode.originalWidth]; 451 | }, 452 | update: function(position) { 453 | var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position); 454 | if (this.options.scaleContent && this.fontSize) 455 | this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType }); 456 | this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale); 457 | }, 458 | finish: function(position) { 459 | if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle); 460 | }, 461 | setDimensions: function(height, width) { 462 | var d = { }; 463 | if (this.options.scaleX) d.width = width.round() + 'px'; 464 | if (this.options.scaleY) d.height = height.round() + 'px'; 465 | if (this.options.scaleFromCenter) { 466 | var topd = (height - this.dims[0])/2; 467 | var leftd = (width - this.dims[1])/2; 468 | if (this.elementPositioning == 'absolute') { 469 | if (this.options.scaleY) d.top = this.originalTop-topd + 'px'; 470 | if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px'; 471 | } else { 472 | if (this.options.scaleY) d.top = -topd + 'px'; 473 | if (this.options.scaleX) d.left = -leftd + 'px'; 474 | } 475 | } 476 | this.element.setStyle(d); 477 | } 478 | }); 479 | 480 | Effect.Highlight = Class.create(Effect.Base, { 481 | initialize: function(element) { 482 | this.element = $(element); 483 | if (!this.element) throw(Effect._elementDoesNotExistError); 484 | var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { }); 485 | this.start(options); 486 | }, 487 | setup: function() { 488 | // Prevent executing on elements not in the layout flow 489 | if (this.element.getStyle('display')=='none') { this.cancel(); return; } 490 | // Disable background image during the effect 491 | this.oldStyle = { }; 492 | if (!this.options.keepBackgroundImage) { 493 | this.oldStyle.backgroundImage = this.element.getStyle('background-image'); 494 | this.element.setStyle({backgroundImage: 'none'}); 495 | } 496 | if (!this.options.endcolor) 497 | this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff'); 498 | if (!this.options.restorecolor) 499 | this.options.restorecolor = this.element.getStyle('background-color'); 500 | // init color calculations 501 | this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this)); 502 | this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this)); 503 | }, 504 | update: function(position) { 505 | this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){ 506 | return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) }); 507 | }, 508 | finish: function() { 509 | this.element.setStyle(Object.extend(this.oldStyle, { 510 | backgroundColor: this.options.restorecolor 511 | })); 512 | } 513 | }); 514 | 515 | Effect.ScrollTo = function(element) { 516 | var options = arguments[1] || { }, 517 | scrollOffsets = document.viewport.getScrollOffsets(), 518 | elementOffsets = $(element).cumulativeOffset(); 519 | 520 | if (options.offset) elementOffsets[1] += options.offset; 521 | 522 | return new Effect.Tween(null, 523 | scrollOffsets.top, 524 | elementOffsets[1], 525 | options, 526 | function(p){ scrollTo(scrollOffsets.left, p.round()); } 527 | ); 528 | }; 529 | 530 | /* ------------- combination effects ------------- */ 531 | 532 | Effect.Fade = function(element) { 533 | element = $(element); 534 | var oldOpacity = element.getInlineOpacity(); 535 | var options = Object.extend({ 536 | from: element.getOpacity() || 1.0, 537 | to: 0.0, 538 | afterFinishInternal: function(effect) { 539 | if (effect.options.to!=0) return; 540 | effect.element.hide().setStyle({opacity: oldOpacity}); 541 | } 542 | }, arguments[1] || { }); 543 | return new Effect.Opacity(element,options); 544 | }; 545 | 546 | Effect.Appear = function(element) { 547 | element = $(element); 548 | var options = Object.extend({ 549 | from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0), 550 | to: 1.0, 551 | // force Safari to render floated elements properly 552 | afterFinishInternal: function(effect) { 553 | effect.element.forceRerendering(); 554 | }, 555 | beforeSetup: function(effect) { 556 | effect.element.setOpacity(effect.options.from).show(); 557 | }}, arguments[1] || { }); 558 | return new Effect.Opacity(element,options); 559 | }; 560 | 561 | Effect.Puff = function(element) { 562 | element = $(element); 563 | var oldStyle = { 564 | opacity: element.getInlineOpacity(), 565 | position: element.getStyle('position'), 566 | top: element.style.top, 567 | left: element.style.left, 568 | width: element.style.width, 569 | height: element.style.height 570 | }; 571 | return new Effect.Parallel( 572 | [ new Effect.Scale(element, 200, 573 | { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }), 574 | new Effect.Opacity(element, { sync: true, to: 0.0 } ) ], 575 | Object.extend({ duration: 1.0, 576 | beforeSetupInternal: function(effect) { 577 | Position.absolutize(effect.effects[0].element); 578 | }, 579 | afterFinishInternal: function(effect) { 580 | effect.effects[0].element.hide().setStyle(oldStyle); } 581 | }, arguments[1] || { }) 582 | ); 583 | }; 584 | 585 | Effect.BlindUp = function(element) { 586 | element = $(element); 587 | element.makeClipping(); 588 | return new Effect.Scale(element, 0, 589 | Object.extend({ scaleContent: false, 590 | scaleX: false, 591 | restoreAfterFinish: true, 592 | afterFinishInternal: function(effect) { 593 | effect.element.hide().undoClipping(); 594 | } 595 | }, arguments[1] || { }) 596 | ); 597 | }; 598 | 599 | Effect.BlindDown = function(element) { 600 | element = $(element); 601 | var elementDimensions = element.getDimensions(); 602 | return new Effect.Scale(element, 100, Object.extend({ 603 | scaleContent: false, 604 | scaleX: false, 605 | scaleFrom: 0, 606 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 607 | restoreAfterFinish: true, 608 | afterSetup: function(effect) { 609 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 610 | }, 611 | afterFinishInternal: function(effect) { 612 | effect.element.undoClipping(); 613 | } 614 | }, arguments[1] || { })); 615 | }; 616 | 617 | Effect.SwitchOff = function(element) { 618 | element = $(element); 619 | var oldOpacity = element.getInlineOpacity(); 620 | return new Effect.Appear(element, Object.extend({ 621 | duration: 0.4, 622 | from: 0, 623 | transition: Effect.Transitions.flicker, 624 | afterFinishInternal: function(effect) { 625 | new Effect.Scale(effect.element, 1, { 626 | duration: 0.3, scaleFromCenter: true, 627 | scaleX: false, scaleContent: false, restoreAfterFinish: true, 628 | beforeSetup: function(effect) { 629 | effect.element.makePositioned().makeClipping(); 630 | }, 631 | afterFinishInternal: function(effect) { 632 | effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity}); 633 | } 634 | }); 635 | } 636 | }, arguments[1] || { })); 637 | }; 638 | 639 | Effect.DropOut = function(element) { 640 | element = $(element); 641 | var oldStyle = { 642 | top: element.getStyle('top'), 643 | left: element.getStyle('left'), 644 | opacity: element.getInlineOpacity() }; 645 | return new Effect.Parallel( 646 | [ new Effect.Move(element, {x: 0, y: 100, sync: true }), 647 | new Effect.Opacity(element, { sync: true, to: 0.0 }) ], 648 | Object.extend( 649 | { duration: 0.5, 650 | beforeSetup: function(effect) { 651 | effect.effects[0].element.makePositioned(); 652 | }, 653 | afterFinishInternal: function(effect) { 654 | effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle); 655 | } 656 | }, arguments[1] || { })); 657 | }; 658 | 659 | Effect.Shake = function(element) { 660 | element = $(element); 661 | var options = Object.extend({ 662 | distance: 20, 663 | duration: 0.5 664 | }, arguments[1] || {}); 665 | var distance = parseFloat(options.distance); 666 | var split = parseFloat(options.duration) / 10.0; 667 | var oldStyle = { 668 | top: element.getStyle('top'), 669 | left: element.getStyle('left') }; 670 | return new Effect.Move(element, 671 | { x: distance, y: 0, duration: split, afterFinishInternal: function(effect) { 672 | new Effect.Move(effect.element, 673 | { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 674 | new Effect.Move(effect.element, 675 | { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 676 | new Effect.Move(effect.element, 677 | { x: -distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 678 | new Effect.Move(effect.element, 679 | { x: distance*2, y: 0, duration: split*2, afterFinishInternal: function(effect) { 680 | new Effect.Move(effect.element, 681 | { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) { 682 | effect.element.undoPositioned().setStyle(oldStyle); 683 | }}); }}); }}); }}); }}); }}); 684 | }; 685 | 686 | Effect.SlideDown = function(element) { 687 | element = $(element).cleanWhitespace(); 688 | // SlideDown need to have the content of the element wrapped in a container element with fixed height! 689 | var oldInnerBottom = element.down().getStyle('bottom'); 690 | var elementDimensions = element.getDimensions(); 691 | return new Effect.Scale(element, 100, Object.extend({ 692 | scaleContent: false, 693 | scaleX: false, 694 | scaleFrom: window.opera ? 0 : 1, 695 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 696 | restoreAfterFinish: true, 697 | afterSetup: function(effect) { 698 | effect.element.makePositioned(); 699 | effect.element.down().makePositioned(); 700 | if (window.opera) effect.element.setStyle({top: ''}); 701 | effect.element.makeClipping().setStyle({height: '0px'}).show(); 702 | }, 703 | afterUpdateInternal: function(effect) { 704 | effect.element.down().setStyle({bottom: 705 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 706 | }, 707 | afterFinishInternal: function(effect) { 708 | effect.element.undoClipping().undoPositioned(); 709 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); } 710 | }, arguments[1] || { }) 711 | ); 712 | }; 713 | 714 | Effect.SlideUp = function(element) { 715 | element = $(element).cleanWhitespace(); 716 | var oldInnerBottom = element.down().getStyle('bottom'); 717 | var elementDimensions = element.getDimensions(); 718 | return new Effect.Scale(element, window.opera ? 0 : 1, 719 | Object.extend({ scaleContent: false, 720 | scaleX: false, 721 | scaleMode: 'box', 722 | scaleFrom: 100, 723 | scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width}, 724 | restoreAfterFinish: true, 725 | afterSetup: function(effect) { 726 | effect.element.makePositioned(); 727 | effect.element.down().makePositioned(); 728 | if (window.opera) effect.element.setStyle({top: ''}); 729 | effect.element.makeClipping().show(); 730 | }, 731 | afterUpdateInternal: function(effect) { 732 | effect.element.down().setStyle({bottom: 733 | (effect.dims[0] - effect.element.clientHeight) + 'px' }); 734 | }, 735 | afterFinishInternal: function(effect) { 736 | effect.element.hide().undoClipping().undoPositioned(); 737 | effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); 738 | } 739 | }, arguments[1] || { }) 740 | ); 741 | }; 742 | 743 | // Bug in opera makes the TD containing this element expand for a instance after finish 744 | Effect.Squish = function(element) { 745 | return new Effect.Scale(element, window.opera ? 1 : 0, { 746 | restoreAfterFinish: true, 747 | beforeSetup: function(effect) { 748 | effect.element.makeClipping(); 749 | }, 750 | afterFinishInternal: function(effect) { 751 | effect.element.hide().undoClipping(); 752 | } 753 | }); 754 | }; 755 | 756 | Effect.Grow = function(element) { 757 | element = $(element); 758 | var options = Object.extend({ 759 | direction: 'center', 760 | moveTransition: Effect.Transitions.sinoidal, 761 | scaleTransition: Effect.Transitions.sinoidal, 762 | opacityTransition: Effect.Transitions.full 763 | }, arguments[1] || { }); 764 | var oldStyle = { 765 | top: element.style.top, 766 | left: element.style.left, 767 | height: element.style.height, 768 | width: element.style.width, 769 | opacity: element.getInlineOpacity() }; 770 | 771 | var dims = element.getDimensions(); 772 | var initialMoveX, initialMoveY; 773 | var moveX, moveY; 774 | 775 | switch (options.direction) { 776 | case 'top-left': 777 | initialMoveX = initialMoveY = moveX = moveY = 0; 778 | break; 779 | case 'top-right': 780 | initialMoveX = dims.width; 781 | initialMoveY = moveY = 0; 782 | moveX = -dims.width; 783 | break; 784 | case 'bottom-left': 785 | initialMoveX = moveX = 0; 786 | initialMoveY = dims.height; 787 | moveY = -dims.height; 788 | break; 789 | case 'bottom-right': 790 | initialMoveX = dims.width; 791 | initialMoveY = dims.height; 792 | moveX = -dims.width; 793 | moveY = -dims.height; 794 | break; 795 | case 'center': 796 | initialMoveX = dims.width / 2; 797 | initialMoveY = dims.height / 2; 798 | moveX = -dims.width / 2; 799 | moveY = -dims.height / 2; 800 | break; 801 | } 802 | 803 | return new Effect.Move(element, { 804 | x: initialMoveX, 805 | y: initialMoveY, 806 | duration: 0.01, 807 | beforeSetup: function(effect) { 808 | effect.element.hide().makeClipping().makePositioned(); 809 | }, 810 | afterFinishInternal: function(effect) { 811 | new Effect.Parallel( 812 | [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }), 813 | new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }), 814 | new Effect.Scale(effect.element, 100, { 815 | scaleMode: { originalHeight: dims.height, originalWidth: dims.width }, 816 | sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true}) 817 | ], Object.extend({ 818 | beforeSetup: function(effect) { 819 | effect.effects[0].element.setStyle({height: '0px'}).show(); 820 | }, 821 | afterFinishInternal: function(effect) { 822 | effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle); 823 | } 824 | }, options) 825 | ); 826 | } 827 | }); 828 | }; 829 | 830 | Effect.Shrink = function(element) { 831 | element = $(element); 832 | var options = Object.extend({ 833 | direction: 'center', 834 | moveTransition: Effect.Transitions.sinoidal, 835 | scaleTransition: Effect.Transitions.sinoidal, 836 | opacityTransition: Effect.Transitions.none 837 | }, arguments[1] || { }); 838 | var oldStyle = { 839 | top: element.style.top, 840 | left: element.style.left, 841 | height: element.style.height, 842 | width: element.style.width, 843 | opacity: element.getInlineOpacity() }; 844 | 845 | var dims = element.getDimensions(); 846 | var moveX, moveY; 847 | 848 | switch (options.direction) { 849 | case 'top-left': 850 | moveX = moveY = 0; 851 | break; 852 | case 'top-right': 853 | moveX = dims.width; 854 | moveY = 0; 855 | break; 856 | case 'bottom-left': 857 | moveX = 0; 858 | moveY = dims.height; 859 | break; 860 | case 'bottom-right': 861 | moveX = dims.width; 862 | moveY = dims.height; 863 | break; 864 | case 'center': 865 | moveX = dims.width / 2; 866 | moveY = dims.height / 2; 867 | break; 868 | } 869 | 870 | return new Effect.Parallel( 871 | [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }), 872 | new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}), 873 | new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }) 874 | ], Object.extend({ 875 | beforeStartInternal: function(effect) { 876 | effect.effects[0].element.makePositioned().makeClipping(); 877 | }, 878 | afterFinishInternal: function(effect) { 879 | effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); } 880 | }, options) 881 | ); 882 | }; 883 | 884 | Effect.Pulsate = function(element) { 885 | element = $(element); 886 | var options = arguments[1] || { }, 887 | oldOpacity = element.getInlineOpacity(), 888 | transition = options.transition || Effect.Transitions.linear, 889 | reverser = function(pos){ 890 | return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5); 891 | }; 892 | 893 | return new Effect.Opacity(element, 894 | Object.extend(Object.extend({ duration: 2.0, from: 0, 895 | afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); } 896 | }, options), {transition: reverser})); 897 | }; 898 | 899 | Effect.Fold = function(element) { 900 | element = $(element); 901 | var oldStyle = { 902 | top: element.style.top, 903 | left: element.style.left, 904 | width: element.style.width, 905 | height: element.style.height }; 906 | element.makeClipping(); 907 | return new Effect.Scale(element, 5, Object.extend({ 908 | scaleContent: false, 909 | scaleX: false, 910 | afterFinishInternal: function(effect) { 911 | new Effect.Scale(element, 1, { 912 | scaleContent: false, 913 | scaleY: false, 914 | afterFinishInternal: function(effect) { 915 | effect.element.hide().undoClipping().setStyle(oldStyle); 916 | } }); 917 | }}, arguments[1] || { })); 918 | }; 919 | 920 | Effect.Morph = Class.create(Effect.Base, { 921 | initialize: function(element) { 922 | this.element = $(element); 923 | if (!this.element) throw(Effect._elementDoesNotExistError); 924 | var options = Object.extend({ 925 | style: { } 926 | }, arguments[1] || { }); 927 | 928 | if (!Object.isString(options.style)) this.style = $H(options.style); 929 | else { 930 | if (options.style.include(':')) 931 | this.style = options.style.parseStyle(); 932 | else { 933 | this.element.addClassName(options.style); 934 | this.style = $H(this.element.getStyles()); 935 | this.element.removeClassName(options.style); 936 | var css = this.element.getStyles(); 937 | this.style = this.style.reject(function(style) { 938 | return style.value == css[style.key]; 939 | }); 940 | options.afterFinishInternal = function(effect) { 941 | effect.element.addClassName(effect.options.style); 942 | effect.transforms.each(function(transform) { 943 | effect.element.style[transform.style] = ''; 944 | }); 945 | }; 946 | } 947 | } 948 | this.start(options); 949 | }, 950 | 951 | setup: function(){ 952 | function parseColor(color){ 953 | if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff'; 954 | color = color.parseColor(); 955 | return $R(0,2).map(function(i){ 956 | return parseInt( color.slice(i*2+1,i*2+3), 16 ); 957 | }); 958 | } 959 | this.transforms = this.style.map(function(pair){ 960 | var property = pair[0], value = pair[1], unit = null; 961 | 962 | if (value.parseColor('#zzzzzz') != '#zzzzzz') { 963 | value = value.parseColor(); 964 | unit = 'color'; 965 | } else if (property == 'opacity') { 966 | value = parseFloat(value); 967 | if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout)) 968 | this.element.setStyle({zoom: 1}); 969 | } else if (Element.CSS_LENGTH.test(value)) { 970 | var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/); 971 | value = parseFloat(components[1]); 972 | unit = (components.length == 3) ? components[2] : null; 973 | } 974 | 975 | var originalValue = this.element.getStyle(property); 976 | return { 977 | style: property.camelize(), 978 | originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0), 979 | targetValue: unit=='color' ? parseColor(value) : value, 980 | unit: unit 981 | }; 982 | }.bind(this)).reject(function(transform){ 983 | return ( 984 | (transform.originalValue == transform.targetValue) || 985 | ( 986 | transform.unit != 'color' && 987 | (isNaN(transform.originalValue) || isNaN(transform.targetValue)) 988 | ) 989 | ); 990 | }); 991 | }, 992 | update: function(position) { 993 | var style = { }, transform, i = this.transforms.length; 994 | while(i--) 995 | style[(transform = this.transforms[i]).style] = 996 | transform.unit=='color' ? '#'+ 997 | (Math.round(transform.originalValue[0]+ 998 | (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() + 999 | (Math.round(transform.originalValue[1]+ 1000 | (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() + 1001 | (Math.round(transform.originalValue[2]+ 1002 | (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() : 1003 | (transform.originalValue + 1004 | (transform.targetValue - transform.originalValue) * position).toFixed(3) + 1005 | (transform.unit === null ? '' : transform.unit); 1006 | this.element.setStyle(style, true); 1007 | } 1008 | }); 1009 | 1010 | Effect.Transform = Class.create({ 1011 | initialize: function(tracks){ 1012 | this.tracks = []; 1013 | this.options = arguments[1] || { }; 1014 | this.addTracks(tracks); 1015 | }, 1016 | addTracks: function(tracks){ 1017 | tracks.each(function(track){ 1018 | track = $H(track); 1019 | var data = track.values().first(); 1020 | this.tracks.push($H({ 1021 | ids: track.keys().first(), 1022 | effect: Effect.Morph, 1023 | options: { style: data } 1024 | })); 1025 | }.bind(this)); 1026 | return this; 1027 | }, 1028 | play: function(){ 1029 | return new Effect.Parallel( 1030 | this.tracks.map(function(track){ 1031 | var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options'); 1032 | var elements = [$(ids) || $$(ids)].flatten(); 1033 | return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) }); 1034 | }).flatten(), 1035 | this.options 1036 | ); 1037 | } 1038 | }); 1039 | 1040 | Element.CSS_PROPERTIES = $w( 1041 | 'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' + 1042 | 'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' + 1043 | 'borderRightColor borderRightStyle borderRightWidth borderSpacing ' + 1044 | 'borderTopColor borderTopStyle borderTopWidth bottom clip color ' + 1045 | 'fontSize fontWeight height left letterSpacing lineHeight ' + 1046 | 'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+ 1047 | 'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' + 1048 | 'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' + 1049 | 'right textIndent top width wordSpacing zIndex'); 1050 | 1051 | Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/; 1052 | 1053 | String.__parseStyleElement = document.createElement('div'); 1054 | String.prototype.parseStyle = function(){ 1055 | var style, styleRules = $H(); 1056 | if (Prototype.Browser.WebKit) 1057 | style = new Element('div',{style:this}).style; 1058 | else { 1059 | String.__parseStyleElement.innerHTML = '
'; 1060 | style = String.__parseStyleElement.childNodes[0].style; 1061 | } 1062 | 1063 | Element.CSS_PROPERTIES.each(function(property){ 1064 | if (style[property]) styleRules.set(property, style[property]); 1065 | }); 1066 | 1067 | if (Prototype.Browser.IE && this.include('opacity')) 1068 | styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]); 1069 | 1070 | return styleRules; 1071 | }; 1072 | 1073 | if (document.defaultView && document.defaultView.getComputedStyle) { 1074 | Element.getStyles = function(element) { 1075 | var css = document.defaultView.getComputedStyle($(element), null); 1076 | return Element.CSS_PROPERTIES.inject({ }, function(styles, property) { 1077 | styles[property] = css[property]; 1078 | return styles; 1079 | }); 1080 | }; 1081 | } else { 1082 | Element.getStyles = function(element) { 1083 | element = $(element); 1084 | var css = element.currentStyle, styles; 1085 | styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) { 1086 | results[property] = css[property]; 1087 | return results; 1088 | }); 1089 | if (!styles.opacity) styles.opacity = element.getOpacity(); 1090 | return styles; 1091 | }; 1092 | } 1093 | 1094 | Effect.Methods = { 1095 | morph: function(element, style) { 1096 | element = $(element); 1097 | new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { })); 1098 | return element; 1099 | }, 1100 | visualEffect: function(element, effect, options) { 1101 | element = $(element); 1102 | var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1); 1103 | new Effect[klass](element, options); 1104 | return element; 1105 | }, 1106 | highlight: function(element, options) { 1107 | element = $(element); 1108 | new Effect.Highlight(element, options); 1109 | return element; 1110 | } 1111 | }; 1112 | 1113 | $w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+ 1114 | 'pulsate shake puff squish switchOff dropOut').each( 1115 | function(effect) { 1116 | Effect.Methods[effect] = function(element, options){ 1117 | element = $(element); 1118 | Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options); 1119 | return element; 1120 | }; 1121 | } 1122 | ); 1123 | 1124 | $w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each( 1125 | function(f) { Effect.Methods[f] = Element[f]; } 1126 | ); 1127 | 1128 | Element.addMethods(Effect.Methods); --------------------------------------------------------------------------------