UNPKG

cloudcms-server

Version:
1,196 lines (1,009 loc) 36.8 kB
/* Copyright 2015 Gitana Software, Inc. 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. For more information, please contact Gitana Software, Inc. at this address: info@gitanasoftware.com */ (function(root, factory) { /* CommonJS */ if (typeof exports == 'object') { var $ = require("jquery"); var io = require("socket.io"); module.exports = factory($, io); } /* AMD module */ else if (typeof define == 'function' && define.amd) { define(["jquery", "socket.io"], factory); } /* Browser global */ else { var $ = root.$; var io = root.io; root.Spinner = factory($, io); } } (this, function($, io) { "use strict"; if ($.fn.insight) { // already initialized with jquery, simply bail out return; } var ALREADY_EXPIRED_DATE = "Mon, 7 Apr 2012, 16:00:00 GMT"; var CLASS_INSIGHT = "insight"; var ATTR_DATA_INSIGHT_ID = "data-insight-id"; var ATTR_DATA_INSIGHT_NODE = "data-insight-node"; var iidCounter = 0; ///////////////////////////////////////////////////////////////////////////////////////////////// // // utility functions // ///////////////////////////////////////////////////////////////////////////////////////////////// var makeArray = function(nonArray) { return Array.prototype.slice.call(nonArray); }; var isFunction = function(obj) { return Object.prototype.toString.call(obj) === "[object Function]"; }; var isString = function(obj) { return (typeof obj == "string"); }; var copyInto = function(target, source, includeFunctions) { for (var i in source) { if (source.hasOwnProperty(i)) { if (isFunction(source[i])) { if (includeFunctions) { target[i] = source[i]; } } else { target[i] = source[i]; } } } }; var createInsightId = function() { var str = window.location.protocol + window.location.hostname + ":" + window.location.port + window.location.pathname; var iid = hashcode(str) + "_" + iidCounter; iidCounter++; return iid; }; var hashcode = function(str) { var hash = 0; if (str.length == 0) return hash; for (var i = 0; i < str.length; i++) { var char2 = str.charCodeAt(i); hash = ((hash<<5)-hash)+char2; hash = hash & hash; // Convert to 32bit integer } if (hash < 0) { hash = hash * -1; } return hash; }; var insightId = function(el, id) { if (id) { if (!$(el).hasClass(CLASS_INSIGHT)) { $(el).addClass(CLASS_INSIGHT); } $(el).attr(ATTR_DATA_INSIGHT_ID, id); } if ($(el).hasClass(CLASS_INSIGHT)) { id = $(el).attr(ATTR_DATA_INSIGHT_ID); } return id; }; var findNearestCloudCMSNode = function(el) { if (!el || $(el).length == 0) { return null; } var attr = $(el).attr(ATTR_DATA_INSIGHT_NODE); if (attr) { var repoId = null; var branchId = null; var nodeId = null; // structure is <repoId>/<branchId>/<nodeId> var i = attr.indexOf("/"); if (i > -1) { repoId = attr.substring(0, i); var j = attr.indexOf("/", i+1); if (j > -1) { branchId = attr.substring(i+1, j); nodeId = attr.substring(j+1); } } return { "repositoryId": repoId, "branchId": branchId, "id": nodeId }; } return findNearestCloudCMSNode($(el).parent()); }; ///////////////////////////////////////////////////////////////////////////////////////////////// // // contexts // ///////////////////////////////////////////////////////////////////////////////////////////////// // construct a map of provider functions // this can be overridden with config var CONTEXTS_DEFAULTS = { "user": function() { return {}; }, "source": function() { return { "user-agent": navigator.userAgent, "platform": navigator.platform }; }, "page": function() { var pathname = window.location.pathname; var replaceAll = function(text, find, replace) { return text.replace(new RegExp(find, 'g'), replace); }; if (pathname) { // replace any " " with "" pathname = replaceAll(pathname, " ", ""); // replace any "//" with "/" pathname = replaceAll(pathname, "//", "/"); // some path correction (assume / is /index.html) if (pathname === "/") { pathname = "/index.html"; } } return { "uri": pathname, "hash": window.location.hash, "fullUri": pathname + window.location.hash, "title": document.title }; }, "application": function() { return { "host": window.location.host, "hostname": window.location.hostname, "port": window.location.port, "protocol": window.location.protocol, "url": window.location.protocol + "//" + window.location.host }; }, "node": function(event) { var x = {}; var descriptor = findNearestCloudCMSNode(event.currentTarget); if (descriptor) { x = { "repositoryId": descriptor.repositoryId, "branchId": descriptor.branchId, "id": descriptor.id }; } return x; }, "attributes": function(event) { var map = {}; var el = event.currentTarget; $.each(el.attributes, function(i, attribute) { var name = null; if (attribute.name.toLowerCase().indexOf("data-insight-") > -1) { name = attribute.name.substring(13); } else if (attribute.name == "href") { name = attribute.name; } if (name) { map[name] = attribute.value; } }); return map; } }; var contexts = {}; ///////////////////////////////////////////////////////////////////////////////////////////////// // // config // ///////////////////////////////////////////////////////////////////////////////////////////////// var config = {}; ///////////////////////////////////////////////////////////////////////////////////////////////// // // worker objects // ///////////////////////////////////////////////////////////////////////////////////////////////// // the dispatcher which uses socket.io to fire messages over to server and listen for special events var Dispatcher = function() { var createSocket = function() { /* reconnection whether to reconnect automatically (true) reconnectionDelay how long to wait before attempting a new reconnection (1000) reconnectionDelayMax maximum amount of time to wait between reconnections (5000). Each attempt increases the reconnection by the amount specified by reconnectionDelay. timeout connection timeout before a connect_error and connect_timeout events are emitted (20000) autoConnect by setting this false, you have to call manager.open whenever you decide it's appropriate */ /* var socket = io({ reconnection: true, reconnectionDelay: 50, reconnectionDelayMax: 200, timeout: 20000, autoConnect: true }); */ var socket = io({ forceNew: true }); socket.on("connect", function() { console.log("socket.io - connect"); }); socket.on("error", function() { console.log("heard socket.error"); }); socket.on("connect_error", function(err) { console.log("socket.io - connect_error"); console.log(err); }); socket.on("connect_timeout", function() { console.log("socket.io - connect_timeout"); }); socket.on("reconnect", function(n) { console.log("socket.io - reconnect"); console.log(n); }); socket.on("reconnect_attempt", function() { console.log("socket.io - reconnect_attempt"); }); socket.on("reconnecting", function(n) { console.log("socket.io - reconnecting"); console.log(n); }); socket.on("reconnect_error", function(err) { console.log("socket.io - reconnect_error"); console.log(err); }); socket.on("reconnect_failed", function() { console.log("socket.io - reconnect_failed"); }); return socket; }; var sendMessage = function() { var socket = null; return function(event, data) { if (!socket) { console.log("Creating first socket"); socket = createSocket(); } socket.emit(event, data); } }(); var QUEUE = []; var BUSY = false; // run a timeout process that periodically fires over socket.io. updates var syncFunction = function(callback) { if (BUSY) { callback(); return; } BUSY = true; var queueLength = QUEUE.length; if (queueLength > 0) { try { // copy into rows var data = { "warehouseId": config.warehouseId, "rows": [] }; for (var i = 0; i < queueLength; i++) { data.rows.push(QUEUE[i]); } console.log("Insight sending " + data.rows.length + " rows"); // send via socket.io sendMessage("insight-push", data); // strip down the queue QUEUE = QUEUE.slice(queueLength); } catch (e) { console.log(e); } } // unbusy BUSY = false; callback(); }; var r = {}; r.push = function(interaction) { QUEUE.push(interaction); }; r.flush = function(callback) { syncFunction(function(err) { if (callback) { callback(err); } }); }; return r; }(); ///////////////////////////////////////////////////////////////////////////////////////////////// // // methods // ///////////////////////////////////////////////////////////////////////////////////////////////// var methods = {}; /** * This is the "endSession" method which can be called at any time to end the current session. */ methods.endSession = function(callback) { var now = new Date().getTime(); if (SESSION_KEY()) { var c = { "warehouseId": config.warehouseId, "event": { "type": "end_session" }, "timestamp": { "ms": now }, "sessionKey": SESSION_KEY() }; if (USER_KEY()) { c.userKey = USER_KEY(); } Dispatcher.push(c); // flush Dispatcher.flush(function(err) { if (callback) { callback(err); } }); SESSION_KEY(null); USER_KEY(null); } }; ///////////////////////////////////////////////////////////////////////////////////////////////// // // event capture logic // ///////////////////////////////////////////////////////////////////////////////////////////////// // stores or retrieves the session key var SESSION_KEY = function(val) { return syncCookie("insight-session-key", val); }; var USER_KEY = function(val) { return syncCookie("insight-user-key", val); }; var startSession = function(callback) { var now = new Date().getTime(); // make sure we have a session started if (!SESSION_KEY()) { // generate session and user keys SESSION_KEY("SESSION_KEY_" + now); USER_KEY("USER_KEY_" + now); // indicate that we started a session Dispatcher.push({ "event": { "type": "start_session" }, "timestamp": { "ms": now }, "sessionKey": SESSION_KEY(), "userKey": USER_KEY(), "page": contexts["page"](), "application": contexts["application"](), "user": contexts["user"](), "source": contexts["source"]() }); // flush Dispatcher.flush(function(err) { if (callback) { callback(err); } }); } }; var captureInteraction = function(event, callback) { var now = new Date().getTime(); // push the interaction Dispatcher.push({ "event": { "type": event.type, "x": event.pageX, "y": event.pageY, "offsetX": event.offsetX, "offsetY": event.offsetY }, "timestamp": { "ms": now }, "element": { "id": event.currentTarget.id, "type": event.currentTarget.nodeName, "iid": insightId(event.currentTarget) }, "sessionKey": SESSION_KEY(), "userKey": USER_KEY(), "page": contexts["page"](), "application": contexts["application"](), "user": contexts["user"](), "source": contexts["source"](), "node": contexts["node"](event), "attributes": contexts["attributes"](event) }); // flush Dispatcher.flush(function(err) { if (callback) { callback(err); } }); }; ///////////////////////////////////////////////////////////////////////////////////////////////// // // binds events // ///////////////////////////////////////////////////////////////////////////////////////////////// var FunctionHandler = function(el, _config) { // el can either be a dom id or a dom element if (el && isString(el)) { el = $("#" + el); } if (!_config) { _config = {}; } /** * If config is a string, then it is a method name * We support: * * 'endSession' * 'destroy' */ var methodName = null; if (typeof(_config) == "string") { methodName = _config; } if (methodName) { return methods[methodName].call(); } /** * Configuration: * * { * "contexts": { * "node": function(event) { * }, * "user": function(event) { * }, * "source": function(event) { * }, * "page": function(event) { * }, * "application": function(event) { * } * }, * "events": ["click"], * "host": <optional - either provided or picked from window.location>, * "warehouseId": <optional - warehouse id> * } */ /** * Notes * * The window.location.href is passed over to the server. If "host" is provided in config, then that is used. * * The server uses this to determine the Insight.APPLICATION_KEY and warehouseId is assumed to be "primary" * unless provided in config. warehouseId can also be specified in config. */ // if events array not specified, assume 'click' event if (!_config.events) { _config.events = ["click"]; } // config config = {}; copyInto(config, _config); // contexts contexts = {}; copyInto(contexts, CONTEXTS_DEFAULTS, true); if (config.contexts) { copyInto(contexts, config.contexts, true); } // walk through our items // for each item, if not already tracked, then: // // - add class CLASS_INSIGHT // - add attribute ATTR_DATA_INSIGHT_ID // - bind event handlers // $(el).each(function() { var eventEl = this; if (!$(eventEl).hasClass(CLASS_INSIGHT)) { // generate a new id and bind to element (apply CLASS_INSIGHT) insightId(eventEl, createInsightId()); // event handlers for (var i = 0; i < config.events.length; i++) { var eventType = config.events[i]; $(eventEl).bindFirst(eventType, function(eventEl, eventType) { return function(event) { // check if already flushed // if so, we skip through our event handler var flushed = $(eventEl).attr("data-insight-flushed"); if (!flushed) { // stop event handling chain event.preventDefault(); event.stopImmediatePropagation(); // capture the interaction captureInteraction(event, function(err) { window.setTimeout(function() { // mark as flushed $(eventEl).attr("data-insight-flushed", "flushed"); // fire event again if (event.originalEvent && event.originalEvent.target && event.originalEvent.type) { try { $(event.originalEvent.target).simulate(event.originalEvent.type); } catch (e) { console.log(e); } } }, 250); // NOTE: this 250 ms delay is needed for web hosted version where when they click on a tel: link // something about it blocks or interrupts socket.io from completing it's communication to the // back end server. this additional delay allows socket.io to complete first }); } else { // remove the flushed marker $(eventEl).attr("data-insight-flushed", null); } }; }(eventEl, eventType)); } } }); // start session startSession(function(err) { // TODO: session started successfully }); }; ///////////////////////////////////////////////////////////////////////////////////////////////// // // jQuery Wrapper // ///////////////////////////////////////////////////////////////////////////////////////////////// $.fn.insight = function() { var args = makeArray(arguments); // append this into the front of args var newArgs = [].concat(this, args); // invoke, hand back field instance return FunctionHandler.apply(this, newArgs); }; // https://github.com/private-face/jquery.bind-first/blob/master/dev/jquery.bind-first.js // jquery.bind-first.js /* * jQuery.bind-first library v0.2.3 * Copyright (c) 2013 Vladimir Zhuravlev * * Released under MIT License * @license * * Date: Thu Feb 6 10:13:59 ICT 2014 **/ (function($) { var splitVersion = $.fn.jquery.split("."); var major = parseInt(splitVersion[0]); var minor = parseInt(splitVersion[1]); var JQ_LT_17 = (major < 1) || (major == 1 && minor < 7); function eventsData($el) { return JQ_LT_17 ? $el.data('events') : $._data($el[0]).events; } function moveHandlerToTop($el, eventName, isDelegated) { var data = eventsData($el); var events = data[eventName]; if (!JQ_LT_17) { var handler = isDelegated ? events.splice(events.delegateCount - 1, 1)[0] : events.pop(); events.splice(isDelegated ? 0 : (events.delegateCount || 0), 0, handler); return; } if (isDelegated) { data.live.unshift(data.live.pop()); } else { events.unshift(events.pop()); } } function moveEventHandlers($elems, eventsString, isDelegate) { var events = eventsString.split(/\s+/); $elems.each(function() { for (var i = 0; i < events.length; ++i) { var pureEventName = $.trim(events[i]).match(/[^\.]+/i)[0]; moveHandlerToTop($(this), pureEventName, isDelegate); } }); } function makeMethod(methodName) { $.fn[methodName + 'First'] = function() { var args = $.makeArray(arguments); var eventsString = args.shift(); if (eventsString) { $.fn[methodName].apply(this, arguments); moveEventHandlers(this, eventsString); } return this; } } // bind makeMethod('bind'); // one makeMethod('one'); // delegate $.fn.delegateFirst = function() { var args = $.makeArray(arguments); var eventsString = args[1]; if (eventsString) { args.splice(0, 2); $.fn.delegate.apply(this, arguments); moveEventHandlers(this, eventsString, true); } return this; }; // live $.fn.liveFirst = function() { var args = $.makeArray(arguments); // live = delegate to the document args.unshift(this.selector); $.fn.delegateFirst.apply($(document), args); return this; }; // on (jquery >= 1.7) if (!JQ_LT_17) { $.fn.onFirst = function(types, selector) { var $el = $(this); var isDelegated = typeof selector === 'string'; $.fn.on.apply($el, arguments); // events map if (typeof types === 'object') { for (var type in types) if (types.hasOwnProperty(type)) { moveEventHandlers($el, type, isDelegated); } } else if (typeof types === 'string') { moveEventHandlers($el, types, isDelegated); } return $el; }; } })($); /* * jquery.simulate - simulate browser mouse and keyboard events * * Copyright (c) 2009 Eduardo Lundgren (eduardolundgren@gmail.com) * and Richard D. Worth (rdworth@gmail.com) * * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. * */ (function($) { $.fn.extend({ simulate: function(type, options) { return this.each(function() { var opt = $.extend({}, $.simulate.defaults, options || {}); new $.simulate(this, type, opt); }); } }); $.simulate = function(el, type, options) { this.target = el; this.options = options; if (/^drag$/.test(type)) { this[type].apply(this, [this.target, options]); } else { this.simulateEvent(el, type, options); } }; $.extend($.simulate.prototype, { simulateEvent: function(el, type, options) { var evt = this.createEvent(type, options); this.dispatchEvent(el, type, evt, options); return evt; }, createEvent: function(type, options) { if (/^mouse(over|out|down|up|move)|(dbl)?click$/.test(type)) { return this.mouseEvent(type, options); } else if (/^key(up|down|press)$/.test(type)) { return this.keyboardEvent(type, options); } }, mouseEvent: function(type, options) { var evt; var e = $.extend({ bubbles: true, cancelable: (type != "mousemove"), view: window, detail: 0, screenX: 0, screenY: 0, clientX: 0, clientY: 0, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, button: 0, relatedTarget: undefined }, options); var relatedTarget = $(e.relatedTarget)[0]; if ($.isFunction(document.createEvent)) { evt = document.createEvent("MouseEvents"); evt.initMouseEvent(type, e.bubbles, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, e.relatedTarget || document.body.parentNode); } else if (document.createEventObject) { evt = document.createEventObject(); $.extend(evt, e); evt.button = { 0:1, 1:4, 2:2 }[evt.button] || evt.button; } return evt; }, keyboardEvent: function(type, options) { var evt; var e = $.extend({ bubbles: true, cancelable: true, view: window, ctrlKey: false, altKey: false, shiftKey: false, metaKey: false, keyCode: 0, charCode: 0 }, options); if ($.isFunction(document.createEvent)) { try { evt = document.createEvent("KeyEvents"); evt.initKeyEvent(type, e.bubbles, e.cancelable, e.view, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.keyCode, e.charCode); } catch(err) { evt = document.createEvent("Events"); evt.initEvent(type, e.bubbles, e.cancelable); $.extend(evt, { view: e.view, ctrlKey: e.ctrlKey, altKey: e.altKey, shiftKey: e.shiftKey, metaKey: e.metaKey, keyCode: e.keyCode, charCode: e.charCode }); } } else if (document.createEventObject) { evt = document.createEventObject(); $.extend(evt, e); } if (($.browser !== undefined) && ($.browser.msie || $.browser.opera)) { evt.keyCode = (e.charCode > 0) ? e.charCode : e.keyCode; evt.charCode = undefined; } return evt; }, dispatchEvent: function(el, type, evt) { if (el.dispatchEvent) { el.dispatchEvent(evt); } else if (el.fireEvent) { el.fireEvent('on' + type, evt); } return evt; }, drag: function(el) { var self = this, center = this.findCenter(this.target), options = this.options, x = Math.floor(center.x), y = Math.floor(center.y), dx = options.dx || 0, dy = options.dy || 0, target = this.target; var coord = { clientX: x, clientY: y }; this.simulateEvent(target, "mousedown", coord); coord = { clientX: x + 1, clientY: y + 1 }; this.simulateEvent(document, "mousemove", coord); coord = { clientX: x + dx, clientY: y + dy }; this.simulateEvent(document, "mousemove", coord); this.simulateEvent(document, "mousemove", coord); this.simulateEvent(target, "mouseup", coord); }, findCenter: function(el) { var el = $(this.target), o = el.offset(); return { x: o.left + el.outerWidth() / 2, y: o.top + el.outerHeight() / 2 }; } }); $.extend($.simulate, { defaults: { speed: 'sync' }, VK_TAB: 9, VK_ENTER: 13, VK_ESC: 27, VK_PGUP: 33, VK_PGDN: 34, VK_END: 35, VK_HOME: 36, VK_LEFT: 37, VK_UP: 38, VK_RIGHT: 39, VK_DOWN: 40 }); })($); // cookies /** * Writes a cookie. * * @param {String} name * @param {String} value * @param [String] path optional path (assumed "/" if not provided) * @param [Number] days optional # of days to store cookie * if null or -1, assume session cookie * if 0, assume expired cookie * if > 0, assume # of days * @param [String] domain optional domain (otherwise assumes wildcard base domain) */ var writeCookie = function(name, value, path, days, domain) { if (typeof(document) !== "undefined") { var createCookie = function(name, value, path, days, host) { // path if (!path) { path = "/"; } var pathString = ";path=" + path; // expiration var expirationString = ""; if (typeof(days) == "undefined" || days == -1) { // session cookie } else if (days == 0) { // expired cookie expirationString = ";expires=" + ALREADY_EXPIRED_DATE; } else if (days > 0) { var date = new Date(); date.setTime(date.getTime()+(days*24*60*60*1000)); expirationString = ";expires="+date.toGMTString(); } // domain var domainString = ""; if (host) { domainString = ";domain=" + host; } document.cookie = name + "=" + value + expirationString + pathString + domainString + ";"; }; createCookie(name, value, path, days, domain); } }; /** * Deletes a cookie. * * @param name * @param path */ var deleteCookie = function(name, path) { var existsCookie = function(name, path) { return readCookie(name); }; if (typeof(document) != "undefined") { // first attempt, let the browser sort out the assumed domain // this works for most modern browsers if (existsCookie(name)) { // use expiration time of 0 to signal expired cookie writeCookie(name, "", path, 0); } // second attempt, if necessary, plug in an assumed domain // this is needed for phantomjs if (existsCookie(name)) { // see if we can resolve a domain if (window) { var domain = window.location.host; if (domain) { // remove :port var i = domain.indexOf(":"); if (i > -1) { domain = domain.substring(0, i); } } // use expiration time of 0 to signal expired cookie writeCookie(name, "", path, 0, domain); } } } }; var readCookie = function(name) { function _readCookie(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for (var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0)==' ') { c = c.substring(1,c.length); } if (c.indexOf(nameEQ) == 0) { return c.substring(nameEQ.length,c.length); } } return null; } var value = null; if (typeof(document) !== "undefined") { value = _readCookie(name); } return value; }; var syncCookie = function(name, val) { if (typeof(val) !== "undefined") { if (val === null) { deleteCookie(name); } else { writeCookie(name, val); } } return readCookie(name); }; }));