diff --git a/.eslintrc.js b/.eslintrc.js index aa594fa7..1222f4a1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,11 @@ module.exports = { "sourceType": "module", "parser": "babel-eslint", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, "extends": "standard", "installedESLint": true, "env": { @@ -13,6 +18,8 @@ module.exports = { "react" ], "rules": { + "react/jsx-uses-react": [2], + "react/jsx-uses-vars": [2], "yoda": [2, "never", { "exceptRange": true }] } } diff --git a/.example-env b/.example-env index 51d89c5d..1afb010b 100644 --- a/.example-env +++ b/.example-env @@ -18,7 +18,6 @@ export SECRET_KEY_BASE='267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c800 # export S3_BUCKET_NAME # export AWS_ACCESS_KEY_ID # export AWS_SECRET_ACCESS_KEY -# export SSO_KEY # # export SMTP_DOMAIN # export SMTP_PASSWORD diff --git a/.simplecov b/.simplecov index b81ebfeb..efee8860 100644 --- a/.simplecov +++ b/.simplecov @@ -1,3 +1,7 @@ if ENV['COVERAGE'] == 'on' - SimpleCov.start 'rails' + SimpleCov.start 'rails' do + add_group 'Policies', 'app/policies' + add_group 'Services', 'app/services' + add_group 'Serializers', 'app/serializers' + end end diff --git a/Gemfile b/Gemfile index 7f34c12e..5f22fdcb 100644 --- a/Gemfile +++ b/Gemfile @@ -25,10 +25,8 @@ gem 'rack-cors' gem 'redis' gem 'slack-notifier' gem 'snorlax' -gem 'uservoice-ruby' # asset stuff -gem 'coffee-rails' gem 'jquery-rails' gem 'jquery-ui-rails' gem 'sass-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 350585ae..519795f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,13 +70,6 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.1) - coffee-rails (4.2.1) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.2.x) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.10.0) concurrent-ruby (1.0.2) debug_inspector (0.0.2) delayed_job (4.1.2) @@ -103,7 +96,6 @@ GEM actionmailer (>= 4.0, < 6) activesupport (>= 4.0, < 6) execjs (2.7.0) - ezcrypto (0.7.2) factory_girl (4.7.0) activesupport (>= 3.0.0) factory_girl_rails (4.7.0) @@ -145,7 +137,6 @@ GEM nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) - oauth (0.5.1) orm_adapter (0.5.0) paperclip (5.1.0) activemodel (>= 4.2.0) @@ -262,10 +253,6 @@ GEM uglifier (3.0.2) execjs (>= 0.3.0, < 3) unicode-display_width (1.1.1) - uservoice-ruby (0.0.11) - ezcrypto (>= 0.7.2) - json (>= 1.7.5) - oauth (>= 0.4.7) warden (1.2.6) rack (>= 1.0) websocket-driver (0.6.4) @@ -282,7 +269,6 @@ DEPENDENCIES better_errors binding_of_caller brakeman - coffee-rails delayed_job delayed_job_active_record devise @@ -315,10 +301,9 @@ DEPENDENCIES snorlax tunemygc uglifier - uservoice-ruby RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.13.2 + 1.13.3 diff --git a/README.md b/README.md index 2ffa9708..1da4e192 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,13 @@ You can find a version of this software running at [metamaps.cc][site-beta], whe Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch by using whichever of these channels you prefer: -## Community - -- To send us a personal message or request an invite to the open beta, get in touch with us at team@metamaps.cc or @metamapps on Twitter. +## How do I learn more? + +- Contact: [team@metamaps.cc](mailto:team@metamaps.cc) or [@metamapps](https://twitter.com/metamapps) on Twitter +- User Documentation: [docs.metamaps.cc](https://docs.metamaps.cc) +- User Community: [hylo.com/c/metamaps](https://www.hylo.com/c/metamaps) +- Development Roadmap: [github.com/metamaps/metamaps/milestones](https://github.com/metamaps/metamaps/milestones) +- To send us a personal message or request an invite to the open beta, get in touch with us via email, Twitter, or Hylo - If you would like to report a bug, please check the [issues][contributing-issues] section in our [contributing instructions][contributing]. - If you would like to get set up as a developer, that's great! Read on for help getting your development environment set up. @@ -51,10 +55,6 @@ We haven't set up instructions for using Vagrant on Windows, but there are instr - [For Windows][windows-installation] -## Contributing guidelines - -Cloning this repository directly is primarily for those wishing to contribute to our codebase. Check out our [contributing instructions][contributing] to get involved. - ## Licensing information This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. @@ -65,7 +65,6 @@ The license can be read [here][license]. Copyright (c) 2016 Connor Turland -[site-blog]: http://blog.metamaps.cc [site-beta]: http://metamaps.cc [license]: https://github.com/metamaps/metamaps/blob/develop/LICENSE [contributing]: https://github.com/metamaps/metamaps/blob/develop/doc/CONTRIBUTING.md diff --git a/app/assets/images/import-example.png b/app/assets/images/import-example.png new file mode 100644 index 00000000..02d59dcd Binary files /dev/null and b/app/assets/images/import-example.png differ diff --git a/app/assets/images/import.png b/app/assets/images/import.png new file mode 100644 index 00000000..29b5b896 Binary files /dev/null and b/app/assets/images/import.png differ diff --git a/app/assets/images/junto.gif b/app/assets/images/junto.gif new file mode 100644 index 00000000..e4a72d4b Binary files /dev/null and b/app/assets/images/junto.gif differ diff --git a/app/assets/images/view-only.png b/app/assets/images/view-only.png new file mode 100644 index 00000000..a4cc262f Binary files /dev/null and b/app/assets/images/view-only.png differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index df086157..051edc8b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,4 +16,3 @@ //= require_directory ./lib //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle -//= require ./src/check-canvas-support diff --git a/app/assets/javascripts/lib/WebSocketMain.swf b/app/assets/javascripts/lib/WebSocketMain.swf deleted file mode 100644 index 20a451f5..00000000 Binary files a/app/assets/javascripts/lib/WebSocketMain.swf and /dev/null differ diff --git a/app/assets/javascripts/lib/WebSocketMainInsecure.swf b/app/assets/javascripts/lib/WebSocketMainInsecure.swf deleted file mode 100644 index 5949ff3d..00000000 Binary files a/app/assets/javascripts/lib/WebSocketMainInsecure.swf and /dev/null differ diff --git a/app/assets/javascripts/lib/attachMediaStream.js b/app/assets/javascripts/lib/attachMediaStream.js deleted file mode 100644 index de3feef5..00000000 --- a/app/assets/javascripts/lib/attachMediaStream.js +++ /dev/null @@ -1,39 +0,0 @@ -var attachMediaStream = function (stream, el, options) { - var URL = window.URL; - var opts = { - autoplay: true, - mirror: false, - muted: false - }; - var element = el || document.createElement('video'); - var item; - - if (options) { - for (item in options) { - opts[item] = options[item]; - } - } - - if (opts.autoplay) element.autoplay = 'autoplay'; - if (opts.muted) element.muted = true; - if (opts.mirror) { - ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { - var styleName = prefix ? prefix + 'Transform' : 'transform'; - element.style[styleName] = 'scaleX(-1)'; - }); - } - - // this first one should work most everywhere now - // but we have a few fallbacks just in case. - if (URL && URL.createObjectURL) { - element.src = URL.createObjectURL(stream); - } else if (element.srcObject) { - element.srcObject = stream; - } else if (element.mozSrcObject) { - element.mozSrcObject = stream; - } else { - return false; - } - - return element; - }; \ No newline at end of file diff --git a/app/assets/javascripts/lib/best_in_place.js b/app/assets/javascripts/lib/best_in_place.js new file mode 100644 index 00000000..5000103f --- /dev/null +++ b/app/assets/javascripts/lib/best_in_place.js @@ -0,0 +1,685 @@ +/* + * BestInPlace (for jQuery) + * version: 3.0.0.alpha (2014) + * + * By Bernat Farrero based on the work of Jan Varwig. + * Examples at http://bernatfarrero.com + * + * Licensed under the MIT: + * http://www.opensource.org/licenses/mit-license.php + * + * @requires jQuery + * + * Usage: + * + * Attention. + * The format of the JSON object given to the select inputs is the following: + * [["key", "value"],["key", "value"]] + * The format of the JSON object given to the checkbox inputs is the following: + * ["falseValue", "trueValue"] + + */ +//= require jquery.autosize + +function BestInPlaceEditor(e) { + 'use strict'; + this.element = e; + this.initOptions(); + this.bindForm(); + this.initPlaceHolder(); + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); +} + +BestInPlaceEditor.prototype = { + // Public Interface Functions ////////////////////////////////////////////// + + activate: function () { + 'use strict'; + var to_display; + if (this.isPlaceHolder()) { + to_display = ""; + } else if (this.original_content) { + to_display = this.original_content; + } else { + switch (this.formType) { + case 'input': + case 'textarea': + if (this.display_raw) { + to_display = this.element.html().replace(/&/gi, '&'); + } + else { + var value = this.element.data('bipValue'); + if (typeof value === 'undefined') { + to_display = ''; + } else if (typeof value === 'string') { + to_display = this.element.data('bipValue').replace(/&/gi, '&'); + } else { + to_display = this.element.data('bipValue'); + } + } + break; + case 'select': + to_display = this.element.html(); + + } + } + + this.oldValue = this.isPlaceHolder() ? "" : this.element.html(); + this.display_value = to_display; + jQuery(this.activator).unbind("click", this.clickHandler); + this.activateForm(); + this.element.trigger(jQuery.Event("best_in_place:activate")); + }, + + abort: function () { + 'use strict'; + this.activateText(this.oldValue); + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:abort")); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + }, + + abortIfConfirm: function () { + 'use strict'; + if (!this.useConfirm) { + this.abort(); + return; + } + + if (confirm(BestInPlaceEditor.defaults.locales[''].confirmMessage)) { + this.abort(); + } + }, + + update: function () { + 'use strict'; + var editor = this, + value = this.getValue(); + + // Avoid request if no change is made + if (this.formType in {"input": 1, "textarea": 1} && value === this.oldValue) { + this.abort(); + return true; + } + + editor.ajax({ + "type": this.requestMethod(), + "dataType": BestInPlaceEditor.defaults.ajaxDataType, + "data": editor.requestData(), + "success": function (data, status, xhr) { + editor.loadSuccessCallback(data, status, xhr); + }, + "error": function (request, error) { + editor.loadErrorCallback(request, error); + } + }); + + + switch (this.formType) { + case "select": + this.previousCollectionValue = value; + + // search for the text for the span + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + break; + + case "checkbox": + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + break; + + default: + if (value !== "") { + if (this.display_raw) { + editor.element.html(value); + } else { + editor.element.text(value); + } + } else { + editor.element.html(this.placeHolder); + } + } + + editor.element.data('bipValue', value); + editor.element.attr('data-bip-value', value); + + editor.element.trigger(jQuery.Event("best_in_place:update")); + + + }, + + activateForm: function () { + 'use strict'; + alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); + }, + + activateText: function (value) { + 'use strict'; + this.element.html(value); + if (this.isPlaceHolder()) { + this.element.html(this.placeHolder); + } + }, + + // Helper Functions //////////////////////////////////////////////////////// + + initOptions: function () { + // Try parent supplied info + 'use strict'; + var self = this; + self.element.parents().each(function () { + var $parent = jQuery(this); + self.url = self.url || $parent.data("bipUrl"); + self.activator = self.activator || $parent.data("bipActivator"); + self.okButton = self.okButton || $parent.data("bipOkButton"); + self.okButtonClass = self.okButtonClass || $parent.data("bipOkButtonClass"); + self.cancelButton = self.cancelButton || $parent.data("bipCancelButton"); + self.cancelButtonClass = self.cancelButtonClass || $parent.data("bipCancelButtonClass"); + self.skipBlur = self.skipBlur || $parent.data("bipSkipBlur"); + }); + + // Load own attributes (overrides all others) + self.url = self.element.data("bipUrl") || self.url || document.location.pathname; + self.collection = self.element.data("bipCollection") || self.collection; + self.formType = self.element.data("bipType") || "input"; + self.objectName = self.element.data("bipObject") || self.objectName; + self.attributeName = self.element.data("bipAttribute") || self.attributeName; + self.activator = self.element.data("bipActivator") || self.element; + self.okButton = self.element.data("bipOkButton") || self.okButton; + self.okButtonClass = self.element.data("bipOkButtonClass") || self.okButtonClass || BestInPlaceEditor.defaults.okButtonClass; + self.cancelButton = self.element.data("bipCancelButton") || self.cancelButton; + self.cancelButtonClass = self.element.data("bipCancelButtonClass") || self.cancelButtonClass || BestInPlaceEditor.defaults.cancelButtonClass; + self.skipBlur = self.element.data("bipSkipBlur") || self.skipBlur || BestInPlaceEditor.defaults.skipBlur; + self.isNewObject = self.element.data("bipNewObject"); + self.dataExtraPayload = self.element.data("bipExtraPayload"); + + // Fix for default values of 0 + if (self.element.data("bipPlaceholder") == null) { + self.placeHolder = BestInPlaceEditor.defaults.locales[''].placeHolder; + } else { + self.placeHolder = self.element.data("bipPlaceholder"); + } + + self.inner_class = self.element.data("bipInnerClass"); + self.html_attrs = self.element.data("bipHtmlAttrs"); + self.original_content = self.element.data("bipOriginalContent") || self.original_content; + + // if set the input won't be satinized + self.display_raw = self.element.data("bip-raw"); + + self.useConfirm = self.element.data("bip-confirm"); + + if (self.formType === "select" || self.formType === "checkbox") { + self.values = self.collection; + self.collectionValue = self.element.data("bipValue") || self.collectionValue; + } + }, + + bindForm: function () { + 'use strict'; + this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; + this.getValue = BestInPlaceEditor.forms[this.formType].getValue; + }, + + + initPlaceHolder: function () { + 'use strict'; + // TODO add placeholder for select and checkbox + if (this.element.html() === "") { + this.element.addClass('bip-placeholder'); + this.element.html(this.placeHolder); + } + }, + + isPlaceHolder: function () { + 'use strict'; + // TODO: It only work when form is deactivated. + // Condition will fail when form is activated + return this.element.html() === "" || this.element.html() === this.placeHolder; + }, + + getValue: function () { + 'use strict'; + alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); + }, + + // Trim and Strips HTML from text + sanitizeValue: function (s) { + 'use strict'; + return jQuery.trim(s); + }, + + requestMethod: function() { + 'use strict'; + return this.isNewObject ? 'post' : BestInPlaceEditor.defaults.ajaxMethod; + }, + + /* Generate the data sent in the POST request */ + requestData: function () { + 'use strict'; + // To prevent xss attacks, a csrf token must be defined as a meta attribute + var csrf_token = jQuery('meta[name=csrf-token]').attr('content'), + csrf_param = jQuery('meta[name=csrf-param]').attr('content'); + + var data = {} + data['_method'] = this.requestMethod() + + data[this.objectName] = this.dataExtraPayload || {} + + data[this.objectName][this.attributeName] = this.getValue() + + if (csrf_param !== undefined && csrf_token !== undefined) { + data[csrf_param] = csrf_token + } + return jQuery.param(data); + }, + + ajax: function (options) { + 'use strict'; + options.url = this.url; + options.beforeSend = function (xhr) { + xhr.setRequestHeader("Accept", "application/json"); + }; + return jQuery.ajax(options); + }, + + // Handlers //////////////////////////////////////////////////////////////// + + loadSuccessCallback: function (data, status, xhr) { + 'use strict'; + data = jQuery.trim(data); + //Update original content with current text. + if (this.display_raw) { + this.original_content = this.element.html(); + } else { + this.original_content = this.element.text(); + } + + if (data && data !== "") { + var response = jQuery.parseJSON(data); + if (response !== null && response.hasOwnProperty("display_as")) { + this.element.data('bip-original-content', this.element.text()); + this.element.html(response.display_as); + } + if (this.isNewObject && response && response[this.objectName]) { + if (response[this.objectName]["id"]) { + this.isNewObject = false + this.url += "/" + response[this.objectName]["id"] // in REST a POST /thing url should become PUT /thing/123 + } + } + } + this.element.toggleClass('bip-placeholder', this.isPlaceHolder()); + + this.element.trigger(jQuery.Event("best_in_place:success"), [data, status, xhr]); + this.element.trigger(jQuery.Event("ajax:success"), [data, status, xhr]); + + // Binding back after being clicked + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + + if (this.collectionValue !== null && this.formType === "select") { + this.collectionValue = this.previousCollectionValue; + this.previousCollectionValue = null; + } + }, + + loadErrorCallback: function (request, error) { + 'use strict'; + this.activateText(this.oldValue); + + this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); + this.element.trigger(jQuery.Event("ajax:error"), request, error); + + // Binding back after being clicked + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + }, + + clickHandler: function (event) { + 'use strict'; + event.preventDefault(); + event.data.editor.activate(); + }, + + setHtmlAttributes: function () { + 'use strict'; + var formField = this.element.find(this.formType); + + if (this.html_attrs) { + var attrs = this.html_attrs; + $.each(attrs, function (key, val) { + formField.attr(key, val); + }); + } + }, + + placeButtons: function (output, field) { + 'use strict'; + if (field.okButton) { + output.append( + jQuery(document.createElement('input')) + .attr('type', 'submit') + .attr('class', field.okButtonClass) + .attr('value', field.okButton) + ); + } + if (field.cancelButton) { + output.append( + jQuery(document.createElement('input')) + .attr('type', 'button') + .attr('class', field.cancelButtonClass) + .attr('value', field.cancelButton) + ); + } + } +}; + + +// Button cases: +// If no buttons, then blur saves, ESC cancels +// If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) +// If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels +// If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels +BestInPlaceEditor.forms = { + "input": { + activateForm: function () { + 'use strict'; + var output = jQuery(document.createElement('form')) + .addClass('form_in_place') + .attr('action', 'javascript:void(0);') + .attr('style', 'display:inline'); + var input_elt = jQuery(document.createElement('input')) + .attr('type', 'text') + .attr('name', this.attributeName) + .val(this.display_value); + + // Add class to form input + if (this.inner_class) { + input_elt.addClass(this.inner_class); + } + + output.append(input_elt); + this.placeButtons(output, this); + + this.element.html(output); + this.setHtmlAttributes(); + + this.element.find("input[type='text']")[0].select(); + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); + if (this.cancelButton) { + this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); + } + if (!this.okButton) { + this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); + } + this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); + this.blurTimer = null; + this.userClicked = false; + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("input").val()); + }, + + // When buttons are present, use a timer on the blur event to give precedence to clicks + inputBlurHandler: function (event) { + 'use strict'; + if (event.data.editor.okButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.abort(); + } + }, 500); + } else { + if (event.data.editor.cancelButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.update(); + } + }, 500); + } else { + event.data.editor.update(); + } + } + }, + + submitHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.update(); + }, + + cancelButtonHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.abort(); + event.stopPropagation(); // Without this, click isn't handled + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abort(); + event.stopImmediatePropagation(); + } + } + }, + + "select": { + activateForm: function () { + 'use strict'; + var output = jQuery(document.createElement('form')) + .attr('action', 'javascript:void(0)') + .attr('style', 'display:inline'), + selected = '', + select_elt = jQuery(document.createElement('select')) + .attr('class', this.inner_class !== null ? this.inner_class : ''), + currentCollectionValue = this.collectionValue, + key, value, + a = this.values; + + $.each(a, function(index, arr){ + key = arr[0]; + value = arr[1]; + var option_elt = jQuery(document.createElement('option')) + .val(key) + .html(value); + + if (currentCollectionValue) { + if (String(key) === String(currentCollectionValue)) option_elt.attr('selected', 'selected'); + } + select_elt.append(option_elt); + }); + output.append(select_elt); + + this.element.html(output); + this.setHtmlAttributes(); + this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); + this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); + this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); + this.element.find("select")[0].focus(); + + // automatically click on the select so you + // don't have to click twice + try { + var e = document.createEvent("MouseEvents"); + e.initMouseEvent("mousedown", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + this.element.find("select")[0].dispatchEvent(e); + } + catch(e) { + // browser doesn't support this, e.g. IE8 + } + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("select").val()); + }, + + blurHandler: function (event) { + 'use strict'; + event.data.editor.update(); + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abort(); + } + } + }, + + "checkbox": { + activateForm: function () { + 'use strict'; + this.collectionValue = !this.getValue(); + this.setHtmlAttributes(); + this.update(); + }, + + getValue: function () { + 'use strict'; + return this.collectionValue; + } + }, + + "textarea": { + activateForm: function () { + 'use strict'; + // grab width and height of text + var width = this.element.css('width'); + var height = this.element.css('height'); + + // construct form + var output = jQuery(document.createElement('form')) + .addClass('form_in_place') + .attr('action', 'javascript:void(0);') + .attr('style', 'display:inline'); + var textarea_elt = jQuery(document.createElement('textarea')) + .attr('name', this.attributeName) + .val(this.sanitizeValue(this.display_value)); + + if (this.inner_class !== null) { + textarea_elt.addClass(this.inner_class); + } + + output.append(textarea_elt); + + this.placeButtons(output, this); + + this.element.html(output); + this.setHtmlAttributes(); + + // set width and height of textarea + jQuery(this.element.find("textarea")[0]).css({'min-width': width, 'min-height': height}); + jQuery(this.element.find("textarea")[0]).autosize(); + + this.element.find("textarea")[0].focus(); + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); + + if (this.cancelButton) { + this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); + } + + if (!this.skipBlur) { + this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); + } + this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); + this.blurTimer = null; + this.userClicked = false; + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("textarea").val()); + }, + + // When buttons are present, use a timer on the blur event to give precedence to clicks + blurHandler: function (event) { + 'use strict'; + if (event.data.editor.okButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.abortIfConfirm(); + } + }, 500); + } else { + if (event.data.editor.cancelButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.update(); + } + }, 500); + } else { + event.data.editor.update(); + } + } + }, + + submitHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.update(); + }, + + cancelButtonHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.abortIfConfirm(); + event.stopPropagation(); // Without this, click isn't handled + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abortIfConfirm(); + } + } + } +}; + +BestInPlaceEditor.defaults = { + locales: {}, + ajaxMethod: "put", //TODO Change to patch when support to 3.2 is dropped + ajaxDataType: 'text', + okButtonClass: '', + cancelButtonClass: '', + skipBlur: false +}; + +// Default locale +BestInPlaceEditor.defaults.locales[''] = { + confirmMessage: "Are you sure you want to discard your changes?", + uninitializedForm: "The form was not properly initialized. getValue is unbound", + placeHolder: '-' +}; + +jQuery.fn.best_in_place = function () { + 'use strict'; + function setBestInPlace(element) { + if (!element.data('bestInPlaceEditor')) { + element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); + return true; + } + } + + jQuery(this.context).delegate(this.selector, 'click', function () { + var el = jQuery(this); + if (setBestInPlace(el)) { + el.click(); + } + }); + + this.each(function () { + setBestInPlace(jQuery(this)); + }); + + return this; +}; + + + diff --git a/app/assets/javascripts/lib/bip.js b/app/assets/javascripts/lib/bip.js deleted file mode 100644 index 1d575fef..00000000 --- a/app/assets/javascripts/lib/bip.js +++ /dev/null @@ -1,780 +0,0 @@ -/* - BestInPlace (for jQuery) - version: 0.1.0 (01/01/2011) - @requires jQuery >= v1.4 - @requires jQuery.purr to display pop-up windows - - By Bernat Farrero based on the work of Jan Varwig. - Examples at http://bernatfarrero.com - - Licensed under the MIT: - http://www.opensource.org/licenses/mit-license.php - - Usage: - - Attention. - The format of the JSON object given to the select inputs is the following: - [["key", "value"],["key", "value"]] - The format of the JSON object given to the checkbox inputs is the following: - ["falseValue", "trueValue"] -*/ - - -function BestInPlaceEditor(e) { - this.element = e; - this.initOptions(); - this.bindForm(); - this.initNil(); - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); -} - -BestInPlaceEditor.prototype = { - // Public Interface Functions ////////////////////////////////////////////// - - activate : function() { - var to_display = ""; - if (this.isNil()) { - to_display = ""; - } - else if (this.original_content) { - to_display = this.original_content; - } - else { - if (this.sanitize) { - to_display = this.element.text(); - } else { - to_display = this.element.html(); - } - } - - this.oldValue = this.isNil() ? "" : this.element.html(); - this.display_value = to_display; - jQuery(this.activator).unbind("click", this.clickHandler); - this.activateForm(); - this.element.trigger(jQuery.Event("best_in_place:activate")); - }, - - abort : function() { - this.activateText(this.oldValue); - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:abort")); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - }, - - abortIfConfirm : function () { - if (!this.useConfirm) { - this.abort(); - return; - } - - if (confirm("Are you sure you want to discard your changes?")) { - this.abort(); - } - }, - - update : function() { - var editor = this; - if (this.formType in {"input":1, "textarea":1} && this.getValue() == this.oldValue) - { // Avoid request if no change is made - this.abort(); - return true; - } - editor.ajax({ - "type" : "post", - "dataType" : "text", - "data" : editor.requestData(), - "success" : function(data){ editor.loadSuccessCallback(data); }, - "error" : function(request, error){ editor.loadErrorCallback(request, error); } - }); - if (this.formType == "select") { - var value = this.getValue(); - this.previousCollectionValue = value; - - jQuery.each(this.values, function(i, v) { - if (value == v[0]) { - editor.element.html(v[1]); - } - } - ); - } else if (this.formType == "checkbox") { - editor.element.html(this.getValue() ? this.values[1] : this.values[0]); - } else { - if (this.getValue() !== "") { - editor.element.text(this.getValue()); - } else { - editor.element.html(this.nil); - } - } - editor.element.trigger(jQuery.Event("best_in_place:update")); - }, - - activateForm : function() { - alert("The form was not properly initialized. activateForm is unbound"); - }, - - activateText : function(value){ - this.element.html(value); - if(this.isNil()) this.element.html(this.nil); - }, - - // Helper Functions //////////////////////////////////////////////////////// - - initOptions : function() { - // Try parent supplied info - var self = this; - self.element.parents().each(function(){ - $parent = jQuery(this); - self.url = self.url || $parent.attr("data-url"); - self.collection = self.collection || $parent.attr("data-collection"); - self.formType = self.formType || $parent.attr("data-type"); - self.objectName = self.objectName || $parent.attr("data-object"); - self.attributeName = self.attributeName || $parent.attr("data-attribute"); - self.activator = self.activator || $parent.attr("data-activator"); - self.okButton = self.okButton || $parent.attr("data-ok-button"); - self.okButtonClass = self.okButtonClass || $parent.attr("data-ok-button-class"); - self.cancelButton = self.cancelButton || $parent.attr("data-cancel-button"); - self.cancelButtonClass = self.cancelButtonClass || $parent.attr("data-cancel-button-class"); - self.nil = self.nil || $parent.attr("data-nil"); - self.inner_class = self.inner_class || $parent.attr("data-inner-class"); - self.html_attrs = self.html_attrs || $parent.attr("data-html-attrs"); - self.original_content = self.original_content || $parent.attr("data-original-content"); - self.collectionValue = self.collectionValue || $parent.attr("data-value"); - }); - - // Try Rails-id based if parents did not explicitly supply something - self.element.parents().each(function(){ - var res = this.id.match(/^(\w+)_(\d+)$/i); - if (res) { - self.objectName = self.objectName || res[1]; - } - }); - - // Load own attributes (overrides all others) - self.url = self.element.attr("data-url") || self.url || document.location.pathname; - self.collection = self.element.attr("data-collection") || self.collection; - self.formType = self.element.attr("data-type") || self.formtype || "input"; - self.objectName = self.element.attr("data-object") || self.objectName; - self.attributeName = self.element.attr("data-attribute") || self.attributeName; - self.activator = self.element.attr("data-activator") || self.element; - self.okButton = self.element.attr("data-ok-button") || self.okButton; - self.okButtonClass = self.element.attr("data-ok-button-class") || self.okButtonClass || ""; - self.cancelButton = self.element.attr("data-cancel-button") || self.cancelButton; - self.cancelButtonClass = self.element.attr("data-cancel-button-class") || self.cancelButtonClass || ""; - self.nil = self.element.attr("data-nil") || self.nil || "—"; - self.inner_class = self.element.attr("data-inner-class") || self.inner_class || null; - self.html_attrs = self.element.attr("data-html-attrs") || self.html_attrs; - self.original_content = self.element.attr("data-original-content") || self.original_content; - self.collectionValue = self.element.attr("data-value") || self.collectionValue; - - if (!self.element.attr("data-sanitize")) { - self.sanitize = true; - } - else { - self.sanitize = (self.element.attr("data-sanitize") == "true"); - } - - if (!self.element.attr("data-use-confirm")) { - self.useConfirm = true; - } else { - self.useConfirm = (self.element.attr("data-use-confirm") != "false"); - } - - if ((self.formType == "select" || self.formType == "checkbox") && self.collection !== null) - { - self.values = jQuery.parseJSON(self.collection); - } - - }, - - bindForm : function() { - this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; - this.getValue = BestInPlaceEditor.forms[this.formType].getValue; - }, - - initNil: function() { - if (this.element.html() === "") - { - this.element.html(this.nil); - } - }, - - isNil: function() { - // TODO: It only work when form is deactivated. - // Condition will fail when form is activated - return this.element.html() === "" || this.element.html() === this.nil; - }, - - getValue : function() { - alert("The form was not properly initialized. getValue is unbound"); - }, - - // Trim and Strips HTML from text - sanitizeValue : function(s) { - return jQuery.trim(s); - }, - - /* Generate the data sent in the POST request */ - requestData : function() { - // To prevent xss attacks, a csrf token must be defined as a meta attribute - csrf_token = jQuery('meta[name=csrf-token]').attr('content'); - csrf_param = jQuery('meta[name=csrf-param]').attr('content'); - - var data = "_method=put"; - data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue()); - - if (csrf_param !== undefined && csrf_token !== undefined) { - data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token); - } - return data; - }, - - ajax : function(options) { - options.url = this.url; - options.beforeSend = function(xhr){ xhr.setRequestHeader("Accept", "application/json"); }; - return jQuery.ajax(options); - }, - - // Handlers //////////////////////////////////////////////////////////////// - - loadSuccessCallback : function(data) { - data = jQuery.trim(data); - - if(data && data!=""){ - var response = jQuery.parseJSON(jQuery.trim(data)); - if (response !== null && response.hasOwnProperty("display_as")) { - this.element.attr("data-original-content", this.element.text()); - this.original_content = this.element.text(); - this.element.html(response["display_as"]); - } - - this.element.trigger(jQuery.Event("best_in_place:success"), data); - this.element.trigger(jQuery.Event("ajax:success"), data); - } else { - this.element.trigger(jQuery.Event("best_in_place:success")); - this.element.trigger(jQuery.Event("ajax:success")); - } - - // Binding back after being clicked - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - - if (this.collectionValue !== null && this.formType == "select") { - this.collectionValue = this.previousCollectionValue; - this.previousCollectionValue = null; - } - }, - - loadErrorCallback : function(request, error) { - this.activateText(this.oldValue); - - this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); - this.element.trigger(jQuery.Event("ajax:error"), request, error); - - // Binding back after being clicked - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - }, - - clickHandler : function(event) { - event.preventDefault(); - event.data.editor.activate(); - }, - - setHtmlAttributes : function() { - var formField = this.element.find(this.formType); - - if(this.html_attrs){ - var attrs = jQuery.parseJSON(this.html_attrs); - for(var key in attrs){ - formField.attr(key, attrs[key]); - } - } - } -}; - - -// Button cases: -// If no buttons, then blur saves, ESC cancels -// If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) -// If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels -// If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels -BestInPlaceEditor.forms = { - "input" : { - activateForm : function() { - var output = jQuery(document.createElement('form')) - .addClass('form_in_place') - .attr('action', 'javascript:void(0);') - .attr('style', 'display:inline'); - var input_elt = jQuery(document.createElement('input')) - .attr('type', 'text') - .attr('name', this.attributeName) - .val(this.display_value); - if(this.inner_class !== null) { - input_elt.addClass(this.inner_class); - } - output.append(input_elt); - if(this.okButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'submit') - .attr('class', this.okButtonClass) - .attr('value', this.okButton) - ) - } - if(this.cancelButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'button') - .attr('class', this.cancelButtonClass) - .attr('value', this.cancelButton) - ) - } - - this.element.html(output); - this.setHtmlAttributes(); - // START METAMAPS CODE - //this.element.find("input[type='text']")[0].select(); - this.element.find("input[type='text']")[0].focus(); - // END METAMAPS CODE - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); - if (this.cancelButton) { - this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); - } - this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); - // START METAMAPS CODE - this.element.find("input[type='text']").bind('keydown', {editor: this}, BestInPlaceEditor.forms.input.keydownHandler); - // END METAMAPS CODE - this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); - this.blurTimer = null; - this.userClicked = false; - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("input").val()); - }, - - // When buttons are present, use a timer on the blur event to give precedence to clicks - inputBlurHandler : function(event) { - if (event.data.editor.okButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.abort(); - } - }, 500); - } else { - if (event.data.editor.cancelButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.update(); - } - }, 500); - } else { - event.data.editor.update(); - } - } - }, - - submitHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.update(); - }, - - cancelButtonHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.abort(); - event.stopPropagation(); // Without this, click isn't handled - }, - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abort(); - } - // START METAMAPS CODE - else if (event.keyCode == 13 && !event.shiftKey) { - event.data.editor.update(); - } - // END METAMAPS CODE - } - }, - - "date" : { - activateForm : function() { - var that = this, - output = jQuery(document.createElement('form')) - .addClass('form_in_place') - .attr('action', 'javascript:void(0);') - .attr('style', 'display:inline'), - input_elt = jQuery(document.createElement('input')) - .attr('type', 'text') - .attr('name', this.attributeName) - .attr('value', this.sanitizeValue(this.display_value)); - if(this.inner_class !== null) { - input_elt.addClass(this.inner_class); - } - output.append(input_elt) - - this.element.html(output); - this.setHtmlAttributes(); - this.element.find('input')[0].select(); - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); - this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); - - this.element.find('input') - .datepicker({ - onClose: function() { - that.update(); - } - }) - .datepicker('show'); - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("input").val()); - }, - - submitHandler : function(event) { - event.data.editor.update(); - }, - - // START METAMAPS CODE - keydownHandler : function(event) { - if (event.keyCode == 13 && !event.shiftKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }, - // END METAMAPS CODE - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abort(); - } - } - }, - - "select" : { - activateForm : function() { - var output = jQuery(document.createElement('form')) - .attr('action', 'javascript:void(0)') - .attr('style', 'display:inline'); - selected = '', - oldValue = this.oldValue, - select_elt = jQuery(document.createElement('select')) - .attr('class', this.inned_class !== null ? this.inner_class : '' ), - currentCollectionValue = this.collectionValue; - - jQuery.each(this.values, function (index, value) { - var option_elt = jQuery(document.createElement('option')) - // .attr('value', value[0]) - .val(value[0]) - .html(value[1]); - if(value[0] == currentCollectionValue) { - option_elt.attr('selected', 'selected'); - } - select_elt.append(option_elt); - }); - output.append(select_elt); - - this.element.html(output); - this.setHtmlAttributes(); - this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); - this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); - this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); - this.element.find("select")[0].focus(); - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("select").val()); - // return this.element.find("select").val(); - }, - - blurHandler : function(event) { - event.data.editor.update(); - }, - - keyupHandler : function(event) { - if (event.keyCode == 27) event.data.editor.abort(); - } - }, - - "checkbox" : { - activateForm : function() { - this.collectionValue = !this.getValue(); - this.setHtmlAttributes(); - this.update(); - }, - - getValue : function() { - return this.collectionValue; - } - }, - - "textarea" : { - activateForm : function() { - // grab width and height of text - width = this.element.css('width'); - height = this.element.css('height'); - - // construct form - var output = jQuery(document.createElement('form')) - .attr('action', 'javascript:void(0)') - .attr('style', 'display:inline') - .append(jQuery(document.createElement('textarea')) - .val(this.sanitizeValue(this.display_value))); - if(this.okButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'submit') - .attr('value', this.okButton) - ); - } - if(this.cancelButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'button') - .attr('value', this.cancelButton) - ) - } - - this.element.html(output); - this.setHtmlAttributes(); - - // set width and height of textarea - jQuery(this.element.find("textarea")[0]).css({ 'min-width': width, 'min-height': height }); - jQuery(this.element.find("textarea")[0]).elastic(); - - this.element.find("textarea")[0].focus(); - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); - if (this.cancelButton) { - this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); - } - this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); - // START METAMAPS CODE - this.element.find("textarea").bind('keydown', {editor: this}, BestInPlaceEditor.forms.textarea.keydownHandler); - // END METAMAPS CODE - this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); - this.blurTimer = null; - this.userClicked = false; - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("textarea").val()); - }, - - // When buttons are present, use a timer on the blur event to give precedence to clicks - blurHandler : function(event) { - if (event.data.editor.okButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.abortIfConfirm(); - } - }, 500); - } else { - if (event.data.editor.cancelButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.update(); - } - }, 500); - } else { - event.data.editor.update(); - } - } - }, - - submitHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.update(); - }, - - cancelButtonHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.abortIfConfirm(); - event.stopPropagation(); // Without this, click isn't handled - }, - - // START METAMAPS CODE - keydownHandler : function(event) { - if (event.keyCode == 13 && !event.shiftKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }, - // END METAMAPS CODE - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abortIfConfirm(); - } - // START METAMAPS CODE - else if (event.keyCode == 13 && !event.shiftKey) { - event.data.editor.update(); - } - // END METAMAPS CODE - } - } -}; - -jQuery.fn.best_in_place = function() { - - function setBestInPlace(element) { - if (!element.data('bestInPlaceEditor')) { - element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); - return true; - } - } - - jQuery(this.context).delegate(this.selector, 'click', function () { - var el = jQuery(this); - if (setBestInPlace(el)) - el.click(); - }); - - this.each(function () { - setBestInPlace(jQuery(this)); - }); - - return this; -}; - - - -/** -* @name Elastic -* @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy -* @version 1.6.5 -* @requires Jquery 1.2.6+ -* -* @author Jan Jarfalk -* @author-email jan.jarfalk@unwrongest.com -* @author-website http://www.unwrongest.com -* -* @licens MIT License - http://www.opensource.org/licenses/mit-license.php -*/ - -(function(jQuery){ - if (typeof jQuery.fn.elastic !== 'undefined') return; - - jQuery.fn.extend({ - elastic: function() { - // We will create a div clone of the textarea - // by copying these attributes from the textarea to the div. - var mimics = [ - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - 'fontSize', - 'lineHeight', - 'fontFamily', - 'width', - 'fontWeight']; - - return this.each( function() { - - // Elastic only works on textareas - if ( this.type != 'textarea' ) { - return false; - } - - var $textarea = jQuery(this), - $twin = jQuery('
').css({'position': 'absolute','display':'none','word-wrap':'break-word'}), - lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), - minheight = parseInt($textarea.css('height'),10) || lineHeight*3, - maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, - goalheight = 0, - i = 0; - - // Opera returns max-height of -1 if not set - if (maxheight < 0) { maxheight = Number.MAX_VALUE; } - - // Append the twin to the DOM - // We are going to meassure the height of this, not the textarea. - $twin.appendTo($textarea.parent()); - - // Copy the essential styles (mimics) from the textarea to the twin - i = mimics.length; - while(i--){ - $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); - } - - - // Sets a given height and overflow state on the textarea - function setHeightAndOverflow(height, overflow){ - curratedHeight = Math.floor(parseInt(height,10)); - if($textarea.height() != curratedHeight){ - $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); - - } - } - - - // This function will update the height of the textarea if necessary - function update() { - - // Get curated content from the textarea. - var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ /g, ' ').replace(/<|>/g, '>').replace(/\n/g, '
'); - - // Compare curated content with curated twin. - var twinContent = $twin.html().replace(/
/ig,'
'); - - if(textareaContent+' ' != twinContent){ - - // Add an extra white space so new rows are added when you are at the end of a row. - $twin.html(textareaContent+' '); - - // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height - if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){ - - var goalheight = $twin.height()+lineHeight; - if(goalheight >= maxheight) { - setHeightAndOverflow(maxheight,'auto'); - } else if(goalheight <= minheight) { - setHeightAndOverflow(minheight,'hidden'); - } else { - setHeightAndOverflow(goalheight,'hidden'); - } - - } - - } - - } - - // Hide scrollbars - $textarea.css({'overflow':'hidden'}); - - // Update textarea size on keyup, change, cut and paste - $textarea.bind('keyup change cut paste', function(){ - update(); - }); - - // Compact textarea on blur - // Lets animate this.... - $textarea.bind('blur',function(){ - if($twin.height() < maxheight){ - if($twin.height() > minheight) { - $textarea.height($twin.height()); - } else { - $textarea.height(minheight); - } - } - }); - - // And this line is to catch the browser paste event - $textarea.on("input paste", function(e){ setTimeout( update, 250); }); - - // Run update once when elastic is initialized - update(); - - }); - - } - }); -})(jQuery); diff --git a/app/assets/javascripts/lib/howler.js b/app/assets/javascripts/lib/howler.js deleted file mode 100644 index f393b3b1..00000000 --- a/app/assets/javascripts/lib/howler.js +++ /dev/null @@ -1,1353 +0,0 @@ -/*! - * howler.js v1.1.26 - * howlerjs.com - * - * (c) 2013-2015, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - -(function() { - // setup - var cache = {}; - - // setup the audio context - var ctx = null, - usingWebAudio = true, - noAudio = false; - try { - if (typeof AudioContext !== 'undefined') { - ctx = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined') { - ctx = new webkitAudioContext(); - } else { - usingWebAudio = false; - } - } catch(e) { - usingWebAudio = false; - } - - if (!usingWebAudio) { - if (typeof Audio !== 'undefined') { - try { - new Audio(); - } catch(e) { - noAudio = true; - } - } else { - noAudio = true; - } - } - - // create a master gain node - if (usingWebAudio) { - var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); - masterGain.gain.value = 1; - masterGain.connect(ctx.destination); - } - - // create global controller - var HowlerGlobal = function(codecs) { - this._volume = 1; - this._muted = false; - this.usingWebAudio = usingWebAudio; - this.ctx = ctx; - this.noAudio = noAudio; - this._howls = []; - this._codecs = codecs; - this.iOSAutoEnable = true; - }; - HowlerGlobal.prototype = { - /** - * Get/set the global volume for all sounds. - * @param {Float} vol Volume from 0.0 to 1.0. - * @return {Howler/Float} Returns self or current volume. - */ - volume: function(vol) { - var self = this; - - // make sure volume is a number - vol = parseFloat(vol); - - if (vol >= 0 && vol <= 1) { - self._volume = vol; - - if (usingWebAudio) { - masterGain.gain.value = vol; - } - - // loop through cache and change volume of all nodes that are using HTML5 Audio - for (var key in self._howls) { - if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) { - // loop through the audio nodes - for (var i=0; i 0) ? node._pos : self._sprite[sprite][0] / 1000; - - // determine how long to play for - var duration = 0; - if (self._webAudio) { - duration = self._sprite[sprite][1] / 1000 - node._pos; - if (node._pos > 0) { - pos = self._sprite[sprite][0] / 1000 + pos; - } - } else { - duration = self._sprite[sprite][1] / 1000 - (pos - self._sprite[sprite][0] / 1000); - } - - // determine if this sound should be looped - var loop = !!(self._loop || self._sprite[sprite][2]); - - // set timer to fire the 'onend' event - var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '', - timerId; - (function() { - var data = { - id: soundId, - sprite: sprite, - loop: loop - }; - timerId = setTimeout(function() { - // if looping, restart the track - if (!self._webAudio && loop) { - self.stop(data.id).play(sprite, data.id); - } - - // set web audio node to paused at end - if (self._webAudio && !loop) { - self._nodeById(data.id).paused = true; - self._nodeById(data.id)._pos = 0; - - // clear the end timer - self._clearEndTimer(data.id); - } - - // end the track if it is HTML audio and a sprite - if (!self._webAudio && !loop) { - self.stop(data.id); - } - - // fire ended event - self.on('end', soundId); - }, duration * 1000); - - // store the reference to the timer - self._onendTimer.push({timer: timerId, id: data.id}); - })(); - - if (self._webAudio) { - var loopStart = self._sprite[sprite][0] / 1000, - loopEnd = self._sprite[sprite][1] / 1000; - - // set the play id to this node and load into context - node.id = soundId; - node.paused = false; - refreshBuffer(self, [loop, loopStart, loopEnd], soundId); - self._playStart = ctx.currentTime; - node.gain.value = self._volume; - - if (typeof node.bufferSource.start === 'undefined') { - loop ? node.bufferSource.noteGrainOn(0, pos, 86400) : node.bufferSource.noteGrainOn(0, pos, duration); - } else { - loop ? node.bufferSource.start(0, pos, 86400) : node.bufferSource.start(0, pos, duration); - } - } else { - if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) { - node.readyState = 4; - node.id = soundId; - node.currentTime = pos; - node.muted = Howler._muted || node.muted; - node.volume = self._volume * Howler.volume(); - setTimeout(function() { node.play(); }, 0); - } else { - self._clearEndTimer(soundId); - - (function(){ - var sound = self, - playSprite = sprite, - fn = callback, - newNode = node; - var listener = function() { - sound.play(playSprite, fn); - - // clear the event listener - newNode.removeEventListener('canplaythrough', listener, false); - }; - newNode.addEventListener('canplaythrough', listener, false); - })(); - - return self; - } - } - - // fire the play event and send the soundId back in the callback - self.on('play'); - if (typeof callback === 'function') callback(soundId); - - return self; - }); - - return self; - }, - - /** - * Pause playback and save the current position. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - pause: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.pause(id); - }); - - return self; - } - - // clear 'onend' timer - self._clearEndTimer(id); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - activeNode._pos = self.pos(null, id); - - if (self._webAudio) { - // make sure the sound has been created - if (!activeNode.bufferSource || activeNode.paused) { - return self; - } - - activeNode.paused = true; - if (typeof activeNode.bufferSource.stop === 'undefined') { - activeNode.bufferSource.noteOff(0); - } else { - activeNode.bufferSource.stop(0); - } - } else { - activeNode.pause(); - } - } - - self.on('pause'); - - return self; - }, - - /** - * Stop playback and reset to start. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - stop: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.stop(id); - }); - - return self; - } - - // clear 'onend' timer - self._clearEndTimer(id); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - activeNode._pos = 0; - - if (self._webAudio) { - // make sure the sound has been created - if (!activeNode.bufferSource || activeNode.paused) { - return self; - } - - activeNode.paused = true; - - if (typeof activeNode.bufferSource.stop === 'undefined') { - activeNode.bufferSource.noteOff(0); - } else { - activeNode.bufferSource.stop(0); - } - } else if (!isNaN(activeNode.duration)) { - activeNode.pause(); - activeNode.currentTime = 0; - } - } - - return self; - }, - - /** - * Mute this sound. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - mute: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.mute(id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = 0; - } else { - activeNode.muted = true; - } - } - - return self; - }, - - /** - * Unmute this sound. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - unmute: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.unmute(id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = self._volume; - } else { - activeNode.muted = false; - } - } - - return self; - }, - - /** - * Get/set volume of this sound. - * @param {Float} vol Volume from 0.0 to 1.0. - * @param {String} id (optional) The play instance ID. - * @return {Howl/Float} Returns self or current volume. - */ - volume: function(vol, id) { - var self = this; - - // make sure volume is a number - vol = parseFloat(vol); - - if (vol >= 0 && vol <= 1) { - self._volume = vol; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.volume(vol, id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = vol; - } else { - activeNode.volume = vol * Howler.volume(); - } - } - - return self; - } else { - return self._volume; - } - }, - - /** - * Get/set whether to loop the sound. - * @param {Boolean} loop To loop or not to loop, that is the question. - * @return {Howl/Boolean} Returns self or current looping value. - */ - loop: function(loop) { - var self = this; - - if (typeof loop === 'boolean') { - self._loop = loop; - - return self; - } else { - return self._loop; - } - }, - - /** - * Get/set sound sprite definition. - * @param {Object} sprite Example: {spriteName: [offset, duration, loop]} - * @param {Integer} offset Where to begin playback in milliseconds - * @param {Integer} duration How long to play in milliseconds - * @param {Boolean} loop (optional) Set true to loop this sprite - * @return {Howl} Returns current sprite sheet or self. - */ - sprite: function(sprite) { - var self = this; - - if (typeof sprite === 'object') { - self._sprite = sprite; - - return self; - } else { - return self._sprite; - } - }, - - /** - * Get/set the position of playback. - * @param {Float} pos The position to move current playback to. - * @param {String} id (optional) The play instance ID. - * @return {Howl/Float} Returns self or current playback position. - */ - pos: function(pos, id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('load', function() { - self.pos(pos); - }); - - return typeof pos === 'number' ? self : self._pos || 0; - } - - // make sure we are dealing with a number for pos - pos = parseFloat(pos); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (pos >= 0) { - self.pause(id); - activeNode._pos = pos; - self.play(activeNode._sprite, id); - - return self; - } else { - return self._webAudio ? activeNode._pos + (ctx.currentTime - self._playStart) : activeNode.currentTime; - } - } else if (pos >= 0) { - return self; - } else { - // find the first inactive node to return the pos for - for (var i=0; i= 0 || x < 0) { - if (self._webAudio) { - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - self._pos3d = [x, y, z]; - activeNode.panner.setPosition(x, y, z); - activeNode.panner.panningModel = self._model || 'HRTF'; - } - } - } else { - return self._pos3d; - } - - return self; - }, - - /** - * Fade a currently playing sound between two volumes. - * @param {Number} from The volume to fade from (0.0 to 1.0). - * @param {Number} to The volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback (optional) Fired when the fade is complete. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - fade: function(from, to, len, callback, id) { - var self = this, - diff = Math.abs(from - to), - dir = from > to ? 'down' : 'up', - steps = diff / 0.01, - stepTime = len / steps; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('load', function() { - self.fade(from, to, len, callback, id); - }); - - return self; - } - - // set the volume to the start position - self.volume(from, id); - - for (var i=1; i<=steps; i++) { - (function() { - var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i, - vol = Math.round(1000 * change) / 1000, - toVol = to; - - setTimeout(function() { - self.volume(vol, id); - - if (vol === toVol) { - if (callback) callback(); - } - }, stepTime * i); - })(); - } - }, - - /** - * [DEPRECATED] Fade in the current sound. - * @param {Float} to Volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback - * @return {Howl} - */ - fadeIn: function(to, len, callback) { - return this.volume(0).play().fade(0, to, len, callback); - }, - - /** - * [DEPRECATED] Fade out the current sound and pause when finished. - * @param {Float} to Volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - fadeOut: function(to, len, callback, id) { - var self = this; - - return self.fade(self._volume, to, len, function() { - if (callback) callback(); - self.pause(id); - - // fire ended event - self.on('end'); - }, id); - }, - - /** - * Get an audio node by ID. - * @return {Howl} Audio node. - */ - _nodeById: function(id) { - var self = this, - node = self._audioNode[0]; - - // find the node with this ID - for (var i=0; i=0; i--) { - if (inactive <= 5) { - break; - } - - if (self._audioNode[i].paused) { - // disconnect the audio source if using Web Audio - if (self._webAudio) { - self._audioNode[i].disconnect(0); - } - - inactive--; - self._audioNode.splice(i, 1); - } - } - }, - - /** - * Clear 'onend' timeout before it ends. - * @param {String} soundId The play instance ID. - */ - _clearEndTimer: function(soundId) { - var self = this, - index = 0; - - // loop through the timers to find the one associated with this sound - for (var i=0; i= 0) { - Howler._howls.splice(index, 1); - } - - // delete this sound from the cache - delete cache[self._src]; - self = null; - } - - }; - - // only define these functions when using WebAudio - if (usingWebAudio) { - - /** - * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API). - * @param {Object} obj The Howl object for the sound to load. - * @param {String} url The path to the sound file. - */ - var loadBuffer = function(obj, url) { - // check if the buffer has already been cached - if (url in cache) { - // set the duration from the cache - obj._duration = cache[url].duration; - - // load the sound into this object - loadSound(obj); - return; - } - - if (/^data:[^;]+;base64,/.test(url)) { - // Decode base64 data-URIs because some browsers cannot load data-URIs with XMLHttpRequest. - var data = atob(url.split(',')[1]); - var dataView = new Uint8Array(data.length); - for (var i=0; i