/* VERSION: 0 */ (function (){ /** * GLOBAL WOVNio OBJECT * Dynamically loaded components must call the registerComponent * method to be available to Widget for loading * * Components in the scrip can make themselves available to * Widget by assigning themselves to the components object using * their component name as the key **/ // only run once if (document.WOVNIO) return; var components = {}; var Widget = function (components, options) { if (!options) options = {}; //var that = this; var instance = {}; var installedComponents = {}; var cachedData = []; var DATA_TIMEOUT = 5000; document.WOVNIO = function () { var obj = {}; obj.registerComponent = function (componentName, component) { components[componentName] = component; delete installedComponents[componentName]; // dispatch load event var loadEvent = document.createEvent('Event'); var eventName = componentName + 'Loaded'; loadEvent.initEvent(eventName, true, true); document.dispatchEvent(loadEvent); }; return obj; }(); var insertedSrcs = []; var html = options.scriptTag || document.currentScript || function () { var scriptTags = document.getElementsByTagName('script'); // this should return on the first loop iteration for (var i = scriptTags.length - 1; i >= 0; i--) { if (scriptTags[i].getAttribute('data-wovnio')) return scriptTags[i]; } return scriptTags[scriptTags.length - 1]; }(); instance.tag = { html: html, getAttribute: function (attr) { attr = (typeof attr === 'string') ? attr : ''; var hasAttr = html.hasAttribute ? html.hasAttribute(attr) : (html[attr] !== undefined); if (hasAttr) { return html.getAttribute(attr); } else { var rx = new RegExp(attr + '=([^&]*)', 'i'); var match = (html.getAttribute('data-wovnio') || '').match(rx); return match ? (match[1] === 'false' ? false : match[1]) : ''; } }, /** * Insert an script tag with the specified src and attributes to the previous of the wovn script. * @param {String} srcAttr the src of the script * @param {Object} attrs additional attributes for the script */ insertScriptBefore: function (srcAttr, attrs) { if (!srcAttr) return; var scriptEle = document.createElement('script'); scriptEle.type = 'text/javascript'; scriptEle.async = true; for (var name in attrs) if (attrs.hasOwnProperty(name)) scriptEle[name] = attrs[name]; scriptEle.src = srcAttr; html.parentNode.insertBefore(scriptEle, html); insertedSrcs.push(srcAttr); return scriptEle; }, isScriptInserted: function (src) { // In tests, cannot call Utils' function because of Widget loading is faster than utils for (var i = 0; i < insertedSrcs.length; i++) { if (insertedSrcs[i] === src) { return true } } return false; }, /** * Remove script tags containing the specified src * @param {String} src the src of the scripts */ removeScript: function(src) { if (!src || !instance.tag.isScriptInserted(src)) return; insertedSrcs.splice(instance.c('Utils').indexOf(insertedSrcs, src), 1); var scripts = document.getElementsByTagName('script'); for (var i = 0; i < scripts.length; ++i) { var script = scripts[i]; if (script.getAttribute('src') === src && script.parentNode) script.parentNode.removeChild(script); } } }; instance.isBackend = function() { return instance.tag.getAttribute('backend'); }; /** * Get component * @param componentName {String} * @returns component */ var getComponent = function (componentName) { // Not use hasOwnProperty to speed up var component = installedComponents[componentName]; if (component) return component; else if (components[componentName]) { installComponent(componentName); return installedComponents[componentName]; } else { return null; } }; var installComponent = function (componentName) { if (installedComponents[componentName] || typeof(components[componentName]) === 'undefined') return; installedComponents[componentName] = new components[componentName](instance); }; instance.c = function (componentName) { return getComponent(componentName); }; instance.isComponentLoaded = function (componentName) { return installedComponents.hasOwnProperty(componentName) || components.hasOwnProperty(componentName); }; /** * Get components src * * @param {String} componentName * @param {Object} options * @param {String} options.location alternative of current location * @param {Boolean} options.failover if true, return URL for failover * * @return {String} src */ var componentSrc = function(componentName, options) { if (!options) options = {}; if (componentName === 'Data') { var key = instance.tag.getAttribute('key'); if (!key) return null; var encodedLocation = getEncodedLocation(options['location']); var path = '/js_data/1/'+ key + '/?u=' + encodedLocation + '&version=1'; var host = options.failover ? 'cdn.wovn.io' : 'wovn.global.ssl.fastly.net'; return '//' + host + path; } else return options.failover ? null : ('//j.wovn.io/1/components/' + componentName); }; /** * Get data src * * @param {Object} options * @param {String} options.location alternative of current location * @param {Boolean} options.failover if true, return URL for failover * * @return {String} src */ var dataJsonSrc = function(options) { if (!options) options = {}; var key = instance.tag.getAttribute('key'); if (!key) return null; var encodedLocation = getEncodedLocation(options['location']); var path = '/js_data/json/1/'+ key + '/?u=' + encodedLocation + '&version=1'; var host = options.failover ? 'cdn.wovn.io' : 'wovn.global.ssl.fastly.net'; return '//' + host + path; }; var savedDataJsonSrc = function() { var token = instance.tag.getAttribute('key'); var session = encodeURIComponent(location.hash.match(/wovn\.editing=(.+)/)[1]); if (!token || !session) { return null; } var encodedLocation = getEncodedLocation(options['location']); var host = instance.c('Url').getApiHost(); var url = host + 'js_saved_data/' + token + '?session_token=' + session + '&u=' + encodedLocation; return url; } var addLoadListener = function(componentName, callback) { var loadEvent = document.createEvent('Event'); var eventName = componentName + 'Loaded'; loadEvent.initEvent(eventName, true, true); if (document.addEventListener) { var handler = function() { document.removeEventListener(eventName, handler, false); callback.apply(this, arguments); }; document.addEventListener(eventName, handler, false); } else if (document.attachEvent) { var handler = function() { document.detachEvent(eventName, handler); callback.apply(this, arguments); }; document.attachEvent(eventName, handler); } }; /** * Load a component * * @param {String} componentName * @param {Object} options * @param {Boolean} options.force if true, insert a script tag always * @param {Function} callback */ instance.loadComponent = function (componentName, options, callback) { if (callback === undefined && typeof(options) === 'function') { callback = options; options = {}; } options = options || {}; if (typeof(callback) !== 'function') callback = function () {}; // if this component is already loaded, call callback and return if (!options['force'] && instance.isComponentLoaded(componentName)) { setTimeout(callback, 0); return; } // setup load event var loaded = false; addLoadListener(componentName, function() { if (loaded) return; loaded = true; callback.apply(this, arguments); }); var retried = false; var load = function() { var src = componentSrc(componentName, options); if (options['force'] || !instance.tag.isScriptInserted(src)) { var retry = function() { if (loaded || retried) return; retried = true; options.failover = true; load(); }; var attrs = {} // retry if the CDN returns an error. attrs.onerror = retry; attrs.onreadystatechange = function() { if (this.readyState === 'loaded' || this.readyState === 'complete') retry(); }; // retry if loading is timed out. setTimeout(retry, DATA_TIMEOUT); } instance.tag.insertScriptBefore(src, attrs); } load(); }; /** * Load data as JSON * @param {Function} callback */ instance.loadDataJson = function(callback) { var src = dataJsonSrc(); instance.c('Utils').sendRequestAsJson('GET', src, callback, errorFailover); function errorFailover(reason) { // Ignore 204 response (page doesn't exist or isn't published). if (reason && reason.status === 204) { return; } var src = dataJsonSrc({failover: true}); instance.c('Utils').sendRequestAsJson('GET', src, callback, function() {}) } }; instance.loadSavedData = function(callback, errorCallback) { var src = savedDataJsonSrc(); instance.c('Utils').sendRequestAsJson('GET', src, callback, errorCallback) }; instance.loadComponents = function(componentNames, callbacks) { var newComponentNames = []; for (var i = 0; i < componentNames.length; ++i) { var componentName = componentNames[i]; var callback = callbacks[componentName] || function() {}; if (instance.isComponentLoaded(componentName)) { setTimeout(callback, 0); } else { addLoadListener(componentName, callback); newComponentNames.push(componentName); } } if (newComponentNames.length) instance.tag.insertScriptBefore(componentSrc(newComponentNames.join('+'))); }; /** * Load domain's option * @param {Function} callback called when succeed * @param {Function} errorCallback called when fail */ instance.loadDomainOption = function(callback, errorCallback) { var key = instance.tag.getAttribute('key'); if (!key) return; var retried = false; var loaded = false; var onsuccess = function(data, headers) { if (loaded) return; loaded = true; // In IE9, cannot access custom headers using XDomainRequest... if (headers && headers['Country-Code']) { data['countryCode'] = headers['Country-Code']; } else if (instance.c('Data').needsCountryCode(data)) { instance.loadCountryCode(function(jsonData) { if (jsonData && jsonData['countryCode']) { data['countryCode'] = jsonData['countryCode']; } callback(data); }, function() {}) return } callback(data); }; var onerror = function() { if (loaded) return; if (retried) { errorCallback.apply(this, arguments); } else { retried = true; load('cdn.wovn.io'); } }; var load = function(host) { var puny_host = instance.c('PunyCode').toASCII(getRealLocation().hostname) var option_url = '//' + host + '/domain/options/' + key + '?host=' + puny_host; instance.c('Utils').sendRequestAsJson('GET', option_url, onsuccess, onerror); }; load('wovn.global.ssl.fastly.net'); setTimeout(onerror, DATA_TIMEOUT); }; instance.loadCountryCode = function(callback, errorCallback) { var loaded = false; var onsuccess = function() { loaded = true; callback.apply(this, arguments); }; var onerror = function() { if (loaded) return; errorCallback.apply(this, arguments); }; // Request must not go to CDN server var option_url = '//cdn.wovn.io/inspect/country'; instance.c('Utils').sendRequestAsJson('GET', option_url, onsuccess, onerror); setTimeout(onerror, DATA_TIMEOUT); }; /** * Get translated values * @param values {Array} original values * @param callback * @param errorCallback */ instance.loadTranslation = function(values, callback, errorCallback) { var key = instance.tag.getAttribute('key'); if (!key) return; var url = '//ee.wovn.io/values/translate'; var data = { srcs: values, defaultLang: instance.c('Lang').defaultCode(), token: key, host: getRealLocation().hostname } instance.c('Utils').postJsonRequest(url, data, callback, errorCallback); } instance.clearCacheData = function() { cachedData = [] } instance.reloadData = function(callback) { var encodedLocation = getEncodedLocation(); var cache = null; for (var i = 0; i < cachedData.length; ++i) { if (cachedData[i].encodedLocation === encodedLocation) { cache = cachedData[i]; break; } } if (cache) { callback(cache.data); } else { instance.c('Interface').loadData(function(data) { cachedData.unshift({encodedLocation: encodedLocation, data: data}); // To not use much memory. if (cachedData.length > 50) cachedData.pop(); callback(data); }); } }; instance.removeComponentScript = function(componentName, options) { instance.tag.removeScript(componentSrc(componentName, options)); }; var destroyComponent = function(componentName) { if (typeof(installedComponents[componentName].destroy) === 'function') { installedComponents[componentName].destroy(); } }; instance.destroy = function () { for (componentName in installedComponents){ if (installedComponents.hasOwnProperty(componentName)) destroyComponent(componentName); } }; instance.reinstallComponent = function(componentName) { destroyComponent(componentName); installedComponents[componentName] = new components[componentName](instance); }; instance.getBackendCurrentLang = function () { return instance.tag.getAttribute('currentLang'); } /** * Gets the current location of the browser without the backend-inserted lang code * * @return {string} The unicode-safe location of this browser without the lang code */ function getEncodedLocation (currentLocation) { // not all browsers handle unicode characters in the path the same, so we have this long mess to handle it // TODO: decodeURIcomponent doesnt handle the case where location has char like this: &submit=%8E%9F%82%D6%90i%82%DE (characters encoded in shift_jis) // adding unescape before it makes the error go away but doesnt fix the pb and creates pb for utf8 encode params if (!currentLocation) currentLocation = location; if (typeof(currentLocation) !== 'string') { var punyHost = instance.c('PunyCode').toASCII(currentLocation.host); currentLocation = currentLocation.protocol + '//' + punyHost + currentLocation.pathname + currentLocation.search; } var urlFormatter = instance.c('UrlFormatter').createFromUrl(currentLocation); currentLocation = urlFormatter.getNormalizedPageUrl(instance.tag.getAttribute('backend'), instance.tag.getAttribute('urlPattern')); return encodeURIComponent(currentLocation); } instance.getEncodedLocation = getEncodedLocation; /** * Gets the current location Object of the browser without the backend-inserted lang code * * @return {object} An object imitating the location, without the backend inserted lang code */ function getRealLocation (currentLocation) { var fakeLocation = currentLocation || location; currentLocation = {} currentLocation.protocol = fakeLocation.protocol; currentLocation.search = fakeLocation.search; currentLocation.href = fakeLocation.href; currentLocation.host = fakeLocation.host; currentLocation.port = fakeLocation.port; currentLocation.hostname = fakeLocation.hostname; currentLocation.origin = fakeLocation.origin; currentLocation.pathname = fakeLocation.pathname; if (instance.tag.getAttribute('backend')) { var langIdentifier = instance.c('Lang').getBackendLangIdentifier(); switch (instance.tag.getAttribute('urlPattern')) { case 'query': currentLocation.search = currentLocation.search.replace(new RegExp('(\\?|&)wovn=' + langIdentifier + '(&|$)'), '$1').replace(/(\?|&)$/, ''); currentLocation.href = currentLocation.href.replace(new RegExp('(\\?|&)wovn=' + langIdentifier + '(&|$)'), '$1').replace(/(\?|&)$/, ''); break; case 'subdomain': currentLocation.host = currentLocation.host.replace(new RegExp('^' + langIdentifier + '\\.', 'i'), ''); currentLocation.hostname = currentLocation.hostname.replace(new RegExp('^' + langIdentifier + '\\.', 'i'), ''); currentLocation.href = currentLocation.href.replace(new RegExp('//' + langIdentifier + '\\.', 'i'), '//'); currentLocation.origin = currentLocation.origin.replace(new RegExp('//' + langIdentifier + '\\.', 'i'), '//'); break; case 'path': currentLocation.href = currentLocation.href.replace(new RegExp('(//[^/]+)/' + langIdentifier + '(/|$)'), '$1/'); currentLocation.pathname = currentLocation.pathname.replace(new RegExp('/' + langIdentifier + '(/|$)'), '/'); } } return currentLocation; } instance.getRealLocation = getRealLocation; return instance; }; var widget = Widget(components); // old widget compatibility document.appendM17nJs = function (res) { var components = {}; components['Data'] = function (widget) { var that = this; var data = res; this.get = function () { return data; }; this.set = function (setData) { data = setData; }; this.getLang = function () { return data['language']; }; this.getUserId = function () { return data['user_id']; }; this.getPageId = function () { return data['id']; }; this.getPublishedLangs = function () { return data['published_langs']; }; this.getOptions = function () { return data['widgetOptions']; }; this.dynamicValues = function () { return data['dynamic_values'] || (that.getOptions() || {})['dynamic_values'] || false; }; }; for (var componentName in components){if(components.hasOwnProperty(componentName)) { document.WOVNIO.registerComponent(componentName, components[componentName]); }} }; if (typeof(components) === 'undefined') var components = {}; components['Agent'] = function () { var that = this; var agentString = window.navigator.userAgent; this.setAgentString = function (newAgentString) { agentString = newAgentString; }; this.getAgentString = function() { return agentString; } this.isIEResult = undefined; // Don't use this function // This is for urgent treatment, and will be removed soon /** * Return whether browser is IE(8,9,10,11) * @returns {Boolean} */ this.isIE = function () { if (this.isIEResult !== undefined) return this.isIEResult; var userAgent = agentString.toLowerCase(); this.isIEResult = (userAgent.indexOf('msie') !== -1 || userAgent.indexOf('trident') !== -1) ? true : false; return this.isIEResult; }; this.isEdgeResult = undefined; // Don't use this function // This is for urgent treatment, and will be removed soon /** * Return whether browser is Edge * @type {Boolean} */ this.isEdge = function() { if (this.isEdgeResult !== undefined) return this.isEdgeResult; this.isEdgeResult = agentString.match(/Edge/)? true: false; return this.isEdgeResult; }; this.isCrawler = function () { var crawlerUserAgents = 'googlebot|slurp|y!j|yahooseeker|bingbot|msnbot|baiduspider|yandex|yeti|naverbot|duckduckbot|360spider|sogou'; var rx = new RegExp(crawlerUserAgents, 'i'); return rx.test(agentString); }; this.isMobile = function () { return !!((agentString.match(/android/i) && agentString.match(/mobile/i)) || agentString.match(/iphone/i) || agentString.match(/ipod/i) || agentString.match(/phone/i) || ((agentString.match(/blackberry/i) || agentString.match(/bb10/i) || agentString.match(/rim/i)) && !agentString.match(/tablet/i)) || ((agentString.match(/\(mobile;/i) || agentString.match(/\(tablet;/i) || agentString.match(/; rv:/i)) && agentString.match(/mobile/i)) || agentString.match(/meego/i)); } var doesMutateText = undefined; this.mutatesTextNodeData = function () { if (doesMutateText !== undefined) return doesMutateText; var newLineText = '0\n1'; var pEle = document.createElement('p'); pEle.innerHTML = newLineText; doesMutateText = pEle.firstChild.data !== newLineText; return doesMutateText; } this.canStoreObjectInNode = function() { return !that.isEdge() && !that.isIE() } /** * true when Data Highlighter of Google Search Console accesses to the page installed WOVN.io. * @return {Boolean} */ this.isDataHighlighter = function() { return agentString.match(/Google PP Default/); }; }; // Original Cookie code from http://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/ if (typeof(components) === 'undefined') var components = {}; components['Cookie'] = function (widget) { var that = this; this.set = function (name, value, days, host) { if (name === '') return; host = host === undefined ? (location.hostname || 'wovn.io') : host; var cookie = name + '=' + (value === null ? '' : value) + '; path=/'; if (days) { var date = new Date(); date.setTime(date.getTime()+(days*24*60*60*1000)); cookie += "; expires=" + date.toGMTString(); } var lastCookie = null; var setCookie = function(domain) { lastCookie = cookie + (domain ? ('; domain=' + domain) : ''); document.cookie = lastCookie; }; if (host.split('.').length === 1) { // no "." in a domain - it's localhost or something similar setCookie(); } else { // Remember the cookie on all subdomains. // // Start with trying to set cookie to the top domain. // (example: if user is on foo.com, try to set // cookie to domain ".com") // // If the cookie will not be set, it means ".com" // is a top level domain and we need to // set the cookie to ".foo.com" var domainParts = host.split('.'); domainParts.shift(); var domain = '.'+domainParts.join('.'); setCookie(domain); // check if cookie was successfuly set to the given domain // (otherwise it was a Top-Level Domain) if (that.get(name) == null || that.get(name) != value) { // append "." to current domain domain = '.'+host; setCookie(domain); // check if cookie was successfuly set to the given domain // (otherwise it was maybe an ip address) if (that.get(name) == null || that.get(name) != value) { // use domain as-is setCookie(host); } } } return lastCookie; }; this.get = function (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; }; this.erase = function (name) { that.set(name, null, -1); that.set(name, null, -1, ''); // also erase no domain cookie }; /*this.eraseOldCookie = function () { var days = -1; var name = 'wovn_selected_lang'; var value = ''; var date = new Date(); date.setTime(date.getTime()+(days*24*60*60*1000)); var expires = "; expires="+date.toGMTString(); var domain = 'wovn.io'; document.cookie = name+"="+value+expires+"; path=/; domain="+domain; }*/ }; if (typeof components === 'undefined') var components = {}; components['PunyCode'] = function () { // Modified from https://github.com/bestiejs/punycode.js // Copyright Mathias Bynens // https://github.com/bestiejs/punycode.js/blob/master/LICENSE-MIT.txt 'use strict'; /** Highest positive signed 32-bit float value */ var maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1 /** Bootstring parameters */ var base = 36; var tMin = 1; var tMax = 26; var skew = 38; var damp = 700; var initialBias = 72; var initialN = 128; // 0x80 var delimiter = '-'; // '\x2D' /** Regular expressions */ var regexNonASCII = /[^\x20-\x7E]/; // unprintable ASCII chars + non-ASCII chars var regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators /** Error messages */ var errors = { 'overflow': 'Overflow: input needs wider integers to process', 'not-basic': 'Illegal input >= 0x80 (not a basic code point)', 'invalid-input': 'Invalid input' }; /** Convenience shortcuts */ var baseMinusTMin = base - tMin; var floor = Math.floor; var stringFromCharCode = String.fromCharCode; /*--------------------------------------------------------------------------*/ /** * A generic error utility function. * @private * @param {String} type The error type. * @returns {Error} Throws a `RangeError` with the applicable error message. */ function error(type) { throw new RangeError(errors[type]); } /** * A generic `Array#map` utility function. * @private * @param {Array} array The array to iterate over. * @param {Function} callback The function that gets called for every array * item. * @returns {Array} A new array of values returned by the callback function. */ function map(array, fn) { var result = []; var length = array.length; while (length--) { result[length] = fn(array[length]); } return result; } /** * A simple `Array#map`-like wrapper to work with domain name strings or email * addresses. * @private * @param {String} domain The domain name or email address. * @param {Function} callback The function that gets called for every * character. * @returns {Array} A new string of characters returned by the callback * function. */ function mapDomain(string, fn) { var parts = string.split('@'); var result = ''; if (parts.length > 1) { // In email addresses, only the domain name should be punycoded. Leave // the local part (i.e. everything up to `@`) intact. result = parts[0] + '@'; string = parts[1]; } // Avoid `split(regex)` for IE8 compatibility. See #17. string = string.replace(regexSeparators, '\x2E'); var labels = string.split('.'); var encoded = map(labels, fn).join('.'); return result + encoded; } /** * Creates an array containing the numeric code points of each Unicode * character in the string. While JavaScript uses UCS-2 internally, * this function will convert a pair of surrogate halves (each of which * UCS-2 exposes as separate characters) into a single code point, * matching UTF-16. * @see `punycode.ucs2.encode` * @see * @memberOf punycode.ucs2 * @name decode * @param {String} string The Unicode input string (UCS-2). * @returns {Array} The new array of code points. */ function ucs2decode(string) { var output = []; var counter = 0; var length = string.length; while (counter < length) { var value = string.charCodeAt(counter++); if (value >= 0xD800 && value <= 0xDBFF && counter < length) { // It's a high surrogate, and there is a next character. var extra = string.charCodeAt(counter++); if ((extra & 0xFC00) == 0xDC00) { // Low surrogate. output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); } else { // It's an unmatched surrogate; only append this code unit, in case the // next code unit is the high surrogate of a surrogate pair. output.push(value); counter--; } } else { output.push(value); } } return output; } /** * Converts a digit/integer into a basic code point. * @see `basicToDigit()` * @private * @param {Number} digit The numeric value of a basic code point. * @returns {Number} The basic code point whose value (when used for * representing integers) is `digit`, which needs to be in the range * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is * used; else, the lowercase form is used. The behavior is undefined * if `flag` is non-zero and `digit` has no uppercase form. */ var digitToBasic = function digitToBasic(digit, flag) { // 0..25 map to ASCII a..z or A..Z // 26..35 map to ASCII 0..9 return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); }; /** * Bias adaptation function as per section 3.4 of RFC 3492. * https://tools.ietf.org/html/rfc3492#section-3.4 * @private */ var adapt = function adapt(delta, numPoints, firstTime) { var k = 0; delta = firstTime ? floor(delta / damp) : delta >> 1; delta += floor(delta / numPoints); for (; /* no initialization */delta > baseMinusTMin * tMax >> 1; k += base) { delta = floor(delta / baseMinusTMin); } return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); }; /** * Converts a string of Unicode symbols (e.g. a domain name label) to a * Punycode string of ASCII-only symbols. * @memberOf punycode * @param {String} input The string of Unicode symbols. * @returns {String} The resulting Punycode string of ASCII-only symbols. */ var encode = function encode(input) { var output = []; // Convert the input in UCS-2 to an array of Unicode code points. input = ucs2decode(input); // Cache the length. var inputLength = input.length; // Initialize the state. var n = initialN; var delta = 0; var bias = initialBias; // Handle the basic code points. try { for (var i = 0; i < input.length; i++) { var currentValue = input[i]; if (currentValue < 0x80) { output.push(stringFromCharCode(currentValue)); } } } catch (err) { // ignore errors } var basicLength = output.length; var handledCPCount = basicLength; // `handledCPCount` is the number of code points that have been handled; // `basicLength` is the number of basic code points. // Finish the basic string with a delimiter unless it's empty. if (basicLength) { output.push(delimiter); } // Main encoding loop: while (handledCPCount < inputLength) { // All non-basic code points < n have been handled already. Find the next // larger one: var m = maxInt; try { for (var i = 0; i < input.length; i++) { var currentValue = input[i]; if (currentValue >= n && currentValue < m) { m = currentValue; } } } catch (err) { // ignore errors } var handledCPCountPlusOne = handledCPCount + 1; if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { error('overflow'); } delta += (m - n) * handledCPCountPlusOne; n = m; try { for (var i = 0; i < input.length; i++) { var currentValue = input[i]; if (currentValue < n && ++delta > maxInt) { error('overflow'); } if (currentValue == n) { // Represent delta as a generalized variable-length integer. var q = delta; for (var k = base;; /* no condition */k += base) { var t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias; if (q < t) { break; } var qMinusT = q - t; var baseMinusT = base - t; output.push(stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0))); q = floor(qMinusT / baseMinusT); } output.push(stringFromCharCode(digitToBasic(q, 0))); bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); delta = 0; ++handledCPCount; } } } catch (err) { // ignore errors } ++delta; ++n; } return output.join(''); }; /** * Converts a Unicode string representing a domain name or an email address to * Punycode. Only the non-ASCII parts of the domain name will be converted, * i.e. it doesn't matter if you call it with a domain that's already in * ASCII. * @memberOf punycode * @param {String} input The domain name or email address to convert, as a * Unicode string. * @returns {String} The Punycode representation of the given domain name or * email address. */ this.toASCII = function(input) { return mapDomain(input, function (string) { return regexNonASCII.test(string) ? 'xn--' + encode(string) : string; }); }; }; // Original Cookie code from http://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/ if (typeof(components) === 'undefined') var components = {}; components['Lang'] = function (widget) { var that = this; var langHash = {"ar":{"name":"العربية","code":"ar","en":"Arabic"},"bg":{"name":"Български","code":"bg","en":"Bulgarian"},"zh-CHS":{"name":"简体中文","code":"zh-CHS","en":"Simp Chinese"},"zh-CHT":{"name":"繁體中文","code":"zh-CHT","en":"Trad Chinese"},"da":{"name":"Dansk","code":"da","en":"Danish"},"nl":{"name":"Nederlands","code":"nl","en":"Dutch"},"en":{"name":"English","code":"en","en":"English"},"fi":{"name":"Suomi","code":"fi","en":"Finnish"},"fr":{"name":"Français","code":"fr","en":"French"},"de":{"name":"Deutsch","code":"de","en":"German"},"el":{"name":"Ελληνικά","code":"el","en":"Greek"},"he":{"name":"עברית","code":"he","en":"Hebrew"},"id":{"name":"Bahasa Indonesia","code":"id","en":"Indonesian"},"it":{"name":"Italiano","code":"it","en":"Italian"},"ja":{"name":"日本語","code":"ja","en":"Japanese"},"ko":{"name":"한국어","code":"ko","en":"Korean"},"ms":{"name":"Bahasa Melayu","code":"ms","en":"Malay"},"my":{"name":"ဗမာစာ","code":"my","en":"Burmese"},"ne":{"name":"नेपाली भाषा","code":"ne","en":"Nepali"},"no":{"name":"Norsk","code":"no","en":"Norwegian"},"pl":{"name":"Polski","code":"pl","en":"Polish"},"pt":{"name":"Português","code":"pt","en":"Portuguese"},"ru":{"name":"Русский","code":"ru","en":"Russian"},"es":{"name":"Español","code":"es","en":"Spanish"},"sv":{"name":"Svensk","code":"sv","en":"Swedish"},"th":{"name":"ภาษาไทย","code":"th","en":"Thai"},"hi":{"name":"हिन्दी","code":"hi","en":"Hindi"},"tr":{"name":"Türkçe","code":"tr","en":"Turkish"},"uk":{"name":"Українська","code":"uk","en":"Ukrainian"},"vi":{"name":"Tiếng Việt","code":"vi","en":"Vietnamese"}}; var defaultCode = null; var secondaryCode = null; var docLangSet = false; var docLang; /** * An object of language aliases passed through snippet options * from backend. * * key => lang code * value => lang alias * */ var langCodeAliases = function () { var langCodeAliasesStr = widget.tag.getAttribute('langCodeAliases'); var langCodeAliasesObject = langCodeAliasesStr !== '' ? widget.c('Utils').parseJSON(langCodeAliasesStr) : {}; for (langCode in langCodeAliasesObject){if(langCodeAliasesObject.hasOwnProperty(langCode)) { if (!langHash[langCode] || langCodeAliasesObject[langCode] === '') { delete langCodeAliasesObject[langCode]; } }} return langCodeAliasesObject; }(); /** * Returns the language object for the given code, name, English name, or alias * * @param {String|Object} langName A code, name, English name, alias, * or object containing a code. * * @returns {Object} The language object who's code, name, English name, or alias * matches the param. If no languages match, returns null. */ this.get = function(langName) { if (langName.code) var langName = langName.code if (typeof langName !== 'string') return null; langName = langName.toLowerCase(); for (langCode in langHash){if(langHash.hasOwnProperty(langCode)) { if (langName === langHash[langCode].name.toLowerCase() || langName === langHash[langCode].code.toLowerCase() || langName === langHash[langCode].en.toLowerCase()) return langHash[langCode]; }} for (langCode in langCodeAliases){if(langCodeAliases.hasOwnProperty(langCode)) { if (langName === langCodeAliases[langCode]) { return langHash[langCode]; } }} return null; }; /** * Normalizes language codes to the iso 6391 * reference: https://support.google.com/webmasters/answer/189077?hl=en * @param langCode {String} The language code to be normalized * @returns {String} The normalized language code */ this.iso6391Normalization = function(langCode) { return langCode.replace(/zh-CHT/i, 'zh-Hant').replace(/zh-CHS/i, 'zh-Hans'); }; this.getCode = function (langName) { var lang = this.get(langName); return lang && lang.code; }; this.getCodes = function() { var codes = []; for (var code in langHash) if (langHash.hasOwnProperty(code)) codes.push(code); return codes; }; this.isCode = function(code) { return langHash.hasOwnProperty(code); }; this.isAlias = function(candidateAlias) { for (langCode in langCodeAliases){if(langCodeAliases.hasOwnProperty(langCode)) { if (candidateAlias === langCodeAliases[langCode]) { return true; } }} return false; }; this.hasAlias = function (langCode) { return langCodeAliases[langCode] && (langCodeAliases[langCode] !== langCode) } this.isCaseInsensitiveCode = function(code) { for (var langCode in langHash) if (langHash.hasOwnProperty(langCode) && code.toLowerCase() === langCode.toLowerCase()) return true; return false; }; this.isCaseInsensitiveAlias = function(candidateAlias) { for (langCode in langCodeAliases){if(langCodeAliases.hasOwnProperty(langCode)) { if (candidateAlias.toLowerCase() === langCodeAliases[langCode].toLowerCase()) { return true; } }} return false; }; this.setDefaultCodeAndRecomputeSecondaryCode = function(code) { defaultCode = code; secondaryCode = computeSecondaryCode(); }; this.defaultCode = function (options) { options = options || {}; return defaultCode || (widget.tag.getAttribute('backend') && widget.tag.getAttribute('defaultLang')) || widget.c('Data').getLang() || 'en'; }; this.getSecondaryCode = function () { if (secondaryCode === null) { secondaryCode = computeSecondaryCode(); } return secondaryCode; } var computeSecondaryCode = function () { var code = widget.c('Data').getSecondaryLang(); var translatableLangs = widget.c('Data').getTranslatableLangs() if (!code || widget.c('Utils').indexOf(translatableLangs, code) === -1) { code = that.defaultCode(); } return code; } this.missingAutoTranslateLangs = function () { var langs = widget.c('Data').getTranslatableLangs(); var autoLangs = widget.c('Data').getAutoTranslateLangs(); return widget.c('Utils').setComplement(autoLangs, langs).length > 0; } this.missingAutoPublishLangs = function () { var langs = widget.c('Data').getTranslatableLangs(); var autoLangs = widget.c('Data').getAutoPublishLangs(); return widget.c('Utils').setComplement(autoLangs, langs).length > 0; } /** * Set docLang of the page. * @param {string} newLangCode - docLang * @return {void} */ this.setDocLang = function (newLangCode) { // Get the oldLangCode, but if this is the first time the docLang has been set, // getDocLang returns the newLangCode so we use the defaultCode var oldLangCode = docLangSet ? that.getDocLang() : that.defaultCode(); newLangCode = newLangCode || that.getDocLang(); var translatableLangs = widget.c('Data').getTranslatableLangs(); if (widget.c('Utils').includes(translatableLangs, newLangCode) === false) { return } (document.getElementsByTagName('html')[0]).setAttribute('lang', newLangCode); // backend case AND lang path case var langPath = widget.c('Data').getOptions().lang_path; if ((langPath === 'query' || langPath === 'path' || (widget.isBackend() && widget.tag.getAttribute('urlPattern'))) && widget.c('Url').getLangCode() !== newLangCode) { widget.c('Url').changeUrl(newLangCode); //return; } widget.c('Cookie').set('wovn_selected_lang', newLangCode, 365); // swap vals widget.c('DomAuditor').swapVals(newLangCode); docLang = newLangCode; // only dispatch event if the lang code has changed if (oldLangCode !== newLangCode) { setTimeout(function () {widget.c('Api').dispatchLangChangedEvent();}, 0); } docLangSet = true; }; this.setDocLangWithoutSwap = function (newLangCode) { // Get the oldLangCode, but if this is the first time the docLang has been set, // getDocLang returns the newLangCode so we use the defaultCode var oldLangCode = docLangSet ? that.getDocLang() : that.defaultCode(); newLangCode = newLangCode || that.getDocLang(); var translatableLangs = widget.c('Data').getTranslatableLangs(); if (widget.c('Utils').includes(translatableLangs, newLangCode) === false) { return } (document.getElementsByTagName('html')[0]).setAttribute('lang', newLangCode); // backend case AND lang path case var langPath = widget.c('Data').getOptions().lang_path; if ((langPath === 'query' || langPath === 'path' || (widget.isBackend() && widget.tag.getAttribute('urlPattern'))) && widget.c('Url').getLangCode() !== newLangCode) { widget.c('Url').changeUrl(newLangCode); } widget.c('Cookie').set('wovn_selected_lang', newLangCode, 365); docLang = newLangCode; // only dispatch event if the lang code has changed if (oldLangCode !== newLangCode) { setTimeout(function () {widget.c('Api').dispatchLangChangedEvent();}, 0); } docLangSet = true; }; var convertedCodes; /** * Checks if a given language code is valid for the page. * * @param {String} langCode The language code to check. * * @returns {Boolean} True if the language code is valid for the page, * otherwise false. */ function isValidLangCode (langCode) { if(!convertedCodes) { convertedCodes = {}; var convertedLangs = that.getConvertedLangs(); for(var i = 0; i < convertedLangs.length; i++) { convertedCodes[convertedLangs[i].code] = true; } } return convertedCodes[langCode] || false; } /** * cache for tag's currentLang */ var currentLangOfWidgetTag = widget.getBackendCurrentLang() /** * Get the actual language of the page. * In contrast to getDocLang, getActualLang always provides the current * language of the page. Indeed getDocLang provides the language that should * be displayed, whether it is already displayed or not. * For instance: If a Japanese page is loaded with cookie set to English * language, getDocLang answers English (even if Japanese text has not been * swapped to English yet) while getActualLang answers Japanese. * * @returns {String} The actual language code of the page (the one of its * text). */ this.getActualLang = function () { if (docLangSet) { return that.getDocLang(); } else { // if backend, then get backend language, because it has already swapped // the values if (widget.isBackend()) { if (isValidLangCode(currentLangOfWidgetTag)) { return currentLangOfWidgetTag; } } return that.defaultCode(); } } /** * Get docLang of the page. * @return {string} docLang */ this.getDocLang = function () { return docLang || function () { var defaultLang = that.defaultCode(); var secondaryLang = that.getSecondaryCode(); // For Data Highlighter of Google Search Console. if (widget.c('Agent').isDataHighlighter()) { if (widget.isBackend()) { docLang = widget.c('Url').getLangCode(); if (!isValidLangCode(docLang)) { docLang = defaultLang; } } else { docLang = defaultLang; } return docLang; } // if backend, trust the backend if (widget.tag.getAttribute('backend')) { if (!widget.c('Cookie').get('wovn_selected_lang')) { var browserLang = that.getBrowserLang(); if (isValidLangCode(browserLang)) { docLang = browserLang; } } if (!docLang) { var currentLang = widget.getBackendCurrentLang(); if (isValidLangCode(currentLang)) { docLang = currentLang; } } if(!docLang) { docLang = secondaryLang; } return docLang; } var urlLang = widget.c('Url').getLangCode(); if (defaultLang !== urlLang && isValidLangCode(urlLang)) { docLang = urlLang; } if (!docLang) { var cookieLang = widget.c('Cookie').get('wovn_selected_lang'); if(isValidLangCode(cookieLang)) { docLang = cookieLang; } } if (!docLang) { var browserLang = that.getBrowserLang(); if (isValidLangCode(browserLang)) { docLang = browserLang; } } if (!docLang) { docLang = secondaryLang; } return docLang; }(); }; /** * Get browser language. */ this.getBrowserLang = function () { // Chrome language settings is set in window.navigator.languages, not window.navigator.language. var browserLang = (window.navigator.languages && window.navigator.languages[0]) || window.navigator.language || window.navigator.userLanguage || window.navigator.browserLanguage; return this.browserLangCodeToWidgetLangCode(browserLang); } this.browserLangCodeToWidgetLangCode = function (langCode) { var widgetLangCodes = that.getCodes(); var code = null; switch (langCode.toLowerCase()) { case 'zh-tw': code = 'zh-CHT'; break; case 'zh-cn': case 'zh': code = 'zh-CHS'; break; case 'iw': code = 'he'; break; default: code = langCode; } if(code) { for (var i = 0; i < widgetLangCodes.length; i++) { if (widgetLangCodes[i] === code) { return widgetLangCodes[i]; } else { var re = new RegExp('^' + widgetLangCodes[i], 'i'); if (code.match(re)) return widgetLangCodes[i]; } } } return null; } this.getConvertedLangs = function() { return widget.c('Data').get().convert_langs; }; this.addConvertedLang = function(lang) { var langs = that.getConvertedLangs(); if (!langs) { langs = []; widget.c('Data').get().convert_langs = langs; } lang = that.get(lang); for (var i = 0; i < langs.length; ++i) { if (langs[i].code === lang.code) return; } langs.push(lang); }; /** * Returns the language identifier for the given language code. * The language identifier is the string that is displayed in the * domain, path, or query. * * @param {String} langCode A language code * * @returns {String} The custom identifier if it is configured on * backend, otherwise the langCode itself. */ this.getLangIdentifier = function (langCode) { if (!widget.isBackend()) return that.getCode(langCode); return langCodeAliases[langCode] || that.getCode(langCode); }; this.getBackendLangIdentifier = function () { var lang = widget.getBackendCurrentLang(); return this.getLangIdentifier(lang); } }; if (typeof(components) === 'undefined') var components = {}; components['Node'] = function (widget) { var that = this; var wovnCommentPrefix = 'wovn-src:'; /** * Check node is appropriate * * When IE, some TextNodes are split to multiple value, this value should not be reported and translated by the node's data * * @param nodeContainer {NodeContainer} * @returns {boolean} */ this.isLegitimateNode = function(nodeContainer) { if (nodeContainer.nodeName === '#text') { if (isSplitTextValue(nodeContainer.node)) return false; } return true; }; /** * Check node is split to multiple TextNode. * * @param node [TextNode] * @returns {boolean} */ function isSplitTextValue(node) { var previousSibling = node.previousSibling; if (previousSibling) { var previousNodeName = previousSibling.nodeName; if (previousNodeName === '#text') return true; if (isWovnComment(previousSibling, previousNodeName)) { var prePreviousNode = previousSibling.previousSibling; if (prePreviousNode && prePreviousNode.nodeName === '#text') { return true; } } } var nextSibling = node.nextSibling; if (nextSibling) { var nextNodeName = nextSibling.nodeName; if (nextNodeName === '#text') return true; if (isWovnComment(nextSibling, nextNodeName)) { var nextNextNode = nextSibling.nextSibling; if (nextNextNode && nextNextNode.nodeName === '#text') { return true; } } } return false; } /** * Check node is CommentNode and node is created by wovn (ValueStore#saveTranslationDataAsComment()) * * @param nodeContainer {NodeContainer} * @returns {*} */ function isWovnComment(nodeContainer) { if (nodeContainer.nodeName !== '#comment') return false; return startsWith(nodeContainer.data, wovnCommentPrefix); } /** * Faster comparator than indexOf() === 0 * * @param text [String] * @param searchText [String] */ function startsWith(text, searchText) { if (text.startsWith) return text.startsWith(searchText); for(var i = 0; i < searchText.length; i++) { if (text.charCodeAt(i) !== searchText.charCodeAt(i)) { return false; } } return true; } /** * Check node is first * @param textNode {Node} * @returns {boolean} */ this.isFirstTextNode = function(textNode) { var currentNode = textNode; while(currentNode) { var previousNode = currentNode.previousSibling; if (!previousNode) { return true; } var previousNodeName = previousNode.nodeName; if (previousNodeName === '#text') { if (/^\s*$/.test(previousNode.data) === false) { return false; } } else if (isWovnComment(previousNode, previousNodeName) == false) { return true; } currentNode = previousNode; } }; /** * Assign empty string to not legitimate node * * IE sometime split text to multiple NodeText, so hide all nodes rather than first * Translate only first node, and others are hidden * @param nodeContainer {NodeContainer} */ this.disableIllegitimateNode = function(nodeContainer) { if (nodeContainer.nodeName !== '#text') return; var checkingNode = nodeContainer.node.nextSibling; while(checkingNode) { var checkingNodeContainer = widget.c('NodeContainer').create(checkingNode) var next = checkingNode.nextSibling; if (checkingNodeContainer.nodeName === '#text') { if (checkingNodeContainer.data !== '') { checkingNodeContainer.replaceData(''); } } else if (isWovnComment(checkingNodeContainer)) { checkingNode.parentNode.removeChild(checkingNode); } else { break; } checkingNode = next; } }; /** * Call wholeText or fallback for IE8 * @param node * @returns {string} */ this.wholeText = function(node) { var wholeText = node.wholeText; if (!wholeText) { wholeText = ""; var currentNode = node; while(currentNode) { if (currentNode.nodeName === '#text') { wholeText += currentNode.data; } else { break; } currentNode = currentNode.nextSibling; } } return wholeText; }; /** * Get xpath from node * * derived from firebug * @see https://github.com/firebug/firebug/blob/master/extension/content/firebug/lib/xpath.js * @param node {Element} * @returns {String|Null} */ this.getXpath = function (node) { var paths = []; var currentNode = node; if (node.nodeName === '#text') { paths.push('text()') currentNode = currentNode.parentElement; } for(; currentNode && currentNode.nodeType === Node.ELEMENT_NODE; currentNode = currentNode.parentElement) { var index = 0; var hasFollowingSiblings = false; for (var sibling = currentNode.previousSibling; sibling; sibling = sibling.previousSibling) { if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE) { continue; } if (sibling.nodeName === currentNode.nodeName) { index++; } } for (var sibling = currentNode.nextSibling; sibling && !hasFollowingSiblings; sibling = sibling.nextSibling) { if (sibling.nodeName === currentNode.nodeName) { hasFollowingSiblings = true; } } // localname and prefix is used at original code, but we use only nodeName at DomAuditor. var tagName = currentNode.nodeName.toLowerCase(); var pathIndex = (index || hasFollowingSiblings) ? '[' + (index + 1) + ']': ''; paths.splice(0, 0, tagName + pathIndex); } return paths.length ? "/" + paths.join("/") : null; } }; if (typeof(components) === 'undefined') var components = {}; components['NodeContainer'] = function (widget) { var that = this; /** * * @param node {Node} * @returns {NodeContainer} */ this.create = function(node) { return new NodeContainer(node) } /** * Class NodeContainer * * @param node * @constructor */ function NodeContainer(node) { var that = this this.node = node this.nodeName = node.nodeName this.data = node.data /** * Return UpperCase nodeName even if #text or something * @returns {String} */ this.getUpperNodeName = function() { if (!that.nodeName) return null //nodeName is always upper or lower var charCode = that.nodeName.charCodeAt(1) if ((65 <= charCode && charCode <= 90) === false) { return that.nodeName.toUpperCase() } else { return that.nodeName } } /** * Replace node's data * @param data {String} */ this.replaceData = function(data) { that.node.data = data that.data = data } /** * Refresh data * WARNING: * Consider using #replaceData. * Ideally, this method shouldn't be called anywhere. * This shouldn't be called without deep consideration */ this.refreshData = function() { var currentData = that.node.data if (currentData !== that.data) { that.data = currentData } } /** * Return true if value is Node (not attribute) * @returns {boolean} */ this.isValueNode = function() { if (that.nodeName === 'alt') return true; if (that.nodeName === 'value') return true; if (that.nodeName === 'placeholder') return true; if (that.nodeName === 'content') return true; if (that.nodeName === 'label') return true; return false } } } if (typeof(components) === 'undefined') var components = {}; components['Url'] = function(widget) { var that = this; var isOptionLoaded = false; var _currentOptions = { urlPattern: null }; /** * Get current options * @returns {{}} */ function getCurrentOptions() { if (isOptionLoaded) { return _currentOptions; } if (widget.tag.getAttribute('urlPattern')) { _currentOptions.urlPattern = widget.tag.getAttribute('urlPattern'); isOptionLoaded = true; } else { var options = widget.c('Data').getOptions(); if (options && options.lang_path) { switch (options.lang_path) { case 'query': _currentOptions.urlPattern = 'query'; break; case 'path': _currentOptions.urlPattern = 'path'; break; case 'subdomain': _currentOptions.urlPattern = 'subdomain'; break; } isOptionLoaded = true; } } return _currentOptions; } /** * Get current option * @returns {{}} */ this.getOptions = function () { return getCurrentOptions(); }; /** * Replace current option * @param {{}} options */ this.setOptions = function(options) { var currentOptions = getCurrentOptions(); for (var key in options) if (currentOptions.hasOwnProperty(key)) currentOptions[key] = options[key]; }; /** * Get current language code * @param {string} url * @returns {string} */ this.getLangCode = function(url) { url = this.getLocation(url || location.href).href; var match = null; var rx; var currentOptions = getCurrentOptions(); switch (currentOptions.urlPattern) { case 'query': rx = new RegExp('((\\?.*&)|\\?)wovn=([^#&]+)(#|&|$)'); match = url.match(rx); match = match ? match[3] : null; break; case 'hash': rx = new RegExp('((\\#.*&)|\\#)wovn=([^&]+)(&|$)'); match = url.match(rx); match = match ? match[3] : null; break; case 'subdomain': rx = new RegExp('://([^.]+)\.'); match = url.match(rx); match = match && (widget.c('Lang').isCaseInsensitiveCode(match[1]) || widget.c('Lang').isCaseInsensitiveAlias(match[1])) ? match[1] : null; break; case 'path': var sitePrefix = widget.c('Config').getSitePrefixPath(); if (sitePrefix) { rx = new RegExp('(://[^/]+|^)/' + sitePrefix + '/([^/#?]+)'); } else { rx = new RegExp('(://[^/]+|^)/([^/#?]+)'); } match = url.match(rx); match = match && (widget.c('Lang').isCode(match[2]) || widget.c('Lang').isAlias(match[2])) ? match[2] : null; break; } if (match) { var langCode = widget.c('Lang').getCode(match); if (langCode) { if (!widget.c('Lang').hasAlias(langCode) || (match === widget.c('Lang').getLangIdentifier(langCode))) { return langCode; } } } return widget.c('Lang').defaultCode(); }; /** * Detects the protocol used for a given URL. * * @param {String} url The URL to process. * * @return {String} The protocol of the given URL. */ this.getProtocol = function(url) { var protocolMatching = /^([a-zA-Z]+):/.exec(url); if (protocolMatching && protocolMatching[1]) { return protocolMatching[1].toLowerCase(); } return location.protocol.replace(/:$/, '').toLowerCase(); } this.getDomainPort = function(url) { var match = /:\/\/(.[^\/]+)\/?/.exec(url); if (match) { return match[1]; } else { return ''; } }; this.getFlags = function (url) { url = url || location.href; var hash = url.match(/#[^?]*$/); hash = hash ? hash[0] : '#'; var match = hash.match(/(^|#|&)wovn=([^#&]*)(&|#|$)/); if (!match || match.length < 3) return []; // remove empty flags in the middle or beginning/ending of string var match = match[2].replace(/,(,+)/g, ',').replace(/^,|,$/g, ''); if (match === '') return []; return match.split(','); }; this.hasFlag = function (flag, url) { url = url || location.href; var flags = that.getFlags(url); return widget.c('Utils').indexOf(flags, flag) !== -1; }; this.getUrl = function(lang, url) { url = url || location.href; var protocol = this.getProtocol(url); if (protocol !== 'http' && protocol !== 'https') { return url; } var oldLangCode = this.getLangCode(url); var newLangCode = widget.c('Lang').getCode(lang); var urlPattern = getCurrentOptions().urlPattern; var urlFormatter = widget.c('UrlFormatter').createFromUrl(url); return urlFormatter.getConvertedLangUrl(oldLangCode, newLangCode, urlPattern) } /** * Get url for specific language * @param {string} lang * @param {Element} node * @param {string} url * @returns {string|null} */ this.langUrl = function(lang, node, url) { var currentOptions = getCurrentOptions(); if (!currentOptions.urlPattern) { return null; } var protocol = this.getProtocol(url); if (protocol !== 'http' && protocol !== 'https') { return null; } if (node.hasAttribute('href') && node.host && (node.host.toLowerCase() === location.host.toLowerCase() || currentOptions.urlPattern === 'subdomain')) { var url = url || node.getAttribute('href'); if (/^\/\//.test(url)) url = location.protocol + url; if (url === '' || url.match(/^[#?]/)) { return null; } else if (!url.match(/^https?:\/\//)) { if (url.match(/^\//)) { url = node.protocol + '//' + node.host + url; } else { url = node.protocol + '//' + node.host + node.pathname.replace(/\/[^\/]*$/,'/') + url; } } else if (node.host.toLowerCase() !== location.host.toLowerCase()) { // case when urlPattern is subdomain and url absolute if (this.getLangCode(url) !== widget.c('Lang').defaultCode()) { url = url.replace(new RegExp('://' + widget.c('Lang').getLangIdentifier(this.getLangCode(url)) + '\\.', 'i'), '://'); node.href = url; // we need to check if the hosts actually match again because if the host never contained a language var parser = document.createElement('a'); parser.href = location.href.replace(new RegExp('://' + widget.c('Lang').getLangIdentifier(this.getLangCode(location.href)) + '\\.', 'i'), '://'); if (node.host.toLowerCase() !== parser.host) { return null; } } else { return null; } } return this.getUrl(lang, url); } return null; }; this.changeUrl = function(langCode) { if (this.isLiveEditor()) return; var newLocation = this.getUrl(langCode); // If a browser doesn't support history.replaceState, the location will be changed // ALSO, if the host(subdomain) changes, the location wil also be changed try { if (widget.c('Data').getOptions().force_reload) throw('dummy exception'); else { var newState = window.history.state || {}; newState['wovn'] = langCode; window.history.replaceState(newState, null, newLocation); } } catch (e) { location.href = newLocation; } } this.isLiveEditor = function () { return /wovn\.editing/i.test(location.hash) } this.getEncodedLocation = function (customLocation) { return encodeURIComponent(getCurrentLocation(customLocation)); }; this.removeHash = function(url) { var index = url.indexOf('#'); return index === -1 ? url : url.substr(0, index); }; /** * Gets the current location of the browser without the backend-inserted lang code * * @return {string} The unicode-safe location of this browser without the lang code */ function getCurrentLocation (currentLocation) { // not all browsers handle unicode characters in the path the same, so we have this long mess to handle it // TODO: decodeURIcomponent doesnt handle the case where location has char like this: &submit=%8E%9F%82%D6%90i %82%DE (characters encoded in shift_jis) // adding unescape before it makes the error go away but doesnt fix the pb and creates pb for utf8 encode params if (!currentLocation) currentLocation = location; if (typeof(currentLocation) !== 'string') currentLocation = currentLocation.protocol + '//' + currentLocation.host + currentLocation.pathname + currentLocation.search; if (widget.tag.getAttribute('backend')) { var currentLang = widget.tag.getAttribute('currentLang'); var currentLangIdentifier = widget.c('Lang').getLangIdentifier(currentLang); switch (widget.tag.getAttribute('urlPattern')) { case 'query': currentLocation = currentLocation.replace(new RegExp('(\\?|&)wovn=' + currentLangIdentifier + '(&|$)'), '$1').replace(/(\?|&)$/, ''); break; case 'subdomain': currentLocation = currentLocation.replace(new RegExp('//' + currentLangIdentifier + '.', 'i'), '//'); break; case 'path': currentLocation = currentLocation.replace(new RegExp('(//[^/]+)/' + currentLangIdentifier + '(/|$)'), '$1/'); } } return currentLocation; } this.apiHostBase = "//ee.wovn.io/"; this.getApiHost = function () { var host = this.apiHostBase; return host.replace(/^.*\/\//, '//'); } this.getLocation = function (url) { var newLocation = document.createElement('a'); newLocation.href = url; // IE dont load the attributes "protocol" and "host" in case the source URL // is just a pathname, that is, "/example" and not "http://domain.com/example". newLocation.href = newLocation.href; // IE 7 and 6 won't load "protocol" and "host" even with the above workaround, // so we take the protocol/host from window.location and place them manually if (newLocation.host === "") { var newProtocolAndHost = window.location.protocol + "//" + window.location.host; if (url.charAt(1) === "/") { newLocation.href = newProtocolAndHost + url; } else { // the regex gets everything up to the last "/" // /path/takesEverythingUpToAndIncludingTheLastForwardSlash/thisIsIgnored // "/" is inserted before because IE takes it of from pathname var currentFolder = ("/"+newLocation.pathname).match(/.*\//)[0]; newLocation.href = newProtocolAndHost + currentFolder + url; } } return newLocation; } this.getNormalizedHost = function (location) { var host = location.host; if (location.protocol === 'http:' && /:80$/.test(host)) { host = host.replace(/:80$/, '') } else if (location.protocol === 'https:' && /:443$/.test(host)) { host = host.replace(/:443$/, '') } return host; } /** * Say true if url is third-party's link */ this.shouldIgnoreLink = function (url) { // get url's location and host var urlFormatter = widget.c('UrlFormatter').createFromUrl(url); var urlHost = urlFormatter.extractHost(); // get current location and host var curLocationFormatter = widget.c('UrlFormatter').createFromUrl("/"); var currentHost = curLocationFormatter.extractHost(); var host_aliases = widget.c('Data').createNormalizedHostAliases(); host_aliases.push(currentHost); return host_aliases.indexOf(urlHost) == -1; } }; if (typeof(components) === 'undefined') var components = {}; components['UrlFormatter'] = function (widget) { var locationCache = {}; function getLocation(url) { var langCode = widget.c('Url').getLangCode(url); if (!locationCache[url]) { locationCache[url] = {}; } if (!locationCache[url][langCode]) { locationCache[url][langCode] = widget.c('Url').getLocation(url); } return locationCache[url][langCode]; } this.createFromUrl = function(url) { var location = getLocation(url); var host = widget.c('Url').getNormalizedHost(location); var startsWithProtocol = /^https?:\/\//.test(url) // Sadly, IE removes first "/" var pathname = (location.pathname.charAt(0) !== "/" ? "/" : "") + location.pathname; // Url#pathname always starts with '/' even if url ends with domain (http://example.com) if (startsWithProtocol && /^https?:\/\/.[^/]+$/.test(url)) { pathname = ''; } var formatter = new UrlFormatter(location.protocol, host, pathname, location.search, location.hash) if (!startsWithProtocol) { if (!/^\//.test(url)) { var fullUrl = formatter.getOriginalUrl() formatter.setBaseIgnorePath(fullUrl.substr(0, fullUrl.indexOf(url))) } else { formatter.setToShowUrlFromPath(); } } return formatter; } this.create = function(protocol, host, pathname, search, hash) { return new UrlFormatter(protocol, host, pathname, search, hash) } /** * Same property name with Spec * * @see https://html.spec.whatwg.org/multipage/browsers.html#the-location-interface * @see https://developer.mozilla.org/en-US/docs/Web/API/Location * * @param protocol * @param host * @param pathname * @param search * @param hash * @constructor */ function UrlFormatter(protocol, host, pathname, search, hash) { var that = this; this.protocol = protocol; this.host = host; this.pathname = pathname; this.search = search; this.hash = hash; this.fromPath = false; this.baseIgnorePath = null; this.setShowFullUrl = function () { this.fromPath = false; this.baseIgnorePath = null; } this.setToShowUrlFromPath = function () { this.fromPath = true; } this.setBaseIgnorePath = function (path) { this.baseIgnorePath = path; } this.getOriginalUrl = function () { return createUrl(that.protocol, that.host, that.pathname, that.search, that.hash); } this.getNormalizedPageUrl = function (isBackend, urlPattern) { var normalizedUrl = that.getOriginalUrl(); if (isBackend) { var langIdentifier = widget.c('Lang').getBackendLangIdentifier(); switch (urlPattern) { case 'query': var newSearch = that.search.replace(new RegExp('(\\?|&)wovn=' + langIdentifier + '(&|$)'), '$1').replace(/(\?|&)$/, ''); normalizedUrl = createUrl(that.protocol, that.host, that.pathname, newSearch, that.hash) break; case 'subdomain': normalizedUrl = normalizedUrl.replace(new RegExp('//' + langIdentifier + '\\.', 'i'), '//'); break; case 'path': var sitePrefix = widget.c('Config').getSitePrefixPath(); if (sitePrefix) { var normalizedPath = that.pathname.replace(new RegExp('^/(' + sitePrefix + ')/' + langIdentifier + '(/|$)', 'i'), '/$1$2'); normalizedUrl = createUrl(that.protocol, that.host, normalizedPath, that.search, that.hash) } else { var normalizedPath = that.pathname.replace(new RegExp('^(/)?' + langIdentifier + '(/|$)', 'i'), '$2'); normalizedUrl = createUrl(that.protocol, that.host, normalizedPath, that.search, that.hash) } } } return normalizedUrl; } this.getConvertedLangUrl = function (fromLangCode, toLangCode, urlPattern) { var currentUrl = that.getOriginalUrl(); var newUrl; var fromLangIdentifier = widget.c('Lang').getLangIdentifier(fromLangCode); var toLangIdentifier = widget.c('Lang').getLangIdentifier(toLangCode); switch (urlPattern) { case 'query': if (toLangCode === widget.c('Lang').defaultCode()) { newUrl = currentUrl.replace(/([\?&])wovn=[^#&]*&?/, '$1'); // url doesn't contain a wovn query param } else if (!currentUrl.match(/[\?&]wovn=[^&#]*/)) { // url has a query string if (currentUrl.match(/\?/)) newUrl = currentUrl.replace(/\?/, '?wovn=' + toLangIdentifier + '&'); else newUrl = currentUrl.replace(/(#|$)/, '?wovn=' + toLangIdentifier + '$1'); } else { newUrl = currentUrl.replace(/([\?&])wovn=[^&#]*/, '$1wovn=' + toLangIdentifier); } // remove trailing ? and & newUrl = newUrl.replace(/&$/, ''); newUrl = newUrl.replace(/\?$/, ''); break; case 'subdomain': if (toLangCode === widget.c('Lang').defaultCode()) newUrl = currentUrl.replace(new RegExp('://' + fromLangIdentifier.toLowerCase() + '\\.', 'i'), '://'); else if (fromLangCode === widget.c('Lang').defaultCode()) newUrl = currentUrl.replace(new RegExp('://', 'i'), '://' + toLangIdentifier.toLowerCase() + '.'); else newUrl = currentUrl.replace(new RegExp('://' + fromLangIdentifier.toLowerCase() + '\\.', 'i'), '://' + toLangIdentifier.toLowerCase() + '.'); break; case 'path': var newPathname = removeLangFromPathname(urlPattern, that.pathname, fromLangCode); newPathname = addLangToPathname(urlPattern, newPathname, toLangCode); newUrl = createUrl(that.protocol, that.host, newPathname, that.search, that.hash); break; default: newUrl = currentUrl; } return newUrl; } function removeLangFromPathname (urlPattern, pathname, currentLang) { if (urlPattern !== 'path') return pathname; var langIdentifier = widget.c('Lang').getLangIdentifier(currentLang); var sitePrefix = widget.c('Config').getSitePrefixPath(); if (sitePrefix) { return pathname.replace(new RegExp('^(/' + sitePrefix + ')/' + langIdentifier + '(/|$)'), '$1$2'); } else { return pathname.replace(new RegExp('^/' + langIdentifier + '(/|$)'), '$1'); } } function addLangToPathname (urlPattern, pathname, lang) { if (urlPattern !== 'path') return pathname; if (lang === widget.c('Lang').defaultCode()) return pathname; var langIdentifier = widget.c('Lang').getLangIdentifier(lang); var sitePrefix = widget.c('Config').getSitePrefixPath(); if (sitePrefix) { return pathname.replace(new RegExp('^(/' + sitePrefix + ')(/|$)'), '$1/' + langIdentifier + '$2'); } else { return '/' + langIdentifier + pathname; } } function createUrl (protocol, host, pathname, search, hash) { var url = protocol + '//' + host + pathname + search + hash; if (that.baseIgnorePath) { if (widget.c('Utils').stringStartsWith(url, that.baseIgnorePath, 0)) { url = url.replace(that.baseIgnorePath, '') } else { url = pathname + search + hash; } } else if (that.fromPath) { url = pathname + search + hash; } return url; } this.extractHost = function () { return that.host; } } } if (typeof(components) === 'undefined') var components = {}; components['SingleWorker'] = function(widget) { var that = this; /** * Default time for cool down time. * This maybe changed at test but may not be changed at production environment. * @type {number} */ this.defaultCoolDownTime = 1000; /** * Create Single Worker * @returns {SingleWorker} */ this.createSingleWorker = function() { return new SingleWorker(this.defaultCoolDownTime); } /** * SingleWorker execute function with cool down time. * SingleWorker ensure only last registered function is called and others are not called. * * @param coolDownTime * @constructor */ var SingleWorker = function(coolDownTime) { var workableId = 0; var isExecuting = false; var previousExecutedTime = null; /** * Like window.setTimeout, this execute after interval. * When executing something, function is not called to avoid long queue. * When previous function is called recently, function is called after cool down time. * * @param func {Function} * @param callback {Function} * @param interval {number} * @optional args.. {*} arguments for func */ this.setTimeout = function(func, callback, interval) { var now = new Date().getTime(); if (isExecuting) { // if call lately and exists long-time function, queue is overflown // to avoid this, do not add function while executing other function return; } else if (isCoolingDown(now)) { var coolDownEndTime = previousExecutedTime + coolDownTime; interval = Math.max(coolDownEndTime + 100, interval + now) - now; } workableId = (workableId + 1) % 10000; var funcArgus = Array.prototype.slice.call(arguments).slice(3); executeSetTimeout(func, callback, interval, workableId, funcArgus); } /** * Check current is cooling down time * @param now {number} * @returns {boolean} */ function isCoolingDown(now) { if (previousExecutedTime && previousExecutedTime + coolDownTime > now) { return true; } else { return false; } } /** * call setTimeout * @param func {Function} * @param callback {Function} * @param interval {number} * @param funcId {number} * @param funcArgus {Array} */ function executeSetTimeout(func, callback, interval, funcId, funcArgus) { var wrappedFunc = function(id, args) { if (id !== workableId) return; isExecuting = true; previousExecutedTime = null; func.apply(this, args); previousExecutedTime = new Date().getTime(); isExecuting = false; callback(); } var delayFunc = function(id, args) { setTimeout(function() { wrappedFunc(id, args) }, interval) }; delayFunc(funcId, funcArgus); } } } if (typeof(components) === 'undefined') var components = {}; components['DomIterator'] = function (widget) { var that = this; /* * This function iterates over all of the nodes in the DOM * it takes two arguments. An options object, and a function (block) * there are four valid keys for the options object: * head - the root of the iteration (can be any node) * DEFAULT - the root of the document * limit - the number of elements to process. After passing limit number of nodes to * the block function, the iteration will exit. * DEFAULT - no limit * filter - a function that takes two arguments, the current element and the current xpath * when this function returns a truthy value, the current element and its children will be skipped * NODE - skips script, noscript, style, and elements that have the same nodeName as node * STRING - skips script, noscript, style, and elements whose nodeName is string * SIGNATURE function (node, xpath) * DEFAULT a function that returns true on script, noscript, and style * NOTE -- the widget will ALWAYS be filtered * target - a function that takes two arguments, the current element and the current xpath * when this function returns a truthy value, the block will be executed on the current element * Also accepts a node, string, or an array of nodes/strings (mixed nodes and strings is ok) * NODE - executes the block when an element's nodeName matches the node's nodeName * STRING - executes the block when an element's nodeName matches the string * ARRAY - executes the block when an element's nodeName matches one of the given strings or nodes' nodeName * SIGNATURE function (node, xpath) * DEFAULT a function that returns true on all nodes except comments * * the function (block) is passed two arguments, in this order: * element - the current element that is being iterated over * xpath - the xpath value of the current element * the function (block) returns true if the node visit is complete (if there * is no need to visit the node's children). * * the function (everyBlock) is passed same arguments with block */ this.go = function (options, block, everyBlock) { if (typeof block !== 'function') return; if (typeof everyBlock !== 'function') return; var defaultOptions = { head: document.head || document.getElementsByTagName('head')[0], limit: -1, filter: function (node) { return node.nodeName.toLowerCase().match(/script|noscript|style/); }, target: function (node) { return node.nodeName !== '#comment'; } }; var headParentXpath; var head; if (options.hasOwnProperty('head') && options['head']) { head = options['head']; headParentXpath = widget.c('Node').getXpath(head) if (headParentXpath) { if (head.nodeName === '#text') { headParentXpath = headParentXpath.replace(/\/text\(\)$/, '') } else { headParentXpath = headParentXpath.replace(new RegExp('/' + head.nodeName.toLowerCase() + '$'), '') } } } if (!headParentXpath) { head = defaultOptions['head']; headParentXpath = '/html' } // check headXPath option if (!options.hasOwnProperty('headXPath') || typeof options['headParentXpath'] !== 'string') options['headParentXpath'] = defaultOptions['headParentXpath']; // check limit option // we need to be sure the limit is an integer so we use Math.floor to convert any floats to int if (!options.hasOwnProperty('limit') || typeof options['limit'] !== 'number') options['limit'] = defaultOptions['limit']; else options['limit'] = Math.floor(options['limit']); // if the limit is 0, there's nothing to do. This cannot be checked before performing Math.floor if (options['limit'] === 0) return; // check filter option if (options.hasOwnProperty('filter')) { if (typeof options['filter'] === 'function') { } else if (typeof options['filter'] === 'object' && options['filter'].nodeName) { options['filter'] = function (filterNodeName) { return function (node) { return node.nodeName.toLowerCase().match(/script|noscript|style/) || node.nodeName.toLowerCase() === filterNodeName; }; }(options['filter'].nodeName.toLowerCase()); } else if (typeof options['filter'] === 'string') { options['filter'] = function (filterNodeName) { return function (node) { return node.nodeName.toLowerCase().match(/script|noscript|style/) || node.nodeName.toLowerCase() === filterNodeName; }; }(options['filter'].toLowerCase()); } else options['filter'] = defaultOptions['filter']; } else options['filter'] = defaultOptions['filter']; // check target option if (options.hasOwnProperty('target') && typeof options['target'] !== 'function') { if (typeof options['target'] === 'object' && options['target'].length) { var regString = ''; for (var i = 0; i < options['target'].length; i++) regString += (options['target'][i].nodeName || options['target'][i]) + '|'; // remove extra '|' and add ^( and )$ regString = '^(' + regString.substr(0, regString.length - 1) + ')$'; var rx = new RegExp(regString, 'i'); options['target'] = function (rx) { return function (node) { return rx.test(node.nodeName); }; }(rx); } else if (typeof options['target'] === 'object' && options['target'].nodeName) { options['target'] = function (targetNodeName) { return function (node) { return node.nodeName.toLowerCase() === targetNodeName; }; }(options['target'].nodeName.toLowerCase()); } else if (typeof options['target'] === 'string') { options['target'] = function (targetNodeName) { return function (node) { return node.nodeName.toLowerCase() === targetNodeName; }; }(options['target'].toLowerCase()); } else options['target'] = defaultOptions['target']; } else options['target'] = defaultOptions['target']; var attributes = options['attributes']; var headElement = (document.head || document.getElementsByTagName('head')[0]).parentElement; domRecurse(headParentXpath, head); //vals.testNewContent(); function domRecurse(xpath, node) { if (!node) return; // With a broken HTML, firstChild returns null, but using childNodes[0] occurs an infinite recursion. // array index of current node in sibling nodes. // this is used for generating newXPath. var currLevel = {}; currLevel[node.nodeName] = 1; // iterate from last to first to avoid an infinite loop when block add nodes var lastNode = node; var nextNode = node.nextSibling; while (nextNode) { var nodeName = nextNode.nodeName; currLevel[nodeName] = currLevel[nodeName] ? currLevel[nodeName] + 1 : 1; lastNode = nextNode; nextNode = nextNode.nextSibling; } node = lastNode; var isFirst = true; var previousSibling = node && node.previousSibling while(true) { if (isFirst) { isFirst = false; } else { node = previousSibling; previousSibling = node && node.previousSibling } if (!node) break; if (node === headElement) return; if (options['filter'](node, xpath) || node.id && node.id === widget.c('Interface').WIDGET_ID) { afterEachLoop(currLevel, node.nodeName); continue; } var stopRecurse = false; var nodeName = node.nodeName; if (nodeName === '#text') { //check for comments and white space if (/^\s+$/.test(node.nodeValue)) { afterEachLoop(currLevel, nodeName); continue; } var xpName = 'text()'; } else { var xpName = nodeName.toLowerCase(); } var currLevelSafix = currLevel[nodeName] > 1 ? '[' + currLevel[nodeName] + ']' : ''; var newXPath = xpath + '/' + xpName + currLevelSafix; if (options['target'](node, newXPath) && options['limit'] !== 0) { stopRecurse = block(node, newXPath); --options['limit']; } everyBlock(node, newXPath); if (options['limit'] === 0) return; if (node.hasChildNodes() && !stopRecurse) domRecurse(newXPath, node.firstChild); if (nodeName === 'IFRAME') { var contentDocument = null; try { contentDocument = node.contentDocument; } catch(e) {} if (contentDocument) domRecurse('', contentDocument.firstChild); } if (attributes) { if (node.hasAttribute) { for (var attribute in attributes) { if (node.hasAttribute(attribute) && options['limit'] !== 0) { var newXPath = xpath + '/' + node.nodeName + '[@' + attribute + ']' + currLevelSafix; attributes[attribute](node, newXPath); --options['limit']; } } } } afterEachLoop(currLevel, nodeName); } function afterEachLoop(currLevel, nodeName) { currLevel[nodeName]--; } } } }; if (typeof(components) === 'undefined') var components = {}; components['ValueStore'] = function (widget) { var that = this; var langs = widget.c('Data').getPublishedLangs(); var defaultLang = widget.c('Lang').defaultCode(); var imgIndex = {}; imgIndex[defaultLang] = widget.c('Data').getImageValues(); var textIndex = {}; var htmlTextIndex = {}; textIndex[defaultLang] = widget.c('Data').getTextValues(); htmlTextIndex[defaultLang] = widget.c('Data').getHTMLTextValues(); //cannot add function to Data because of cache. var propertyIndex = widget.c('Data').get()['prop_vals'] || {}; var propertyIndexTags = []; var translatedLangs = [defaultLang]; var imgSrcPrefix = 'http://st.wovn.io/ImageValue/' + widget.c('Data').getPageId() + '/'; var corruptedVals = []; var newDetectedValueSet = {}; var wovnCommentPrefix = 'wovn-src:'; var originalBackgroundAttributeName = 'data-wovn-original-background'; this.srcsetOriginalValueStore = {}; /** * Property for propertyIndexTags * @returns {Array} */ this.propertyIndexTags = function() { return propertyIndexTags; }; this.imgSrcPrefix = function() { return imgSrcPrefix; }; /** * Provides a source text from a given node. * * @param {Node|Element} node The node to convert into a string. * @param {Boolean} useInsideWovnIgnore True if results includes inside wovn-ignore * @param {Boolean} surround True if the string must contain the tags of the * given node, false otherwise. * * @returns {String} The string representation of the node's data. */ this._nodeToSrc = function(node, useInsideWovnIgnore, surround) { var src = "" var tagName = node.nodeName.toLowerCase() if (tagName == "#text") { src = widget.c('Utils').normalizeText(node.data.replace('<', '<').replace('>', '>'), true) return src } if (tagName == 'br' || tagName == 'img') { src = "<" + tagName + ">"; return src } var content = "" var ignoresNode = isIgnoreNode(node) var visitsInsideNode = useInsideWovnIgnore || ignoresNode === false if (visitsInsideNode) { var cs = node.childNodes for(var i = 0; i < cs.length; i++) { content += widget.c('Utils').normalizeText(that._nodeToSrc(cs[i], useInsideWovnIgnore, true), true) } } if (surround) { if (ignoresNode) { src = "<" + tagName + " wovn-ignore>" + content + "" } else { src = "<" + tagName + ">" + content + "" } } else { src = content } return widget.c('Utils').normalizeText(src, true) }; function isIgnoreNode(node) { return node.hasAttribute && node.hasAttribute('wovn-ignore'); } var hostAliasSrcs = {} this.addHostAliasSrc = function (path, hostAlias) { if (!hostAliasSrcs.hasOwnProperty(path)) { hostAliasSrcs[path] = []; } if(!widget.c('Utils').includes(hostAliasSrcs, hostAlias)) { hostAliasSrcs[path].push(hostAlias) } } function fixDefaultIndex() { fixDefaultLangIndex(imgIndex); setHostAliasSrcs(); fixDefaultLangIndex(textIndex); fixDefaultLangIndex(htmlTextIndex); } function setHostAliasSrcs() { var hostAliases = widget.c('Data').createNormalizedHostAliases(); var defaultIndex = imgIndex[defaultLang]; for (var imageUrl in defaultIndex) { if (!defaultIndex.hasOwnProperty(imageUrl)) { continue; } var hostMatch = imageUrl.match(/^https?:\/\/(.+?)(\/|$)/) if (!hostMatch) { continue; } var host = hostMatch[1]; var path = imageUrl; if (!widget.c('Utils').includes(hostAliases, host)) { continue; } path = widget.c('Utils').extractPath(imageUrl); that.addHostAliasSrc(path, host) } } function fixDefaultLangIndex(index) { var defaultIndex = index[defaultLang]; for (var src in defaultIndex) { var entry = defaultIndex[src]; var xpath = ''; for (var lang in entry) { xpath = entry[lang][0].xpath; break; } if (!entry[defaultLang]) entry[defaultLang] = [{data: src, xpath: xpath}]; } } /** * Fix propertyIndexTags */ function fixPropertyIndexTags() { var tagHash = {}; for(var i = 0; i < langs.length; i++) { var lang = langs[i]; if (propertyIndex[lang]) { var keys = widget.c('Utils').keys(propertyIndex[lang]); for(var j = 0; j < keys.length; j++) { tagHash[keys[j]] = true; } } } propertyIndexTags = widget.c('Utils').keys(tagHash); } /** * Return image linked to original image * @param {string} imagePath * @param {string} language * @returns {string|null} */ this.getDstImage = function(imagePath, language) { if (!imagePath) return null; var imageData = getImageFromIndex(imagePath, imgIndex[defaultLang]); if (imageData && imageData[language]) { return imageData[language][0].data; } }; /** * Return data using multiple host in HostAliases * @param path {String} * @param index {Object} * @returns {Object} */ function getImageFromIndex(path, index) { if (index.hasOwnProperty(path)) { return index[path]; } return getHostAliasImage(path, index) } function getHostAliasImage(url, index) { var path = widget.c('Utils').extractPath(url); if (!hostAliasSrcs.hasOwnProperty(path)) { return undefined; } var hostAliases = hostAliasSrcs[path]; for (var i = 0; i < hostAliases.length; i++) { var currentHostAlias = hostAliases[i]; var otherSrc = url.replace(/(http(s)?:\/\/)[^\/]+(\/)?/, '$1' + currentHostAlias + '$3'); if (index.hasOwnProperty(otherSrc)) { var defaultLang = widget.c('Lang').defaultCode(); // re-fix index to support current HostAlias if (index[otherSrc].hasOwnProperty(defaultLang)) { for (var j = 0; j < index[otherSrc][defaultLang].length; j++) { var dst = index[otherSrc][defaultLang][i].data; var alias = dst.replace(/https?:\/\//, '').replace(/\/.*$/, ''); if (alias === currentHostAlias) { var dataPath = widget.c('Utils').extractPath(index[otherSrc][defaultLang][i].data) index[otherSrc][defaultLang][i].data = dataPath; } } } return index[otherSrc]; } } } /** * Add propertyIndexTags * @param {string} lang * @param {string} tagName */ this.addPropertyIndexTag = function(lang, tagName) { propertyIndex[lang] = propertyIndex[lang] || []; if (!propertyIndex[lang][tagName]) { propertyIndex[lang][tagName] = []; fixPropertyIndexTags(); } }; initialize(); function initialize () { fixDefaultIndex(); fixPropertyIndexTags(); cleanNewLines(); } /** * Searches within the textIndex and htmlTextIndex and normalize any values. * * @method cleanNewLines * @return Does not return a value */ function cleanNewLines () { for (var defaultLanguage in textIndex) { cleanNewLinesForIndexAndLang(textIndex, defaultLanguage) } for (var defaultLanguage in htmlTextIndex) { cleanNewLinesForIndexAndLang(htmlTextIndex, defaultLanguage) } } function cleanNewLinesForIndexAndLang(index, lang) { for (var textNode in index[lang]) { if (!index[lang].hasOwnProperty(textNode)) continue; var normalizedText = textNode; if (index === htmlTextIndex) { var dummyElm = document.createElement('P') dummyElm.innerHTML = normalizedText; normalizedText = that._nodeToSrc(dummyElm, false, false); } else { normalizedText = widget.c('Utils').normalizeText(textNode); } if (normalizedText !== textNode) { var cleanedNodeValue = index[lang][textNode]; for (var language in cleanedNodeValue){ if (!cleanedNodeValue.hasOwnProperty(language)) continue; for (var version in cleanedNodeValue[language]){ if (!cleanedNodeValue[language].hasOwnProperty(version)) continue; } } // create new keys and delete the original index[lang][normalizedText] = cleanedNodeValue; delete index[lang][textNode]; } } } this.corruptedVals = corruptedVals; this.isCorrupted = function (val) { return isCorrupted(val); }; function isCorrupted (val) { if (typeof(val) !== 'object') { corruptedVals.push(val); return true; } if (!val.hasOwnProperty('src') || !val.hasOwnProperty('dst') || !val.hasOwnProperty('language')) { corruptedVals.push(val); return true; } if (val.src === val.dst) { corruptedVals.push(val); return true; } return false; } function add (nVals) { nVals = nVals || []; for (var i = 0; i < nVals.length; i++) { if (isCorrupted(nVals[i])) { continue; } var index; var srcData; var data; if (/img(\[[^\]]*\])?$/.test(nVals[i].xpath) || nVals[i].src_img_path || /\[@background-image\]$/.test(nVals[i].xpath)) { index = imgIndex[defaultLang]; srcData = nVals[i].src || nVals[i].src_img_path; data = nVals[i].dst || nVals[i].img_path; if (data !== srcData) data = imgSrcPrefix + data; } else { if (nVals[i].complex === true) { index = htmlTextIndex[defaultLang]; } else { index = textIndex[defaultLang]; } srcData = widget.c('Utils').trimString(nVals[i].hasOwnProperty('src') ? nVals[i].src : nVals[i].src_body); // replace empty string with the zero-width character. If we don't, // the text node disappears on replace. data = widget.c('Utils').trimString(nVals[i].hasOwnProperty('dst') ? nVals[i].dst : nVals[i].body) || '\u200b'; } var lang = nVals[i].language; widget.c('Utils').pushUnique(translatedLangs, lang); if (!index.hasOwnProperty(srcData)) { index[srcData] = {} index[srcData][defaultLang] = []; } // dom iterator doesn't support [1] index[srcData][defaultLang].push({xpath: (nVals[i].xpath || '').replace(/\[1\]/g, ''), data: srcData}); if (lang) { if (!index[srcData].hasOwnProperty(lang)) index[srcData][lang] = []; index[srcData][lang].push({xpath: (nVals[i].xpath || '').replace(/\[1\]/g, ''), data: data}); } } // clean new lines if they're mutated if (widget.c('Agent').mutatesTextNodeData()) cleanNewLines(); } this.addValues = add; function buildIndex (lang) { build(textIndex, lang); build(htmlTextIndex, lang); build(imgIndex, lang); } /** * Build specified index if not build * * @param {Dictionary} index - Index for build * @param {String} lang - lang for build * @return Does not return a value */ function build (index, lang) { var mergeEntries = function(from, to) { for (var i = 0; i < from.length; ++i) { if (widget.c('Utils').findIndex(to, from[i], function(entry1, entry2) {return entry1.data === entry2.data && entry1.xpath === entry2.xpath})) { to.push(from[i]); } } } if (index[lang]) return; index[lang] = {}; for (var defLangSrc in index[defaultLang]){ if(index[defaultLang].hasOwnProperty(defLangSrc)) { var transObj = index[defaultLang][defLangSrc]; if (transObj[lang]) { for (var i = 0; i < transObj[lang].length; i++) { var data = transObj[lang][i].data; var langTransObj = index[lang][data]; if (!langTransObj) { langTransObj = {}; index[lang][data] = langTransObj; } for (var l in transObj) if (transObj.hasOwnProperty(l)) { if (!l || !transObj[l]) continue; if (!langTransObj[l]) langTransObj[l] = []; mergeEntries(transObj[l], langTransObj[l]); } } } } } if(index === textIndex || index === htmlTextIndex) { cleanNewLinesForIndexAndLang(index, lang); } } /** * Add non-translated value if not added * @param value {String} */ this.noteMissedValue = function(value) { if (!newDetectedValueSet.hasOwnProperty(value)) { newDetectedValueSet[value] = false; } } /** * Get and merge non-translated value to textIndex/imgIndex from server */ this.loadNewDetectedValue = function() { var valueCountThreshold = 100; var values = []; for(var value in newDetectedValueSet) { if (!newDetectedValueSet.hasOwnProperty(value)) continue; if (newDetectedValueSet[value]) continue; newDetectedValueSet[value] = true; values.push(value); if (values.length >= valueCountThreshold) { break; } } if (values.length === 0) return; var callback = function(d) { widget.c('ValueStore').mergeValues(d['text_vals'], d['img_vals']) } widget.loadTranslation(values, callback, function() {}) } /** * Merge new values to textIndex/imgIndex * @param textValues {Array} * @param imageValues {Array} */ this.mergeValues = function(textValues, imageValues) { var addedLangs = []; var zippedObject = [[textValues, textIndex], [imageValues, imgIndex]]; for (var i = 0; i < zippedObject.length; i++) { var values = zippedObject[i][0]; var index = zippedObject[i][1]; for (var src in values) { if(!values.hasOwnProperty(src)) continue; var translation = values[src]; index[defaultLang][src] = translation; var translationLangs = Object.keys(translation); widget.c('Utils').pushUnique(addedLangs, translationLangs) } } fixDefaultIndex(); for(var i = 0; i < addedLangs.length; i++) { buildIndex(addedLangs[i]); } } // score is calculated by number of nodes that are equal function matchScore (a, b) { var result = 0; var x = a.length - 1; var y = b.length - 1; while (x >= 0 && y >= 0) { if (a[x] !== b[y]) return result; if (a[x] === '/') result++; x--; y--; } return result; } function bestMatch (options, path) { if (!options || options.length === 0) return null; if (options.length === 1) return options[0]; var bestMatch = options[0]; var bestScore = matchScore(path, options[0].xpath); var score = 0; for (var i = 1; i < options.length; i++) { score = matchScore(path, options[i].xpath); if (score > bestScore) { bestMatch = options[i]; bestScore = score; } } return bestMatch; } /** * Save the original data of the node to wovnTranslation and the index. * @param {Object} value - an entry of the index ({data: "text", path: "xpath"}) * @param {NodeContainer} nodeContainer - a NodeContainer that holds the value to translate * @return {Object} value */ function saveNodeData(value, nodeContainer) { var nodeData = nodeContainer.data || widget.c('ValueStore').getData(nodeContainer); if (value && widget.c('Utils').normalizeText(value.data) === widget.c('Utils').normalizeText(nodeData)) { value.data = nodeData; } return value; } this.srcExists = function (node, langCode) { if (!textIndex[langCode]) buildIndex(langCode); var nodeContainer = widget.c('NodeContainer').create(node) var data = this.getData(nodeContainer); if (data === '') return true; var index = getIndex(nodeContainer, langCode); return index.hasOwnProperty(data); }; /** * Apply property setting from prop_vals * @param {Node} node * @param {String} newLangCode * @param {boolean} forceRevert */ this.applyPropertySetting = function(node, newLangCode, forceRevert) { if (forceRevert) { revertProperty(node); } var property = that.getProperty(widget.c('NodeContainer').create(node), newLangCode); if (!property) return; replaceProperty(property['dst'], node, null); }; /** * Get Property setting for specific node * @param {NodeContainer} nodeContainer * @param {String} newLangCode * @returns {Object} property setting */ this.getProperty = function(nodeContainer, newLangCode) { if(!(propertyIndex[newLangCode] && propertyIndex[newLangCode][nodeContainer.nodeName])) return null; var betterProperties = []; for(var i = 0; i < propertyIndex[newLangCode][nodeContainer.nodeName].length; i++) { var property = propertyIndex[newLangCode][nodeContainer.nodeName][i]; if (hasApplyPossibility(property, nodeContainer.node)) { betterProperties.push(property); } } var bestProperty = betMatchProperties(betterProperties, nodeContainer.node); return bestProperty; }; /** * Whether property have any possibility of applying to node * @param {Object} property * @param {Node} node * @returns {boolean} */ function hasApplyPossibility(property, node) { for(var srcPropertyName in property['src_property']) { if (!property['src_property'].hasOwnProperty(srcPropertyName)) continue; if (srcPropertyName === 'childTextContent') { var expectedSrc = property['src_property'][srcPropertyName]; var childTextContent = that.getSrcChildTextContent(node); return expectedSrc === childTextContent; } else { if (window.getComputedStyle(node)[srcPropertyName] !== property['src_property'][srcPropertyName]) { return false; } } } return true; } /** * Get ndoe-children's data * @param {Node} node * @returns {string} */ this.getSrcChildTextContent = function(node) { var childTextContent = ""; var children = getChildNodes(node); for(var i = 0; i < children.length; i++) { var childNodeContainer = widget.c('NodeContainer').create(children[i]); if (childNodeContainer.nodeName !== '#text') continue; var childSrc = widget.c('ValueStore').getDefaultValue(childNodeContainer, ''); if (!childSrc || !childSrc.data) continue; childTextContent += widget.c('Utils').normalizeText(childSrc.data); } return childTextContent; }; /** * Select best property settings from candidates * @param properties * @param node * @returns {Object} */ function betMatchProperties(properties, node) { var maxEstimationPoint = -1; var estimatedProperty = null; for(var i = 0; i < properties.length; i++) { var property = properties[i]; var selectors = property['dst']['selectors']; var estimationPoint = getEstimationPoint(selectors, node); if (estimationPoint > 0 && maxEstimationPoint <= estimationPoint) { estimatedProperty = property; maxEstimationPoint = estimationPoint; } } if (estimatedProperty) { return estimatedProperty; } return null; } /** * Get Property's point for comparing suitable property setting * @param {Array.Object.}selectors * @param {Node} node * @returns {number} */ function getEstimationPoint(selectors, node) { var currentNode = node; var estimationPoint = 0; for(var selectorIndex = 0; selectorIndex < selectors.length; selectorIndex++) { currentNode = getParentElement(currentNode); var selector = selectors[selectorIndex]; if (currentNode.nodeName.toUpperCase() !== selector['tag_name'].toUpperCase()) { return -1; } var siblings = currentNode.parentNode.children; var estimatedPosition = selector['position'] || 0; var siblingPosition = 0; for(var siblingIndex = 0; siblingIndex < siblings.length; siblingIndex++) { var sibling = siblings[siblingIndex]; if (sibling.nodeName.toUpperCase() == selector['tag_name'].toUpperCase()) { if (siblingPosition === estimatedPosition) { if (currentNode !== sibling) { return -1; } break; } siblingPosition++; } } estimationPoint++; if (currentNode.getAttribute('id') == selector['element_id']) { estimationPoint += 10; } if (selector['classes'] && currentNode.className) { var classes = selector['classes']; var nodeClass = widget.c('Utils').to_set(currentNode.className.split(/\s+/)); for (var classIndex = 0; classIndex < classes.length; classIndex++) { if (nodeClass[classes[classIndex]]) { estimationPoint += 10 / classes.length; } } } } return estimationPoint; } /** * Replace Node's property * @param {Object} dstProperty * @param {Element} node * @param {Object} originalProperty */ function replaceProperty(dstProperty, node, originalProperty) { revertProperty(node); var styleKeys = widget.c('Utils').keys(dstProperty['style']); var originalProperty = originalProperty || {style: {}}; for(var i = 0; i < styleKeys.length; i++) { var key = styleKeys[i]; if(originalProperty.style.hasOwnProperty(key) == false) { originalProperty.style[key] = node.style[key]; } node.style[key] = dstProperty['style'][key]; var rightCssStyleName = key.replace(/([A-Z])/, "-$1").toLowerCase(); var regex = new RegExp('((' + rightCssStyleName + ': [^;]+?)( !important)?);', 'g'); node.style.cssText = node.style.cssText.replace(regex, "$1 !important;") } saveOriginalProperties(originalProperty, node); } /** * Set property to node * @param {Object} dstProperty * @param {Element} node */ this.setProperty = function(dstProperty, node) { var originalProperty = that.getOriginalProperties(node); replaceProperty(dstProperty, node, originalProperty); }; /** * Revert Property * @param {Element} node */ function revertProperty(node) { var originalProperty = that.getOriginalProperties(node); //supports only style, now if (originalProperty && originalProperty['style']) { var originalStyle = originalProperty['style']; var originalKeys = widget.c('Utils').keys(originalStyle); for(var i = 0; i < originalKeys.length; i++) { var key = originalKeys[i]; node.style[key] = originalStyle[key]; } } } /** * Save original property to data attribute * @param {Object} properties * @param {Element} node */ function saveOriginalProperties(properties, node) { node.setAttribute(getOriginalPropertyName(), JSON.stringify(properties)); } /** * Get saved property from data attribute * @param {Element} node * @returns {Object} */ this.getOriginalProperties = function(node) { if (node.getAttribute) { var propertiesString = node.getAttribute(getOriginalPropertyName()); if (propertiesString) { return widget.c('Utils').parseJSON(propertiesString); } } return null; } /** * Get data attribute's name for original property * @returns {string} */ function getOriginalPropertyName() { return 'data-wovn-original-property'; } this.getParentElementOverrideFunc = undefined; /** * Get Parent (override when LiveEditor) * @param {Node} node * @returns {Node} parent */ function getParentElement(node) { if (that.getParentElementOverrideFunc) { return that.getParentElementOverrideFunc(node); } else { return node.parentNode; } } this.getChildNodesOverrideFunc = undefined; /** * Get Children (override when LiveEditor) * @param {Node} node * @returns {NodeList} children */ function getChildNodes(node) { if (that.getChildNodesOverrideFunc) { return that.getChildNodesOverrideFunc(node); } else { return node.childNodes; } } /** * Get the translated value in destLang given the node and xpath * * @param {NodeContainer} nodeContainer - The NodeContainer that holds the value to translate * @param {String} docPath - The xpath of the node * @param {String} destLang - The language code of the language to translate to * @returns {String} the translated value */ this.getByValue = function (nodeContainer, docPath, destLang) { var node = nodeContainer.node var data = this.getData(nodeContainer); // cacheLogic for IE if (!widget.c('Agent').canStoreObjectInNode() && /text/i.test(nodeContainer.nodeName)) { buildIndex(widget.c('Lang').getActualLang()); return getByValueFallback(data, nodeContainer, docPath, destLang); } // if translation not cached, cache it var wovnTranslation = node.wovnTranslation; if (!wovnTranslation || typeof wovnTranslation !== 'object') { buildIndex(widget.c('Lang').getActualLang()); var index = getIndex(nodeContainer, widget.c('Lang').getActualLang()); var translationData = getTranslation(nodeContainer, index, data); // we're keeping this to test non-text nodes try { node.wovnTranslation = translationData; } catch (e) { var options = (index[data] && (index[data][destLang] || index[data][defaultLang])); if (!options) { options = []; } return saveNodeData(bestMatch(options, docPath), nodeContainer);; } if (translationData == undefined) return null; that.validateCache(nodeContainer, docPath, destLang); } else { that.validateCache(nodeContainer, docPath, destLang); if (!node.wovnTranslation) return null; } // return cached translation return saveNodeData(bestMatch(node.wovnTranslation[destLang] || [], docPath), nodeContainer); }; this.translateTexts = function(fromLang, toLang, texts) { build(textIndex, fromLang) var index = textIndex[fromLang] || {}; var translatedTexts = {} for (var i = 0; i < texts.length; i++) { var text = texts[i]; if (index[text] && index[text][toLang] && index[text][toLang][0]) { translatedTexts[text] = index[text][toLang][0]['data']; } else { translatedTexts[text] = null; } } return translatedTexts; } this.getCachedOriginalSrcsetAttribute = function (node) { return (this.srcsetOriginalValueStore[node] || {})[node.value]; } this.cacheOriginalSrcsetAttribute = function (node, originalValue) { if (!this.srcsetOriginalValueStore.hasOwnProperty(node)) { this.srcsetOriginalValueStore[node] = {}; } this.srcsetOriginalValueStore[node][node.value] = originalValue; } /* * Get the source url from srcset's attribute, * Example: * srcset="hoge.jpg 1x, fuga.png 2x" => ['hoge.jpg', 'fuga.png'] */ function getSrcValuesFromSrcsetAttribute(originalAttribute) { var singleSrc = /[^\s,]+\s+[^\s,]+/g; var terminator = /\s+[^\s]+$/; var urls = originalAttribute.match(singleSrc); var urlSrcs = {}; for (var i in urls) { if (!urls.hasOwnProperty(i)) continue; var src = urls[i].replace(terminator, ''); var urlFormatter = widget.c('UrlFormatter').createFromUrl(src); urlFormatter.setShowFullUrl(); var fullUrl = urlFormatter.getOriginalUrl(); urlSrcs[fullUrl] = src; } return urlSrcs; } function getSrcsetAttributeTranslation(originalAttribute, srcValues, index, destLang, docPath) { if (!originalAttribute) return; var newAttribute = originalAttribute; for (var fullUrl in srcValues) { if (!srcValues.hasOwnProperty(fullUrl)) continue; var srcValue = srcValues[fullUrl]; if (!index[fullUrl]) { continue; } destValue = bestMatch(index[fullUrl][destLang] || [], docPath); if (destValue && destValue.data) { newAttribute = newAttribute.replace(srcValue, destValue.data); } } return newAttribute; } /* * @param {DOMNode}: srset attribute node of img tag. * @return {Array}: srcs that have been swapped. */ this.replaceSrcsetNode = function (node, docPath, destLang) { // if cache exist, use it. var originalAttribute = this.getCachedOriginalSrcsetAttribute(node) || node.value; buildIndex(defaultLang); var index = imgIndex[defaultLang]; var srcValues = getSrcValuesFromSrcsetAttribute(originalAttribute); var newAttributes = getSrcsetAttributeTranslation(originalAttribute, srcValues, index, destLang, docPath); if (!newAttributes) { return []; } node.value = newAttributes; this.cacheOriginalSrcsetAttribute(node, originalAttribute); return srcValues; } /** * Get the translated value in destLang given the node containing HTML text * and xpath. * * @param {Node} node The node that holds the value to translate. * @param {String} docPath The xpath of the node. * @param {String} destLang The language code of the language to translate to. * * @returns {String} the translated value. */ this.getByComplexValue = function (node, docPath, destLang) { var data = getComplexData(node, false) buildIndex(destLang); var translationData = getComplexTranslationData(node, widget.c('Lang').getDocLang(), data); //TODO bestMatch() and saveNodeData() if (translationData && translationData[destLang] && translationData[destLang].length > 0) { return translationData[destLang][0] } return null; } /** * Get the translated value in destLang givven the node and xpath with comment node * * @param {String} data * @param {NodeContainer} nodeContainer - The NodeContainer that holds the value to translate * @param {String} docPath - The xpath of the node * @param {String} destLang - The language code of the language to translate to * @returns {String} the translated value */ function getByValueFallback(data, nodeContainer, docPath, destLang) { var node = nodeContainer.node var index = getIndex(nodeContainer, widget.c('Lang').getActualLang()); var translationData = getTranslationDataFromComment(nodeContainer, index); if (!translationData && widget.c('Utils').isEmpty(data)) return; if (!translationData || validateValueNotChanged(data, translationData, nodeContainer, docPath) === false) { translationData = getTranslationData(nodeContainer, index, data); if (!translationData) return; saveTranslationDataAsComment(node, translationData); } return saveNodeData(bestMatch(translationData[destLang] || [], docPath), nodeContainer); } /** * Check translation is not changed * @param originalData {Hash} * @param translationData {Hash} * @param nodeContainer {NodeContainer} * @param docPath {String} * @returns {boolean} */ function validateValueNotChanged(originalData, translationData, nodeContainer, docPath) { var lang = widget.c('Lang').getActualLang(); var val = translationData[lang] && saveNodeData(bestMatch(translationData[lang], docPath), nodeContainer); return val && widget.c('Utils').normalizeText(val.data) === widget.c('Utils').normalizeText(originalData); } /** * Return translation data * When swap is first time and widget is inserted by backend, try to get original source from comment. * * @param nodeContainer {NodeContainer} * @param index {Object} * @param data {String} * @returns {Object} */ function getTranslation(nodeContainer, index, data) { if (widget.c('DomAuditor').isSwappedMoreThanOnce == false && widget.c('Config').backend()) { var translationData = getTranslationDataFromComment(nodeContainer, index); if (translationData) { return translationData; } } return getTranslationData(nodeContainer, index, data); } /** * Return translation data from specific node and index * @param nodeContainer {NodeContainer} * @param index {Object} * @param data {String} * @returns {Object} */ function getTranslationData(nodeContainer, index, data) { if (index.hasOwnProperty(data)) { return index[data]; } var normalizedText = widget.c('Utils').normalizeText(data); var translationData = that.getTranslationDataFromIndex(normalizedText, nodeContainer, index); if (translationData) { return translationData; } var defaultIndex = getIndex(nodeContainer, defaultLang); translationData = that.getTranslationDataFromIndex(normalizedText, nodeContainer, defaultIndex); return translationData ? translationData : undefined; } /** * Get data from index * @param normalizedText {String} * @param nodeContainer {NodeContainer} * @param index {Object} * @returns {Object} */ this.getTranslationDataFromIndex = function(normalizedText, nodeContainer, index) { if (index.hasOwnProperty(normalizedText)) { return index[normalizedText]; } if (isImageNode(nodeContainer)) { return getImageFromIndex(normalizedText, index); } return null } /** * Gets the translation data for a given node containing HTML. * * @param {Node} node The node for which to get the translation data. * @param {String} lang The language of the translation. * @param {String} data The current data of the node. * * @returns {String} The translation for the given node and its data. */ function getComplexTranslationData(node, lang, data) { var index = getHTMLIndex(lang); if (index.hasOwnProperty(data)) { return index[data]; } var normalizedText = widget.c('Utils').normalizeText(data); if (index.hasOwnProperty(normalizedText)) { return index[normalizedText]; } var defaultIndex = getHTMLIndex(defaultLang); var translationData = defaultIndex[normalizedText]; return translationData ? translationData : undefined; } /** * Save the src of the data to the comment node * * @param {Node} node - a node * @param {Object} data - the translation data of the node */ function saveTranslationDataAsComment(node, data) { var currentComment = getTranslationDataCommentNode(node); var comment = wovnCommentPrefix + data[defaultLang][0]["data"]; if(currentComment) { currentComment.data = comment; } else { var srcComment = document.createComment(comment); node = getParentOwningNode(node); if (node.parentNode) { node.parentNode.insertBefore(srcComment, node); } } } function getTranslationDataCommentNode(node) { var previousSibling = getParentOwningNode(node).previousSibling; if(previousSibling && previousSibling.nodeName === '#comment') { var comment = previousSibling.data; if (comment.indexOf(wovnCommentPrefix) === 0) { return previousSibling; } } return null; } /** * Get translation data from comment * sometimes data is saved as comment instead of wovnTranslation * @param nodeContainer {NodeContainer} * @param index {Object} * @returns {Object} */ function getTranslationDataFromComment(nodeContainer, index) { var node = nodeContainer.node var commentNode = getTranslationDataCommentNode(node); if (commentNode) { var defaultLangData = commentNode.data.substring(wovnCommentPrefix.length); if (defaultLangData.length) return getTranslationData(nodeContainer, index, defaultLangData); } return null; } function getParentOwningNode(node) { var parentNode = node.parentElement || node.parentNode; if (parentNode) { // TITLE's wovn-src is added before tag from backend, because HTML doesn't allow // Otherwise add inside <title> // @see https://www.w3.org/TR/html401/struct/global.html#h-7.4.2 if (widget.c('DomAuditor').isSwappedMoreThanOnce && parentNode.nodeName === 'TITLE') { return parentNode; } return node; } else { return node.ownerElement; } } /** * Return default language's value * @param nodeContainer {NodeContainer} * @param docPath {String} * @returns {String} */ this.getDefaultValue = function (nodeContainer, docPath) { return this.getByValue(nodeContainer, docPath, defaultLang) || {data: that.getData(nodeContainer, true)}; }; /** * Gets the original data for a given node containing HTML. * * @param {Node} node The node for which to get the translation data. * @param {String} docPath The xpath of the node. * * @returns {String} The original data of the given node. */ this.getDefaultComplexValue = function (node, docPath) { return this.getByComplexValue(node, docPath, defaultLang) || {data: getComplexData(node, false)} } /** * cache translation data by adding "wovnTranslation" property * * @param nodeContainer {NodeContainer} to cache * @param docPath {String} * @param destLang {String} */ this.validateCache = function (nodeContainer, docPath, destLang) { var node = nodeContainer.node var wovnTranslation = node.wovnTranslation; if (!wovnTranslation || typeof(wovnTranslation) !== 'object') return; var data = this.getData(nodeContainer); var lang = widget.c('Lang').getActualLang(); var langValue = wovnTranslation[lang]; var val = langValue && saveNodeData(bestMatch(langValue, docPath), nodeContainer); var normalizedText = widget.c('Utils').normalizeText(data); if (val && widget.c('Utils').normalizeText(val.data) === normalizedText) return; buildIndex(lang); var langIndex = getIndex(nodeContainer, lang); var translation = this.getTranslationDataFromIndex(normalizedText, nodeContainer, langIndex); if (!translation) { var defaultLangIndex = getIndex(nodeContainer, defaultLang); translation = this.getTranslationDataFromIndex(normalizedText, nodeContainer, defaultLangIndex); } node.wovnTranslation = translation }; /** * Return Index for specified languages * @param nodeContainer {NodeContainer} * @param langs {String| Array{String}} * @returns {Object} */ function getIndex (nodeContainer, langs) { var index; if (!langs) langs = [defaultLang]; if (typeof langs === 'string') langs = [langs]; if (nodeContainer.nodeName === 'IMG') { while (!index && langs.length > 0) { index = imgIndex[langs.shift()]; } } else if (nodeContainer.nodeName === 'INPUT') { while (!index && langs.length > 0) { index = imgIndex[langs.shift()]; } } else if (nodeContainer.nodeName === 'META') { while (!index && langs.length > 0) { index = textIndex[langs.shift()]; } } // text nodes, attr nodes else { while (!index && langs.length > 0) { index = textIndex[langs.shift()]; } } return index; } function getHTMLIndex (lang) { if (!htmlTextIndex[lang]) buildIndex(lang); return htmlTextIndex[lang]; } /** * Gets the data from inside a node * @param {NodeContainer} nodeContainer * @param {Boolean} useDefault * @returns {String} data - The data of the node * @example * // returns "hello" * this.getData(<input type="text" value="hello">) */ this.getData = function (nodeContainer, useDefault) { var data; var node = nodeContainer.node; if (isValueNode(nodeContainer.getUpperNodeName() || node.name.toUpperCase())) { if (useDefault) { data = node.value; } else { data = widget.c('Utils').normalizeText(node.value); } } else if (isImageNode(nodeContainer)) { data = node.src; } else { if (widget.c('Node').isLegitimateNode(nodeContainer)) { data = nodeContainer.data; } else { if(widget.c('Node').isFirstTextNode(node)) { data = widget.c('Node').wholeText(node); } else { return ''; } } if (useDefault !== true) { data = widget.c('Utils').normalizeText(data); } } return data; }; /** * Gets the data from inside a node containing HTML text. * * @param {DomElement} node The node from which to extract the data. * @param {Boolean} useInsideWovnIgnore True if the data includes inside wovn-ignore * * @returns {String} The data of the node. * * @example * // returns "some<a>link</a>." * this.getComplexData(<p>some <a href="#">link</a>.</p>) */ function getComplexData (node, useInsideWovnIgnore) { var data = that._nodeToSrc(node, useInsideWovnIgnore, false); return data } /** * Gets the original data from inside a node containing HTML text. * * the data includes values inside wovn-ignore * @param {Node} node The node from which to extract the data. * * @returns {String} The original data of the node. * * @example * // returns "some<a wovn-ignore>link</a>." * this.getComplexData(<p>some <a wovn-ignore>link</a>.</p>) */ this.getOriginalComplexData = function(node) { return that._nodeToSrc(node, true, false); } /** * Check Node's name is relates node to get node.value * @param upperName {String} * @returns {boolean} true if value needed node */ function isValueNode(upperName) { if (upperName === 'ALT') return true; if (upperName === 'VALUE') return true; if (upperName === 'PLACEHOLDER') return true; if (upperName === 'CONTENT') return true; if (upperName === 'LABEL') return true; return false } /** * Check Node's name is relates node to image * @param nodeContainer {NodeContainer} * @returns {boolean} true if image node */ function isImageNode(nodeContainer) { if (nodeContainer.nodeName === 'IMG') { return true; } if (nodeContainer.nodeName === 'INPUT' && nodeContainer.node.src) { return true } } /** * Replaces the data of a node * @param {NodeContainer} nodeContainer * @param {String} data - the data to replace in the node */ this.replaceData = function (nodeContainer, data) { if (!data) return; var node = nodeContainer.node var nodeName = nodeContainer.nodeName; switch (true) { case /(alt|value|placeholder|content|label)/i.test(nodeName || node.name): replaceAttrib(node, data); break; case /#text/i.test(nodeName): replaceText(nodeContainer, data); break; case /img/i.test(nodeName) || !!(/input/i.test(nodeName) && node.src): replaceImg(node, data); break; } }; /** * Replaces the attribute of a node * @param {NodeContainer} nodeContainer The nodeContainer to be replaced. * @param {String} attribute */ this.replaceAttribute = function (nodeContainer, attribute, data) { var attributeNode = nodeContainer.node.getAttributeNode(attribute); replaceAttrib(attributeNode, data); }; var complexDataCache = {}; /** * Replaces the data of a node that contains HTML text. * Entry point for recursive function * * @param {NodeContainer} nodeContainer The nodeContainer to be replaced. * @param {String} data The data to put in the node. */ this.replaceComplexData = function(nodeContainer, data, index) { replaceComplexDataRecurse(nodeContainer.node, data, index) // To shrink time to implement, replace nodeContainer's data here. // If necessary, don't hesitate to pass nodeContainer to replaceComplexDataRecurse. nodeContainer.refreshData() } /** * Replaces the data of a node that contains HTML text. * * @param {Node} node The node to be replaced. * @param {String} data The data to put in the node. */ function replaceComplexDataRecurse(node, data, index) { if(node.nodeType !== 1) { var nodeContainer = widget.c('NodeContainer').create(node) replaceText(nodeContainer, data) } else { if (isIgnoreNode(node)) return; var nodeChildren = node.childNodes; if(nodeChildren.length > 0) { var nodeCache = index || complexDataCache[data]; if (!nodeCache) { var swappingNode = document.createElement(node.tagName); swappingNode.innerHTML = data; fixEmptyTextNodes(node, swappingNode); nodeCache = createComplexDataCache(swappingNode); complexDataCache[data] = nodeCache; } var realIndex = 0; for(var i = 0; i < nodeChildren.length; ++i) { var swapData = nodeCache[realIndex]; replaceComplexDataRecurse(nodeChildren[i], swapData, nodeCache[realIndex]); realIndex++; } } } } /** * Referencing node, adds empty text nodes to swappingNodes where they are missing * @param node {Object} The reference node * @param swappingNode {Object} The node to be swapped with (may contain missing text nodes) */ function fixEmptyTextNodes(node, swappingNode) { for (var i = 0; i < node.childNodes.length; i++) { if (node.childNodes[i].childNodes.length > 0) { fixEmptyTextNodes(node.childNodes[i], swappingNode.childNodes[i]); } else if (!isTextNode(swappingNode)) { if (swappingNode.childNodes.length <= i && isTextNode(node.childNodes[i])) { var emptyNode = document.createTextNode(''); swappingNode.insertBefore(emptyNode, null); } if (swappingNode.childNodes[i] !== undefined && node.childNodes[i].nodeName !== swappingNode.childNodes[i].nodeName && isTextNode(node.childNodes[i])) { var emptyNode = document.createTextNode(''); swappingNode.insertBefore(emptyNode, swappingNode.childNodes[i]); } } } } /** * Checks if the current node is a text node * @param node {Object} * @returns {boolean} */ function isTextNode(node) { return node.nodeType === 3; } /** * Creates the cache of a translation node structure for replaceComplexData(). * * @param {Node} node The translation node to cache. * * @returns {Array} The cached structure of the node. */ function createComplexDataCache(node) { var cache = []; if(node.nodeType !== 1) { cache.push(node.data); } else { var nodeChildren = node.childNodes; for(var i = 0; i < nodeChildren.length; ++i) { var child = nodeChildren[i]; var subCache = createComplexDataCache(nodeChildren[i]) if(child.nodeType !== 1) { cache = cache.concat(subCache); } else { cache.push(subCache); } } } return cache; } /** * Replaces the text of a text node * @param {NodeContainer} nodeContainer * @param {String} text - the text to replace in the node */ function replaceText (nodeContainer, text) { if (text === undefined) return; var originalData = nodeContainer.data; var data = originalData.replace(/^(\s*)[\s\S]*?(\s*)$/, '$1' + text.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\$/g, '$$$$') + '$2'); if (originalData !== data) nodeContainer.replaceData(data) } this.replaceText = replaceText; /* * Replaces the src attribute of an image * @param {DomElement} node * @param {String} data - the url of the new src attribute */ function replaceImg (node, data) { // TODO handle case where image has not yet loaded // get this info from translate page?? //if (!node.getAttribute('width') && node.width > 0 && node.height > 0) { // node.setAttribute('width', node.width); // node.setAttribute('height', node.height); //} if(widget.tag.getAttribute('urlPattern') == 'path') { var locationDomainPort = location.hostname; if(location.port) { locationDomainPort = locationDomainPort + ":" + location.port; } if (widget.c('Url').getDomainPort(data).toLowerCase() == locationDomainPort) { data = widget.c('Url').getUrl(widget.c('Lang').defaultCode(), data); } } replaceAttrib(node.getAttributeNode('src'), data); } /** * Revert Css Image * @param {Element} node */ this.revertImage = function(node) { if (node.style['backgroundImage'] === 'none') return; var originalImage = getOriginalBackgroundImage(node); if (!originalImage && originalImage !== '') return; node.style['backgroundImage'] = originalImage; }; /** * Get original background image * @param node {Element} * @returns {string} */ function getOriginalBackgroundImage(node) { return node.getAttribute(originalBackgroundAttributeName); } /** * Set original background image * @param node {Element} * @returns {string} */ function setOriginalBackgroundImage(node) { node.setAttribute(originalBackgroundAttributeName, node.style['backgroundImage'] || ''); } /** * Replace Css Image * @param {Element} node * @param {string} dstImage */ this.replaceCssImageData = function(node, dstImage) { var originalImage = getOriginalBackgroundImage(node); if (!originalImage && originalImage !== '') { setOriginalBackgroundImage(node); } if (dstImage) { node.style['backgroundImage'] = 'url(' + dstImage + ')'; } else { node.style['backgroundImage'] = ''; } }; /* * Replaces the value of an attribute node * @param {DomElement} node - is an attribute node (like alt="") * @param {String} data - the new value of the attribute node */ function replaceAttrib (node, data) { if (node && node.value !== data) node.value = data; } this.getTranslatedLangs = function () { return translatedLangs; }; this.empty = function () { for (var val in textIndex[defaultLang]){if(textIndex[defaultLang].hasOwnProperty(val)) { return false; }} for (var val in htmlTextIndex[defaultLang]){if(htmlTextIndex[defaultLang].hasOwnProperty(val)) { return false; }} for (var val in imgIndex[defaultLang]){if(imgIndex[defaultLang].hasOwnProperty(val)) { return false; }} return true; }; }; if (typeof(components) === 'undefined') var components = {}; components['DomAuditor'] = function (widget) { // TODO: we might want to initialize this so that we can add custom values NonRecursiveTextContainers = ['div', 'p', 'pre', 'blockquote', 'figcaption', 'address', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'dt', 'dd', 'th', 'td']; InlineTextContainers = ['span', 'a', 'em', 'strong', 'small', 'tt', 's', 'cite', 'q', 'dfn', 'abbr', 'time', 'code', 'var', 'samp', 'sub', 'sup', 'i', 'b', 'kdd', 'mark', 'u', 'rb', 'rt', 'rtc', 'rp', 'bdi', 'bdo', 'wbr', 'nobr']; AllowedWithinTextContainers = ['br', 'img', 'ruby', 'ul', 'ol']; var that = this; var srcs; var newSrcs; var reportedCssImages; var newCssImages; var hasNewMissedSrc; var auditCount; var reportTime = widget.c('Url').hasFlag('instantReport') ? 1000 : 5000; var reportTimer; var reportTimerResetCount; var reportCount; var forceReporting = false; var currentLang = undefined; this.isSwappedMoreThanOnce = false; widget.c('AuditTrigger').auditor = audit; /** * Get current language * while swapping value, previous language is returned * @returns {String} */ this.getInternalCurrentLang = function() { return currentLang } this.setReportTime = function(time) { reportTime = time; } function reset() { srcs = {}; newSrcs = {}; reportedCssImages = {}; newCssImages = {}; hasNewMissedSrc = false; auditCount = 0; clearTimeout(reportTimer); reportTimer = undefined; reportTimerResetCount = 0; reportCount = 0; } reset(); /** * Check value needs to report * @param src {String} * @returns {boolean} */ function isAddableSrc(src) { // FILTER OUT NON-TRANSLATEABLE VALUES // 翻訳できないvalueを除く // prevent sources with bad encoding to break the report try { encodeURIComponent(widget.c('Utils').toJSON(src)); } catch(err) { return false; } var rxEmail = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i; var rxOnlySpecial = /^[`~!@#\$%\^&\*\(\)\-_=\+\[\{\]\}\|;:'",\/?]+$/; var rxEncodedURIComponent = /^(%([a-f]|[0-9]){2})+$/i; var trimmedSrc = widget.c('Utils').trimString(src); var isNaNOrAllowedNumber = isNaN(trimmedSrc) || widget.c('Data').numberSwappingAllowed(); return trimmedSrc !== "" && isNaNOrAllowedNumber && !rxEmail.test(trimmedSrc) && !rxOnlySpecial.test(trimmedSrc) && !rxEncodedURIComponent.test(trimmedSrc); } this.isAddableSrc = isAddableSrc; function createDataObject(src, xpath, complex) { return {src: src, xpath: xpath, complex: complex, exists: true}; } function addSrc(src, xpath, complex) { if (widget.c('Lang').getActualLang() === widget.c('Lang').defaultCode()) { var srcObject = createDataObject(src, xpath, complex); if (!isUnifiedValueChild(srcObject)) { if (!srcs.hasOwnProperty(src)) { if (!newSrcs.hasOwnProperty(src) || /meta/.test(newSrcs[src].xpath)) { newSrcs[src] = srcObject; } } if (!srcs.hasOwnProperty(src) || /meta/.test(srcs[src].xpath)) { srcs[src] = srcObject; } } } } function isUnifiedValueChild (value) { if (!value.complex && widget.c('Data').useFragmentedValue() && value.xpath.match(/text\(\)$/)) { for (key in srcs) { if (srcs.hasOwnProperty(key)) { var existingValue = srcs[key]; if (existingValue.complex && widget.c('Utils').stringStartsWith(value.xpath, existingValue.xpath)) { return true; } } } } return false; } /** * Swaps through nodes on the page and translates content * @param {String} newLangCode * @param {Object} options - for test purposes can determine the start point of the swapping * @param {boolean} swapsProperty - true if swapProperty (default is true) */ this.swapVals = function (newLangCode, options, swapsProperty) { if (!options) options = {}; var target = ['#text', 'img', 'meta', 'a', 'area', 'form', 'input', 'textarea', 'option']; var attributes = null; if (widget.c('Data').useAriaLabel()) { attributes = attributes ? attributes : {}; attributes['aria-label'] = function(node, xpath) { var nodeContainer = widget.c('NodeContainer').create(node) nodeContainer.data = node.getAttribute('aria-label'); var newVal = widget.c('ValueStore').getByValue(nodeContainer, xpath, newLangCode); if (newVal) { widget.c('ValueStore').replaceAttribute(nodeContainer, 'aria-label', newVal.data); } }; } var propertyIndexTags = widget.c('ValueStore').propertyIndexTags(); for(var i = 0; i < propertyIndexTags.length; i++) { target.push(propertyIndexTags[i]); } // for fragmented values feature, dom iterator must call block on more text // container targets if (widget.c('Data').useFragmentedValue()) { target = target.concat(NonRecursiveTextContainers); target = target.concat(InlineTextContainers); } widget.c('PerformanceMonitor').mark('swap_start'); // default arguments if (swapsProperty !== false) { swapsProperty = true } var everyBlockFn = swapsProperty ? everyBlock: function () {} widget.c('DomIterator').go({ target: target, attributes: attributes, filter: this.createFilterCallback(newLangCode), head: options.head }, block, everyBlockFn); currentLang = newLangCode; this.isSwappedMoreThanOnce = true; widget.c('PerformanceMonitor').mark('swap_end'); if (widget.c('Data').dynamicLoading()) { widget.c('ValueStore').loadNewDetectedValue(); } function isSrcsetNode(_, xpath) { return xpath && xpath.match(/\[@srcset]$/) } /** * Callback function when visit target Element * * @param {Node} node * @param {String} xpath * * @return {Boolean} True to prevent from visiting children of node, false * otherwise. */ function block (node, xpath) { //Because IE is too slow to resolve DOM's attribute, need to reduce access as possible if (isSrcsetNode(node, xpath)) { swapSrcset(node, xpath); } var isTranslatedComplexNode = false; // handle forms (for backend) var tagName = node.tagName; if (tagName === 'FORM') { if (widget.c('Config').urlPattern('query') && (!node.getAttribute('method') || node.getAttribute('method').toUpperCase() === 'GET')) { var children = node.children; // start from the end because the hidden field is inserted at the end for (var i = children.length - 1; i >= 0; i--) { if (children[i].tagName === 'INPUT' && children[i].getAttribute('name') === 'wovn' && children[i].getAttribute('type') === 'hidden') { children[i].setAttribute('value', newLangCode); return false; } } // input doesn't exist, so create it var hiddenField = document.createElement('input'); hiddenField.setAttribute('type', 'hidden'); hiddenField.setAttribute('name', 'wovn'); hiddenField.setAttribute('value', newLangCode); node.appendChild(hiddenField); } else if (widget.c('Config').backend()) { var oldAction = !node.getAttribute('action') || node.getAttribute('action').length === 0 ? location.href : node.getAttribute('action'); if (!widget.c('Url').shouldIgnoreLink(oldAction)) { var newAction = widget.c('Url').getUrl(newLangCode, oldAction); node.setAttribute('action', newAction); } } return false; } // INSERT META NAME/PROPERTY VALUE HERE // meta elements without a name/property value are filtered out if (tagName === 'META') { xpath += node.getAttribute('name') ? "[@name='" + node.getAttribute('name') + "']" : "[@property='" + node.getAttribute('property') + "']"; var contentNode = node.getAttributeNode('content') if (contentNode && contentNode.value !== '') { block(contentNode, xpath); } return false; } if (tagName === 'OPTION') { if (node.hasAttribute('label')) { xpath += "[@label]" var contentNode = node.getAttributeNode('label') if (contentNode && contentNode.value !== '') { block(contentNode, xpath); } } return false; } if (tagName === 'IMG' && node.hasAttribute('alt') && node.getAttribute('alt') !== '') block(node.getAttributeNode('alt'), xpath + '[@alt]'); if (tagName === 'INPUT') { if (node.hasAttribute('value') && node.getAttribute('value') !== '' && node.hasAttribute('type') && node.getAttribute('type') !== "text" && node.getAttribute('type') !== "search" && node.getAttribute('type') !== "password" && node.getAttribute('type') !== "number") { block(node.getAttributeNode('value'), xpath + '[@value]'); } if (node.hasAttribute('alt') && node.getAttribute('alt') !== '') { block(node.getAttributeNode('alt'), xpath + '[@alt]'); } } if (tagName === 'INPUT' || tagName === 'TEXTAREA') { if (node.hasAttribute('placeholder') && node.getAttribute('placeholder') !== '') { block(node.getAttributeNode('placeholder'), xpath + '[@placeholder]'); } } if (tagName === 'IMG') { if (node.hasAttribute('srcset') && node.getAttribute('srcset') !== '') { block(node.getAttributeNode('srcset'), xpath + '[@srcset]'); } } var nodeContainer = widget.c('NodeContainer').create(node) if (isHrefTag(tagName)) { var langUrl = widget.c('Url').langUrl(newLangCode, node); if (langUrl && !widget.c('Url').isLiveEditor()) { node.setAttribute('href', langUrl); } } // unified value detection if (widget.c('Data').useFragmentedValue() && isTextValue(node, xpath) && !isValidTextNode(node, xpath)) { var defaultVal = widget.c('ValueStore').getDefaultComplexValue(node, xpath); if (defaultVal && defaultVal.data) { var originalSrc = widget.c('ValueStore').getOriginalComplexData(node) var newVal = widget.c('ValueStore').getByComplexValue(node, xpath, newLangCode); if (newVal) { isTranslatedComplexNode = true; widget.c('ValueStore').replaceComplexData(nodeContainer, newVal.data); } addSrc(originalSrc, xpath, true); } } else if (isReplaceValueTarget(nodeContainer, tagName)) { if (isSVGNonSwappableTextNode(xpath)) { return false; } if (tagName === 'INPUT' && nodeContainer.node.src) { xpath += '[@image]'; } var src = widget.c('ValueStore').getDefaultValue(nodeContainer, xpath); if (!src) return false; var srcData = src.data; if (!srcData) return false; if (!isAddableSrc(srcData)) return false; var newVal = widget.c('ValueStore').getByValue(nodeContainer, xpath, newLangCode); if (!newVal) { newVal = src; // Ignore image values to reduce access if (tagName !== 'IMG') { widget.c('ValueStore').noteMissedValue(srcData); } if (!srcs.hasOwnProperty(srcData)) hasNewMissedSrc = true; } widget.c('Node').disableIllegitimateNode(nodeContainer); widget.c('ValueStore').replaceData(nodeContainer, newVal.data); if (widget.c('Node').isLegitimateNode(nodeContainer)) { addSrc(src.data, xpath, false); } } if (tagName === 'INPUT' || tagName === 'TEXTAREA') return false; var forceRevert = newLangCode !== currentLang; widget.c('ValueStore').applyPropertySetting(node, newLangCode, forceRevert); return isTranslatedComplexNode; } function swapSrcset(node, xpath) { var originalSrcs = widget.c('ValueStore').replaceSrcsetNode(node, xpath, newLangCode) var newSrcset = node.value; for (var fullUrl in originalSrcs) { if (!originalSrcs.hasOwnProperty(fullUrl)) continue; var src = originalSrcs[fullUrl]; var dataObject = createDataObject(fullUrl, xpath, true); if (!srcs.hasOwnProperty(dataObject) && newSrcset.indexOf(src) !== -1) { hasNewMissingSrcs = true; } addSrc(fullUrl, xpath, false); } } function isSVGNonSwappableTextNode(xpath) { return /\/svg\/.*text\(\)/.test(xpath) && !/\/text\/text\(\)$/.test(xpath); } /** * Check there is possibility of having href attributes * @see http://www.w3schools.com/tags/att_href.asp * @param tagName {String} * @returns {boolean} */ function isHrefTag(tagName) { return tagName === 'A' || tagName === 'AREA' } /** * True if node can replace text or image * @param nodeContainer {NodeContainer} * @param tagName {String} * @returns {boolean} */ function isReplaceValueTarget(nodeContainer, tagName) { if (nodeContainer.nodeName === '#text' && widget.c('Utils').trimString(nodeContainer.node.textContent) !== '') { return true; } if (nodeContainer.isValueNode()) { return true; } if (tagName === 'IMG' || tagName === 'META' || tagName === 'FORM' || tagName === 'OPTION') { return true; } if (tagName === 'INPUT' && nodeContainer.node.src) { return true; } return false; } /** * Callback Function called when visit all node * @param {Node} node * @param {String} xpath */ function everyBlock(node, xpath) { if (widget.c('Utils').canStyleChange(node)) { var defaultCssImage = applyCssImage(node); if (defaultCssImage) { addSrc(defaultCssImage, xpath + '[@background-image]', false); } } } /** * Determines if a given HTML node represent a TextValue. * @param node [Object] The node to check. * @param path [String] The xpath of the current node. * @return [Boolean] True if the node represent a TextValue, false otherwise. */ function isTextValue(node, path) { if (isValidTextNode(node, path)) return true; if (isParentOfSingleTextContainer(node, path)) return false; if (isEmptyTextContainer(node, path)) return false; if (!isValidTextContainer(node)) return false; return true; } /** * Determines if a given HTML node is a text node that can be used as TextValue. Empty text nodes are rejected. * @param node [Object] The node to check. * @param path [String] The xpath of the current node. * @return [Boolean] True if the node is a valid text node, false otherwise. */ function isValidTextNode(node, path) { return node.nodeName.toLowerCase() === '#text' && node.nodeValue.replace(/^[`~!@#\$%\^&\*\(\)\-_=\+\[\{\]\}\|;:'",\/\\?]+$/, '') !== '' } /** * Determines if a given HTML node contains only one text container. * In that case we want to continue the scraping further down. * @param node [Object] The node to check. * @return [Boolean] True if the node contains a single text container, false otherwise. */ function isParentOfSingleTextContainer(node, path) { var nonEmptyChildrenRes = nonEmptyChildren(node); if (nonEmptyChildrenRes.length === 0) return true; if (nonEmptyChildrenRes.length === 1 && isValidTextNode(nonEmptyChildrenRes[0], path)) return true; if (nonEmptyChildrenRes.length === 1 && isValidTextContainer(nonEmptyChildrenRes[0])) return true; return false; } function isEmptyTextContainer(node, path) { return widget.c('Utils').normalizeText(node.innerText || '', true) === ''; } /** * Retrieves the non-empty children of a given HTML node. * @param node [Object] The node to check. * @ return [Array] The non-empty children of the given node. */ function nonEmptyChildren(node) { var children = []; if (node.childNodes) { var values = new Array(); for(var i = 0; i < node.childNodes.length; i++){ values.push(node.childNodes[i]); } children = values.filter(function (n) { var result = true; if (n.nodeName.toLowerCase() === '#text' && widget.c('Utils').normalizeText(n.nodeValue)) { result = n; } return result; }); } return children; } /** * Determines if a given HTML node contains only text. * @param node [Object] The node to check. * @returns {boolean} True if the node is a valid text container, false otherwise. */ function isValidTextContainer(node) { if (isNonRecursiveTextContainer(node) || isInlineTextContainer(node)) { if (node.childNodes.length > 0) { for (var i = 0; i < node.childNodes.length; ++i) { if (!isAcceptedWithinTextContainer(node.childNodes[i])) return false; } } return true } return false } /** * Tells if a given HTML node belongs to the category of nodes that can * represent text value, but cannot contain another node of this category. * @param node [Object] The node to check. * @returns {boolean} True if the node is a non-recursive container, false otherwise. */ function isNonRecursiveTextContainer(node) { return widget.c('Utils').indexOf(NonRecursiveTextContainers, node.nodeName.toLowerCase()) > -1; } /** * Tells if a given HTML node belongs to the category of nodes that can represent text value. * @param node [Object] The node to check. * @returns {boolean} True if the node is an inline container, false otherwise. */ function isInlineTextContainer(node) { return widget.c('Utils').indexOf(InlineTextContainers, node.nodeName.toLowerCase()) > -1; } /** * Tells if a given HTML node is allow to be within a TextValue. * @param node [Object] The node to check. * @returns {boolean} True if the node can be within a TextValue, false otherwise. */ function isAcceptedWithinTextContainer(node) { if (!isNonRecursiveTextContainer(node) && (widget.c('Utils').indexOf(AllowedWithinTextContainers, node.nodeName.toLowerCase()) != -1 || isInlineTextContainer(node))) { if (node.childNodes.length > 0) { for (var i = 0; i < node.childNodes.length; ++i) { if (!isAcceptedWithinTextContainer(node.childNodes[i])) return false; } } return true; } return node.nodeName.toLowerCase() === '#text' } /** * Swap images defined by css * @param {Node} node * @returns {String} defalut image's path */ function applyCssImage(node) { var defaultImage = null; if (newLangCode !== currentLang) { widget.c('ValueStore').revertImage(node); } var imagePath = getUrlFromCssImageNode(node); if (newLangCode !== currentLang) { if (node.style['backgroundImage'] === '') { defaultImage = imagePath; } } var dstImage = widget.c('ValueStore').getDstImage(imagePath, newLangCode); if (imagePath && dstImage && imagePath !== dstImage) { widget.c('ValueStore').revertImage(node); defaultImage = getUrlFromCssImageNode(node); widget.c('ValueStore').replaceCssImageData(node, dstImage); } defaultImage = defaultImage || node.getAttribute('data-wovn-style-image'); return defaultImage; } function getUrlFromCssImageNode(node) { var backgroundImage = window.getComputedStyle(node).getPropertyValue('background-image'); return widget.c('Parser').getUrlFromCss(backgroundImage); } }; /** * Filters out the nodes that should not be visited by swapVals * @param {string} newLangCode - the new lang code * @returns {Function} * @example * // returns true * this.createFilterCallback with node: <a wovn-ignore> * // returns false * this.createFilterCallback with node: <a href="next.html"> */ this.createFilterCallback = function (newLangCode) { return function (node, xpath) { if (typeof(node.getAttribute) === 'function' && node.getAttribute('wovn-ignore') !== null) return true; var nodeName = node.nodeName; if (isScriptOrStyle(nodeName)) return true; if (filterAds(node, nodeName)) return true; if (filterMetaTags(node, nodeName)) return true; if (filterOptionLabel(node, nodeName)) return true; if (filterInputTags(node, nodeName)) return true; if (filterExtensionContents(node, xpath)) { return true; } return false; /** * Return true if nodeName is script or style related * @param nodeName {String} * @returns {boolean} */ function isScriptOrStyle(nodeName) { if (nodeName === "SCRIPT") return true if (nodeName === "NOSCRIPT") return true if (nodeName === "STYLE") return true return false } }; } /** * Filters out Adwords * @param node {Node} * @param nodeName {String} equal to node.nodeName, exits to reduce the number of access * @returns {boolean} */ function filterAds(node, nodeName) { return nodeName === 'IMG' && /googlesyndication\.com/i.test(node.src); } /** * Filter for Input tags - will filter anything but button or submit types with value or placeholder attributes * @param {Node} node * @param {String} nodeName * @returns {Boolean} * @example * // returns false * filterInputTags('<input type="text" value="Hello">', 'html/body/div/input') */ function filterInputTags (node, nodeName) { if (nodeName !== 'INPUT') return false; var nodeType = node.getAttribute('type'); // keep input button, submit with value if (/^(button|submit)$/i.test(nodeType) && node.getAttribute('value')) return false; // keep text, search, password, number with placeholder if (/^(email|text|search|password|number)$/i.test(nodeType) && node.getAttribute('placeholder')) return false; if (/^image$/i.test(nodeType) && node.src) return false; return true; } /** * Filter for meta tag * @param node {Element} * @param nodeName {String} * @returns {boolean} */ function filterMetaTags (node, nodeName) { if (nodeName !== 'META') return false; // RegExp.test on null is always false return !(/^(description)$/.test(node.getAttribute('name')) || /^(og:description|twitter:description|og:title|twitter:title)$/.test(node.getAttribute('property'))); } /** * Filter for option tag * @param node {Element} * @param nodeName {String} * @returns {boolean} */ function filterOptionLabel (node, nodeName) { if (nodeName !== 'OPTION') return false; return !node.hasAttribute('label') && node.innerText.length <= 0; } var bodyElement = document.body; /** * Filter for google's translation information * @param node {Node} * @param xpath {String} * @returns {boolean} */ function filterExtensionContents(node, xpath) { return node.id === "goog-gt-tt" // chrome auto translation information || filterAfterNode(node, bodyElement); // extension content added after the body } /** * Filter nodes existing after arguments's node * @param currentNode {Node} * @param beforeNode {Node} * @returns {boolean} */ function filterAfterNode(currentNode, beforeNode) { var nextNode = beforeNode.nextSibling; while(nextNode) { if(nextNode === currentNode) return true; nextNode = nextNode.nextSibling; } return false; } function postSrcs(path, srcs, onEachPost, onComplete) { if (srcs.length === 0) { if (onComplete) onComplete(); return; } var r = new XMLHttpRequest(); var host = widget.c('Url').getApiHost(); r.open("POST", host + path, true); r.onreadystatechange = function () { if (r.readyState != 4 || r.status != 200) return; if (onEachPost) onEachPost(r.responseText); if (onComplete) onComplete(); }; r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); var postBody = ''; var srcJson = widget.c('Utils').toJSON(srcs, null, 4); postBody += 'url=' + widget.c('Url').getEncodedLocation() + '&no_record_vals=' + encodeURIComponent(srcJson); if (reportCount === 1 && widget.c('ValueStore').corruptedVals.length > 0) postBody += '&corruptedVals=' + encodeURIComponent(JSON.stringify(widget.c('ValueStore').corruptedVals, null, 4)); r.send(postBody); } function mergeNewSrcs() { for (var src in newSrcs) { if (newSrcs.hasOwnProperty(src)) { if (!srcs.hasOwnProperty(src) || /meta/.test(srcs[src].xpath)) srcs[src] = newSrcs[src]; } } newSrcs = {}; hasNewMissedSrc = false; } function reportVals() { if (location.hash.match(/wovn.haltReporting/) || location.hash.match(/wovn.editing/) || haltReporting) return; ++reportCount; mergeNewSrcs(); var restSrcs = []; for (var src in srcs) if (srcs.hasOwnProperty(src)) restSrcs.push(srcs[src]); postSrcs("report_values/" + widget.tag.getAttribute('key'), restSrcs); } function isReportable() { return !(widget.c('Config').backend() && widget.c('Lang').getDocLang() !== widget.c('Lang').defaultCode()); } function resetReportTimer() { if (!isReportable()) return; ++reportTimerResetCount; clearTimeout(reportTimer); mergeNewSrcs(); reportTimer = setTimeout(function () { reportTimerResetCount = 0; reportVals(); }, reportTime); } this.reportCount = function () { return reportCount; }; this.resetReportCount = function () { reportCount = 0; clearTimeout(reportTimer); }; function hasNewSrc() { for (var src in newSrcs) if (newSrcs.hasOwnProperty(src)) return true; return false; } /** * Swap and report * @param callback {Function} * @param swapsProperty {boolean} */ function audit(callback, swapsProperty) { if (location.hash.match(/wovn.debugAudit/)) console.log("AUDIT"); auditCount++; var lang = widget.c('Lang').getDocLang() || widget.c('Lang').defaultCode(); widget.c('DomAuditor').swapVals(lang, null, swapsProperty || true); if(containsThirdPartyContents()) { // remove newly found srcs as they could contain third party contents removeNewSrcs(); } if (forceReporting || (widget.c('Data').dynamicValues() && reportCount < 10 && reportTimerResetCount < 10 && (hasNewSrc() && hasNewMissedSrc))) { resetReportTimer(); forceReporting = false; } if (callback) setTimeout(callback, 0); } this.audit = audit; function containsThirdPartyContents() { if (isChromeTranslating()) return true; if (isLanguageTranslated()) return true; if (isGoogleAnalyticsExtensionWorking()) return true; return false; } function isChromeTranslating() { //return document.documentElement.classList.contains("translated-ltr") // || document.documentElement.classList.contains("translated-rtl"); return document.documentElement.className.match("translated"); } /** * Checks if the language in the wovn widget were translated by external service * @param langSwitches {Array of DomElement} * @returns {boolean} true if translated, false if not */ function isLanguageTranslated(langSwitches) { var widgetElement = widget.c('Interface').getWidgetElement(); var dataTheme = widgetElement && widgetElement.getAttribute('data-theme'); // do not check for customize widget if (dataTheme && dataTheme === 'built-in') return false; var langSwitches = langSwitches || widgetElement && widgetElement.querySelectorAll('.wovn-switch'); if (langSwitches && langSwitches.length > 0) { for (var i=0; i<langSwitches.length; i++) { langAttribute = langSwitches[i].getAttribute('data-value'); langObject = langAttribute && widget.c('Lang').get(langAttribute); langName = langObject && langObject.name; if (langName && langName !== langSwitches[i].innerHTML) { return true; } } } return false; } this.isLanguageTranslated = isLanguageTranslated; function isGoogleAnalyticsExtensionWorking () { var googleAnalyticsExtensionElements = document.getElementsByClassName('view-in-ga-link-logo') for (var i = 0; i < googleAnalyticsExtensionElements.length; i++) { var element = googleAnalyticsExtensionElements[i]; if (/chrome-extension:\/\/.+analytics_logo\.png/.test(getComputedStyle(element)['background-image'])) { return true; } } return false; } function removeNewSrcs() { for(var src in newSrcs) { if(srcs.hasOwnProperty(src)) { delete srcs[src] } } newSrcs = {}; hasNewMissedSrc = false; } this.stop = function () { reset(); widget.c('AuditTrigger').stop(); }; this.destroy = function () { that.stop(); }; var haltReporting = widget.c('Agent').mutatesTextNodeData(); forceReporting = (widget.c('Lang').missingAutoTranslateLangs() || widget.c('Lang').missingAutoPublishLangs() || (widget.c('ValueStore').corruptedVals.length > 0 && Math.random() < 0.1) || location.hash.match(/wovn.forceReporting/)); }; if (typeof(components) === 'undefined') var components = {}; components['AuditTrigger'] = function(widget) { var that = this; var timeout; var editMode = false; this.auditor = function () {}; this.auditWorker = widget.c('SingleWorker').createSingleWorker(); widget.c('Utils').onDomReady(function () { var touchEnabled = 'ontouchstart' in document; var clickNodes = [document.body]; clickNodes = clickNodes.concat(widget.c('Utils').toArrayFromDomList(document.getElementsByTagName('a'))); clickNodes = clickNodes.concat(widget.c('Utils').toArrayFromDomList(document.getElementsByTagName('button'))); clickNodes = clickNodes.concat(widget.c('Utils').toArrayFromDomList(document.getElementsByTagName('input'))); for (var i = 0; i < clickNodes.length; i++) { // add touch listener instead of click if the device is touch enabled if (touchEnabled){ processEvent(clickNodes, i, 'touchend'); } else { processEvent(clickNodes, i, 'click'); } } }, true); /** * At each event trigger, renews the timer of the audit or decorates the page * @param {object} clickNodes html nodes on the page being tracked for events * @param {number} i the node index * @param {string} eventName the event name */ function processEvent(clickNodes, i, eventName) { widget.c('Utils').onEvent(clickNodes[i], eventName, function () { if (!editMode) { renewTimeout(); } else { widget.c('EditorInterface').decoratePage(); } }); } this.start = function() { renewTimeout(); }; this.editStop = function() { editMode = true; clearTimeout(timeout); }; this.stop = function() { clearTimeout(timeout); }; this.destroy = function() { that.stop(); }; /** * reset Audit's count and execute audit * @param maxInterval */ function renewTimeout(maxInterval) { if (!maxInterval) maxInterval = 25000; var totalAuditCount = 5; var currentAuditCount = 0; var callAudit = function() { // When current language is same as default, almost values are properly swapped (includes new values). // so reduce opportunity of swapVals() if (widget.c('DomAuditor').getInternalCurrentLang() === widget.c('Lang').defaultCode()) { if (currentAuditCount % 2 === 0) { return } } // First SwapVals() is slower than later, (maybe because of JIT or DOM's cache?) // For faster rendering (e.g. scroll), ignore everyBlock's swap (everyBlock is for swap css' image) var swapsProperty = currentAuditCount !== 0; that.auditor(null, swapsProperty); }; var bookNext = function() { if (currentAuditCount >= totalAuditCount) return; currentAuditCount++; var interval = maxInterval * Math.pow(currentAuditCount, 2) / Math.pow(totalAuditCount, 2); that.auditWorker.setTimeout(callAudit, bookNext, interval); } that.auditWorker.setTimeout(callAudit, bookNext, 0); } }; if (typeof(components) === 'undefined') var components = {}; components['Interface'] = function(widget) { var that = this; //var translating = false; this.WIDGET_ID = 'wovn-translate-widget'; this.BUILT_IN_ID = 'wovn-languages'; var appendedChildren = []; var attachedHandlers = []; var widgetElement; this.addClass = function (ele, targetClass) { var trimmedClass = widget.c('Utils').trimString(targetClass); var rx = new RegExp('(^| )' + trimmedClass + '( |$)'); // if class list already contains className if (rx.test(ele.className)) return; ele.className = ele.className.length == 0 ? targetClass : ele.className + ' ' + targetClass; } this.removeClass = function (ele, targetClass) { var trimmedClass = widget.c('Utils').trimString(targetClass); var rx = new RegExp('(^| )' + trimmedClass + '( |$)', 'g'); var className = ele.className.replace(rx, '').replace(/\s+/g, ' '); ele.className = widget.c('Utils').trimString(className); } function wovnGetElementsByClassName(node, classname) { if (typeof document.getElementsByClassName === 'function') return node.getElementsByClassName(classname); else { var a = []; var re = new RegExp('(^| )' + classname + '( |$)'); var els = node.getElementsByTagName("*"); for (var i = 0, j = els.length; i < j; i++) if (re.test(els[i].className) || re.test(els[i].getAttribute('class'))) a.push(els[i]); return a; } } function setInnerHTMLByClass(ancestor, className, value) { var targets = wovnGetElementsByClassName(ancestor, className); for (var i = 0; i < targets.length; i++) targets[i].innerHTML = value; } function onEvent (target, eventName, handler) { widget.c('Utils').onEvent(target, eventName, handler); attachedHandlers.push({'target': target, 'eventName': eventName, 'handler': handler}); } this.insertStyles = function (styles) { if (!styles) return; if (styles.constructor === Array) styles = styles.join('\n'); var styleElement = document.createElement('style'); styleElement.type = 'text/css'; styleElement.className = 'wovn-style'; try { styleElement.innerHTML = styles; } catch (e) { styleElement.styleSheet.cssText = styles; } document.getElementsByTagName('head')[0].appendChild(styleElement); appendedChildren.push(styleElement); } function disableBrowserTranslation () { if (widget.c('Utils').getMetaElement('google', {value: 'notranslate'})) return; var chrome = document.createElement('meta'); chrome.setAttribute('name', 'google'); chrome.setAttribute('value', 'notranslate'); document.getElementsByTagName('head')[0].appendChild(chrome); appendedChildren.push(chrome); } function getQueryVal(param, href) { var link = !href ? window.location : function () { var l = document.createElement('a'); l.setAttribute('href', href); return l; }(); param = encodeURIComponent(param); var match = link.search.match(new RegExp(param + '=([^&#?]*)')); return (match && match[1]) || ''; } var lastViewportWidth = null; var lastViewportHeight = null; /** * Rescale the widget element * @param selector widget element node */ function rescaleWidget (selector) { selector = selector || document.getElementById('wovn-translate-widget'); if (!selector) return; var ruler = document.createElement('div'); ruler.setAttribute('style', 'position:fixed;display:block;visibility:hidden;right:0;left:0;padding:0;font-size:16px;border:0;'); document.body.appendChild(ruler); var viewportWidth = ruler.offsetWidth; ruler.setAttribute('style', 'position:fixed;display:block;visibility:hidden;width:1px;top:0;bottom:0;'); var viewportHeight = ruler.offsetHeight; document.body.removeChild(ruler); // if the last variables are initialized and // both of them haven't changed, return if (lastViewportWidth && lastViewportHeight && !(lastViewportWidth !== viewportWidth && lastViewportHeight !== viewportHeight)) return; // formula derived from the following constraints // widget should cover 2.5% of the screen area in portrait // widget should cover 5% of the screen area in landscape // percent covered is actually fontSize*viewportWidth (not actually entire widget) // using this fs*width/width*height => fs/height = (.05 and .025 respectively) // so we need fs = height*x where x is based on the dimensions of the viewport // using Sony XPeria Z1's dimensions, a linear function // was derived which yields .025 in portrait and .05 in landscape // the resulting function was plugged in for x giving us // height*(function with width and height inputs) // this was reduced to the following form below var fontSize = viewportWidth*.0205714 + viewportHeight*.0134286; selector.style.fontSize = fontSize + 'px'; lastViewportWidth = viewportWidth; lastViewportHeight = viewportHeight; } var scrollTop = 0; var widgetTimeout = 0; function showWidget () { if (document.body.scrollTop < scrollTop) { window.clearTimeout(widgetTimeout); widgetTimeout = setTimeout(animHideWidget, 3000); rescaleWidget(); animShowWidget(); } scrollTop = document.body.scrollTop; } /** * set animation to hide the widget */ function animHideWidget() { var widget = document.getElementById('wovn-translate-widget'); if (!widget) return; // if dropdown is open, wait 3 more seconds before trying to hide if (widget.getElementsByTagName('ul')[0].style.display === 'block') widgetTimeout = setTimeout(animHideWidget, 3000); else widget.className = widget.className.replace(/slide-in/, 'slide-out slid-out'); } /** * set animation to show the widget */ function animShowWidget() { var widget = document.getElementById('wovn-translate-widget'); if (!widget) return; rescaleWidget(); widget.className = widget.className.replace(/slid-out/, '').replace(/slide-out/, 'slide-in'); } /** * Build Widget's Language List * * @param langs to use */ function buildWidgetLangList (langs) { var widgetElem = document.getElementById(that.WIDGET_ID); if (!widgetElem) return; var widgetList = widgetElem.className.match(/\bwovn-lang-list\b/) ? widgetElem : wovnGetElementsByClassName(widgetElem, 'wovn-lang-list')[0]; if (!widgetList) return; langs = langs || []; // c('Url').getLangCode will return the path lang if using backend or wovnDefaultLang otherwise var selectedLang = widget.c('Url').getLangCode(); if (selectedLang != widget.c('Lang').getDocLang()) { selectedLang = widget.c('Lang').getDocLang(); } if (widget.c('Utils').findIndex(langs, selectedLang, function (ele, val) { return ele.code === val;}) === -1) { selectedLang = widget.c('Lang').defaultCode(); } var listItem, selectedLangName; for (var i = 0; i < langs.length; i++) { var lang = langs[i]; listItem = document.createElement('li'); listItem.setAttribute('class', 'wovn-switch'); listItem.innerHTML = lang.name; listItem.setAttribute('data-value', lang.code); if (lang.code == selectedLang) { listItem.setAttribute('class', 'wovn-switch selected'); selectedLangName = lang.name; } widgetList.appendChild(listItem); } setInnerHTMLByClass(widgetElem, 'wovn-current-lang', selectedLangName || ''); } function getWovnLangQuery(lang_code) { var query = window.location.search; // change wovn parameter if (query.replace(/\?/, '').length == 0) { return 'wovn=' + lang_code; } else if (query.match(/wovn=/)) { return query.replace(/wovn=[^&]*/, 'wovn=' + lang_code); } else { return query + '&wovn=' + lang_code; } } /** * Flag of changeLang() is working or not * @type {boolean} */ var changingLang = false; /** * Change Language * If argument is string, convert to appropriate element. * If 'ed' is argued, LiveEditor starts. * * @param ele element or language's code */ this.changeLang = function (ele) { if (changingLang) { setTimeout(function(){that.changeLang(ele);}, 100); return; } changingLang = true; var oldLang = widget.c('Lang').defaultCode(); var isEd = false; if (typeof(ele) === 'string') { isEd = (ele === 'ed'); var lis = document.getElementById(that.WIDGET_ID); if (lis) { lis = wovnGetElementsByClassName(lis, 'wovn-switch'); var langCode = ele; ele = lis[0]; for (var i = 0; i < lis.length; i++) { if (lis[i].getAttribute('data-value') === langCode) ele = lis[i]; } } } if (isEd) { var loadedOne = false; function kickoffEditor () { if (loadedOne) widget.c('EditorInterface').start(); else loadedOne = true; } widget.loadComponents(['Vue', 'EditorInterface'], {'Vue': kickoffEditor, 'EditorInterface': kickoffEditor}); } // if we got the element (it's not a string) if (ele.parentElement) { var listItems = wovnGetElementsByClassName(ele.parentElement, 'wovn-switch'); for (var i = 0; i < listItems.length; i ++) { if (listItems[i].className.indexOf('selected') != -1) oldLang = listItems[i].getAttribute('data-value'); that.removeClass(listItems[i], 'selected'); } that.addClass(ele, 'selected'); var currentLangEle = ele.parentElement.parentElement.parentElement; setInnerHTMLByClass(currentLangEle, 'wovn-current-lang', ele.textContent || ele.innerText); newLang = ele.getAttribute('data-value'); } else { oldLang = widget.c('Lang').getDocLang(); newLang = ele; } widget.c('Lang').setDocLang(newLang); if (widget.c('ParcelForwarding').banner) { widget.c('ParcelForwarding').banner.changeLang(); } changingLang = false; }; function attachLangClickHandlers () { var widgetElem = document.getElementById(that.WIDGET_ID); if (!widgetElem) return; var clickTargets = wovnGetElementsByClassName(widgetElem, 'wovn-switch'); if (clickTargets.length === 0) clickTargets = widgetElem.getElementsByTagName('a'); if (clickTargets.length === 0) clickTargets = widgetElem.getElementsByTagName('li'); if (clickTargets.length === 0) return; for (var i = 0; i < clickTargets.length; i++) { onEvent(clickTargets[i], 'click', function(ele) { return function () {that.changeLang(ele);}; }(clickTargets[i]), false); } } var widgetOptionShori = (function () { var shoris = {}; shoris.type = function (opts, opt) { var type = opts[opt]; if (type === 'widget' || (type === 'auto' && !document.getElementById(that.BUILT_IN_ID)) || (type !== 'built_in' && type !== 'auto' && type !== 'widget')) { buildWidgetLangList(widget.c('Lang').getConvertedLangs()); attachLangClickHandlers(); return; } if (type === 'built_in' && !document.getElementById(that.BUILT_IN_ID)) { that.insertStyles('#wovn-translate-widget {display: none !important;}'); return; } var dataAttribute = ''; if (document.getElementById(that.WIDGET_ID)) { var oldWidget = document.getElementById(that.WIDGET_ID); dataAttribute = oldWidget.getAttribute('data-ready'); oldWidget.parentNode.removeChild(oldWidget); } that.WIDGET_ID = that.BUILT_IN_ID; var container = document.getElementById(that.WIDGET_ID); container.setAttribute('data-ready', dataAttribute); container.setAttribute('data-theme', 'built-in'); // if there is a template if (wovnGetElementsByClassName(container, 'wovn-switch-template').length !== 0) { var original = wovnGetElementsByClassName(container, 'wovn-switch-template')[0]; var hasSwitch = original.className.match(/(^| )wovn-switch( |$)/i) || function () { for (var i = 0; i < original.children.length; i++) { if (original.children[i].className.match(/(^| )wovn-switch( |$)/i)) return true; } return false; }(); // if there's no switch class we will put it on the template element if (!hasSwitch) that.addClass(original, 'wovn-switch'); var template = document.createElement('div'); template.appendChild(original.cloneNode(true)); var newSwitch; var convertedLangs = widget.c('Lang').getConvertedLangs(); for (var i = 0; i < convertedLangs.length; i++) { newSwitch = document.createElement('div'); newSwitch.innerHTML = template.innerHTML.replace(/wovn-lang-name/g, convertedLangs[i].name); wovnGetElementsByClassName(newSwitch, 'wovn-switch')[0].setAttribute('data-value', convertedLangs[i].code); original.parentNode.insertBefore(newSwitch.children[0], original); } original.parentNode.removeChild(original); } // if there are no switches in the container, we may have to build them else if (wovnGetElementsByClassName(container, 'wovn-switch').length === 0) { // if there are no anchors (and no switches), we have to build the inner structure if (container.getElementsByTagName('a').length === 0) { container.innerHTML = ''; if (container.nodeName.toLowerCase() === 'ul' || container.nodeName.toLowerCase() === 'ol') { var list = container; that.addClass(list, 'wovn-lang-list'); } else { var list = document.createElement('ul'); list.className = 'wovn-lang-list'; container.appendChild(list); } buildWidgetLangList(widget.c('Lang').getConvertedLangs()); } // if there are no switches, but there are anchor tags, make the anchor tags switches else { var switches = container.getElementsByTagName('a'); for (var i = 0; i < switches.length; i++) switches[i].className = switches[i].className + (switches[i].className.length > 0 ? switches[i].className + ' ' : '') + 'wovn-switch'; } } attachLangClickHandlers(); }; shoris.position = function (opts, opt) { if (!opts[opt] || opts[opt] === 'default') return; var widgetElem = document.getElementById(that.WIDGET_ID); if (widgetElem) that.addClass(widgetElem, 'position-' + opts[opt].replace(/[ _]/g, '-')); }; /** * Hide widget by setting and browser language. * * @param opts {Object} widget.c('Data').getOptions() * @param opt {String} always returns 'auto_hide_widget' */ shoris.auto_hide_widget = function (opts, opt) { if (!opts[opt] || (typeof opts[opt] === 'string' && !opts[opt].match(/true/i))) return; var browserLang = widget.c('Lang').getBrowserLang(); var rx = new RegExp('^' + widget.c('Data').getLang(), 'i'); if (widget.c('Data').getLang() === browserLang || rx.test(browserLang)) { var widgetElem = document.getElementById(that.WIDGET_ID); if (widgetElem) widgetElem.parentNode.removeChild(widgetElem); var builtIn = document.getElementById(that.BUILT_IN_ID); if (builtIn) builtIn.parentNode.removeChild(builtIn); } }; /** * Hide WOVN.io logo. * @param opts {Object} widget.c('Data').getOptions() * @param opt {String} always returns 'hide_logo' */ shoris.hide_logo = function (opts, opt) { if (!opts[opt]) return; var widgetElem = document.getElementById(that.WIDGET_ID); if (widgetElem) { that.addClass(widgetElem, 'hide-logo'); } }; /** * Show translated by machine image * @param opts {Object} widget.c('Data').getOptions() */ shoris.show_tbm = function (opts) { if (opts["show_tbm"] !== true) return; var widgetElem = document.getElementById(that.WIDGET_ID); if (widgetElem) { that.addClass(widgetElem, 'show-tbm'); } }; return function (options, opt) { if (typeof shoris[opt] === 'object') { if (arguments.length === 3 && typeof arguments[2] === 'string' && typeof shoris[opt][arguments[2]] === 'function') return shoris[opt][arguments[2]](options, opt); } else if (typeof shoris[opt] === 'function') return shoris[opt](options, opt); }; })(); function applyWidgetOptions(options) { if (options) var opts = options; else if (widget.c('Data').getOptions()) var opts = widget.c('Data').getOptions(); else return; var widgetOptionStyles = (widget.c('Data').get().widgetOptions && widget.c('Data').get().widgetOptions.css) || {}; var styles = []; for (var opt in opts){if(opts.hasOwnProperty(opt)) { if (widgetOptionStyles.hasOwnProperty(opt)) { if (typeof opts[opt] === 'boolean') { if (opts[opt]) styles.push(widgetOptionStyles[opt]); } else { styles.push(widgetOptionStyles[opt]); } } var shoriResult = widgetOptionShori(opts, opt); // if shori result is an array if (typeof shoriResult === 'object' && shoriResult.constructor === Array) styles = styles.concat(shoriResult); }} that.insertStyles(styles); // user must have parcel_forwarding feature and viewer must be outside Japan if (!!opts.parcel_forwarding && widget.c('Data').getCountryCode() !== 'JP') { var loadedOne = false; function kickoffParcelForwarding () { if (loadedOne) widget.c('ParcelForwarding').start(); else loadedOne = true; } widget.loadComponents(['Vue', 'ParcelForwarding'], {'Vue': kickoffParcelForwarding, 'ParcelForwarding': kickoffParcelForwarding}); } // 404 unpublish feature if (opts.not_found_unpublish && widget.c('Data').getPublishedLangs().length > 0) { widget.c('PageChecker').notifyWovnIfNotFound(); } } this.build = function() { if (!document || !document.body) setTimeout(function () {that.body(options)}, 100); var oldWidget = document.getElementById('wovn-translate-widget'); if (oldWidget) oldWidget.parentNode.removeChild(oldWidget); while (true) { var oldStyles = document.getElementsByClassName('wovn-style') if (oldStyles.length == 0) { break; } // oldStyles' elements is removed when node is removed oldStyles[0].parentNode.removeChild(oldStyles[0]) } var styles = "#wovn-translate-widget[wovn] {\n display: none;\n position: fixed;\n z-index: 9999999999;\n top: auto;\n right: 0;\n bottom: 25px;\n left: auto;\n margin: 0;\n padding: 0 0 0 9px;\n font-size: 12px;\n color: #6c7984;\n background: #f6f8fa;\n border: solid 1px #dee5ec;\n border-right: none;\n border-radius: 3px 0 0 3px;\n -webkit-box-shadow: 0 2px 0 0 rgba(0, 0, 0, .03);\n -moz-box-shadow: 0 2px 0 0 rgba(0, 0, 0, .03);\n box-shadow: 0 2px 0 0 rgba(0, 0, 0, .03)\n}\n#wovn-translate-widget[wovn].mobile {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n padding: 0;\n /*width:282px;padding-left:38px;*/\n\n font-size: 16px;\n border-right: none;\n border-left: none;\n border-bottom: none;\n border-radius: 0;\n -webkit-box-shadow: none;\n -moz-box-shadow: none;\n box-shadow: none\n}\n#wovn-translate-widget[wovn].wovn-element-hide {\n display: none;\n}\n#wovn-translate-widget[wovn] #wovn-collapse-btn {\n position: absolute;\n width: 9px;\n top: 0;\n right: auto;\n bottom: 0;\n left: 0;\n margin: 0;\n padding: 0;\n cursor: pointer;\n}\n#wovn-translate-widget[wovn].mobile #wovn-collapse-btn {\n right: auto;\n left: 0;\n width: 12%\n}\n#wovn-translate-widget[wovn] #wovn-collapse-btn:hover {\n background-position: right 22px\n}\n#wovn-translate-widget[wovn] #wovn-content {\n position: relative;\n display: block;\n top: auto;\n right: auto;\n bottom: auto;\n left: auto;\n margin: 0;\n padding: .65em 10px .65em 0\n}\n#wovn-translate-widget[wovn].mobile #wovn-content {\n margin: 0 0 0 3%;\n padding: .45em 0 .45em 0;\n text-align: left\n}\n#wovn-translate-widget[wovn] h6 {\n box-sizing: initial;\n position: relative;\n margin: 0 85px 0 0;\n display: block;\n padding: 0 16px 0 6px;\n width: 102px;\n font-weight: normal;\n font-family: Verdana, sans-serif;\n font-size: 1em;\n border: 1px solid #dee5ec;\n border-radius: 3px;\n line-height: 1.8em !important;\n font-weight: 400;\n color: #6c7984;\n overflow: visible;\n background: white;\n cursor: pointer;\n outline: none;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n min-width: 70px\n}\n#wovn-translate-widget[wovn] h6:before,\n#wovn-translate-widget[wovn] h6:after {\n content: none;\n}\n#wovn-translate-widget[wovn] h6 > span.wovn-current-lang {\n font-size: 12px !important;\n position: static;\n margin: 0;\n display: inline;\n padding: 0;\n text-transform: none;\n font-family: Verdana, sans-serif;\n font-size: 1em;\n font-style: normal;\n border: none;\n line-height: 1.8em !important;\n font-weight: 400;\n color: #545f66;\n opacity: 0.7;\n white-space: nowrap;\n background: transparent;\n outline: none;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none\n}\n#wovn-translate-widget[wovn].mobile h6 {\n box-sizing: initial;\n display: inline-block;\n min-width: 7.5em;\n margin: 0 103px 0 0;\n padding: 0 3em 0 .5em;\n}\n#wovn-translate-widget[wovn] #wovn-content:hover h6 {\n box-sizing: initial;\n border: 1px solid #bec8d2;\n}\n#wovn-translate-widget[wovn] #wovn-content:active h6 {\n box-sizing: initial;\n background-color: #EEF1F3;\n}\n#wovn-translate-widget[wovn] h6:after {\n content: \' \';\n position: absolute;\n top: .7em;\n right: .85em;\n width: 0;\n height: 0;\n border-top: .35em solid #6c7984;\n border-right: .25em solid transparent;\n border-left: .25em solid transparent;\n}\n#wovn-translate-widget[wovn] #wovn-content > a {\n position: absolute;\n display: block;\n top: 5px;\n right: 7px;\n bottom: 0;\n left: auto;\n width: 77px;\n margin: 0;\n padding: 0\n}\n#wovn-translate-widget[wovn].mobile #wovn-content > a {\n width: 40%;\n top: 0;\n right: 0;\n left: auto;\n margin: .5em;\n background: url(https://s3-us-west-1.amazonaws.com/st.wovn.io/images_store/wovn_widget_logo.svg) no-repeat right;\n -webkit-background-size: auto 100%;\n background-size: auto 75%;\n text-align: center\n}\n#wovn-translate-widget[wovn] #wovn-content > a #wovn-logo {\n position: relative;\n display: inline-block;\n width: 77px;\n height: 18px;\n top: 5px;\n background: url(https://s3-us-west-1.amazonaws.com/st.wovn.io/images_store/wovn_widget_logo.png) no-repeat\n}\n#wovn-translate-widget[wovn] #wovn-content > div#translated-by-machine {\n box-sizing: initial;\n position: absolute;\n display: none;\n width: 100px;\n height: 10px;\n bottom: 8%;\n background: url(https://s3-us-west-1.amazonaws.com/st.wovn.io/images_store/wovn_widget_text.png) no-repeat\n}\n\n/* tbm, yes logo, no mobile*/\n#wovn-translate-widget[wovn].show-tbm #wovn-content {\n padding-bottom: 1.65em;\n}\n#wovn-translate-widget[wovn].show-tbm.position-top-left #wovn-content > div#translated-by-machine {\n box-sizing: initial;\n right: 0px\n}\n#wovn-translate-widget[wovn].show-tbm.position-bottom-left #wovn-content > div#translated-by-machine {\n box-sizing: initial;\n right: 0px\n}\n\n/*tbm, yes logo, yes mobile*/\n#wovn-translate-widget[wovn].show-tbm.mobile #wovn-content {\n padding-bottom: .45em;\n}\n#wovn-translate-widget[wovn].show-tbm.mobile #wovn-content > div#translated-by-machine {\n box-sizing: initial;\n right: 0.3em;\n width: 4.7em;\n height: auto;\n top: 0;\n bottom: 0;\n background-position: 100% 86%;\n background-size: 100%;\n}\n#wovn-translate-widget[wovn].show-tbm.mobile #wovn-content > a {\n background-position: right top;\n background-size: auto 60%;\n margin: 0.3em;\n}\n\n/*tbm, no logo, yes mobile*/\n#wovn-translate-widget[wovn].show-tbm.mobile.hide-logo #wovn-content {\n}\n#wovn-translate-widget[wovn].show-tbm.mobile.hide-logo #wovn-content > div#translated-by-machine {\n box-sizing: initial;\n right: 0.3em;\n width: 4.7em;\n height: auto;\n top: 0;\n bottom: 0;\n background-position: right;\n background-size: 100%;\n}\n#wovn-translate-widget[wovn].mobile #wovn-content > a #wovn-logo {\n display: none\n}\n#wovn-translate-widget[wovn] div.wovn-lang-list-container {\n font-size: 12px !important;\n display: none;\n position: absolute;\n bottom: 2.1em;\n left: 0;\n}\n#wovn-translate-widget[wovn].mobile div.wovn-lang-list-container {\n font-size: 12px !important;\n bottom: 2.1em;\n}\n#wovn-translate-widget[wovn] ul {\n z-index: 1;\n max-height: 290px;\n position: relative;\n left: -11px;\n right: 3px;\n width: auto;\n margin: 0;\n padding: 0;\n font-size: 1em;\n background: white;\n overflow-y: auto;\n overflow-x: hidden;\n border: 1px solid #dee5ec;\n border-radius: 3px;\n -webkit-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .04);\n -moz-box-shadow: 0 0 3px 3px rgba(0, 0, 0, .04);\n box-shadow: 0 0 3px 3px rgba(0, 0, 0, .04)\n}\n#wovn-translate-widget[wovn].mobile ul {\n top: auto !important;\n left: 0;\n}\n#wovn-translate-widget[wovn] li {\n position: relative;\n list-style-type: none;\n float: none;\n width: auto;\n padding: 10px 8px 10px 28px;\n line-height: 16px;\n text-transform: none;\n border: none;\n border-radius: 0;\n font-weight: normal;\n font-family: Verdana, sans-serif;\n font-size: 1em;\n font-style: normal;\n color: #82959f;\n white-space: nowrap;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none\n}\n#wovn-translate-widget[wovn].mobile li {\n padding: 0 1.2em;\n line-height: 2.5em;\n text-align: left\n}\n#wovn-translate-widget[wovn] li:hover {\n background: #f6f8fa;\n}\n#wovn-translate-widget[wovn] li.selected:before {\n content: \' \';\n width: 8px;\n height: 8px;\n position: absolute;\n left: 10px;\n top: 14px;\n border-radius: 4px;\n background: #46b77e\n}\n#wovn-translate-widget[wovn].mobile li.selected:before {\n left: inherit;\n top: inherit;\n}\n#wovn-translate-widget[wovn] li.selected {\n color: #545f66\n}\n#wovn-translate-widget[wovn].mobile li.selected:before {\n width: .275em;\n height: .275em;\n bottom: 1.21em;\n left: .5em\n}\n#wovn-translate-widget[wovn] h6 > img {\n position: absolute;\n display: block;\n top: 9px;\n right: 10px;\n width: 6px;\n height: 4px;\n border: none;\n border-radius: 0;\n}\n#wovn-translate-widget[wovn].mobile h6 > img {\n display: none\n}\n#wovn-translate-widget[wovn] p {\n margin: 0;\n padding: 2px 0 4px 4px;\n line-height: 9px;\n font-size: 9px;\n font-family: Verdana, sans-serif;\n color: #c5cfd9\n}\n#wovn-translate-widget[wovn] p span {\n font-size: 9px;\n font-family: Verdana, sans-serif;\n color: #6c7984\n}\n#wovn-translate-widget[wovn] {\n -webkit-animation: desktop-slide-in .5s;\n animation: desktop-slide-in .5s\n}\n#wovn-translate-widget[wovn].mobile.slide-in {\n -webkit-animation: slidein .2s;\n animation: slidein .2s\n}\n#wovn-translate-widget[wovn].slid-out {\n bottom: -3em;\n opacity: 0\n}\n#wovn-translate-widget[wovn].mobile.slide-out {\n -webkit-animation: slideout .5s;\n animation: slideout .5s\n}\n@-webkit-keyframes desktop-slide-in {\n 0% {\n right: -100%\n }\n 100% {\n right: 0\n }\n}\n@keyframes desktop-slide-in {\n 0% {\n right: -100%\n }\n 100% {\n right: 0\n }\n}\n@-webkit-keyframes desktop-slide-in-left {\n 0% {\n left: -100%\n }\n 100% {\n left: 0\n }\n}\n@keyframes desktop-slide-in-left {\n 0% {\n left: -100%\n }\n 100% {\n left: 0\n }\n}\n#wovn-translate-widget.mobile.slide-out {\n -webkit-animation: slideout .5s;\n animation: slideout .5s\n}\n@-webkit-keyframes slidein {\n 0% {\n bottom: -3em;\n opacity: 0;\n }\n 50% {\n opacity: 1\n }\n 100% {\n bottom: 0\n }\n}\n@keyframes slidein {\n 0% {\n bottom: -3em;\n opacity: 0;\n }\n 50% {\n opacity: 1\n }\n 100% {\n bottom: 0\n }\n}\n@-webkit-keyframes slideout {\n 0% {\n bottom: 0;\n opacity: 1;\n }\n 100% {\n bottom: -3em;\n opacity: 0\n }\n}\n@keyframes slideout {\n 0% {\n bottom: 0\n }\n 50% {\n opacity: 1\n }\n 100% {\n bottom: -3em;\n opacity: 0\n }\n}\n" that.insertStyles(styles); widgetElement = document.createElement('div'); widgetElement.id = 'wovn-translate-widget' widgetElement.setAttribute('wovn', ''); widgetElement.innerHTML = '<div id="wovn-collapse-btn"></div><div id="wovn-content"><div id="background"></div><h6><div class="wovn-lang-list-container"><ul class="wovn-lang-list"></ul></div><span class="wovn-current-lang">Loading...</span></h6><a href="http://wovn.io" target="_blank"><div id="wovn-logo"></div></a><div id="translated-by-machine"></div></div></div>'; document.body.appendChild(widgetElement); appendedChildren.push(widgetElement); var clickCatcher = document.createElement('div'); clickCatcher.setAttribute('style', 'z-index:9999999999;position:fixed;display:none;top:0;right:0;bottom:0;left:0;background:transparent;'); onEvent(clickCatcher, 'click', closeDropDown); widgetElement.parentNode.insertBefore(clickCatcher, widgetElement); var dropDownButton = widgetElement.getElementsByTagName('h6')[0]; onEvent(dropDownButton, 'click', openDropDown); if (widget.c('Agent').isMobile()) { widgetElement.className += ' mobile slide-in'; rescaleWidget(widgetElement); onEvent(window, 'scroll', showWidget); if (window.navigator.msPointerEnabled) { // Pointer events are supported. onEvent(window, 'pointerup', function (e) { if (e.buttons === 0) rescaleWidget(widgetElement); }); } else { onEvent(window, 'touchend', function (e) { if (e.touches.length === 0) rescaleWidget(widgetElement); }); } if (!widget.c('Utils').pageIsWidgetPreview()) { widgetTimeout = setTimeout(animHideWidget, 5000); } } function openDropDown () { var e = arguments[0] || window.event; if (e.stopPropagation) e.stopPropagation(); else e.returnValue = false; if (dropDownButton.firstChild.style.display == 'block') { dropDownButton.firstChild.style.display = 'none'; clickCatcher.style.display = 'none'; } else { dropDownButton.firstChild.style.display = 'block'; clickCatcher.style.display = 'block'; } } function closeDropDown () { var e = arguments[0] || window.event; if (e.stopPropagation) e.stopPropagation(); else e.returnValue = false; dropDownButton.firstChild.style.display = 'none'; clickCatcher.style.display = 'none'; } widgetElement.setAttribute('data-ready', widget.tag.getAttribute('data-wovnio') + '&ready=true'); //if (translating) widgetElement.style.display = 'none'; applyWidgetOptions(); //var widget = document.getElementById(that.WIDGET_ID); that.refresh(widgetElement); }; this.getWidgetElement = function () { return document.getElementById(that.WIDGET_ID); }; var clearWidgetLangList = function() { var widgetElement = that.getWidgetElement(); if (!widgetElement) return; var listItems = widget.c('Utils').toArrayFromDomList(widgetElement.getElementsByTagName('li')); for (var i = 0; i < listItems.length; ++i) listItems[i].parentNode.removeChild(listItems[i]); }; this.refresh = function () { var widgetElement = that.getWidgetElement(); if (!widgetElement) return; // TODO: reset the lang list and/or remove unused/duplicate languages if (wovnGetElementsByClassName(widgetElement, 'wovn-switch').length === 0) { buildWidgetLangList(widget.c('Lang').getConvertedLangs()); attachLangClickHandlers(); } if (wovnGetElementsByClassName(widgetElement, 'wovn-switch').length > 1 && !widget.c('ValueStore').empty()) { // TODO: THIS SEEMS LIKE THE WRONG WAY TO DO THIS? if (!/wovn\.editing/.test(location.hash)) widgetElement.style.display = 'block'; disableBrowserTranslation(); } else widgetElement.style.display = 'none'; }; /** * start wovn's main function * @param {Function} callback called when succeed */ this.start = function(callback) { // if the browser is a web crawler, do nothing if (widget.c('Agent').isCrawler()) return; // shims widget.c('Utils'); // load data loadData(init); widget.c('PerformanceMonitor').mark('data_load_script_insert'); function init () { if (!widget.c('Data').getImageValues) widget.c('Data').getImageValues = function() {return {}}; if (!widget.c('Data').getTextValues) widget.c('Data').getTextValues = function() {return {}}; widget.c('PerformanceMonitor').mark('data_load_end'); widgetOnLoadedDocument(); // waits for the page to be loaded before creating the widget function widgetOnLoadedDocument() { if(document.readyState != 'complete') { setTimeout(widgetOnLoadedDocument, 100); } else { insertHreflangLinks(); widget.c('Interface').build(); // loads API widget.c('Api'); // lang will set doc lang if (widget.c('Url').isLiveEditor()) { // Use original language for LiveEditor's initialization. widget.c('Lang').setDocLang(widget.c('Data').getLang()); } else { widget.c('Lang').setDocLang(); } widget.c('AuditTrigger').start(); widget.c('SPA').listen(); widget.c('ViewAnalytics').start(); widget.c('Api').dispatchWovnApiReadyEvent(); if (callback) callback(); widget.c('PerformanceMonitor').mark('first_translation_finish'); if (widget.c('Url').isLiveEditor()) { widget.c('Interface').changeLang('ed'); } } } } }; /** * Add the hreflang tags to the page */ function insertHreflangLinks() { var prerender_io = widget.c('Data').getOptions().prerender_io; if (prerender_io) { widget.c('Data').updateOptions({lang_path: 'query'}) } var langPath = widget.c('Data').getOptions().lang_path; if (!widget.isBackend() && (langPath === 'query' || langPath === 'path' || langPath === 'subdomain')) { var defaultCode = widget.c('Lang').defaultCode(); var availableLanguages = widget.c('Data').getPublishedLangs(); availableLanguages.push(defaultCode); var insertionLocation = document.getElementsByTagName('head').length > 0 ? document.getElementsByTagName('head')[0] : null; if (insertionLocation) { for(var i = 0; i < availableLanguages.length; i++) { if (availableLanguages[i]) { var langUrl = widget.c('Url').getUrl(availableLanguages[i], document.location.href); var link = document.createElement('link'); link.rel = 'alternate'; link.hreflang = widget.c('Lang').iso6391Normalization(availableLanguages[i]); link.href = langUrl; insertionLocation.appendChild(link); } } } } } /** * get Data-related data and callback when all information cllected * @param {Function} callback called when all information cllected */ function loadData(callback) { var remainCount = 2; var optionData = {}; that.loadData(function(data) { widget.c('Data').set(data); successCallback(); }) widget.loadDomainOption(function(option) { optionData = option; successCallback(); }, function() {}); // if error occured, callback won't executed. function successCallback() { remainCount--; if (remainCount == 0) { widget.c('Data').setOptions(optionData); callback(); } } } this.loadData = function (callback) { var isLiveEditor = widget.c('Url').isLiveEditor(); if (isLiveEditor) { widget.loadSavedData(function(data) { callback(data); }, function() { alert("Failed to load wovn's data. please reload.") }); } else { widget.loadDataJson(function(data) { callback(data); }); } } /** * Reload Data component for SPA */ this.reload = function() { var options = widget.c('Data').getOptions(); widget.c('DomAuditor').stop(); widget.c('AuditTrigger').stop(); widget.c('SPA').stop(); widget.c('ViewAnalytics').stop(); widget.reloadData(function(data) { // Set options simultaneously to avoid race condition. data['widgetOptions'] = options; widget.c('Data').set(data); widget.reinstallComponent('ValueStore'); widget.reinstallComponent('AuditTrigger'); widget.reinstallComponent('DomAuditor'); clearWidgetLangList(); widget.c('Interface').refresh(); widget.c('AuditTrigger').start(); widget.c('SPA').listen(); widget.c('ViewAnalytics').start(); }); }; this.destroy = function () { for (var i = 0; i < attachedHandlers.length; i++) { widget.c('Utils').removeHandler(attachedHandlers[i].target, attachedHandlers[i].eventName, attachedHandlers[i].handler); } for (var i = 0; i < appendedChildren.length; i++) { if (appendedChildren[i].parentNode) appendedChildren[i].parentNode.removeChild(appendedChildren[i]); } }; }; if (typeof(components) === 'undefined') var components = {}; components['Api'] = function (widget) { var that = this; // dispatched when language is changed var langChangedEvent = widget.c('Utils').createInitEvent('wovnLangChanged', true, true); // dispatched when WOVN API is ready var wovnApiReadyEvent = widget.c('Utils').createInitEvent('wovnApiReady', true, true); // Create WOVN.io object WOVN = {}; WOVN.io = {}; WOVN.io.changeLang = function (lang) { var langCode = widget.c('Lang').getCode(lang); // invalid lang if (!langCode) return false; widget.c('Interface').changeLang(langCode); return true; }; WOVN.io.getCurrentLang = function () { return getCurrentLang(); }; WOVN.io.getWovnUrl = function (url) { var lang = widget.c('Lang').getActualLang() return widget.c('Url').getUrl(lang, url) } WOVN.io.swap = function(element) { var lang = getCurrentLang() if (!lang) return var langCode = lang.code if (element) { widget.c('DomAuditor').swapVals(langCode, {head: element}, true) } else { widget.c('DomAuditor').swapVals(langCode, {}, true) } } WOVN.io.translateTexts = function(fromLang, toLang, texts) { return widget.c('ValueStore').translateTexts(fromLang, toLang, texts); } WOVN.io._private = { widget: widget } this.dispatchLangChangedEvent = function () { widget.c('Utils').dispatchEvent(langChangedEvent); }; function getCurrentLang() { return widget.c('Lang').get(widget.c('Lang').getDocLang()); } // Create Wovnio object for backwards compatibility Wovnio = WOVN.io; // Dispatch API loaded event this.dispatchWovnApiReadyEvent = function () { widget.c('Utils').dispatchEvent(wovnApiReadyEvent); // only allow this event to be called once that.dispatchWovnApiReadyEvent = function () { }; }; }; // Original Cookie code from http://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/ if (typeof(components) === 'undefined') var components = {}; components['Utils'] = function (widget) { var that = this; var DomReady = (function(){var d={};var e=navigator.userAgent.toLowerCase();var f={version:(e.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1],safari:/webkit/.test(e),opera:/opera/.test(e),msie:(/msie/.test(e))&&(!/opera/.test(e)),mozilla:(/mozilla/.test(e))&&(!/(compatible|webkit)/.test(e))};var g=false;var h=false;var j=[];function domReady(){if(!h){h=true;if(j){for(var a=0;a<j.length;a++){j[a].call(window,[])}j=[]}}};function addLoadEvent(a){var b=window.onload;if(typeof window.onload!='function'){window.onload=a}else{window.onload=function(){if(b){b()}a()}}};function bindReady(){if(g){return}g=true;if(document.addEventListener&&!f.opera){document.addEventListener("DOMContentLoaded",domReady,false)}if(f.msie&&window==top)(function(){if(h)return;try{document.documentElement.doScroll("left")}catch(error){setTimeout(arguments.callee,0);return}domReady()})();if(f.opera){document.addEventListener("DOMContentLoaded",function(){if(h)return;for(var i=0;i<document.styleSheets.length;i++)if(document.styleSheets[i].disabled){setTimeout(arguments.callee,0);return}domReady()},false)}if(f.safari){var c;(function(){if(h)return;if(document.readyState!="loaded"&&document.readyState!="complete"){setTimeout(arguments.callee,0);return}if(c===undefined){var a=document.getElementsByTagName("link");for(var i=0;i<a.length;i++){if(a[i].getAttribute('rel')=='stylesheet'){c++}}var b=document.getElementsByTagName("style");c+=b.length}if(document.styleSheets.length!=c){setTimeout(arguments.callee,0);return}domReady()})()}addLoadEvent(domReady)};d.ready=function(a,b){bindReady();if(h){a.call(window,[])}else{j.push(function(){return a.call(window,[])})}};bindReady();return d})(); var browserFingerprint; this.pageIsWidgetPreview = function () { return /fake_page\/blank/.test(window.location.pathname) && /wovn\.(io|com)/.test(window.location.hostname); } /** * Creates and initializes an event * @param {DOMString} type * @param {Boolean} bubbles * @param {Boolean} cancelable */ this.createInitEvent = function (type, bubbles, cancelable) { var event = null; if (document.createEvent) { event = document.createEvent('Event'); event.initEvent(type, bubbles, cancelable); } else if (document.createEventObject) { event = document.createEventObject(); } return event; }; /** * Add event listener * @param {String} type * @param {object} listener */ this.addEventListener = function (type, listener) { if (document.addEventListener) { document.addEventListener(type, listener); } else if (document.createEventObject) { document.attachEvent(type, listener); } }; /** * Dispatches event * @param {Event} event */ this.dispatchEvent = function (event) { if (document.dispatchEvent) { document.dispatchEvent(event); } else if (document.createEventObject) { document.documentElement[event]++; } }; this.getMetaElement = function(name, attributes) { if (!attributes) attributes = {}; var metas = document.getElementsByTagName('meta'); for (var i = 0; i < metas.length; ++i) { if (metas[i].getAttribute('name') === name) { var matched = true; for (var attributeName in attributes) { if (attributes.hasOwnProperty(attributeName) && attributes[attributeName] !== metas[i].getAttribute(attributeName)) { matched = false; break; } } if (matched) return metas[i]; } } return null; }; this.GetElementsByClassName = function (node, classname) { if (typeof document.getElementsByClassName === 'function') return node.getElementsByClassName(classname); else { var a = []; var re = new RegExp('(^| )' + classname + '( |$)'); var els = node.getElementsByTagName("*"); for (var i = 0, j = els.length; i < j; i++) if (re.test(els[i].className) || re.test(els[i].getAttribute('class'))) a.push(els[i]); return a; } }; /** * Cache result for hasComputedStyle() * @type {bool|undefined} */ var hasComputedStyleCache = undefined; /** * Return whether window has getComputedStyle() * @returns {bool} */ function hasComputedStyle() { if (hasComputedStyleCache === undefined) { hasComputedStyleCache = window.getComputedStyle ? true: false; } return hasComputedStyleCache; } /** * Whether node can be changed their style * @param {Node} node * @returns {boolean} */ this.canStyleChange = function(node) { if (!hasComputedStyle()) return false; if (!node.style) return false; var nodeName = node.nodeName; if (nodeName === 'META') return false; if (nodeName === 'IMG') return false; if (nodeName === '#text') return false; if (nodeName === '#comment') return false; return true; }; var addedEvents = []; /** * Add event handler * @param {Node} target * @param {string} eventName * @param {Function} handler */ this.onEvent = function (target, eventName, handler) { eventName = eventName.replace(/^on(.)/i, function (group0, group1) { return group1.toLowerCase(); }); if (target.addEventListener) target.addEventListener(eventName, handler, false); else if (target.attachEvent) target.attachEvent('on' + eventName, handler); addedEvents.push([target, eventName, handler]); }; /** * Remove event handler * @param {Node} target * @param {string} eventName * @param {Function} handler */ this.removeHandler = function (target, eventName, handler) { if (target.removeEventListener) target.removeEventListener(eventName, handler, false); else if (target.detachEvent) target.detachEvent('on' + eventName, handler); }; /** * Remove All EventHandler which registered by this instance. */ function removeAllHandler() { for(var i = 0; i < addedEvents.length; i++) { var event = addedEvents[i]; that.removeHandler(event[0], event[1], event[2]); } } this.onDomReady = function (func, applyIfNoLongerLoading) { if (typeof(func) === 'function') { if (applyIfNoLongerLoading && document.readyState !== 'loading') { setTimeout(func, 0); } else { DomReady.ready(func); } } }; /** * Send ajax request * Expected returned as json * @param {String} method 'GET' or anything * @param {String} url for endpoint * @param {Function} callback for success * @param {Function} callback for failure */ this.sendRequestAsJson = function (method, url, callback, errorCallback) { var requestCallback = createJsonHandler(callback, errorCallback); this.sendRequest(method, url, null, requestCallback, errorCallback) }; /** * POST ajax request * Expected returned as json * @param {String} url for endpoint * @param {Function} callback for success * @param {Function} callback for failure */ this.postJsonRequest = function (url, data, callback, errorCallback) { var requestCallback = createJsonHandler(callback, errorCallback); this.sendRequest('POST', url, data, requestCallback, errorCallback) }; /** * Create json response handler * @param callback {Function} * @param errorCallback {Function} * @returns {Function<string>} */ function createJsonHandler (callback, errorCallback) { return function (responseText, headers) { if (responseText) { try { var data = JSON.parse(responseText); } catch (e) { errorCallback(); return; } callback(data, headers); return; } errorCallback(); }; } /** * Creates a request object. */ this.createXHR = function () { var xhr; if (window.XDomainRequest) { xhr = new XDomainRequest(); } else { xhr = new XMLHttpRequest(); } return xhr; } /** * Send ajax request * Note: This function does not support HTTP requests that return * an ArrayBuffer, Blob, Document, or JavaScript object. Please * use a different appropriate function or adapt this function for * those use cases * * @param {String} method 'GET' or anything * @param {String} url for endpoint * @param {Function} callback for success * @param {Function} errorCallback for failure * @param {Function} beforeSendFunc called before send request */ this.sendRequest = function(method, url, data, callback, errorCallback) { var xhr; if (window.XDomainRequest) { xhr = new XDomainRequest(); xhr.onload = function() { callback(xhr.responseText, null); }; xhr.onerror = function() { errorCallback(); }; xhr.ontimeout = function() { errorCallback(); }; } else { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState === XMLHttpRequest.DONE) { if (this.status === 200 || this.status === 304) { // see https://xhr.spec.whatwg.org/#the-getallresponseheaders()-method var headerDict = {} var headers = xhr.getAllResponseHeaders().split('\r\n'); for (var i = 0; i < headers.length; i++) { if (headers[i] === '') continue; var headerPair = headers[i].split(': '); headerDict[headerPair[0]] = headerPair[1]; } callback(xhr.responseText, headerDict); } else { errorCallback(xhr); } } }; } xhr.open(method, url, true); if (data) { if ((typeof data) === 'object') { xhr.send(this.toJSON(data)); // if ((typeof data) === 'string') } else { xhr.send(data); } } else { xhr.send(); } } this.pushUnique = function(arr, val) { for (var i = 0; i < arr.length; i++) { if (val == arr[i]) return; } arr.push(val); }; this.findIndex = function (container, val, comparator) { comparator = comparator || function (ele, val) {return ele == val;}; for (var key in container){if(container.hasOwnProperty(key)) { if (comparator(container[key], val)) return key; }} return -1; }; this.setComplement = function (a, b, comparator) { comparator = comparator || function (a, b) {return a == b;}; var res = []; for (var a_ele in a){if(a.hasOwnProperty(a_ele)) { if (that.findIndex(b, a[a_ele], comparator) === -1) res.push(a[a_ele]); }} return res; }; /** * Creates a unique browser identifier based on its features. * * @returns {String} Returns an MD5 hash identifying the browser. */ this.getBrowserFingerprint = function() { if(browserFingerprint) return browserFingerprint; // ensure that every features used for the fingerprint exist if(window.navigator.vendor === undefined) window.navigator.vendor = "None"; if(window.navigator.userAgent === undefined) window.navigator.userAgent = "None"; if(window.navigator.hardwareConcurrency === undefined) window.navigator.hardwareConcurrency = "None"; if(window.navigator.language === undefined) window.navigator.language = "None"; if(window.navigator.languages === undefined) window.navigator.languages = []; if(window.navigator.plugins === undefined) window.navigator.plugins = []; if(window.screen === undefined) window.screen = { height: "None", width: "None", colorDepth: "None", pixelDepth: "None" }; var stringFingerprint = window.navigator.vendor + "::" + window.navigator.userAgent + "::" + window.navigator.hardwareConcurrency + "::" + window.navigator.language + "::" + window.navigator.languages.join() + "::" // aggregate plugins informations + function() { var plugins = window.navigator.plugins; var pluginsDescr = []; for(var i = 0; i < plugins.length; ++i) { plugin = plugins[i]; pluginsDescr.push( plugin.name + "-" + plugin.description + "-" + plugin.filename ); } return pluginsDescr; }().join() + "::" + window.screen.height + "::" + window.screen.width + "::" + window.screen.colorDepth + "::" + window.screen.pixelDepth + "::" + (new Date()).getTimezoneOffset(); browserFingerprint = widget.c('CipherMD5').encrypt(stringFingerprint); return browserFingerprint; }; /** * normalizeTextCache is for caching normalizeText()'s result. * Cache is used because normalizeText() is called many times but slow. * In function, regex is too slow, especially when Backward Reference(e.g. $) * * @type {Object.<string, string>} */ var normalizeTextCache = {}; var wovnEmptyCharacter = '\u200B'; /** * Normalizes text * If IE8 and the new line character is surrounded by non ASCII characters, it removes the new line. * Removes new lines, white space, tabs, line feeds * Trims unicode spaces * Assign \u200B if empty. * * @note IE8 displays new lines surrounded by non ASCII characters differently than if it was not surrounded. * @param {String} originalText Text to normalize * @return {String} Normalized text */ this.normalizeText = function(originalText, ignoreEmpty) { if (ignoreEmpty === undefined) { ignoreEmpty = false; } if (normalizeTextCache[originalText]) { var cached = normalizeTextCache[originalText]; if (ignoreEmpty && cached === wovnEmptyCharacter) { cached = ''; } else if (!ignoreEmpty && cached === '') { cached = wovnEmptyCharacter; } return cached; } var text = originalText; if (widget.c('Agent').mutatesTextNodeData()) { text = text.replace(/([^\u0000-\u007F])\n([^\u0000-\u007F])/g, '$1$2'); } text = text.replace(/[\n \t\u0020\u0009\u000C\u200B\u000D\u000A]+/g, ' ').replace(/^[\s\u00A0\uFEFF\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]+|[\s\u00A0\uFEFF\u1680\u180E\u2000-\u200A\u202F\u205F\u3000]+$/g, ''); if (ignoreEmpty === false && text.length === 0) { text = wovnEmptyCharacter; } normalizeTextCache[originalText] = text; return text; }; /** * Extract path from url * @param {string} url * @returns {string} */ this.extractPath = function(url) { var path = url.replace(/^.*?\/\/[^\/]+/, ''); if (path === '') { return '/'; } else { return path; } } /** * Convert NodeList to Array * * Because IE9 fail below, need shims * var arr = Array.prototype.slice.call( coll, 0 ); * @param nodeList {NodeList} * @returns {Array} */ this.toArrayFromDomList = function (nodeList) { var result = []; for(var i = 0; i < nodeList.length; i++) { result.push(nodeList[i]); } return result; } /** * Get keys from dictionary * @param {Dictionary} object * @returns {Array.<string>} */ this.keys = function(object) { if(Object.keys) return Object.keys(object); var keys = []; for(var key in object) { if (object.hasOwnProperty(key)) { keys.push(key); } } return keys; }; /** * Get values from dictionary * @param {Dictionary} object * @returns {Array.<string>} */ this.values = function(object) { var keys = this.keys(object); var values = []; for(var i = 0; i < keys.length; i++) { values.push(object[keys[i]]); } return values; }; this.each = function (object, fn) { var keys = this.keys(object); for(var i = 0; i < keys.length; i++) { var key = keys[i]; fn(key, object[key]); } } /** * Whether include value inside Object * @param {Array} objectArray * @param {Object} target * @returns {boolean} true if exists */ this.includes = function(objectArray, target) { for(var i = 0; i < objectArray.length; i++) { if (objectArray[i] === target) { return true; } } return false; } this.indexOf = function(arr, target, start) { if (arr.indexOf) { return arr.indexOf(target); } var arrayLength = arr.length >>> 0; if (arrayLength === 0) { return -1; } var startPosition = +start || 0; if (Infinity === Math.abs(startPosition)) { startPosition = 0; } if (startPosition >= arrayLength) { return -1; } startPosition = Math.max(0 <= startPosition ? startPosition : arrayLength - Math.abs(startPosition), 0) for (; startPosition < arrayLength; startPosition++) { if (startPosition in arr && arr[startPosition] === target) { return startPosition; } } return -1 } this.parseJSON = function (jsonText, reviver) { if (JSON && JSON.parse) { return JSON.parse(jsonText); } var s = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g s.lastIndex = 0; s.test(jsonText) && (jsonText = jsonText.replace(s, function (a) { return "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) })); var replace = jsonText.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, "@"); var replace2 = replace.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, "]"); var s2 = replace2.replace(/(?:^|:|,)(?:\s*\[)+/g, ""); if (/^[\],:{}\s]*$/.test(s2)) { var d = eval('(' + jsonText + ')') if ('function' === typeof reviver) { return jsonReviver({'': d}, '', reviver) } else { return d } } throw new SyntaxError('JSON.parse'); } function jsonReviver(a, d, reviver) { var g, f, b = a[d]; if (b && "object" === typeof b) for (g in b) Object.prototype.hasOwnProperty.call(b, g) && (f = jsonReviver(b, g), void 0 !== f ? b[g] = f : delete b[g]); return reviver.call(a, d, b) } var removesZeroWidthByTrim = undefined; this.trimString = function(text) { if (text.trim && removesZeroWidthByTrim === undefined) { // Some browser's implementation of String#trim() removes zero-width(U+200B) (e.g. Phantomjs) // But zero-width has meaning for wovn, so we must use polyfill if implemented incorrectly removesZeroWidthByTrim = ('\u200B'.trim().length === 0) } if (text.trim && removesZeroWidthByTrim === false) { return text.trim(); } // Polyfill from mozilla // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill return text.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); } /** * Convert list to Object to fasten access speed * @param {Object} list * @returns {Array.<Object, true>} * @example this.to_set([1,2]) #=> {1: true, 2: true} */ this.to_set = function(list) { var s = {}; for (var i = 0; i < list.length; i++) { s[list[i]] = true; } return s; } /** * Check text is empty for wovn * @param text {string} * @returns {boolean} */ this.isEmpty = function(text) { var value = widget.c('Utils').normalizeText(text); return value === wovnEmptyCharacter; }; this.destroy = function() { removeAllHandler(); }; /** * Convert to Json * * JSON.stringify can be changed by overloading Foo.prototype.toJSON. * So sometimes Invalid Json is created by old library * * @param obj {Object} * @param replacer {String|Number} * @param space {String|Number} * @returns {String} */ this.toJSON = function (obj, replacer, space) { if (this.loadsJsonBreakingPrototype()) { // old PrototypeJS break JSON.stringify and provide Object.toJSON // @see http://prototypejs.org/learn/json return Object.toJSON(obj) } else if (this.loadsJsonBreakingMooTools() && JSON.encode !== undefined) { // JSON.stringify adds extra double quotes to arrays when using MooTools, // use JSON.encode that they MooTools defines instead return JSON.encode(obj) } return JSON.stringify(obj, replacer, space) } /** * Return prototypeJS is loaded or not * @returns {boolean} */ this.loadsJsonBreakingPrototype = function () { return typeof Prototype !== 'undefined' && JSON.stringify(["a"]) !== '["a"]' } /** * Return MooTools is loaded or not * @returns {boolean} */ this.loadsJsonBreakingMooTools = function () { return typeof MooTools !== 'undefined' && JSON.stringify(["a"]) !== '["a"]' } /** * Determines whether a string begins with the characters of another string, * returning true or false as appropriate. * source: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith * * @param string {String} The string to look at. * @param searchString {String} The characters to be searched for at the * start of this string. * @param position {Number} The position in this string at which to * begin searching for searchString. Defaults * to 0. * * @returns {Boolean} True if the string begins with the characters of the search string. False, otherwise. */ this.stringStartsWith = function(string, searchString, position){ position = position || 0; return string.substr(position, searchString.length) === searchString; } }; if (typeof(components) === 'undefined') var components = {}; components['SPA'] = function(widget) { var that = this; var timer = undefined; widget.c('Utils').onEvent(window, 'popstate', function () { var langPath = widget.c('Data').getOptions().lang_path; if ((langPath === 'query' || langPath === 'path' || (widget.isBackend() && widget.tag.getAttribute('urlPattern'))) && widget.c('Lang').getDocLang() !== widget.c('Url').getLangCode()) { widget.c('Interface').changeLang(widget.c('Url').getLangCode()); } }); var fixHref = function(href) { return widget.c('Url').removeHash(href); }; this.getCurrentFixedHref = function () { return fixHref(location.href); } var lastHref = null; this.listen = function() { if (!lastHref) { lastHref = that.getCurrentFixedHref(); } timer = setInterval(function() { var currentHref = that.getCurrentFixedHref(); // if url hasn't changed OR new url is the result of a lang change(from the old url) if (lastHref === currentHref || currentHref === widget.c('Url').getUrl(widget.c('Url').getLangCode(currentHref), lastHref)) return; lastHref = currentHref; widget.c('Interface').reload(); }, 100); }; this.stop = function() { clearInterval(timer); }; this.destroy = function() { that.stop(); }; }; // FIXME use cookies instead??? if (typeof(components) === "undefined") var components = {}; components["ViewAnalytics"] = function (widget) { var that = this; var viewAnalytics; this.get = function () { return viewAnalytics; }; this.start = function () { viewAnalytics = { token: widget.tag.getAttribute("key"), url: decodeURIComponent(widget.c("Url").getEncodedLocation(location.href)), timestamp: (new Date()).toString(), fingerprint: viewFingerprint = widget.c("Utils").getBrowserFingerprint(), selectedLanguages: [widget.c("Lang").getDocLang()] }; if (viewAnalytics.token !== "false") { // record selected languages on language changed widget.c('Utils').addEventListener("wovnLangChanged", this.onLanguageChanged); // send view analytics when the window/tab is closed or reloaded widget.c("Utils").onEvent(window, "beforeunload", this.deliverViewAnalytics); widget.c("Utils").onEvent(window, "beforereload", this.deliverViewAnalytics); } }; this.stop = function () { var wovnSwitches = document.getElementsByClassName("wovn-switch"); document.removeEventListener("wovnLangChanged", this.onLanguageChanged); widget.c("Utils").removeHandler(window, "beforeunload", this.deliverViewAnalytics); widget.c("Utils").removeHandler(window, "beforereload", this.deliverViewAnalytics); this.deliverViewAnalytics(); }; this.deliverViewAnalytics = function () { var request = new XMLHttpRequest(); var host = "//view-analytics.wovn.io/".replace(/^.*\/\//, "//"); var jsonViewAnalytics = JSON.stringify(viewAnalytics, null, 4); var postBody = "viewAnalytics=" + encodeURIComponent(jsonViewAnalytics); var action = "view_analytics/deliver_view_analytics"; request.open("POST", host + action, true); request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); request.send(postBody); }; this.onLanguageChanged = function () { var selectedLangCode = widget.c("Lang").getDocLang(); if (widget.c('Utils').indexOf(viewAnalytics.selectedLanguages, selectedLangCode) == -1) { viewAnalytics.selectedLanguages.push(selectedLangCode); } } }; /*! * Copyright 1999-2010, Joseph Myers, Paul Johnston, Greg Holt, Will Bond <will@wbond.net> * http://www.myersdaily.org/joseph/javascript/md5-text.html * http://pajhome.org.uk/crypt/md5 * * Released under the BSD license * http://www.opensource.org/licenses/bsd-license */ if (typeof(components) === 'undefined') var components = {}; components['CipherMD5'] = function (widget) { var that = this; this.encrypt = function(str) { return md5(str); } function md5cycle(x, k) { var a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = add32(a, x[0]); x[1] = add32(b, x[1]); x[2] = add32(c, x[2]); x[3] = add32(d, x[3]); } function cmn(q, a, b, x, s, t) { a = add32(add32(a, q), add32(x, t)); return add32((a << s) | (a >>> (32 - s)), b); } function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); } function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); } function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); } function md51(s) { // Converts the string to UTF-8 "bytes" when necessary if (/[\x80-\xFF]/.test(s)) { s = unescape(encodeURI(s)); } txt = ''; var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= s.length; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } s = s.substring(i - 64); var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; for (i = 0; i < s.length; i++) tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); tail[i >> 2] |= 0x80 << ((i % 4) << 3); if (i > 55) { md5cycle(state, tail); for (i = 0; i < 16; i++) tail[i] = 0; } tail[14] = n * 8; md5cycle(state, tail); return state; } function md5blk(s) { /* I figured global was faster. */ var md5blks = [], i; /* Andy King said do it this way. */ for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); } return md5blks; } var hex_chr = '0123456789abcdef'.split(''); function rhex(n) { var s = '', j = 0; for (; j < 4; j++) s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + hex_chr[(n >> (j * 8)) & 0x0F]; return s; } function hex(x) { for (var i = 0; i < x.length; i++) x[i] = rhex(x[i]); return x.join(''); } function md5(s) { return hex(md51(s)); } /* this function is much faster, so if possible we use it. Some IEs are the only ones I know of that need the idiotic second function, generated by an if clause. */ function add32(a, b) { return (a + b) & 0xFFFFFFFF; } if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { function add32(x, y) { var lsw = (x & 0xFFFF) + (y & 0xFFFF), msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xFFFF); } } }; if (typeof(components) === 'undefined') var components = {}; components['PerformanceMonitor'] = function(widget) { //This component allow using newest function, because this is for special environment. var performanceResults = { 'page_request_start': null, 'page_response_end': null, 'wovn_js_request_start': null, 'wovn_js_response_end': null }; var markPrefix = 'wovn_'; var that = this; this.mark = function(name) { if(that.isMonitorable == false) return; var markName = markPrefix + name; //performance.mark(markName); if (performanceResults[markName] == undefined || performanceResults[markName] == null) { performanceResults[markName] = new Date().getTime(); } }; this.getResult = function() { if(that.isMonitorable == false) return {}; var keys = Object.keys(performanceResults); var result = {}; for(var i = 0; i < keys.length; i++) { var originalKey = keys[i]; var key = originalKey; if(new RegExp(markPrefix).test(key)) { key = key.substring(markPrefix.length); } result[key] = performanceResults[originalKey]; } return result; }; this.isMonitorable = false; this.resetIsMonitorable = function() { if(widget.c('Cookie').get('wovn_monitor_enable') === 'true') { that.isMonitorable = true; } else { that.isMonitorable = false; } }; initIsMonitorable(); function initIsMonitorable() { that.resetIsMonitorable(); } }; // Original Cookie code from http://developers.livechatinc.com/blog/setting-cookies-to-subdomains-in-javascript/ if (typeof(components) === 'undefined') var components = {}; components['Config'] = function (widget) { var that = this; this.urlPattern = function (test) { var urlPattern; if (widget.tag.getAttribute('urlPattern')) urlPattern = widget.tag.getAttribute('urlPattern'); else if (widget.c('Data').getOptions().lang_path) { switch (widget.c('Data').getOptions().lang_path) { case 'query': urlPattern = 'query'; break; case 'path': urlPattern = 'path'; break; } } return arguments.length === 0 ? urlPattern : urlPattern === test; }; this.backend = function (test) { var backend; backend = widget.tag.getAttribute('backend') ? true : false; return arguments.length === 0 ? backend : backend === !!test; }; this.getSitePrefixPath = function() { return getAttributeUsingCache('site_prefix_path'); } var tagCaches = {}; function getAttributeUsingCache (name) { if (!tagCaches.hasOwnProperty(name)) { tagCaches[name] = widget.tag.getAttribute(name); } return tagCaches[name]; } this.setConfig = function (name, val) { if (name === 'setConfig') return nil; this[name] = val; return val; }; }; /** * Parser utility */ if (typeof(components) === 'undefined') var components = {}; components['Parser'] = function (widget) { var that = this; /** * Get image's url from css style * @see https://www.w3.org/TR/CSS22/syndata.html#uri * @param {String} cssUrl */ this.getUrlFromCss = function(cssUrl) { cssUrl = cssUrl.split(',')[0]; var match = /^url\(["']?([^"']+?)["']?\)?$/.exec(cssUrl); if (match) { return match[1]; } return null; }; }; if (typeof(components) === 'undefined') var components = {}; components['Data'] = function (widget) { var cachedHostAliases = undefined; var that = this; var data = {}; var savedData = {}; /** * Set Data * @param {{}.<string, {}>} d */ this.set = function(d) { data = d; }; this.setSavedData = function(data) { savedData = data; } this.getSavedData = function() { return savedData; } /** * Get Data * @returns {{}} */ this.get = function () { return data; }; /** * Get language * @returns {string} */ this.getLang = function () { return data['language']; }; /** * Get secondary language * @returns {string} */ this.getSecondaryLang = function () { return that.getOptions()['secondary_language']; }; /** * Get user_id * @returns {Integer} */ this.getUserId = function () { return data['user_id']; }; /** * Get Page-id * @returns {Integer} */ this.getPageId = function () { return data['id']; }; /** * Get text_vals * @returns {{}} */ this.getTextValues = function() { return data['text_vals'] || {}; }; /** * Get html_text_vals * @returns {{}} */ this.getHTMLTextValues = function() { return data['html_text_vals'] || {}; }; /** * Get img_vals * @returns {{}} */ this.getImageValues = function() { return data['img_vals'] || {}; }; /** * Get published languages * @returns {Array.string} */ this.getPublishedLangs = function () { return data['published_langs']; }; /** * Get Translatable languages * * i.e.) published languages plus original language * @returns {Array.string} */ this.getTranslatableLangs = function () { return this.getPublishedLangs().concat(this.getLang()) } this.getAutoTranslateLangs = function() { var autoTranslateLangs = that.getOptions()['auto_translate_langs']; if (autoTranslateLangs) { return autoTranslateLangs; } return data['auto_translate_langs']; } this.getAutoPublishLangs = function() { var autoPublishLangs = that.getOptions()['auto_publish_langs']; if (autoPublishLangs) { return autoPublishLangs; } return data['auto_translate_langs']; } /** * Get option * @returns {{}} */ this.getOptions = function () { return data['widgetOptions']; }; /** * Set option * @param {Dictionary} options to set */ this.setOptions = function(options) { data['widgetOptions'] = options; }; /** * Update option * @param {Dictionary} options to update */ this.updateOptions = function(options) { if(!data['widgetOptions']) { data['widgetOptions'] = {}; } for (var option in options) { if (options.hasOwnProperty(option)) { data['widgetOptions'][option] = options[option]; } } }; this.getStyleColor = function() { var style = getValue('style') if (!style) { return null } var splitStyle = style.split(' ') if (splitStyle.length < 2) { return null } return splitStyle[1] } /** * Return if needs country code * @param data {Dictionary} * @returns {*|boolean} */ this.needsCountryCode = function(data) { return data['useCountryData'] || false; }; /** * Return country-code * @returns {*} undefined if not set */ this.getCountryCode = function() { if (!data['widgetOptions']) return undefined; return that.getOptions()['countryCode']; }; /** * True if user see at japan * @returns {boolean} */ this.browsesFromJapan = function() { if (!data['widgetOptions']) return false; return that.getOptions()['countryCode'] === 'JP'; }; /** * Set CountryCode * @param code {String} */ this.setCountryCode = function(code) { if (!data['widgetOptions']) { data['widgetOptions'] = {}; } data['widgetOptions']['countryCode'] = code; }; /** * Get whether use dynamic values * @returns {boolean} */ this.dynamicValues = function () { return getValue('dynamic_values') || false; }; /** * Get whether use dynamic loading * @returns {boolean} */ this.dynamicLoading = function () { return getValue('dynamic_loading') || false; }; /** * Get whether use aria_label * Slow if this option is enabled * @returns {boolean} */ this.useAriaLabel = function () { return data['widgetOptions']['aria_label']; } /** * Get whether use fragmented value * Always false, until work completed * @returns {boolean} */ this.useFragmentedValue = function () { return data['widgetOptions']['scraping2']; } /** * Get whether swapping numbers is allowed * @returns {boolean} */ this.numberSwappingAllowed = function () { return data['widgetOptions']['number_swapping'] || false; } /** * Create HostAlias without regex character * @returns {Array} */ this.createNormalizedHostAliases = function () { if (cachedHostAliases) { return cachedHostAliases; } var hostAliases = that.getOptions()['host_aliases'] || []; for(var i = 0; i < hostAliases.length; i++) { hostAliases[i] = hostAliases[i].replace(/^\^/, '').replace(/\$$/, '').replace(/\\([^\\])/g, '$1'); } cachedHostAliases = hostAliases; return hostAliases; } /** * Get value set in data or options * @param name * @returns {*} */ function getValue(name) { return data[name] || (that.getOptions() || {})[name] } }; if (typeof(components) === 'undefined') var components = {}; components['PageChecker'] = function (widget) { var that = this; /** * Checks if the page is not found and notify wovn.io if so (only supports * pages generated by GET method). * First, this function looks for clues that the page might be undefined. * Second it sends a request to get window.location and looks for 404 status * code in the response header. * By doing so, the widget does not always send requests to check if the page * is not found, but it only notifies wovn.io if the page is trully not found. */ this.notifyWovnIfNotFound = function () { if (pageHasNotFoundClues()) { checkPageNotFoundInHeader(function () { var xhr = widget.c('Utils').createXHR(); var apiHost = '//ee.wovn.io/'.replace(/^.*\/\//, "//"); var notifyUrl = apiHost + 'page_not_found/' + widget.tag.getAttribute('key'); xhr.open("POST", notifyUrl, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('url=' + widget.c('Url').getEncodedLocation()) }); } } /** * Looks for clues that the page might be not found. * Clues: * - page title contains 'not found' keyword * - page title contains '404' keyword * * @return {Boolean} True if there are clues that the page might be not found. */ function pageHasNotFoundClues () { return notFoundInTitle() || notFoundInBody(); } /** * Sends a request to get the page and applies a given callback if page is not * found (only supports pages generated by GET method). * * @param [Function] callback The function to call if the page is not found. */ function checkPageNotFoundInHeader (notFoundCallback) { // only reliable for pages generated by GET method widget.c('Utils').sendRequest('HEAD', window.location.href, null, function () {}, function (xhr) { if (xhr.status === 404) { notFoundCallback(); } }); } var notFoundOccurrences = [ '404', //'غير معثور عليه', // Arabic 'не е намерена', // Bulgarian '未找到', // Simplified Chinese '未找到', // Traditional Chinese 'ikke fundet', // Danish 'niet gevonden', // Dutch 'not found', // English 'ei löydetty', // Finish 'pas trouvé', // French 'non trouvé', // French 'introuvable', // French 'nicht gefunden', // German 'δεν βρέθηκε', // Greek 'לא נמצא', // Hebrew 'नहीं मिला', // Hindi 'tidak ditemukan', // Indonesian 'non trovato', // Italian '見つかりません', // Japanese '찾을 수 없음', // Korean 'tidak ditemui', // Malay 'ikke funnet', // Norwegian 'nie znaleziono', // Polish 'não encontrado', // Portuguese 'не обнаружена', // Russian 'extraviado', // Spanish 'no encontrada', // Spanish 'hittades inte', // Swedish 'ไม่พบ', // Thai 'bulunamadı', // Turkish 'не знайдено', // Ukrainian 'không tìm thấy', // Vietnamese ]; var notFoundRegExp = new RegExp('(' + notFoundOccurrences.join('|') + ')', 'i'); /** * Checks if there is any occurence of page not found within the title. * * @return {Boolean} True if the title contains an occurrence of page not * found text within the title, false otherwise. */ function notFoundInTitle () { return document.title.search(notFoundRegExp) !== -1; } /** * Checks if there is any occurence of page not found within the body. * This function uses innerText, which is non-standard but supported by * Chrome (4+), Firefox (45+), IE (6+), Opera (9.6+), Safari (3+). * innerText is useful because it is aware of styling and won't show text from * non-displayed nodes. * * @return {Boolean} True if the title contains an occurrence of page not * found text within the body, false otherwise. */ function notFoundInBody () { var body = document.body; var innerText = body.innerText; // TODO do not apply this on mobile if there is performance issues??? return innerText && innerText.search(notFoundRegExp) !== -1; } }; if (typeof(components) === "undefined") var components = {}; components['SwapIntercom'] = function (widget) { var MESSAGES = { 'subscribe': 'WOVNIO_SWAP_INTERCOM_SUBSCRIBE', 'unsubscribe': 'WOVNIO_SWAP_INTERCOM_UNSUBSCRIBE', 'acknowledge': 'WOVNIO_SWAP_INTERCOM_ACKNOWLEDGE', 'swap': 'WOVNIO_SWAP_INTERCOM_SWAP' } var intercomNode = null; this.start = function () { if (!widget.c('Utils').pageIsWidgetPreview()) { if (this.isMasterIntercom()) { intercomNode = this.createParentNode(); } else { intercomNode = this.createChildNode(); } intercomNode.start(); } } this.stop = function () { intercomNode.stop(); } this.isMasterIntercom = function () { return window.self === window.top; } this.createParentNode = function () { return new ParentNode(); } this.createChildNode = function () { return new ChildNode(); } function ParentNode() { var childNodes = []; this.start = function () { widget.c('Utils').onEvent(window.self, 'message', listen); widget.c('Utils').addEventListener('wovnLangChanged', dispatchSwappingRequestToEveryChild); } this.stop = function () { widget.c('Utils').removeHandler(window.self, 'message', listen); document.removeEventListener('wovnLangChanged', dispatchSwappingRequestToEveryChild); } function listen(event) { switch (event.data) { case MESSAGES['subscribe']: var childNode = event.source; var added = addChildNode(childNode); if (added) { var langCode = widget.c('Lang').getDocLang(); dispatchSwappingRequest(childNode, langCode); } break; case MESSAGES['unsubscribe']: removeChildNode(event.source); break; } } function addChildNode(childNode) { if (!widget.c('Utils').includes(childNodes, childNode)) { childNodes.push(childNode); childNode.postMessage(MESSAGES['acknowledge'], '*'); return true; } return false } function removeChildNode(childNode) { var childIndex = widget.c('Utils').indexOf(childNodes, childNode); if (childIndex >= 0) { childNodes.splice(childIndex, 1); return true; } return false; } function dispatchSwappingRequest(childNode, langCode) { if (childNode) { childNode.postMessage(MESSAGES['swap'] + ':' + langCode, '*'); } } function dispatchSwappingRequestToEveryChild() { var langCode = widget.c('Lang').getDocLang(); for (var i = 0; i < childNodes.length; ++i) { dispatchSwappingRequest(childNodes[i], langCode); } } } function ChildNode() { var parentNode = window.top; var subscribeTimeoutID = null; var subscriptionTryCount = 0; this.start = function () { widget.c('Utils').onEvent(window.self, 'message', listen); subscriptionTryCount = 0; subscribe(); } this.stop = function () { widget.c('Utils').removeHandler(window.self, 'message', listen); unsubscribe(); } function listen(event) { var args = event.data.split(':'); switch (args[0]) { case MESSAGES['acknowledge']: clearTimeout(subscribeTimeoutID); widget.c('Interface').destroy(); break; case MESSAGES['swap']: var langCode = args[1]; widget.c('Lang').setDocLang(langCode); break; } } function subscribe() { if (parentNode) { subscriptionTryCount += 1; parentNode.postMessage(MESSAGES['subscribe'], '*'); subscribeTimeoutID = setTimeout(subscribe, 1000 * subscriptionTryCount); } } function unsubscribe() { clearTimeout(subscribeTimeoutID); parentNode.postMessage(MESSAGES['unsubscribe'], '*'); } } }; if (typeof(components) === 'undefined') var components = {} components['ParcelForwarding'] = function(widget) { var that = this var Vue = widget.c('Vue') this.banner = null this.start = function () { widget.c('Interface').insertStyles("div#wovn-tenso-modal {\n display: none;\n z-index: 99999999999;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n text-align: center;\n background: rgba(84,95,102, 0.8);\n overflow-y: auto;\n font-family: helvetica, arial, \'hiragino kaku gothic pro\', meiryo, \'ms pgothic\', sans-serif;\n}\n.tenso-img {\n display: inline-block;\n}\n.raku-ichiban-img {\n display: none;\n}\n.raku-ichiban .tenso-img {\n display: none;\n}\n.raku-ichiban .raku-ichiban-img {\n display: inline-block;\n}\ndiv#wovn-tenso-modal.opened {\n display: block;\n}\ndiv.wovn-tenso-dialog {\n width: 652px;\n height: 396px;\n position: relative;\n margin: 7% auto;\n padding: 24px 25px 16px;\n display: inline-block;\n border-radius: 3px;\n opacity: 1;\n background-color: #ffffff;\n box-shadow: 0 19px 38px 0 rgba(0, 0, 0, 0.3), 0 15px 12px 0 rgba(0, 0, 0, 0.22);\n}\ndiv.wovn-tenso-close {\n position: absolute;\n width: 32px;\n top: 16px;\n right: 0;\n margin: 9px;\n line-height: 14px;\n font-size: 30px;\n color: #bdc4c8;\n cursor: pointer;\n}\ndiv.wovn-tenso-header {\n text-align: center;\n}\ndiv.wovn-tenso-logo {\n position: absolute;\n top: 71px;\n left: 69px;\n}\ndiv.wovn-tenso-title {\n text-align: center;\n color: #545f66;\n font-size: 20px;\n margin-top: 27px;\n margin-bottom: 25px;\n height: 30px;\n}\ndiv.wovn-tenso-lang-selector {\n display: inline-block;\n padding: 0 5px;\n}\ndiv.wovn-tenso-lang-selector:after {\n content: \'|\';\n color: #8f9aa0;\n font-size: 16px;\n}\ndiv.wovn-tenso-lang-selector:last-child:after {\n content: \'\';\n}\nspan.wovn-tenso-lang-selector-name {\n font-size: 14px;\n color: #469fd6;\n cursor: pointer;\n}\nspan.wovn-tenso-lang-selector-name.active {\n color: #545f66;\n}\ndiv.wovn-tenso-subtitle {\n text-align: center;\n font-size: 14px;\n color: #8f9aa0;\n margin-bottom: 16px;\n height: 42px;\n}\ndiv.wovn-tenso-subtitle span {\n display: block;\n}\ndiv.wovn-tenso-steps {\n height: 170px;\n position: relative;\n}\ndiv.wovn-tenso-step {\n text-align:center;\n display:inline-block;\n vertical-align: bottom;\n width: 160px;\n height: 140px;\n margin: 5px 17px;\n border-radius: 3px;\n background-color: #ffffff;\n border: solid 1px #e6e6e6;\n}\ndiv.wovn-tenso-step-content {\n padding: 5px 10px;\n}\ndiv.wovn-tenso-step-title {\n padding: 15px 0;\n font-size: 20px;\n color: #ff4d09;\n}\n.raku-ichiban div.wovn-tenso-step-title {\n color: #ab263b;\n}\ndiv.wovn-tenso-step-text {\n font-size: 14px;\n color: #545f66;\n}\ndiv.wovn-tenso-step-separator {\n display: inline-block;\n color: #ff4d09;\n position: relative;\n margin-bottom: 70px;\n}\n.raku-ichiban div.wovn-tenso-step-separator {\n color: #ab263b;\n}\ndiv.wovn-tenso-footer-border {\n border-top: 1px solid rgba(0,0,0, 0.12);\n margin: 2px -25px 0 -25px;\n}\ndiv.wovn-tenso-footer {\n}\ndiv.wovn-tenso-footer-buttons {\n margin-top: 16px;\n}\ndiv.wovn-tenso-cancel-button {\n display: inline-block;\n font-size: 12px;\n padding: 12px 30px;\n color: #545f66;\n}\ndiv.wovn-tenso-cancel-button:hover {\n cursor: pointer;\n}\ndiv.wovn-tenso-ok-button {\n display: inline-block;\n font-size: 12px;\n padding: 12px 30px;\n color: #ffffff;\n background-color: #FF4D09;\n border-radius: 3px;\n}\n.raku-ichiban div.wovn-tenso-ok-button {\n background-color: #ab263b;\n}\ndiv.wovn-tenso-ok-button:hover {\n background-color: #FF703A;\n}\n.raku-ichiban div.wovn-tenso-ok-button:hover {\n background-color: #C55062;\n}\ndiv.wovn-tenso-ok-button:active {\n background-color: #E54508;\n}\n@media(max-width: 600px) {\n div.wovn-tenso-step-separator {\n display:none;\n }\n div.wovn-tenso-logo {\n position: relative;\n padding-top: 20px;\n top: initial;\n left: initial;\n }\n div.wovn-tenso-dialog {\n width: 80%;\n height: 472px;\n }\n div.wovn-tenso-step {\n width: 100%;\n height: 61px;\n margin: 5px auto;\n }\n div.wovn-tenso-step-title {\n margin-top: 5px;\n padding: 0;\n font-size: 16px;\n color: #ff4d09;\n }\n div.wovn-tenso-step-text {\n margin-top: -5px;\n padding: 8px 0 16px 0;\n font-size: 11px;\n }\n div.wovn-tenso-footer-border {\n margin: 62px -25px 0 -25px;\n }\n div.wovn-tenso-title {\n margin: 20px 0 0 0;\n font-size: 16px;\n }\n div.wovn-tenso-subtitle {\n font-size: 12px;\n }\n div.wovn-tenso-footer-buttons {\n margin: 16px 0;\n }\n}\n@media(max-width: 320px) {\n div.wovn-tenso-dialog {\n width: 85%;;\n height: 478px;\n padding: 24px 16px 16px;\n }\n div.wovn-tenso-subtitle {\n margin-bottom: 22px;\n }\n}\n\n/* BANNER */\nbody[wovn-tenso-banner-on] {\n padding-top: 60px;\n}\ndiv#wovn-tenso-banner {\n display: none;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 60px;\n color: #3991c9;\n background-color: #b7e2fd;\n font-family: helvetica, arial, \'hiragino kaku gothic pro\', meiryo, \'ms pgothic\', sans-serif;\n text-align: center;\n box-shadow: 0 -1px 3px 0 rgba(0, 0, 0, 0);\n}\ndiv#wovn-tenso-banner.raku-ichiban {\n color: white;\n background-color: #ab263b;\n}\ndiv#wovn-tenso-banner.opened {\n display: block;\n}\na.wovn-tenso-banner-content {\n display: block;\n width: 100%;\n height: 100%;\n text-decoration: none;\n}\ndiv.wovn-tenso-banner-logo {\n display: inline-block;\n top: 14px;\n position: relative;\n}\n.raku-ichiban div.wovn-tenso-banner-logo {\n top: 12px;\n width: 72px;\n height: 33.9px;\n}\ndiv.wovn-tenso-banner-text {\n display: inline-block;\n font-size: 14px;\n top: 7px;\n position: relative;\n padding-left: 10px;\n}\n.raku-ichiban div.wovn-tenso-banner-text {\n color: #ffffff;\n}\ndiv.wovn-tenso-banner-link {\n display: inline-block;\n color: #f95c29;\n font-size: 16px;\n top: 7px;\n position: relative;\n padding-left: 10px;\n}\n\n.raku-ichiban div.wovn-tenso-banner-link {\n color: #ffffff;\n}\n\n@media (max-width: 440px) {\n a.wovn-tenso-banner-content {\n text-decoration: none;\n }\n div.wovn-tenso-banner-logo, .raku-ichiban div.wovn-tenso-banner-logo {\n display: block;\n top:9px;\n }\n .raku-ichiban div.wovn-tenso-banner-logo {\n width: auto;\n }\n div.wovn-tenso-banner-logo img {\n width: 90px;\n }\n .raku-ichiban div.wovn-tenso-banner-logo img {\n width: 70px;\n }\n div.wovn-tenso-banner-text {\n top: 8px;\n font-size: 10px;\n }\n div.wovn-tenso-banner-link {\n top: 8px;\n padding-left: 0;\n font-size: 12px;\n }\n}\n") var PARCEL_FORWARDING_LANG_COOKIE = "wovn_parcel_forwarding_lang" var provider = widget.c('Data').get()["widgetOptions"]["parcel_forwarding"] var providerName = {}; if (provider === 'raku-ichiban') { providerName['ja'] = '楽一番'; providerName['en'] = 'Leyifan'; providerName['cht'] = '楽一番'; providerName['chs'] = '楽一番'; } else { providerName['ja'] = '転送コム'; providerName['en'] = 'Tenso'; providerName['ko'] = 'tenso.com'; providerName['cht'] = 'tenso'; providerName['chs'] = 'tenso'; } var parcelForwardingLangs = [ {name: '日本', code: 'jp'}, {name: 'EN', code: 'en'}, {name: '繁體', code: 'cht'}, {name: '简体', code: 'chs'}, ] if (provider !== 'raku-ichiban') { parcelForwardingLangs.push({name: '한글', code: 'kr'}) } function getParcelForwardingLang(force) { var currLang = widget.c('Cookie').get(PARCEL_FORWARDING_LANG_COOKIE) if (currLang === null || force) { var docLang = widget.c('Lang').getDocLang() if (provider === 'raku-ichiban') { currLang = 'chs' } else { //provider == tenso currLang = 'en' } switch (docLang) { case 'ja': currLang = 'jp' break case 'zh-CHS': currLang = 'chs' break case 'zh-CHT': currLang = 'cht' break case 'ko': if (provider !== 'raku-ichiban') { currLang = 'kr' } break case 'en': currLang = 'en' } widget.c('Cookie').set(PARCEL_FORWARDING_LANG_COOKIE, currLang, 365) } return currLang } // do not create tenso modal if already shown to user if (widget.c('Cookie').get(PARCEL_FORWARDING_LANG_COOKIE) === null) { var tensoModal = document.createElement('div') tensoModal.id = 'wovn-tenso-modal'; if (provider == "raku-ichiban") { tensoModal.className = 'raku-ichiban'; } tensoModal.setAttribute('wovn-ignore', '') tensoModal.innerHTML = "<div class=\"wovn-tenso-dialog\" @click.stop>\n <div class=\"wovn-tenso-content\">\n <div class=\"wovn-tenso-close\" @click=\"close\">×<\/div>\n <div class=\"wovn-tenso-header\">\n <div class=\"wovn-tenso-lang-selector\" v-for=\"lang in languages\">\n <span v-text=\"lang.name\" @click=\"changeLang(lang)\" :class=\"{ active: lang.code === currentLangCode }\" class=\"wovn-tenso-lang-selector-name\"><\/span>\n <\/div>\n <\/div>\n <div class=\"wovn-tenso-logo\">\n <img src=\"//wovn.io/assets/tenso_logo_modal.png\" class=\"tenso-img\" alt=\"Tenso\">\n <img src=\"//wovn.io/assets/raku_ichiban_logo_color.png\" class=\"raku-ichiban-img\" alt=\"Tenso\">\n <\/div>\n <div class=\"wovn-tenso-title\">\n <span v-text=\"textContents[currentLangCode].title\"><\/span>\n <\/div>\n <div class=\"wovn-tenso-subtitle\">\n <span v-text=\"textContents[currentLangCode].subtitle1\"><\/span>\n <span v-text=\"textContents[currentLangCode].subtitle2\"><\/span>\n <\/div>\n <div class=\"wovn-tenso-steps\">\n <div class=\"wovn-tenso-step\">\n <div class=\"wovn-tenso-step-content\">\n <div class=\"wovn-tenso-step-title\">STEP 1<\/div>\n <div class=\"wovn-tenso-step-text\" v-text=\"textContents[currentLangCode].step1\"><\/div>\n <\/div>\n <\/div>\n <div class=\"wovn-tenso-step-separator\">\n <img src=\"//wovn.io/assets/tenso_next_step.png\" class=\"tenso-img\" alt=\">\">\n <img src=\"//wovn.io/assets/raku_ichiban_next_step.png\" class=\"raku-ichiban-img\" alt=\">\">\n <\/div>\n <div class=\"wovn-tenso-step\">\n <div class=\"wovn-tenso-step-content\">\n <div class=\"wovn-tenso-step-title\">STEP 2<\/div>\n <div class=\"wovn-tenso-step-text\" v-text=\"textContents[currentLangCode].step2\"><\/div>\n <\/div>\n <\/div>\n <div class=\"wovn-tenso-step-separator\">\n <img src=\"//wovn.io/assets/tenso_next_step.png\" class=\"tenso-img\" alt=\">\">\n <img src=\"//wovn.io/assets/raku_ichiban_next_step.png\" class=\"raku-ichiban-img\" alt=\">\">\n <\/div>\n <div class=\"wovn-tenso-step\">\n <div class=\"wovn-tenso-step-content\">\n <div class=\"wovn-tenso-step-title\">STEP 3<\/div>\n <div class=\"wovn-tenso-step-text\" v-text=\"textContents[currentLangCode].step3\"><\/div>\n <\/div>\n <\/div>\n <\/div>\n <div class=\"wovn-tenso-footer-border\"><\/div>\n <div class=\"wovn-tenso-footer\">\n <div class=\"wovn-tenso-footer-buttons\">\n <div class=\"wovn-tenso-cancel-button\" v-text=\"textContents[currentLangCode].cancel\" @click=\"close\"><\/div>\n <a v-bind:href=\"langLink\" target=\"_blank\"><div class=\"wovn-tenso-ok-button\" v-text=\"textContents[currentLangCode].ok\"><\/div><\/a>\n <\/div>\n <\/div>\n <\/div>\n<\/div>\n"; tensoModal.setAttribute('v-bind:class', '{opened: opened}') tensoModal.setAttribute('v-on:click', 'close') document.body.appendChild(tensoModal) var tensoModalVue = new Vue({ el: '#wovn-tenso-modal', data: { opened: false, currentLangCode: getParcelForwardingLang(), languages: parcelForwardingLangs, textContents: { 'jp': { 'title': '簡単に海外発送することができます!', 'subtitle1': providerName['ja'] + 'を使えば、簡単に海外配送が可能になります。', 'subtitle2': '日本の通販サイトの商品を、あなたの国へお届けします。登録は無料!', 'step1': providerName['ja'] + 'に登録して日本の住所をゲット!登録は無料!', 'step2': '日本の通販サイトでお好きなアイテムを購入', 'step3': '日本国外へ商品を転送!', 'cancel': '閉じる', 'ok': '登録はこちら' }, 'en': { 'title': 'Easily shop in Japan and ship overseas!', 'subtitle1': 'With ' + providerName['en'] + ', you can easily order products from Japan and have them shipped to your home address in your country.', 'subtitle2': 'Registration is free!', 'step1': 'Get a ' + providerName['en'] + ' Japanese mailing address. Registration is free!', 'step2': 'Purchase your items from any Japanese e-commerce site.', 'step3': 'Your items will be forwarded from your Japanese address to your overseas address.', 'cancel': 'Close', 'ok': 'Register now' }, 'cht': { 'title': '將您購買的商品快速便捷地送往海外!', 'subtitle1': '利用' + providerName['cht'] + ',將原本困難的海外配送瞬間化為可能', 'subtitle2': '免費註冊!讓您在日本網站購買的商品被直接送到您家!', 'step1': '在' + providerName['cht'] + '註冊後即獲得日本地址!註冊免費!', 'step2': '在日本的網站選購您喜愛的商品', 'step3': '商品將會從日本國內被送往您所在的國家!', 'cancel': '關閉', 'ok': '點擊這裡註冊' }, 'chs': { 'title': '将您购买的商品快速便捷地送往海外!', 'subtitle1': '利用' + providerName['chs'] + ',将原本困难的海外配送瞬间化为可能', 'subtitle2': '免费注册!让您在日本网站购买的商品被直接送到家!', 'step1': '在' + providerName['chs'] + '注册后即获得日本地址!注册免费!', 'step2': '在日本的网站选购您喜爱的商品', 'step3': '商品将会从日本国内被送往您所在的国家!', 'cancel': '关闭', 'ok': '点击这里注册' }, 'kr': { 'title': '쉽게 해외 배송 수 있습니다!', 'subtitle1': '전송 컴을 사용하면 쉽게 해외 배송이 가능합니다.', 'subtitle2': '일본 인터넷 쇼핑몰의 상품을 당신의 국가에 제공합니다. 가입은 무료!', 'step1': '전송 컴에 가입하고 일본 주소를 겟트! 가입은 무료!', 'step2': '일본 인터넷 쇼핑몰에서 원하는 상품을 구입', 'step3': '일본 국외에 상품을 전송!', 'cancel': '닫기', 'ok': '등록은 이쪽' } } }, computed: { langLink: function () { if (provider == "raku-ichiban") { return 'http://www.leyifan.com/' + (this.currentLangCode === 'chs' ? '' : this.currentLangCode) } else { // provider == tenso return 'http://www.tenso.com/' + this.currentLangCode + '/static/lp_shop_index' } } }, methods: { changeLang: function (langObj) { this.currentLangCode = langObj.code widget.c('Cookie').set(PARCEL_FORWARDING_LANG_COOKIE, this.currentLangCode, 365) }, open: function () { this.opened = true }, close: function () { this.opened = false } }, watch: { currentLangCode: function () { tensoBannerVue.currentLangCode = this.currentLangCode } } }) tensoModalVue.open() } //========================================================================== var tensoBanner = document.createElement('div') tensoBanner.id = 'wovn-tenso-banner' if (provider == "raku-ichiban") { tensoBanner.className = 'raku-ichiban'; } tensoBanner.setAttribute('wovn-ignore', '') tensoBanner.innerHTML = "<a class=\"wovn-tenso-banner-content\" v-bind:href=\"langLink\" target=\"_blank\">\n <div class=\"wovn-tenso-banner-logo\">\n <img src=\"//wovn.io/assets/tenso_logo_banner.png\" class=\"tenso-img\" alt=\"Tenso\">\n <img src=\"//wovn.io/assets/raku_ichiban_logo_white.png\" class=\"raku-ichiban-img\" alt=\"Tenso\" id=\"banner-image\">\n <\/div>\n <div class=\"wovn-tenso-banner-text\" v-text=\"textContents[currentLangCode].bannerText\"><\/div>\n <div class=\"wovn-tenso-banner-link\" v-text=\"textContents[currentLangCode].link\"><\/div>\n<\/a>\n" tensoBanner.setAttribute('v-bind:class', '{opened: opened}') document.body.appendChild(tensoBanner) var tensoBannerVue = new Vue({ el: '#wovn-tenso-banner', data: { opened: false, imageSrc:'', currentLangCode: tensoModalVue ? tensoModalVue.currentLangCode : getParcelForwardingLang(), languages: parcelForwardingLangs, textContents: { 'jp': { 'bannerText': '海外の顧客、商品を購入するにはこちらをクリック!', 'link' : 'ここをクリック' }, 'en': { 'bannerText': 'Overseas customers, click here to buy this item!', 'link' : 'Click Here' }, 'cht': { 'bannerText': '海外客戶,點擊這裡買這個商品!', 'link' : '點擊這裡' }, 'chs': { 'bannerText': '海外客户,点击这里买这个商品!', 'link' : '点击这里' }, 'kr': { 'bannerText': '해외 고객이 상품을 구입하려면 여기를 클릭!', 'link' : '여기를 클릭하세요' } } }, computed: { langLink: function () { if (provider == "raku-ichiban") { return 'http://www.leyifan.com/' + (this.currentLangCode === 'chs' ? '' : this.currentLangCode) } else { // provider == "tenso" return 'http://www.tenso.com/' + this.currentLangCode + '/static/lp_shop_index' } } }, methods: { changeLang: function () { this.currentLangCode = getParcelForwardingLang(true) widget.c('Cookie').set(PARCEL_FORWARDING_LANG_COOKIE, this.currentLangCode, 365) }, open: function () { document.body.setAttribute('wovn-tenso-banner-on', '') this.opened = true }, close: function () { this.opened = false document.body.removeAttribute('wovn-tenso-banner-on') } } }) tensoBannerVue.open() this.banner = tensoBannerVue } } /** Starts initialization if there is not a wovn-ignore attribute in the <html> tag. */ function kickoffWidgetInit () { var htmlTag = document.getElementsByTagName('html')[0]; if (!htmlTag || (htmlTag.getAttribute('wovn-ignore') === null)) widget.c('Interface').start(function () { widget.c('SwapIntercom').start(); }); else if(console && console.log) console.log("WOVN DISABLED"); } kickoffWidgetInit(); }());