1 /*global global: true, process: true, require: true, module: true */ 2 3 /** 4 * Establishes the `GCN' object and exposes it in the global context. 5 */ 6 GCN = (function (global) { 7 'use strict'; 8 9 // Check whether we are in nodeJS context. 10 if (typeof process !== 'undefined' && process.versions 11 && process.versions.node) { 12 global.isNode = true; 13 var XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; 14 jQuery = global.$ = global.jQuery = require('jquery'); 15 global.jQuery.ajaxSettings.xhr = function createNodeXHRForGCN() { 16 return new XMLHttpRequest(); 17 }; 18 // http://stackoverflow.com/a/6432602 19 global.jQuery.support.cors = true; 20 } 21 22 // Temporarily deactivate JS Lints "dangerous '.'" test for regular expressions. 23 /*jslint regexp: false*/ 24 /** 25 * Match URL to internal page 26 * 27 * @const 28 * @type {RegExp} 29 */ 30 var INTERNAL_PAGEURL = /\/CNPortletapp\/alohapage\?.*realid=([0-9]+).*$/; 31 32 /** 33 * Match URL to internal file 34 * 35 * @const 36 * @type {RegExp} 37 */ 38 var INTERNAL_FILEURL = /^http.*\?.*&do=16000&id=([0-9]+).*$/; 39 /*jslint regexp: true*/ 40 41 /** 42 * @private 43 * @type {object} An object to indicate which handlers have been 44 * registered through the `GCN.onRender()' function. 45 */ 46 var onRenderHandler = {}; 47 48 /** 49 * @private 50 * @type {boolean} A flag to indicate whether or not a handler has been 51 * registerd through the `GCN.onError()' function. 52 */ 53 var hasOnErrorHandler = false; 54 55 /** 56 * @ignore 57 * @type {boolean} An internal flag that stores whether an authentication 58 * handler has been set. 59 */ 60 var hasAuthenticationHandler = false; 61 62 /** 63 * GCN JS API error object. This is the object passed to error handlers. 64 * 65 * @class 66 * @name GCNError 67 * @param {string} code error code for the error 68 * @param {string} message descriptive error message 69 * @param {object} data additional data 70 */ 71 var GCNError = function (code, message, data) { 72 this.code = code; 73 this.message = message; 74 this.data = data; 75 }; 76 77 /** 78 * Returns a human-readable representation of this error object. 79 * 80 * @public 81 * @return {string} 82 */ 83 GCNError.prototype.toString = function () { 84 return 'GCN ERROR (' + this.code + '): "' + (this.message || '') + '"'; 85 }; 86 87 /** 88 * @name GCN 89 * @class 90 * 91 * Base namespace for the Gentics Content.Node JavaScript API. 92 */ 93 var GCN = global.GCN || {}; 94 95 jQuery.extend(GCN, { 96 /** @lends GCN */ 97 98 /** 99 * Reference to the global context. 100 * 101 * @type {object} 102 */ 103 global: global, 104 105 /** 106 * Settings for the Gentics Content.Node JavaScript API. 107 * 108 * @type {object<string, object>} 109 */ 110 settings: { 111 112 /** 113 * The language code with which to render tags. 114 * 115 * @const 116 * @name settings.lang 117 * @default 'en' 118 * @memberOf GCN 119 * @type {string} 120 */ 121 lang: 'en', 122 123 /** 124 * Default GCN backend path. Do not add a trailing slash here. 125 * 126 * @const 127 * @default '/CNPortletapp' 128 * @name settings.BACKEND_PATH 129 * @memberOf GCN 130 * @type {string} 131 */ 132 BACKEND_PATH: '/CNPortletapp', 133 134 /** 135 * The keyword for the construct that defines Aloha Editor links. In 136 * most Content.Node installations this will be "gtxalohapagelink", 137 * but can be otherwise defined. 138 * 139 * @const 140 * @default 'gtxalohapagelink' 141 * @name settings.MAGIC_LINK 142 * @memberOf GCN 143 * @type {string} 144 */ 145 MAGIC_LINK: 'gtxalohapagelink', 146 147 /** 148 * Determines whether links will be rendered as back-end urls or 149 * front-end urls. Can either be set to "backend" or "frontend". 150 * 151 * @const 152 * @default 'backend' 153 * @name settings.linksRenderMode 154 * @memberOf GCN 155 * @type {string} 156 */ 157 linksRenderMode: 'backend', 158 159 /** 160 * The default callback to determine if a URL is an internal link. 161 * 162 * Matches the given href agains <code>INTERNAL_PAGEURL</code> and 163 * <code>INTERNAL_FILEURL</code> respectively. 164 * 165 * @const 166 * @type {function} 167 * @memberOf GCN 168 * @name settings.checkForInternalLink 169 * @param {string} href The URL to be checked. 170 * @return {object} An object containing the fields 171 * <ul> 172 * <li>match (boolean): indicating if the URL is internal</li> 173 * <li>url (string|integer): The ID of the internal page or 0 if 174 * the URL points to an internal file, or the external URL</li> 175 * <li>fileurl (string|integer): The ID of the internal file, and 176 * zero if the URL is external or points to a page.</li> 177 * </ul> 178 */ 179 checkForInternalLink: function (href) { 180 var urlMatch = href.match(INTERNAL_PAGEURL); 181 182 if (urlMatch) { 183 return { match: true, url: parseInt(urlMatch[1], 10), fileurl: 0 }; 184 } 185 186 urlMatch = href.match(INTERNAL_FILEURL); 187 188 if (urlMatch) { 189 return { match: true, url: 0, fileurl: parseInt(urlMatch[1], 10) }; 190 } 191 192 return { match: false, url: href, fileurl: 0 }; 193 }, 194 195 /** 196 * Set a channelid to work on for multichannelling or false if no 197 * channel should be used 198 * 199 * @memberOf GCN 200 * @default false 201 * @type {bool|int|string} 202 */ 203 channel: false 204 }, 205 206 /** 207 * Publish a message 208 * 209 * @param {string} message channel name 210 * @param {*=} params 211 */ 212 pub: function (channel, params) { 213 if (!hasOnErrorHandler && channel === 'error-encountered') { 214 // throw an error if there is no subscription to 215 // error-encountered. 216 throw params; 217 } 218 219 switch (channel) { 220 case 'tag.rendered': 221 case 'page.rendered': 222 case 'content-rendered': 223 // for these channels, we need to have a custom implementation: 224 // param[0] is the html of the rendered tag 225 // param[1] is the tag object 226 // param[2] is the callback, that must be called from the event handler 227 // If more than one handler subscribed, we will chain them by calling the 228 // next handler in the callback of the previous. Only the callback of the 229 // last handler will call the original callback 230 if (jQuery.isArray(onRenderHandler[channel])) { 231 var handlers = onRenderHandler[channel]; 232 // substitute callback function with wrapper 233 var callback = params[2], i = 0; 234 params[2] = function (html) { 235 if (++i < handlers.length) { 236 // call the next handler. Pass the html returned from the 237 // previous callback to the next handler 238 params[0] = html; 239 handlers[i].apply(null, params); 240 } else { 241 // there are no more handlers, so call the original 242 // callback with the final html 243 callback(html); 244 } 245 }; 246 handlers[0].apply(null, params); 247 } 248 return; 249 } 250 // errors and exceptions in (custom-)event-handler should not stop the execution of GCN-JS-API methods 251 try { 252 jQuery(GCN).trigger(channel, params); 253 } catch (err) { 254 // include the original error and stack in the error message 255 var msg = 'Encountered error when publishing message for channel "' + channel + '" with following details: \n'; 256 msg += 'original error: ' + err + '\n'; 257 // check if the stack property exists on the error object because it is non-standard 258 if (typeof err.stack !== 'undefined') { 259 msg += 'and stack: \n' + err.stack; 260 } 261 GCN.error('PUBSUB_HANDLER_FAILED', msg); 262 } 263 }, 264 265 /** 266 * Subscribe to a message channel 267 * 268 * @param {string} message channel name 269 * @param {function} handler function - message parameters will be 270 * passed. 271 */ 272 sub: function (channel, handler) { 273 // register default handlers 274 switch (channel) { 275 case 'error-encountered': 276 hasOnErrorHandler = true; 277 break; 278 case 'tag.rendered': 279 case 'page.rendered': 280 case 'content-rendered': 281 // store all the handlers in an array. 282 onRenderHandler[channel] = onRenderHandler[channel] || []; 283 onRenderHandler[channel].push(handler); 284 return; 285 case 'authentication-required': 286 case 'session.authentication-required': 287 hasAuthenticationHandler = true; 288 break; 289 } 290 291 jQuery(GCN).bind(channel, function (event, param1, param2, param3) { 292 handler(param1, param2, param3); 293 }); 294 }, 295 296 /** 297 * Tigger an error message 'error-encountered'. 298 * 299 * @param {string} error code 300 * @param {string} error message 301 * @param {object} additional error data 302 */ 303 error: function (code, message, data) { 304 var error = new GCNError(code, message, data); 305 this.pub('error-encountered', error); 306 }, 307 308 /** 309 * Returns an object containing the formal error fields. The object 310 * contains a `toString' method to print any uncaught exceptions 311 * nicely. 312 * 313 * @param {string} code 314 * @param {string} message 315 * @param {object} data 316 * @return {GCNError} 317 */ 318 createError: function (code, message, data) { 319 return new GCNError(code, message, data); 320 }, 321 322 /** 323 * Wraps the `jQuery.ajax()' method. 324 * 325 * @public 326 * @param {object} settings 327 * @throws HTTP_ERROR 328 */ 329 ajax: function (settings) { 330 if (settings.json) { 331 settings.data = JSON.stringify(settings.json); 332 delete settings.json; 333 } 334 settings.dataType = 'json'; 335 settings.contentType = 'application/json; charset=utf-8'; 336 jQuery.ajax(settings); 337 }, 338 339 /** 340 * Set links render mode if a parameter is given 341 * retrieve it if not 342 * 343 * @param {string} mode 344 * @return {string} mode 345 */ 346 linksRenderMode: function (mode) { 347 if (mode) { 348 GCN.settings.linksRenderMode = mode; 349 } 350 return GCN.settings.linksRenderMode; 351 }, 352 353 /** 354 * Set channel if a parameter is given retrieve it otherwise. 355 * 356 * If you don't want to work on a channel just set it to false, which 357 * is the default value. 358 * 359 * @param {string|boolean} channel The id of the channel to be set or false to unset the channel. 360 * @return {string} current channel id. 361 */ 362 channel: function (channel) { 363 if (channel || false === channel) { 364 GCN.settings.channel = channel; 365 } 366 return GCN.settings.channel; 367 }, 368 369 /** 370 * Constructs the nodeId query parameter for rest calls. 371 * 372 * @param {AbstractContentObject} contentObject A content object instance. 373 * @param {string=} delimiter Optional delimiter character. 374 * @return {string} Query parameter string. 375 */ 376 _getChannelParameter: function (contentObject, delimiter) { 377 if (false === contentObject._channel) { 378 return ''; 379 } 380 return (delimiter || '?') + 'nodeId=' + contentObject._channel; 381 }, 382 383 /** 384 * @param {string} html Rendered content 385 * @param {Chainback} obj The rendered ContentObject. 386 * @param {function(html)} callback Receives the processed html. 387 */ 388 _handleContentRendered: function (html, obj, callback) { 389 var channel = obj._type + '.rendered'; 390 if (onRenderHandler[channel]) { 391 GCN.pub(channel, [html, obj, callback]); 392 } else if (onRenderHandler['content-rendered']) { 393 // Because 'content-rendered' has been deprecated in favor of 394 // '{tag|page}.rendered'. 395 GCN.pub('content-rendered', [html, obj, callback]); 396 } else { 397 callback(html); 398 } 399 }, 400 401 /** 402 * Handles the ajax transport error. It will invoke the custom error 403 * handler if one is provided, and propagate the error onto the global 404 * handler if the an error handler does not return `false'. 405 * 406 * @param {object} xhr 407 * @param {string} msg The error message 408 * @param {function} handler Custom error handler. 409 * @throws HTTP_ERROR 410 */ 411 handleHttpError: function (xhr, msg, handler) { 412 var throwException = true; 413 414 if (handler) { 415 throwException = handler(GCN.createError('HTTP_ERROR', msg, 416 xhr)); 417 } 418 419 if (throwException !== 'false') { 420 GCN.error('HTTP_ERROR', msg, xhr); 421 } 422 }, 423 424 /** 425 * Handles error that occur when an ajax request succeeds but the 426 * backend responds with an error. 427 * 428 * @param {object} reponse The REST API response object. 429 * @param {function(GCNError):boolean} handler Custom error handler. 430 */ 431 handleResponseError: function (response, handler) { 432 var info = response.responseInfo; 433 var throwException = true; 434 435 if (handler) { 436 throwException = handler(GCN.createError( 437 info.responseCode, 438 info.responseMessage, 439 response 440 )); 441 } 442 443 if (throwException !== false) { 444 GCN.error(info.responseCode, info.responseMessage, response); 445 } 446 }, 447 448 /** 449 * Tiggers the GCN error event. 450 * 451 * @param {GCNError} error 452 * @param {function(GCNError):boolean} handler Custom error handler. 453 * @return {boolean} Whether or not to the exception was thrown. 454 */ 455 handleError: function (error, handler) { 456 var throwException = true; 457 458 if (handler) { 459 throwException = handler(error); 460 } 461 462 if (throwException !== false) { 463 GCN.error(error.code, error.message, error.data); 464 } 465 466 return throwException; 467 }, 468 469 /** 470 * Check if an authentication handler has been registered. 471 * 472 * @return {boolean} True if an handler for the 473 * 'authentication-required' message has been 474 * registered. 475 */ 476 _hasAuthenticationHandler: function () { 477 return hasAuthenticationHandler; 478 } 479 480 }); 481 482 // Expose the Gentics Content.Node JavaScript API to the global context. 483 // This will be `window' in most cases. 484 return (global.GCN = GCN); 485 486 }(typeof global !== 'undefined' ? global : window)); 487