initial commit.

This commit is contained in:
mntmn
2017-04-07 01:29:05 +02:00
commit 7ff2926578
258 changed files with 83743 additions and 0 deletions

View File

@@ -0,0 +1,331 @@
var api_endpoint = ENV.apiEndpoint;
var api_socket_endpoint = ENV.websocketsEndpoint;
var api_token = null;
var websocket = null;
var channel_id = null;
var space_auth = null;
function load_resource(method, path, data, on_success, on_error, on_progress) {
var req = new XMLHttpRequest();
req.onload = function(evt,b,c) {
if (req.status>=200 && req.status<=299) {
var parsed = null;
try {
var parsed = JSON.parse(req.response);
} catch(e) {};
if (data && parsed && parsed._id) {
// mutate the local object and update its _id
data._id = parsed._id;
}
if (on_success) {
on_success(parsed,req);
}
} else {
if (on_error) {
on_error(req);
}
}
};
req.onerror = function(err) {
console.log(err,err.target);
// window._spacedeck_location_change is a flag set by redirect / reload functions
if (!window._spacedeck_location_change) {
if (window.spacedeck && window.spacedeck.active_space) {
window.spacedeck.offline = true;
} else {
alert("Could not connect to Spacedeck. Please reconnect and try again.");
}
}
if (on_error) on_error(req);
}
req.withCredentials = true;
req.open(method, api_endpoint+"/api"+path, true);
if (api_token) {
req.setRequestHeader("X-Spacedeck-Auth", api_token);
}
if (space_auth) {
console.log("set space auth", space_auth);
req.setRequestHeader("X-Spacedeck-Space-Auth", space_auth);
}
if (channel_id) {
req.setRequestHeader("X-Spacedeck-Channel", channel_id);
}
if (csrf_token) {
req.setRequestHeader("X-csrf-token", csrf_token);
}
try {
if (data) {
if (data.toString() == "[object File]") {
req.setRequestHeader("Content-type", data.type);
req.setRequestHeader("Accepts", "application/json");
req.upload.onprogress = function(e) {
console.log("upload progress: ",e.loaded,e.total);
if (on_progress) on_progress(e);
}
req.send(data);
} else {
req.setRequestHeader("Content-type", "application/json");
req.send(JSON.stringify(data));
}
} else {
req.send();
}
} catch (e) {
if (on_error) {
on_error(req, e);
} else {
throw(e);
}
}
}
function get_resource(path, on_success, on_error, on_progress) {
load_resource("get", path, null, on_success, on_error, on_progress);
}
function load_profile(name, on_success, on_error) {
load_resource("get", "/users/slug?slug="+name, null, on_success, on_error);
}
function load_current_user(on_success, on_error) {
load_resource("get", "/users/current", null, on_success, on_error);
}
function load_space(id, on_success, on_error) {
if (!id || id=="undefined") {
console.error("load_space id:", id);
return;
}
var url = "/spaces/"+id;
load_resource("get", url, null, function(space, req) {
var role = req.getResponseHeader("x-spacedeck-space-role");
on_success(space, role);
}, on_error);
}
function load_space_path(id, on_success, on_error) {
var url = "/spaces/"+id+"/path";
load_resource("get", url, null, function(space, req) {
on_success(space);
}, on_error);
}
function load_spaces(id, is_home, on_success, on_error) {
if (!id || id=="undefined") {
console.error("load_spaces id:", id);
return;
}
var q = "?parent_space_id="+id;
load_resource("get", "/spaces"+q, null, function(spaces) {
on_success(spaces);
}, on_error);
}
function load_writable_folders( on_success, on_error) {
load_resource("get", "/spaces?writablefolders=true", null, on_success, on_error);
}
function load_history(s, on_success, on_error) {
load_resource("get", "/spaces/"+ s._id +"/digest", null, on_success, on_error);
}
function load_filtered_spaces(filter, on_success, on_error) {
load_resource("get", "/spaces?filter="+filter, null, on_success, on_error);
}
function load_spaces_search(query, on_success, on_error) {
load_resource("get", "/spaces?search="+query, null, on_success, on_error);
}
function load_artifacts(id, on_success, on_error) {
load_resource("get", "/spaces/"+id+"/artifacts", null, on_success, on_error);
}
function save_artifact(a, on_success, on_error) {
if (a._id) {
load_resource("put", "/spaces/"+a.space_id+"/artifacts/"+a._id,a,on_success,on_error);
} else {
load_resource("post", "/spaces/"+a.space_id+"/artifacts",a,on_success,on_error);
}
}
function save_pdf_file(space, point, file, zones, on_success, on_error, on_progress) {
load_resource("post", "/spaces/"+space._id+"/artifacts-pdf?filename="+file.name + "&x="+point.x+"&y="+point.y + "&zones="+zones,file,on_success,on_error,on_progress);
}
function save_artifact_file(a, file,filename, on_success, on_error, on_progress) {
load_resource("post", "/spaces/"+a.space_id+"/artifacts/"+a._id+"/payload?filename="+filename,file,on_success,on_error,on_progress);
}
function save_space(s, on_success, on_error) {
if (s._id) {
delete s['artifacts'];
load_resource("put", "/spaces/"+s._id,s,on_success,on_error);
} else {
load_resource("post", "/spaces",s,on_success,on_error);
}
}
function delete_space(s, on_success, on_error) {
load_resource("delete", "/spaces/"+s._id, null, on_success, on_error);
}
function delete_artifact(a, on_success, on_error) {
load_resource("delete", "/spaces/"+a.space_id+"/artifacts/"+a._id);
}
function duplicate_space(s, to_space_id, on_success, on_error) {
var path = "/spaces/"+s._id+"/duplicate";
if(to_space_id) {
path += "?parent_space_id=" + to_space_id
}
load_resource("post", path, null,on_success,on_error);
}
function load_members(space, on_success, on_error) {
load_resource("get", "/spaces/"+ space._id +"/memberships", null, on_success, on_error);
}
function create_membership(space, m, on_success, on_error) {
load_resource("post", "/spaces/"+ space._id +"/memberships", m, on_success, on_error);
}
function save_membership(space, m, on_success, on_error) {
load_resource("put", "/spaces/"+ space._id +"/memberships/" + m._id, m, on_success, on_error);
}
function delete_membership(space, m, on_success, on_error) {
load_resource("delete", "/spaces/"+ space._id +"/memberships/"+m._id, m, on_success, on_error);
}
function accept_invitation(id, code, on_success, on_error) {
load_resource("get", "/memberships/"+ id +"/accept?code="+code, null, on_success, on_error);
}
function get_join_link(space_id, on_success, on_error) {
load_resource("get", "/invitation_codes?space_id="+space_id, null, on_success, on_error);
}
function create_join_link(space_id, role, on_success, on_error) {
load_resource("post", "/invitation_codes", {join_role:role, sticky:true, space_id:space_id}, on_success, on_error);
}
function delete_join_link(link_id, on_success, on_error) {
load_resource("delete", "/invitation_codes/"+link_id, null, on_success, on_error);
}
function load_team_members(id, on_success, on_error) {
load_resource("get", "/teams/"+ id +"/memberships", null, function(team) {
on_success(team);
}, on_error);
}
function save_avatar_file(type, o, file, on_success, on_error) {
load_resource("post", "/"+type+"s/"+o._id+"/avatar", file, on_success,on_error);
}
function remove_avatar_file(type, o, on_success, on_error) {
load_resource("delete", "/"+type+"s/"+o._id+"/avatar", null, on_success,on_error);
}
function save_space_background_file(space, file, on_success, on_error) {
load_resource("post", "/spaces/"+space._id+"/background?filename="+file.name, file, on_success,on_error);
}
function save_user_background_file(user, file, on_success, on_error) {
load_resource("post", "/users/"+user._id+"/background", file, on_success,on_error);
}
function save_user_password(u, pass, newPass, on_success, on_error) {
load_resource("post", "/users/" + u._id + "/password", {old_password:pass, new_password:newPass}, on_success, on_error);
}
function get_featured_users(on_success, on_error) {
load_resource("get", "/users/featured", null, on_success, on_error);
}
function save_user(u, on_success, on_error) {
load_resource("put", "/users/"+u._id,u,on_success,on_error);
}
function delete_user(u, password, on_success, on_error) {
load_resource("delete", "/users/"+u._id +"?password="+password,null,on_success,on_error);
}
function create_user(name, email, password, password_confirmation, on_success, on_error) {
load_resource("post", "/users", {email:email, nickname:name, password:password, password_confirmation: password_confirmation}, on_success, on_error);
}
function create_session(email, password, on_success, on_error) {
load_resource("post", "/sessions", {email:email, password:password}, on_success, on_error);
}
function delete_session(on_success, on_error) {
load_resource("delete", "/sessions/current", null, on_success, on_error);
}
function create_oauthtoken(on_success, on_error) {
load_resource("get", "/users/oauth2callback/url", null, on_success, on_error);
}
function create_session_for_oauthtoken(token, on_success, on_error) {
load_resource("get", "/users/loginorsignupviagoogle?code="+token, null, on_success, on_error);
}
function create_password_reset(email, on_success, on_error) {
load_resource("post", "/users/password_reset_requests?email=" + encodeURIComponent(email), null, on_success, on_error);
}
function confirm_password_reset(password, confirm, on_success, on_error) {
load_resource("post", "/users/password_reset_requests/"+confirm+"/confirm", {password:password}, on_success, on_error);
}
function confirm_user(user, token, on_success, on_error) {
load_resource("put", "/users/"+user._id+"/confirm", {token:token}, on_success, on_error);
}
function resent_confirm_mail(user, on_success, on_error) {
load_resource("post", "/users/"+user._id+"/confirm", {}, on_success, on_error);
}
function create_feedback(user, m, on_success, on_error) {
load_resource("post", "/users/feedback", {text: m}, on_success, on_error);
}
function save_team(u, on_success, on_error) {
load_resource("put", "/teams/"+u._id,u,on_success,on_error);
}
function load_comments(space_id, on_success, on_error) {
load_resource("get", "/spaces/"+space_id+"/messages", null, on_success, on_error);
}
function save_comment(space_id, data, on_success, on_error) {
load_resource("post", "/spaces/"+space_id +"/messages", data, on_success, on_error);
}
function delete_comment(space_id, message_id,on_success, on_error) {
load_resource("delete", "/spaces/"+space_id +"/messages/"+ message_id, null , on_success, on_error);
}
function update_comment(space_id, data, on_success, on_error) {
load_resource("post", "/spaces/"+space_id+"/messages/" + data._id , data, on_success, on_error);
}
function load_notifications(u, on_success, on_error) {
load_resource("get", "/notifications", null, on_success, on_error);
}

745
public/javascripts/clipboard.js Executable file
View File

@@ -0,0 +1,745 @@
/*!
* clipboard.js v1.5.5
* https://zenorocha.github.io/clipboard.js
*
* Licensed MIT © Zeno Rocha
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var matches = require('matches-selector')
module.exports = function (element, selector, checkYoSelf) {
var parent = checkYoSelf ? element : element.parentNode
while (parent && parent !== document) {
if (matches(parent, selector)) return parent;
parent = parent.parentNode
}
}
},{"matches-selector":2}],2:[function(require,module,exports){
/**
* Element prototype.
*/
var proto = Element.prototype;
/**
* Vendor function.
*/
var vendor = proto.matchesSelector
|| proto.webkitMatchesSelector
|| proto.mozMatchesSelector
|| proto.msMatchesSelector
|| proto.oMatchesSelector;
/**
* Expose `match()`.
*/
module.exports = match;
/**
* Match `el` to `selector`.
*
* @param {Element} el
* @param {String} selector
* @return {Boolean}
* @api public
*/
function match(el, selector) {
if (vendor) return vendor.call(el, selector);
var nodes = el.parentNode.querySelectorAll(selector);
for (var i = 0; i < nodes.length; ++i) {
if (nodes[i] == el) return true;
}
return false;
}
},{}],3:[function(require,module,exports){
var closest = require('closest');
/**
* Delegates event to a selector.
*
* @param {Element} element
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function delegate(element, selector, type, callback) {
var listenerFn = listener.apply(this, arguments);
element.addEventListener(type, listenerFn);
return {
destroy: function() {
element.removeEventListener(type, listenerFn);
}
}
}
/**
* Finds closest match and invokes callback.
*
* @param {Element} element
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Function}
*/
function listener(element, selector, type, callback) {
return function(e) {
e.delegateTarget = closest(e.target, selector, true);
if (e.delegateTarget) {
callback.call(element, e);
}
}
}
module.exports = delegate;
},{"closest":1}],4:[function(require,module,exports){
/**
* Check if argument is a HTML element.
*
* @param {Object} value
* @return {Boolean}
*/
exports.node = function(value) {
return value !== undefined
&& value instanceof HTMLElement
&& value.nodeType === 1;
};
/**
* Check if argument is a list of HTML elements.
*
* @param {Object} value
* @return {Boolean}
*/
exports.nodeList = function(value) {
var type = Object.prototype.toString.call(value);
return value !== undefined
&& (type === '[object NodeList]' || type === '[object HTMLCollection]')
&& ('length' in value)
&& (value.length === 0 || exports.node(value[0]));
};
/**
* Check if argument is a string.
*
* @param {Object} value
* @return {Boolean}
*/
exports.string = function(value) {
return typeof value === 'string'
|| value instanceof String;
};
/**
* Check if argument is a function.
*
* @param {Object} value
* @return {Boolean}
*/
exports.function = function(value) {
var type = Object.prototype.toString.call(value);
return type === '[object Function]';
};
},{}],5:[function(require,module,exports){
var is = require('./is');
var delegate = require('delegate');
/**
* Validates all params and calls the right
* listener function based on its target type.
*
* @param {String|HTMLElement|HTMLCollection|NodeList} target
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listen(target, type, callback) {
if (!target && !type && !callback) {
throw new Error('Missing required arguments');
}
if (!is.string(type)) {
throw new TypeError('Second argument must be a String');
}
if (!is.function(callback)) {
throw new TypeError('Third argument must be a Function');
}
if (is.node(target)) {
return listenNode(target, type, callback);
}
else if (is.nodeList(target)) {
return listenNodeList(target, type, callback);
}
else if (is.string(target)) {
return listenSelector(target, type, callback);
}
else {
throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');
}
}
/**
* Adds an event listener to a HTML element
* and returns a remove listener function.
*
* @param {HTMLElement} node
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenNode(node, type, callback) {
node.addEventListener(type, callback);
return {
destroy: function() {
node.removeEventListener(type, callback);
}
}
}
/**
* Add an event listener to a list of HTML elements
* and returns a remove listener function.
*
* @param {NodeList|HTMLCollection} nodeList
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenNodeList(nodeList, type, callback) {
Array.prototype.forEach.call(nodeList, function(node) {
node.addEventListener(type, callback);
});
return {
destroy: function() {
Array.prototype.forEach.call(nodeList, function(node) {
node.removeEventListener(type, callback);
});
}
}
}
/**
* Add an event listener to a selector
* and returns a remove listener function.
*
* @param {String} selector
* @param {String} type
* @param {Function} callback
* @return {Object}
*/
function listenSelector(selector, type, callback) {
return delegate(document.body, selector, type, callback);
}
module.exports = listen;
},{"./is":4,"delegate":3}],6:[function(require,module,exports){
function select(element) {
var selectedText;
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
element.focus();
element.setSelectionRange(0, element.value.length);
selectedText = element.value;
}
else {
if (element.hasAttribute('contenteditable')) {
element.focus();
}
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(element);
selection.removeAllRanges();
selection.addRange(range);
selectedText = selection.toString();
}
return selectedText;
}
module.exports = select;
},{}],7:[function(require,module,exports){
function E () {
// Keep this empty so it's easier to inherit from
// (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
}
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
return this;
},
once: function (name, callback, ctx) {
var self = this;
function listener () {
self.off(name, listener);
callback.apply(ctx, arguments);
};
listener._ = callback
return this.on(name, listener, ctx);
},
emit: function (name) {
var data = [].slice.call(arguments, 1);
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
return this;
},
off: function (name, callback) {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback && evts[i].fn._ !== callback)
liveEvents.push(evts[i]);
}
}
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
(liveEvents.length)
? e[name] = liveEvents
: delete e[name];
return this;
}
};
module.exports = E;
},{}],8:[function(require,module,exports){
'use strict';
exports.__esModule = true;
var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
var _select = require('select');
var _select2 = _interopRequireDefault(_select);
/**
* Inner class which performs selection from either `text` or `target`
* properties and then executes copy or cut operations.
*/
var ClipboardAction = (function () {
/**
* @param {Object} options
*/
function ClipboardAction(options) {
_classCallCheck(this, ClipboardAction);
this.resolveOptions(options);
this.initSelection();
}
/**
* Defines base properties passed from constructor.
* @param {Object} options
*/
ClipboardAction.prototype.resolveOptions = function resolveOptions() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
this.action = options.action;
this.emitter = options.emitter;
this.target = options.target;
this.text = options.text;
this.trigger = options.trigger;
this.selectedText = '';
};
/**
* Decides which selection strategy is going to be applied based
* on the existence of `text` and `target` properties.
*/
ClipboardAction.prototype.initSelection = function initSelection() {
if (this.text && this.target) {
throw new Error('Multiple attributes declared, use either "target" or "text"');
} else if (this.text) {
this.selectFake();
} else if (this.target) {
this.selectTarget();
} else {
throw new Error('Missing required attributes, use either "target" or "text"');
}
};
/**
* Creates a fake textarea element, sets its value from `text` property,
* and makes a selection on it.
*/
ClipboardAction.prototype.selectFake = function selectFake() {
var _this = this;
this.removeFake();
this.fakeHandler = document.body.addEventListener('click', function () {
return _this.removeFake();
});
this.fakeElem = document.createElement('textarea');
this.fakeElem.style.position = 'absolute';
this.fakeElem.style.left = '-9999px';
this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
this.fakeElem.setAttribute('readonly', '');
this.fakeElem.value = this.text;
document.body.appendChild(this.fakeElem);
this.selectedText = _select2['default'](this.fakeElem);
this.copyText();
};
/**
* Only removes the fake element after another click event, that way
* a user can hit `Ctrl+C` to copy because selection still exists.
*/
ClipboardAction.prototype.removeFake = function removeFake() {
if (this.fakeHandler) {
document.body.removeEventListener('click');
this.fakeHandler = null;
}
if (this.fakeElem) {
document.body.removeChild(this.fakeElem);
this.fakeElem = null;
}
};
/**
* Selects the content from element passed on `target` property.
*/
ClipboardAction.prototype.selectTarget = function selectTarget() {
this.selectedText = _select2['default'](this.target);
this.copyText();
};
/**
* Executes the copy operation based on the current selection.
*/
ClipboardAction.prototype.copyText = function copyText() {
var succeeded = undefined;
try {
succeeded = document.execCommand(this.action);
} catch (err) {
succeeded = false;
}
this.handleResult(succeeded);
};
/**
* Fires an event based on the copy operation result.
* @param {Boolean} succeeded
*/
ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
if (succeeded) {
this.emitter.emit('success', {
action: this.action,
text: this.selectedText,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
} else {
this.emitter.emit('error', {
action: this.action,
trigger: this.trigger,
clearSelection: this.clearSelection.bind(this)
});
}
};
/**
* Removes current selection and focus from `target` element.
*/
ClipboardAction.prototype.clearSelection = function clearSelection() {
if (this.target) {
this.target.blur();
}
window.getSelection().removeAllRanges();
};
/**
* Sets the `action` to be performed which can be either 'copy' or 'cut'.
* @param {String} action
*/
/**
* Destroy lifecycle.
*/
ClipboardAction.prototype.destroy = function destroy() {
this.removeFake();
};
_createClass(ClipboardAction, [{
key: 'action',
set: function set() {
var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
this._action = action;
if (this._action !== 'copy' && this._action !== 'cut') {
throw new Error('Invalid "action" value, use either "copy" or "cut"');
}
},
/**
* Gets the `action` property.
* @return {String}
*/
get: function get() {
return this._action;
}
/**
* Sets the `target` property using an element
* that will be have its content copied.
* @param {Element} target
*/
}, {
key: 'target',
set: function set(target) {
if (target !== undefined) {
if (target && typeof target === 'object' && target.nodeType === 1) {
this._target = target;
} else {
throw new Error('Invalid "target" value, use a valid Element');
}
}
},
/**
* Gets the `target` property.
* @return {String|HTMLElement}
*/
get: function get() {
return this._target;
}
}]);
return ClipboardAction;
})();
exports['default'] = ClipboardAction;
module.exports = exports['default'];
},{"select":6}],9:[function(require,module,exports){
'use strict';
exports.__esModule = true;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var _clipboardAction = require('./clipboard-action');
var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
var _tinyEmitter = require('tiny-emitter');
var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
var _goodListener = require('good-listener');
var _goodListener2 = _interopRequireDefault(_goodListener);
/**
* Base class which takes one or more elements, adds event listeners to them,
* and instantiates a new `ClipboardAction` on each click.
*/
var Clipboard = (function (_Emitter) {
_inherits(Clipboard, _Emitter);
/**
* @param {String|HTMLElement|HTMLCollection|NodeList} trigger
* @param {Object} options
*/
function Clipboard(trigger, options) {
_classCallCheck(this, Clipboard);
_Emitter.call(this);
this.resolveOptions(options);
this.listenClick(trigger);
}
/**
* Helper function to retrieve attribute value.
* @param {String} suffix
* @param {Element} element
*/
/**
* Defines if attributes would be resolved using internal setter functions
* or custom functions that were passed in the constructor.
* @param {Object} options
*/
Clipboard.prototype.resolveOptions = function resolveOptions() {
var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
this.text = typeof options.text === 'function' ? options.text : this.defaultText;
};
/**
* Adds a click event listener to the passed trigger.
* @param {String|HTMLElement|HTMLCollection|NodeList} trigger
*/
Clipboard.prototype.listenClick = function listenClick(trigger) {
var _this = this;
this.listener = _goodListener2['default'](trigger, 'click', function (e) {
return _this.onClick(e);
});
};
/**
* Defines a new `ClipboardAction` on each click event.
* @param {Event} e
*/
Clipboard.prototype.onClick = function onClick(e) {
var trigger = e.delegateTarget || e.currentTarget;
if (this.clipboardAction) {
this.clipboardAction = null;
}
this.clipboardAction = new _clipboardAction2['default']({
action: this.action(trigger),
target: this.target(trigger),
text: this.text(trigger),
trigger: trigger,
emitter: this
});
};
/**
* Default `action` lookup function.
* @param {Element} trigger
*/
Clipboard.prototype.defaultAction = function defaultAction(trigger) {
return getAttributeValue('action', trigger);
};
/**
* Default `target` lookup function.
* @param {Element} trigger
*/
Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
var selector = getAttributeValue('target', trigger);
if (selector) {
return document.querySelector(selector);
}
};
/**
* Default `text` lookup function.
* @param {Element} trigger
*/
Clipboard.prototype.defaultText = function defaultText(trigger) {
return getAttributeValue('text', trigger);
};
/**
* Destroy lifecycle.
*/
Clipboard.prototype.destroy = function destroy() {
this.listener.destroy();
if (this.clipboardAction) {
this.clipboardAction.destroy();
this.clipboardAction = null;
}
};
return Clipboard;
})(_tinyEmitter2['default']);
function getAttributeValue(suffix, element) {
var attribute = 'data-clipboard-' + suffix;
if (!element.hasAttribute(attribute)) {
return;
}
return element.getAttribute(attribute);
}
exports['default'] = Clipboard;
module.exports = exports['default'];
},{"./clipboard-action":8,"good-listener":5,"tiny-emitter":7}]},{},[9])(9)
});

View File

@@ -0,0 +1,841 @@
;(function () {
'use strict';
/**
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
*
* @codingstandard ftlabs-jsv2
* @copyright The Financial Times Limited [All Rights Reserved]
* @license MIT License (see LICENSE.txt)
*/
/*jslint browser:true, node:true*/
/*global define, Event, Node*/
/**
* Instantiate fast-clicking listeners on the specified layer.
*
* @constructor
* @param {Element} layer The layer to listen on
* @param {Object} [options={}] The options to override the defaults
*/
function FastClick(layer, options) {
var oldOnClick;
options = options || {};
/**
* Whether a click is currently being tracked.
*
* @type boolean
*/
this.trackingClick = false;
/**
* Timestamp for when click tracking started.
*
* @type number
*/
this.trackingClickStart = 0;
/**
* The element being tracked for a click.
*
* @type EventTarget
*/
this.targetElement = null;
/**
* X-coordinate of touch start event.
*
* @type number
*/
this.touchStartX = 0;
/**
* Y-coordinate of touch start event.
*
* @type number
*/
this.touchStartY = 0;
/**
* ID of the last touch, retrieved from Touch.identifier.
*
* @type number
*/
this.lastTouchIdentifier = 0;
/**
* Touchmove boundary, beyond which a click will be cancelled.
*
* @type number
*/
this.touchBoundary = options.touchBoundary || 10;
/**
* The FastClick layer.
*
* @type Element
*/
this.layer = layer;
/**
* The minimum time between tap(touchstart and touchend) events
*
* @type number
*/
this.tapDelay = options.tapDelay || 200;
/**
* The maximum time for a tap
*
* @type number
*/
this.tapTimeout = options.tapTimeout || 700;
if (FastClick.notNeeded(layer)) {
return;
}
// Some old versions of Android don't have Function.prototype.bind
function bind(method, context) {
return function() { return method.apply(context, arguments); };
}
var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
var context = this;
for (var i = 0, l = methods.length; i < l; i++) {
context[methods[i]] = bind(context[methods[i]], context);
}
// Set up event handlers as required
if (deviceIsAndroid) {
layer.addEventListener('mouseover', this.onMouse, true);
layer.addEventListener('mousedown', this.onMouse, true);
layer.addEventListener('mouseup', this.onMouse, true);
}
layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};
layer.addEventListener = function(type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (type === 'click') {
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}
// If a handler is already declared in the element's onclick attribute, it will be fired before
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
// adding it as listener.
if (typeof layer.onclick === 'function') {
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
// - the old one won't work if passed to addEventListener directly.
oldOnClick = layer.onclick;
layer.addEventListener('click', function(event) {
oldOnClick(event);
}, false);
layer.onclick = null;
}
}
/**
* Windows Phone 8.1 fakes user agent string to look like Android and iPhone.
*
* @type boolean
*/
var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0;
/**
* Android requires exceptions.
*
* @type boolean
*/
var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone;
/**
* iOS requires exceptions.
*
* @type boolean
*/
var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone;
/**
* iOS 4 requires an exception for select elements.
*
* @type boolean
*/
var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
/**
* iOS 6.0-7.* requires the target element to be manually derived
*
* @type boolean
*/
var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent);
/**
* BlackBerry requires exceptions.
*
* @type boolean
*/
var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0;
/**
* Determine whether a given element requires a native click.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element needs a native click
*/
FastClick.prototype.needsClick = function(target) {
switch (target.nodeName.toLowerCase()) {
// Don't send a synthetic click to disabled inputs (issue #62)
case 'button':
case 'select':
case 'textarea':
if (target.disabled) {
return true;
}
break;
case 'input':
// File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
if ((deviceIsIOS && target.type === 'file') || target.disabled) {
return true;
}
break;
case 'label':
case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames
case 'video':
return true;
}
return (/\bneedsclick\b/).test(target.className);
};
/**
* Determine whether a given element requires a call to focus to simulate click into element.
*
* @param {EventTarget|Element} target Target DOM element
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
*/
FastClick.prototype.needsFocus = function(target) {
switch (target.nodeName.toLowerCase()) {
case 'textarea':
return true;
case 'select':
return !deviceIsAndroid;
case 'input':
switch (target.type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
return false;
}
// No point in attempting to focus disabled inputs
return !target.disabled && !target.readOnly;
default:
return (/\bneedsfocus\b/).test(target.className);
}
};
/**
* Send a click event to the specified element.
*
* @param {EventTarget|Element} targetElement
* @param {Event} event
*/
FastClick.prototype.sendClick = function(targetElement, event) {
var clickEvent, touch;
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[0];
// Synthesise a click event, with an extra attribute so it can be tracked
clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};
FastClick.prototype.determineEventType = function(targetElement) {
//Issue #159: Android Chrome Select Box does not open with a synthetic click event
if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
return 'mousedown';
}
return 'click';
};
/**
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.focus = function(targetElement) {
var length;
// Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724.
if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
length = targetElement.value.length;
targetElement.setSelectionRange(length, length);
} else {
targetElement.focus();
}
};
/**
* Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
*
* @param {EventTarget|Element} targetElement
*/
FastClick.prototype.updateScrollParent = function(targetElement) {
var scrollParent, parentElement;
scrollParent = targetElement.fastClickScrollParent;
// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
// target element was moved to another parent.
if (!scrollParent || !scrollParent.contains(targetElement)) {
parentElement = targetElement;
do {
if (parentElement.scrollHeight > parentElement.offsetHeight) {
scrollParent = parentElement;
targetElement.fastClickScrollParent = parentElement;
break;
}
parentElement = parentElement.parentElement;
} while (parentElement);
}
// Always update the scroll top tracker if possible.
if (scrollParent) {
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
}
};
/**
* @param {EventTarget} targetElement
* @returns {Element|EventTarget}
*/
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
if (eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}
return eventTarget;
};
/**
* On touch start, record the position and scroll offset.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchStart = function(event) {
var targetElement, touch, selection;
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
if (event.targetTouches.length > 1) {
return true;
}
targetElement = this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[0];
if (deviceIsIOS) {
// Only trusted events will deselect text on iOS (issue #49)
selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {
return true;
}
if (!deviceIsIOS4) {
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
// Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
// immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
// Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
// which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
// random integers, it's safe to to continue if the identifier is 0 here.
if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
this.lastTouchIdentifier = touch.identifier;
// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
// 1) the user does a fling scroll on the scrollable layer
// 2) the user stops the fling scroll with another tap
// then the event.target of the last 'touchend' event will be the element that was under the user's finger
// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
this.updateScrollParent(targetElement);
}
}
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
return true;
};
/**
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.touchHasMoved = function(event) {
var touch = event.changedTouches[0], boundary = this.touchBoundary;
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
return true;
}
return false;
};
/**
* Update the last position.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchMove = function(event) {
if (!this.trackingClick) {
return true;
}
// If the touch has moved, cancel the click tracking
if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
}
return true;
};
/**
* Attempt to find the labelled control for the given label element.
*
* @param {EventTarget|HTMLLabelElement} labelElement
* @returns {Element|null}
*/
FastClick.prototype.findControl = function(labelElement) {
// Fast path for newer browsers supporting the HTML5 control attribute
if (labelElement.control !== undefined) {
return labelElement.control;
}
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
if (labelElement.htmlFor) {
return document.getElementById(labelElement.htmlFor);
}
// If no for attribute exists, attempt to retrieve the first labellable descendant element
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
};
/**
* On touch end, determine whether to send a click event at once.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchEnd = function(event) {
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
if (!this.trackingClick) {
return true;
}
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
this.cancelNextClick = true;
return true;
}
if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
}
// Reset to prevent wrong click cancel on input (issue #156).
this.cancelNextClick = false;
this.lastClickTime = event.timeStamp;
trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.trackingClickStart = 0;
// On some iOS devices, the targetElement supplied with the event is invalid if the layer
// is performing a transition or scroll, and has to be re-detected manually. Note that
// for this to function correctly, it must be called *after* the event target is checked!
// See issue #57; also filed as rdar://13048589 .
if (deviceIsIOSWithBadTarget) {
touch = event.changedTouches[0];
// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
}
targetTagName = targetElement.tagName.toLowerCase();
if (targetTagName === 'label') {
forElement = this.findControl(targetElement);
if (forElement) {
this.focus(targetElement);
if (deviceIsAndroid) {
return false;
}
targetElement = forElement;
}
} else if (this.needsFocus(targetElement)) {
// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
this.targetElement = null;
return false;
}
this.focus(targetElement);
this.sendClick(targetElement, event);
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
// Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
if (!deviceIsIOS || targetTagName !== 'select') {
this.targetElement = null;
event.preventDefault();
}
return false;
}
if (deviceIsIOS && !deviceIsIOS4) {
// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
scrollParent = targetElement.fastClickScrollParent;
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
return true;
}
}
// Prevent the actual click from going though - unless the target node is marked as requiring
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
if (!this.needsClick(targetElement)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
return false;
};
/**
* On touch cancel, stop tracking the click.
*
* @returns {void}
*/
FastClick.prototype.onTouchCancel = function() {
this.trackingClick = false;
this.targetElement = null;
};
/**
* Determine mouse events which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onMouse = function(event) {
// If a target element was never set (because a touch event was never fired) allow the event
if (!this.targetElement) {
return true;
}
if (event.forwardedTouchEvent) {
return true;
}
// Programmatically generated events targeting a specific element should be permitted
if (!event.cancelable) {
return true;
}
// Derive and check the target element to see whether the mouse event needs to be permitted;
// unless explicitly enabled, prevent non-touch click events from triggering actions,
// to prevent ghost/doubleclicks.
if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
// Prevent any user-added listeners declared on FastClick element from being fired.
if (event.stopImmediatePropagation) {
event.stopImmediatePropagation();
} else {
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
event.propagationStopped = true;
}
// Cancel the event
event.stopPropagation();
event.preventDefault();
return false;
}
// If the mouse event is permitted, return true for the action to go through.
return true;
};
/**
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
* an actual click which should be permitted.
*
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onClick = function(event) {
var permitted;
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
if (this.trackingClick) {
this.targetElement = null;
this.trackingClick = false;
return true;
}
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
if (event.target.type === 'submit' && event.detail === 0) {
return true;
}
permitted = this.onMouse(event);
// Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
if (!permitted) {
this.targetElement = null;
}
// If clicks are permitted, return true for the action to go through.
return permitted;
};
/**
* Remove all FastClick's event listeners.
*
* @returns {void}
*/
FastClick.prototype.destroy = function() {
var layer = this.layer;
if (deviceIsAndroid) {
layer.removeEventListener('mouseover', this.onMouse, true);
layer.removeEventListener('mousedown', this.onMouse, true);
layer.removeEventListener('mouseup', this.onMouse, true);
}
layer.removeEventListener('click', this.onClick, true);
layer.removeEventListener('touchstart', this.onTouchStart, false);
layer.removeEventListener('touchmove', this.onTouchMove, false);
layer.removeEventListener('touchend', this.onTouchEnd, false);
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
};
/**
* Check whether FastClick is needed.
*
* @param {Element} layer The layer to listen on
*/
FastClick.notNeeded = function(layer) {
var metaViewport;
var chromeVersion;
var blackberryVersion;
var firefoxVersion;
// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
return true;
}
// Chrome version - zero for other browsers
chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
if (chromeVersion) {
if (deviceIsAndroid) {
metaViewport = document.querySelector('meta[name=viewport]');
if (metaViewport) {
// Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
return true;
}
// Chrome 32 and above with width=device-width or less don't need FastClick
if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
return true;
}
}
// Chrome desktop doesn't need FastClick (issue #15)
} else {
return true;
}
}
if (deviceIsBlackBerry10) {
blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
// BlackBerry 10.3+ does not require Fastclick library.
// https://github.com/ftlabs/fastclick/issues/251
if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
metaViewport = document.querySelector('meta[name=viewport]');
if (metaViewport) {
// user-scalable=no eliminates click delay.
if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
return true;
}
// width=device-width (or less than device-width) eliminates click delay.
if (document.documentElement.scrollWidth <= window.outerWidth) {
return true;
}
}
}
}
// IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97)
if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
return true;
}
// Firefox version - zero for other browsers
firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
if (firefoxVersion >= 27) {
// Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896
metaViewport = document.querySelector('meta[name=viewport]');
if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {
return true;
}
}
// IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version
// http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx
if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
return true;
}
return false;
};
/**
* Factory method for creating a FastClick object
*
* @param {Element} layer The layer to listen on
* @param {Object} [options={}] The options to override the defaults
*/
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};
if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
return FastClick;
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
window.FastClick = FastClick;
}
}());

View File

@@ -0,0 +1,249 @@
function validateEmail(email) {
var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(email);
}
function zero_pad(num) {
zero = 2 - num.toString().length + 1;
return Array(+(zero > 0 && zero)).join("0") + num;
}
function format_time(seconds) {
if (isNaN(seconds)) seconds = 0;
return zero_pad(parseInt(seconds/60)) + ":" + zero_pad(parseInt(seconds%60));
}
var url_to_links_rx = /(^|[\s\n]|>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi;
function urls_to_links(text) {
return text.replace(url_to_links_rx, "$1<a target='_blank' href='$2'>$2</a>");
}
// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript
function get_query_param(name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
// http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript
function random_string(len) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!-_";
for (var i=0; i < len; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
function fixup_touches(evt) {
// convert touch events
var e = evt;
if (evt.originalEvent) e = evt.originalEvent;
evt = {
pageX: evt.pageX,
pageY: evt.pageY,
offsetX: evt.offsetX,
offsetY: evt.offsetY,
clientX: evt.clientX,
clientY: evt.clientY,
layerX: evt.layerX,
layerY: evt.layerY,
target: evt.target,
currentTarget: evt.currentTarget
};
if (e.changedTouches && e.changedTouches.length) {
evt.pageX = e.changedTouches[0].pageX;
evt.pageY = e.changedTouches[0].pageY;
} else if (e.touches && e.touches.length) {
evt.pageX = e.touches[0].pageX;
evt.pageY = e.touches[0].pageY;
}
return evt;
}
function rgb_to_hex(r, g, b) {
return ((1 << 24) + (parseInt(r) << 16) + (parseInt(g) << 8) + parseInt(b)).toString(16).slice(1);
}
function hex_to_rgba(color) {
if (!color || color == "transparent") {
return {r:0,g:0,b:0,a:0};
}
if (color.match("rgb\\(")) {
color = color.replace("rgb(","").replace(")","").split(",");
return {
r: color[0],
g: color[1],
b: color[2],
a: 255
};
}
if (color.match("rgba\\(")) {
color = color.replace("rgba(","").replace(")","").split(",");
return {
r: color[0],
g: color[1],
b: color[2],
a: color[3]*255
};
}
var r = parseInt(color.substr(1,2), 16);
var g = parseInt(color.substr(3,2), 16);
var b = parseInt(color.substr(5,2), 16);
var a = 255;
if (color.length>7) {
a = parseInt(color.substr(7,2), 16);
}
return {r:r,g:g,b:b,a:a};
}
function rgb_to_hsv () {
var rr, gg, bb,
r = arguments[0] / 255,
g = arguments[1] / 255,
b = arguments[2] / 255,
h, s,
v = Math.max(r, g, b),
diff = v - Math.min(r, g, b),
diffc = function(c) {
return (v - c) / 6 / diff + 1 / 2;
};
if (diff == 0) {
h = s = 0;
} else {
s = diff / v;
rr = diffc(r);
gg = diffc(g);
bb = diffc(b);
if (r === v) {
h = bb - gg;
} else if (g === v) {
h = (1 / 3) + rr - bb;
} else if (b === v) {
h = (2 / 3) + gg - rr;
}
if (h < 0) {
h += 1;
} else if (h > 1) {
h -= 1;
}
}
return {
h: h || 0,
s: s || 0,
v: v || 0
};
}
// values?
function hsv_to_rgb(h, s, v) {
var r, g, b, i, f, p, q, t;
if (h && s === undefined && v === undefined) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
temp_grid_canvas = document.createElement("canvas");
function render_grid(w,h,divisions) {
temp_grid_canvas.width = w;
temp_grid_canvas.height = h;
var step = w / divisions;
var ctx = temp_grid_canvas.getContext('2d');
ctx.strokeStyle = "#f0f0f0";
ctx.lineWidth = 1;
var gc1 = "rgba(60,60,60,0.125)";
var gc2 = "rgba(60,60,60,0.075)";
for (var y=0; y<h; y+=step) {
if (y==0) {
ctx.fillStyle = gc1;
} else {
ctx.fillStyle = gc2;
}
ctx.fillRect(0,y,w,1);
}
for (var x=0; x<h; x+=step) {
if (x==0) {
ctx.fillStyle = gc1;
} else {
ctx.fillStyle = gc2;
}
ctx.fillRect(x,0,1,h);
}
var data_url = temp_grid_canvas.toDataURL()
return data_url;
}
function focus_contenteditable(el, end) {
range = document.createRange();
if (!range || !el) return;
var p = $(el).find("p");
if (!p.length) return;
// get last paragraph
p = p[p.length-1];
range.selectNodeContents(p);
selection = window.getSelection();
selection.removeAllRanges();
if (range.toString()!="Text") {
// move cursor to the end
range.collapse(false);
}
selection.addRange(range);
el.focus();
}
function setup_exclusive_audio_video_playback() {
document.addEventListener('play', function(e) {
var tags = ["audio","video"];
for (var i=0; i<tags.length; i++) {
var tag = tags[i];
var players = document.getElementsByTagName(tag);
for (var i = 0, len = players.length; i < len; i++) {
if (players[i] != e.target) {
players[i].pause();
}
}
}
}, true);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,133 @@
function parse_link(data) {
if (data == null) {
return "";
}
var html = data;
var recommendedWidth = 400;
var recommendedHeight = 300;
var extraViewClasses = "";
var sourceLink = null;
var plainText = false;
var genericEmbedMatcher = /embed\:(https?\:\/\/[^ <]+)/;
var genericUriMatcher = /(https?\:\/\/[^ <]+)/;
var type = "unknown";
var provider_name = "unknown";
isDataFileUrl = function(url) {
var file, suffix;
try {
if (url.split("/").length < 4) {
return false;
}
file = _.last(url.split("/"));
if (file.indexOf(".") < 0) {
return false;
}
suffix = _.last(file.split("."));
if (!suffix) {
return false;
}
if (_.include(["png", "jpg", "jpeg", "gif", "zip", "rar", "7z", "tar", "tgz", "gz", "xls", "xlsx", "doc", "docx", "ppt", "pptx", "mp3", "ogg", "oga", "ogv", "pdf", "dmg", "exe", "iso", "dxf", "ipa", "mov", "wmv", "wma", "wav", "aiff", "mp4", "m4a", "prg", "bin", "dat", "psd", "ai", "eps", "key"], suffix)) {
return true;
}
} catch (_error) {}
return false;
};
if (m = data.match(genericEmbedMatcher)) {
embedUri = m[1];
html = "<iframe width='100%' height='100%' src=\"" + embedUri + "\" seamless=\"1\" allowfullscreen=\"1\"></iframe>";
recommendedWidth = 640 / 2;
recommendedHeight = 390 / 2;
sourceLink = embedUri;
extraViewClasses = "external-embed";
} else if (data.match(/http/) && data.replace(/[^<]/g, "").length < 3) {
youtubeMatcher = /youtube\.com\/.*v=([^&<]+)/;
youtubeMatcher2 = /youtu\.be\/([^&<]+)/;
soundcloudMatcher = /soundcloud\.com\/([^<]+)/;
vimeoMatcher = /vimeo.com\/([^<]*)/;
dailyMotionMatcher = /dailymotion.com\/video\/([^<]*)/;
googleMapsMatcher = /google.com\/maps\?([^<]*)/;
spacedeckMatcher = new RegExp(location.host + "\/(spaces|folders)\/([0-9a-f]{24})");
if (m = data.match(youtubeMatcher) || (m = data.match(youtubeMatcher2))) {
videoId = m[1];
html = "<iframe src=\"https://www.youtube.com/embed/" + videoId + "?html5=1&rel=0&showinfo=0&autohide=1\" frameborder=\"0\" allowfullscreen=\"1\"></iframe>";
recommendedWidth = 640 / 2;
recommendedHeight = 390 / 2;
provider_name = "youtube";
type = "video";
} else if (m = data.match(dailyMotionMatcher)) {
videoId = m[1];
html = "<iframe src=\"https://www.dailymotion.com/embed/video/" + videoId + "\" frameborder=\"0\"></iframe>";
recommendedWidth = 536 / 2;
recommendedHeight = 302 / 2;
provider_name = "dailymotion";
type = "video";
} else if (m = data.match(vimeoMatcher)) {
videoId = m[1];
html = "<iframe src=\"https://player.vimeo.com/video/" + videoId + "\" frameborder=\"0\"></iframe>";
recommendedWidth = 536 / 2;
recommendedHeight = 302 / 2;
provider_name = "vimeo";
type = "video";
} else if (m = data.match(soundcloudMatcher)) {
var scurl = "https://" + m[0];
var url;
if (m[0].indexOf("soundcloud.com/player")>=0) {
url = "https://w." + m[0];
} else {
url = "https://w.soundcloud.com/player/?url="+encodeURI(scurl);
}
html = "<iframe scrolling=\"no\" frameborder=\"no\" src=\"" + url + "\"></iframe>";
recommendedWidth = 720 / 2;
recommendedHeight = 184;
sourceLink = scurl;
provider_name = "soundcloud";
type = "audio";
} else if ((m = data.match(googleMapsMatcher))) {
mapsParams = m[1];
html = "<iframe src=\"https://maps-api-ssl.google.com/maps?" + mapsParams + "\" seamless=\"1\" allowfullscreen=\"1\"></iframe>";
recommendedWidth = 640 / 2;
recommendedHeight = 390 / 2;
provider_name = "google";
type = "map";
} else if ((m = data.match(genericUriMatcher)) && !isDataFileUrl(m[1])) {
uri = m[1];
grabUri = uri;
endPoint = "/api/webgrabber/" + (encodeURIComponent(btoa(grabUri)));
html = data.replace(uri, " <img src=\"" + endPoint + "\" title=\"" + uri + "\"/> ");
recommendedWidth = 300;
recommendedHeight = 300;
sourceLink = uri;
} else {
plainText = true;
}
} else {
plainText = true;
}
if (plainText) {
// replace links with clickable links
return null;
}
result = {
html: html,
thumbnail_width: recommendedWidth,
thumbnail_height: recommendedHeight,
type: type,
provider_name: provider_name,
url: sourceLink
};
return result;
};

View File

@@ -0,0 +1,946 @@
window.locales = {};
window.locales.en = {};
window.locales.de = {};
window.locales.fr = {};
window.locales.en.translation =
{
"ok": "OK",
"cancel": "Cancel",
"close": "Close",
"open": "Open",
"folder": "Folder",
"save": "Save",
"saved": "Saved",
"created": "created",
"duplicate": "Duplicate",
"delete": "Delete",
"remove": "Remove",
"set": "set",
"reset": "reset",
"thanks": "Thanks",
"share": "Share",
"signup": "Sign Up",
"login": "Log in",
"logout": "Log out",
"email": "Email Address",
"password": "Password",
"width": "Width",
"height": "Height",
"nick": "Name",
"role": "Role",
"members": "Members",
"actions": "Actions",
"or": "or",
"you": "you",
"via": "via",
"by": "by",
"zero": "Zero",
"page": "Page",
"new": "New",
"copy": "Copy",
"home": "Home",
"owner": "Owner",
"space": "Space",
"second": "Second",
"not_found": "Not Found.",
"untitled_space": "Untitled Space",
"untitled_folder": "Untitled Folder",
"untitled": "untitled",
"sure": "Are you sure?",
"specify": "Please Specify",
"confirm": "Please Confirm",
"signup_google": "Sign In with Google",
"error_unknown_email": "This email/password combination is unknown. Try login with Google.",
"error_password_confirmation": "The entered passwords don't match.",
"error_domain_blocked": "Your domain is blocked.",
"error_user_email_already_used": "This email address is already in use.",
"support": "Spacedeck Support",
"offline": "Offline. Click for more.",
"error": "Sorry, but something went wrong. Please contact support@spacedeck.com",
"welcome": "Welcome",
"claim": "Your digital Whiteboard.",
"trynow": "Try now.",
"about": "About us",
"terms": "Terms",
"contact": "Contact",
"privacy": "Privacy",
"business_adress": "Business Adress",
"post_adress": "Post Adress",
"phone": "Phone",
"ceo": "Managing Director",
"name": "Name",
"confirm_subject": "Spacedeck Email Confirmation",
"confirm_body": "Thank you for signing up at Spacedeck.\nPlease click on the following link to confirm your email address.\n",
"confirm_action": "Confirm Now",
"team_invite_membership_subject": "Team Invitation for %s",
"team_invite_membership_body": "You have been invited to %s on Spacedeck. Please click on the following link to accept the invitation.",
"team_invite_user_body": "You have been invited to %s on Spacedeck.\nYour temporary password is \"%s\".\nPlease click on the following link to accept the invitation.",
"team_invite_admin_body": "%s was invited for your team: %s. The temporary password is \"%s\".",
"team_invite_membership_acction": "Accept",
"team_new_member_subject": "New Team Member for %s signed up",
"team_new_member_body": "%s just joined Team %s on Spacedeck.",
"space_invite_membership_subject": "%s invited you to a Space %s ",
"space_invite_membership_body": "You have been invited by %s to join a Space %s on Spacedeck. Please click on the following link to accept the invitation.",
"space_invite_membership_action": "Accept",
"folder_invite_membership_subject": "Space",
"folder_invite_membership_body": "You have been invited to a Team on Spacedeck. Please click on the following link to accept the invitation.",
"folder_invite_membership_acction": "Accept",
"login_google": "Login With Google",
"save_changes": "Save Changes",
"upgrade": "Upgrade",
"upgrade_now": "Upgrade Now",
"create_space": "Create Space",
"create_folder": "Create Folder",
"email_unconfirmed": "Email Unconfirmed",
"confirmation_sent": "Email Sent",
"folder_filter": "Filter",
"sort_by": "Sort by",
"last_modified": "Last Modified",
"last_opened": "Last Opened",
"title": "Title",
"edit_team": "Edit Team",
"edit_account": "Edit Account",
"log_out": "Log Out",
"no_spaces_yet": "Welcome! You can create Spaces and Folders here using the buttons in the top left corner.",
"new_folder_title": "New title for folder",
"folder_settings": "Folder Settings",
"upload_cover_image": "Upload Cover Image",
"spacedeck_pro_ad_folders": "With Spacedeck Pro, you can organize an unlimited amount of Spaces in Folders and manage access controls for each Folder. Would you like to learn more about Pro features?",
"spacedeck_pro_ad_versions": "With Spacedeck Pro, you can save unlimited versions of each Space to track your progress or keep snapshots safe. Would you like to learn more about Pro features?",
"spacedeck_pro_ad_pdf": "With Spacedeck Pro, you can export your Spaces as crisp PDFs for archiving, mailing around, or printing. Do you want to learn more about Pro features?",
"spacedeck_pro_ad_zip": "With Spacedeck Pro, you can export the contents of a Space as a ZIP package. Do you want to learn more about Pro features?",
"spacedeck_pro_ad_colors": "With Spacedeck Pro, you can mix your own colors using a professional color picker.",
"profile_caption": "Profile",
"upload_avatar": "Upload Avatar",
"uploading_avatar": "Uploading Avatar…",
"avatar_dimensions": "Recommended dimensions: 200×200 pixels.",
"profile_name": "Name",
"profile_email": "Email Address",
"send_again": "Send Again",
"confirmation_sent_long": "Email confirmation link sent. Please check your inbox.",
"confirmation_sent_another": "Another confirmation link sent.",
"confirmation_sent_dialog_text": "We sent you an email explaining how to confirm your email address.",
"payment_caption": "Payment",
"language_caption": "Language",
"notifications_caption": "Notifications",
"notifications_option_chat": "Inform me via email about new comments",
"notifications_option_spaces": "Send me a daily digest of what happened in my Spaces and Folders",
"password_caption": "Password",
"current_password": "Current Password",
"new_password": "New Password",
"verify_password": "Verify Password",
"change_password": "Change Password",
"reset_password": "Reset Password",
"terminate_caption": "Delete Account",
"terminate_warning": "If you delete your account, all Spaces, Folders and Messages including all content you and other people created in your Spaces will be destroyed.",
"terminate_warning2": "This cannot be undone.",
"terminate_reason": "Message",
"terminate_reason_caption": "Help us improve by sharing your reasons for cancelling.",
"terminate_terminate": "Terminate",
"space_blank1": "Welcome to a fresh new Space!",
"space_blank2": "Drop files, paste links",
"space_blank3": "or use the tools below",
"space_blank4": "to fill this Space with content.",
"draft": "Draft",
"publish": "Publish",
"published": "Published",
"save_version": "Save Version",
"version_saved": "Version Saved",
"post": "Post Message",
"chat_invite_cta1": "Collaboration is fun!",
"chat_invite_cta2": "Why not ",
"chat_invite_cta3": "invite some people",
"chat_invite_cta4": "to work with you?",
"chat_message_placeholder": "Write your message…",
"view": "View",
"edit": "Edit",
"present": "Present",
"chat": "Chat",
"meta": "Meta",
"tool_search": "Search",
"tool_upload": "Upload",
"tool_text": "Text",
"tool_shape": "Shape",
"tool_zones": "Zones",
"tool_canvas": "Canvas",
"search_media": "Search media…",
"type_here": "Type here",
"text_formats": "Formats",
"format_p": "Paragraph",
"format_bullets": "Bullet List",
"format_numbers": "Numbered List",
"format_h1": "Headline 1",
"format_h2": "Headline 2",
"format_h3": "Headline 3",
"font_size": "Font Size",
"line_height": "Line Height",
"tool_align": "Align",
"tool_styles": "Styles",
"tool_bullets": "Bullets",
"tool_numbers": "Numbers",
"color_fill": "Fill",
"color_stroke": "Stroke",
"color_text": "Text",
"tool_type": "Type",
"tool_box": "Box",
"tool_link": "Link",
"tool_layout": "Layout",
"tool_options": "Options",
"tool_stroke": "Stroke",
"tool_delete": "Delete",
"tool_lock": "Lock",
"tool_copy": "Copy",
"stack": "Stack",
"tool_circle": "Circle",
"tool_hexagon": "Hexagon",
"tool_square": "Square",
"tool_diamond": "Diamond",
"tool_bubble": "Bubble",
"tool_cloud": "Cloud",
"tool_burst": "Burst",
"tool_star": "Star",
"tool_heart": "Heart",
"tool_scribble": "Scribble",
"tool_line": "Line",
"tool_arrow": "Arrow",
"search_media_placeholder": "Search web media…",
"add_zone": "New Zone",
"palette": "Palette",
"picker": "Picker",
"background_image_caption": "Image",
"background_color_caption": "Color",
"upload_background_caption": "Click to upload a background image",
"upload_background": "Upload Background",
"access_caption": "Access",
"versions_caption": "Versions",
"info_caption": "Info",
"mode_private": "Private: Only members can view or edit",
"mode_public": "Public: Anyone with the link can view",
"invite_collaborators": "Invite Collaborators",
"revoke_access": "Revoke Access",
"invite": "Send Invitations",
"invitee_email_address": "Email address of new member",
"optional_message": "Optional message",
"role_viewer": "Viewer",
"role_editor": "Editor",
"role_admin": "Admin",
"new_space_title": "New title for Space",
"team": "Team",
"search": "Search",
"search_no_results": "search_no_results",
"search_clear": "search_clear",
"rename": "Rename",
"mobile": "mobile",
"image": "image",
"tool_filter": "filter",
"canel": "canel",
"invite_membership_action": "invite_membership_action",
"viewer": "viewer",
"editor": "editor",
"admin": "admin",
"logging_in": "logging in",
"password_confirmation": "Password Confirmation",
"confirm_again": "We sent you an email explaining how to confirm your email address.",
"confirmed": "Your Account was confirmed successfully. Thank you.",
"signing_up": "Signing up",
"password_check_inbox": "Please check your inbox",
"new_space": "New Space",
"tool_more": "More",
"what_is_your_name": "Welcome to %s! Please choose a username.",
"lang": "en",
"landing_title": "Your Whiteboard on the Web.",
"landing_claim": "Spacedeck lets you easily combine all kinds of media on virtual whiteboards: Text notes, photos, web links, even videos and audio recordings. ",
"landing_example": "People use Spacedeck to organize their ideas, in teams to see whole projects at a glance, or in schools and universities for richer, connected learning experiences.",
"spaces": "My Spaces",
"access_editor_link": "Instant Edit Link",
"access_editor_link_desc": "Give this link to anyone who should be able to instantly edit this Space, no account required: ",
"access_anonymous_edit_blocking": "Anonymous Editors may only change their own items",
"access_current_members": "Current Members",
"access_new_members": "Invite New Members",
"access_no_members": "Members of this Space will show up here.",
"comments": "comments",
"landing_customers": "Trusted by Thousands.",
"landing_features_title": "A Breeze To Use.",
"landing_features_text": "The new Spacedeck 5 has a streamlined, beautiful user interface that makes your work easier and more fun than ever before while giving you even more powerful features:",
"landing_features_1": "<b>Drag & drop</b> images, videos and audio from your computer or the web",
"landing_features_2": "<b>Write and format tex</b>t with full control over fonts, colors and style",
"landing_features_3": "<b>Draw, annotate and highlight</b> with included graphical shapes",
"landing_features_4": "Turn your board into a <b>zooming presentation</b>",
"landing_features_5": "<b>Collaborate and chat</b> in realtime with teammates, students or friends.",
"landing_features_6": "<b>Share Spaces</b> on the web or via email",
"landing_features_7": "<b>Export your work</b> as printable PDF or ZIP",
"landing_pricing": "Incredibly Affordable.",
"landing_pricing_lite": "Free/Personal Use",
"landing_pricing_lite_text": "The basic, well-rounded version for collecting pictures and keeping notes.",
"landing_pricing_pro_features_list": "<ul><li>Unlimited Spaces</li><li>Folder Structures</li><li>PDF and ZIP Export</li><li>No Watermarks</li><li>Custom Backgrounds</li><li>Activity History</li><li>20 GB Storage</li><ul>",
"landing_pricing_pro": "€4,90/User/Mo. <br><small>or 49,90/User/Year</small>",
"landing_pricing_pro_text": "Turbocharged with all the power you expect.",
"landing_pricing_pro_features": "Turbocharged with all the power you expect.",
"welcome_subject": "Welcome to Spacedeck",
"welcome_body": "Hello!\nThank you for signing up at Spacedeck.<br>We hope you will enjoy working in Spaces.<br>Remember, your account includes unlimited collaborators. Feel free to share your Spaces with friends and colleagues all over the world.",
"invite_emails": "Email addresses (Comma separated)",
"history_recently_updated": "Recently Updated",
"history_recently_empty": "Nothing has happened yet.",
"parent_folder": "parent_folder",
"created_by": "Created by",
"last_updated": "Last updated",
"feedback_sent": "Thanks for your feedback!",
"role_member": "Member",
"team_invite_membership_action": "Accept invitation",
"space_message_subject": "New Message in Space %s",
"space_message_body": "%s wrote in %s: \n",
"pro_ad_history_headline": "When you upgrade to Spacedeck Pro, you will see a history of recent updates across all your (shared) Spaces here.",
"password_reset_subject": "Reset Password for Spacedeck",
"password_reset_body": "You requested a reset of your Spacedeck password.\nPlease click on the following link to set a new password.",
"password_reset_action": "Reset Now",
"was_offline": "The connection to Spacedeck was interrupted. If you have unsaved work, please keep this browser tab open until the connection is re-established, then touch the unsaved objects again.",
"subscription_failed_user_subject": "Problem with your Spacedeck Payment",
"subscription_failed_user_body": "Unfortunately, we could not process your Payment-method. You can easly create a new payment method including PayPal in your account settings.",
"subscription_failed_team_subject": "Problem with your Spacedeck Payment",
"subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.",
"team_name": "Team Name",
"subdomain": "Subdomain",
"team_adresses": "Email adresses",
"add": "add",
"invited": "invited",
"duplicate_destination": "Into which folder do you want to duplicate this Space?",
"duplicate_confirm": "Duplicate %s into %s?",
"duplicate_success": "%s was duplicated into %s.",
"goto_space": "Go to Space %s",
"goto_folder": "Go to Folder %s",
"stay_here": "Stay here",
"sharing": "sharing",
"list": "Export List",
"link": "link",
"download_space": "Download Space",
"type": "Type",
"download": "download",
"Previous Zone": "Previous Zone",
"Next Zone": "Next Zone",
"promote": "promote",
"demote": "demote"
}
window.locales.de.translation =
{
"lang": "de",
"ok": "OK",
"cancel": "Abbrechen",
"close": "Schließen",
"open": "Öffnen",
"folder": "Ordner",
"duplicate": "Duplizieren",
"save": "Speichern",
"saved": "Gespeichert",
"created": "Erstellt",
"delete": "Löschen",
"remove": "Entfernen",
"set": "Übernehmen",
"reset": "Zurücksetzen",
"thanks": "Danke",
"share": "Teilen",
"signup": "Registrieren",
"login": "Anmelden",
"logout": "Abmelden",
"email": "E-Mail-Adresse",
"password": "Passwort",
"width": "Breite",
"height": "Höhe",
"nick": "Benutzername",
"role": "Rolle",
"members": "Mitglieder",
"actions": "Aktionen",
"or": "oder",
"you": "du",
"via": "via",
"by": "von",
"new": "Neu",
"zero": "Null",
"page": "Seite",
"copy": "Kopie",
"home": "Übersicht",
"owner": "Besitzer",
"space": "Space",
"second": "Sekunde",
"not_found": "Nicht Gefunden.",
"untitled_space": "Unbenannter Space",
"untitled_folder": "Unbenannter Order",
"untitled": "Unbenannter",
"sure": "Bist du sicher?",
"specify": "Bitte spezifiziere",
"confirm": "Bitte bestätige",
"signup_google": "Mit Google anmelden",
"error_unknown_email": "Unbekannte Kombination von Email und Passwort. Oder versuche dich mit Google anzumelden.",
"error_password_confirmation": "Die beiden Passwörter stimmen nicht überein.",
"error_domain_blocked": "Diese Domain ist gesperrt.",
"error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.",
"support": "Spacedeck-Support",
"offline": "Verbindungsverlust. Mehr Infos hier.",
"error": "Entschuldigung, etwas ist schiefgegangen. Bitte kontaktiere support@spacedeck.com",
"welcome": "Willkommen",
"claim": "Dein digitales Whiteboard.",
"trynow": "Jetzt probieren.",
"about": "Über uns",
"terms": "AGBs",
"contact": "Kontakt",
"privacy": "Privatsphäre",
"post_adress": "Postadresse",
"phone": "Phone",
"business_address": "business_address",
"ceo": "Geschäftsführer",
"business_adress": "business_adress",
"title": "Titel",
"name": "Name",
"confirm_subject": "E-Mail Bestätigung für Spacedeck",
"confirm_body": "Danke, dass du dich bei Spacedeck angemeldet hast.\nBitte klicke auf den folgenden Link, um deine E-Mail Adresse zu bestätigen.\n ",
"confirm_action": "E-Mail Bestätigen",
"team_invite_membership_subject": "Einladung zu %s auf Spacedeck",
"team_invite_membership_body": "Du wurdest zu %s auf Spacedeck eingeladen. \n Bitte klicke auf den folgenden Link, um die Einladung anzunehmen.",
"team_invite_user_body": "Du wurdest zu %s auf Spacedeck eingeladen. Dein temporäres Passwort ist \"%s\".\n Bitte klicke auf den folgenden Link, um die Einladung anzunehmen.",
"team_invite_admin_body": " %s wurde zu %s auf Spacedeck eingeladen. Das temporäres Passwort ist \"%s\".",
"team_invite_membership_action": "Annehmen",
"team_new_member_subject": "Neues Team Mitglied",
"team_new_member_body": "%s hat gerade seine Einladung zum Team %s angenommen.",
"space_invite_membership_subject": "Einladung von %s in Space %s",
"space_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.",
"space_invite_membership_action": "Annehmen",
"folder_invite_membership_subject": "Einladung von %s in Ordner %s",
"folder_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.",
"folder_invite_membership_action": "Accept",
"upgrade": "Upgrade",
"upgrade_now": "Jetzt Upgraden",
"create_space": "Space Erstellen",
"create_folder": "Ordner Erstellen",
"email_unconfirmed": "Email Unbestätigt",
"confirmation_sent": "Email Versandt",
"folder_filter": "Filter",
"sort_by": "Reihenfolge",
"last_modified": "Zuletzt Geändert",
"last_opened": "Zuletzt Geöffnet",
"edit_team": "Team Verwalten",
"edit_account": "Konto Bearbeiten",
"log_out": "Abmelden",
"no_spaces_yet": "Du hast noch keine Spaces erstellt.",
"new_folder_title": "Neuer Titel für Ordner",
"folder_settings": "Ordner-Einstellungen",
"upload_cover_image": "Ordnerbild Hochladen",
"spacedeck_pro_ad_folders": "Mit Spacedeck Pro kannst du beliebig viele Spaces in Ordnerstrukturen organisieren und für jeden Ordner Zugriffsrechte regeln. Möchtest du mehr über die Pro-Version erfahren?",
"spacedeck_pro_ad_versions": "Mit Spacedeck Pro kannst du beliebig viele Versionen deines Spaces festhalten und später die Entwicklungsgeschichte nachvollziehen. Möchtest du mehr über die Pro-Version erfahren?",
"spacedeck_pro_ad_pdf": "Mit Spacedeck Pro kannst du Spaces und sogar ganze Ordner als druckreife PDFs exportieren oder per Mail versenden. Möchtest du mehr über die Pro-Version erfahren?",
"spacedeck_pro_ad_zip": "Mit Spacedeck Pro kannst du die Inhalte deiner Spaces jederzeit als ZIP-Paket exportieren. Möchtest du mehr über die Pro-Version erfahren?",
"spacedeck_pro_ad_colors": "Spacedeck Pro enthält einen Profi-Farbmischer, mit dem du deine eigenen Farben mischen kannst.",
"profile_caption": "Profil",
"upload_avatar": "Profilbild Hochladen",
"uploading_avatar": "Profilbild wird hochgeladen…",
"avatar_dimensions": "Bestes Format: 200×200 Pixel.",
"profile_name": "Name",
"profile_email": "Email-Adresse",
"send_again": "Erneut Senden",
"confirmation_sent_long": "Email-Bestätigungslink versandt. Bitte überprüfe deine Mails.",
"confirmation_sent_another": "Wir haben eine weiteren Bestätigungslink versandt.",
"confirmation_sent_dialog_text": "Wir haben dir eine Email geschickt, die erklärt, wie das mit der Bestätigung läuft.",
"payment_caption": "Bezahlung",
"language_caption": "Sprache",
"notifications_caption": "Emails",
"notifications_option_chat": "Haltet mich über neue Kommentare auf dem Laufenden.",
"notifications_option_spaces": "Schickt mir täglich eine Zusammenfassung über Änderungen an meinen Spaces und Ordnern.",
"password_caption": "Passwort",
"current_password": "Altes Passwort",
"new_password": "Neues Passwort",
"verify_password": "Zur Sicherheit nochmal",
"change_password": "Passwort Ändern",
"reset_password": "Passwort Zurücksetzen",
"terminate_caption": "Kündigen",
"terminate_warning": "Wenn du kündigst, werden all deine Spaces, Ordner und Nachrichten und alle ihre Inhalte gelöscht.",
"terminate_warning2": "Das kann man nicht rückgängig machen.",
"terminate_reason": "Kündigungsgrund",
"terminate_reason_caption": "Wenn du uns mitteilst, was dich gestört hat, hilft uns das dabei ein besseres Produkt zu machen.",
"terminate_terminate": "Wirklich Kündigen",
"space_blank1": "Dies ist dein brandneuer, leerer Space!",
"space_blank2": "Wirf Dateien rein, paste Web-Links",
"space_blank3": "oder nutz die Werkzeuge da unten.",
"space_blank4": "Sei kreativ und tob dich aus!",
"draft": "Entwurf",
"publish": "Veröffentlichen",
"published": "Veröffentlicht",
"save_version": "Version Speichern",
"version_saved": "Version Gespeichert",
"post": "Abschicken",
"chat_invite_cta1": "Zusammen arbeiten macht Spaß!",
"chat_invite_cta2": "Warum ",
"chat_invite_cta3": "lädst du nicht ein paar Leute ein",
"chat_invite_cta4": "mit denen du dann zusammen arbeiten kannst?",
"chat_message_placeholder": "Schreib hier deine Nachricht…",
"view": "Ansicht",
"edit": "Bearb.",
"present": "Präse.",
"chat": "Chat",
"meta": "Teilen",
"tool_search": "Suche",
"tool_upload": "Upload",
"tool_text": "Text",
"tool_shape": "Grafik",
"tool_zones": "Zonen",
"tool_canvas": "Wand",
"search_media": "Medien im Web suchen…",
"type_here": "Hier was eingeben",
"text_formats": "Formate",
"format_p": "Absatz",
"format_bullets": "Bullet-Liste",
"format_numbers": "Nummerierte Liste",
"format_h1": "Überschrift 1",
"format_h2": "Überschrift 2",
"format_h3": "Überschrift 3",
"font_size": "Schriftgröße",
"line_height": "Zeilenhöhe",
"tool_align": "Bund",
"tool_styles": "Stil",
"tool_bullets": "Bullets",
"tool_numbers": "Zahlen",
"color_fill": "Füllung",
"color_stroke": "Strich",
"color_text": "Text",
"tool_type": "Typo",
"tool_box": "Box",
"tool_link": "Link",
"tool_layout": "Layout",
"tool_options": "Mehr",
"tool_stroke": "Strich",
"tool_delete": "Löschen",
"tool_lock": "Sperren",
"tool_copy": "Kopie",
"stack": "Anordnung",
"tool_circle": "Kreis",
"tool_hexagon": "Sechseck",
"tool_square": "Quadrat",
"tool_diamond": "Diamant",
"tool_bubble": "Blase",
"tool_cloud": "Wolke",
"tool_burst": "Burst",
"tool_star": "Stern",
"tool_heart": "Herz",
"tool_scribble": "Kritzeln",
"tool_line": "Linie",
"tool_arrow": "Pfeil",
"search_media_placeholder": "Online-Medien suchen…",
"add_zone": "Neue Zone",
"palette": "Palette",
"picker": "Mischen",
"background_image_caption": "Bild",
"background_color_caption": "Farbe",
"upload_background_caption": "Klicke hier, um ein Hintergrundbild hochzuladen.",
"upload_background": "Hintergrund Hochladen",
"access_caption": "Zugriff",
"versions_caption": "Versionen",
"info_caption": "Info",
"mode_private": "Privat: Nur Mitglieder können zugreifen",
"mode_public": "Öffentlich: Jede(r) mit Kenntnis des Links darf reinschauen",
"invite_collaborators": "Mitarbeiter Einladen",
"invitee_email_address": "Email-Adresse des neuen Mitglieds",
"optional_message": "Optionale Nachricht",
"revoke_access": "Zugriff Entfernen",
"invite": "Einladen",
"role_viewer": "Betrachter",
"role_editor": "Bearbeiter",
"role_admin": "Admin",
"new_space_title": "Neuer Titel für Space",
"logging_in": "logging_in",
"password_confirmation": "Passwort Wiederholung",
"confirm_again": "In deinem Postfach solltest du eine Bestätigungsmail finden. Bitte klicke auf den Link darin.",
"confirmed": "E-Mail Adresse wurde erfolgreich bestätigt. Danke!",
"password_check_inbox": "password_check_inbox",
"viewer": "Zuschauer",
"editor": "Bearbeiter",
"admin": "Admin",
"mobile": "Mobil",
"image": "Bild",
"tool_filter": "Filter",
"team": "Team",
"search": "Suche",
"search_no_results": "Keine Resultate",
"search_clear": "Zurücksetzen",
"rename": "Umbennen",
"login_google": "Mit Google anmelden",
"save_changes": "Änderungen speichern",
"what_is_your_name": "Willkommen bei %s ! Bitte wähle einen Benutzernamen.",
"landing_title": "Dein Online-Whiteboard.",
"landing_claim": "Mit Spacedeck kannst du multimedial auf virtuellen Whiteboards im Internet zusammenarbeiten: Kombiniere Texte, Fotos, Websites oder sogar Videos und Sounds. ",
"landing_example": "Spacedeck ist ideal, um Ideen zu visualisieren, in kreativen Teams Projekte zu überblicken oder um den Unterricht in Schulen und Universitäten interaktiv zu gestalten.",
"spaces": "Meine Spaces",
"access_editor_link": "Sofort-Mitmachen-Link",
"access_editor_link_desc": "Mit diesem Link kann man sogar ohne Spacedeck-Account sofort mitarbeiten. Praktisch!",
"access_anonymous_edit_blocking": "Anonyme Mitarbeiter dürfen keine Daten anderer anonymer Mitarbeiter ändern.",
"access_current_members": "Aktuelle Mitarbeiter",
"access_new_members": "Neue Mitarbeiter einladen",
"landing_customers": "Tausende Anwender weltweit vertrauen uns.",
"landing_features_title": "Schneller zum Ergebnis.",
"landing_features_text": "Spacedeck 5 hat eine brandneue Benutzeroberfläche, die das Arbeiten einfacher und intuitiver und macht - gleichzeitig aber auch mehr mächtige Werkzeuge bereitstellt.",
"landing_features_1": "<b>Drag & Drop:</b> Bilder-, Video- und Ton-Dateien direkt vom Desktop oder von anderen Webseiten in Spaces ziehen",
"landing_features_2": "<b>Textnotizen</b> mit allen Möglichkeiten bei Schriftart, Farbe und Stil",
"landing_features_3": "<b>Zeichne und Markiere</b> freihändig oder mit fertigen Formen",
"landing_features_4": "Verwandle dein Whiteboard in eine <b>zoombare Präsentation</b>",
"landing_features_5": "<b>Arbeite in Echtzeit</b> mit deinen Kollegen, Schülern oder Freunden zusammen",
"landing_features_6": "<b>Teile deine Whiteboards</b> per Link oder per E-Mail",
"landing_features_7": "<b>Exportiere deine Arbeit</b> als PDF- oder ZIP-Datei",
"landing_pricing": "Unfassbar günstig.",
"landing_pricing_lite": "Private Nutzung",
"landing_pricing_lite_text": "Basisvariante, ausreichend um multimedial zu arbeiten.",
"landing_pricing_pro_features_list": "<ul><li>Unbegrenzte Spaces</li><li>Hierarchische Ordnerstruktur</li><li>PDF und ZIP Export</li><li>Keine Wasserzeichen</li><li>Eigene Hintergründe</li><li>Liste von Aktivitäten</li><li>20 GB Speicherplatz</li><ul>",
"landing_pricing_pro": "€4,90/Anwender/Mo. <br><small>oder €49,90/Anwender/Jahr</small>",
"landing_pricing_pro_text": "Alle Features um professionell zu arbeiten.",
"welcome_subject": "Willkommen bei Spacedeck",
"welcome_body": "Danke, dass du dich bei Spacedeck angemeldet hast. <br> Wir hoffen, du wirst viel Spaß mit Spacedeck haben. <br> Vergiss nicht, dass du mit unbegrenzt vielen Kollegen und Freunden kostenlos zusammen arbeiten kannst. ",
"parent_folder": "Übergeordneter Ordner",
"access_no_members": "Noch keine Mitglieder",
"invite_emails": "E-Mail Adressen",
"created_by": "Erstellt von",
"last_updated": "Zuletzt aktualisiert",
"comments": "Kommentare",
"history_recently_updated": "Aktuelles",
"history_recently_empty": "Noch nichts passiert.",
"signing_up": "Registierung läuft",
"feedback_sent": "Danke für dein Feedback!",
"role_member": "Mitglied",
"space_message_subject": "Neue Nachricht im Space %s",
"space_message_body": "%s schrieb in %s: \n",
"password_reset_subject": "Neues Passwort für Spacedeck",
"password_reset_body": "Du möchtest das Passwort für deinen Spacedeck Account zurücksetzen?\nBitte klicke dafür auf den folgenden Link:",
"password_reset_action": "Jetzt Passwort neu setzen",
"pro_ad_history_headline": "Nach einem Upgrade zu Spacedeck Pro kannst du hier einen Überblick über alle aktuellen Aktivitäten in Spaces bekommen.",
"was_offline": "Die Verbindung wurde unterbrochen. Wir empfehlen, alle geänderten Objekte erneut zu selektieren, um sie zu speichern.",
"subscription_failed_user_subject": "Zahlung fehlgeschlagen",
"subscription_failed_user_body": "Unfortunately, we could not process your payment method. You can easly create a new payment method including PayPal in your account settings.",
"subscription_failed_team_subject": "Zahlung fehlgeschlagen",
"subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.",
"add": "hinzufügen",
"team_name": "Team-Name",
"subdomain": "Subdomain",
"invited": "eingeladen",
"team_adresses": "E-Mail Adressen",
"duplicate_destination": "In welchen Ordner möchtest du den Space duplizieren?",
"duplicate_confirm": "%s nach %s duplizieren?",
"duplicate_success": "%s wurde in %s dupliziert.",
"goto_space": "Gehe zu %s",
"goto_folder": "Gehe zu Ordner %s",
"stay_here": "Hier bleiben",
"sharing": "sharing",
"list": "Liste",
"download_space": "Space Herunterladen",
"duplicate_destination_folder": "Zielordner für Duplikat",
"type": "Typ",
"promote": "Befördern",
"demote": "Zurückstufen",
"Previous Zone": "Vorherige Zone",
"Next Zone": "Nächste Zone"
}
window.locales.fr.translation =
{
"lang": "fr",
"ok": "OK",
"cancel": "Annuler",
"close": "Fermer",
"open": "Ouvrir",
"folder": "Dossier",
"save": "Enregistrer",
"saved": "Enregistrée",
"created": "établi",
"duplicate": "Dupliquer",
"delete": "Supprimer",
"remove": "Enlever",
"set": "Définir",
"reset": "Rédéfinir",
"thanks": "Merci",
"share": "Partager",
"signup": "S'inscrire",
"login": "Se connecter",
"logout": "Se déconnecter",
"email": "Adresse email",
"password": "Mot de passe",
"width": "Largeur",
"height": "Hauteur",
"nick": "Nom",
"role": "Rôle",
"members": "Membres",
"actions": "Actions",
"or": "ou",
"you": "vous",
"via": "par",
"by": "par",
"zero": "Zero",
"page": "Page",
"new": "Neuf",
"copy": "Copier",
"owner": "Possesseur",
"home": "Accueil",
"space": "Espace",
"second": "Seconde",
"not_found": "Pas trouvé.",
"untitled": "sans titre",
"untitled_space": "Espace sans titre",
"untitled_folder": "Dossier sans titre",
"sure": "Êtes-vous sûr?",
"specify": "Veuillez préciser:",
"confirm": "Veuillez confirmer",
"signup_google": "S'inscrire avec Google",
"error_unknown_email": "Combinaison inconnue de l'email et mot de passe. Ou essayer de signer avec Google.",
"error_password_confirmation": "Les deux mots de passe ne correspondent pas.",
"error_domain_blocked": "Ce domaine a été désactivé.",
"error_user_email_already_used": "Cette adresse email est déjà enregistré.",
"support": "Aide Spacedeck",
"offline": "Désolé , mais les serveurs Spacedeck ne peuvent pas être atteint pour le moment. Plus d' informations ici.",
"error": "Désolé, une erreur s'est produite. Veuillez contacter support@spacedeck.com",
"welcome": "Bienvenue",
"claim": "Le tableau blanc partagé pour tout le monde",
"trynow": "Essayez-le gratuitement",
"about": "de nous",
"terms": "termes",
"contact": "contact",
"privacy": "sphère privée",
"business_adress": "Siège social",
"post_adress": "Adresse courrier",
"phone": "téléphone",
"ceo": "Gestionnaire",
"name": "name",
"confirm_subject": "Confirmation de l'email Spacedeck",
"confirm_body": "Merci pour votre inscription à Spacedeck.\nSil vous plaît cliquez sur le lien suivant pour confirmer votre adresse e-mail.",
"confirm_action": "Confirmer",
"team_invite_membership_subject": "Team Invitation for %s",
"team_invite_membership_body": "You have been invited to %s on Spacedeck.\nPlease click on the following link to accept the invitation.",
"team_invite_user_body": "You have been invited to %s on Spacedeck.\nYour temporary password is \"%s\".\nPlease click on the following link to accept the invitation.",
"team_invite_admin_body": "%s was invited for your team: %s. The temporary password is \"%s\".",
"team_invite_membership_acction": "Accept",
"team_new_member_subject": "New Team Member",
"team_new_member_body": "%s just joined Team %s on Spacedeck.",
"invite_emails": "Entrer les adresses email (séparées pas des virgules)",
"optional_message": "Message personnel (facultatif)",
"space_invite_membership_subject": "Invitation Espace par %s: %s",
"space_invite_membership_body": "Vous avez été invité par %s à Espace \"%s\"",
"space_invite_membership_action": "Accepter L'invitation",
"folder_invite_membership_subject": "Space",
"folder_invite_membership_body": "You have been invited to a Team on Spacedeck. Please click on the following link to accept the invitation.",
"folder_invite_membership_acction": "Accept",
"login_google": "S'inscrire avec Google",
"save_changes": "Enregistrer",
"upgrade": "Upgrade",
"upgrade_now": "Mise à niveau",
"create_space": "Créer un espace",
"create_folder": "Créer un dossier",
"email_unconfirmed": "Email non confirmée",
"confirmation_sent": "L'email est envoyé.",
"folder_filter": "Filtre",
"sort_by": "Ordre",
"last_modified": "Dernière modification",
"last_opened": "Dernière ouverture",
"title": "Titre",
"edit_team": "Modifier l'équipe",
"edit_account": "Modifier le compte",
"log_out": "Déconnecter",
"no_spaces_yet": "Vous ne avez pas encore créé d'espaces.",
"new_folder_title": "Nouveau titre pour le dossier",
"folder_settings": "Paramètres du dossier",
"upload_cover_image": "Charger image de couverture",
"spacedeck_pro_ad_folders": "Avec Spacedeck Pro, vous pouvez organiser un nombre illimité de espaces dans les dossiers et gérer les contrôles d'accès pour chaque dossier. Voulez-vous en savoir plus sur les fonctionnalités Pro?",
"spacedeck_pro_ad_versions": "Avec Spacedeck Pro, vous pouvez enregistrer des versions illimitées de chaque espace pour suivre vos progrès ou de conserver des instantanés sécurité. Voulez-vous en savoir plus sur les fonctionnalités Pro?",
"spacedeck_pro_ad_pdf": "Avec Spacedeck Pro, vous pouvez exporter vos espaces et même des dossiers entiers belles PDF pour l'archivage, de diffusion, ou autour de l'impression. Voulez-vous en savoir plus sur les fonctionnalités Pro?",
"spacedeck_pro_ad_zip": "Avec Spacedeck Pro, vous pouvez exporter le contenu d'un espace comme un paquet ZIP. Voulez-vous en savoir plus sur les fonctionnalités Pro?",
"spacedeck_pro_ad_colors": "Avec Spacedeck Pro, vous pouvez mélanger vos propres couleurs en utilisant un sélecteur de couleur professionnelle.",
"profile_caption": "Profil",
"upload_avatar": "Télécharger l'image profil",
"uploading_avatar": "L'image de profil est téléchargée…",
"avatar_dimensions": "Format suggéré: 200×200 pixels.",
"profile_name": "Name",
"profile_email": "Email",
"send_again": "Renvoyer",
"confirmation_sent_long": "Lien de confirmation email envoyé. Se il vous plaît vérifier votre courrier.",
"confirmation_sent_another": "Nous avons envoyé un autre lien de confirmation.",
"confirmation_sent_dialog_text": "Nous vous avons envoyé un email expliquant comment confirmer votre adresse email.",
"payment_caption": "Paiement",
"language_caption": "Langue",
"notifications_caption": "Emails",
"notifications_option_chat": "Envoyez-moi les nouveaux commentaires par email.",
"notifications_option_spaces": "Envoyez-moi un résumé quotidien des modifications à mes espaces.",
"password_caption": "Mot de passe",
"current_password": "Ancien mot de passe",
"new_password": "Nouveau mot de passe",
"verify_password": "Répéter mot de passe",
"change_password": "Enregistrer",
"reset_password": "Mot de passe oublié?",
"terminate_caption": "Supprimer le compte",
"terminate_warning": "En supprimant votre compte, vos messages, espaces, dossiers et tout leur contenu seront effacés. Cette action ne peut être annulée.",
"terminate_warning2": "Cela ne peut pas être annulée.",
"terminate_reason": "Problèmes rencontrés",
"terminate_reason_caption": "Aidez-nous à améliorer le produit en précisant les raisons de la suppression de votre compte.",
"terminate_terminate": "Supprimer le compte définitivement?",
"space_blank1": "Ceci est votre nouvel espace.",
"space_blank2": "Déposez des fichiers, collez des liens web",
"space_blank3": "ou utilisez les outils.",
"space_blank4": "Soyez créatifs!",
"draft": "Conception",
"publish": "Publier",
"published": "Publié",
"save_version": "Enregistrer une version",
"version_saved": "Version enregistrée.",
"post": "Envoyer",
"chat_invite_cta1": "Travailler ensemble est amusant!",
"chat_invite_cta2": "Pourquoi ",
"chat_invite_cta3": "ne pas vous invitez quelques collaborateurs?",
"chat_invite_cta4": "",
"chat_message_placeholder": "Votre message ici…",
"view": "Vue",
"edit": "Éditer",
"present": "Prés.",
"chat": "Chat",
"meta": "Meta",
"tool_search": "Chercher",
"tool_upload": "Charger",
"tool_text": "Texte",
"tool_shape": "Dessin",
"tool_zones": "Zones",
"tool_canvas": "Mur",
"search_media": "Chercher le web pour les médias…",
"type_here": "Entrez quelque chose ici",
"text_formats": "Formats",
"format_p": "Paragraphe",
"format_bullets": "Liste à puces",
"format_numbers": "Liste numérotée",
"format_h1": "Titre 1",
"format_h2": "Titre 2",
"format_h3": "Titre 3",
"font_size": "Taille de la police",
"line_height": "Hauteur de ligne",
"tool_align": "Align",
"tool_styles": "Style",
"tool_bullets": "Puces",
"tool_numbers": "Numéros",
"color_fill": "Fond",
"color_stroke": "Ligne",
"color_text": "Text",
"tool_type": "Typo.",
"tool_box": "Box",
"tool_link": "Lien",
"tool_layout": "Layout",
"tool_options": "Plus",
"tool_stroke": "Ligne",
"tool_delete": "Effacer",
"tool_lock": "Bloquer",
"tool_copy": "Copie",
"stack": "Empiler",
"tool_circle": "Cercle",
"tool_hexagon": "Hexagone",
"tool_square": "Carré",
"tool_diamond": "Diamant",
"tool_bubble": "Bulle",
"tool_cloud": "Nuage",
"tool_burst": "Éclat",
"tool_star": "Étoile",
"tool_heart": "Cœur",
"tool_scribble": "Crayon",
"tool_line": "Ligne",
"tool_arrow": "Flèche",
"search_media_placeholder": "Chercher le web pour les médias…",
"add_zone": "Ajouter Zone",
"palette": "Palette",
"picker": "Mélange",
"background_image_caption": "Image",
"background_color_caption": "Couleur",
"upload_background_caption": "Cliquez ici pour télécharger une image de fond.",
"upload_background": "Télécharger",
"access_caption": "Accès",
"versions_caption": "Versions",
"info_caption": "Info",
"mode_private": "Privé",
"mode_public": "Public",
"invite_collaborators": "Inviter les collaborateurs",
"revoke_access": "Révoquer l'accès",
"invite": "Inviter",
"role_viewer": "Spectateur",
"role_editor": "Éditeur",
"role_admin": "Administrateur",
"new_space_title": "Nouveau titre pour l'espace",
"invitee_email_address": "Adresse e-mail de invitee",
"viewer": "Spectateur",
"editor": "Éditeur",
"admin": "Administrateur",
"mobile": "Mobile",
"image": "Image",
"tool_filter": "Filter",
"team": "Team",
"search": "Recherche",
"search_no_results": "Aucun résultat trouvé",
"search_clear": "Supprimer",
"rename": "Renommer",
"logging_in": "Connexion",
"password_confirmation": "Confirmation du mot de passe",
"confirm_again": "Veuillez consulter votre boîte pour confirmer votre email.",
"confirmed": "Adresse email confirmée avec succès. merci!",
"password_check_inbox": "password_check_inbox",
"what_is_your_name": "Bonjour! Choisir un nom d'utilisateur s'il vous plaît.",
"landing_title": "Le tableau blanc partagé pour tout le monde.",
"landing_claim": "Le tableau blanc partagé pour tout le monde.",
"landing_example": "Que vous soyez étudiant, enseignant ou chercheur: Avec Spacedeck il est facile pour vous de créer, de gérer et de partager des cours ou le travail en classe. Développez vos théories visuellement. Organisez des notes de recherche, web, images, audio et vidéo.",
"spaces": "Espaces",
"access_editor_link": "Lien instantané.",
"access_editor_link_desc": "Donnez ce lien à tous ceux que vous voulez inviter rapidement. Ils nont pas besoin créer un compte Spacedeck.",
"access_anonymous_edit_blocking": "Ces invités ne peuvent modifier que les éléments quils ont eux-même créé.",
"access_current_members": "Membres actuels",
"comments": "Commentaires",
"access_no_members": "No members yet. You can invite some below.",
"access_new_members": "Inviter de nouveaux membres.",
"landing_customers": "Approuvé par des milliers.",
"landing_features_title": "Un jeu d'enfant.",
"landing_features_text": "Le tout nouveau Spacedeck 5 vous permet de travailler bien plus facilement grâce à sa magnifique interface simplifiée.",
"landing_features_1": "Glissez & déposez images, vidéos et audios de votre ordinateur ou du web",
"landing_features_2": "Ecrivez directement sur l'espace et choisissez les polices de caractère, couleurs et styles",
"landing_features_3": "Dessinez, annotez et surlignez grâce aux formes graphiques intégrées",
"landing_features_4": "Transformez votre espace en une présentation dynamique",
"landing_features_5": "Collaborez et discutez en temps réel avec vos collègues, élèves et amis",
"landing_features_6": "Partagez vos espaces sur le web ou par email",
"landing_features_7": "Exportez votre espace en PDF pour l'imprimer",
"landing_pricing": "Incroyablement abordable.",
"landing_pricing_lite": "Usage personnel",
"landing_pricing_lite_text": "La version de base, bien arrondi pour recueillir des images et de garder des notes.",
"landing_pricing_pro_features_list": "<ul><li>Unlimited Spaces</li><li>Exporter PDF, ZIP</li><li>No Watermarks</li><li>Image de fonds</li><li>Activity History</li><li>20 Go de stockage</li><ul>",
"landing_pricing_pro": "€4,90/User/Mo. <br><small> €49,90/User/Year</small>",
"landing_pricing_pro_text": "Avec toute la puissance que vous attendez.",
"landing_pricing_pro_features": "Avec toute la puissance que vous attendez.",
"welcome_subject": "Bienvenue sur Spacedeck",
"welcome_body": "Merci pour votre inscription à Spacedeck.\nNous espérons que vous aurez plaisir à travailler dans les Espaces. <br> Rappelez-vous que votre compte comprend un nombre illimité de collaborateurs. <br> N''hésitez pas à partager vos espaces avec des amis et collègues du monde entier.",
"parent_folder": "Dossier origine",
"created_by": "Créé par",
"last_updated": "Mis à jour",
"history_recently_updated": "Nouvelles",
"history_recently_empty": "Rien ne se passe",
"signing_up": "Signing Up",
"feedback_sent": "Merci pour votre commentaire!",
"space_message_subject": "A posté sur %s",
"space_message_body": "%s a commenté dans %s:\n",
"role_member": "role_member",
"password_reset_subject": "Réinitialiser le Mot de passe pour Spacedeck",
"password_reset_body": "Salut!<br><br>Vous avez demandé la réinitialisation de votre Mot de passe.<br>Veuillez cliquer sur le lien suivant pour définir un nouveau Mot de passe.<br>",
"password_reset_action": "Définir un nouveau Mot de passe",
"was_offline": "The connection to Spacedeck was interrupted. If you have unsaved work, please keep this browser tab open until the connection is re-established, then touch the unsaved objects again.",
"subscription_failed_user_subject": "Problem with your Spacedeck Payment",
"subscription_failed_user_body": "Unfortunately, we could not process your Payment-method. You can easly create a new payment method including PayPal in your account settings.",
"subscription_failed_team_subject": "Problem with your Spacedeck Payment",
"subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.",
"pro_ad_history_headline": "Après une mise à niveau vous pouvez obtenir un aperçu de toutes les activités actuelles dans les espaces ici.",
"add": "ajouter",
"team_name": "Nom de l'équipe",
"subdomain": "sous-domaine",
"invited": "invité",
"team_adresses": "E-mail adresse",
"duplicate_destination": "Sélectionnez le dossier de destination",
"duplicate_confirm": "Dupliquer %s dans %s?",
"duplicate_success": "%s a été dupliqué dans %s.",
"goto_space": "Aller à l'espace %s",
"goto_folder": "Aller au dossier %s",
"stay_here": "Reste ici",
"download_space": "télécharger un espace",
"type": "Type",
"Previous Zone": "Zone précédent",
"Next Zone": "Zone suivante",
"list": "liste",
"promote": "promouvoir",
"demote": "rétrograder"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2610
public/javascripts/moment.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,951 @@
/*global define:false */
/**
* Copyright 2013 Craig Campbell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Mousetrap is a simple keyboard shortcut library for Javascript with
* no external dependencies
*
* @version 1.4.6
* @url craig.is/killing/mice
*/
(function(window, document, undefined) {
/**
* mapping of special keycodes to their corresponding keys
*
* everything in this dictionary cannot use keypress events
* so it has to be here to map to the correct keycodes for
* keyup/keydown events
*
* @type {Object}
*/
var _MAP = {
8: 'backspace',
9: 'tab',
13: 'enter',
16: 'shift',
17: 'ctrl',
18: 'alt',
20: 'capslock',
27: 'esc',
32: 'space',
33: 'pageup',
34: 'pagedown',
35: 'end',
36: 'home',
37: 'left',
38: 'up',
39: 'right',
40: 'down',
45: 'ins',
46: 'del',
91: 'meta',
93: 'meta',
224: 'meta'
},
/**
* mapping for special characters so they can support
*
* this dictionary is only used incase you want to bind a
* keyup or keydown event to one of these keys
*
* @type {Object}
*/
_KEYCODE_MAP = {
106: '*',
107: '+',
109: '-',
110: '.',
111 : '/',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
192: '`',
219: '[',
220: '\\',
221: ']',
222: '\''
},
/**
* this is a mapping of keys that require shift on a US keypad
* back to the non shift equivelents
*
* this is so you can use keyup events with these keys
*
* note that this will only work reliably on US keyboards
*
* @type {Object}
*/
_SHIFT_MAP = {
'~': '`',
'!': '1',
'@': '2',
'#': '3',
'$': '4',
'%': '5',
'^': '6',
'&': '7',
'*': '8',
'(': '9',
')': '0',
'_': '-',
'+': '=',
':': ';',
'\"': '\'',
'<': ',',
'>': '.',
'?': '/',
'|': '\\'
},
/**
* this is a list of special strings you can use to map
* to modifier keys when you specify your keyboard shortcuts
*
* @type {Object}
*/
_SPECIAL_ALIASES = {
'option': 'alt',
'command': 'meta',
'return': 'enter',
'escape': 'esc',
'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
},
/**
* variable to store the flipped version of _MAP from above
* needed to check if we should use keypress or not when no action
* is specified
*
* @type {Object|undefined}
*/
_REVERSE_MAP,
/**
* a list of all the callbacks setup via Mousetrap.bind()
*
* @type {Object}
*/
_callbacks = {},
/**
* direct map of string combinations to callbacks used for trigger()
*
* @type {Object}
*/
_directMap = {},
/**
* keeps track of what level each sequence is at since multiple
* sequences can start out with the same sequence
*
* @type {Object}
*/
_sequenceLevels = {},
/**
* variable to store the setTimeout call
*
* @type {null|number}
*/
_resetTimer,
/**
* temporary state where we will ignore the next keyup
*
* @type {boolean|string}
*/
_ignoreNextKeyup = false,
/**
* temporary state where we will ignore the next keypress
*
* @type {boolean}
*/
_ignoreNextKeypress = false,
/**
* are we currently inside of a sequence?
* type of action ("keyup" or "keydown" or "keypress") or false
*
* @type {boolean|string}
*/
_nextExpectedAction = false;
/**
* loop through the f keys, f1 to f19 and add them to the map
* programatically
*/
for (var i = 1; i < 20; ++i) {
_MAP[111 + i] = 'f' + i;
}
/**
* loop through to map numbers on the numeric keypad
*/
for (i = 0; i <= 9; ++i) {
_MAP[i + 96] = i;
}
/**
* cross browser add event method
*
* @param {Element|HTMLDocument} object
* @param {string} type
* @param {Function} callback
* @returns void
*/
function _addEvent(object, type, callback) {
if (object.addEventListener) {
object.addEventListener(type, callback, false);
return;
}
object.attachEvent('on' + type, callback);
}
/**
* takes the event and returns the key character
*
* @param {Event} e
* @return {string}
*/
function _characterFromEvent(e) {
// for keypress events we should return the character as is
if (e.type == 'keypress') {
var character = String.fromCharCode(e.which);
// if the shift key is not pressed then it is safe to assume
// that we want the character to be lowercase. this means if
// you accidentally have caps lock on then your key bindings
// will continue to work
//
// the only side effect that might not be desired is if you
// bind something like 'A' cause you want to trigger an
// event when capital A is pressed caps lock will no longer
// trigger the event. shift+a will though.
if (!e.shiftKey) {
character = character.toLowerCase();
}
return character;
}
// for non keypress events the special maps are needed
if (_MAP[e.which]) {
return _MAP[e.which];
}
if (_KEYCODE_MAP[e.which]) {
return _KEYCODE_MAP[e.which];
}
// if it is not in the special map
// with keydown and keyup events the character seems to always
// come in as an uppercase character whether you are pressing shift
// or not. we should make sure it is always lowercase for comparisons
return String.fromCharCode(e.which).toLowerCase();
}
/**
* checks if two arrays are equal
*
* @param {Array} modifiers1
* @param {Array} modifiers2
* @returns {boolean}
*/
function _modifiersMatch(modifiers1, modifiers2) {
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
/**
* resets all sequence counters except for the ones passed in
*
* @param {Object} doNotReset
* @returns void
*/
function _resetSequences(doNotReset) {
doNotReset = doNotReset || {};
var activeSequences = false,
key;
for (key in _sequenceLevels) {
if (doNotReset[key]) {
activeSequences = true;
continue;
}
_sequenceLevels[key] = 0;
}
if (!activeSequences) {
_nextExpectedAction = false;
}
}
/**
* finds all callbacks that match based on the keycode, modifiers,
* and action
*
* @param {string} character
* @param {Array} modifiers
* @param {Event|Object} e
* @param {string=} sequenceName - name of the sequence we are looking for
* @param {string=} combination
* @param {number=} level
* @returns {Array}
*/
function _getMatches(character, modifiers, e, sequenceName, combination, level) {
var i,
callback,
matches = [],
action = e.type;
// if there are no events related to this keycode
if (!_callbacks[character]) {
return [];
}
// if a modifier key is coming up on its own we should allow it
if (action == 'keyup' && _isModifier(character)) {
modifiers = [character];
}
// loop through all callbacks for the key that was pressed
// and see if any of them match
for (i = 0; i < _callbacks[character].length; ++i) {
callback = _callbacks[character][i];
// if a sequence name is not specified, but this is a sequence at
// the wrong level then move onto the next match
if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
continue;
}
// if the action we are looking for doesn't match the action we got
// then we should keep going
if (action != callback.action) {
continue;
}
// if this is a keypress event and the meta key and control key
// are not pressed that means that we need to only look at the
// character, otherwise check the modifiers as well
//
// chrome will not fire a keypress if meta or control is down
// safari will fire a keypress if meta or meta+shift is down
// firefox will fire a keypress if meta or control is down
if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
// when you bind a combination or sequence a second time it
// should overwrite the first one. if a sequenceName or
// combination is specified in this call it does just that
//
// @todo make deleting its own method?
var deleteCombo = !sequenceName && callback.combo == combination;
var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
if (deleteCombo || deleteSequence) {
_callbacks[character].splice(i, 1);
}
matches.push(callback);
}
}
return matches;
}
/**
* takes a key event and figures out what the modifiers are
*
* @param {Event} e
* @returns {Array}
*/
function _eventModifiers(e) {
var modifiers = [];
if (e.shiftKey) {
modifiers.push('shift');
}
if (e.altKey) {
modifiers.push('alt');
}
if (e.ctrlKey) {
modifiers.push('ctrl');
}
if (e.metaKey) {
modifiers.push('meta');
}
return modifiers;
}
/**
* prevents default for this event
*
* @param {Event} e
* @returns void
*/
function _preventDefault(e) {
if (e.preventDefault) {
e.preventDefault();
return;
}
e.returnValue = false;
}
/**
* stops propogation for this event
*
* @param {Event} e
* @returns void
*/
function _stopPropagation(e) {
if (e.stopPropagation) {
e.stopPropagation();
return;
}
e.cancelBubble = true;
}
/**
* actually calls the callback function
*
* if your callback function returns false this will use the jquery
* convention - prevent default and stop propogation on the event
*
* @param {Function} callback
* @param {Event} e
* @returns void
*/
function _fireCallback(callback, e, combo, sequence) {
// if this event should not happen stop here
if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
return;
}
if (callback(e, combo) === false) {
_preventDefault(e);
_stopPropagation(e);
}
}
/**
* handles a character key event
*
* @param {string} character
* @param {Array} modifiers
* @param {Event} e
* @returns void
*/
function _handleKey(character, modifiers, e) {
var callbacks = _getMatches(character, modifiers, e),
i,
doNotReset = {},
maxLevel = 0,
processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) {
if (callbacks[i].seq) {
maxLevel = Math.max(maxLevel, callbacks[i].level);
}
}
// loop through matching callbacks for this key event
for (i = 0; i < callbacks.length; ++i) {
// fire for all sequence callbacks
// this is because if for example you have multiple sequences
// bound such as "g i" and "g t" they both need to fire the
// callback for matching g cause otherwise you can only ever
// match the first one
if (callbacks[i].seq) {
// only fire callbacks for the maxLevel to prevent
// subsequences from also firing
//
// for example 'a option b' should not cause 'option b' to fire
// even though 'option b' is part of the other sequence
//
// any sequences that do not match here will be discarded
// below by the _resetSequences call
if (callbacks[i].level != maxLevel) {
continue;
}
processedSequenceCallback = true;
// keep a list of which sequences were matches for later
doNotReset[callbacks[i].seq] = 1;
_fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
continue;
}
// if there were no sequence matches but we are still here
// that means this is a regular match so we should fire that
if (!processedSequenceCallback) {
_fireCallback(callbacks[i].callback, e, callbacks[i].combo);
}
}
// if the key you pressed matches the type of sequence without
// being a modifier (ie "keyup" or "keypress") then we should
// reset all sequences that were not matched by this event
//
// this is so, for example, if you have the sequence "h a t" and you
// type "h e a r t" it does not match. in this case the "e" will
// cause the sequence to reset
//
// modifier keys are ignored because you can have a sequence
// that contains modifiers such as "enter ctrl+space" and in most
// cases the modifier key will be pressed before the next key
//
// also if you have a sequence such as "ctrl+b a" then pressing the
// "b" key will trigger a "keypress" and a "keydown"
//
// the "keydown" is expected when there is a modifier, but the
// "keypress" ends up matching the _nextExpectedAction since it occurs
// after and that causes the sequence to reset
//
// we ignore keypresses in a sequence that directly follow a keydown
// for the same character
var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
_resetSequences(doNotReset);
}
_ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
}
/**
* handles a keydown event
*
* @param {Event} e
* @returns void
*/
function _handleKeyEvent(e) {
// normalize e.which for key events
// @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
if (typeof e.which !== 'number') {
e.which = e.keyCode;
}
var character = _characterFromEvent(e);
// no character found then stop
if (!character) {
return;
}
// need to use === for the character check because the character can be 0
if (e.type == 'keyup' && _ignoreNextKeyup === character) {
_ignoreNextKeyup = false;
return;
}
Mousetrap.handleKey(character, _eventModifiers(e), e);
}
/**
* determines if the keycode specified is a modifier key or not
*
* @param {string} key
* @returns {boolean}
*/
function _isModifier(key) {
return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
}
/**
* called to set a 1 second timeout on the specified sequence
*
* this is so after each key press in the sequence you have 1 second
* to press the next key before you have to start over
*
* @returns void
*/
function _resetSequenceTimer() {
clearTimeout(_resetTimer);
_resetTimer = setTimeout(_resetSequences, 1000);
}
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
*
* @return {Object}
*/
function _getReverseMap() {
if (!_REVERSE_MAP) {
_REVERSE_MAP = {};
for (var key in _MAP) {
// pull out the numeric keypad from here cause keypress should
// be able to detect the keys from the character
if (key > 95 && key < 112) {
continue;
}
if (_MAP.hasOwnProperty(key)) {
_REVERSE_MAP[_MAP[key]] = key;
}
}
}
return _REVERSE_MAP;
}
/**
* picks the best action based on the key combination
*
* @param {string} key - character for key
* @param {Array} modifiers
* @param {string=} action passed in
*/
function _pickBestAction(key, modifiers, action) {
// if no action was picked in we should try to pick the one
// that we think would work best for this key
if (!action) {
action = _getReverseMap()[key] ? 'keydown' : 'keypress';
}
// modifier keys don't work as expected with keypress,
// switch to keydown
if (action == 'keypress' && modifiers.length) {
action = 'keydown';
}
return action;
}
/**
* binds a key sequence to an event
*
* @param {string} combo - combo specified in bind call
* @param {Array} keys
* @param {Function} callback
* @param {string=} action
* @returns void
*/
function _bindSequence(combo, keys, callback, action) {
// start off by adding a sequence level record for this combination
// and setting the level to 0
_sequenceLevels[combo] = 0;
/**
* callback to increase the sequence level for this sequence and reset
* all other sequences that were active
*
* @param {string} nextAction
* @returns {Function}
*/
function _increaseSequence(nextAction) {
return function() {
_nextExpectedAction = nextAction;
++_sequenceLevels[combo];
_resetSequenceTimer();
};
}
/**
* wraps the specified callback inside of another function in order
* to reset all sequence counters as soon as this sequence is done
*
* @param {Event} e
* @returns void
*/
function _callbackAndReset(e) {
_fireCallback(callback, e, combo);
// we should ignore the next key up if the action is key down
// or keypress. this is so if you finish a sequence and
// release the key the final key will not trigger a keyup
if (action !== 'keyup') {
_ignoreNextKeyup = _characterFromEvent(e);
}
// weird race condition if a sequence ends with the key
// another sequence begins with
setTimeout(_resetSequences, 10);
}
// loop through keys one at a time and bind the appropriate callback
// function. for any key leading up to the final one it should
// increase the sequence. after the final, it should reset all sequences
//
// if an action is specified in the original bind call then that will
// be used throughout. otherwise we will pass the action that the
// next key in the sequence should match. this allows a sequence
// to mix and match keypress and keydown events depending on which
// ones are better suited to the key provided
for (var i = 0; i < keys.length; ++i) {
var isFinal = i + 1 === keys.length;
var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
_bindSingle(keys[i], wrappedCallback, action, combo, i);
}
}
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function _keysFromString(combination) {
var keys = combination.split('+');
if (combination[combination.length-1] === '+') {
keys.pop();
if (keys.length) keys.pop();
keys.push('+');
}
return keys;
}
/**
* Gets info for a specific key combination
*
* @param {string} combination key combination ("command+s" or "a" or "*")
* @param {string=} action
* @returns {Object}
*/
function _getKeyInfo(combination, action) {
var keys,
key,
i,
modifiers = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
keys = _keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
// normalize key names
if (_SPECIAL_ALIASES[key]) {
key = _SPECIAL_ALIASES[key];
}
// if this is not a keypress event then we should
// be smart about using shift keys
// this will only work for US keyboards however
if (action && action != 'keypress' && _SHIFT_MAP[key]) {
key = _SHIFT_MAP[key];
modifiers.push('shift');
}
// if this key is a modifier then add it to the list of modifiers
if (_isModifier(key)) {
modifiers.push(key);
}
}
// depending on what the key combination is
// we will try to pick the best event for it
action = _pickBestAction(key, modifiers, action);
return {
key: key,
modifiers: modifiers,
action: action
};
}
/**
* binds a single keyboard combination
*
* @param {string} combination
* @param {Function} callback
* @param {string=} action
* @param {string=} sequenceName - name of sequence if part of sequence
* @param {number=} level - what part of the sequence the command is
* @returns void
*/
function _bindSingle(combination, callback, action, sequenceName, level) {
// store a direct mapped reference for use with Mousetrap.trigger
_directMap[combination + ':' + action] = callback;
// make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' ');
var sequence = combination.split(' '),
info;
// if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time
if (sequence.length > 1) {
_bindSequence(combination, sequence, callback, action);
return;
}
info = _getKeyInfo(combination, action);
// make sure to initialize array if this is the first time
// a callback is added for this key
_callbacks[info.key] = _callbacks[info.key] || [];
// remove an existing match if there is one
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
// add this call back to the array
// if it is a sequence put it at the beginning
// if not put it at the end
//
// this is important because the way these are processed expects
// the sequence ones to come first
_callbacks[info.key][sequenceName ? 'unshift' : 'push']({
callback: callback,
modifiers: info.modifiers,
action: info.action,
seq: sequenceName,
level: level,
combo: combination
});
}
/**
* binds multiple combinations to the same callback
*
* @param {Array} combinations
* @param {Function} callback
* @param {string|undefined} action
* @returns void
*/
function _bindMultiple(combinations, callback, action) {
for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action);
}
}
// start!
_addEvent(document, 'keypress', _handleKeyEvent);
_addEvent(document, 'keydown', _handleKeyEvent);
_addEvent(document, 'keyup', _handleKeyEvent);
var Mousetrap = {
/**
* binds an event to mousetrap
*
* can be a single key, a combination of keys separated with +,
* an array of keys, or a sequence of keys separated by spaces
*
* be sure to list the modifier keys first to make sure that the
* correct key ends up getting bound (the last key in the pattern)
*
* @param {string|Array} keys
* @param {Function} callback
* @param {string=} action - 'keypress', 'keydown', or 'keyup'
* @returns void
*/
bind: function(keys, callback, action) {
keys = keys instanceof Array ? keys : [keys];
_bindMultiple(keys, callback, action);
return this;
},
/**
* unbinds an event to mousetrap
*
* the unbinding sets the callback function of the specified key combo
* to an empty function and deletes the corresponding key in the
* _directMap dict.
*
* TODO: actually remove this from the _callbacks dictionary instead
* of binding an empty function
*
* the keycombo+action has to be exactly the same as
* it was defined in the bind method
*
* @param {string|Array} keys
* @param {string} action
* @returns void
*/
unbind: function(keys, action) {
return Mousetrap.bind(keys, function() {}, action);
},
/**
* triggers an event that has already been bound
*
* @param {string} keys
* @param {string=} action
* @returns void
*/
trigger: function(keys, action) {
if (_directMap[keys + ':' + action]) {
_directMap[keys + ':' + action]({}, keys);
}
return this;
},
/**
* resets the library back to its initial state. this is useful
* if you want to clear out the current keyboard shortcuts and bind
* new ones - for example if you switch to another page
*
* @returns void
*/
reset: function() {
_callbacks = {};
_directMap = {};
return this;
},
/**
* should we stop this event before firing off callbacks
*
* @param {Event} e
* @param {Element} element
* @return {boolean}
*/
stopCallback: function(e, element) {
// if the element has the class "mousetrap" then no need to stop
if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
return false;
}
// stop for input, select, and textarea
return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
},
/**
* exposes _handleKey publicly so it can be overwritten by extensions
*/
handleKey: _handleKey
};
// expose mousetrap to the global object
window.Mousetrap = Mousetrap;
}) (window, document);

View File

@@ -0,0 +1,147 @@
/******************************************************************************
This is a binary tree based bin packing algorithm that is more complex than
the simple Packer (packer.js). Instead of starting off with a fixed width and
height, it starts with the width and height of the first block passed and then
grows as necessary to accomodate each subsequent block. As it grows it attempts
to maintain a roughly square ratio by making 'smart' choices about whether to
grow right or down.
When growing, the algorithm can only grow to the right OR down. Therefore, if
the new block is BOTH wider and taller than the current target then it will be
rejected. This makes it very important to initialize with a sensible starting
width and height. If you are providing sorted input (largest first) then this
will not be an issue.
A potential way to solve this limitation would be to allow growth in BOTH
directions at once, but this requires maintaining a more complex tree
with 3 children (down, right and center) and that complexity can be avoided
by simply chosing a sensible starting block.
Best results occur when the input blocks are sorted by height, or even better
when sorted by max(width,height).
Inputs:
------
blocks: array of any objects that have .w and .h attributes
Outputs:
-------
marks each block that fits with a .fit attribute pointing to a
node with .x and .y coordinates
Example:
-------
var blocks = [
{ w: 100, h: 100 },
{ w: 100, h: 100 },
{ w: 80, h: 80 },
{ w: 80, h: 80 },
etc
etc
];
var packer = new GrowingPacker();
packer.fit(blocks);
for(var n = 0 ; n < blocks.length ; n++) {
var block = blocks[n];
if (block.fit) {
Draw(block.fit.x, block.fit.y, block.w, block.h);
}
}
******************************************************************************/
GrowingPacker = function() { };
GrowingPacker.prototype = {
fit: function(blocks) {
var n, node, block, len = blocks.length;
var w = len > 0 ? blocks[0].w : 0;
var h = len > 0 ? blocks[0].h : 0;
this.root = { x: 0, y: 0, w: w, h: h };
for (n = 0; n < len ; n++) {
block = blocks[n];
if (node = this.findNode(this.root, block.w, block.h))
block.fit = this.splitNode(node, block.w, block.h);
else
block.fit = this.growNode(block.w, block.h);
}
},
findNode: function(root, w, h) {
if (root.used)
return this.findNode(root.right, w, h) || this.findNode(root.down, w, h);
else if ((w <= root.w) && (h <= root.h))
return root;
else
return null;
},
splitNode: function(node, w, h) {
node.used = true;
node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h };
node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h };
return node;
},
growNode: function(w, h) {
var canGrowDown = (w <= this.root.w);
var canGrowRight = (h <= this.root.h);
var shouldGrowRight = canGrowRight && (this.root.h >= (this.root.w + w)); // attempt to keep square-ish by growing right when height is much greater than width
var shouldGrowDown = canGrowDown && (this.root.w >= (this.root.h + h)); // attempt to keep square-ish by growing down when width is much greater than height
if (shouldGrowRight)
return this.growRight(w, h);
else if (shouldGrowDown)
return this.growDown(w, h);
else if (canGrowRight)
return this.growRight(w, h);
else if (canGrowDown)
return this.growDown(w, h);
else
return null; // need to ensure sensible root starting size to avoid this happening
},
growRight: function(w, h) {
this.root = {
used: true,
x: 0,
y: 0,
w: this.root.w + w,
h: this.root.h,
down: this.root,
right: { x: this.root.w, y: 0, w: w, h: this.root.h }
};
if (node = this.findNode(this.root, w, h))
return this.splitNode(node, w, h);
else
return null;
},
growDown: function(w, h) {
this.root = {
used: true,
x: 0,
y: 0,
w: this.root.w,
h: this.root.h + h,
down: { x: 0, y: this.root.h, w: this.root.w, h: h },
right: this.root
};
if (node = this.findNode(this.root, w, h))
return this.splitNode(node, w, h);
else
return null;
}
}

View File

@@ -0,0 +1,627 @@
(function(__exports__) {
"use strict";
var specials = [
'/', '.', '*', '+', '?', '|',
'(', ')', '[', ']', '{', '}', '\\'
];
var escapeRegex = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
function isArray(test) {
return Object.prototype.toString.call(test) === "[object Array]";
}
// A Segment represents a segment in the original route description.
// Each Segment type provides an `eachChar` and `regex` method.
//
// The `eachChar` method invokes the callback with one or more character
// specifications. A character specification consumes one or more input
// characters.
//
// The `regex` method returns a regex fragment for the segment. If the
// segment is a dynamic of star segment, the regex fragment also includes
// a capture.
//
// A character specification contains:
//
// * `validChars`: a String with a list of all valid characters, or
// * `invalidChars`: a String with a list of all invalid characters
// * `repeat`: true if the character specification can repeat
function StaticSegment(string) { this.string = string; }
StaticSegment.prototype = {
eachChar: function(callback) {
var string = this.string, ch;
for (var i=0, l=string.length; i<l; i++) {
ch = string.charAt(i);
callback({ validChars: ch });
}
},
regex: function() {
return this.string.replace(escapeRegex, '\\$1');
},
generate: function() {
return this.string;
}
};
function DynamicSegment(name) { this.name = name; }
DynamicSegment.prototype = {
eachChar: function(callback) {
callback({ invalidChars: "/", repeat: true });
},
regex: function() {
return "([^/]+)";
},
generate: function(params) {
return params[this.name];
}
};
function StarSegment(name) { this.name = name; }
StarSegment.prototype = {
eachChar: function(callback) {
callback({ invalidChars: "", repeat: true });
},
regex: function() {
return "(.+)";
},
generate: function(params) {
return params[this.name];
}
};
function EpsilonSegment() {}
EpsilonSegment.prototype = {
eachChar: function() {},
regex: function() { return ""; },
generate: function() { return ""; }
};
function parse(route, names, types) {
// normalize route as not starting with a "/". Recognition will
// also normalize.
if (route.charAt(0) === "/") { route = route.substr(1); }
var segments = route.split("/"), results = [];
for (var i=0, l=segments.length; i<l; i++) {
var segment = segments[i], match;
if (match = segment.match(/^:([^\/]+)$/)) {
results.push(new DynamicSegment(match[1]));
names.push(match[1]);
types.dynamics++;
} else if (match = segment.match(/^\*([^\/]+)$/)) {
results.push(new StarSegment(match[1]));
names.push(match[1]);
types.stars++;
} else if(segment === "") {
results.push(new EpsilonSegment());
} else {
results.push(new StaticSegment(segment));
types.statics++;
}
}
return results;
}
// A State has a character specification and (`charSpec`) and a list of possible
// subsequent states (`nextStates`).
//
// If a State is an accepting state, it will also have several additional
// properties:
//
// * `regex`: A regular expression that is used to extract parameters from paths
// that reached this accepting state.
// * `handlers`: Information on how to convert the list of captures into calls
// to registered handlers with the specified parameters
// * `types`: How many static, dynamic or star segments in this route. Used to
// decide which route to use if multiple registered routes match a path.
//
// Currently, State is implemented naively by looping over `nextStates` and
// comparing a character specification against a character. A more efficient
// implementation would use a hash of keys pointing at one or more next states.
function State(charSpec) {
this.charSpec = charSpec;
this.nextStates = [];
}
State.prototype = {
get: function(charSpec) {
var nextStates = this.nextStates;
for (var i=0, l=nextStates.length; i<l; i++) {
var child = nextStates[i];
var isEqual = child.charSpec.validChars === charSpec.validChars;
isEqual = isEqual && child.charSpec.invalidChars === charSpec.invalidChars;
if (isEqual) { return child; }
}
},
put: function(charSpec) {
var state;
// If the character specification already exists in a child of the current
// state, just return that state.
if (state = this.get(charSpec)) { return state; }
// Make a new state for the character spec
state = new State(charSpec);
// Insert the new state as a child of the current state
this.nextStates.push(state);
// If this character specification repeats, insert the new state as a child
// of itself. Note that this will not trigger an infinite loop because each
// transition during recognition consumes a character.
if (charSpec.repeat) {
state.nextStates.push(state);
}
// Return the new state
return state;
},
// Find a list of child states matching the next character
match: function(ch) {
// DEBUG "Processing `" + ch + "`:"
var nextStates = this.nextStates,
child, charSpec, chars;
// DEBUG " " + debugState(this)
var returned = [];
for (var i=0, l=nextStates.length; i<l; i++) {
child = nextStates[i];
charSpec = child.charSpec;
if (typeof (chars = charSpec.validChars) !== 'undefined') {
if (chars.indexOf(ch) !== -1) { returned.push(child); }
} else if (typeof (chars = charSpec.invalidChars) !== 'undefined') {
if (chars.indexOf(ch) === -1) { returned.push(child); }
}
}
return returned;
}
/** IF DEBUG
, debug: function() {
var charSpec = this.charSpec,
debug = "[",
chars = charSpec.validChars || charSpec.invalidChars;
if (charSpec.invalidChars) { debug += "^"; }
debug += chars;
debug += "]";
if (charSpec.repeat) { debug += "+"; }
return debug;
}
END IF **/
};
/** IF DEBUG
function debug(log) {
console.log(log);
}
function debugState(state) {
return state.nextStates.map(function(n) {
if (n.nextStates.length === 0) { return "( " + n.debug() + " [accepting] )"; }
return "( " + n.debug() + " <then> " + n.nextStates.map(function(s) { return s.debug() }).join(" or ") + " )";
}).join(", ")
}
END IF **/
// This is a somewhat naive strategy, but should work in a lot of cases
// A better strategy would properly resolve /posts/:id/new and /posts/edit/:id.
//
// This strategy generally prefers more static and less dynamic matching.
// Specifically, it
//
// * prefers fewer stars to more, then
// * prefers using stars for less of the match to more, then
// * prefers fewer dynamic segments to more, then
// * prefers more static segments to more
function sortSolutions(states) {
return states.sort(function(a, b) {
if (a.types.stars !== b.types.stars) { return a.types.stars - b.types.stars; }
if (a.types.stars) {
if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
if (a.types.dynamics !== b.types.dynamics) { return b.types.dynamics - a.types.dynamics; }
}
if (a.types.dynamics !== b.types.dynamics) { return a.types.dynamics - b.types.dynamics; }
if (a.types.statics !== b.types.statics) { return b.types.statics - a.types.statics; }
return 0;
});
}
function recognizeChar(states, ch) {
var nextStates = [];
for (var i=0, l=states.length; i<l; i++) {
var state = states[i];
nextStates = nextStates.concat(state.match(ch));
}
return nextStates;
}
var oCreate = Object.create || function(proto) {
function F() {}
F.prototype = proto;
return new F();
};
function RecognizeResults(queryParams) {
this.queryParams = queryParams || {};
}
RecognizeResults.prototype = oCreate({
splice: Array.prototype.splice,
slice: Array.prototype.slice,
push: Array.prototype.push,
length: 0,
queryParams: null
});
function findHandler(state, path, queryParams) {
var handlers = state.handlers, regex = state.regex;
var captures = path.match(regex), currentCapture = 1;
var result = new RecognizeResults(queryParams);
for (var i=0, l=handlers.length; i<l; i++) {
var handler = handlers[i], names = handler.names, params = {};
for (var j=0, m=names.length; j<m; j++) {
params[names[j]] = captures[currentCapture++];
}
result.push({ handler: handler.handler, params: params, isDynamic: !!names.length });
}
return result;
}
function addSegment(currentState, segment) {
segment.eachChar(function(ch) {
var state;
currentState = currentState.put(ch);
});
return currentState;
}
// The main interface
var RouteRecognizer = function() {
this.rootState = new State();
this.names = {};
};
RouteRecognizer.prototype = {
add: function(routes, options) {
var currentState = this.rootState, regex = "^",
types = { statics: 0, dynamics: 0, stars: 0 },
handlers = [], allSegments = [], name;
var isEmpty = true;
for (var i=0, l=routes.length; i<l; i++) {
var route = routes[i], names = [];
var segments = parse(route.path, names, types);
allSegments = allSegments.concat(segments);
for (var j=0, m=segments.length; j<m; j++) {
var segment = segments[j];
if (segment instanceof EpsilonSegment) { continue; }
isEmpty = false;
// Add a "/" for the new segment
currentState = currentState.put({ validChars: "/" });
regex += "/";
// Add a representation of the segment to the NFA and regex
currentState = addSegment(currentState, segment);
regex += segment.regex();
}
var handler = { handler: route.handler, names: names };
handlers.push(handler);
}
if (isEmpty) {
currentState = currentState.put({ validChars: "/" });
regex += "/";
}
currentState.handlers = handlers;
currentState.regex = new RegExp(regex + "$");
currentState.types = types;
if (name = options && options.as) {
this.names[name] = {
segments: allSegments,
handlers: handlers
};
}
},
handlersFor: function(name) {
var route = this.names[name], result = [];
if (!route) { throw new Error("There is no route named " + name); }
for (var i=0, l=route.handlers.length; i<l; i++) {
result.push(route.handlers[i]);
}
return result;
},
hasRoute: function(name) {
return !!this.names[name];
},
generate: function(name, params) {
var route = this.names[name], output = "";
if (!route) { throw new Error("There is no route named " + name); }
var segments = route.segments;
for (var i=0, l=segments.length; i<l; i++) {
var segment = segments[i];
if (segment instanceof EpsilonSegment) { continue; }
output += "/";
output += segment.generate(params);
}
if (output.charAt(0) !== '/') { output = '/' + output; }
if (params && params.queryParams) {
output += this.generateQueryString(params.queryParams, route.handlers);
}
return output;
},
generateQueryString: function(params, handlers) {
var pairs = [];
var keys = [];
for(var key in params) {
if (params.hasOwnProperty(key)) {
keys.push(key);
}
}
keys.sort();
for (var i = 0, len = keys.length; i < len; i++) {
key = keys[i];
var value = params[key];
if (value == null) {
continue;
}
var pair = key;
if (isArray(value)) {
for (var j = 0, l = value.length; j < l; j++) {
var arrayPair = key + '[]' + '=' + encodeURIComponent(value[j]);
pairs.push(arrayPair);
}
} else {
pair += "=" + encodeURIComponent(value);
pairs.push(pair);
}
}
if (pairs.length === 0) { return ''; }
return "?" + pairs.join("&");
},
parseQueryString: function(queryString) {
var pairs = queryString.split("&"), queryParams = {};
for(var i=0; i < pairs.length; i++) {
var pair = pairs[i].split('='),
key = decodeURIComponent(pair[0]),
keyLength = key.length,
isArray = false,
value;
if (pair.length === 1) {
value = 'true';
} else {
//Handle arrays
if (keyLength > 2 && key.slice(keyLength -2) === '[]') {
isArray = true;
key = key.slice(0, keyLength - 2);
if(!queryParams[key]) {
queryParams[key] = [];
}
}
value = pair[1] ? decodeURIComponent(pair[1]) : '';
}
if (isArray) {
queryParams[key].push(value);
} else {
queryParams[key] = decodeURIComponent(value);
}
}
return queryParams;
},
recognize: function(path) {
var states = [ this.rootState ],
pathLen, i, l, queryStart, queryParams = {},
isSlashDropped = false;
path = decodeURI(path);
queryStart = path.indexOf('?');
if (queryStart !== -1) {
var queryString = path.substr(queryStart + 1, path.length);
path = path.substr(0, queryStart);
queryParams = this.parseQueryString(queryString);
}
// DEBUG GROUP path
if (path.charAt(0) !== "/") { path = "/" + path; }
pathLen = path.length;
if (pathLen > 1 && path.charAt(pathLen - 1) === "/") {
path = path.substr(0, pathLen - 1);
isSlashDropped = true;
}
for (i=0, l=path.length; i<l; i++) {
states = recognizeChar(states, path.charAt(i));
if (!states.length) { break; }
}
// END DEBUG GROUP
var solutions = [];
for (i=0, l=states.length; i<l; i++) {
if (states[i].handlers) { solutions.push(states[i]); }
}
states = sortSolutions(solutions);
var state = solutions[0];
if (state && state.handlers) {
// if a trailing slash was dropped and a star segment is the last segment
// specified, put the trailing slash back
if (isSlashDropped && state.regex.source.slice(-5) === "(.+)$") {
path = path + "/";
}
return findHandler(state, path, queryParams);
}
}
};
__exports__.RouteRecognizer = RouteRecognizer;
function Target(path, matcher, delegate) {
this.path = path;
this.matcher = matcher;
this.delegate = delegate;
}
Target.prototype = {
to: function(target, callback) {
var delegate = this.delegate;
if (delegate && delegate.willAddRoute) {
target = delegate.willAddRoute(this.matcher.target, target);
}
this.matcher.add(this.path, target);
if (callback) {
if (callback.length === 0) { throw new Error("You must have an argument in the function passed to `to`"); }
this.matcher.addChild(this.path, target, callback, this.delegate);
}
return this;
}
};
function Matcher(target) {
this.routes = {};
this.children = {};
this.target = target;
}
Matcher.prototype = {
add: function(path, handler) {
this.routes[path] = handler;
},
addChild: function(path, target, callback, delegate) {
var matcher = new Matcher(target);
this.children[path] = matcher;
var match = generateMatch(path, matcher, delegate);
if (delegate && delegate.contextEntered) {
delegate.contextEntered(target, match);
}
callback(match);
}
};
function generateMatch(startingPath, matcher, delegate) {
return function(path, nestedCallback) {
var fullPath = startingPath + path;
if (nestedCallback) {
nestedCallback(generateMatch(fullPath, matcher, delegate));
} else {
return new Target(startingPath + path, matcher, delegate);
}
};
}
function addRoute(routeArray, path, handler) {
var len = 0;
for (var i=0, l=routeArray.length; i<l; i++) {
len += routeArray[i].path.length;
}
path = path.substr(len);
var route = { path: path, handler: handler };
routeArray.push(route);
}
function eachRoute(baseRoute, matcher, callback, binding) {
var routes = matcher.routes;
for (var path in routes) {
if (routes.hasOwnProperty(path)) {
var routeArray = baseRoute.slice();
addRoute(routeArray, path, routes[path]);
if (matcher.children[path]) {
eachRoute(routeArray, matcher.children[path], callback, binding);
} else {
callback.call(binding, routeArray);
}
}
}
}
RouteRecognizer.prototype.map = function(callback, addRouteCallback) {
var matcher = new Matcher();
callback(generateMatch("", matcher, this.delegate));
eachRoute([], matcher, function(route) {
if (addRouteCallback) { addRouteCallback(this, route); }
else { this.add(route); }
}, this);
};
})(window);

517
public/javascripts/smoke.js Normal file
View File

@@ -0,0 +1,517 @@
/*
SMOKE.JS - 0.1.3
(c) 2011-2013 Jonathan Youngblood
demos / documentation: http://smoke-js.com/
*/
;(function(window, document) {
/*jslint browser: true, onevar: true, undef: true, nomen: false, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */
var smoke = {
smoketimeout: [],
init: false,
zindex: 40000,
i: 0,
bodyload: function(id) {
var ff = document.createElement('div');
ff.setAttribute('id','smoke-out-'+id);
ff.className = 'smoke-base';
ff.style.zIndex = smoke.zindex;
smoke.zindex++;
document.body.appendChild(ff);
},
newdialog: function() {
var newid = new Date().getTime();
newid = Math.random(1,99) + newid;
if (!smoke.init) {
smoke.listen(window,"load", function() {
smoke.bodyload(newid);
});
} else {
smoke.bodyload(newid);
}
return newid;
},
forceload: function() {},
build: function (e, f) {
smoke.i++;
f.stack = smoke.i;
e = e.replace(/\n/g,'<br />');
e = e.replace(/\r/g,'<br />');
var prompt = '',
ok = 'OK',
cancel = 'Cancel',
classname = '',
buttons = '',
box;
if (f.type === 'prompt') {
prompt =
'<div class="smoke-dialog-prompt">'+
'<input class="input" id="smoke-dialog-input-'+f.newid+'" type="text" ' + (f.params.value ? 'value="' + f.params.value + '"' : '') + ' />'+
'</div>';
}
if (f.params.ok) {
ok = f.params.ok;
}
if (f.params.cancel) {
cancel = f.params.cancel;
}
if (f.params.classname) {
classname = f.params.classname;
}
if (f.type !== 'signal') {
buttons = '<div class="smoke-dialog-buttons">';
if (f.type === 'alert') {
buttons +=
'<button class="btn btn-md btn-round" id="alert-ok-'+f.newid+'">'+ok+'</button>';
}
else if (f.type === 'quiz') {
if (f.params.button_1) {
buttons +=
'<button class="btn btn-md btn-round quiz-button" id="'+f.type+'-ok1-'+f.newid+'">'+f.params.button_1+'</button>';
}
if (f.params.button_2) {
buttons +=
'<button class="btn btn-md btn-round quiz-button" id="'+f.type+'-ok2-'+f.newid+'">'+f.params.button_2+'</button>';
}
if (f.params.button_3) {
buttons +=
'<button class="btn btn-md btn-round quiz-button" id="'+f.type+'-ok3-'+f.newid+'">'+f.params.button_3+'</button>';
}
if (f.params.button_cancel) {
buttons +=
'<button id="'+f.type+'-cancel-'+f.newid+'" class="btn btn-md btn-round cancel">'+f.params.button_cancel+'</button>';
}
}
else if (f.type === 'prompt' || f.type === 'confirm') {
if (f.params.reverseButtons) {
buttons +=
'<button class="btn btn-md btn-round btn-primary" id="'+f.type+'-ok-'+f.newid+'">'+ok+'</button>' +
'<button class="btn btn-md btn-round cancel" id="'+f.type+'-cancel-'+f.newid+'">'+cancel+'</button>';
} else {
buttons +=
'<button class="btn btn-md btn-round cancel" id="'+f.type+'-cancel-'+f.newid+'">'+cancel+'</button>'+
'<button class="btn btn-md btn-round btn-primary" id="'+f.type+'-ok-'+f.newid+'">'+ok+'</button>';
}
}
buttons += '</div>';
}
box =
'<div class="smoke-dialog smoke '+classname+'">'+
'<div class="smoke-dialog-inner">'+
e+
prompt+
buttons+
'</div>'+
'</div>';
if (!smoke.init) {
smoke.listen(window,"load", function() {
smoke.finishbuild(e,f,box);
});
} else{
smoke.finishbuild(e,f,box);
}
},
finishbuild: function(e, f, box) {
var ff = document.getElementById('smoke-out-'+f.newid);
ff.className = 'smoke-base smoke-visible smoke-' + f.type;
ff.innerHTML = box;
while (ff.innerHTML === "") {
ff.innerHTML = box;
}
if (smoke.smoketimeout[f.newid]) {
clearTimeout(smoke.smoketimeout[f.newid]);
}
// smoke.listen(
// document.getElementById('smoke-bg-'+f.newid),
// "click",
// function () {
// smoke.destroy(f.type, f.newid);
// if (f.type === 'prompt' || f.type === 'confirm' || f.type === 'quiz') {
// f.callback(false);
// } else if (f.type === 'alert' && typeof f.callback !== 'undefined') {
// f.callback();
// }
// }
// );
switch (f.type) {
case 'alert':
smoke.finishbuildAlert(e, f, box);
break;
case 'confirm':
smoke.finishbuildConfirm(e, f, box);
break;
case 'quiz':
smoke.finishbuildQuiz(e, f, box);
break;
case 'prompt':
smoke.finishbuildPrompt(e, f, box);
break;
case 'signal':
smoke.finishbuildSignal(e, f, box);
break;
default:
throw "Unknown type: " + f.type;
}
},
finishbuildAlert: function (e, f, box) {
smoke.listen(
document.getElementById('alert-ok-'+f.newid),
"click",
function () {
smoke.destroy(f.type, f.newid);
if (typeof f.callback !== 'undefined') {
f.callback();
}
}
);
document.onkeyup = function (e) {
if (!e) {
e = window.event;
}
if (e.keyCode === 13 || e.keyCode === 32 || e.keyCode === 27) {
smoke.destroy(f.type, f.newid);
if (typeof f.callback !== 'undefined') {
f.callback();
}
}
};
},
finishbuildConfirm: function (e, f, box) {
smoke.listen(
document.getElementById('confirm-cancel-' + f.newid),
"click",
function ()
{
smoke.destroy(f.type, f.newid);
f.callback(false);
}
);
smoke.listen(
document.getElementById('confirm-ok-' + f.newid),
"click",
function ()
{
smoke.destroy(f.type, f.newid);
f.callback(true);
}
);
document.onkeyup = function (e) {
if (!e) {
e = window.event;
}
if (e.keyCode === 13 || e.keyCode === 32) {
smoke.destroy(f.type, f.newid);
f.callback(true);
} else if (e.keyCode === 27) {
smoke.destroy(f.type, f.newid);
f.callback(false);
}
};
},
finishbuildQuiz: function (e, f, box) {
var a, b, c;
smoke.listen(
document.getElementById('quiz-cancel-' + f.newid),
"click",
function ()
{
smoke.destroy(f.type, f.newid);
f.callback(false);
}
);
if (a = document.getElementById('quiz-ok1-'+f.newid))
smoke.listen(
a,
"click",
function () {
smoke.destroy(f.type, f.newid);
f.callback(a.innerHTML);
}
);
if (b = document.getElementById('quiz-ok2-'+f.newid))
smoke.listen(
b,
"click",
function () {
smoke.destroy(f.type, f.newid);
f.callback(b.innerHTML);
}
);
if (c = document.getElementById('quiz-ok3-'+f.newid))
smoke.listen(
c,
"click",
function () {
smoke.destroy(f.type, f.newid);
f.callback(c.innerHTML);
}
);
document.onkeyup = function (e) {
if (!e) {
e = window.event;
}
if (e.keyCode === 27) {
smoke.destroy(f.type, f.newid);
f.callback(false);
}
};
},
finishbuildPrompt: function (e, f, box) {
var pi = document.getElementById('smoke-dialog-input-'+f.newid);
setTimeout(function () {
pi.focus();
pi.select();
}, 100);
smoke.listen(
document.getElementById('prompt-cancel-'+f.newid),
"click",
function () {
smoke.destroy(f.type, f.newid);
f.callback(false);
}
);
smoke.listen(
document.getElementById('prompt-ok-'+f.newid),
"click",
function () {
smoke.destroy(f.type, f.newid);
f.callback(pi.value);
}
);
document.onkeyup = function (e) {
if (!e) {
e = window.event;
}
if (e.keyCode === 13) {
smoke.destroy(f.type, f.newid);
f.callback(pi.value);
} else if (e.keyCode === 27) {
smoke.destroy(f.type, f.newid);
f.callback(false);
}
};
},
finishbuildSignal: function (e, f, box) {
document.onkeyup = function (e) {
if (!e) {
e = window.event;
}
if (e.keyCode === 27) {
smoke.destroy(f.type, f.newid);
if (typeof f.callback !== 'undefined') {
f.callback();
}
}
};
smoke.smoketimeout[f.newid] = setTimeout(function () {
smoke.destroy(f.type, f.newid);
if (typeof f.callback !== 'undefined') {
f.callback();
}
}, f.timeout);
},
destroy: function (type,id) {
var box = document.getElementById('smoke-out-'+id);
if (type !== 'quiz') {
var okButton = document.getElementById(type+'-ok-'+id);
}
var cancelButton = document.getElementById(type+'-cancel-'+id);
box.className = 'smoke-base';
if (okButton) {
smoke.stoplistening(okButton, "click", function() {});
document.onkeyup = null;
}
if (type === 'quiz') {
var quiz_buttons = document.getElementsByClassName("quiz-button");
for (var i = 0; i < quiz_buttons.length; i++) {
smoke.stoplistening(quiz_buttons[i], "click", function() {});
document.onkeyup = null;
}
}
if (cancelButton) {
smoke.stoplistening(cancelButton, "click", function() {});
}
smoke.i = 0;
box.innerHTML = '';
},
alert: function (e, f, g) {
if (typeof g !== 'object') {
g = false;
}
var id = smoke.newdialog();
smoke.build(e, {
type: 'alert',
callback: f,
params: g,
newid: id
});
},
signal: function (e, f, g) {
if (typeof g !== 'object') {
g = false;
}
var duration = 5000;
if (g.duration !== 'undefined'){
duration = g.duration;
}
var id = smoke.newdialog();
smoke.build(e, {
type: 'signal',
callback: f,
timeout: duration,
params: g,
newid: id
});
},
confirm: function (e, f, g) {
if (typeof g !== 'object') {
g = false;
}
var id = smoke.newdialog();
smoke.build(e, {
type: 'confirm',
callback: f,
params: g,
newid: id
});
},
quiz: function (e, f, g) {
if (typeof g !== 'object') {
g = false;
}
var id = smoke.newdialog();
smoke.build(e, {
type: 'quiz',
callback: f,
params: g,
newid: id
});
},
prompt: function (e, f, g) {
if (typeof g !== 'object') {
g = false;
}
var id = smoke.newdialog();
return smoke.build(e,{type:'prompt',callback:f,params:g,newid:id});
},
listen: function (e, f, g) {
if (e.addEventListener) {
return e.addEventListener(f, g, false);
}
if (e.attachEvent) {
return e.attachEvent('on'+f, g);
}
return false;
},
stoplistening: function (e, f, g) {
if (e.removeEventListener) {
return e.removeEventListener(f, g, false);
}
if (e.detachEvent) {
return e.detachEvent('on'+f, g);
}
return false;
}
};
smoke.init = true;
if (typeof module != 'undefined' && module.exports) {
module.exports = smoke;
}
else if (typeof define === 'function' && define.amd) {
define('smoke', [], function() {
return smoke;
});
}
else {
this.smoke = smoke;
}
})(window, document);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
/*
SpacedeckAccount
This module contains functions dealing with the spacedeck account.
*/
SpacedeckAccount = {
data: {
account_confirmed_sent: false,
account_tab: 'invoices',
password_change_error: null,
feedback_text: ""
},
methods: {
show_account: function(user) {
this.activate_dropdown('account');
this.load_subscription();
this.load_billing();
},
account_save_user_digest: function(val) {
this.user.preferences.daily_digest = val;
this.save_user(function(){
});
},
account_save_user_notifications: function(val) {
this.user.preferences.email_notifications = val;
this.save_user(function(){
});
},
save_user_email: function() {
this.save_user(function() {
}.bind(this));
},
save_user_language: function(lang) {
localStorage.lang = lang;
if (this.user.preferences) {
this.user.preferences.language = lang;
this.save_user(function() {
window._spacedeck_location_change = true;
location.href="/spaces";
}.bind(this));
}
},
save_user: function(on_success) {
if (this.user.email_changed) {
this.user.confirmed_at = null;
}
window._spacedeck_location_change = true;
save_user(this.user, function(user) {
if (on_success) on_success();
else location.href="/spaces";
}.bind(this), function(xhr){
console.error(xhr)
});
},
save_user_password: function(oldPass, newPass, newPassConfirm) {
this.password_change_error = null;
if (!oldPass) {
this.password_change_error = "Current password required";
return;
}
if (!newPass || !newPassConfirm) {
this.password_change_error = "New password/password confirmation required";
return;
}
if (newPass!=newPassConfirm) {
this.password_change_error = "New Passwords do not match";
return;
}
if (newPass.length < 6) {
this.password_change_error = "New Password to short";
return;
}
save_user_password(this.user, oldPass, newPass, function() {
alert("OK. Password Changed.");
this.password_change_current = "";
this.password_change_new = "";
this.password_change_new_confirmation = "";
}.bind(this), function(xhr) {
if (xhr.status == 403) {
this.password_change_error = "Old Password not correct";
} else {
this.password_change_error = "Something went wrong. Please try again later.";
}
}.bind(this));
},
confirm_again: function() {
resent_confirm_mail(this.user, function(re) {
this.account_confirmed_sent = true;
alert(__("confirm_again"));
}.bind(this), function(xhr){
console.error(xhr);
alert("Something went wrong, please try again.");
});
},
confirm_account: function(token) {
confirm_user(this.user, token, function(re) {
smoke.alert(__("confirmed"), function() {
this.redirect_to("/spaces");
}.bind(this));
}.bind(this), function(xhr) {
console.error(xhr);
alert(xhr.responseText);
this.redirect_to("/spaces");
}.bind(this));
},
}
}

View File

@@ -0,0 +1,81 @@
var SpacedeckAvatars = {
data: {
uploading_avatar: false,
uploading_folder_avatar: false,
uploading_cover: false
},
methods: {
save_avatar_image: function(input, object_type, object) {
if (input.files.length > 0) {
var f = input.files[0];
var finished = function() {
this.uploading_avatar = false;
this.uploading_cover = false;
this.uploading_folder_avatar = false;
}.bind(this);
if (!_.include(["image/jpeg","image/jpg","image/png","image/gif"], f.type)) {
alert("Unsupported file type. Please upload JPEG, PNG or GIF.");
finished();
return;
}
if (f.size > 1024*1024*3) {
alert("File must be smaller than 3 megabytes.");
finished();
return;
}
save_avatar_file(object_type, object, f, function(res) {
finished();
this.uploading_avatar = false;
this.uploading_cover = false;
var newUri = res.avatar_thumb_uri;
object.avatar_thumb_uri = newUri + "?cachebuster=" + Math.random();
}.bind(this), function(error) {
alert("Upload failed: " + error);
finished();
});
}
},
save_space_avatar_image: function(viewmodel) {
this.uploading_avatar = true;
var func = this.save_avatar_image.bind(this);
func(viewmodel.$event.target, "space", this.active_space);
},
save_folder_avatar_image: function(viewmodel) {
this.uploading_folder_avatar = true;
var func = this.save_avatar_image.bind(this);
func(viewmodel.$event.target, "space", this.active_folder);
},
save_user_avatar_image: function(viewmodel) {
this.uploading_avatar = true;
var func = this.save_avatar_image.bind(this);
func(viewmodel.$event.target, "user", viewmodel.$root.user);
},
delete_user_avatar_image: function() {
this.user.avatar_original_uri = "";
this.user.avatar_thumb_uri = "";
save_user(this.user,function(updated) {
}.bind(this));
},
save_user_background_image: function(viewmodel) {
var input = viewmodel.$event.target;
this.uploading_cover = true;
var f = input.files[0];
save_user_background_file(this.user, f, function(res) {
this.user.background_original_uri = res.background_original_uri;
this.uploading_cover = false;
}.bind(this));
}
}
}

View File

@@ -0,0 +1,670 @@
/*
SpacedeckBoardArtifacts
This module contains functions dealing with absolute positioned Board Section Artifacts.
*/
var SpacedeckBoardArtifacts = {
update_board_artifact_viewmodel: function(a) {
var mt = this.artifact_major_type(a);
a.view = {
_id: a._id,
classes: this.artifact_classes(a),
style: this.artifact_style(a),
grid_style: this.artifact_style(a, true),
inner_style: this.artifact_inner_style(a),
text_cell_style: this.artifact_text_cell_style(a),
vector_svg: this.artifact_vector_svg(a),
payload_uri: a.payload_uri,
thumbnail_uri: this.artifact_thumbnail_uri(a),
major_type: mt,
text_blank: this.artifact_is_text_blank(a),
payload_alternatives: a.payload_alternatives,
filename: this.artifact_filename(a),
oembed_html: this.artifact_oembed_html(a),
link: this.artifact_link(a),
link_caption: this.artifact_link_caption(a),
interactive: 0
};
if ((mt == "audio" || mt == "video") && !a.player_view) {
a.player_view = {
state: "stop",
current_time_string: "",
total_time_string: "",
current_time_float: 0.0,
inpoint_float: 0.0,
outpoint_float: 0.0
};
}
if ("medium_for_object" in this) {
var medium = this.medium_for_object[a._id];
if (medium && a._id != this.editing_artifact_id) {
medium.value(a.description);
}
}
},
is_artifact_audio: function(a) {
if (a) {
return a.mime.match("audio");
} else return false;
},
artifact_filename: function(a) {
if (a.payload_uri) {
return a.payload_uri.replace(/.*\//g,"");
} else {
return "";
}
},
artifact_link: function(a) {
if (a.meta && a.meta.link_uri) {
return a.meta.link_uri;
} else {
return "";
}
},
artifact_link_caption: function(a) {
if (a.meta && a.meta.link_uri) {
var parts = a.meta.link_uri.split("/");
// scheme://domain.foo/...
// 0 1 2
if (parts.length>2) {
return parts[2];
}
return "Link";
} else {
return "";
}
},
artifact_is_selected: function(a) {
if (!a) return false;
return !!this.selected_artifacts_dict[a._id];
},
artifact_is_text_blank: function(a) {
if(a.description){
var filtered = a.description.replace(/<[^>]+>/g,"").replace(/\s/g,"");
return (filtered.length<1);
}else{
return false;
}
},
artifact_classes: function(a) {
clzs = ["artifact", "artifact-"+this.artifact_major_type(a), a.mime.replace("/","-")];
if (this.artifact_is_selected(a) && this.editing_artifact_id!=a._id) clzs.push("selected");
if (!a._id) clzs.push("creating");
if (a.style) {
clzs.push("align-"+a.style.align);
clzs.push("align-"+a.style.valign);
}
clzs.push("state-"+a.state);
if (this.artifact_is_text_blank(a)) {
clzs.push("text-blank");
}
if (a.locked) {
clzs.push("locked");
}
return clzs.join(" ");
},
artifact_inner_style: function(a) {
var styles = [];
if (a.style) {
var svg_style = ((a.mime.match("vector") || a.mime.match("shape")) && a.style.shape!="square");
if (!svg_style) {
if (a.style.stroke) {
styles.push("border-width:"+a.style.stroke+"px");
styles.push("border-style:"+(a.style.stroke_style||"solid"));
}
if (a.style.stroke_color) {
styles.push("border-color:"+a.style.stroke_color);
}
if (a.style.border_radius) {
styles.push("border-radius:"+a.style.border_radius+"px");
}
}
if (a.style.fill_color && !svg_style) {
styles.push("background-color:"+a.style.fill_color);
}
if (a.style.text_color) {
styles.push("color:"+a.style.text_color);
}
var filters = [];
if (!isNaN(a.style.brightness) && a.style.brightness != 100) {
filters.push("brightness("+a.style.brightness+"%)");
}
if (!isNaN(a.style.contrast) && a.style.contrast != 100) {
filters.push("contrast("+a.style.contrast+"%)");
}
if (!isNaN(a.style.opacity) && a.style.opacity != 100) {
filters.push("opacity("+a.style.opacity+"%)");
}
if (!isNaN(a.style.hue) && a.style.hue) {
filters.push("hue-rotate("+a.style.hue+"deg)");
}
if (!isNaN(a.style.saturation) && a.style.saturation != 100) {
filters.push("saturate("+a.style.saturation+"%)");
}
if (!isNaN(a.style.blur) && a.style.blur) {
filters.push("blur("+a.style.blur+"px)");
}
if (filters.length) {
styles.push("-webkit-filter:"+filters.join(" "));
styles.push("filter:"+filters.join(" "));
}
}
return styles.join(";");
},
artifact_text_cell_style: function(a, for_text_editor) {
var styles = [];
if (a.style) {
if (a.style.padding_left) styles.push("padding-left:"+a.style.padding_left+"px");
if (a.style.padding_right) styles.push("padding-right:"+a.style.padding_right+"px");
if (a.style.padding_top) styles.push("padding-top:"+a.style.padding_top+"px");
if (a.style.padding_bottom) styles.push("padding-bottom:"+a.style.padding_bottom+"px");
}
return styles.join(";");
},
artifact_style: function(a, for_grid) {
var styles = [];
var z = 0;
if (a.board) {
z = a.board.z;
if (z<0) z=0; // fix negative z-index
styles = [
"left:" +a.board.x+"px",
"top:" +a.board.y+"px",
"width:" +a.board.w+"px",
"height:"+a.board.h+"px",
"z-index:"+z
];
}
if (a.style) {
if (a.style.margin_left) styles.push("margin-left:"+a.style.margin_left+"px");
if (a.style.margin_right) styles.push("margin-right:"+a.style.margin_right+"px");
if (a.style.margin_top) styles.push("margin-top:"+a.style.margin_top+"px");
if (a.style.margin_bottom) styles.push("margin-bottom:"+a.style.margin_bottom+"px");
}
// FIXME: via class logic?
if (a.mime.match("vector")) {
styles.push("overflow:visible");
}
return styles.join(";");
},
artifact_major_type: function(a) {
if (a.mime.match("oembed")) return "oembed";
if (a.mime.match("zone")) return "zone";
if (a.mime.match("svg")) return "svg";
if (a.mime.match("image")) return "image";
if (a.mime.match("pdf")) return "image";
if (a.mime.match("video")) return "video";
if (a.mime.match("audio")) return "audio";
if (a.mime.match("website")) return "website";
if (a.mime.match("vector")) return "vector";
if (a.mime.match("shape")) return "shape";
if (a.mime.match("placeholder")) return "placeholder";
if (a.mime.match("text") || a.mime.match("note")) return "text";
return "file";
},
artifact_thumbnail_uri: function(a) {
if (a.payload_thumbnail_big_uri && a.board) {
if (a.board.w>800) {
return a.payload_thumbnail_big_uri;
}
}
return a.payload_thumbnail_medium_uri || a.payload_thumbnail_big_uri || a.payload_thumbnail_web_uri || "";
},
artifact_oembed_html: function(a) {
if (this.artifact_major_type(a) != "oembed") return "";
var parts = a.mime.split("/")[1].split("-");
var type = parts[0];
var provider = parts[1];
if (!a.meta || !a.meta.link_uri) {
console.log("missing meta / link_uri: ",a);
console.log("type/provider: ",type,provider);
return ("missing metadata: "+a._id);
}
if (provider=="youtube") {
var vid = a.meta.link_uri.match(/(v=|\/)([a-zA-Z0-9\-_]{11})/);
if (vid && vid.length>2) {
var uri = "https://youtube.com/embed/"+vid[2];
return "<iframe frameborder=0 allowfullscreen src=\""+uri+"?showinfo=0&rel=0&controls=0\"></iframe>";
} else return "Can't resolve: "+a.payload_uri;
} else if (provider=="dailymotion") {
var match = a.meta.link_uri.match(/dailymotion.com\/video\/([^<]*)/);
if (match && match.length>1) {
var uri = "https://www.dailymotion.com/embed/video/"+match[1];
return "<iframe frameborder=0 allowfullscreen src=\""+uri+"\"></iframe>";
} else return "Can't resolve: "+a.payload_uri;
} else if (provider=="vimeo") {
var match = a.meta.link_uri.match(/https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/);
if (match) {
var uri = "https://player.vimeo.com/video/"+match[2];
return "<iframe frameborder=0 allowfullscreen src=\""+uri+"\"></iframe>";
} else return "Can't resolve: "+a.payload_uri;
} else if (provider=="soundcloud") {
return '<iframe width="100%" height="166" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url='+a.meta.link_uri.replace(":", "%3A")+'"></iframe>';
} else if (provider=="spacedeck") {
return ""; //<iframe frameborder=0 allowfullscreen src=\""+ a.meta.link_uri+"\"></iframe>
} else {
return "Don't know how to embed "+a.mime+".";
}
},
artifact_vector_svg: function(a) {
var mtype = this.artifact_major_type(a);
if (mtype != "vector" && mtype != "shape") return "";
var shape = a.style.shape || "";
var padding = 32 + a.style.stroke*2;
var path_svg;
var fill = "";
if (mtype == "vector") {
path_svg = render_vector_drawing(a, padding);
fill = "fill:none";
} else {
path_svg = render_vector_shape(a, padding);
fill = "fill:"+a.style.fill_color+";";
padding = 0;
}
var margin = padding;
var svg = "<svg xmlns='http://www.w3.org/2000/svg' width='"+(a.board.w+2*padding)+"' height='"+(a.board.h+2*padding)+"' ";
svg += "style='margin-left:"+(-margin)+"px;margin-top:"+(-margin)+"px;stroke-width:"+a.style.stroke+";stroke:"+a.style.stroke_color+";"+fill+"'>";
svg += path_svg;
svg += "</svg>";
return svg;
},
/* whiteboard layouting functions */
artifact_enclosing_rect: function(arts) {
if (arts.length==0) return null;
r = {
x1: parseInt(_.min(arts.map(function(a){return a.board.x}))),
y1: parseInt(_.min(arts.map(function(a){return a.board.y}))),
x2: parseInt(_.max(arts.map(function(a){return a.board.x+a.board.w}))),
y2: parseInt(_.max(arts.map(function(a){return a.board.y+a.board.h})))
};
r.x=r.x1;
r.y=r.y1;
r.w=r.x2-r.x1;
r.h=r.y2-r.y1;
return r;
},
artifact_selection_rect: function() {
return this.artifact_enclosing_rect(this.selected_artifacts());
},
rects_intersecting: function(r1,r2) {
if ( (r1.x+r1.w < r2.x)
|| (r1.x > r2.x+r2.w)
|| (r1.y+r1.h < r2.y)
|| (r1.y > r2.y+r2.h) ) return false;
return true;
},
artifacts_in_rect: function(rect) {
return _.filter(this.active_space_artifacts, function(a) {
return this.rects_intersecting(a.board, rect);
}.bind(this));
},
layout_stack_top: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
var overlapping = _.filter(this.artifacts_in_rect(rect), function(a){return !this.is_selected(a)}.bind(this));
var max_z = _.max(overlapping,function(a){ return a.board.z; });
if (max_z.board) {
max_z = max_z.board.z + 1;
} else {
max_z = 1;
}
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { z: max_z }) };
});
},
layout_stack_bottom: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
var overlapping = _.filter(this.artifacts_in_rect(rect), function(a){return !this.is_selected(a);}.bind(this));
var min_z = _.min(overlapping,function(a){ return (a.board?a.board.z:0); });
if (min_z.board) {
min_z = min_z.board.z - 1;
} else {
min_z = 0;
}
var my_z = _.max(this.selected_artifacts(),function(a){ (a.board?a.board.z:0); });
if (my_z.board) {
my_z = my_z.board.z - 1;
} else {
my_z = 0;
}
// TODO: move all other items up in this case?
if (min_z < 0) {
this.update_artifacts(overlapping, function(a) {
return { board: _.extend(a.board, { z: (my_z + (a.board?a.board.z:0) + 1) }) };
});
return;
}
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { z: min_z }) };
});
},
layout_align_left:function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { x: rect.x1 }) };
});
},
layout_align_top: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { y: rect.y1 }) };
});
},
layout_align_right: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { x: rect.x2 - a.board.w }) };
});
},
layout_align_bottom: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { y: rect.y2 - a.board.h }) };
});
},
layout_align_center: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
var cx = rect.x1 + (rect.x2-rect.x1)/2;
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { x: cx - a.board.w/2 }) };
});
},
layout_align_middle: function() {
this.begin_transaction();
var rect = this.artifact_selection_rect();
var cy = rect.y1 + (rect.y2-rect.y1)/2;
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { y: cy - a.board.h/2 }) };
});
},
layout_match_size_horiz:function() {
this.begin_transaction();
var arts = this.selected_artifacts();
if (arts.length<2) return;
var totalw = _.reduce(arts, function(sum, a) { return sum + a.board.w }, 0);
var avgw = totalw / arts.length;
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { w: avgw }) };
});
},
layout_match_size_vert:function() {
this.begin_transaction();
var arts = this.selected_artifacts();
if (arts.length<2) return;
var totalh = _.reduce(arts, function(sum, a) { return sum + a.board.h }, 0);
var avgh = totalh / arts.length;
this.update_selected_artifacts(function(a) {
return { board: _.extend(a.board, { h: avgh }) };
});
},
layout_match_size_both:function() {
this.layout_match_size_horiz();
this.layout_match_size_vert();
},
layout_distribute_horizontal: function() {
this.begin_transaction();
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.board.x });
var startx = sorted[0].board.x + sorted[0].board.w/2;
var stopx = _.last(sorted).board.x + _.last(sorted).board.w/2;
var step = (stopx-startx)/(sorted.length-1);
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var x = startx + step*i - a.board.w/2;
this.update_artifacts([a],function(a) {
return { board: _.extend(a.board, {x: x}) }
});
}
},
layout_distribute_vertical: function() {
this.begin_transaction();
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.board.y });
var starty = sorted[0].board.y + sorted[0].board.h/2;
var stopy = _.last(sorted).board.y + _.last(sorted).board.h/2;
var step = (stopy-starty)/(sorted.length-1);
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var y = starty + step*i - a.board.h/2;
this.update_artifacts([a],function(a) {
return { board: _.extend(a.board, {y: y}) }
});
}
},
layout_distribute_horizontal_spacing: function() {
this.begin_transaction();
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.board.x });
var startx = sorted[0].board.x;
var stopx = _.last(sorted).board.x + _.last(sorted).board.w;
var range = stopx - startx;
var totalw = _.reduce(sorted, function(sum, a) { return sum + a.board.w }, 0);
var avgs = (range - totalw) / (sorted.length-1);
var prevend = startx + sorted[0].board.w;
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var x = prevend + avgs;
this.update_artifacts([a],function(a) {
return { board: _.extend(a.board, {x: x}) }
});
prevend = x+a.board.w;
}
},
layout_distribute_vertical_spacing: function() {
this.begin_transaction();
var selected = this.selected_artifacts();
if (selected.length<3) return;
var sorted = _.sortBy(selected, function(a) { return a.board.y });
var starty = sorted[0].board.y;
var stopy = _.last(sorted).board.y + _.last(sorted).board.h;
var range = stopy - starty;
var totalh = _.reduce(sorted, function(sum, a) { return sum + a.board.h }, 0);
var avgs = (range - totalh) / (sorted.length-1);
var prevend = starty + sorted[0].board.h;
for (var i=1; i<sorted.length-1; i++) {
var a = sorted[i];
var y = prevend + avgs;
this.update_artifacts([a],function(a) {
return { board: _.extend(a.board, {y: y}) }
});
prevend = y+a.board.h;
}
},
layout_auto: function() {
this.begin_transaction();
var selected = this.selected_artifacts();
if (selected.length<2) return;
var sorted = _.sortBy(selected, function(a) { return a.board.x+a.board.y*this.active_space.advanced.width }.bind(this));
var minx = sorted[0].board.x;
var miny = sorted[0].board.y;
var sorted = _.sortBy(selected, function(a) { return -Math.max(a.board.w,a.board.h) }.bind(this));
var blocks = [];
for (var i=0; i<sorted.length; i++) {
var a = sorted[i];
blocks.push({
w: a.board.w,
h: a.board.h,
a: a
});
}
var packer = new GrowingPacker();
packer.fit(blocks);
for (var i=0; i<blocks.length; i++) {
var block = blocks[i];
if (block.fit) {
var a = block.a;
this.update_artifacts([a],function(a) {
return { board: _.extend(a.board, {
x: minx+block.fit.x,
y: miny+block.fit.y
}) }
});
}
}
},
show_artifact_comments: function(evt) {
evt.preventDefault();
evt.stopPropagation();
var artifact = this.selected_artifacts()[0];
this.selected_artifact = artifact;
this.activate_modal('artifact');
},
create_artifact_comment: function(artifact, comment) {
var data = {
artifact_id: artifact._id,
space_id: this.active_space._id,
message: comment,
user: this.user
};
save_comment(this.active_space._id, data, function(comment) {
this.active_space_messages.push(comment);
this.artifact_comment = "";
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
remove_artifact_comment: function(comment) {
delete_comment(this.active_space._id, comment._id, function(comment) {
this.active_space_messages.pop(comment);
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
}
}
if (typeof(window) == 'undefined') {
exports.SpacedeckBoardArtifacts = SpacedeckBoardArtifacts;
}

View File

@@ -0,0 +1,568 @@
/*
Spacedeck Directives
This module registers custom Vue directives for Spacedeck.
*/
function setup_directives() {
Vue.directive('clipboard', {
bind: function () {
this.clipboard = new Clipboard(".clipboard-btn");
},
update: function (value) {
},
unbind: function () {
this.clipboard.destroy()
}
});
Vue.directive('t', {
update: function (value, key) {
this.el.innerHTML = key;
}
});
if ('ontouchstart' in window) {
var edown = "touchstart";
var emove = "touchmove";
var eup = "touchend";
} else {
var edown = "mousedown";
var emove = "mousemove";
var eup = "mouseup";
}
Vue.directive('videoplayer', {
update: function (a) {
var el = this.el;
var scope = this.vm.$root;
var video = el.querySelectorAll("video")[0];
var play_button = el.querySelectorAll(".play")[0];
var pause_button = el.querySelectorAll(".pause")[0];
var stop_button = el.querySelectorAll(".stop")[0];
var player_state = "stop";
var update_view = function() {
try {
if (!a.player_view) { a.player_view = {} };
a.player_view.state = player_state;
} catch (e) {
// catch InvalidStateError
}
}
var play_func = function() {
video.play();
player_state = "playing";
update_view();
}
var pause_func = function() {
try {
video.pause();
player_state = "paused";
update_view();
} catch (e) {
// catch InvalidStateError
}
}
var stop_func = function() {
try {
player_state = "stop";
video.pause();
video.currentTime = 0;
update_view();
} catch (e) {
// catch InvalidStateError
}
}
el.addEventListener("remote_play",play_func);
el.addEventListener("remote_pause",pause_func);
el.addEventListener("remote_stop",stop_func);
play_button.addEventListener(edown, function(evt) {
try {
play_func();
spacedeck.presenter_send_media_action(a._id,"video","play",video.currentTime);
} catch (e) {
// catch InvalidStateError
}
}, false);
pause_button.addEventListener(edown, function(evt) {
pause_func();
spacedeck.presenter_send_media_action(a._id,"video","pause",video.currentTime);
}, false);
stop_button.addEventListener(edown, function(evt) {
stop_func();
spacedeck.presenter_send_media_action(a._id,"video","stop",0);
}, false);
}
});
Vue.directive('audioplayer', {
update: function (a) {
var el = this.el;
var scope = this.vm.$root;
var play_button = el.querySelectorAll(".play")[0];
var pause_button = el.querySelectorAll(".pause")[0];
var stop_button = el.querySelectorAll(".stop")[0];
var timeline = el.querySelectorAll(".timeline")[0];
var set_inpoint = el.querySelectorAll(".set-inpoint")[0];
var set_outpoint = el.querySelectorAll(".set-outpoint")[0];
var reset_points = el.querySelectorAll(".reset-points")[0];
var player_state = "stop";
var play_from = 0.0;
var play_to = 0.0;
var audio = el.querySelectorAll("audio")[0];
var update_markers = function() {
try {
if (a.meta) {
if (!a.meta.play_to) a.meta.play_to = audio.duration;
play_from = parseFloat(a.meta.play_from) || 0.0;
play_to = parseFloat(a.meta.play_to) || 0.0;
} else {
play_from = 0.0;
play_to = parseFloat(audio.duration) || 0.0;
a.meta = {};
}
} catch (e) {
// catch InvalidStateError
}
}
var update_view = function() {
try {
if (!a.player_view) { a.player_view = {} };
a.player_view.state = player_state;
a.player_view.total_time_string = format_time(audio.duration);
a.player_view.current_time_string = format_time(audio.currentTime);
a.player_view.current_time_float = audio.currentTime/audio.duration;
a.player_view.inpoint_float = play_from/audio.duration;
a.player_view.outpoint_float = play_to/audio.duration;
a.player_view.duration = audio.duration;
} catch (e) {
// catch InvalidStateError
}
}
var pause_audio = function() {
try {
audio.pause();
player_state = "paused";
} catch (e) {
// catch InvalidStateError
}
update_view();
}
var stop_audio = function() {
try {
audio.currentTime = play_from;
audio.pause();
player_state = "stop";
} catch (e) {
// catch InvalidStateError
}
update_view();
}
update_view();
audio.addEventListener("loadedmetadata", function(evt) {
update_markers();
update_view();
}, false);
audio.addEventListener("timeupdate", function(evt) {
try {
update_markers();
if (audio.currentTime >= play_to && player_state == "playing") stop_audio();
update_view();
} catch (e) {
// catch InvalidStateError
}
}, false);
var play_func = function() {
if (player_state == "stop") {
audio.currentTime = play_from;
}
player_state = "playing";
update_markers();
audio.play();
update_view();
}
var pause_func = function() {
pause_audio();
update_view();
}
var stop_func = function() {
stop_audio();
update_view();
}
el.addEventListener("remote_play",play_func);
el.addEventListener("remote_pause",pause_func);
el.addEventListener("remote_stop",stop_func);
play_button.addEventListener(edown, function(evt) {
try {
play_func();
spacedeck.presenter_send_media_action(a._id,"audio","play",audio.currentTime);
} catch (e) {
// catch InvalidStateError
}
}, false);
pause_button.addEventListener(edown, function(evt) {
pause_func();
spacedeck.presenter_send_media_action(a._id,"audio","pause",audio.currentTime);
}, false);
stop_button.addEventListener(edown, function(evt) {
stop_func();
spacedeck.presenter_send_media_action(a._id,"audio","stop",0);
}, false);
timeline.addEventListener(edown, function(evt) {
var clicked_time = (parseFloat(evt.offsetX)/evt.currentTarget.offsetWidth)*audio.duration;
if (isNaN(clicked_time)) {
clicked_time = 0.0;
}
try {
audio.currentTime = clicked_time;
} catch (e) {
// catch InvalidStateErrors
}
}, false);
set_inpoint.addEventListener(edown, function(evt) {
if (!a.meta) a.meta = {};
a.meta.play_from = audio.currentTime;
if (a.meta.play_to<a.meta.play_from) a.meta.play_to = audio.duration;
update_markers();
stop_audio();
update_view();
scope.save_artifact(a);
}, false);
set_outpoint.addEventListener(edown, function(evt) {
if (!a.meta) a.meta = {};
a.meta.play_to = audio.currentTime;
if (a.meta.play_to<a.meta.play_from) a.meta.play_from = 0.0;
update_markers();
stop_audio();
update_view();
scope.save_artifact(a);
}, false);
reset_points.addEventListener(edown, function(evt) {
if (!a.meta) a.meta = {};
a.meta.play_from = 0.0;
a.meta.play_to = audio.duration;
update_markers();
stop_audio();
update_view();
scope.save_artifact(a);
}, false);
}
});
Vue.directive('sd-richtext', {
twoWay: true,
update: function(obj) {
this.mode = 'rich';
$(this.el).addClass("text-editing");
this.medium = new Medium({
element: this.el,
mode: Medium.richMode,
attributes: {
remove: ['class','href','onclick','onmousedown','onmouseup']
},
});
this.medium.value(obj.description);
this.medium.element.addEventListener('keyup', function() {
obj.description = this.medium.value();
spacedeck.queue_artifact_for_save(obj);
}.bind(this));
spacedeck.medium_for_object[obj._id] = this.medium;
}
});
Vue.directive('focus', {
bind: function () {
var el = this.el;
window.setTimeout(function() {
if (el.contentEditable && el.contentEditable!="inherit") {
var range = document.createRange();
range.selectNodeContents(el);
} else {
el.focus();
el.select();
}
}, 500);
},
});
Vue.directive('sd-draggable', {
update: function(data) {
var el = this.el;
el.addEventListener(
'dragstart',
function(evt) {
if ($(el).find(".text-editing").length) {
// FIXME: technical debt
evt.stopPropagation();
evt.preventDefault();
return;
}
evt.dataTransfer.setData('application/json', JSON.stringify(data));
$(el).addClass("dragging");
},
false
);
}
});
Vue.directive('sd-droppable', {
isFn: true,
bind: function() {
var el = this.el;
var expression = this.expression;
var parts = expression.split(";");
var func_key = parts[0];
var data_key = parts[1];
el.addEventListener(
'dragover',
function(e) {
e.dataTransfer.dropEffect = 'copy';
// allows us to drop
if (e.preventDefault) e.preventDefault();
el.classList.add('over');
return false;
}.bind(this),
false
);
el.addEventListener(
'dragenter',
function(e) {
el.classList.add('over');
return false;
}.bind(this),
false
);
el.addEventListener(
'dragleave',
function(e) {
el.classList.remove('over');
return false;
},
false
);
el.addEventListener(
'drop',
function(e) {
e.stopPropagation();
e.preventDefault();
$(e.currentTarget).find(".over").removeClass('over');
$(e.currentTarget).find(".dragging").removeClass('dragging');
var func = this.vm.$root[func_key].bind(this.vm.$root);
if (this._scope) {
var obj = this._scope[data_key];
} else {
var obj = this.vm[data_key];
}
func(e, obj);
return false;
}.bind(this),
false
);
}
});
Vue.directive('sd-fader', {
bind: function (section) {
function clamp(v, mn, mx) {
return Math.max(mn,Math.min(mx,v));
}
var scope = this.vm.$root;
this.fader_state = "idle";
this.fader_mx = 0;
this.fader_my = 0;
var $el = $(this.el);
var handle = $el.find(".fader-selector");
var indicator = $el.find(".fader-indicator");
var constraint = $el.find(".fader-constraint");
if (!constraint.length) constraint = $el;
var fader_var_x = $el.attr("sd-fader-var-x");
var fader_var_y = $el.attr("sd-fader-var-y");
var knob_size = 0;
var minx = 0;
var miny = 0;
var maxx = 0;
var maxy = 0;
var nx = 0;
if (xfader) nx = scope.$get(fader_var_x);
var ny = 0;
if (yfader) ny = scope.$get(fader_var_y);
var xfader = !!fader_var_x;
var yfader = !!fader_var_y;
var encoder = !handle[0];
var step = parseFloat($el.attr("sd-fader-step"))||1;
var sensitivity = parseFloat($el.attr("sd-fader-sens"))||1;
var discover_minmax = function() {
minx = (parseInt($el.attr("sd-fader-min-x"))||0);
miny = (parseInt($el.attr("sd-fader-min-y"))||0);
maxx = parseInt($el.attr("sd-fader-max-x"))||(constraint.width() - 1);
maxy = parseInt($el.attr("sd-fader-max-y"))||(constraint.height() - 1);
}
var position_handle = function() {
discover_minmax();
if (!nx || isNaN(nx)) nx = 0;
if (!ny || isNaN(ny)) ny = 0;
if (handle[0]) {
if (xfader) handle[0].style.left = nx+"px";
if (yfader) handle[0].style.top = (maxy-ny)+"px";
}
if (indicator[0]) {
indicator[0].style.height = ny+"px";
}
}.bind(this);
var move_handle = function(dx,dy) {
discover_minmax();
if (xfader) {
nx = clamp(dx, minx, maxx);
scope.$set(fader_var_x, nx);
}
if (yfader) {
ny = clamp(dy, miny, maxy);
if (step<1) ny = ny.toFixed(1); // float precision hack
scope.$set(fader_var_y, ny);
}
}.bind(this);
var handle_move = function(evt) {
evt = fixup_touches(evt);
var dx = parseInt((evt.pageX - this.fader_mx) * sensitivity);
var dy = parseInt((evt.pageY - this.fader_my) * sensitivity);
dx *= step;
dy *= step;
move_handle(this.fader_oldx+dx,this.fader_oldy-dy);
}.bind(this);
var handle_up = function(evt) {
this.fader_state = "idle";
$("body").off(emove, handle_move);
$("body").off("mouseleave "+eup+" blur", handle_up);
window._sd_fader_moving = false; // signal for other event systems
}.bind(this);
function prevent_event(evt) {
evt.preventDefault();
evt.stopPropagation();
};
$el.on(edown,function(evt) {
evt.preventDefault();
evt.stopPropagation();
evt = fixup_touches(evt);
var offset = $(evt.target).offset();
this.fader_state = "drag";
if (!encoder) {
move_handle(evt.pageX-offset.left, maxy - (evt.pageY - offset.top) + knob_size/2);
}
if (yfader) {
ny = scope.$get(fader_var_y);
}
$("body").on(emove, handle_move);
$("body").on("mouseleave "+eup+" blur", handle_up);
this.fader_mx = evt.pageX;
this.fader_my = evt.pageY;
this.fader_oldx = nx||0;
this.fader_oldy = ny||0;
window._sd_fader_moving = true; // signal for other event systems
}.bind(this));
// initial state
position_handle();
if (xfader) {
scope.$watch(fader_var_x, function(a) {
nx = parseInt(scope.$get(fader_var_x));
position_handle();
});
}
if (yfader) {
scope.$watch(fader_var_y, function(a) {
ny = parseInt(scope.$get(fader_var_y));
position_handle();
});
}
},
unbind: function() {
var scope = this.vm.$root;
var $el = $(this.el);
var fader_var_x = $el.attr("sd-fader-var-x");
var fader_var_y = $el.attr("sd-fader-var-y");
}
});
}

View File

@@ -0,0 +1,20 @@
/*
SpacedeckFormatting
This module contains functions dealing with Rich Text Formatting.
*/
var SpacedeckFormatting = {
apply_formatting: function(section, cmd, arg1, arg2) {
console.log("apply_formatting: ",section,cmd);
var scribe = _scribe_handle_for_object[section._id];
var command = scribe.getCommand(cmd);
if (cmd == 'createLink') {
arg1 = prompt("Link URL?");
}
scribe.el.focus();
command.execute(arg1,arg2);
}
}

View File

@@ -0,0 +1,101 @@
var SpacedeckModals = {
data: {
active_modal: null,
active_account_section: "user",
active_space_profile_section: null,
account_sections: [
{
id: "user",
title: "Profile",
icon: "icon-user",
},
{
id: "language",
title: "Language",
icon: "icon-globe",
},
{
id: "email-notifications",
title: "Notifications",
icon: "icon-bell",
},
{
id: "reset-password",
title: "Password",
icon: "icon-lock-closed",
},
{
id: "remove-account",
title: "Terminate",
icon: "icon-logout",
}
],
folder_profile_sections: [
{
id: "editors",
title: "Editors",
icon: "icon-user-group",
count: 1
},
{
id: "visibility",
title: "Visibility",
icon: "icon-eye-open",
count: 1
}
],
space_profile_sections: [
{
id: "comments",
title: "Comments",
icon: "icon-messages",
count: 1
},
{
id: "history",
title: "History",
icon: "icon-history",
count: 1
},
{
id: "editors",
title: "Editors",
icon: "icon-user-group",
count: 1
},
{
id: "visibility",
title: "Visibility",
icon: "icon-eye-open",
count: 1
}
]
},
methods: {
activate_modal: function(id) {
this.active_modal = id;
if (id == "folder-settings") {
this.access_settings_space = this.active_folder;
this.access_settings_memberships = this.active_space_memberships;
this.editors_section = "list";
}
},
close_modal: function() {
this.active_modal = null;
},
activate_account_section: function(section_id) {
this.active_account_section = section_id;
},
activate_space_profile_section: function(section_id) {
this.active_space_profile_section = section_id;
}
}
}

View File

@@ -0,0 +1,326 @@
/*
SpacedeckRoutes
This module contains functions dealing with Routing and View Switching.
*/
var SpacedeckRoutes = {
internal_route: function(path, on_success) {
if(!this.router) {
this.router = new RouteRecognizer();
this.router.add([
{
path: "/spaces/:id",
handler: function(params, on_success) {
this.load_space(params.id, on_success);
}.bind(this)
}
]);
this.router.add([
{
path: "/confirm/:token",
handler: function(params) {
if (!this.logged_in) {
this.redirect_to("/login");
} else {
this.confirm_account(params.token);
}
}.bind(this)
}
]);
this.router.add([
{
path: "/password-confirm/:token",
handler: function(params) {
console.log(params.token);
if (this.logged_in) {
this.redirect_to("/spaces");
} else {
this.reset_token = params.token;
this.active_view = "password-confirm";
}
}.bind(this)
}
]);
this.router.add([
{
path: "/password-reset",
handler: function(params, test) {
if (this.logged_in) {
} else {
this.active_view = "password-reset";
}
}.bind(this)
}
]);
this.router.add([
{
path: "/accept/:membership_id",
handler: function(params, test) {
if (this.logged_in) {
var invitation_token = get_query_param("code");
accept_invitation(params.membership_id, invitation_token , function(m) {
window._spacedeck_location_change = true;
location.href = "/spaces/"+m.space._id;
}.bind(this), function(xhr) {
smoke.alert("Error ("+xhr.status+")", function() {
this.redirect_to("/spaces");
}.bind(this));
}.bind(this));
} else {
this.redirect_to("/login");
}
}.bind(this)
}
]);
this.router.add([
{
path: "/signup",
handler: function(params) {
var invitation_token = get_query_param("code");
if (invitation_token) {
this.invitation_token = invitation_token;
}
if (this.logged_in) {
this.redirect_to("/spaces");
} else {
this.active_view = "signup";
}
}.bind(this)
}
]);
this.router.add([
{
path: "/login",
handler: function(params) {
if (this.logged_in) {
if(this.invitation_token) {
accept_invitation(this.accept_invitation, function(m) {
window._spacedeck_location_change = true;
location.href = "spaces/"+m.space_id;
}.bind(this), function(xhr) { console.error(xhr); });
} else {
this.redirect_to("/spaces");
}
} else {
this.active_view = "login";
token = get_query_param("code");
if (token) {
this.login_with_token(token);
}
}
}.bind(this)
}
]);
this.router.add([
{
path: "/logout",
handler: function(params) {
if (this.logged_in) {
this.logout(function(m) {
this.redirect_to("/login");
}.bind(this), function(xhr) { console.error(xhr); });
} else {
this.redirect_to("/login");
}
}.bind(this)
}
]);
this.router.add([
{
path: "/spaces",
handler: function(params) {
if (!this.logged_in) {
window._spacedeck_location_change = true;
location.href = "/login";
} else {
if (this.logged_in && this.user.home_folder_id) {
this.load_space(this.user.home_folder_id);
} else {
location.href = "/";
}
}
}.bind(this)
}
]);
this.router.add([
{
path: "/account",
handler: function(params) {
if (!this.logged_in) {
window._spacedeck_location_change = true;
location.href = "/";
} else {
this.active_view = "account";
this.load_subscription();
}
}.bind(this)
}
]);
this.router.add([
{
path: "/team",
handler: function(params) {
if (!this.logged_in) {
window._spacedeck_location_change = true;
location.href = "/";
} else {
this.active_view = "team";
this.load_team();
}
}.bind(this)
}
]);
this.router.add([
{
path: "/folders/:id",
handler: function(params) {
this.load_space(params.id, null, function(xhr) {
// on_error
console.log("couldn't load folder: "+xhr.status);
this.redirect_to("/spaces", function(){});
}.bind(this));
}.bind(this)
}
]);
this.router.add([
{
path: "/",
handler: function(params) {
location.href = "/";
}.bind(this)
}
]);
this.router.add([
{
path: "/terms",
handler: function(params) {
location.href = "/terms";
}.bind(this)
}
]);
this.router.add([
{
path: "/privacy",
handler: function(params) {
location.href = "/privacy";
}.bind(this)
}
]);
}
var foundRoute = this.router.recognize(path);
if (foundRoute) {
foundRoute[0].handler(foundRoute[0].params, on_success);
} else {
location.href = "/not_found";
}
},
route: function() {
window.onpopstate = function (event) {
event.preventDefault();
this.internal_route(location.pathname);
}.bind(this);
$("body").on("click", "a", function(event) {
// #hash
if (event.currentTarget.hash && event.currentTarget.hash.length>1) return;
console.log("clicked", event.currentTarget.pathname);
// external link?
if (event.currentTarget.host != location.host) return;
// modifier keys?
if (event.metaKey || event.ctrlKey || event.shiftKey) return;
// /t/ path
if (event.currentTarget.pathname.match(/^\/t\//)) return;
this.internal_route(event.currentTarget.pathname);
history.pushState(null, null, event.currentTarget.pathname);
event.preventDefault();
}.bind(this));
if (location.host!=ENV.webHost) {
if (!subdomainTeam) {
location.href = ENV.webEndpoint;
return;
} else {
if(subdomainTeam.subdomain) {
var realHost = (subdomainTeam.subdomain + "." + ENV.webHost);
if (location.host != realHost) {
location.href = realHost;
return;
}
} else {
location.href = ENV.webEndpoint;
return;
}
}
}
if (this.logged_in) {
if (this.user.team) {
if (this.user.team.subdomain && this.user.team.subdomain.length > 0) {
var realHost = (this.user.team.subdomain + "." + ENV.webHost);
if (location.host != realHost) {
location.href = location.protocol + "//" + realHost + location.pathname;
return;
}
}
}
}
this.internal_route(location.pathname);
},
open_url: function(url) {
window.open(url,'_blank');
},
redirect_to: function(path, on_success) {
if (on_success) {
this.internal_route(path, on_success);
history.pushState(null, null, path);
} else {
window._spacedeck_location_change = true;
location.href = path;
}
},
link_to_parent_folder: function(space_id) {
return "/folders/"+space_id;
},
link_to_space: function(space) {
return "/"+space.space_type+"s/"+space._id;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,973 @@
/*
SpacedeckSpaces
This module contains functions dealing with Spaces UI.
*/
var SpacedeckSpaces = {
data: {
active_space: {advanced:{}},
active_space_loaded: false,
active_space_role: "viewer",
active_space_version_dirty: true,
active_space_messages: [],
active_space_memberships: [],
active_folder_history_items: [],
active_space_users: [],
active_space_artifacts: [],
active_space_path: [],
access_settings_space: null,
access_settings_memberships: [],
duplicate_folders: [],
duplicate_folder_id: "",
pending_pdf_files: [],
meta_visible: false,
meta_unseen: 0,
present_mode: false,
space_editing_title: false,
create_space_title: "",
folder_reverse: 1,
embedded: false,
remix_cta: "Create Reply",
publish_cta: "Publish",
remix_copying: true,
remix_style: "",
guest_signup_enabled: false,
space_embed_html: "",
share_base: location.origin,
share_base_url: location.origin+"/spaces/",
share_base_url_enc: encodeURIComponent(location.origin+"/spaces/"),
social_bar: true,
can_add_comment: false,
space_info_section: "access",
editors_section: "list",
selected_member: null,
invite_member_role: 'viewer',
invite_email_error: null,
invite_email: "",
invite_message: "",
active_join_link: "",
join_link_role: "viewer",
mouse_state: "idle",
// folders
active_folder: null,
folder_sorting: "updated_at",
folder_spaces_filter: null,
active_path_length : 0,
space_comment: "",
folder_spaces_search: "",
// map of artifact IDs to medium rich text editor objects
medium_for_object: {},
},
methods: {
search_spaces: function() {
var query = this.folder_spaces_search;
console.log("search query: ",query);
load_spaces_search(query, function(spaces) {
console.log("results: ",spaces);
this.active_profile_spaces = spaces;
}.bind(this));
},
guest_logout: function() {
if ("localStorage" in window && localStorage) {
delete localStorage['guest_nickname'];
}
this.guest_nickname = "";
location.reload();
},
ask_guestname: function(dft, cb) {
console.log("ask_guestname");
var team_name = "Spacedeck";
if(subdomainTeam) {
team_name = subdomainTeam.name;
}
smoke.prompt(__('what_is_your_name', team_name) , function(content) {
if (!content || (content.length === 0)) {
this.ask_guestname(dft, cb);
} else {
this.guest_nickname = content;
if ("localStorage" in window && localStorage) {
try {
localStorage['guest_nickname'] = this.guest_nickname;
}catch(e) {
console.error(e);
}
}
if (cb) cb();
}
}.bind(this), {value: dft || "Guest "+parseInt(10000*Math.random()), ok: __("ok"), cancel: __("cancel")});
},
load_space: function(space_id, on_success, on_error) {
console.log("load space: ", space_id);
this.folder_spaces_filter="";
this.folder_spaces_search="";
space_auth = get_query_param("spaceAuth");
var userReady = function() {
if (get_query_param("embedded")) {
this.embedded = true;
this.guest_signup_enabled = true;
if (get_query_param("publish_cta")) {
this.publish_cta = get_query_param("publish_cta");
}
if (get_query_param("nosocial")) {
this.social_bar = false;
}
}
if (get_query_param("confirm") && this.logged_in) {
var token = get_query_param("confirm");
confirm_user(this.user, token, function() {
this.redirect_to("/spaces/"+space_id+"?show_access=1");
}.bind(this), function() {
alert("An error occured confirming your email with the given token.");
});
return;
}
this.close_dropdown();
this.active_space_loaded = false;
this.viewport_zoom = 1;
this.viewport_zoom_percent = 100;
this.loading_space_id = space_id;
this.present_mode = false;
this.active_space_is_readonly = true;
this.opened_dialog = "none";
this.open_space_dialog = "none";
this.selected_artifacts_dict = {};
this.update_selection_metrics();
this.can_add_comment = false;
var is_home = false;
if (this.user) {
is_home = (space_id == this.user.home_folder_id);
}
document.title = "Loading… | Spacedeck";
load_space(space_id, function(space, role) {
document.title = space.name;
this.active_space_role = role || "viewer"; //via req header from backend
this.space_embed_html = "<iframe width=\"1024\" height=\"768\" seamless src=\""+ENV.webEndpoint+"/spaces/"+space._id+"?embedded=1\"></iframe>";
if (!is_home) {
//console.log(space);
load_members(space, function(members) {
this.active_space_memberships = members;
}.bind(this));
}
console.log("[websocket] auth start");
if (space.space_type == "folder") {
this.active_space = {advanced:{}};
document.title = "Spacedeck";
load_spaces(space_id, is_home, function(spaces) {
space.children = spaces;
this.loading_space_id = null;
this.active_profile_spaces = space.children;
this.active_folder = space;
this.access_settings_space = space;
this.auth_websocket(this.active_folder);
this.load_space_path(this.active_folder);
if (is_home) {
this.root_folder = space;
}
load_history(space, function(history) {
console.log("loaded digest", history);
this.active_folder_history_items = history;
this.meta_unseen = 0;
if ("localStorage" in window && localStorage) {
var last_seen = parseInt(localStorage[this.meta_last_seen_key()], 10);
} else {
var last_seen = 0;
}
for (var i=0; i<history.length; i++) {
var item = history[i];
var t = new Date(item.last_action).getTime();
var my_own = false;
if (item.users.length==1 && item.users[0]=="you") {
my_own = true;
}
if (t>last_seen && !my_own) this.meta_unseen++;
}
}.bind(this));
this.active_view = "folders";
}.bind(this));
if ("localStorage" in window) {
var key = "folder_sorting_"+space_id;
var key2 = "folder_reverse_"+space_id;
if (localStorage[key] && localStorage[key2]) {
this.folder_sorting = localStorage[key];
this.folder_reverse = parseInt(localStorage[key2]);
console.log("loaded folder sorting: ",this.folder_sorting,this.folder_reverse);
}
}
// legacy fix
if (this.folder_sorting == "opened_at") {
this.folder_sorting = "name";
}
} else if (space.space_type == "space") {
this.artifacts = [];
this.loading_space_id = null;
document.title = space.name;
if (space_auth || this.logged_in) {
this.can_add_comment = true;
}
this.setup_watches();
load_artifacts(space._id, function(artifacts) {
// FIXME: how to cleanly handle this error?
if (!artifacts) {
artifacts = [];
}
// FIXME: remove kludge
for (var i=0; i<artifacts.length; i++) {
this.update_board_artifact_viewmodel(artifacts[i]);
}
this.active_space_artifacts = artifacts;
this.$set("active_space", space);
this.active_space = space;
this.auth_websocket(this.active_space);
this.active_view = "space";
this.fixup_space_size();
if (space._id != this.active_space._id) {
this.present_mode = true;
this.active_space_is_readonly = true;
} else {
this.active_space_is_readonly = false;
}
this.discover_zones();
window.setTimeout(function() {
this.zoom_to_fit();
}.bind(this),10);
if (on_success) {
on_success();
}
this.active_space_loaded = true;
this.extract_properties_from_selection(); // populates zones etc
load_comments(space._id, function(messages) {
if (!messages) messages = [];
this.active_space_messages = messages;
this.refresh_space_comments();
}.bind(this), function(xhr) { console.error(xhr); });
}.bind(this));
if (this.active_space_role == "editor" || this.active_space_role == "admin") {
this.present_mode = false;
this.active_space_is_readonly = false;
}
// FIXME
this.active_join_link = "";
this.join_link_role = "viewer";
// FIXME
if (this.active_space_role == "admin") {
this.space_info_section="access";
} else if (this.active_space_role == "editor") {
//this.space_info_section="versions";
} else {
this.space_info_section="info";
}
}
}.bind(this), function(xhr) {
if (on_error) {
return on_error(xhr);
}
if (xhr.status == 403) {
if (!this.logged_in) {
this.redirect_to("/login?space_id="+space_id);
} else {
this.redirect_to("/");
}
} else {
this.redirect_to("/not_found");
console.error(xhr);
}
}.bind(this));
}.bind(this);
var default_guest = "";
if (("localStorage" in window && localStorage) && localStorage['guest_nickname']) {
this.guest_nickname = localStorage['guest_nickname'];
default_guest = this.guest_nickname;
userReady();
}
if (space_auth) {
if (this.guest_nickname) {
userReady();
} else {
this.ask_guestname(default_guest, function() {
userReady();
});
}
} else {
this.guest_nickname = "";
userReady();
}
},
refresh_space_comments: function() {
this.meta_unseen = 0;
var messages = this.active_space_messages;
var last_seen = 0;
if ("localStorage" in window && localStorage) {
last_seen = parseInt(localStorage[this.meta_last_seen_key()], 10);
}
for (var i=0; i<messages.length; i++) {
var item = messages[i];
var t = new Date(item.updated_at).getTime();
var my_own = false;
if (this.user && this.user._id!=item.user_id && !item.editor_name) {
my_own = true;
}
if (t>last_seen && !my_own) this.meta_unseen++;
}
},
go_to_next_space: function() {
var space_ids = this.active_folder.children.map(function(s){return s._id});
var idx = space_ids.indexOf(this.active_space._id);
console.log("index: ",idx);
var cur_idx = idx;
var done = false;
while (!done) {
var next = this.active_folder.children[(idx+1)%space_ids.length];
if (next.space_type == "folder") {
done = false;
idx++;
} else {
done = true;
}
if (cur_idx == idx) done = true; // wraparound
}
this.load_space(next._id);
},
go_to_previous_space: function() {
var space_ids = this.active_folder.children.map(function(s){return s._id});
var idx = space_ids.indexOf(this.active_space._id);
console.log("index: ",idx);
var cur_idx = idx;
var done = false;
while (!done) {
var idx = (idx<1?space_ids.length:idx)-1;
var prev = this.active_folder.children[idx];
if (prev.space_type == "folder") {
done = false;
idx--;
} else {
done = true;
}
if (cur_idx == idx) done = true; // wraparound
}
this.load_space(prev._id);
},
filtered_folder_children: function(type){
var type = type || "space";
return _.filter(this.active_folder.children, function(s){
return s.space_type == type;
})
},
load_space_path: function(space) {
if (!space) return [];
load_space_path(space._id, function(path) {
this.active_space_path = path;
}.bind(this), function() { console.log("could not load folder path")});
},
is_active_space_role: function(rolename) {
if(!this.active_space) return false;
return this.active_space_role == rolename;
},
create_space: function(space_type) {
if (!this.active_folder) return;
this.close_modal();
this.folder_spaces_filter="";
if (!this.active_folder.children) {
this.active_folder.children = [];
}
if (!space_type) space_type = "space";
var s = {
name: space_type == "space" ? __("untitled_space") : __("untitled_folder") ,
artifacts: [],
space_type: space_type,
parent_space_id: this.active_folder._id
};
if (this.create_space_title.length) {
s.name = this.create_space_title;
}
save_space(s, function(saved_space) {
this.active_folder.children.push(saved_space);
if (space_type != "folder") {
this.redirect_to("/"+saved_space.space_type+"s/"+saved_space._id, function(succ) {
});
} else {
this.rename_folder(saved_space);
}
}.bind(this), function(xhr) {
alert("Error: Could not create Space ("+xhr.status+").");
}.bind(this));
},
save_space: function(s) {
save_space(s);
},
create_space_version: function() {
if (!this.is_pro(this.user)) {
// pro feature
smoke.confirm(__("spacedeck_pro_ad_versions"), function(confirmed) {
if (confirmed) this.show_upgrade_modal();
}.bind(this));
return;
}
this.version_saving = true;
this.present_mode = false;
var s = this.active_space.draft_space;
console.log("create_space_version:", s);
duplicate_space(s, null, function(new_version_space) {
load_spaces(this.active_space._id, false, function(space) {
this.version_saving = false;
this.activate_space_version(space, space.draft_space);
alert("Version saved.");
}.bind(this));
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
finalize_folder_profile_edit: function() {
save_space(this.active_folder, function(saved_space) {
this.close_modal();
}.bind(this));
},
finalize_space_profile_edit: function() {
save_space(this.active_space, function(saved_space) {
this.close_modal();
}.bind(this));
},
delete_space: function(space) {
smoke.confirm("Really delete "+space.name+"?", function(confirmed) {
if (!confirmed) return;
var idx = this.active_folder.children.indexOf(space);
delete_space(space, function() {
if (space.parent_space_id){
this.redirect_to("/folders/"+space.parent_space_id, function(succ) {});
} else {
this.redirect_to("/spaces", function(succ) {});
}
this.close_modal();
this.active_folder.children.splice(idx,1);
}.bind(this));
}.bind(this));
},
duplicate_space: function(space) {
duplicate_space(space, null, function(new_space) {
//alert("Space duplicated.");
this.active_folder.children.push(new_space);
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
remove_avatar: function(space) {
remove_avatar_file("space", space, function(s) {
this.active_space = s;
}.bind(this));
},
rename_space: function(space) {
this.close_dropdown();
if (space.space_type == "folder") return this.rename_folder(space);
smoke.prompt(__("new_space_title"), function(title) {
if (title && title.length) {
space.name = title;
save_space(space);
}
}.bind(this), {value: space.name});
},
rename_folder: function(folder) {
this.close_dropdown();
smoke.prompt(__("new_folder_title"), function(title) {
if (title && title.length) {
folder.name = title;
save_space(folder);
}
}.bind(this), {value: folder.name});
},
edit_space_title: function() {
this.close_dropdown();
if (this.active_space_role=="editor" || this.active_space_role=="admin") {
this.space_editing_title = true;
$("#space-title").focus();
}
},
save_space_title: function(name) {
this.active_space.name = name;
save_space(this.active_space, function() {
this.space_editing_title = false;
}.bind(this));
},
save_space_keydown: function($event) {
if ($event) {
if ($event.keyCode != 13) {
this.space_editing_title = true;
return;
}
$event.preventDefault();
$event.stopPropagation();
$event.target.blur();
}
save_space(this.active_space, function(updated_space) {
this.active_space.edit_slug = updated_space.edit_slug;
this.space_editing_title = false;
}.bind(this));
},
save_space_description: function($event) {
$event.preventDefault();
$event.stopPropagation();
var val = $event.target.innerText;
$event.target.blur();
this.active_space.description = val;
save_space(this.active_space);
},
save_space_domain: function($event) {
$event.preventDefault();
$event.stopPropagation();
var val = $event.target.innerText;
$event.target.blur();
this.active_space.domain = val;
save_space(this.active_space);
},
download_space: function() {
smoke.quiz(__("download_space"), function(e, test) {
if (e == "PDF"){
this.download_space_as_pdf(this.active_space);
}else if (e == "ZIP"){
this.download_space_as_zip(this.active_space);
}else if (e == "TXT"){
this.download_space_as_list(this.active_space);
}
}.bind(this), {
button_1: "PDF",
button_2: "ZIP",
button_3: "TXT",
button_cancel:__("cancel")
});
},
download_space_as_png: function(space) {
window.open(ENV.apiEndpoint + "/api/spaces/" + space._id + "/png");
},
download_space_as_pdf: function(space) {
this.global_spinner = true;
get_resource("/spaces/" + space._id + "/pdf", function(o) {
this.global_spinner = false;
location.href = o.url;
}.bind(this), function(xhr) {
this.global_spinner = false;
alert("PDF export problem (" + xhr.status + ").");
}.bind(this));
},
download_space_as_zip: function(space) {
this.global_spinner = true;
get_resource("/spaces/" + space._id + "/zip", function(o) {
this.global_spinner = false;
location.href = o.url;
}.bind(this), function(xhr) {
this.global_spinner = false;
alert("ZIP export problem (" + xhr.status + ").");
}.bind(this));
},
download_space_as_list: function(space) {
this.global_spinner = true;
location.href = "/api/spaces/" + space._id + "/list";
},
duplicate_space_into_folder: function() {
load_writable_folders( function(folders){
this.duplicate_folders = _.sortBy(folders, function (folder) { return folder.name; });
}.bind(this), function(xhr) {
console.error(xhr);
});
},
duplicate_folder_confirm: function() {
var folderId = this.duplicate_folder_id;
var idx = _.findIndex(this.duplicate_folders, function(s) { return s._id == folderId;});
if (idx<0) idx = 0;
var folder = this.duplicate_folders[idx];
console.log("df f",folder);
if (!folder) return;
duplicate_space(this.active_space, folder._id, function(new_space) {
this.duplicate_folders = [];
this.duplicate_folder = null;
smoke.quiz(__("duplicate_success", this.active_space.name, folder.name), function(e, test){
if (e == __("goto_space", new_space.name)){
this.redirect_to("/spaces/" + new_space._id);
}else if (e == __("goto_folder", folder.name)){
this.redirect_to("/folders/" + folder._id);
}
}.bind(this), {
button_1: __("goto_space", new_space.name),
button_2: __("goto_folder", folder.name),
button_cancel:__("stay_here")
});
}.bind(this), function(xhr){
console.error(xhr);
smoke.prompt("error: " + xhr.statusText);
}.bind(this));
},
toggle_follow_mode: function() {
this.deselect();
this.follow_mode = !this.follow_mode;
},
toggle_present_mode: function() {
this.deselect();
this.present_mode = !this.present_mode;
if (this.present_mode) {
//this.go_to_first_zone();
}
},
meta_last_seen_key: function() {
var seen_key = "meta-seen-";
if (this.active_view == 'space') {
if (!this.active_space) return "invalid";
seen_key += this.active_space._id;
} else if (this.active_view == 'folders') {
if (!this.active_folder) return "invalid";
seen_key += this.active_folder._id;
}
return seen_key;
},
toggle_meta: function() {
this.meta_visible = !this.meta_visible;
if (this.meta_visible) {
var seen_key = this.meta_last_seen_key();
if ("localStorage" in window && localStorage) {
localStorage[seen_key] = new Date().getTime();
console.log("seen_key: ",seen_key,localStorage[seen_key]);
this.meta_last_seen = localStorage[seen_key];
}
this.meta_unseen = 0;
}
},
toggle_space_access_mode: function() {
this.access_settings_space.access_mode = (this.access_settings_space.access_mode=="public")?"private":"public";
save_space(this.access_settings_space);
},
save_space_access_mode: function(evt) {
// FIXME really bad that i have to do this manually. why is the
// value not already updated when v-on="change" is fired?!
this.access_settings_space.access_mode = evt.currentTarget.value;
save_space(this.access_settings_space);
},
save_space_editors_locking: function(evt) {
// FIXME same issue as above
this.access_settings_space.editors_locking = evt.currentTarget.checked;
save_space(this.access_settings_space);
},
create_join_link: function() {
create_join_link(this.active_space._id, this.join_link_role, function(result) {
this.active_join_link = "https://"+location.host+"/invitations/"+result.code+"/accept";
}.bind(this));
},
delete_join_link: function() {
get_join_link(this.active_space._id, function(result) {
if (result && result.length) {
delete_join_link(result[result.length-1]._id, function() {
this.active_join_link = "";
}.bind(this));
}
}.bind(this));
},
invite_member: function(space, emails_text, txt, role) {
this.invite_email_error = null;
var emails = emails_text.split(",");
var displayed_success = false;
_.each(emails, function(email) {
email = email.trim();
if (!validateEmail(email)) {
this.invite_email_error = "Please enter a valid address."
return;
}
var m = {
email_invited: email,
personal_message: txt,
role: role
}
create_membership(space, m, function(m) {
this.access_settings_memberships.push(m);
console.log("membership created:", m);
this.editors_section="list";
if (!displayed_success) {
displayed_success = true;
smoke.alert("Invitation(s) sent.");
this.invite_email = "";
this.invite_message = "";
}
}.bind(this), function(xhr){
text = JSON.stringify(xhr.responseText);
smoke.alert("Error: "+text);
}.bind(this));
}.bind(this));
},
update_member: function(space, m, role) {
m.role = role;
save_membership(space, m, function() {
console.log("saved")
}.bind(this), function(xhr) {
console.error(xhr);
}.bind(this));
},
// revoke
remove_member: function(space, m) {
delete_membership(space, m, function() {
this.access_settings_memberships.splice(this.access_settings_memberships.indexOf(m), 1);
}.bind(this), function(xhr) {
console.error(xhr);
}.bind(this));
},
history_back: function() {
window.history.back();
},
create_space_comment: function(comment) {
if (!comment.length) return;
var data = {
space: this.active_space._id,
message: comment,
editor_name: this.guest_nickname,
user: this.user
};
save_comment(this.active_space._id, data, function(comment) {
console.log("comment saved: ",comment.created_at);
this.active_space_messages.push(comment);
this.space_comment = "";
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
remove_space_comment: function(comment) {
delete_comment(this.active_space._id, comment._id, function() {
console.log("comment id:",comment._id);
this.active_space_messages = _.filter(this.active_space_messages, function(c){return c._id!=comment._id;});
}.bind(this), function(xhr){
console.error(xhr);
}.bind(this));
},
emojified_comment: function(comment) {
return twemoji.parse(comment);
},
set_folder_sorting: function(key,reverse) {
this.folder_sorting = key;
this.folder_reverse = reverse?-1:1;
console.log(key, reverse);
if ("localStorage" in window) {
localStorage["folder_sorting_"+this.active_folder._id] = this.folder_sorting;
localStorage["folder_reverse_"+this.active_folder._id] = this.folder_reverse;
}
},
activate_space_info_section: function(id) {
this.space_info_section = id;
this.editors_section = "list";
if (id == "versions") {
// load space versions
load_spaces(this.active_space._id, null, function(space_with_children) {
this.active_space.children = space_with_children.children;
console.log("loaded: ",space_with_children);
}.bind(this));
} else if (id == "info") {
// load replies
}
},
handle_folder_drop: function(evt, dest) {
try {
var source = JSON.parse(evt.dataTransfer.getData("application/json"));
} catch (e) {
return;
}
if (!source || !source._id || !source.parent_space_id || !dest._id) return;
if (source._id==dest._id) return;
if (dest.space_type!="folder") {
alert("Spaces can only be moved into folders.");
return;
}
source.parent_space_id = dest._id;
save_space(source, function() {
var idx = _.findIndex(this.active_folder.children, function(s) { return s._id == source._id;});
if (idx>=0) {
this.active_folder.children.splice(idx,1);
console.log("spliced: ",idx);
}
}.bind(this));
},
activate_access: function() {
this.activate_modal("access");
//this.meta_visible = false;
if (this.active_space._id) {
this.access_settings_space = this.active_space;
} else if (this.active_folder && this.active_folder._id) {
this.access_settings_space = this.active_folder;
} else {
return;
}
this.access_settings_memberships = this.active_space_memberships;
},
close_access: function() {
this.close_modal();
},
show_offline_help: function() {
smoke.confirm(__('was_offline'), function(confirmed) {
if (!confirmed) return;
location.reload();
});
}
}
}

View File

@@ -0,0 +1,127 @@
/*
SpacedeckTeams
This module contains functions dealing with Teams.
*/
var SpacedeckTeams = {
data: {
team_members: [],
team_loading: false,
team_logo: "",
team_emails: "",
team_email_invited: false,
team_plan_calculation: "",
},
methods: {
is_admin: function(user) {
return _.filter(user.team.admins, function(admin_id) {
return admin_id == user._id;
}).length > 0;
},
calculate_team: function() {
this.team_plan_calculation = "";
},
load_team: function() {
if (this.user.team) {
load_resource("GET", "/teams/" + this.user.team._id + "/memberships", null, function(members) {
this.team_members = members;
this.calculate_team();
}.bind(this), function(xhr, textStatus, errorThrown) {
console.log(xhr, textStatus, errorThrown);
});
}
},
team_save: function() {
load_resource("PUT", "/teams/" + this.user.team._id , this.user.team, function(res, xhr) {
alert("Team updated.");
}.bind(this), function(xhr) {
console.error(xhr);
alert("Could not update Team.");
});
},
team_update_member: function(m){
load_resource("PUT", "/teams/" + this.user.team._id + "/memberships/" + m._id, m, function(res, xhr) {
console.log("members updated");
}.bind(this), function(xhr) {
console.error(xhr);
});
},
team_invite_members: function(emails) {
var emailList = emails.split(",");
for (_i = 0, _len = emailList.length; _i < _len; _i++) {
email = emailList[_i];
email = email.replace(new RegExp(" ", "g"), "").toLowerCase();
if (validateEmail(email)) {
var data = {
email: email
};
load_resource("POST", "/teams/" + this.user.team._id + "/memberships", data, function(res, xhr) {
this.team_email_invited = true;
this.team_members.push(res);
var timeoutID = window.setTimeout(function(){
this.team_email_invited = false;
}.bind(this), 1000);
this.team_emails = "";
}.bind(this), function(xhr, textStatus, errorThrown) {
console.log(xhr, textStatus, errorThrown);
this.team_invite_error = JSON.parse(xhr.responseText).error;
}.bind(this));
}
}
},
team_promote_member: function(m) {
load_resource("GET", "/teams/" + this.user.team._id + "/memberships/" + m._id + "/promote" , null, function(res, xhr) {
this.load_user(function() {
this.load_team();
}.bind(this));
}.bind(this), function(xhr) {
console.error(xhr);
});
},
team_demote_member: function(m) {
load_resource("GET", "/teams/" + this.user.team._id + "/memberships/" + m._id + "/demote" , null, function(res, xhr) {
this.load_user(function() {
this.load_team();
}.bind(this));
}.bind(this), function(xhr) {
console.error(xhr);
});
},
team_remove_member: function(m) {
if (confirm("Really delete this member?")) {
if (m.user_id && m.state === "active") {
load_resource("DELETE", "/users/" + m._id, null, function(res, xhr) {
var idx = this.team_members.indexOf(m);
this.team_members.splice(idx, 1);
}.bind(this), function(xhr) {
console.error(xhr);
});
} else {
load_resource("DELETE", "/teams/" + this.user.team._id + "/memberships/" + m._id, null, function(res, xhr) {
var idx = this.team_members.indexOf(m);
this.team_members.splice(idx, 1);
}.bind(this), function(xhr) {
console.error(xhr);
});
}
}
}
}
}

View File

@@ -0,0 +1,35 @@
/*
SpacedeckUpdates
This module contains functions dealing with Updates / Notifications.
*/
SpacedeckUpdates = {
user_notifications: [],
updates_items: [],
update_name_for_key: function(key) {
var updates_mapping = {
'space_like': "liked",
'space_comment': "commented in",
'space_follow': "is now following",
'space_publish': "published a new version of"
}
var name = updates_mapping[key];
if(name) return name;
return key;
},
load_updates: function() {
load_notifications(this.user, function(notifications) {
this.user_notifications = notifications;
});
},
activate_updates: function() {
$location.path("/updates");
}
}

View File

@@ -0,0 +1,310 @@
/*
SpacedeckUsers
This module contains functions dealing with Users and Authentication.
*/
SpacedeckUsers = {
data: {
user_forms_email: "",
user_forms_name: "",
invitation_token: null,
login_email: "",
login_password: "",
signup_password: "",
signup_password_confirmation: "",
account_remove_error: null,
loading_user: false,
password_reset_confirm_error: "",
password_reset_error: ""
},
methods:{
load_user: function(on_success, on_error) {
this.loading_user = true;
load_current_user(function(user) {
this.user = user;
this.loading_user = false;
this.logged_in = true;
if (on_success) {
on_success(user);
}
}.bind(this), function() {
// error
this.loading_user = false;
this.logout();
if (on_error) {
on_error();
}
}.bind(this));
},
login_google: function(evt) {
this.loading_user = true;
create_oauthtoken(function(data){
this.loading_user = false;
location.href = data.url;
}, function(xhr){
this.loading_user = false;
alert("could not get oauth token");
});
},
finalize_login: function(session_token, on_success) {
if(!window.socket_auth || window.socket_auth == '' || window.socket_auth == 'null') {
window.socket_auth = session_token;
}
this.load_user(function(user) {
if (this.invitation_token) {
accept_invitation(this.invitation_token, function(memberships){
this.redirect_to("/spaces/"+memberships.space_id);
}.bind(this), function(xhr){
console.error(xhr);
alert("Could not accept invitation. Maybe it was already accepted?");
this.redirect_to("/spaces");
}.bind(this));
} else {
if (on_success) {
on_success(this.user);
} else {
if (get_query_param("space_id") && get_query_param("space_id").length==24) {
this.redirect_to("/spaces/"+get_query_param("space_id"));
} else {
this.redirect_to("/spaces", function() {});
}
}
}
}.bind(this));
},
login_with_token: function(token) {
create_session_for_oauthtoken(token, function(session) {
this.session = session;
this.finalize_login(session.token);
}.bind(this), function(xhr){
// FIXME: handle error
}.bind(this));
},
login_submit: function(email, password, $event, on_success) {
this.loading_user = true;
this.login_error = null;
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
create_session(email, password, function(session) {
console.log("session: ", session);
this.loading_user = false;
this.session = session;
this.finalize_login(session.token, on_success);
}.bind(this), function(req) {
this.loading_user = false;
var msg = "";
if (req.status>=403) {
var msg = "error_unknown_email";
} else {
try {
var msg = "error_"+(JSON.parse(req.responseText).error);
} catch (e) {
var msg = (req.responseText||"Unknown Error.").replace(/,/g," ");
}
}
this.login_error = __(msg);
}.bind(this));
},
login_submit_modal: function(email, password) {
this.login_submit(email, password, null, function() {
location.reload();
});
},
signup_guest: function(on_success) {
},
signup_submit: function($event, name, email, password, password_confirmation, on_success) {
this.creating_user = true;
this.signup_error = null;
if (("localStorage" in window) && localStorage) {
localStorage["sd_api_token"] = null;
}
api_token = null;
if ($event) {
$event.preventDefault();
$event.stopPropagation();
}
create_user(name, email, password, password_confirmation, function(session) {
this.creating_user = false;
this.login_submit(email, password, null, on_success);
}.bind(this), function(req) {
this.creating_user = false;
try {
var msg = "error_"+(JSON.parse(req.responseText).error);
} catch (e) {
var msg = (req.responseText||"Unknown Error.").replace(/,/g," ");
}
var msg = __(msg);
this.signup_error = msg;
}.bind(this));
},
signup_submit_modal: function($event, name, email, password, password_confirmation) {
this.signup_submit($event, name, email, password, password_confirmation, function() {
alert("Success.");
location.reload();
});
},
password_reset_submit: function(evt, email) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
this.password_reset_error = null;
this.password_reset_send = false;
if (email === undefined || email.length < 3) {
this.password_reset_error = "This is not a valid email address";
return;
}
create_password_reset(email, function(parsed,req) {
if(req.status==201) {
this.password_reset_send = true;
}
}.bind(this), function(req) {
console.log(req.status);
if (req.status==404) {
var msg = "error_unknown_email";
} else {
try {
var msg = "error_"+(JSON.parse(req.responseText).error);
} catch (e) {
var msg = (req.responseText||"Unknown Error.").replace(/,/g," ");
}
}
this.password_reset_error = __(msg);
}.bind(this));
},
password_reset_confirm: function(evt, password, password_confirmation) {
if (evt) {
evt.preventDefault();
evt.stopPropagation();
}
this.password_reset_confirm_error = null;
this.password_reset_send = false;
if(password != password_confirmation) {
this.password_reset_confirm_error = "Passwords do not match.";
return;
}
if(password.length < 5) {
this.password_reset_confirm_error = "Password too short (must have at least 5 characters).";
return;
}
confirm_password_reset(password, this.reset_token, function(parsed,req) {
if(req.status==201){
this.active_view = "login";
}
}.bind(this), function(req) {
if (req.status==404) {
var msg = "user not found";
} else {
var msg = "error: " + req.statusText;
}
this.password_reset_confirm_error = msg;
}.bind(this));
},
logout: function() {
this.active_view="login";
this.logged_in = false;
delete_session(function() {
this.active_space = {advanced:{}};
this.active_space_loaded = false;
this.active_sidebar_item = "none";
this.sidebar_state = "closed";
this.loading_user = false;
api_token = null;
this.user = {};
this.active_content_type = "login";
this.redirect_to("/");
}.bind(this));
},
send_feedback: function(text) {
if (text.length>0) {
create_feedback(this.user, text, function(xhr) {
alert(__("feedback_sent"));
this.close_modal()
}.bind(this), function(xhr) {
console.error(xhr);
});
}
},
remove_account: function(password, reason) {
this.account_remove_error = null;
if (reason && reason.length && (reason.length > 1)) {
create_feedback(this.user, reason, function(xhr) {
console.log("feedback sent");
}, function(xhr){});
}
if (!password) {
this.account_remove_error = "Password not correct";
return;
}
delete_user(this.user, password, function(xhr) {
alert("Sorry to see you go. Goodbye!");
this.logout();
}.bind(this), function(xhr) {
this.account_remove_error = "Password not correct ("+xhr.status+")";
}.bind(this));
},
user_avatar_image: function(user) {
return user.avatar_thumb_uri;
},
user_initials: function(user) {
var parts = (user?(user.nickname||user.email):"anonymous").replace(/[^a-zA-Z]/g,' ').replace(/ +/g,' ').split(' ');
if (parts.length>1) {
return parts[0][0]+parts[1][0];
}
return parts[0].substring(0,2);
},
has_avatar_image: function(user) {
return !!(user && user.avatar_thumb_uri && user.avatar_thumb_uri.length>0);
},
is_pro: function(user) {
return true;
},
}
}

View File

@@ -0,0 +1,167 @@
function boot_spacedeck() {
console.log("booting...");
// custom directives
setup_directives();
setup_whiteboard_directives();
setup_exclusive_audio_video_playback();
var data = {
active_view: null,
online: true,
was_offline: false,
account: "profile",
logged_in: false,
guest_nickname: null,
user: {},
active_profile: null,
active_profile_spaces: [],
active_dropdown: "none",
creating_user: false,
signup_error: null,
login_error: null,
password_reset_send: false,
password_reset_error: null,
password_reset_email: null,
password_reset_confirm_error: null,
reset_token: null,
global_spinner: false
};
var methods = {
activate_dropdown: function(id, evt) {
if (this.active_dropdown == id) {
this.active_dropdown = "none";
return;
}
this.active_dropdown = id;
},
close_dropdown: function(evt) {
if (evt) {
if ($(evt.target).parents(".dropdown").length) {
return;
}
}
this.active_dropdown = "none";
},
translate: function() {
return i18n.t(arguments)
},
};
// mix in functions from all Spacedeck modules
methods = _.extend(methods, SpacedeckUsers.methods);
methods = _.extend(methods, SpacedeckWebsockets.methods);
methods = _.extend(methods, SpacedeckSpaces.methods);
methods = _.extend(methods, SpacedeckTeams.methods);
methods = _.extend(methods, SpacedeckBoardArtifacts);
methods = _.extend(methods, SpacedeckFormatting);
methods = _.extend(methods, SpacedeckSections.methods);
methods = _.extend(methods, SpacedeckAvatars.methods);
methods = _.extend(methods, SpacedeckModals.methods);
methods = _.extend(methods, SpacedeckAccount.methods);
methods = _.extend(methods, SpacedeckRoutes);
data = _.extend(data, SpacedeckUsers.data);
data = _.extend(data, SpacedeckAccount.data);
data = _.extend(data, SpacedeckWebsockets.data);
data = _.extend(data, SpacedeckSpaces.data);
data = _.extend(data, SpacedeckTeams.data);
data = _.extend(data, SpacedeckSections.data);
data = _.extend(data, SpacedeckAvatars.data);
data = _.extend(data, SpacedeckModals.data);
Vue.filter('select', function (array, key, operant, value) {
var res = _.filter(array, function(e){
var test = eval(e[key] + " " + operant + " " + value);
return test;
});
return res;
});
Vue.filter('date', function (value, format) {
var day = moment(value);
return day.format(format).replace("\'", "").replace("\'", "");
});
Vue.filter('exceptFilter', function (array, key) {
var filtered = _.filter(array, function(i){
return i[key]==undefined;
});
return filtered;
});
Vue.filter('size', function (array) {
return array.length;
});
Vue.filter('empty?', function (array) {
return array.length==0;
});
Vue.filter('urls_to_links', function (text) {
return urls_to_links(text);
});
window.spacedeck = new Vue({
el: "body",
data: data,
methods: methods
});
var lang = "en";
window.refreshLocale = function() {
if (spacedeck && spacedeck.user && spacedeck.user.preferences) {
lang = spacedeck.user.preferences.language || "en";
} else if (window.browser_lang) {
lang = window.browser_lang;
}
}
window.refreshLocale();
i18n.init({ lng: lang, resStore: window.locales }, function(err, t) {
console.log("i18n initialized: "+lang);
});
window.__ = function() {
var params = Array.prototype.slice.call(arguments);
params.shift();
window.refreshLocale();
return i18n.t(arguments[0], { postProcess: "sprintf", sprintf: params });
};
spacedeck.setup_section_module();
spacedeck.load_user(function() {
spacedeck.route();
},function() {
spacedeck.route();
});
window.addEventListener("paste", function(evt) {
if (evt.target.nodeName=="INPUT" || (evt.target.nodeName=="TEXTAREA" && evt.target.id!="clipboard-ta") || evt.target.contenteditable) {
// cancel
return;
}
if (spacedeck.active_space) {
spacedeck.handle_section_paste(evt);
}
});
}
$(document).ready(function(){
window.smoke = smoke;
window.alert = smoke.alert;
FastClick.attach(document.body);
boot_spacedeck();
});

View File

@@ -0,0 +1,265 @@
SpacedeckWebsockets = {
data: {
users_online: {}
},
methods: {
handle_live_updates: function(msg) {
if (msg.model == "Space" && msg.object) {
if (msg.object.space_type == "space") {
if (this.active_space) {
if (this.active_space._id == msg.object._id) {
this.active_space = _.merge(this.active_space, msg.object);
}
}
}
}
if (msg.model == "Message") {
if (msg.action == "create" && msg.object) {
var new_message = msg.object;
if(this.active_space && this.active_space._id == new_message.space._id) {
this.active_space_messages.push(new_message);
this.refresh_space_comments();
} else console.log("message created in another space.");
}
}
if (msg.model == "Artifact") {
if (msg.action == "create" && msg.object) {
var new_artifact = msg.object;
if (this.active_space && this.active_space._id == new_artifact.space_id) {
var o = new_artifact;
if (o._id && !this.find_artifact_by_id(o._id)) {
this.update_board_artifact_viewmodel(new_artifact);
this.active_space_artifacts.push(new_artifact)
} else {
console.log("warning: got create on existing artifact.");
msg.action = "update"; // hackety hack!
}
} else console.log("artifact created in another space.");
}
else if (msg.action == "update" && msg.object) {
if (this.active_space) {
var o = msg.object;
if (o && o._id) {
var existing_artifact = this.find_artifact_by_id(o._id);
if (!existing_artifact) {
existing_artifact = o;
} else {
for (key in o) {
existing_artifact[key] = o[key];
this.update_board_artifact_viewmodel(existing_artifact);
}
}
}
}
}
else if (msg.action == "delete" && msg.object) {
if (this.active_space) {
var o = msg.object;
if(o._id){
var existing_artifact = this.find_artifact_by_id(o._id);
if (existing_artifact) {
var idx = this.active_space_artifacts.indexOf(existing_artifact);
this.active_space_artifacts.splice(idx, 1);
} else console.log("existing artifact to delete not found");
}else console.error("object without _id");
}
}
}
},
subscribe: function(space) {
if (this.websocket && this.websocket.readyState==1) {
this.websocket.send(JSON.stringify({action: "subscribe", space_id: space._id}));
} else {
console.error("socket not ready yet. (subscribe)");
}
},
is_member_online: function(space, member) {
if (!member.user) {
return false;
}
if (!this.users_online[space._id]) {
return false;
}
var isOnline = _.find(this.users_online[space._id], function(u) {
return (u._id == member.user._id);
});
return isOnline;
},
auth_websocket: function(space){
if (!this.websocket) {
this.init_websocket();
}
if (this.websocket && this.websocket.readyState==1) {
var auth_params = {
action: "auth",
editor_auth: space_auth,
editor_name: this.guest_nickname,
auth_token: window.socket_auth,
space_id: space._id
};
console.log("[websocket] auth space");
this.websocket.send(JSON.stringify(auth_params));
}
},
websocket_send: function(msg) {
if (!this.websocket) return;
if (this.websocket.readyState!=1) return;
try {
this.websocket.send(JSON.stringify(msg));
} catch (e) {
// catch NS problems
}
},
init_websocket: function() {
if (this.websocket) this.websocket = null;
if (this.current_timeout) {
clearTimeout(this.current_timeout);
this.current_timeout = null;
}
try {
this.websocket = new WebSocket(ENV.websocketsEndpoint + "/socket");
} catch (e) {
console.log("[websocket] cannot establish websocket connection: ",e);
this.current_timeout = setTimeout(function() {
console.log("[websocket] reconnecting", e);
this.init_websocket();
}.bind(this),5000);
}
if (!this.websocket) {
console.log("[websocket] no websocket support?");
return;
}
this.websocket.onopen = function(evt) {
if (this.current_timeout) {
clearTimeout(this.current_timeout);
this.current_timeout = null;
}
if (this.active_space_loaded) {
this.auth_websocket(this.active_space);
}
this.online = true;
}.bind(this);
this.websocket.onclose = function(evt) {
if (!window._spacedeck_location_change) {
this.online = false;
}
if (!this.current_timeout) {
this.current_timeout = setTimeout(function() {
console.log("[websocket] onclose: reconnecting", evt);
this.init_websocket();
}.bind(this),5000);
}
}.bind(this);
this.websocket.onmessage = function(evt) {
this.online = true;
try {
var msg = JSON.parse(evt.data);
} catch (e) {
console.log("[websocket] malformed message: ",evt.data);
return;
}
if (msg.channel_id == channel_id) {
return;
}
if (msg.action == "cursor") {
this.handle_user_cursor_update(msg);
}
else if (msg.action == "viewport") {
this.handle_presenter_viewport_update(msg);
}
else if (msg.action == "media") {
this.handle_presenter_media_update(msg);
}
if (msg.action == "update" || msg.action == "create" || msg.action == "delete"){
this.handle_live_updates(msg);
}
if (msg.action == "init") {
channel_id = msg.channel_id;
}
if (msg.action == "auth_valid") {
if (this.active_space) {
this.subscribe(this.active_space);
if (this.unsaved_transactions()) {
console.log("[websockets-saver] found unsaved transactions, triggering save.");
this.process_artifact_save_queue();
}
}
}
if (msg.action == "subscription_valid") {
console.log("subscription_valid");
}
if (msg.action == "status_update") {
var spaceId = msg.space_id;
var users = msg.users;
// filter ourselves
if (this.user && this.user._id) {
users = _.filter(users, function(u) {
return (u && (u._id != this.user._id));
}.bind(this));
}
users = _.filter(users, function(u) {
return (u && (u._id || u.nickname));
});
this.users_online[spaceId] = users;
if (this.active_space) {
if (this.active_space._id == spaceId) {
this.active_space_users = users;
}
}
}
}.bind(this);
this.websocket.onerror = function(evt) {
console.log("websocket.onerror:", evt);
if (!window._spacedeck_location_change) {
this.online = false;
this.was_offline = true;
}
if (!this.current_timeout) {
this.current_timeout = setTimeout(function() {
console.log("websocket.onerror: reconnecting", evt);
this.init_websocket();
}.bind(this),5000);
}
}.bind(this);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,307 @@
function vec2_add(a,b) {
return {dx:a.dx+b.dx, dy:a.dy+b.dy};
}
function vec2_sub(a,b) {
return {dx:a.dx-b.dx, dy:a.dy-b.dy};
}
function vec2_mul(a,f) {
return {dx:a.dx*f, dy:a.dy*f};
}
function vec2_magn(v) {
return Math.sqrt(v.dx*v.dx + v.dy*v.dy);
}
function vec2_unit(v) {
var m = vec2_magn(v);
if (m == 0) return {dx:0, dy:0};
return {dx:v.dx/m, dy:v.dy/m};
}
function vec2_angle(v) {
if (v.dx==0) return Math.atan2(v.dx+0.01, v.dy);
return Math.atan2(v.dx, v.dy);
}
function render_vector_drawing(a, padding) {
var shape = a.style.shape || "";
var path = [];
var p = a.control_points[0];
if (!p) return "";
path.push("M" + (p.dx + padding) + "," + (p.dy + padding) + " ");
if (shape.match("arrow")) {
var cps = a.control_points[0];
var cpe = a.control_points[1];
var cpm = a.control_points[2];
if (!cpm) cpm = cpe;
var markerId = a._id;
var origin = cps;
var end = cpe;
var vec = vec2_sub(end, origin);
var length = vec2_magn(vec);
var middleVec = vec2_mul(vec2_unit(vec),length / 2);
var middlePoint = vec2_add(origin, middleVec);
var ortho = vec2_sub(cpm, middlePoint);
var scaledMiddlePoint = vec2_add(vec2_mul(ortho,2), middlePoint);
var d = "M" + (cps.dx + padding) + "," + (cps.dy + padding) + " Q" + (scaledMiddlePoint.dx + padding) + "," + (scaledMiddlePoint.dy + padding) + " " + (cpe.dx + padding) + "," + (cpe.dy + padding);
var tip = "<defs><marker id='ae" + markerId + "' refX=\"0.1\" refY=\"3\" markerWidth=\"3\" markerHeight=\"6\" orient=\"auto\">";
tip += "<path d=\"M-3,0 V6 L3,3 Z\" fill=\""+a.style.stroke_color+"\" stroke-width=\"0\"/></marker></defs>";
var svg = tip + "<path d='" + d + "' style='stroke-width:" + a.style.stroke + ";' marker-end='url(#ae" + markerId + ")'/>";
return svg;
}
else if (false /*shape.match("scribble")*/) {
var idx = 0;
while (idx < a.control_points.length - 1) {
var prevP = a.control_points[idx];
if (a.control_points.length > idx + 1) {
var p = a.control_points[idx + 1];
} else {
var p = prevP;
}
if (a.control_points.length > idx + 2) {
var nextP = a.control_points[idx + 2];
} else {
var nextP = p;
}
var dpy = (p.dy - prevP.dy);
var dpx = (p.dx - prevP.dx);
var dny = (nextP.dy - p.dy);
var dnx = (nextP.dx - p.dx);
var distToNext = Math.sqrt(dny * dny + dnx * dnx);
var distToPrev = Math.sqrt(dpy * dpy + dpx * dpx);
var r = Math.sqrt((distToNext + distToPrev) / 2) * 2;
var prevAngle = Math.atan2(dpy, dpx);
var nextAngle = Math.atan2(dny, dnx);
var bisectAngle = (prevAngle + nextAngle) / 2;
var tangentAngle = bisectAngle;
var cp1x = p.dx + Math.cos(tangentAngle) * -r;
var cp1y = p.dy + Math.sin(tangentAngle) * -r;
var cp2x = p.dx + Math.cos(tangentAngle) * r;
var cp2y = p.dy + Math.sin(tangentAngle) * r;
var dcp1x = cp1x - nextP.dx;
var dcp1y = cp1y - nextP.dy;
var dcp2x = cp2x - nextP.dx;
var dcp2y = cp2y - nextP.dy;
var distToCp1 = Math.sqrt(dcp1x * dcp1x + dcp1y * dcp1y) / r;
var distToCp2 = Math.sqrt(dcp2x * dcp2x + dcp2y * dcp2y) / r;
if (distToCp1 > distToCp2) {
var curve = "S" + (cp1x + padding) + "," + (cp1y + padding) + " " + (p.dx + padding) + "," + (p.dy + padding);
}
else {
var curve = "S" + (cp2x + padding) + "," + (cp2y + padding) + " " + (p.dx + padding) + "," + (p.dy + padding);
}
path.push(curve);
idx += 1;
}
} else {
for (var idx=0; idx<a.control_points.length; idx++) {
var p = a.control_points[idx];
var command = (idx==0) ? 'M' : 'L';
path.push(command+(p.dx+padding)+','+(p.dy+padding));
}
}
return "<path d='"+path.join(' ')+"'>";
}
function render_vector_star(edges,xradius,yradius,offset) {
edges *= 2;
var points = [];
var degrees = 360 / edges;
for (var i=0; i < edges; i++) {
var a = i * degrees - 90;
var xr = xradius;
var yr = yradius;
if (i%2) {
if (edges==20) {
xr/=1.5;
yr/=1.5;
} else {
xr/=2.8;
yr/=2.8;
}
}
var x = offset + xradius + xr * Math.cos(a * Math.PI / 180);
var y = offset + yradius + yr * Math.sin(a * Math.PI / 180);
points.push(x+","+y);
}
return "<polygon points='"+points.join(" ")+"'/>";
}
function transform_vector_template(cmds, xr, yr, offset) {
var cmd_str = "";
for (var i = 0; i<cmds.length; i+=2) {
var vals = cmds[i+1];
for (var j = 0; j<vals.length; j+=2) {
vals[j]*=(2*xr/100.0);
vals[j+1]*=(2*yr/100.0);
}
cmd_str+=cmds[i]+cmds[i+1].join(',')+" ";
}
return cmd_str;
}
function render_vector_heart(xr, yr, offset) {
var cmds = ['M',[50.141,98.5],
'c',[0,0,-49,-38.334,-49,-67.982],
'C',[1.141,15.333,14.356,1,30.659,1],
'c',[7.437,0,14.244,2.791,19.435,7.33],
'l',[0,0],
'C',[55.296,3.742,62.141,1,69.622,1],
'c',[16.303,0,29.519,14.166,29.519,29.518],
'C',[99.141,60.334,50.141,98.5,50.141,98.5],
'z',[]];
svg ="<path d='"+ transform_vector_template(cmds, xr, yr, offset) +"'/>";
return svg;
}
function render_vector_cloud(xr, yr, offset) {
var cmds = ['M',[17.544,99.729],
'c',[0,0,-17.544,6.929,-17.544,-36.699],
'c',[0,-18.698,19.298,-28.047,19.298,-9.35],
'c',[0,0,-3.508,-54.46,26.316,-53.672],
'C',[71.93,0.704,68.421,34.983,68.421,34.983],
'S',[100,25.634,100,72.379],
'c',[0,28.047,-21.053,27.351,-21.053,27.351],
'z',[]];
svg ="<path d='"+ transform_vector_template(cmds, xr, yr, offset) +"'/>";
return svg;
}
function render_vector_ellipse(xr, yr, offset) {
svg = "<ellipse cx="+(xr+offset)+" cy="+(yr+offset)+" rx="+xr+" ry="+yr+">";
return svg;
}
function render_vector_speechbubble(xr, yr, offset) {
var cmds = ['M',[100,50],
'c',[0,9.5,-2.7,18,-7.4,26],
'C',[90,80,100,100,100,100],
's',[-23.194,-6.417,-28,-4.162],
'c',[-6.375,3,-13.5,4.7,-21,4.7],
'C',[23,100,0.5,77,0.5,50],
'C',[0.5,23,23,0.5,50,0.5],
'C',[77,0.5,100,23,100,50],
'z',[]];
svg ="<path d='"+ transform_vector_template(cmds, xr, yr, offset) +"'/>";
return svg;
}
function render_vector_ngon(edges,xradius,yradius,offset) {
var points = [];
var degrees = 360 / edges;
for (var i=0; i < edges; i++) {
var a = i * degrees - 90;
var x = offset + xradius + xradius * Math.cos(a * Math.PI / 180);
var y = offset + yradius + yradius * Math.sin(a * Math.PI / 180);
points.push(x+","+y);
}
return "<polygon points='"+points.join(" ")+"'/>";
}
function render_vector_rect(xradius,yradius,offset) {
return "<rect x='0' y='0' width='"+xradius*2+"' height='"+xradius*2+"'/>";
}
function render_vector_shape(a) {
var stroke = parseInt(a.style.stroke) + 4;
var offset = stroke / 2;
var xr = (a.board.w-stroke) / 2;
var yr = (a.board.h-stroke) / 2;
var shape_renderers = {
ellipse: function() { return render_vector_ellipse(xr, yr, offset); },
pentagon: function() { return render_vector_ngon(5, xr, yr, offset); },
hexagon: function() { return render_vector_ngon(6, xr, yr, offset); },
octagon: function() { return render_vector_ngon(8, xr, yr, offset); },
diamond: function() { return render_vector_ngon(4, xr, yr, offset); },
square: function() { return "" },
triangle: function() { return render_vector_ngon(3, xr, yr, offset); },
star: function() { return render_vector_star(5, xr, yr, offset); },
burst: function() { return render_vector_star(10, xr, yr, offset); },
speechbubble: function() { return render_vector_speechbubble(xr, yr, offset); },
heart: function() { return render_vector_heart(xr, yr, offset); },
cloud: function() { return render_vector_cloud(xr, yr, offset); },
}
var render_func = shape_renderers[a.style.shape];
if (!render_func) return "";
return render_func();
}
function simplify_scribble_points(control_points) {
var filtered_points = [];
var thresh = 2;
var idx=0;
for (var i=0; i<control_points.length; i++) {
var cp = control_points[i];
var next = control_points[i+1];
if (i>0) {
var prev = control_points[i-1];
}
if (next && prev) {
dprev = vec2_sub(cp, prev);
dnext = vec2_sub(next, cp);
aprev = vec2_angle(dprev);
anext = vec2_angle(dnext);
delta = Math.abs(Math.abs(aprev)-Math.abs(anext));
delta2 = vec2_magn(vec2_sub(cp,prev));
if (delta2>thresh && delta>0.1) {
filtered_points.push(cp);
}
}
else {
filtered_points.push(cp);
}
}
return filtered_points;
}
if (typeof(window) == 'undefined') {
exports.render_vector_shape = render_vector_shape;
exports.render_vector_drawing = render_vector_drawing;
}

10030
public/javascripts/vue.js Normal file

File diff suppressed because it is too large Load Diff