From 1d86419cc1fe0824ce1f3d7fa830f2a33560cb21 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Fri, 28 Nov 2014 22:25:42 -0800 Subject: [PATCH 1/3] GUAC-605: Use MessageFormat interpolation for angular-translate. --- guacamole/pom.xml | 2 + .../index/config/indexTranslationConfig.js | 7 + .../webapp/lib/messageformat/messageformat.js | 1593 +++++++++++++++++ ...r-translate-interpolation-messageformat.js | 62 + 4 files changed, 1664 insertions(+) create mode 100644 guacamole/src/main/webapp/lib/messageformat/messageformat.js create mode 100644 guacamole/src/main/webapp/lib/plugins/angular-translate-interpolation-messageformat.js diff --git a/guacamole/pom.xml b/guacamole/pom.xml index 1d99627cf..c07e8b308 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -135,9 +135,11 @@ lib/plugins/angular-route.js lib/plugins/angular-translate.js lib/plugins/angular-translate-loader-static-files.js + lib/plugins/angular-translate-interpolation-messageformat.js lib/plugins/modal.min.js lib/blob/blob.js lib/filesaver/filesaver.js + lib/messageformat/messageformat.js license.txt guacamole-common-js/all.js scripts/session.js diff --git a/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js b/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js index 237112493..658b45288 100644 --- a/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js +++ b/guacamole/src/main/webapp/app/index/config/indexTranslationConfig.js @@ -24,10 +24,17 @@ * The configuration block for setting up everything having to do with i18n. */ angular.module('index').config(['$translateProvider', function($translateProvider) { + + // Use US English by default $translateProvider.preferredLanguage('en_US'); + // Load translations from static JSON files $translateProvider.useStaticFilesLoader({ prefix: 'translations/', suffix: '.json' }); + + // Provide pluralization, etc. via messageformat.js + $translateProvider.useMessageFormatInterpolation(); + }]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/lib/messageformat/messageformat.js b/guacamole/src/main/webapp/lib/messageformat/messageformat.js new file mode 100644 index 000000000..b8ba9e3b3 --- /dev/null +++ b/guacamole/src/main/webapp/lib/messageformat/messageformat.js @@ -0,0 +1,1593 @@ +/** + * messageformat.js + * + * ICU PluralFormat + SelectFormat for JavaScript + * + * @author Alex Sexton - @SlexAxton + * @version 0.1.7 + * @license WTFPL + * @contributor_license Dojo CLA +*/ +(function ( root ) { + + // Create the contructor function + function MessageFormat ( locale, pluralFunc ) { + var fallbackLocale; + + if ( locale && pluralFunc ) { + MessageFormat.locale[ locale ] = pluralFunc; + } + + // Defaults + fallbackLocale = locale = locale || "en"; + pluralFunc = pluralFunc || MessageFormat.locale[ fallbackLocale = MessageFormat.Utils.getFallbackLocale( locale ) ]; + + if ( ! pluralFunc ) { + throw new Error( "Plural Function not found for locale: " + locale ); + } + + // Own Properties + this.pluralFunc = pluralFunc; + this.locale = locale; + this.fallbackLocale = fallbackLocale; + } + + // Set up the locales object. Add in english by default + MessageFormat.locale = { + "en" : function ( n ) { + if ( n === 1 ) { + return "one"; + } + return "other"; + } + }; + + // Build out our basic SafeString type + // more or less stolen from Handlebars by @wycats + MessageFormat.SafeString = function( string ) { + this.string = string; + }; + + MessageFormat.SafeString.prototype.toString = function () { + return this.string.toString(); + }; + + MessageFormat.Utils = { + numSub : function ( string, key, depth ) { + // make sure that it's not an escaped octothorpe + return string.replace( /^#|[^\\]#/g, function (m) { + var prefix = m && m.length === 2 ? m.charAt(0) : ''; + return prefix + '" + (function(){ var x = ' + + key+';\nif( isNaN(x) ){\nthrow new Error("MessageFormat: `"+lastkey_'+depth+'+"` isnt a number.");\n}\nreturn x;\n})() + "'; + }); + }, + escapeExpression : function (string) { + var escape = { + "\n": "\\n", + "\"": '\\"' + }, + badChars = /[\n"]/g, + possible = /[\n"]/, + escapeChar = function(chr) { + return escape[chr] || "&"; + }; + + // Don't escape SafeStrings, since they're already safe + if ( string instanceof MessageFormat.SafeString ) { + return string.toString(); + } + else if ( string === null || string === false ) { + return ""; + } + + if ( ! possible.test( string ) ) { + return string; + } + return string.replace( badChars, escapeChar ); + }, + getFallbackLocale: function( locale ) { + var tagSeparator = locale.indexOf("-") >= 0 ? "-" : "_"; + + // Lets just be friends, fallback through the language tags + while ( ! MessageFormat.locale.hasOwnProperty( locale ) ) { + locale = locale.substring(0, locale.lastIndexOf( tagSeparator )); + if (locale.length === 0) { + return null; + } + } + + return locale; + } + }; + + // This is generated and pulled in for browsers. + var mparser = (function(){ + /* + * Generated by PEG.js 0.7.0. + * + * http://pegjs.majda.cz/ + */ + + function quote(s) { + /* + * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a + * string literal except for the closing quote character, backslash, + * carriage return, line separator, paragraph separator, and line feed. + * Any character may appear in the form of an escape sequence. + * + * For portability, we also escape escape all control and non-ASCII + * characters. Note that "\0" and "\v" escape sequences are not used + * because JSHint does not like the first and IE the second. + */ + return '"' + s + .replace(/\\/g, '\\\\') // backslash + .replace(/"/g, '\\"') // closing quote character + .replace(/\x08/g, '\\b') // backspace + .replace(/\t/g, '\\t') // horizontal tab + .replace(/\n/g, '\\n') // line feed + .replace(/\f/g, '\\f') // form feed + .replace(/\r/g, '\\r') // carriage return + .replace(/[\x00-\x07\x0B\x0E-\x1F\x80-\uFFFF]/g, escape) + + '"'; + } + + var result = { + /* + * Parses the input with a generated parser. If the parsing is successfull, + * returns a value explicitly or implicitly specified by the grammar from + * which the parser was generated (see |PEG.buildParser|). If the parsing is + * unsuccessful, throws |PEG.parser.SyntaxError| describing the error. + */ + parse: function(input, startRule) { + var parseFunctions = { + "start": parse_start, + "messageFormatPattern": parse_messageFormatPattern, + "messageFormatPatternRight": parse_messageFormatPatternRight, + "messageFormatElement": parse_messageFormatElement, + "elementFormat": parse_elementFormat, + "pluralStyle": parse_pluralStyle, + "selectStyle": parse_selectStyle, + "pluralFormatPattern": parse_pluralFormatPattern, + "offsetPattern": parse_offsetPattern, + "selectFormatPattern": parse_selectFormatPattern, + "pluralForms": parse_pluralForms, + "stringKey": parse_stringKey, + "string": parse_string, + "id": parse_id, + "chars": parse_chars, + "char": parse_char, + "digits": parse_digits, + "hexDigit": parse_hexDigit, + "_": parse__, + "whitespace": parse_whitespace + }; + + if (startRule !== undefined) { + if (parseFunctions[startRule] === undefined) { + throw new Error("Invalid rule name: " + quote(startRule) + "."); + } + } else { + startRule = "start"; + } + + var pos = 0; + var reportFailures = 0; + var rightmostFailuresPos = 0; + var rightmostFailuresExpected = []; + + function padLeft(input, padding, length) { + var result = input; + + var padLength = length - input.length; + for (var i = 0; i < padLength; i++) { + result = padding + result; + } + + return result; + } + + function escape(ch) { + var charCode = ch.charCodeAt(0); + var escapeChar; + var length; + + if (charCode <= 0xFF) { + escapeChar = 'x'; + length = 2; + } else { + escapeChar = 'u'; + length = 4; + } + + return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length); + } + + function matchFailed(failure) { + if (pos < rightmostFailuresPos) { + return; + } + + if (pos > rightmostFailuresPos) { + rightmostFailuresPos = pos; + rightmostFailuresExpected = []; + } + + rightmostFailuresExpected.push(failure); + } + + function parse_start() { + var result0; + var pos0; + + pos0 = pos; + result0 = parse_messageFormatPattern(); + if (result0 !== null) { + result0 = (function(offset, messageFormatPattern) { return { type: "program", program: messageFormatPattern }; })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_messageFormatPattern() { + var result0, result1, result2; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + result0 = parse_string(); + if (result0 !== null) { + result1 = []; + result2 = parse_messageFormatPatternRight(); + while (result2 !== null) { + result1.push(result2); + result2 = parse_messageFormatPatternRight(); + } + if (result1 !== null) { + result0 = [result0, result1]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, s1, inner) { + var st = []; + if ( s1 && s1.val ) { + st.push( s1 ); + } + for( var i in inner ){ + if ( inner.hasOwnProperty( i ) ) { + st.push( inner[ i ] ); + } + } + return { type: 'messageFormatPattern', statements: st }; + })(pos0, result0[0], result0[1]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_messageFormatPatternRight() { + var result0, result1, result2, result3, result4, result5; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + if (input.charCodeAt(pos) === 123) { + result0 = "{"; + pos++; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"{\""); + } + } + if (result0 !== null) { + result1 = parse__(); + if (result1 !== null) { + result2 = parse_messageFormatElement(); + if (result2 !== null) { + result3 = parse__(); + if (result3 !== null) { + if (input.charCodeAt(pos) === 125) { + result4 = "}"; + pos++; + } else { + result4 = null; + if (reportFailures === 0) { + matchFailed("\"}\""); + } + } + if (result4 !== null) { + result5 = parse_string(); + if (result5 !== null) { + result0 = [result0, result1, result2, result3, result4, result5]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, mfe, s1) { + var res = []; + if ( mfe ) { + res.push(mfe); + } + if ( s1 && s1.val ) { + res.push( s1 ); + } + return { type: "messageFormatPatternRight", statements : res }; + })(pos0, result0[2], result0[5]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_messageFormatElement() { + var result0, result1, result2; + var pos0, pos1, pos2; + + pos0 = pos; + pos1 = pos; + result0 = parse_id(); + if (result0 !== null) { + pos2 = pos; + if (input.charCodeAt(pos) === 44) { + result1 = ","; + pos++; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("\",\""); + } + } + if (result1 !== null) { + result2 = parse_elementFormat(); + if (result2 !== null) { + result1 = [result1, result2]; + } else { + result1 = null; + pos = pos2; + } + } else { + result1 = null; + pos = pos2; + } + result1 = result1 !== null ? result1 : ""; + if (result1 !== null) { + result0 = [result0, result1]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, argIdx, efmt) { + var res = { + type: "messageFormatElement", + argumentIndex: argIdx + }; + if ( efmt && efmt.length ) { + res.elementFormat = efmt[1]; + } + else { + res.output = true; + } + return res; + })(pos0, result0[0], result0[1]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_elementFormat() { + var result0, result1, result2, result3, result4, result5, result6; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + result0 = parse__(); + if (result0 !== null) { + if (input.substr(pos, 6) === "plural") { + result1 = "plural"; + pos += 6; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("\"plural\""); + } + } + if (result1 !== null) { + result2 = parse__(); + if (result2 !== null) { + if (input.charCodeAt(pos) === 44) { + result3 = ","; + pos++; + } else { + result3 = null; + if (reportFailures === 0) { + matchFailed("\",\""); + } + } + if (result3 !== null) { + result4 = parse__(); + if (result4 !== null) { + result5 = parse_pluralStyle(); + if (result5 !== null) { + result6 = parse__(); + if (result6 !== null) { + result0 = [result0, result1, result2, result3, result4, result5, result6]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, t, s) { + return { + type : "elementFormat", + key : t, + val : s.val + }; + })(pos0, result0[1], result0[5]); + } + if (result0 === null) { + pos = pos0; + } + if (result0 === null) { + pos0 = pos; + pos1 = pos; + result0 = parse__(); + if (result0 !== null) { + if (input.substr(pos, 6) === "select") { + result1 = "select"; + pos += 6; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("\"select\""); + } + } + if (result1 !== null) { + result2 = parse__(); + if (result2 !== null) { + if (input.charCodeAt(pos) === 44) { + result3 = ","; + pos++; + } else { + result3 = null; + if (reportFailures === 0) { + matchFailed("\",\""); + } + } + if (result3 !== null) { + result4 = parse__(); + if (result4 !== null) { + result5 = parse_selectStyle(); + if (result5 !== null) { + result6 = parse__(); + if (result6 !== null) { + result0 = [result0, result1, result2, result3, result4, result5, result6]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, t, s) { + return { + type : "elementFormat", + key : t, + val : s.val + }; + })(pos0, result0[1], result0[5]); + } + if (result0 === null) { + pos = pos0; + } + } + return result0; + } + + function parse_pluralStyle() { + var result0; + var pos0; + + pos0 = pos; + result0 = parse_pluralFormatPattern(); + if (result0 !== null) { + result0 = (function(offset, pfp) { + return { type: "pluralStyle", val: pfp }; + })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_selectStyle() { + var result0; + var pos0; + + pos0 = pos; + result0 = parse_selectFormatPattern(); + if (result0 !== null) { + result0 = (function(offset, sfp) { + return { type: "selectStyle", val: sfp }; + })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_pluralFormatPattern() { + var result0, result1, result2; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + result0 = parse_offsetPattern(); + result0 = result0 !== null ? result0 : ""; + if (result0 !== null) { + result1 = []; + result2 = parse_pluralForms(); + while (result2 !== null) { + result1.push(result2); + result2 = parse_pluralForms(); + } + if (result1 !== null) { + result0 = [result0, result1]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, op, pf) { + var res = { + type: "pluralFormatPattern", + pluralForms: pf + }; + if ( op ) { + res.offset = op; + } + else { + res.offset = 0; + } + return res; + })(pos0, result0[0], result0[1]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_offsetPattern() { + var result0, result1, result2, result3, result4, result5, result6; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + result0 = parse__(); + if (result0 !== null) { + if (input.substr(pos, 6) === "offset") { + result1 = "offset"; + pos += 6; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("\"offset\""); + } + } + if (result1 !== null) { + result2 = parse__(); + if (result2 !== null) { + if (input.charCodeAt(pos) === 58) { + result3 = ":"; + pos++; + } else { + result3 = null; + if (reportFailures === 0) { + matchFailed("\":\""); + } + } + if (result3 !== null) { + result4 = parse__(); + if (result4 !== null) { + result5 = parse_digits(); + if (result5 !== null) { + result6 = parse__(); + if (result6 !== null) { + result0 = [result0, result1, result2, result3, result4, result5, result6]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, d) { + return d; + })(pos0, result0[5]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_selectFormatPattern() { + var result0, result1; + var pos0; + + pos0 = pos; + result0 = []; + result1 = parse_pluralForms(); + while (result1 !== null) { + result0.push(result1); + result1 = parse_pluralForms(); + } + if (result0 !== null) { + result0 = (function(offset, pf) { + return { + type: "selectFormatPattern", + pluralForms: pf + }; + })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_pluralForms() { + var result0, result1, result2, result3, result4, result5, result6, result7; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + result0 = parse__(); + if (result0 !== null) { + result1 = parse_stringKey(); + if (result1 !== null) { + result2 = parse__(); + if (result2 !== null) { + if (input.charCodeAt(pos) === 123) { + result3 = "{"; + pos++; + } else { + result3 = null; + if (reportFailures === 0) { + matchFailed("\"{\""); + } + } + if (result3 !== null) { + result4 = parse__(); + if (result4 !== null) { + result5 = parse_messageFormatPattern(); + if (result5 !== null) { + result6 = parse__(); + if (result6 !== null) { + if (input.charCodeAt(pos) === 125) { + result7 = "}"; + pos++; + } else { + result7 = null; + if (reportFailures === 0) { + matchFailed("\"}\""); + } + } + if (result7 !== null) { + result0 = [result0, result1, result2, result3, result4, result5, result6, result7]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, k, mfp) { + return { + type: "pluralForms", + key: k, + val: mfp + }; + })(pos0, result0[1], result0[5]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_stringKey() { + var result0, result1; + var pos0, pos1; + + pos0 = pos; + result0 = parse_id(); + if (result0 !== null) { + result0 = (function(offset, i) { + return i; + })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + if (result0 === null) { + pos0 = pos; + pos1 = pos; + if (input.charCodeAt(pos) === 61) { + result0 = "="; + pos++; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"=\""); + } + } + if (result0 !== null) { + result1 = parse_digits(); + if (result1 !== null) { + result0 = [result0, result1]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, d) { + return d; + })(pos0, result0[1]); + } + if (result0 === null) { + pos = pos0; + } + } + return result0; + } + + function parse_string() { + var result0, result1, result2, result3, result4; + var pos0, pos1, pos2; + + pos0 = pos; + pos1 = pos; + result0 = parse__(); + if (result0 !== null) { + result1 = []; + pos2 = pos; + result2 = parse__(); + if (result2 !== null) { + result3 = parse_chars(); + if (result3 !== null) { + result4 = parse__(); + if (result4 !== null) { + result2 = [result2, result3, result4]; + } else { + result2 = null; + pos = pos2; + } + } else { + result2 = null; + pos = pos2; + } + } else { + result2 = null; + pos = pos2; + } + while (result2 !== null) { + result1.push(result2); + pos2 = pos; + result2 = parse__(); + if (result2 !== null) { + result3 = parse_chars(); + if (result3 !== null) { + result4 = parse__(); + if (result4 !== null) { + result2 = [result2, result3, result4]; + } else { + result2 = null; + pos = pos2; + } + } else { + result2 = null; + pos = pos2; + } + } else { + result2 = null; + pos = pos2; + } + } + if (result1 !== null) { + result0 = [result0, result1]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, ws, s) { + var tmp = []; + for( var i = 0; i < s.length; ++i ) { + for( var j = 0; j < s[ i ].length; ++j ) { + tmp.push(s[i][j]); + } + } + return { + type: "string", + val: ws + tmp.join('') + }; + })(pos0, result0[0], result0[1]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_id() { + var result0, result1, result2, result3; + var pos0, pos1; + + pos0 = pos; + pos1 = pos; + result0 = parse__(); + if (result0 !== null) { + if (/^[a-zA-Z$_]/.test(input.charAt(pos))) { + result1 = input.charAt(pos); + pos++; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("[a-zA-Z$_]"); + } + } + if (result1 !== null) { + result2 = []; + if (/^[^ \t\n\r,.+={}]/.test(input.charAt(pos))) { + result3 = input.charAt(pos); + pos++; + } else { + result3 = null; + if (reportFailures === 0) { + matchFailed("[^ \\t\\n\\r,.+={}]"); + } + } + while (result3 !== null) { + result2.push(result3); + if (/^[^ \t\n\r,.+={}]/.test(input.charAt(pos))) { + result3 = input.charAt(pos); + pos++; + } else { + result3 = null; + if (reportFailures === 0) { + matchFailed("[^ \\t\\n\\r,.+={}]"); + } + } + } + if (result2 !== null) { + result3 = parse__(); + if (result3 !== null) { + result0 = [result0, result1, result2, result3]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, s1, s2) { + return s1 + (s2 ? s2.join('') : ''); + })(pos0, result0[1], result0[2]); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_chars() { + var result0, result1; + var pos0; + + pos0 = pos; + result1 = parse_char(); + if (result1 !== null) { + result0 = []; + while (result1 !== null) { + result0.push(result1); + result1 = parse_char(); + } + } else { + result0 = null; + } + if (result0 !== null) { + result0 = (function(offset, chars) { return chars.join(''); })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_char() { + var result0, result1, result2, result3, result4; + var pos0, pos1; + + pos0 = pos; + if (/^[^{}\\\0-\x1F \t\n\r]/.test(input.charAt(pos))) { + result0 = input.charAt(pos); + pos++; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("[^{}\\\\\\0-\\x1F \\t\\n\\r]"); + } + } + if (result0 !== null) { + result0 = (function(offset, x) { + return x; + })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + if (result0 === null) { + pos0 = pos; + if (input.substr(pos, 2) === "\\#") { + result0 = "\\#"; + pos += 2; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"\\\\#\""); + } + } + if (result0 !== null) { + result0 = (function(offset) { + return "\\#"; + })(pos0); + } + if (result0 === null) { + pos = pos0; + } + if (result0 === null) { + pos0 = pos; + if (input.substr(pos, 2) === "\\{") { + result0 = "\\{"; + pos += 2; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"\\\\{\""); + } + } + if (result0 !== null) { + result0 = (function(offset) { + return "\u007B"; + })(pos0); + } + if (result0 === null) { + pos = pos0; + } + if (result0 === null) { + pos0 = pos; + if (input.substr(pos, 2) === "\\}") { + result0 = "\\}"; + pos += 2; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"\\\\}\""); + } + } + if (result0 !== null) { + result0 = (function(offset) { + return "\u007D"; + })(pos0); + } + if (result0 === null) { + pos = pos0; + } + if (result0 === null) { + pos0 = pos; + pos1 = pos; + if (input.substr(pos, 2) === "\\u") { + result0 = "\\u"; + pos += 2; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("\"\\\\u\""); + } + } + if (result0 !== null) { + result1 = parse_hexDigit(); + if (result1 !== null) { + result2 = parse_hexDigit(); + if (result2 !== null) { + result3 = parse_hexDigit(); + if (result3 !== null) { + result4 = parse_hexDigit(); + if (result4 !== null) { + result0 = [result0, result1, result2, result3, result4]; + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + } else { + result0 = null; + pos = pos1; + } + if (result0 !== null) { + result0 = (function(offset, h1, h2, h3, h4) { + return String.fromCharCode(parseInt("0x" + h1 + h2 + h3 + h4)); + })(pos0, result0[1], result0[2], result0[3], result0[4]); + } + if (result0 === null) { + pos = pos0; + } + } + } + } + } + return result0; + } + + function parse_digits() { + var result0, result1; + var pos0; + + pos0 = pos; + if (/^[0-9]/.test(input.charAt(pos))) { + result1 = input.charAt(pos); + pos++; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("[0-9]"); + } + } + if (result1 !== null) { + result0 = []; + while (result1 !== null) { + result0.push(result1); + if (/^[0-9]/.test(input.charAt(pos))) { + result1 = input.charAt(pos); + pos++; + } else { + result1 = null; + if (reportFailures === 0) { + matchFailed("[0-9]"); + } + } + } + } else { + result0 = null; + } + if (result0 !== null) { + result0 = (function(offset, ds) { + return parseInt((ds.join('')), 10); + })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + return result0; + } + + function parse_hexDigit() { + var result0; + + if (/^[0-9a-fA-F]/.test(input.charAt(pos))) { + result0 = input.charAt(pos); + pos++; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("[0-9a-fA-F]"); + } + } + return result0; + } + + function parse__() { + var result0, result1; + var pos0; + + reportFailures++; + pos0 = pos; + result0 = []; + result1 = parse_whitespace(); + while (result1 !== null) { + result0.push(result1); + result1 = parse_whitespace(); + } + if (result0 !== null) { + result0 = (function(offset, w) { return w.join(''); })(pos0, result0); + } + if (result0 === null) { + pos = pos0; + } + reportFailures--; + if (reportFailures === 0 && result0 === null) { + matchFailed("whitespace"); + } + return result0; + } + + function parse_whitespace() { + var result0; + + if (/^[ \t\n\r]/.test(input.charAt(pos))) { + result0 = input.charAt(pos); + pos++; + } else { + result0 = null; + if (reportFailures === 0) { + matchFailed("[ \\t\\n\\r]"); + } + } + return result0; + } + + + function cleanupExpected(expected) { + expected.sort(); + + var lastExpected = null; + var cleanExpected = []; + for (var i = 0; i < expected.length; i++) { + if (expected[i] !== lastExpected) { + cleanExpected.push(expected[i]); + lastExpected = expected[i]; + } + } + return cleanExpected; + } + + function computeErrorPosition() { + /* + * The first idea was to use |String.split| to break the input up to the + * error position along newlines and derive the line and column from + * there. However IE's |split| implementation is so broken that it was + * enough to prevent it. + */ + + var line = 1; + var column = 1; + var seenCR = false; + + for (var i = 0; i < Math.max(pos, rightmostFailuresPos); i++) { + var ch = input.charAt(i); + if (ch === "\n") { + if (!seenCR) { line++; } + column = 1; + seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + line++; + column = 1; + seenCR = true; + } else { + column++; + seenCR = false; + } + } + + return { line: line, column: column }; + } + + + var result = parseFunctions[startRule](); + + /* + * The parser is now in one of the following three states: + * + * 1. The parser successfully parsed the whole input. + * + * - |result !== null| + * - |pos === input.length| + * - |rightmostFailuresExpected| may or may not contain something + * + * 2. The parser successfully parsed only a part of the input. + * + * - |result !== null| + * - |pos < input.length| + * - |rightmostFailuresExpected| may or may not contain something + * + * 3. The parser did not successfully parse any part of the input. + * + * - |result === null| + * - |pos === 0| + * - |rightmostFailuresExpected| contains at least one failure + * + * All code following this comment (including called functions) must + * handle these states. + */ + if (result === null || pos !== input.length) { + var offset = Math.max(pos, rightmostFailuresPos); + var found = offset < input.length ? input.charAt(offset) : null; + var errorPosition = computeErrorPosition(); + + throw new this.SyntaxError( + cleanupExpected(rightmostFailuresExpected), + found, + offset, + errorPosition.line, + errorPosition.column + ); + } + + return result; + }, + + /* Returns the parser source code. */ + toSource: function() { return this._source; } + }; + + /* Thrown when a parser encounters a syntax error. */ + + result.SyntaxError = function(expected, found, offset, line, column) { + function buildMessage(expected, found) { + var expectedHumanized, foundHumanized; + + switch (expected.length) { + case 0: + expectedHumanized = "end of input"; + break; + case 1: + expectedHumanized = expected[0]; + break; + default: + expectedHumanized = expected.slice(0, expected.length - 1).join(", ") + + " or " + + expected[expected.length - 1]; + } + + foundHumanized = found ? quote(found) : "end of input"; + + return "Expected " + expectedHumanized + " but " + foundHumanized + " found."; + } + + this.name = "SyntaxError"; + this.expected = expected; + this.found = found; + this.message = buildMessage(expected, found); + this.offset = offset; + this.line = line; + this.column = column; + }; + + result.SyntaxError.prototype = Error.prototype; + + return result; + })(); + + MessageFormat.prototype.parse = function () { + // Bind to itself so error handling works + return mparser.parse.apply( mparser, arguments ); + }; + + MessageFormat.prototype.precompile = function ( ast ) { + var self = this, + needOther = false, + fp = { + begin: 'function(d){\nvar r = "";\n', + end : "return r;\n}" + }; + + function interpMFP ( ast, data ) { + // Set some default data + data = data || {}; + var s = '', i, tmp, lastkeyname; + + switch ( ast.type ) { + case 'program': + return interpMFP( ast.program ); + case 'messageFormatPattern': + for ( i = 0; i < ast.statements.length; ++i ) { + s += interpMFP( ast.statements[i], data ); + } + return fp.begin + s + fp.end; + case 'messageFormatPatternRight': + for ( i = 0; i < ast.statements.length; ++i ) { + s += interpMFP( ast.statements[i], data ); + } + return s; + case 'messageFormatElement': + data.pf_count = data.pf_count || 0; + s += 'if(!d){\nthrow new Error("MessageFormat: No data passed to function.");\n}\n'; + if ( ast.output ) { + s += 'r += d["' + ast.argumentIndex + '"];\n'; + } + else { + lastkeyname = 'lastkey_'+(data.pf_count+1); + s += 'var '+lastkeyname+' = "'+ast.argumentIndex+'";\n'; + s += 'var k_'+(data.pf_count+1)+'=d['+lastkeyname+'];\n'; + s += interpMFP( ast.elementFormat, data ); + } + return s; + case 'elementFormat': + if ( ast.key === 'select' ) { + s += interpMFP( ast.val, data ); + s += 'r += (pf_' + + data.pf_count + + '[ k_' + (data.pf_count+1) + ' ] || pf_'+data.pf_count+'[ "other" ])( d );\n'; + } + else if ( ast.key === 'plural' ) { + s += interpMFP( ast.val, data ); + s += 'if ( pf_'+(data.pf_count)+'[ k_'+(data.pf_count+1)+' + "" ] ) {\n'; + s += 'r += pf_'+data.pf_count+'[ k_'+(data.pf_count+1)+' + "" ]( d ); \n'; + s += '}\nelse {\n'; + s += 'r += (pf_' + + data.pf_count + + '[ MessageFormat.locale["' + + self.fallbackLocale + + '"]( k_'+(data.pf_count+1)+' - off_'+(data.pf_count)+' ) ] || pf_'+data.pf_count+'[ "other" ] )( d );\n'; + s += '}\n'; + } + return s; + /* // Unreachable cases. + case 'pluralStyle': + case 'selectStyle':*/ + case 'pluralFormatPattern': + data.pf_count = data.pf_count || 0; + s += 'var off_'+data.pf_count+' = '+ast.offset+';\n'; + s += 'var pf_' + data.pf_count + ' = { \n'; + needOther = true; + // We're going to simultaneously check to make sure we hit the required 'other' option. + + for ( i = 0; i < ast.pluralForms.length; ++i ) { + if ( ast.pluralForms[ i ].key === 'other' ) { + needOther = false; + } + if ( tmp ) { + s += ',\n'; + } + else{ + tmp = 1; + } + s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val, + (function(){ var res = JSON.parse(JSON.stringify(data)); res.pf_count++; return res; })() ); + } + s += '\n};\n'; + if ( needOther ) { + throw new Error("No 'other' form found in pluralFormatPattern " + data.pf_count); + } + return s; + case 'selectFormatPattern': + + data.pf_count = data.pf_count || 0; + s += 'var off_'+data.pf_count+' = 0;\n'; + s += 'var pf_' + data.pf_count + ' = { \n'; + needOther = true; + + for ( i = 0; i < ast.pluralForms.length; ++i ) { + if ( ast.pluralForms[ i ].key === 'other' ) { + needOther = false; + } + if ( tmp ) { + s += ',\n'; + } + else{ + tmp = 1; + } + s += '"' + ast.pluralForms[ i ].key + '" : ' + interpMFP( ast.pluralForms[ i ].val, + (function(){ + var res = JSON.parse( JSON.stringify( data ) ); + res.pf_count++; + return res; + })() + ); + } + s += '\n};\n'; + if ( needOther ) { + throw new Error("No 'other' form found in selectFormatPattern " + data.pf_count); + } + return s; + /* // Unreachable + case 'pluralForms': + */ + case 'string': + return 'r += "' + MessageFormat.Utils.numSub( + MessageFormat.Utils.escapeExpression( ast.val ), + 'k_' + data.pf_count + ' - off_' + ( data.pf_count - 1 ), + data.pf_count + ) + '";\n'; + default: + throw new Error( 'Bad AST type: ' + ast.type ); + } + } + return interpMFP( ast ); + }; + + MessageFormat.prototype.compile = function ( message ) { + return (new Function( 'MessageFormat', + 'return ' + + this.precompile( + this.parse( message ) + ) + ))(MessageFormat); + }; + + + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = MessageFormat; + } + exports.MessageFormat = MessageFormat; + } + else if (typeof define === 'function' && define.amd) { + define(function() { + return MessageFormat; + }); + } + else { + root['MessageFormat'] = MessageFormat; + } + +})( this ); diff --git a/guacamole/src/main/webapp/lib/plugins/angular-translate-interpolation-messageformat.js b/guacamole/src/main/webapp/lib/plugins/angular-translate-interpolation-messageformat.js new file mode 100644 index 000000000..2aa9470be --- /dev/null +++ b/guacamole/src/main/webapp/lib/plugins/angular-translate-interpolation-messageformat.js @@ -0,0 +1,62 @@ +/*! + * angular-translate - v2.2.0 - 2014-06-03 + * http://github.com/PascalPrecht/angular-translate + * Copyright (c) 2014 ; Licensed MIT + */ +angular.module('pascalprecht.translate').constant('TRANSLATE_MF_INTERPOLATION_CACHE', '$translateMessageFormatInterpolation').factory('$translateMessageFormatInterpolation', [ + '$cacheFactory', + 'TRANSLATE_MF_INTERPOLATION_CACHE', + function ($cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE) { + var $translateInterpolator = {}, $cache = $cacheFactory.get(TRANSLATE_MF_INTERPOLATION_CACHE), $mf = new MessageFormat(), $identifier = 'messageformat', $sanitizeValueStrategy = null, sanitizeValueStrategies = { + escaped: function (params) { + var result = {}; + for (var key in params) { + if (params.hasOwnProperty(key)) { + result[key] = angular.element('
').text(params[key]).html(); + } + } + return result; + } + }; + var sanitizeParams = function (params) { + var result; + if (angular.isFunction(sanitizeValueStrategies[$sanitizeValueStrategy])) { + result = sanitizeValueStrategies[$sanitizeValueStrategy](params); + } else { + result = params; + } + return result; + }; + if (!$cache) { + $cache = $cacheFactory(TRANSLATE_MF_INTERPOLATION_CACHE); + } + $cache.put('en', $mf); + $translateInterpolator.setLocale = function (locale) { + $mf = $cache.get(locale); + if (!$mf) { + $mf = new MessageFormat(locale); + $cache.put(locale, $mf); + } + }; + $translateInterpolator.getInterpolationIdentifier = function () { + return $identifier; + }; + $translateInterpolator.useSanitizeValueStrategy = function (value) { + $sanitizeValueStrategy = value; + return this; + }; + $translateInterpolator.interpolate = function (string, interpolateParams) { + interpolateParams = interpolateParams || {}; + if ($sanitizeValueStrategy) { + interpolateParams = sanitizeParams(interpolateParams); + } + var interpolatedText = $cache.get(string + angular.toJson(interpolateParams)); + if (!interpolatedText) { + interpolatedText = $mf.compile(string)(interpolateParams); + $cache.put(string + angular.toJson(interpolateParams), interpolatedText); + } + return interpolatedText; + }; + return $translateInterpolator; + } +]); \ No newline at end of file From 5b31b206a761e9b422bc5c38ad6d571a3040fb5c Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 29 Nov 2014 19:18:38 -0800 Subject: [PATCH 2/3] GUAC-605: Add guacNotification directive. --- .../src/main/webapp/app/index/indexModule.js | 3 +- .../directives/guacNotification.js | 151 ++++++++++++++++++ .../app/notification/notificationModule.js | 26 +++ .../app/notification/styles/notification.css | 102 ++++++++++++ .../templates/guacNotification.html | 47 ++++++ 5 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/webapp/app/notification/directives/guacNotification.js create mode 100644 guacamole/src/main/webapp/app/notification/notificationModule.js create mode 100644 guacamole/src/main/webapp/app/notification/styles/notification.css create mode 100644 guacamole/src/main/webapp/app/notification/templates/guacNotification.html diff --git a/guacamole/src/main/webapp/app/index/indexModule.js b/guacamole/src/main/webapp/app/index/indexModule.js index 1eb80c9fd..2911a14c2 100644 --- a/guacamole/src/main/webapp/app/index/indexModule.js +++ b/guacamole/src/main/webapp/app/index/indexModule.js @@ -23,4 +23,5 @@ /** * The module for the root of the application. */ -angular.module('index', ['ngRoute', 'pascalprecht.translate', 'auth', 'home', 'manage', 'login', 'client']); +angular.module('index', ['ngRoute', 'pascalprecht.translate', + 'auth', 'home', 'manage', 'login', 'client', 'notification']); diff --git a/guacamole/src/main/webapp/app/notification/directives/guacNotification.js b/guacamole/src/main/webapp/app/notification/directives/guacNotification.js new file mode 100644 index 000000000..b37ff8ffd --- /dev/null +++ b/guacamole/src/main/webapp/app/notification/directives/guacNotification.js @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * A directive for the guacamole client. + */ +angular.module('notification').directive('guacNotification', [function guacNotification() { + + return { + restrict: 'E', + replace: true, + scope: { + + /** + * The CSS class to apply to the notification. + * + * @type String + */ + className : '=', + + /** + * The title of the notification. + * + * @type String + */ + title : '=', + + /** + * The body text of the notification. + * + * @type String + */ + text : '=', + + /** + * The text to use for displaying the countdown. For the sake of + * i18n, the variable REMAINING will be applied within the + * translation string for formatting plurals, etc. + * + * @type String + * @example + * "Only {REMAINING} {REMAINING, plural, one{second} other{seconds}} remain." + */ + countdownText : '=', + + /** + * The number of seconds to wait before automatically calling the + * default callback. + * + * @type Number + */ + countdown : '=', + + /** + * The function to call when timeRemaining expires. If timeRemaining + * is not set, this does not apply. + * + * @type Function + */ + defaultCallback : '=', + + /** + * Arbitrary value denoting how much progress has been made + * in some ongoing task that this notification represents. + * + * @type Number + */ + progress : '=', + + /** + * Array of name/callback pairs for each action the user is allowed + * to take once the notification is shown. + * + * @type Array + * @example + * [ + * { + * name : "Action 1 name", + * callback : actionCallback1 + * }, + * { + * name : "Action 2 text", + * callback : actionCallback2 + * } + * ] + */ + actions : '=' + + }, + + templateUrl: 'app/notification/templates/guacNotification.html', + controller: ['$scope', '$interval', function guacNotificationController($scope, $interval) { + + // Set countdown interval when associated property is set + $scope.$watch("countdown", function resetTimeRemaining(countdown) { + + $scope.timeRemaining = countdown; + + // Clean up any existing interval + if ($scope.interval) + $interval.cancel($scope.interval); + + // Update and handle countdown, if provided + if ($scope.timeRemaining) { + + $scope.interval = $interval(function updateTimeRemaining() { + + // Update time remaining + $scope.timeRemaining--; + + // Call countdown callback when time remaining expires + if ($scope.timeRemaining === 0 && $scope.defaultCallback) + $scope.defaultCallback(); + + }, 1000, $scope.timeRemaining); + + } + + }); + + // Clean up interval upon destruction + $scope.$on("$destroy", function destroyNotification() { + + if ($scope.interval) + $interval.cancel($scope.interval); + + }); + + }] + + }; +}]); \ No newline at end of file diff --git a/guacamole/src/main/webapp/app/notification/notificationModule.js b/guacamole/src/main/webapp/app/notification/notificationModule.js new file mode 100644 index 000000000..54c64fdfb --- /dev/null +++ b/guacamole/src/main/webapp/app/notification/notificationModule.js @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/** + * The module for code used to display arbitrary notifications. + */ +angular.module('notification', []); diff --git a/guacamole/src/main/webapp/app/notification/styles/notification.css b/guacamole/src/main/webapp/app/notification/styles/notification.css new file mode 100644 index 000000000..9322773d2 --- /dev/null +++ b/guacamole/src/main/webapp/app/notification/styles/notification.css @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +.notification { + border: 1px solid rgba(0, 0, 0, 0.125); + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.125); + background: white; + color: black; +} + +.notification .body { + margin: 0.5em; +} + +.notification .buttons { + margin: 0.5em; +} + +@keyframes notification-progress { + from {background-position: 0px 0px;} + to {background-position: 64px 0px;} +} + +@-webkit-keyframes notification-progress { + from {background-position: 0px 0px;} + to {background-position: 64px 0px;} +} + +.notification .title-bar { + font-size: 1.25em; + font-weight: bold; + + text-transform: uppercase; + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); + background: rgba(0, 0, 0, 0.04); + + padding: 0.5em; + margin-bottom: 1em; +} + +.notification .progress .bar { + background: #A3D655; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0; + box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5), + inset -1px -1px 0 rgba( 0, 0, 0, 0.1), + 1px 1px 0 gray; +} + +.notification .progress { + + width: 100%; + background: #C2C2C2 url('images/progress.png'); + background-size: 16px 16px; + -moz-background-size: 16px 16px; + -webkit-background-size: 16px 16px; + -khtml-background-size: 16px 16px; + + animation-name: notification-progress; + animation-duration: 2s; + animation-timing-function: linear; + animation-iteration-count: infinite; + + -webkit-animation-name: notification-progress; + -webkit-animation-duration: 2s; + -webkit-animation-timing-function: linear; + -webkit-animation-iteration-count: infinite; + + padding: 0.25em; + + border: 1px solid gray; + -moz-border-radius: 0.2em; + -webkit-border-radius: 0.2em; + -khtml-border-radius: 0.2em; + border-radius: 0.2em; + + position: relative; + +} diff --git a/guacamole/src/main/webapp/app/notification/templates/guacNotification.html b/guacamole/src/main/webapp/app/notification/templates/guacNotification.html new file mode 100644 index 000000000..710c9986f --- /dev/null +++ b/guacamole/src/main/webapp/app/notification/templates/guacNotification.html @@ -0,0 +1,47 @@ +
+ + + +
+
{{title | translate}}
+
+ +
+ + +

{{text | translate}}

+ + +
{{progress}}
+ + +

{{countdownText | translate:"{ REMAINING: timeRemaining}"}}

+ +
+ + +
+ +
+ +
\ No newline at end of file From ed31e0c026825eada680bafa5d3820973c1235f1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sat, 29 Nov 2014 20:54:50 -0800 Subject: [PATCH 3/3] GUAC-605: Migrate status to guacNotification. Add countdown string to en_US translation. Use 15 second reconnect countdown if appropriate for error at hand. --- .../client/controllers/clientController.js | 49 +++++- .../app/client/styles/notification-area.css | 43 +++++ .../webapp/app/client/styles/notification.css | 159 ------------------ .../app/index/controllers/indexController.js | 59 ++++--- .../main/webapp/app/index/styles/status.css | 45 ++--- guacamole/src/main/webapp/index.html | 56 +++--- .../src/main/webapp/translations/en_US.json | 3 +- 7 files changed, 166 insertions(+), 248 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/styles/notification-area.css delete mode 100644 guacamole/src/main/webapp/app/client/styles/notification.css diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 350aa3840..2ce19d486 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -51,6 +51,18 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', 0x031D: true }; + /** + * All error codes for which automatic reconnection is appropriate when a + * client error occurs. + */ + var CLIENT_AUTO_RECONNECT = { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0301: true, + 0x0308: true + }; + /** * All tunnel error codes handled and passed off for translation. Any error * code not present in this list will be represented by the "DEFAULT" @@ -67,7 +79,18 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', 0x0308: true, 0x031D: true }; - + + /** + * All error codes for which automatic reconnection is appropriate when a + * tunnel error occurs. + */ + var TUNNEL_AUTO_RECONNECT = { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0308: true + }; + /** * The reconnect action to be provided along with the object sent to * showStatus. @@ -81,6 +104,16 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', } }; + /** + * The reconnect countdown to display if an error or status warrants an + * automatic, timed reconnect. + */ + var RECONNECT_COUNTDOWN = { + text: "client.action.reconnectCountdown", + callback: RECONNECT_ACTION.callback, + remaining: 15 + }; + // Get DAO for reading connections and groups var connectionGroupDAO = $injector.get('connectionGroupDAO'); var connectionDAO = $injector.get('connectionDAO'); @@ -146,7 +179,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', return true; } - + $scope.$watch('menuShown', function setKeyboardEnabled(menuShown, menuShownPreviousState) { // Send clipboard data if menu is hidden @@ -218,7 +251,10 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', $scope.id = null; // Determine translation name of error - var errorName = (status in CLIENT_ERRORS) ? status.toString(16) : "DEFAULT"; + var errorName = (status in CLIENT_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + + // Determine whether the reconnect countdown applies + var countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; // Override any existing status $scope.showStatus(false); @@ -228,6 +264,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', className: "error", title: "client.error.connectionErrorTitle", text: "client.error.clientErrors." + errorName, + countdown: countdown, actions: [ RECONNECT_ACTION ] }); @@ -253,7 +290,10 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', $scope.id = null; // Determine translation name of error - var errorName = (status in TUNNEL_ERRORS) ? status.toString(16) : "DEFAULT"; + var errorName = (status in TUNNEL_ERRORS) ? status.toString(16).toUpperCase() : "DEFAULT"; + + // Determine whether the reconnect countdown applies + var countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; // Override any existing status $scope.showStatus(false); @@ -263,6 +303,7 @@ angular.module('home').controller('clientController', ['$scope', '$routeParams', className: "error", title: "client.error.connectionErrorTitle", text: "client.error.tunnelErrors." + errorName, + countdown: countdown, actions: [ RECONNECT_ACTION ] }); diff --git a/guacamole/src/main/webapp/app/client/styles/notification-area.css b/guacamole/src/main/webapp/app/client/styles/notification-area.css new file mode 100644 index 000000000..f3b9d7fc4 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/notification-area.css @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2013 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#notificationArea { + position: fixed; + right: 0.5em; + bottom: 0.5em; + max-width: 25%; + width: 2in; +} + +#notificationArea .notification { + font-size: 0.7em; + text-align: center; + width: 100%; + overflow: hidden; +} + +#notificationArea .notification .text { + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/guacamole/src/main/webapp/app/client/styles/notification.css b/guacamole/src/main/webapp/app/client/styles/notification.css deleted file mode 100644 index 9b559efd5..000000000 --- a/guacamole/src/main/webapp/app/client/styles/notification.css +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright (C) 2013 Glyptodon LLC - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#notificationArea { - position: fixed; - right: 0.5em; - bottom: 0.5em; - max-width: 25%; - min-width: 10em; -} - -.notification { - - font-size: 0.7em; - text-align: center; - - border: 1px solid rgba(0, 0, 0, 0.125); - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.125); - background: white; - - color: black; - - padding: 0.5em; - margin: 1em; - width: 2in; - max-width: 75%; - overflow: hidden; -} - -.notification .buttons { - margin: 0; -} - -@keyframes notification-progress { - from {background-position: 0px 0px;} - to {background-position: 64px 0px;} -} - -@-webkit-keyframes notification-progress { - from {background-position: 0px 0px;} - to {background-position: 64px 0px;} -} - -.notification .title-bar { - font-size: 1.25em; - font-weight: bold; - - text-transform: uppercase; - border-bottom: 1px solid rgba(0, 0, 0, 0.125); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); - background: rgba(0, 0, 0, 0.04); - - margin-bottom: 1em; -} - -.notification .text { - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.notification.upload .progress { - float: none; - width: 80%; - margin-left: auto; - margin-right: auto; -} - -.notification.download .progress div, -.notification.upload .progress div { - position: relative; -} - -.notification.download .progress .bar, -.notification.upload .progress .bar { - background: #A3D655; - position: absolute; - top: 0; - left: 0; - height: 100%; - width: 0; - box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5), - inset -1px -1px 0 rgba( 0, 0, 0, 0.1), - 1px 1px 0 gray; -} - -.notification.upload .progress, -.notification.download .progress { - - width: 100%; - background: #C2C2C2 url('images/progress.png'); - background-size: 16px 16px; - -moz-background-size: 16px 16px; - -webkit-background-size: 16px 16px; - -khtml-background-size: 16px 16px; - - animation-name: notification-progress; - animation-duration: 2s; - animation-timing-function: linear; - animation-iteration-count: infinite; - - -webkit-animation-name: notification-progress; - -webkit-animation-duration: 2s; - -webkit-animation-timing-function: linear; - -webkit-animation-iteration-count: infinite; - - padding: 0.25em; - min-width: 5em; - - border: 1px solid gray; - -moz-border-radius: 0.2em; - -webkit-border-radius: 0.2em; - -khtml-border-radius: 0.2em; - border-radius: 0.2em; - - text-align: center; - float: right; - - position: relative; - -} - -.notification.download .download { - background: rgb(16, 87, 153); - cursor: pointer; -} - -.notification.message { - background: #DFD; - animation: fadein 0.125s linear, fadeout 2s 3s linear; - -webkit-animation: fadein 0.125s linear, fadeout 2s 3s linear; -} - -.notification.message .caption { - vertical-align: middle; - white-space: normal; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index 5dc066c19..4f7f54776 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -73,20 +73,30 @@ angular.module('index').controller('indexController', ['$scope', '$injector', $location.path('/login'); /** - * Shows or hides the status. If a status is currently shown, - * no further statuses will be shown until the current status is - * hidden. + * Shows or hides the given notification as a modal status. If a status + * notification is currently shown, no further statuses will be shown + * until the current status is hidden. * - * @param {Boolean|Object} status The status to show, or false to hide the - * current status. - * @param {String} [status.title] The title of the status. - * @param {String} [status.text] The body text of the status. + * @param {Object} status The status notification to show. + * @param {String} [status.title] The title of the notification. + * @param {String} [status.text] The body text of the notification. * @param {String} [status.className] The CSS class name to apply. - * @param {Object[]} [status.actions] Array of action objects which - * contain an action name and callback to - * be executed when that action is - * invoked. - * + * + * @param {String} [status.countdown.text] + * In the case that a countdown applies to the notification, the text to + * display while the countdown is active. + * + * @param {Function} [status.countdown.callback] + * The callback to call when the countdown expires. + * + * @param {String} [status.countdown.remaining] + * The number of seconds remaining before the countdown callback is + * called. + * + * @param {Object[]} [status.actions] + * Array of action objects which contain an action name and callback to + * be executed when that action is invoked. + * * @example * * // To show a status message with actions @@ -114,15 +124,26 @@ angular.module('index').controller('indexController', ['$scope', '$injector', * * @param {Object} notification The notification to add. * @param {String} [notification.title] The title of the notification. - * @param {String} [notification.text] The body text of the status modal. + * @param {String} [notification.text] The body text of the notification. * @param {String} [notification.className] The CSS class name to apply. - * @param {Object[]} [notification.actions] Array of action objects which - * contain an action name and callback to - * be executed when that action is - * invoked. + * + * @param {String} [notification.countdown.text] + * In the case that a countdown applies to the notification, the text to + * display while the countdown is active. + * + * @param {Function} [notification.countdown.callback] + * The callback to call when the countdown expires. + * + * @param {String} [notification.countdown.remaining] + * The number of seconds remaining before the countdown callback is + * called. + * + * @param {Object[]} [notification.actions] + * Array of action objects which contain an action name and callback to + * be executed when that action is invoked. + * * @returns {Number} A unique ID for the notification that's just been added. * - * * @example * * var id = $scope.addNotification({ @@ -138,7 +159,7 @@ angular.module('index').controller('indexController', ['$scope', '$injector', */ $scope.addNotification = function addNotification(notification) { var id = ++notificationUniqueID; - + $scope.notifications.push({ notification : notification, id : id diff --git a/guacamole/src/main/webapp/app/index/styles/status.css b/guacamole/src/main/webapp/app/index/styles/status.css index f15be62d5..abb077e21 100644 --- a/guacamole/src/main/webapp/app/index/styles/status.css +++ b/guacamole/src/main/webapp/app/index/styles/status.css @@ -20,16 +20,6 @@ * THE SOFTWARE. */ -.status-container { - position: fixed; - top: 0; - left: 0; - bottom: 0; - right: 0; - background: rgba(0, 0, 0, 0.5); - padding: 1em; -} - .status-outer { display: table; height: 100%; @@ -48,7 +38,7 @@ vertical-align: middle; } -.status { +.status-middle .notification { width: 75%; max-width: 5in; @@ -56,33 +46,22 @@ margin-right: auto; overflow: auto; - background: white; - border: 1px solid rgba(0, 0, 0, 0.25); - box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.25); - text-align: left; } -.status .title { - font-size: 1.25em; - font-weight: bold; - background: rgba(0, 0, 0, 0.04); - border-bottom: 1px solid rgba(0, 0, 0, 0.125); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.125); - margin: 0; - padding: 0.5em 1em; - text-transform: uppercase; -} - -.status.error { - background: #FDD; -} - -.status > * { +.status-middle .notification .body { margin: 1.25em; } +.status-middle .notification .buttons { + margin: 1em; +} + +.status-middle .notification.error { + background: #FDD; +} + /* Fade entire status area in/out based on shown status */ .status-outer { @@ -99,10 +78,10 @@ /* Hide dialog immediately based on status */ -.status { +.status-middle .notification { visibility: hidden; } -.shown .status { +.shown .status-middle .notification { visibility: visible; } diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html index f4e61cc28..5f3ac9404 100644 --- a/guacamole/src/main/webapp/index.html +++ b/guacamole/src/main/webapp/index.html @@ -37,20 +37,17 @@ THE SOFTWARE.
-
- - -

{{status.title | translate}}

- - -

{{status.text | translate}}

- - -
- -
- -
+ + +
@@ -59,24 +56,19 @@ THE SOFTWARE.
-
- - -
-
{{wrapper.notification.title | translate}}
-
- - -

{{wrapper.notification.text}}

- -
- {{wrapper.notification.progress}} -
- -
- -
-
+
+ + + +
diff --git a/guacamole/src/main/webapp/translations/en_US.json b/guacamole/src/main/webapp/translations/en_US.json index 53635a75f..679d7c5c0 100644 --- a/guacamole/src/main/webapp/translations/en_US.json +++ b/guacamole/src/main/webapp/translations/en_US.json @@ -267,7 +267,8 @@ } }, "action" : { - "reconnect" : "Reconnect" + "reconnect" : "Reconnect", + "reconnectCountdown" : "Reconnecting in {REMAINING} {REMAINING, plural, one{second} other{seconds}}..." }, "fileTransfer" : { "title" : "File Transfer",