Merge branch 'develop' of github.com:metamaps/metamaps into window.resize.fix

This commit is contained in:
Robert Best 2016-10-22 04:54:31 +00:00
commit ad1889dfc5
142 changed files with 4860 additions and 18406 deletions

View file

@ -1,6 +1,11 @@
module.exports = { module.exports = {
"sourceType": "module", "sourceType": "module",
"parser": "babel-eslint", "parser": "babel-eslint",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
}
},
"extends": "standard", "extends": "standard",
"installedESLint": true, "installedESLint": true,
"env": { "env": {
@ -13,6 +18,8 @@ module.exports = {
"react" "react"
], ],
"rules": { "rules": {
"react/jsx-uses-react": [2],
"react/jsx-uses-vars": [2],
"yoda": [2, "never", { "exceptRange": true }] "yoda": [2, "never", { "exceptRange": true }]
} }
} }

View file

@ -18,7 +18,6 @@ export SECRET_KEY_BASE='267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c800
# export S3_BUCKET_NAME # export S3_BUCKET_NAME
# export AWS_ACCESS_KEY_ID # export AWS_ACCESS_KEY_ID
# export AWS_SECRET_ACCESS_KEY # export AWS_SECRET_ACCESS_KEY
# export SSO_KEY
# #
# export SMTP_DOMAIN # export SMTP_DOMAIN
# export SMTP_PASSWORD # export SMTP_PASSWORD

View file

@ -1,3 +1,7 @@
if ENV['COVERAGE'] == 'on' 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 end

View file

@ -25,10 +25,8 @@ gem 'rack-cors'
gem 'redis' gem 'redis'
gem 'slack-notifier' gem 'slack-notifier'
gem 'snorlax' gem 'snorlax'
gem 'uservoice-ruby'
# asset stuff # asset stuff
gem 'coffee-rails'
gem 'jquery-rails' gem 'jquery-rails'
gem 'jquery-ui-rails' gem 'jquery-ui-rails'
gem 'sass-rails' gem 'sass-rails'

View file

@ -70,13 +70,6 @@ GEM
cocaine (0.5.8) cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0) climate_control (>= 0.0.3, < 1.0)
coderay (1.1.1) 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) concurrent-ruby (1.0.2)
debug_inspector (0.0.2) debug_inspector (0.0.2)
delayed_job (4.1.2) delayed_job (4.1.2)
@ -103,7 +96,6 @@ GEM
actionmailer (>= 4.0, < 6) actionmailer (>= 4.0, < 6)
activesupport (>= 4.0, < 6) activesupport (>= 4.0, < 6)
execjs (2.7.0) execjs (2.7.0)
ezcrypto (0.7.2)
factory_girl (4.7.0) factory_girl (4.7.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
factory_girl_rails (4.7.0) factory_girl_rails (4.7.0)
@ -145,7 +137,6 @@ GEM
nokogiri (1.6.8) nokogiri (1.6.8)
mini_portile2 (~> 2.1.0) mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7) pkg-config (~> 1.1.7)
oauth (0.5.1)
orm_adapter (0.5.0) orm_adapter (0.5.0)
paperclip (5.1.0) paperclip (5.1.0)
activemodel (>= 4.2.0) activemodel (>= 4.2.0)
@ -262,10 +253,6 @@ GEM
uglifier (3.0.2) uglifier (3.0.2)
execjs (>= 0.3.0, < 3) execjs (>= 0.3.0, < 3)
unicode-display_width (1.1.1) 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) warden (1.2.6)
rack (>= 1.0) rack (>= 1.0)
websocket-driver (0.6.4) websocket-driver (0.6.4)
@ -282,7 +269,6 @@ DEPENDENCIES
better_errors better_errors
binding_of_caller binding_of_caller
brakeman brakeman
coffee-rails
delayed_job delayed_job
delayed_job_active_record delayed_job_active_record
devise devise
@ -315,10 +301,9 @@ DEPENDENCIES
snorlax snorlax
tunemygc tunemygc
uglifier uglifier
uservoice-ruby
RUBY VERSION RUBY VERSION
ruby 2.3.0p0 ruby 2.3.0p0
BUNDLED WITH BUNDLED WITH
1.13.2 1.13.3

View file

@ -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: 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 ## How do I learn more?
- 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. - 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 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. - 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] - [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 ## 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. 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 Copyright (c) 2016 Connor Turland
[site-blog]: http://blog.metamaps.cc
[site-beta]: http://metamaps.cc [site-beta]: http://metamaps.cc
[license]: https://github.com/metamaps/metamaps/blob/develop/LICENSE [license]: https://github.com/metamaps/metamaps/blob/develop/LICENSE
[contributing]: https://github.com/metamaps/metamaps/blob/develop/doc/CONTRIBUTING.md [contributing]: https://github.com/metamaps/metamaps/blob/develop/doc/CONTRIBUTING.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 B

BIN
app/assets/images/junto.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

View file

@ -16,4 +16,3 @@
//= require_directory ./lib //= require_directory ./lib
//= require ./src/Metamaps.Erb //= require ./src/Metamaps.Erb
//= require ./webpacked/metamaps.bundle //= require ./webpacked/metamaps.bundle
//= require ./src/check-canvas-support

View file

@ -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;
};

View file

@ -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(/&amp;/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(/&amp;/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;
};

View file

@ -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('<div />').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,'&amp;').replace(/ /g, '&nbsp;').replace(/<|>/g, '&gt;').replace(/\n/g, '<br />');
// Compare curated content with curated twin.
var twinContent = $twin.html().replace(/<br>/ig,'<br />');
if(textareaContent+'&nbsp;' != twinContent){
// Add an extra white space so new rows are added when you are at the end of a row.
$twin.html(textareaContent+'&nbsp;');
// 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);

File diff suppressed because it is too large Load diff

View file

@ -1,180 +0,0 @@
/**
* jquery.purr.js
* Copyright (c) 2008 Net Perspective (net-perspective.com)
* Licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php)
*
* @author R.A. Ray
* @projectDescription jQuery plugin for dynamically displaying unobtrusive messages in the browser. Mimics the behavior of the MacOS program "Growl."
* @version 0.1.0
*
* @requires jquery.js (tested with 1.2.6)
*
* @param fadeInSpeed int - Duration of fade in animation in miliseconds
* default: 500
* @param fadeOutSpeed int - Duration of fade out animationin miliseconds
default: 500
* @param removeTimer int - Timeout, in miliseconds, before notice is removed once it is the top non-sticky notice in the list
default: 4000
* @param isSticky bool - Whether the notice should fade out on its own or wait to be manually closed
default: false
* @param usingTransparentPNG bool - Whether or not the notice is using transparent .png images in its styling
default: false
*/
( function( $ ) {
$.purr = function ( notice, options )
{
// Convert notice to a jQuery object
notice = $( notice );
// Add a class to denote the notice as not sticky
if ( !options.isSticky )
{
notice.addClass( 'not-sticky' );
};
// Get the container element from the page
var cont = document.getElementById( 'purr-container' );
// If the container doesn't yet exist, we need to create it
if ( !cont )
{
cont = '<div id="purr-container"></div>';
}
// Convert cont to a jQuery object
cont = $( cont );
// Add the container to the page
$( 'body' ).append( cont );
notify();
function notify ()
{
// Set up the close button
var close = document.createElement( 'a' );
$( close ).attr(
{
className: 'close',
href: '#close',
innerHTML: 'Close'
}
)
.appendTo( notice )
.click( function ()
{
removeNotice();
return false;
}
);
// Add the notice to the page and keep it hidden initially
notice.appendTo( cont )
.hide();
if ( jQuery.browser.msie && options.usingTransparentPNG )
{
// IE7 and earlier can't handle the combination of opacity and transparent pngs, so if we're using transparent pngs in our
// notice style, we'll just skip the fading in.
notice.show();
}
else
{
//Fade in the notice we just added
notice.fadeIn( options.fadeInSpeed );
}
// Set up the removal interval for the added notice if that notice is not a sticky
if ( !options.isSticky )
{
var topSpotInt = setInterval( function ()
{
// Check to see if our notice is the first non-sticky notice in the list
if ( notice.prevAll( '.not-sticky' ).length == 0 )
{
// Stop checking once the condition is met
clearInterval( topSpotInt );
// Call the close action after the timeout set in options
setTimeout( function ()
{
removeNotice();
}, options.removeTimer
);
}
}, 200 );
}
}
function removeNotice ()
{
// IE7 and earlier can't handle the combination of opacity and transparent pngs, so if we're using transparent pngs in our
// notice style, we'll just skip the fading out.
if ( jQuery.browser.msie && options.usingTransparentPNG )
{
notice.css( { opacity: 0 } )
.animate(
{
height: '0px'
},
{
duration: options.fadeOutSpeed,
complete: function ()
{
notice.remove();
}
}
);
}
else
{
// Fade the object out before reducing its height to produce the sliding effect
notice.animate(
{
opacity: '0'
},
{
duration: options.fadeOutSpeed,
complete: function ()
{
notice.animate(
{
height: '0px'
},
{
duration: options.fadeOutSpeed,
complete: function ()
{
notice.remove();
}
}
);
}
}
);
}
};
};
$.fn.purr = function ( options )
{
options = options || {};
options.fadeInSpeed = options.fadeInSpeed || 500;
options.fadeOutSpeed = options.fadeOutSpeed || 500;
options.removeTimer = options.removeTimer || 4000;
options.isSticky = options.isSticky || false;
options.usingTransparentPNG = options.usingTransparentPNG || false;
this.each( function()
{
new $.purr( this, options );
}
);
return this;
};
})( jQuery );

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,23 +0,0 @@
function SocketIoConnection(config) {
this.connection = io.connect(config.url, config.socketio);
}
SocketIoConnection.prototype.on = function (ev, fn) {
this.connection.on(ev, fn);
};
SocketIoConnection.prototype.emit = function () {
this.connection.emit.apply(this.connection, arguments);
};
SocketIoConnection.prototype.removeAllListeners = function () {
this.connection.removeAllListeners();
};
SocketIoConnection.prototype.getSessionid = function () {
return this.connection.socket.sessionid;
};
SocketIoConnection.prototype.disconnect = function () {
return this.connection.disconnect();
};

View file

@ -1,41 +0,0 @@
var USERVOICE;
if(USERVOICE == undefined) {
USERVOICE = {};
}
USERVOICE.load = function (name, id, email, sso_token) {
// Include the UserVoice JavaScript SDK (only needed once on a page)
UserVoice=window.UserVoice||[];(function(){var uv=document.createElement('script');uv.type='text/javascript';uv.async=true;uv.src='//widget.uservoice.com/wybK0nSMNuhlWkIKzTyWg.js';var s=document.getElementsByTagName('script')[0];s.parentNode.insertBefore(uv,s)})();
//
// UserVoice Javascript SDK developer documentation:
// https://www.uservoice.com/o/javascript-sdk
//
// Set colors
UserVoice.push(['set', {
accent_color: '#448dd6',
trigger_color: 'white',
trigger_background_color: 'rgba(46, 49, 51, 0.6)'
}]);
// Identify the user and pass traits
// To enable, replace sample data with actual user traits and uncomment the line
if (name) {
UserVoice.push(['setSSO', sso_token]);
UserVoice.push(['identify', {
'email': email, // Users email address
'name': name, // Users real name
'id': id, // Optional: Unique id of the user
}]);
}
// Add default trigger to the bottom-left corner of the window:
UserVoice.push(['addTrigger', { mode: 'contact', trigger_position: 'bottom-left' }]);
// Or, use your own custom trigger:
//UserVoice.push(['addTrigger', '#barometer_tab', { mode: 'contact' }]);
// Autoprompt for Satisfaction and SmartVote (only displayed under certain conditions)
UserVoice.push(['autoprompt', {}]);
};

View file

@ -15,6 +15,7 @@ Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>'
Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>'
Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>'
Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>'
Metamaps.Erb['import-example.png'] = '<%= asset_path('import-example.png') %>'
Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>'
Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>'
Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %>

View file

@ -1,15 +0,0 @@
// TODO document this user agent function
var labelType, useGradients, nativeTextSupport, animate
;(function () {
var ua = navigator.userAgent,
iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i),
typeOfCanvas = typeof HTMLCanvasElement,
nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'),
textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function')
// I'm setting this based on the fact that ExCanvas provides text support for IE
// and that as of today iPhone/iPad current text support is lame
labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML'
nativeTextSupport = labelType == 'Native'
useGradients = nativeCanvasSupport
animate = !(iStuff || !nativeCanvasSupport)
})()

View file

@ -1526,9 +1526,8 @@ h3.filterBox {
background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>);
} }
/* map info box */ /* map info box */
/* map info box */
.wrapper div.mapInfoBox { .wrapper .mapInfoBox {
display: none; display: none;
position: absolute; position: absolute;
bottom: 40px; bottom: 40px;
@ -1536,12 +1535,40 @@ h3.filterBox {
background-color: #424242; background-color: #424242;
color: #F5F5F5; color: #F5F5F5;
border-radius: 2px; border-radius: 2px;
box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0px 3px 3px rgba(0,0,0,0.16);
text-align: center;
font-style: normal;
}
.import-dialog{
button {
margin: 1em 0.5em;
}
.import-blue-button {
display: inline-block;
box-sizing: border-box;
margin: 0.75em;
padding: 0.75em;
height: 3em;
background-color: #AAB0FB;
border-radius: 0.3em;
color: white;
cursor: pointer;
}
.fileupload {
box-sizing: border-box;
margin: 0.75em;
padding: 0.75em;
height: 3em;
border: 3px dashed #AAB0FB;
width: 75%;
text-align: center;
cursor: pointer;
}
}
.wrapper .mapInfoBox {
width: 360px; width: 360px;
min-height: 300px; min-height: 300px;
padding: 0; padding: 0;
font-style: normal;
text-align: center;
box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0px 3px 3px rgba(0,0,0,0.16);
} }
.requestTitle { .requestTitle {
display: none; display: none;
@ -2028,17 +2055,17 @@ and it won't be important on password protected instances */
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: fixed; position: absolute;
z-index: 1000000; z-index: 1000000;
display: none; display: none;
} }
#lightbox_main { #lightbox_main {
width: 800px; width: 800px;
height: auto;
margin: 0 auto; margin: 0 auto;
z-index: 2; z-index: 2;
position: relative; position: relative;
top: 50%; top: 5vh;
height: 90vh;
background-color: transparent; background-color: transparent;
color: black; color: black;
} }
@ -2077,8 +2104,11 @@ and it won't be important on password protected instances */
background-position: center center; background-position: center center;
} }
#lightbox_content { #lightbox_content {
width: 552px; width: 800px;
height: 434px; height: 500px;
max-height: 90vh;
box-sizing: border-box;
overflow-y: auto;
background-color: #e0e0e0; background-color: #e0e0e0;
padding: 64px 124px 64px 124px; padding: 64px 124px 64px 124px;
box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19); box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19);

View file

@ -17,7 +17,6 @@
} }
#center-container { #center-container {
position:relative; position:relative;
height:100%; height:100%;
@ -143,6 +142,16 @@
margin-top:5px; margin-top:5px;
} }
.CardOnGraph .desc ol,
.CardOnGraph .desc ul {
margin-left: 1em;
}
.CardOnGraph .desc a:hover {
text-decoration: underline;
opacity: 0.9;
}
.CardOnGraph .best_in_place_desc { .CardOnGraph .best_in_place_desc {
display:block; display:block;
margin-top:2px; margin-top:2px;
@ -582,10 +591,10 @@ background-color: #E0E0E0;
position: relative; position: relative;
} }
.CardOnGraph .hoverForTip:hover .tip, .mapCard .hoverForTip:hover .tip, #mapContribs:hover .tip { .CardOnGraph .hoverForTip:hover .tip, #mapContribs:hover .tip {
display:block; display:block;
} }
.CardOnGraph .tip, .mapCard .tip { .CardOnGraph .tip {
display:none; display:none;
position: absolute; position: absolute;
background: black; background: black;
@ -942,154 +951,7 @@ font-family: 'din-regular', helvetica, sans-serif;
background-position: 0 -24px; background-position: 0 -24px;
} }
/* Map Cards */
.map {
display:inline-block;
width:220px;
height:340px;
font-size: 12px;
text-align: left;
overflow: visible;
background: #e8e8e8;
border-radius:2px;
margin:16px 16px 16px 19px;
box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
}
.map:hover {
background: #dcdcdc;
}
.map.newMap {
float: left;
position: relative;
}
.map.newMap a {
height: 340px;
display: block;
position: relative;
}
.newMap .newMapImage {
display: block;
width: 72px;
height: 72px;
background-image: url("<%= asset_data_uri('newmap_sprite.png') %>");
background-repeat: no-repeat;
background-position: 0 0;
position: absolute;
left: 50%;
margin-left: -36px;
top: 50%;
margin-top: -36px;
}
.map:hover .newMapImage {
background-position: 0 -72px;
}
.newMap span {
font-family: 'din-regular', sans-serif;
font-size: 18px;
line-height: 22px;
text-align: center;
display: block;
padding-top: 220px;
}
.mapCard {
display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */
display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */
display: -ms-flexbox; /* TWEENER - IE 10 */
display: -webkit-flex; /* NEW - Chrome */
display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-webkit-box-direction: normal;
-moz-box-direction: normal;
-ms-flex-direction: column;
-webkit-flex-direction: column;
flex-direction: column;
position:relative;
width:100%;
height:308px;
padding: 16px 0;
color: #424242;
}
.mapCard .title {
word-wrap: break-word;
font-size:18px;
line-height:22px;
height: 44px;
display:block;
padding: 0 16px;
text-align: center;
-webkit-box-flex: none; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: none; /* OLD - Firefox 19- */
-webkit-flex: none; /* Chrome */
-ms-flex: none; /* IE 10 */
flex: none; /* NEW, Spec - Opera 12.1, Firefox 20+ */
font-family: 'din-regular', sans-serif;
}
.mapCard .mapScreenshot {
width: 188px;
height: 126px;
padding: 8px 16px;
}
.mapCard .mapScreenshot img {
width: 188px;
height: 126px;
border-radius: 2px;
}
.mapCard .scroll {
display:block;
-webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: 1; /* OLD - Firefox 19- */
-webkit-flex: 1; /* Chrome */
-ms-flex: 1; /* IE 10 */
flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */
padding:0 16px 8px;
font-family: helvetica, sans-serif;
font-style: italic;
font-size: 12px;
word-wrap: break-word;
}
.mCS_no_scrollbar {
padding-right: 5px;
}
.mapCard .mapMetadata {
font-family: 'din-regular', sans-serif;
font-size: 12px;
position:relative;
border-top: 1px solid #BDBDBD;
-webkit-box-flex: none; /* OLD - iOS 6-, Safari 3.1-6 */
-moz-box-flex: none; /* OLD - Firefox 19- */
-webkit-flex: none; /* Chrome */
-ms-flex: none; /* IE 10 */
flex: none; /* NEW, Spec - Opera 12.1, Firefox 20+ */
}
.mapCard .metadataSection {
padding: 8px 16px 0 16px;
width: 78px;
float: left;
}
.mapPermission {
font-family: 'din-medium', sans-serif;
}
.cCountColor {
font-family: 'din-medium', sans-serif;
color: #DB5D5D;
}
.tCountColor {
font-family: 'din-medium', sans-serif;
color: #4FC059;
}
.sCountColor {
font-family: 'din-medium', sans-serif;
color: #DAB539;
}
/* mapper card */ /* mapper card */

View file

@ -188,7 +188,7 @@
.upperRightIcon { .upperRightIcon {
width: 32px; width: 32px;
height: 32px; height: 32px;
background-image: url(<%= asset_data_uri('topright_sprite.png') %>); background-image: url(<%= asset_path('topright_sprite.png') %>);
background-repeat: no-repeat; background-repeat: no-repeat;
cursor: pointer; cursor: pointer;
} }
@ -325,7 +325,7 @@
} }
.fullWidthWrapper.withPartners { .fullWidthWrapper.withPartners {
background: url(<%= asset_data_uri('homepage_bg_fade.png') %>) no-repeat center -300px; background: url(<%= asset_path('homepage_bg_fade.png') %>) no-repeat center -300px;
} }
.homeWrapper.homePartners { .homeWrapper.homePartners {
padding: 64px 0 280px; padding: 64px 0 280px;
@ -364,7 +364,7 @@
cursor: pointer; cursor: pointer;
} }
.openCheatsheet { .openCheatsheet {
background-image: url(<%= asset_data_uri('help_sprite.png') %>); background-image: url(<%= asset_path('help_sprite.png') %>);
background-repeat:no-repeat; background-repeat:no-repeat;
} }
.openCheatsheet:hover { .openCheatsheet:hover {
@ -373,7 +373,7 @@
.mapInfoIcon { .mapInfoIcon {
position: relative; position: relative;
top: 56px; /* puts it just offscreen */ top: 56px; /* puts it just offscreen */
background-image: url(<%= asset_data_uri('mapinfo_sprite.png') %>); background-image: url(<%= asset_path('mapinfo_sprite.png') %>);
background-repeat:no-repeat; background-repeat:no-repeat;
} }
.mapInfoIcon:hover { .mapInfoIcon:hover {
@ -382,8 +382,14 @@
.mapPage .mapInfoIcon { .mapPage .mapInfoIcon {
top: 0; top: 0;
} }
.importDialog {
background-image: url(<%= asset_path('import.png') %>);
background-position: 0 0;
background-repeat: no-repeat;
width: 32px;
}
.starMap { .starMap {
background-image: url(<%= asset_data_uri('starmap_sprite.png') %>); background-image: url(<%= asset_path('starmap_sprite.png') %>);
background-position: 0 0; background-position: 0 0;
background-repeat: no-repeat; background-repeat: no-repeat;
width: 32px; width: 32px;
@ -437,7 +443,7 @@
.takeScreenshot { .takeScreenshot {
margin-bottom: 5px; margin-bottom: 5px;
border-radius: 2px; border-radius: 2px;
background-image: url(<%= asset_data_uri 'screenshot_sprite.png' %>); background-image: url(<%= asset_path 'screenshot_sprite.png' %>);
display: none; display: none;
} }
.takeScreenshot:hover { .takeScreenshot:hover {
@ -450,7 +456,7 @@
.zoomExtents { .zoomExtents {
margin-bottom:5px; margin-bottom:5px;
border-radius: 2px; border-radius: 2px;
background-image: url(<%= asset_data_uri('extents_sprite.png') %>); background-image: url(<%= asset_path('extents_sprite.png') %>);
} }
.zoomExtents:hover { .zoomExtents:hover {
@ -458,7 +464,7 @@
} }
.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, .zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder,
.mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, importDialog:hover .tooltipsAbove, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin {
display: block; display: block;
} }
@ -623,7 +629,7 @@
} }
.zoomIn { .zoomIn {
background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); background-image: url(<%= asset_path('zoom_sprite.png') %>);
background-position: 0 /…0; background-position: 0 /…0;
border-top-left-radius: 2px; border-top-left-radius: 2px;
border-top-right-radius: 2px; border-top-right-radius: 2px;
@ -632,7 +638,7 @@
background-position: -32px 0; background-position: -32px 0;
} }
.zoomOut { .zoomOut {
background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); background-image: url(<%= asset_path('zoom_sprite.png') %>);
background-position:0 -32px; background-position:0 -32px;
border-bottom-left-radius: 2px; border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
@ -650,15 +656,14 @@
} }
#exploreMaps { #exploreMaps {
padding: 0 5%;
position: absolute; position: absolute;
width: 90%;
height: 100%; height: 100%;
width: 100%;
overflow-y: auto; overflow-y: auto;
} }
#exploreMaps > div { #exploreMaps > div {
margin-top: 110px; margin: 110px auto 0 auto;
} }
.button.loadMore { .button.loadMore {
@ -740,23 +745,23 @@
left:5px; left:5px;
} }
.exploreMapsCenter .myMaps .exploreMapsIcon { .exploreMapsCenter .myMaps .exploreMapsIcon {
background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -32px 0; background-position: -32px 0;
} }
.exploreMapsCenter .sharedMaps .exploreMapsIcon { .exploreMapsCenter .sharedMaps .exploreMapsIcon {
background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -128px 0; background-position: -128px 0;
} }
.exploreMapsCenter .activeMaps .exploreMapsIcon { .exploreMapsCenter .activeMaps .exploreMapsIcon {
background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: 0 0; background-position: 0 0;
} }
.exploreMapsCenter .featuredMaps .exploreMapsIcon { .exploreMapsCenter .featuredMaps .exploreMapsIcon {
background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -96px 0; background-position: -96px 0;
} }
.exploreMapsCenter .starredMaps .exploreMapsIcon { .exploreMapsCenter .starredMaps .exploreMapsIcon {
background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-image: url(<%= asset_path 'exploremaps_sprite.png' %>);
background-position: -96px 0; background-position: -96px 0;
} }
.myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon { .myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon {

View file

@ -1,11 +1,9 @@
/* =USERVOICE ICON DEFINE .unauthenticated .feedback-icon {
--------------------------------------------------------*/
.unauthenticated .uv-icon {
display: none; display: none;
} }
div.uv-icon.uv-bottom-left { .feedback-icon {
position: fixed;
background-image: url(<%= asset_data_uri 'feedback_sprite.png' %>); background-image: url(<%= asset_data_uri 'feedback_sprite.png' %>);
background-repeat: no-repeat; background-repeat: no-repeat;
color:#FFFFFF; color:#FFFFFF;
@ -20,6 +18,8 @@ div.uv-icon.uv-bottom-left {
opacity: 1; opacity: 1;
} }
div.uv-icon.uv-bottom-left:hover { .feedback-icon:hover {
background-position: 0 -110px; background-position: 0 -110px;
} }

View file

@ -0,0 +1,259 @@
/* Map Cards */
.map {
display:inline-block;
width:220px;
height:340px;
font-size: 12px;
text-align: left;
overflow: visible;
background: #e8e8e8;
border-radius:2px;
margin:16px;
box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16);
&.newMap {
float: left;
position: relative;
&:hover {
background: #dcdcdc;
.newMapImage {
background-position: 0 -72px;
}
}
a {
height: 340px;
display: block;
position: relative;
}
.newMapImage {
display: block;
width: 72px;
height: 72px;
background-image: url("<%= asset_data_uri('newmap_sprite.png') %>");
background-repeat: no-repeat;
background-position: 0 0;
position: absolute;
left: 50%;
margin-left: -36px;
top: 50%;
margin-top: -36px;
}
span {
font-family: 'din-regular', sans-serif;
font-size: 18px;
line-height: 22px;
text-align: center;
display: block;
padding-top: 220px;
}
}
.mapCard {
position:relative;
width:100%;
height:308px;
padding: 0 0 16px 0;
color: #424242;
&:hover {
.dropdownMenu .menuToggle .circle {
background-color: #FFF;
}
.dropdownMenu .menuToggle:hover .circle {
background-color: #DDD;
}
.mainContent {
filter: blur(2px);
}
.mapMetadata {
display: block;
}
}
.mapHasMapper, .mapHasConversation {
position: absolute;
top: 8px;
left: 8px;
min-width: 32px;
min-height: 32px;
&:hover {
background-color: #FFF;
border-radius: 2px;
.mapperList {
display: block;
}
}
.mapperList {
display: none;
padding: 8px;
list-style-type: none;
li {
&.live {
height: 32px;
padding-left: 32px;
font-size: 16px;
}
img {
width: 24px;
height: 24px;
border-radius: 12px;
display: inline-block;
vertical-align: middle;
}
span {
padding-left: 10px;
font-size: 14px;
}
}
}
}
.mapHasMapper {
background: url('<%= asset_path('junto.png') %>') no-repeat 4px 0;
}
.mapHasConversation {
background: url('<%= asset_path('junto.gif') %>') no-repeat 4px 0;
}
.dropdownMenu {
position: absolute;
top: 8px;
right: 8px;
cursor: pointer;
.menuToggle {
width: 30px;
height: 10px;
.circle {
display: inline-block;
background-color: #454545;
width: 6px;
height: 6px;
border-radius: 3px;
margin: 2px;
}
&:hover .circle {
background-color: #222;
}
}
.menuItems {
position: absolute;
top: 18px;
right: 0px;
background: #FFF;
border-radius: 2px;
list-style-type: none;
color: #454545;
li {
white-space: nowrap;
padding: 6px;
&:hover {
background-color: #DDD;
}
}
}
}
.mapScreenshot {
width: 100%;
height: 220px;
}
.mapScreenshot img {
width: 100%;
}
.title {
word-wrap: break-word;
font-size:18px;
line-height:22px;
height: 71px;
display:table;
padding: 0 16px;
font-family: 'din-regular', sans-serif;
margin: 0 auto;
.innerTitle {
display: table-cell;
vertical-align: middle;
text-align: center;
}
}
.creatorAndPerm {
padding: 8px;
}
.creatorImage {
display: inline-block;
border-radius: 16px;
vertical-align: middle;
width: 32px;
height: 32px;
}
span.creatorName {
margin-left: 8px;
}
.cardViewOnly {
float: right;
line-height: 32px;
padding-right: 10px;
color: #454545;
}
.scroll {
display:block;
font-family: helvetica, sans-serif;
font-size: 12px;
word-wrap: break-word;
text-align: center;
margin-top: 16px;
}
.mapMetadata {
display: none;
position: absolute;
top: 0;
left: 0;
padding: 40px 20px 0;
height: 300px;
font-family: 'din-regular', sans-serif;
font-size: 12px;
color: #FFF;
background: -moz-linear-gradient(top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.43) 81%, rgba(0,0,0,0) 100%);
background: -webkit-linear-gradient(top, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%);
background: linear-gradient(to bottom, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a6000000', endColorstr='#00000000',GradientType=0 );
}
.metadataSection {
padding: 16px 0;
width: 90px;
float: left;
font-family: 'din-medium', sans-serif;
text-align: center;
}
}
}

View file

@ -56,7 +56,7 @@
width: 100%; width: 100%;
} }
.wrapper div.mapInfoBox { .wrapper .mapInfoBox {
position: fixed; position: fixed;
top: 50px; top: 50px;
right: 0px; right: 0px;

View file

@ -0,0 +1,98 @@
.viewOnly {
float: left;
margin-left: 16px;
display: none;
height: 32px;
border: 1px solid #BDBDBD;
border-radius: 2px;
background-color: #424242;
color: #FFF;
font-size: 14px;
line-height: 32px;
&.isViewOnly {
display: block;
}
.eyeball {
background: url('<%= asset_path('view-only.png') %>') no-repeat 4px 0;
padding-left: 40px;
border-right: #747474;
padding-right: 10px;
display: inline-block;
}
.requestNotice {
display: none;
padding: 0 8px;
}
.requestAccess {
background-color: #a354cd;
&:hover {
background-color: #9150bc;
}
cursor: pointer;
}
.requestPending {
background-color: #4fc059;
}
.requestNotAccepted {
background-color: #c04f4f;
}
&.sendRequest .requestAccess {
display: inline-block;
}
&.sentRequest .requestPending {
display: inline-block;
}
&.requestDenied .requestNotAccepted {
display: inline-block;
}
}
.request_access {
position: absolute;
width: 90%;
margin: 0 5%;
.monkey {
width: 250px;
height: 250px;
border: 6px solid #424242;
border-radius: 125px;
background: url(https://s3.amazonaws.com/metamaps-assets/site/monkeyselfie.jpg) no-repeat;
background-position: 50% 20%;
background-size: 100%;
margin: 80px auto 20px auto;
}
.explainer_text {
padding: 0 20% 0 20%;
font-size: 24px;
line-height: 30px;
margin-bottom: 20px;
text-align: center;
}
.make_request {
background-color: #a354cd;
display: block;
width: 220px;
height: 14px;
padding: 16px 0;
margin-bottom: 16px;
text-align: center;
border-radius: 2px;
font-size: 14px;
box-shadow: 0px 1px 1.5px rgba(0,0,0,0.12), 0 1px 1px rgba(0,0,0,0.24);
margin: 0 auto 20px auto;
text-decoration: none;
color: #FFFFFF !important;
cursor: pointer;
}
}

View file

@ -0,0 +1,98 @@
# frozen_string_literal: true
class AccessController < ApplicationController
before_action :require_user, only: [:access, :access_request, :approve_access, :approve_access_post,
:deny_access, :deny_access_post, :request_access]
before_action :set_map, only: [:access, :access_request, :approve_access, :approve_access_post,
:deny_access, :deny_access_post, :request_access]
after_action :verify_authorized
# GET maps/:id/request_access
def request_access
@map = nil
respond_to do |format|
format.html do
render 'maps/request_access'
end
end
end
# POST maps/:id/access_request
def access_request
request = AccessRequest.create(user: current_user, map: @map)
# what about push notification to map owner?
MapMailer.access_request_email(request, @map).deliver_later
respond_to do |format|
format.json do
head :ok
end
end
end
# POST maps/:id/access
def access
user_ids = params[:access] || []
@map.add_new_collaborators(user_ids).each do |user_id|
# add_new_collaborators returns array of added users,
# who we then send an email to
MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later
end
@map.remove_old_collaborators(user_ids)
respond_to do |format|
format.json do
head :ok
end
end
end
# GET maps/:id/approve_access/:request_id
def approve_access
request = AccessRequest.find(params[:request_id])
request.approve()
respond_to do |format|
format.html { redirect_to map_path(@map), notice: 'Request was approved' }
end
end
# GET maps/:id/deny_access/:request_id
def deny_access
request = AccessRequest.find(params[:request_id])
request.deny()
respond_to do |format|
format.html { redirect_to map_path(@map), notice: 'Request was turned down' }
end
end
# POST maps/:id/approve_access/:request_id
def approve_access_post
request = AccessRequest.find(params[:request_id])
request.approve()
respond_to do |format|
format.json do
head :ok
end
end
end
# POST maps/:id/deny_access/:request_id
def deny_access_post
request = AccessRequest.find(params[:request_id])
request.deny()
respond_to do |format|
format.json do
head :ok
end
end
end
private
def set_map
@map = Map.find(params[:id])
authorize @map
end
end

View file

@ -2,11 +2,11 @@
module Api module Api
module V1 module V1
class DeprecatedController < ApplicationController class DeprecatedController < ApplicationController
# rubocop:disable Style/MethodMissing def deprecated
def method_missing render json: {
render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } error: '/api/v1 has been deprecated! Please use /api/v2 instead.'
}, status: :gone
end end
# rubocop:enable Style/MethodMissing
end end
end end
end end

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class MappingsController < DeprecatedController
end
end
end

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class MapsController < DeprecatedController
end
end
end

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class SynapsesController < DeprecatedController
end
end
end

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class TokensController < DeprecatedController
end
end
end

View file

@ -1,7 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class TopicsController < DeprecatedController
end
end
end

View file

@ -29,6 +29,11 @@ module Api
head :no_content head :no_content
end end
def catch_404
skip_authorization
render json: { error: '404 Not found' }, status: :not_found
end
private private
def accessible_records def accessible_records

View file

@ -18,12 +18,6 @@ module Api
create_action create_action
respond_with_resource respond_with_resource
end end
def my_tokens
authorize resource_class
instantiate_collection
respond_with_collection
end
end end
end end
end end

View file

@ -21,23 +21,14 @@ class ApplicationController < ActionController::Base
helper_method :authenticated? helper_method :authenticated?
helper_method :admin? helper_method :admin?
def after_sign_in_path_for(resource)
sign_in_url = url_for(action: 'new', controller: 'sessions', only_path: false)
if request.referer == sign_in_url
super
elsif params[:uv_login] == '1'
'http://support.metamaps.cc/login_success?sso=' + current_sso_token
else
stored_location_for(resource) || request.referer || root_path
end
end
def handle_unauthorized def handle_unauthorized
if authenticated? if authenticated? and params[:controller] == 'maps' and params[:action] == 'show'
head :forbidden # TODO: make this better redirect_to request_access_map_path(params[:id])
elsif authenticated?
redirect_to root_path, notice: "You don't have permission to see that page."
else else
redirect_to new_user_session_path, notice: 'Try signing in to do that.' store_location_for(resource, request.fullpath)
redirect_to sign_in_path, notice: 'Try signing in to do that.'
end end
end end
@ -55,7 +46,7 @@ class ApplicationController < ActionController::Base
def require_user def require_user
return true if authenticated? return true if authenticated?
redirect_to new_user_session_path, notice: 'You must be logged in.' redirect_to sign_in_path, notice: 'You must be logged in.'
return false return false
end end

View file

@ -9,7 +9,7 @@ class ExploreController < ApplicationController
# GET /explore/active # GET /explore/active
def active def active
@maps = map_scope(Map.where.not(name: 'Untitled Map')) @maps = map_scope(Map.where.not(name: 'Untitled Map').where.not(permission: 'private'))
respond_to do |format| respond_to do |format|
format.html do format.html do

View file

@ -8,7 +8,7 @@ class MainController < ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
if authenticated? if authenticated?
@maps = policy_scope(Map).where.not(name: 'Untitled Map') @maps = policy_scope(Map).where.not(name: 'Untitled Map').where.not(permission: 'private')
.order(updated_at: :desc).page(1).per(20) .order(updated_at: :desc).page(1).per(20)
render 'explore/active' render 'explore/active'
else else

View file

@ -1,8 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class MapsController < ApplicationController class MapsController < ApplicationController
before_action :require_user, only: [:create, :update, :destroy, :access, :events] before_action :require_user, only: [:create, :update, :destroy, :events]
before_action :set_map, only: [:show, :update, :destroy, :access, :contains, before_action :set_map, only: [:show, :update, :destroy, :contains, :events, :export]
:events, :export]
after_action :verify_authorized after_action :verify_authorized
# GET maps/:id # GET maps/:id
@ -16,6 +15,7 @@ class MapsController < ApplicationController
@allmappings = policy_scope(@map.mappings) @allmappings = policy_scope(@map.mappings)
@allmessages = @map.messages.sort_by(&:created_at) @allmessages = @map.messages.sort_by(&:created_at)
@allstars = @map.stars @allstars = @map.stars
@allrequests = @map.access_requests
end end
format.json { render json: @map } format.json { render json: @map }
format.csv { redirect_to action: :export, format: :csv } format.csv { redirect_to action: :export, format: :csv }
@ -80,24 +80,6 @@ class MapsController < ApplicationController
end end
end end
# POST maps/:id/access
def access
user_ids = params[:access] || []
@map.add_new_collaborators(user_ids).each do |user_id|
# add_new_collaborators returns array of added users,
# who we then send an email to
MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later
end
@map.remove_old_collaborators(user_ids)
respond_to do |format|
format.json do
render json: { message: 'Successfully altered edit permissions' }
end
end
end
# GET maps/:id/contains # GET maps/:id/contains
def contains def contains
respond_to do |format| respond_to do |format|

View file

@ -22,7 +22,7 @@ class TopicsController < ApplicationController
end end
@all= @topics.to_a.concat(@maps.to_a).sort { |a, b| a.name <=> b.name } @all= @topics.to_a.concat(@maps.to_a).sort { |a, b| a.name <=> b.name }
render json: autocomplete_array_json(@all) render json: autocomplete_array_json(@all).to_json
end end
# GET topics/:id # GET topics/:id

View file

@ -7,6 +7,6 @@ class Users::PasswordsController < Devise::PasswordsController
end end
def after_sending_reset_password_instructions_path_for(_resource_name) def after_sending_reset_password_instructions_path_for(_resource_name)
new_user_session_path if is_navigational_format? sign_in_path if is_navigational_format?
end end
end end

View file

@ -2,19 +2,34 @@
class Users::RegistrationsController < Devise::RegistrationsController class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create] before_action :configure_sign_up_params, only: [:create]
before_action :configure_account_update_params, only: [:update] before_action :configure_account_update_params, only: [:update]
after_action :store_location, only: [:new]
protected protected
def after_sign_up_path_for(resource)
signed_in_root_path(resource)
end
def after_update_path_for(resource) def after_update_path_for(resource)
signed_in_root_path(resource) signed_in_root_path(resource)
end end
def after_sign_in_path_for(resource)
stored = stored_location_for(User)
return stored if stored
if request.referer&.match(sign_in_url) || request.referer&.match(sign_up_url)
super
else
request.referer || root_path
end
end
private private
def store_location
if params[:redirect_to]
store_location_for(User, params[:redirect_to])
end
end
def configure_sign_up_params def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode]) devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode])
end end

View file

@ -0,0 +1,2 @@
class Users::SessionsController < Devise::SessionsController
end

View file

@ -2,10 +2,17 @@
class MapMailer < ApplicationMailer class MapMailer < ApplicationMailer
default from: 'team@metamaps.cc' default from: 'team@metamaps.cc'
def access_request_email(request, map)
@request = request
@map = map
subject = @map.name + ' - request to edit'
mail(to: @map.user.email, subject: subject)
end
def invite_to_edit_email(map, inviter, invitee) def invite_to_edit_email(map, inviter, invitee)
@inviter = inviter @inviter = inviter
@map = map @map = map
subject = @map.name + ' - Invitation to edit' subject = @map.name + ' - invitation to edit'
mail(to: invitee.email, subject: subject) mail(to: invitee.email, subject: subject)
end end
end end

View file

@ -0,0 +1,18 @@
class AccessRequest < ApplicationRecord
belongs_to :user
belongs_to :map
def approve
self.approved = true
self.answered = true
self.save
UserMap.create(user: self.user, map: self.map)
MapMailer.invite_to_edit_email(self.map, self.map.user, self.user).deliver_later
end
def deny
self.approved = false
self.answered = true
self.save
end
end

View file

@ -9,6 +9,7 @@ class Map < ApplicationRecord
has_many :messages, as: :resource, dependent: :destroy has_many :messages, as: :resource, dependent: :destroy
has_many :stars has_many :stars
has_many :access_requests, dependent: :destroy
has_many :user_maps, dependent: :destroy has_many :user_maps, dependent: :destroy
has_many :collaborators, through: :user_maps, source: :user has_many :collaborators, through: :user_maps, source: :user
@ -18,10 +19,10 @@ class Map < ApplicationRecord
# This method associates the attribute ":image" with a file attachment # This method associates the attribute ":image" with a file attachment
has_attached_file :screenshot, has_attached_file :screenshot,
styles: { styles: {
thumb: ['188x126#', :png] thumb: ['220x220#', :png]
#:full => ['940x630#', :png] #:full => ['940x630#', :png]
}, },
default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png'
validates :name, presence: true validates :name, presence: true
validates :arranged, inclusion: { in: [true, false] } validates :arranged, inclusion: { in: [true, false] }
@ -58,13 +59,17 @@ class Map < ApplicationRecord
delegate :name, to: :user, prefix: true delegate :name, to: :user, prefix: true
def user_image def user_image
user.image.url user.image.url(:thirtytwo)
end end
def contributor_count def contributor_count
contributors.length contributors.length
end end
def star_count
stars.length
end
def collaborator_ids def collaborator_ids
collaborators.map(&:id) collaborators.map(&:id)
end end
@ -86,7 +91,7 @@ class Map < ApplicationRecord
end end
def as_json(_options = {}) def as_json(_options = {})
json = super(methods: [:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json = super(methods: [:user_name, :user_image, :star_count, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at])
json[:created_at_clean] = created_at_str json[:created_at_clean] = created_at_str
json[:updated_at_clean] = updated_at_str json[:updated_at_clean] = updated_at_str
json json
@ -102,7 +107,8 @@ class Map < ApplicationRecord
mappers: contributors, mappers: contributors,
collaborators: editors, collaborators: editors,
messages: messages.sort_by(&:created_at), messages: messages.sort_by(&:created_at),
stars: stars stars: stars,
requests: access_requests
} }
end end
@ -122,6 +128,7 @@ class Map < ApplicationRecord
removed = current_collaborators.map(&:id).map do |old_user_id| removed = current_collaborators.map(&:id).map do |old_user_id|
next nil if user_ids.include?(old_user_id) next nil if user_ids.include?(old_user_id)
user_maps.where(user_id: old_user_id).find_each(&:destroy) user_maps.where(user_id: old_user_id).find_each(&:destroy)
access_requests.where(user_id: old_user_id).find_each(&:destroy)
old_user_id old_user_id
end end
removed.compact removed.compact

View file

@ -65,6 +65,11 @@ class User < ApplicationRecord
json json
end end
def all_accessible_maps
#TODO: is there a way to keep this an ActiveRecord relation?
maps + shared_maps
end
def recentMetacodes def recentMetacodes
array = [] array = []
self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t| self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t|

View file

@ -37,10 +37,36 @@ class MapPolicy < ApplicationPolicy
end end
def access? def access?
# note that this is to edit who can access the map # this is for the map creator to bulk change who can access the map
user.present? && record.user == user user.present? && record.user == user
end end
def request_access?
# this is to access the page where you can request access to a map
user.present?
end
def access_request?
# this is to actually request access
user.present?
end
def approve_access?
record.user == user
end
def deny_access?
approve_access?
end
def approve_access_post?
approve_access?
end
def deny_access_post?
approve_access?
end
def contains? def contains?
show? show?
end end

View file

@ -8,11 +8,13 @@ class MappingPolicy < ApplicationPolicy
# a private topic, since you can't see the private topic anyways # a private topic, since you can't see the private topic anyways
visible = %w(public commons) visible = %w(public commons)
permission = 'maps.permission IN (?)' permission = 'maps.permission IN (?)'
if user return scope.joins(:map).where(permission, visible) unless user
scope.joins(:map).where(permission, visible).or(scope.joins(:map).where(user_id: user.id))
else # if this is getting changed, the policy_scope for messages should also be changed
scope.joins(:map).where(permission, visible) # as it is based entirely on the map to which it belongs
end scope.joins(:map).where(permission, visible)
.or(scope.joins(:map).where('maps.id IN (?)', user.shared_maps.map(&:id)))
.or(scope.joins(:map).where('maps.user_id = ?', user.id))
end end
end end

View file

@ -4,11 +4,13 @@ class MessagePolicy < ApplicationPolicy
def resolve def resolve
visible = %w(public commons) visible = %w(public commons)
permission = 'maps.permission IN (?)' permission = 'maps.permission IN (?)'
if user return scope.joins(:map).where(permission, visible) unless user
scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id)
else # if this is getting changed, the policy_scope for mappings should also be changed
scope.where(permission, visible) # as it is based entirely on the map to which it belongs
end scope.joins(:map).where(permission, visible)
.or(scope.joins(:map).where('maps.id IN (?)', user.shared_maps.map(&:id)))
.or(scope.joins(:map).where('maps.user_id = ?', user.id))
end end
end end

View file

@ -3,11 +3,10 @@ class SynapsePolicy < ApplicationPolicy
class Scope < Scope class Scope < Scope
def resolve def resolve
visible = %w(public commons) visible = %w(public commons)
return scope.where(permission: visible) unless user return scope.where(permission: visible) unless user
scope.where(permission: visible) scope.where(permission: visible)
.or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id)))
.or(scope.where(user_id: user.id)) .or(scope.where(user_id: user.id))
end end
end end

View file

@ -10,11 +10,11 @@ class TokenPolicy < ApplicationPolicy
end end
end end
def create? def index?
user.present? user.present?
end end
def my_tokens? def create?
user.present? user.present?
end end

View file

@ -6,7 +6,7 @@ class TopicPolicy < ApplicationPolicy
return scope.where(permission: visible) unless user return scope.where(permission: visible) unless user
scope.where(permission: visible) scope.where(permission: visible)
.or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id)))
.or(scope.where(user_id: user.id)) .or(scope.where(user_id: user.id))
end end
end end

View file

@ -2,12 +2,14 @@
<%= render :partial => 'layouts/templates' %> <%= render :partial => 'layouts/templates' %>
<%= render :partial => 'shared/metacodeBgColors' %> <%= render :partial => 'shared/metacodeBgColors' %>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
// TODO move this into Metamaps.ServerData somehow
<% if current_user %> <% if current_user %>
Metamaps.Active.Mapper = <%= current_user.to_json.html_safe %> Metamaps.Active.Mapper = <%= current_user.to_json.html_safe %>
<% else %> <% else %>
Metamaps.Active.Mapper = null; Metamaps.Active.Mapper = null;
<% end %> <% end %>
// TODO move this into frontend/
Metamaps.Loading = { Metamaps.Loading = {
loader: new CanvasLoader('loading'), loader: new CanvasLoader('loading'),
hide: function () { hide: function () {
@ -22,8 +24,6 @@
Metamaps.Loading.loader.setDensity(41); // default is 40 Metamaps.Loading.loader.setDensity(41); // default is 40
Metamaps.Loading.loader.setRange(0.9); // default is 1.3 Metamaps.Loading.loader.setRange(0.9); // default is 1.3
Metamaps.Loading.loader.show(); // Hidden by default Metamaps.Loading.loader.show(); // Hidden by default
USERVOICE.load();
</script> </script>
<%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %> <%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %>
</body> </body>

View file

@ -74,7 +74,7 @@
<a id="chromeIcon" href="https://www.google.com/chrome/browser/" target="_blank">Chrome</a> <a id="chromeIcon" href="https://www.google.com/chrome/browser/" target="_blank">Chrome</a>
<a id="fireFoxIcon" href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a> <a id="fireFoxIcon" href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>
<a id="safariIcon" href="http://support.apple.com/downloads/#safari" target="_blank">Safari</a> <a id="safariIcon" href="http://support.apple.com/downloads/#safari" target="_blank">Safari</a>
<p id="noIEbody">While it's downloading, explore our <a href="http://blog.metamaps.cc/">blog</a>,<br> watch the <a href="http://vimeo.com/88334167">tutorials</a>, or visit our <a href="http://metamapscc.uservoice.com/">knowledge base</a>! <p id="noIEbody">While it's downloading, explore our <a href="http://blog.metamaps.cc/">blog</a>,<br> watch the <a href="http://vimeo.com/88334167">tutorials</a>, or visit our <a href="https://docs.metamaps.cc">knowledge base</a>!
</div> </div>

View file

@ -8,11 +8,11 @@
<div class="infoAndHelp"> <div class="infoAndHelp">
<%= render :partial => 'maps/mapinfobox' %> <%= render :partial => 'maps/mapinfobox' %>
<% starred = current_user && @map && current_user.starred_map?(@map) <% starred = current_user && @map && current_user.starred_map?(@map)
starClass = starred ? 'starred' : '' starClass = starred ? 'starred' : ''
tooltip = starred ? 'Star' : 'Unstar' %> tooltip = starred ? 'Star' : 'Unstar' %>
<div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div> <div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div>
<div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div> <div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div>
<div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div> <div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div>
<div class="clearfloat"></div> <div class="clearfloat"></div>
</div> </div>

View file

@ -30,7 +30,7 @@
</li> </li>
<% end %> <% end %>
<li> <li>
<%= link_to "Global Maps", explore_active_path, :data => { :router => 'true'} %> <%= link_to "All Maps", explore_active_path, :data => { :router => 'true'} %>
</li> </li>
<% if not current_user %> <% if not current_user %>
<li> <li>
@ -42,7 +42,7 @@
<%= link_to "Request Invite", request_path %> <%= link_to "Request Invite", request_path %>
</li> </li>
<li> <li>
<%= link_to "Login", new_user_session_path %> <%= link_to "Login", sign_in_path %>
</li> </li>
<% end %> <% end %>
<% if current_user %> <% if current_user %>

View file

@ -183,11 +183,12 @@
<span class="title"> <span class="title">
<div class="titleWrapper" id="titleActivator"> <div class="titleWrapper" id="titleActivator">
<span class="best_in_place best_in_place_name" <span class="best_in_place best_in_place_name"
data-url="/topics/{{id}}" data-bip-url="/topics/{{id}}"
data-object="topic" data-bip-object="topic"
data-attribute="name" data-bip-attribute="name"
data-activator="#titleActivator" data-bip-activator="#titleActivator"
data-type="textarea">{{name}}</span> data-bip-value="{{name}}"
data-bip-type="textarea">{{name}}</span>
</div> </div>
</span> </span>
<div class="links"> <div class="links">
@ -220,7 +221,7 @@
</div> </div>
<div class="scroll"> <div class="scroll">
<div class="desc"> <div class="desc">
<span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea">{{desc}}</span> <span class="best_in_place best_in_place_desc" data-bip-url="/topics/{{id}}" data-bip-object="topic" data-bip-nil="{{desc_nil}}" data-bip-attribute="desc" data-bip-type="textarea" data-bip-value="{{desc_markdown}}">{{{desc_html}}}</span>
<div class="clearfloat"></div> <div class="clearfloat"></div>
</div> </div>
</div> </div>

View file

@ -14,11 +14,32 @@
<div class="sidebarSearchIcon"></div> <div class="sidebarSearchIcon"></div>
<div class="clearfloat"></div> <div class="clearfloat"></div>
</div> <!-- end sidebarSearch --> </div> <!-- end sidebarSearch -->
<% request = current_user && @map && @allrequests.find{|a| a.user == current_user}
className = (@map and not policy(@map).update?) ? 'isViewOnly ' : ''
if @map
className += 'sendRequest' if not request
className += 'sentRequest' if request and not request.answered
className += 'requestDenied' if request and request.answered and not request.approved
end %>
<div class="viewOnly <%= className %>">
<div class="eyeball">View Only</div>
<% if current_user %>
<div class="requestAccess requestNotice">Request Access</div>
<div class="requestPending requestNotice">Request Pending</div>
<div class="requestNotAccepted requestNotice">Request Not Accepted</div>
<% end %>
</div>
<div class="clearfloat"></div> <div class="clearfloat"></div>
</div><!-- end upperLeftUI --> </div><!-- end upperLeftUI -->
<div class="upperRightUI"> <div class="upperRightUI">
<div class="mapElement upperRightEl upperRightMapButtons"> <div class="mapElement upperRightEl upperRightMapButtons">
<div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox">
<div class="tooltipsAbove">Import data</div>
</div>
<!-- filtering --> <!-- filtering -->
<div class="sidebarFilter upperRightEl"> <div class="sidebarFilter upperRightEl">
<div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div> <div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div>

View file

@ -9,7 +9,7 @@
<body class="<%= authenticated? ? "authenticated" : "unauthenticated" %>"> <body class="<%= authenticated? ? "authenticated" : "unauthenticated" %>">
<a class='feedback-icon' target='_blank' href='https://hylo.com/c/metamaps'></a>
<%= content_tag :div, class: "main" do %> <%= content_tag :div, class: "main" do %>

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body style="font-family: sans-serif; width: 100%; padding: 24px 16px 16px 16px; background-color: #f5f5f5; text-align: center;">
<div style="padding: 16px; background: white; text-align: left;">
<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %>
<p><span style="font-weight: bold;"><%= @request.user.name %></span> is requesting access to <span style="font-weight: bold">collaboratively edit</span> the following metamap:</p>
<p><%= @map.name %></p>
<p><%= link_to "Grant", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>
<p><%= link_to "Deny", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %></p>
<%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %>
<p style="font-size: 12px;">Make sense with Metamaps</p>
</div>
</body>
</html>

View file

@ -0,0 +1,10 @@
<%= @request.user.name %> has requested to collaboratively edit the following metamap:
<%= @map.name %> [<%= map_url(@map) %>]
Approve Request [<%= approve_access_map_url(id: @map.id, request_id: @request.id) %>]
Deny Request [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>]
Make sense with Metamaps

View file

@ -8,7 +8,7 @@
<div style="padding: 16px; background: white; text-align: left;"> <div style="padding: 16px; background: white; text-align: left;">
<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %>
<p><span style="font-weight: bold;"><%= @inviter.name %></span> has invited you to <span style="font-weight: bold">collaboratively edit</span> the following metamap:</p> <p><span style="font-weight: bold;"><%= @inviter.name %></span> has invited you to <span style="font-weight: bold">collaboratively edit</span> the following map:</p>
<p><%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %></p> <p><%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %></p>
<% if @map.desc %> <% if @map.desc %>
<p style="font-size: 12px;"><%= @map.desc %></p> <p style="font-size: 12px;"><%= @map.desc %></p>

View file

@ -1,4 +1,4 @@
<%= @inviter.name %> has invited you to collaboratively edit the following metamap: <%= @inviter.name %> has invited you to collaboratively edit the following map:
<%= @map.name %> [<%= map_url(@map) %>] <%= @map.name %> [<%= map_url(@map) %>]

View file

@ -16,7 +16,7 @@
<% if @map %> <% if @map %>
<div class="mapInfoName" id="mapInfoName"> <div class="mapInfoName" id="mapInfoName">
<% if policy(@map).update? %> <% if policy(@map).update? %>
<span class="best_in_place best_in_place_name" id="best_in_place_map_<%= @map.id %>_name" data-url="/maps/<%= @map.id %>" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName"><%= @map.name %></span> <span class="best_in_place best_in_place_name" id="best_in_place_map_<%= @map.id %>_name" data-bip-url="/maps/<%= @map.id %>" data-bip-object="map" data-bip-attribute="name" data-bip-type="textarea" data-bip-activator="#mapInfoName" data-bip-value="<%= @map.name %>"><%= @map.name %></span>
<% else %> <% else %>
<%= @map.name %> <%= @map.name %>
<% end %> <% end %>
@ -67,7 +67,7 @@
<div class="mapInfoDesc" id="mapInfoDesc"> <div class="mapInfoDesc" id="mapInfoDesc">
<% if policy(@map).update? %> <% if policy(@map).update? %>
<span class="best_in_place best_in_place_desc" id="best_in_place_map_<%= @map.id %>_desc" data-url="/maps/<%= @map.id %>" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc"><%= @map.desc %></span> <span class="best_in_place best_in_place_desc" id="best_in_place_map_<%= @map.id %>_desc" data-bip-url="/maps/<%= @map.id %>" data-bip-object="map" data-bip-attribute="desc" data-bip-nil="Click to add description..." data-bip-type="textarea" data-bip-activator="#mapInfoDesc" data-bip-value="<%= @map.desc %>"><%= @map.desc %></span>
<% else %> <% else %>
<%= @map.desc %> <%= @map.desc %>
<% end %> <% end %>

View file

@ -0,0 +1,37 @@
<%#
# @file
# Code to request access to a map
# /maps/:id/request_access
#%>
<% content_for :title, 'Request Access | Metamaps' %>
<% content_for :mobile_title, 'Request Access' %>
<div id="yield">
<div class='request_access'>
<div class='monkey'></div>
<div class='explainer_text'>
Hmmm. This map is private, but you can request to edit it from the map creator.
</div>
<div class='make_request'>REQUEST ACCESS</div>
</div>
</div>
<script>
$(document).ready(function() {
$('.make_request').click(function() {
var that = $(this)
that.off('click')
that.text('requesting...')
$.ajax({
url: '/maps/<%= params[:id] %>/access_request',
type: 'POST',
contentType: 'application/json',
statusCode: {
200: function () { that.text('Request Sent'); setTimeout(function () {window.location.href = '/'}, 2000) },
400: function () { that.text('An error occurred') }
}
})
})
})
</script>

View file

@ -39,7 +39,7 @@
<div class="csItem indented"><span class="csTitle">Esc:</span> Hides auto-suggestion results</div> <div class="csItem indented"><span class="csTitle">Esc:</span> Hides auto-suggestion results</div>
<div class="csItem indented"><span class="csTitle">Enter:</span> create a new topic</div> <div class="csItem indented"><span class="csTitle">Enter:</span> create a new topic</div>
<div class="csItem indented"><span class="csTitle">Gear Icon:</span> open up metacode settings</div> <div class="csItem indented"><span class="csTitle">Gear Icon:</span> open up metacode settings</div>
<div class="csItem"><br><a href="http://metamapscc.uservoice.com/knowledgebase/articles/425787-creating-and-editing-topics" target= "_blank">Learn More</a></div> <div class="csItem"><br><a href="https://docs.metamaps.cc/creating_topics.html" target= "_blank">Learn More</a></div>
</div> </div>
<div id="csEditingTopics"> <div id="csEditingTopics">
@ -71,7 +71,7 @@
<span class="csTitle">Open 'Context Menu':</span> Right-click/alt+click on topic icon or synapse <span class="csTitle">Open 'Context Menu':</span> Right-click/alt+click on topic icon or synapse
</div> </div>
<div class="csItem indented">*Hide/Remove/Delete topic within context menu</div> <div class="csItem indented">*Hide/Remove/Delete topic within context menu</div>
<div class="csItem"><br><a href="http://metamapscc.uservoice.com/knowledgebase/articles/425787-creating-and-editing-topics" target= "_blank">Learn More</a></div> <div class="csItem"><br><a href="https://docs.metamaps.cc/creating_topics.html" target= "_blank">Learn More</a></div>
</div> </div>
@ -83,7 +83,7 @@
<div class="csItem"><span class="csTitle">Create new Topic with Synapse:</span> Right-click + drag from topic to open canvas</div> <div class="csItem"><span class="csTitle">Create new Topic with Synapse:</span> Right-click + drag from topic to open canvas</div>
<div class="csItem indented"><span class="csTitle">Enter:</span> Create topic</div> <div class="csItem indented"><span class="csTitle">Enter:</span> Create topic</div>
<div class="csItem indented"><span class="csTitle">Enter:</span> Create synapse</div> <div class="csItem indented"><span class="csTitle">Enter:</span> Create synapse</div>
<div class="csItem"><br><a href="http://metamapscc.uservoice.com/knowledgebase/articles/425790-creating-and-editing-synapses" target= "_blank">Learn More</a></div> <div class="csItem"><br><a href="https://docs.metamaps.cc/creating_synapses.html" target= "_blank">Learn More</a></div>
</div> </div>
@ -96,7 +96,7 @@
<div class="csItem indented"><span class="csTitle">Browse synapses / change visible synapse</span> click on arrow icon and select desired synapse</div> <div class="csItem indented"><span class="csTitle">Browse synapses / change visible synapse</span> click on arrow icon and select desired synapse</div>
<div class="csItem"><span class="csTitle">Open 'Context Menu':</span> Right-click/alt-click on Synapse</div> <div class="csItem"><span class="csTitle">Open 'Context Menu':</span> Right-click/alt-click on Synapse</div>
<div class="csItem indented">*Hide/Remove/Delete synapse within context menu</div> <div class="csItem indented">*Hide/Remove/Delete synapse within context menu</div>
<div class="csItem"><br><a href="http://metamapscc.uservoice.com/knowledgebase/articles/425790-creating-and-editing-synapses" target= "_blank">Learn More</a></div> <div class="csItem"><br><a href="https://docs.metamaps.cc/creating_synapses.html" target= "_blank">Learn More</a></div>
</div> </div>
@ -104,7 +104,7 @@
<div class="csItem"><span class="csTitle">Move around Canvas:</span> Click and drag</div> <div class="csItem"><span class="csTitle">Move around Canvas:</span> Click and drag</div>
<div class="csItem"><span class="csTitle">Zoom in/out:</span> Scroll OR click on <div id="zoomIn"> </div> & <div id="zoomOut"> </div></div> <div class="csItem"><span class="csTitle">Zoom in/out:</span> Scroll OR click on <div id="zoomIn"> </div> & <div id="zoomOut"> </div></div>
<div class="csItem"><span class="csTitle">Zoom to see all:</span> Click <div id="centerMap"></div> OR Ctrl + E</div> <div class="csItem"><span class="csTitle">Zoom to see all:</span> Click <div id="centerMap"></div> OR Ctrl + E</div>
<div class="csItem"><br><a href="http://metamapscc.uservoice.com/knowledgebase/articles/425784-viewing-existing-maps" target= "_blank">Learn More</a></div> <div class="csItem"><br><a href="https://docs.metamaps.cc/exploring_maps.html" target= "_blank">Learn More</a></div>
</div> </div>
@ -153,43 +153,21 @@
<div id="moreResources"> <div id="moreResources">
<p>For more information about Metamaps.cc, visit our Knowledge Base or skip directly to a section by clicking on one of the categories below.</p> <p>For more information about Metamaps.cc, visit our Knowledge Base or skip directly to a section by clicking on one of the categories below.</p>
<div class="resourcesColumnOne resourcesColumn"> <div class="resourcesColumnOne resourcesColumn">
<a href="http://metamapscc.uservoice.com/forums/262715-general" target="_blank" class="button">Feedback Forums</a> <a href="https://hylo.com/c/metamaps" target="_blank" class="button">Hylo User Community</a>
<ul> <ul>
<li> <li><a href="https://docs.metamaps.cc/getting_started.html" target="_blank">Getting Started</a></li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/61031-getting-started" target="_blank">Getting Started</a> <li><a href="https://docs.metamaps.cc/best_practices.html" target="_blank">Best Practices</a></li>
</li> <li><a href="https://docs.metamaps.cc/applications_and_use_cases.html" target="_blank">Applications & Use Cases</a></li>
<li> <li><a href="https://docs.metamaps.cc/advanced_features.html" target="_blank">Advanced Features</a></li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63372-key-fundamentals" target="_blank">Key Fundamentals</a>
</li>
<li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/61033-best-practices" target="_blank">Best Practices</a>
</li>
<li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63377-general-troubleshooting" target="_blank">General Troubleshooting</a>
</li>
<li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63443-applications-use-cases" target="_blank">Applications & Use Cases</a>
</li>
</ul> </ul>
</div> </div>
<div class="resourcesColumnTwo resourcesColumn"> <div class="resourcesColumnTwo resourcesColumn">
<a href="http://metamapscc.uservoice.com/knowledgebase" target="_blank" class="button">KNOWLEDGE BASE</a> <a href="https://docs.metamaps.cc" target="_blank" class="button">KNOWLEDGE BASE</a>
<ul> <ul>
<li> <li><a href="https://docs.metamaps.cc/general_questions.html" target="_blank">General Questions</a></li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63440-general-questions" target="_blank">General Questions</a> <li><a href="https://docs.metamaps.cc/project_organization_and_governance.html" target="_blank">Organization & Governance</a></li>
</li> <li><a href="https://docs.metamaps.cc/realtime_collaboration_junto.html" target="_blank">Realtime Collaboration</a></li>
<li> <li><a href="https://docs.metamaps.cc/importing_and_exporting_data.html" target="_blank">Importing and Exporting Data</a></li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63375-getting-involved" target="_blank">Getting Involved</a>
</li>
<li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63376-project-organization-governance" target="_blank">Organization & Governance</a>
</li>
<li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63378-technical-infrastructure" target="_blank">Technical Infrastructure</a>
</li>
<li>
<a href="http://metamapscc.uservoice.com/knowledgebase/topics/63587-theory-references" target="_blank">References & Key Theory</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -2,8 +2,16 @@
# Note: you need to run `npm install` before using this script or raml2html won't be installed # Note: you need to run `npm install` before using this script or raml2html won't be installed
OLD_DIR=$(pwd)
cd $(dirname $0)/..
if [[ ! -x ./node_modules/.bin/raml2html ]]; then if [[ ! -x ./node_modules/.bin/raml2html ]]; then
npm install npm install
fi fi
./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html ./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html -t doc/api/templates/template.nunjucks
if [[ -x $(which open) ]]; then
open public/api/index.html
fi
cd $OLD_DIR

3
bin/bundle Executable file
View file

@ -0,0 +1,3 @@
#!/usr/bin/env ruby
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
load Gem.bin_path('bundler', 'bundle')

4
bin/rails Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'

4
bin/rake Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/env ruby
require_relative '../config/boot'
require 'rake'
Rake.application.run

View file

@ -32,7 +32,7 @@ Rails.application.configure do
# Print deprecation notices to the Rails logger # Print deprecation notices to the Rails logger
config.active_support.deprecation = :log config.active_support.deprecation = :log
config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' config.action_mailer.preview_path = "#{Rails.root}/spec/mailers/previews"
# Expands the lines which load the assets # Expands the lines which load the assets
config.assets.debug = false config.assets.debug = false

View file

@ -30,6 +30,7 @@ Metamaps::Application.configure do
# The :test delivery method accumulates sent emails in the # The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array. # ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'localhost:3000' }
# Print deprecation notices to the stderr # Print deprecation notices to the stderr
config.active_support.deprecation = :stderr config.active_support.deprecation = :stderr

View file

@ -5,13 +5,13 @@ Doorkeeper.configure do
# This block will be called to check whether the resource owner is authenticated or not. # This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do resource_owner_authenticator do
current_user || redirect_to(new_user_session_url) current_user || redirect_to(sign_in_url)
end end
# If you want to restrict access to the web interface for adding oauth authorized applications, # If you want to restrict access to the web interface for adding oauth authorized applications,
# you need to declare the block below. # you need to declare the block below.
admin_authenticator do admin_authenticator do
current_user || redirect_to(new_user_session_url) current_user || redirect_to(sign_in_url)
end end
# Authorization Code expiration time (default 10 minutes). # Authorization Code expiration time (default 10 minutes).

View file

@ -1,11 +0,0 @@
# frozen_string_literal: true
require 'uservoice-ruby'
def current_sso_token
@current_sso_token ||= UserVoice.generate_sso_token(
'metamapscc',
ENV['SSO_KEY'],
{ email: current_user.email },
300 # Default expiry time is 5 minutes = 300 seconds
)
end

View file

@ -19,9 +19,17 @@ Metamaps::Application.routes.draw do
get :export get :export
post 'events/:event', action: :events post 'events/:event', action: :events
get :contains get :contains
post :access, default: { format: :json }
post :star, to: 'stars#create', defaults: { format: :json } get :request_access, to: 'access#request_access'
post :unstar, to: 'stars#destroy', defaults: { format: :json } get 'approve_access/:request_id', to: 'access#approve_access', as: :approve_access
get 'deny_access/:request_id', to: 'access#deny_access', as: :deny_access
post :access_request, to: 'access#access_request', default: { format: :json }
post 'approve_access/:request_id', to: 'access#approve_access_post', default: { format: :json }
post 'deny_access/:request_id', to: 'access#deny_access_post', default: { format: :json }
post :access, to: 'access#access', default: { format: :json }
post :star, to: 'stars#create', default: { format: :json }
post :unstar, to: 'stars#destroy', default: { format: :json }
end end
end end
@ -54,6 +62,19 @@ Metamaps::Application.routes.draw do
end end
end end
devise_for :users, skip: :sessions, controllers: {
registrations: 'users/registrations',
passwords: 'users/passwords',
sessions: 'users/sessions'
}
devise_scope :user do
get 'login' => 'users/sessions#new', :as => :sign_in
post 'login' => 'users/sessions#create', :as => :user_session
get 'logout' => 'users/sessions#destroy', :as => :destroy_user_session
get 'join' => 'users/registrations#new', :as => :sign_up
end
resources :users, except: [:index, :destroy] do resources :users, except: [:index, :destroy] do
member do member do
get :details get :details
@ -70,38 +91,18 @@ Metamaps::Application.routes.draw do
delete :stars, to: 'stars#destroy', on: :member delete :stars, to: 'stars#destroy', on: :member
end end
resources :synapses, only: [:index, :create, :show, :update, :destroy] resources :synapses, only: [:index, :create, :show, :update, :destroy]
resources :tokens, only: [:create, :destroy] do resources :tokens, only: [:index, :create, :destroy]
get :my_tokens, on: :collection
end
resources :topics, only: [:index, :create, :show, :update, :destroy] resources :topics, only: [:index, :create, :show, :update, :destroy]
resources :users, only: [:index, :show] do resources :users, only: [:index, :show] do
get :current, on: :collection get :current, on: :collection
end end
match '*path', to: 'restful#catch_404', via: :all
end end
namespace :v1, path: '/v1' do namespace :v1, path: '/v1' do
# api v1 routes all lead to a deprecation error method root to: 'deprecated#deprecated', via: :all
# see app/controllers/api/v1/deprecated_controller.rb match '*path', to: 'deprecated#deprecated', via: :all
resources :maps, only: [:create, :show, :update, :destroy]
resources :synapses, only: [:create, :show, :update, :destroy]
resources :topics, only: [:create, :show, :update, :destroy]
resources :mappings, only: [:create, :show, :update, :destroy]
resources :tokens, only: [:create, :destroy] do
get :my_tokens, on: :collection
end
end end
end match '*path', to: 'v2/restful#catch_404', via: :all
devise_for :users, skip: :sessions, controllers: {
registrations: 'users/registrations',
passwords: 'users/passwords',
sessions: 'devise/sessions'
}
devise_scope :user do
get 'login' => 'devise/sessions#new', :as => :new_user_session
post 'login' => 'devise/sessions#create', :as => :user_session
get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session
get 'join' => 'devise/registrations#new', :as => :new_user_registration_path
end end
namespace :hacks do namespace :hacks do

View file

@ -0,0 +1,12 @@
class CreateAccessRequests < ActiveRecord::Migration[5.0]
def change
create_table :access_requests do |t|
t.references :user, foreign_key: true
t.boolean :approved, default: false
t.boolean :answered, default: false
t.references :map, foreign_key: true
t.timestamps
end
end
end

View file

@ -10,11 +10,22 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160928022635) do ActiveRecord::Schema.define(version: 20161013162214) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
create_table "access_requests", force: :cascade do |t|
t.integer "user_id"
t.boolean "approved", default: false
t.boolean "answered", default: false
t.integer "map_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["map_id"], name: "index_access_requests_on_map_id", using: :btree
t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree
end
create_table "delayed_jobs", force: :cascade do |t| create_table "delayed_jobs", force: :cascade do |t|
t.integer "priority", default: 0, null: false t.integer "priority", default: 0, null: false
t.integer "attempts", default: 0, null: false t.integer "attempts", default: 0, null: false
@ -36,13 +47,10 @@ ActiveRecord::Schema.define(version: 20160928022635) do
t.string "eventable_type" t.string "eventable_type"
t.integer "user_id" t.integer "user_id"
t.integer "map_id" t.integer "map_id"
t.integer "sequence_id"
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree
t.index ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree
t.index ["map_id"], name: "index_events_on_map_id", using: :btree t.index ["map_id"], name: "index_events_on_map_id", using: :btree
t.index ["sequence_id"], name: "index_events_on_sequence_id", using: :btree
t.index ["user_id"], name: "index_events_on_user_id", using: :btree t.index ["user_id"], name: "index_events_on_user_id", using: :btree
end end
@ -67,6 +75,7 @@ ActiveRecord::Schema.define(version: 20160928022635) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "mappable_id" t.integer "mappable_id"
t.string "mappable_type" t.string "mappable_type"
t.boolean "in_trash"
t.index ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree t.index ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree
t.index ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree t.index ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree
t.index ["map_id"], name: "index_mappings_on_map_id", using: :btree t.index ["map_id"], name: "index_mappings_on_map_id", using: :btree
@ -75,16 +84,16 @@ ActiveRecord::Schema.define(version: 20160928022635) do
end end
create_table "maps", force: :cascade do |t| create_table "maps", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "name" t.text "name"
t.boolean "arranged"
t.text "desc" t.text "desc"
t.text "permission" t.text "permission"
t.integer "user_id" t.integer "user_id"
t.datetime "created_at", null: false t.boolean "arranged"
t.datetime "updated_at", null: false
t.boolean "featured" t.boolean "featured"
t.string "screenshot_file_name" t.string "screenshot_file_name", limit: 255
t.string "screenshot_content_type" t.string "screenshot_content_type", limit: 255
t.integer "screenshot_file_size" t.integer "screenshot_file_size"
t.datetime "screenshot_updated_at" t.datetime "screenshot_updated_at"
t.index ["user_id"], name: "index_maps_on_user_id", using: :btree t.index ["user_id"], name: "index_maps_on_user_id", using: :btree
@ -103,21 +112,21 @@ ActiveRecord::Schema.define(version: 20160928022635) do
end end
create_table "metacode_sets", force: :cascade do |t| create_table "metacode_sets", force: :cascade do |t|
t.string "name" t.string "name", limit: 255
t.text "desc" t.text "desc"
t.integer "user_id" t.integer "user_id"
t.boolean "mapperContributed" t.boolean "mapperContributed"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree t.index ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree
end end
create_table "metacodes", force: :cascade do |t| create_table "metacodes", force: :cascade do |t|
t.text "name" t.text "name"
t.string "manual_icon" t.string "manual_icon", limit: 255
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "color" t.string "color", limit: 255
t.string "aws_icon_file_name" t.string "aws_icon_file_name"
t.string "aws_icon_content_type" t.string "aws_icon_content_type"
t.integer "aws_icon_file_size" t.integer "aws_icon_file_size"
@ -173,14 +182,15 @@ ActiveRecord::Schema.define(version: 20160928022635) do
create_table "synapses", force: :cascade do |t| create_table "synapses", force: :cascade do |t|
t.text "desc" t.text "desc"
t.text "category" t.text "category"
t.text "weight"
t.text "permission"
t.integer "topic1_id" t.integer "topic1_id"
t.integer "topic2_id" t.integer "topic2_id"
t.integer "user_id" t.integer "user_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.text "permission"
t.text "weight"
t.integer "defer_to_map_id" t.integer "defer_to_map_id"
t.boolean "in_trash"
t.index ["topic1_id", "topic1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree t.index ["topic1_id", "topic1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree
t.index ["topic1_id"], name: "index_synapses_on_topic1_id", using: :btree t.index ["topic1_id"], name: "index_synapses_on_topic1_id", using: :btree
t.index ["topic2_id", "topic2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree t.index ["topic2_id", "topic2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree
@ -201,20 +211,21 @@ ActiveRecord::Schema.define(version: 20160928022635) do
t.text "name" t.text "name"
t.text "desc" t.text "desc"
t.text "link" t.text "link"
t.text "permission"
t.integer "user_id" t.integer "user_id"
t.integer "metacode_id" t.integer "metacode_id"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "image_file_name" t.text "permission"
t.string "image_content_type" t.string "image_file_name", limit: 255
t.string "image_content_type", limit: 255
t.integer "image_file_size" t.integer "image_file_size"
t.datetime "image_updated_at" t.datetime "image_updated_at"
t.string "audio_file_name" t.string "audio_file_name", limit: 255
t.string "audio_content_type" t.string "audio_content_type", limit: 255
t.integer "audio_file_size" t.integer "audio_file_size"
t.datetime "audio_updated_at" t.datetime "audio_updated_at"
t.integer "defer_to_map_id" t.integer "defer_to_map_id"
t.boolean "in_trash"
t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree
t.index ["user_id"], name: "index_topics_on_user_id", using: :btree t.index ["user_id"], name: "index_topics_on_user_id", using: :btree
end end
@ -229,30 +240,30 @@ ActiveRecord::Schema.define(version: 20160928022635) do
end end
create_table "users", force: :cascade do |t| create_table "users", force: :cascade do |t|
t.string "name" t.string "name", limit: 255
t.string "email" t.string "email", limit: 255
t.text "settings" t.string "crypted_password", limit: 255
t.string "code", limit: 8 t.string "password_salt", limit: 255
t.string "joinedwithcode", limit: 8 t.string "persistence_token", limit: 255
t.string "crypted_password" t.string "perishable_token", limit: 255
t.string "password_salt"
t.string "persistence_token"
t.string "perishable_token"
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.string "code", limit: 8
t.string "joinedwithcode", limit: 8
t.text "settings"
t.string "encrypted_password", limit: 128, default: "" t.string "encrypted_password", limit: 128, default: ""
t.string "remember_token" t.string "remember_token", limit: 255
t.datetime "remember_created_at" t.datetime "remember_created_at"
t.string "reset_password_token" t.string "reset_password_token", limit: 255
t.datetime "last_sign_in_at" t.datetime "last_sign_in_at"
t.string "last_sign_in_ip" t.string "last_sign_in_ip", limit: 255
t.integer "sign_in_count", default: 0 t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at" t.datetime "current_sign_in_at"
t.string "current_sign_in_ip" t.string "current_sign_in_ip", limit: 255
t.datetime "reset_password_sent_at" t.datetime "reset_password_sent_at"
t.boolean "admin" t.boolean "admin"
t.string "image_file_name" t.string "image_file_name", limit: 255
t.string "image_content_type" t.string "image_content_type", limit: 255
t.integer "image_file_size" t.integer "image_file_size"
t.datetime "image_updated_at" t.datetime "image_updated_at"
t.integer "generation" t.integer "generation"
@ -268,5 +279,7 @@ ActiveRecord::Schema.define(version: 20160928022635) do
t.index ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree t.index ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree
end end
add_foreign_key "access_requests", "maps"
add_foreign_key "access_requests", "users"
add_foreign_key "tokens", "users" add_foreign_key "tokens", "users"
end end

View file

@ -1,13 +1,19 @@
#%RAML 1.0 #%RAML 1.0
--- ---
title: Metamaps title: Metamaps
version: v2.0 version: 2.0
baseUri: https://metamaps.cc/api/v2 baseUri: https://metamaps.cc/api/v2
mediaType: application/json mediaType: application/json
protocols: [ HTTPS ]
documentation:
- title: Getting Started
content: !include pages/getting-started.md
securitySchemes: securitySchemes:
cookie: !include securitySchemes/cookie.raml
token: !include securitySchemes/token.raml
oauth_2_0: !include securitySchemes/oauth_2_0.raml oauth_2_0: !include securitySchemes/oauth_2_0.raml
securedBy: [ oauth_2_0 ] securedBy: [ cookie, token, oauth_2_0 ]
traits: traits:
pageable: !include traits/pageable.raml pageable: !include traits/pageable.raml

View file

@ -1,4 +1,5 @@
#type: collection #type: collection
securedBy: [ null, cookie, token, oauth_2_0 ]
get: get:
is: [ searchable: { searchFields: "name" }, orderable, pageable ] is: [ searchable: { searchFields: "name" }, orderable, pageable ]
responses: responses:
@ -7,6 +8,7 @@ get:
application/json: application/json:
example: !include ../examples/metacodes.json example: !include ../examples/metacodes.json
/{id}: /{id}:
securedBy: [ null, cookie, token, oauth_2_0 ]
#type: item #type: item
get: get:
responses: responses:

View file

@ -1,4 +1,13 @@
#type: collection #type: collection
get:
description: |
A list of the current user's tokens.
is: [ searchable: { searchFields: description }, pageable, orderable ]
responses:
200:
body:
application/json:
example: !include ../examples/tokens.json
post: post:
body: body:
application/json: application/json:
@ -11,14 +20,6 @@ post:
body: body:
application/json: application/json:
example: !include ../examples/token.json example: !include ../examples/token.json
/my_tokens:
get:
is: [ searchable: { searchFields: description }, pageable, orderable ]
responses:
200:
body:
application/json:
example: !include ../examples/tokens.json
/{id}: /{id}:
#type: item #type: item
delete: delete:

View file

@ -1,4 +1,5 @@
#type: collection #type: collection
securedBy: [ null, cookie, token, oauth_2_0 ]
get: get:
is: [ searchable: { searchFields: "name" }, orderable, pageable ] is: [ searchable: { searchFields: "name" }, orderable, pageable ]
responses: responses:
@ -6,6 +7,15 @@ get:
body: body:
application/json: application/json:
example: !include ../examples/users.json example: !include ../examples/users.json
/{id}:
#type: item
securedBy: [ null, cookie, token, oauth_2_0 ]
get:
responses:
200:
body:
application/json:
example: !include ../examples/user.json
/current: /current:
#type: item #type: item
get: get:
@ -14,11 +24,3 @@ get:
body: body:
application/json: application/json:
example: !include ../examples/current_user.json example: !include ../examples/current_user.json
/{id}:
#type: item
get:
responses:
200:
body:
application/json:
example: !include ../examples/user.json

View file

@ -0,0 +1,3 @@
One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI.
To run a POST or DELETE request, you can use the Fetch API. See the example in the next section.

View file

@ -0,0 +1,2 @@
There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. Check the security tab of any of the endpoints above for instructions on logging in.

View file

@ -0,0 +1,41 @@
We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1)
The first step is to register your client app.
#### Registering the client
Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now).
#### Requesting authorization
To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL:
```
http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code
```
Once you are there, you should sign in and click on `Authorize`.
You will then see a response that contains your "authorization code", which you need to exchange for an access token.
#### Requesting the access token
To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch`
```javascript
fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', {
method: 'POST',
credentials: 'same-origin'
}).then(response => {
return response.json()
}).then(console.log).catch(console.error)
# The response will be like
{
"access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54",
"token_type": "bearer",
"expires_in": 7200,
"refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
}
```
You can now make requests to the API with the access token returned.

View file

@ -0,0 +1,25 @@
If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means.
To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website.
```
fetch('/api/v2/tokens', {
method: 'GET',
credentials: 'same-origin' // needed to use the cookie-based auth
}).then(response => {
return response.json()
}).then(console.log).catch(console.error)
```
If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method:
```
fetch('/api/v2/tokens', {
method: 'POST',
credentials: 'same-origin'
}).then(response => {
return response.json()
}).then(console.log).catch(console.error)
```
`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere.

View file

@ -0,0 +1,3 @@
description: !include ../pages/cookie_tutorial.md
type: x-cookie
displayName: Secured by cookie-based authentication

View file

@ -1,5 +1,4 @@
description: | description: !include ../pages/oauth_2_0_tutorial.md
OAuth 2.0 implementation
type: OAuth 2.0 type: OAuth 2.0
settings: settings:
authorizationUri: https://metamaps.cc/api/v2/oauth/authorize authorizationUri: https://metamaps.cc/api/v2/oauth/authorize

View file

@ -0,0 +1,3 @@
description: !include ../pages/token_tutorial.md
type: x-token
displayName: Secured by token-based authentication

View file

@ -0,0 +1,61 @@
<li>
{% if item.displayName %}
<strong>{{ item.displayName }}</strong>:
{% else %}
<strong>{{ item.key }}</strong>:
{% endif %}
{% if not item.structuredValue %}
<em>
{%- if item.required -%}required {% endif -%}
(
{%- if item.enum -%}
{%- if item.enum.length === 1 -%}
{{ item.enum.join(', ') }}
{%- else -%}
one of {{ item.enum.join(', ') }}
{%- endif -%}
{%- else -%}
{{ item.type }}
{%- endif -%}
{%- if item.default or item.default == 0 or item.default == false %} - default: {{ item.default }}{%- endif -%}
{%- if item.repeat %} - repeat: {{ item.repeat }}{%- endif -%}
{%- if item.type == 'string' -%}
{%- if item.minLength or item.minLength == 0 %} - minLength: {{ item.minLength }}{%- endif -%}
{%- if item.maxLength or item.maxLength == 0 %} - maxLength: {{ item.maxLength }}{%- endif -%}
{%- else -%}
{%- if item.minimum or item.minimum == 0 %} - minimum: {{ item.minimum }}{%- endif -%}
{%- if item.maximum or item.maximum == 0 %} - maximum: {{ item.maximum }}{%- endif -%}
{%- endif -%}
{%- if item.pattern %} - pattern: {{ item.pattern }}{%- endif -%}
)
</em>
{% endif %}
{% markdown %}
{{ item.description }}
{% endmarkdown %}
{#
{% if item.type %}
<p><strong>Type</strong>:</p>
<pre><code>{{ item.type | escape }}</code></pre>
{% endif %}
#}
{% if item.examples.length %}
<p><strong>Examples</strong>:</p>
{% for example in item.examples %}
{% if item.type == 'string' %}
<pre>{{ example | escape }}</pre>
{% else %}
<pre><code>{{ example | escape }}</code></pre>
{% endif %}
{% endfor %}
{% endif %}
{% if item.structuredValue %}
<pre><code>{{ item.structuredValue | dump }}</code></pre>
{% endif %}
</li>

View file

@ -0,0 +1,314 @@
{% if (resource.methods or (resource.description and resource.parentUrl)) %}
<div class="panel panel-white">
<div class="panel-heading">
<h4 class="panel-title">
<a class="collapsed" data-toggle="collapse" href="#panel_{{ resource.uniqueId }}">
<span class="parent">{{ resource.parentUrl }}</span>{{ resource.relativeUri }}
</a>
<span class="methods">
{% for method in resource.methods %}
<a href="#{{ resource.uniqueId }}_{{ method.method }}"><!-- modal shown by hashchange event -->
<span class="badge badge_{{ method.method }}">{{ method.method }}
{% if method.securedBy.length %}
{% if method.securedBy | first == null %}
<span class="glyphicon glyphicon-transfer" title="Authentication not required"></span>
{% endif %}
<span class="glyphicon glyphicon-lock" title="Authentication required"></span>
{% endif %}
</span>
</a>
{% endfor %}
</span>
</h4>
</div>
<div id="panel_{{ resource.uniqueId }}" class="panel-collapse collapse">
<div class="panel-body">
{% if resource.parentUrl %}
{% if resource.description %}
<div class="resource-description">
{% markdown %}
{{ resource.description }}
{% endmarkdown %}
</div>
{% endif %}
{% endif %}
<div class="list-group">
{% for method in resource.methods %}
<div onclick="window.location.href = '#{{ resource.uniqueId }}_{{ method.method }}'" class="list-group-item">
<span class="badge badge_{{ method.method }}">
{{ method.method }}
{% if method.securedBy.length %}
{% if method.securedBy | first == null %}
<span class="glyphicon glyphicon-transfer" title="Authentication not required"></span>
{% endif %}
<span class="glyphicon glyphicon-lock" title="Authentication required"></span>
{% endif %}
</span>
<div class="method_description">
{% markdown %}
{{ method.description}}
{% endmarkdown %}
</div>
<div class="clearfix"></div>
</div>
{% endfor %}
</div>
</div>
</div>
{% for method in resource.methods %}
<div class="modal fade" tabindex="0" id="{{ resource.uniqueId }}_{{ method.method }}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="myModalLabel">
<span class="badge badge_{{ method.method }}">
{{ method.method }}
{% if method.securedBy.length %}
{% if method.securedBy | first == null %}
<span class="glyphicon glyphicon-transfer" title="Authentication not required"></span>
{% endif %}
<span class="glyphicon glyphicon-lock" title="Authentication required"></span>
{% endif %}
</span>
<span class="parent">{{ resource.parentUrl }}</span>{{ resource.relativeUri }}
</h4>
</div>
<div class="modal-body">
{% if method.description %}
<div class="alert alert-info">
{% markdown %}
{{ method.description}}
{% endmarkdown %}
</div>
{% endif %}
<!-- Nav tabs -->
<ul class="nav nav-tabs">
{% if method.allUriParameters.length or method.queryString or method.queryParameters or method.headers or method.body %}
<li class="active">
<a href="#{{ resource.uniqueId }}_{{ method.method }}_request" data-toggle="tab">Request</a>
</li>
{% endif %}
{% if method.responses %}
<li{%
if not method.allUriParameters.length and not method.queryParameters
and not method.queryString
and not method.headers and not method.body
%} class="active"{%
endif
%}>
<a href="#{{ resource.uniqueId }}_{{ method.method }}_response" data-toggle="tab">Response</a>
</li>
{% endif %}
{% if method.securedBy.length %}
<li>
<a href="#{{ resource.uniqueId }}_{{ method.method }}_securedby" data-toggle="tab">Security</a>
</li>
{% endif %}
</ul>
<!-- Tab panes -->
<div class="tab-content">
{% if method.allUriParameters.length or method.queryString or method.queryParameters or method.headers or method.body %}
<div class="tab-pane active" id="{{ resource.uniqueId }}_{{ method.method }}_request">
{% if resource.allUriParameters.length %}
<h3>URI Parameters</h3>
<ul>
{% for item in resource.allUriParameters %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if method.annotations.length %}
<h3>Annotations</h3>
<ul>
{% for item in method.annotations %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if method.headers.length %}
<h3>Headers</h3>
<ul>
{% for item in method.headers %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if method.queryString and method.queryString.properties.length %}
<h3>Query String</h3>
<ul>
{% for item in method.queryString.properties %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if method.queryParameters.length %}
<h3>Query Parameters</h3>
<ul>
{% for item in method.queryParameters %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if method.body %}
<h3>Body</h3>
{% for b in method.body %}
<p><strong>Type: {{ b.key }}</strong></p>
{#
{% if b.type %}
<p><strong>Type</strong>:</p>
<pre><code>{{ b.type | escape }}</code></pre>
{% endif %}
#}
{% if b.properties.length %}
<strong>Properties</strong>
<ul>
{% for item in b.properties %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if b.examples.length %}
<p><strong>Examples</strong>:</p>
{% for example in b.examples %}
<pre><code>{{ example | escape }}</code></pre>
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
</div>
{% endif %}
{% if method.responses %}
<div class="tab-pane{%
if not method.allUriParameters.length and not method.queryParameters.length
and not method.queryString
and not method.headers.length and not method.body.length
%} active{%
endif
%}" id="{{ resource.uniqueId }}_{{ method.method }}_response">
{% for response in method.responses %}
<h2>HTTP status code <a href="http://httpstatus.es/{{ response.code }}" target="_blank">{{ response.code }}</a></h2>
{% markdown %}
{{ response.description}}
{% endmarkdown %}
{% if response.headers.length %}
<h3>Headers</h3>
<ul>
{% for item in response.headers %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if response.body.length %}
<h3>Body</h3>
{% for b in response.body %}
<p><strong>Type: {{ b.key }}</strong></p>
{#
{% if b.type %}
<p><strong>Type</strong>:</p>
<pre><code>{{ b.type | escape }}</code></pre>
{% endif %}
#}
{% if b.properties.length %}
<strong>Properties</strong>
<ul>
{% for item in b.properties %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% if b.examples.length %}
<p><strong>Examples</strong>:</p>
{% for example in b.examples %}
<pre><code>{{ example | escape }}</code></pre>
{% endfor %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
</div>
{% endif %}
{% if method.securedBy.length %}
<div class="tab-pane" id="{{ resource.uniqueId }}_{{ method.method }}_securedby">
{% for securedBy in method.securedBy %}
{% if securedBy == null %}
<div class="alert alert-info">
<span class="glyphicon glyphicon-transfer" title="Authentication not required"></span>
This route can be accessed anonymously.</h1>
</div>
{% else %}
{% set securityScheme = securitySchemes[securedBy] %}
<div class="alert alert-warning">
{% set securedByScopes = renderSecuredBy(securedBy) %}
<span class="glyphicon glyphicon-lock" title="Authentication required"></span> Secured by {{ securedByScopes }}
{% set securityScheme = securitySchemes[securedBy] %}
{% if securityScheme.description %}
{% markdown %}
{{ securityScheme.description }}
{% endmarkdown %}
{% endif %}
{% if securityScheme.describedBy.headers.length %}
<h3>Headers</h3>
<ul>
{% for item in securityScheme.describedBy.headers %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% for response in securityScheme.describedBy.responses.length %}
<h2>HTTP status code <a href="http://httpstatus.es/{{ response.code }}" target="_blank">{{ response.code }}</a></h2>
{% markdown %}
{{ response.description}}
{% endmarkdown %}
{% if response.headers.length %}
<h3>Headers</h3>
<ul>
{% for item in response.headers %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
{% endfor %}
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% for resource in resource.resources %}
{% include "./resource.nunjucks" %}
{% endfor %}

View file

@ -0,0 +1,232 @@
<!DOCTYPE HTML>
<html>
<head>
<title>{{ title }} API documentation</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="generator" content="https://github.com/raml2html/raml2html {{ config.raml2HtmlVersion }}">
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/styles/default.min.css">
<script type="text/javascript" src="https://code.jquery.com/jquery-1.11.0.min.js"></script>
<script type="text/javascript" src="https://netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/highlight.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$('.page-header pre code, .top-resource-description pre code, .modal-body pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
$('[data-toggle]').click(function() {
var selector = $(this).data('target') + ' pre code';
$(selector).each(function(i, block) {
hljs.highlightBlock(block);
});
});
// open modal on hashes like #_action_get
$(window).bind('hashchange', function(e) {
var anchor_id = document.location.hash.substr(1); //strip #
var element = $('#' + anchor_id);
// do we have such element + is it a modal? --> show it
if (element.length && element.hasClass('modal')) {
element.modal('show');
}
});
// execute hashchange on first page load
$(window).trigger('hashchange');
// remove url fragment on modal hide
$('.modal').on('hidden.bs.modal', function() {
try {
if (history && history.replaceState) {
history.replaceState({}, '', '#');
}
} catch(e) {}
});
});
</script>
<style>
.hljs {
background: transparent;
}
.parent {
color: #999;
}
.list-group-item > .badge {
float: none;
margin-right: 6px;
}
.panel-title > .methods {
float: right;
}
.badge {
border-radius: 0;
text-transform: uppercase;
width: 70px;
font-weight: normal;
color: #f3f3f6;
line-height: normal;
}
.badge_get {
background-color: #63a8e2;
}
.badge_post {
background-color: #6cbd7d;
}
.badge_put {
background-color: #22bac4;
}
.badge_delete {
background-color: #d26460;
}
.badge_patch {
background-color: #ccc444;
}
.list-group, .panel-group {
margin-bottom: 0;
}
.panel-group .panel+.panel-white {
margin-top: 0;
}
.panel-group .panel-white {
border-bottom: 1px solid #F5F5F5;
border-radius: 0;
}
.panel-white:last-child {
border-bottom-color: white;
-webkit-box-shadow: none;
box-shadow: none;
}
.panel-white .panel-heading {
background: white;
}
.tab-pane ul {
padding-left: 2em;
}
.tab-pane h1 {
font-size: 1.3em;
}
.tab-pane h2 {
font-size: 1.2em;
padding-bottom: 4px;
border-bottom: 1px solid #ddd;
}
.tab-pane h3 {
font-size: 1.1em;
}
.tab-content {
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 10px;
}
#sidebar {
margin-top: 30px;
padding-right: 5px;
overflow: auto;
height: 90%;
}
.top-resource-description {
border-bottom: 1px solid #ddd;
background: #fcfcfc;
padding: 15px 15px 0 15px;
margin: -15px -15px 10px -15px;
}
.resource-description {
border-bottom: 1px solid #fcfcfc;
background: #fcfcfc;
padding: 15px 15px 0 15px;
margin: -15px -15px 10px -15px;
}
.resource-description p:last-child {
margin: 0;
}
.list-group .badge {
float: left;
}
.method_description {
margin-left: 85px;
}
.method_description p:last-child {
margin: 0;
}
.list-group-item {
cursor: pointer;
}
.list-group-item:hover {
background-color: #f5f5f5;
}
pre code {
overflow: auto;
word-wrap: normal;
white-space: pre;
}
</style>
</head>
<body data-spy="scroll" data-target="#sidebar">
<div class="container">
<div class="row">
<div class="col-md-9" role="main">
<div class="page-header">
<h1>{{ title }} API documentation{% if version %} <small>version {{ version }}</small>{% endif %}</h1>
<p>{{ baseUri }}</p>
{% if baseUriParameters %}
<ul>
{% for item in baseUriParameters %}
{% include "./item.nunjucks" %}
{% endfor %}
</ul>
{% endif %}
</div>
{% for resource in resources %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 id="{{ resource.uniqueId }}" class="panel-title">{% if resource.displayName %}{{ resource.displayName}}{% else %}{{ resource.relativeUri }}{% endif %}</h3>
</div>
<div class="panel-body">
{% if resource.description %}
<div class="top-resource-description">
{% markdown %}
{{ resource.description }}
{% endmarkdown %}
</div>
{% endif %}
<div class="panel-group">
{% include "./resource.nunjucks" %}
</div>
</div>
</div>
{% endfor %}
{% for chapter in documentation %}
<h3 id="{{ chapter.uniqueId }}"><a href="#{{ chapter.uniqueId }}">{{ chapter.title }}</a></h3>
{% markdown %}
{{ chapter.content }}
{% endmarkdown %}
{% endfor %}
</div>
<div class="col-md-3">
<div id="sidebar" class="hidden-print affix" role="complementary">
<ul class="nav nav-pills nav-stacked">
{% for resource in resources %}
<li><a href="#{{ resource.uniqueId}}">{% if resource.displayName %}{{ resource.displayName}}{% else %}{{ resource.relativeUri }}{% endif %}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -63,7 +63,7 @@ _Backbone.Map = Backbone.Model.extend({
this.on('saved', this.savedEvent) this.on('saved', this.savedEvent)
}, },
savedEvent: function () { savedEvent: function () {
Realtime.sendMapChange(this) Realtime.updateMap(this)
}, },
authorizeToEdit: function (mapper) { authorizeToEdit: function (mapper) {
if (mapper && ( if (mapper && (
@ -370,7 +370,7 @@ _Backbone.init = function () {
return node return node
}, },
savedEvent: function () { savedEvent: function () {
Realtime.sendTopicChange(this) Realtime.updateTopic(this)
}, },
updateViews: function () { updateViews: function () {
var onPageWithTopicCard = Active.Map || Active.Topic var onPageWithTopicCard = Active.Map || Active.Topic
@ -549,7 +549,7 @@ _Backbone.init = function () {
return edge return edge
}, },
savedEvent: function () { savedEvent: function () {
Realtime.sendSynapseChange(this) Realtime.updateSynapse(this)
}, },
updateViews: function () { updateViews: function () {
this.updateCardView() this.updateCardView()

View file

@ -0,0 +1,38 @@
/* global $ */
import React from 'react'
import ReactDOM from 'react-dom'
import outdent from 'outdent'
import ImportDialogBox from '../../components/ImportDialogBox'
import PasteInput from '../PasteInput'
const ImportDialog = {
openLightbox: null,
closeLightbox: null,
init: function (serverData, openLightbox, closeLightbox) {
const self = ImportDialog
self.openLightbox = openLightbox
self.closeLightbox = closeLightbox
$('#lightbox_content').append($(outdent`
<div class="lightboxContent" id="import-dialog-lightbox">
<div class="importDialogWrapper" />
</div>
`))
ReactDOM.render(React.createElement(ImportDialogBox, {
onFileAdded: PasteInput.handleFile,
exampleImageUrl: serverData['import-example.png']
}), $('.importDialogWrapper').get(0))
},
show: function () {
ImportDialog.openLightbox('import-dialog')
},
hide: function () {
ImportDialog.closeLightbox('import-dialog')
}
}
export default ImportDialog

View file

@ -1,11 +1,14 @@
/* global Metamaps, $ */ /* global Metamaps, $ */
import clipboard from 'clipboard-js'
import Active from '../Active' import Active from '../Active'
import Create from '../Create' import Create from '../Create'
import Search from './Search' import Search from './Search'
import CreateMap from './CreateMap' import CreateMap from './CreateMap'
import Account from './Account' import Account from './Account'
import ImportDialog from './ImportDialog'
/* /*
* Metamaps.Backbone * Metamaps.Backbone
@ -21,6 +24,7 @@ const GlobalUI = {
self.Search.init() self.Search.init()
self.CreateMap.init() self.CreateMap.init()
self.Account.init() self.Account.init()
self.ImportDialog.init(Metamaps.Erb, self.openLightbox, self.closeLightbox)
if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) if ($('#toast').html().trim()) self.notifyUser($('#toast').html())
@ -137,9 +141,19 @@ const GlobalUI = {
self.hideDiv('#toast') self.hideDiv('#toast')
}, },
shareInvite: function (inviteLink) { shareInvite: function (inviteLink) {
window.prompt('To copy the invite link, press: Ctrl+C, Enter', inviteLink) clipboard.copy({
'text/plain': inviteLink
}).then(() => {
$('#joinCodesBox .popup').remove()
$('#joinCodesBox').append('<p class="popup" style="text-align: center">Copied!</p>')
window.setTimeout(() => $('#joinCodesBox .popup').remove(), 1500)
}, () => {
$('#joinCodesBox .popup').remove()
$('#joinCodesBox').append(`<p class="popup" style="text-align: center">Your browser doesn't support copying, please copy manually.</p>`)
window.setTimeout(() => $('#joinCodesBox .popup').remove(), 1500)
})
} }
} }
export { Search, CreateMap, Account } export { Search, CreateMap, Account, ImportDialog }
export default GlobalUI export default GlobalUI

Some files were not shown because too many files have changed in this diff Show more