| // Copyright (c) 2016, Compiler Explorer Authors |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // * Redistributions of source code must retain the above copyright notice, |
| // this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| // POSSIBILITY OF SUCH DAMAGE. |
| |
| 'use strict'; |
| |
| // setup analytics before anything else so we can capture any future errors in sentry |
| var analytics = require('./analytics'); |
| |
| // eslint-disable-next-line requirejs/no-js-extension |
| require('popper.js'); |
| require('bootstrap'); |
| require('bootstrap-slider'); |
| |
| var sharing = require('./sharing'); |
| var _ = require('underscore'); |
| var cloneDeep = require('lodash.clonedeep'); |
| var $ = require('jquery'); |
| var GoldenLayout = require('golden-layout'); |
| var Components = require('./components'); |
| var url = require('./url'); |
| var clipboard = require('clipboard'); |
| var Hub = require('./hub'); |
| var Sentry = require('@sentry/browser'); |
| var settings = require('./settings'); |
| var local = require('./local'); |
| var Alert = require('./alert'); |
| var themer = require('./themes'); |
| var motd = require('./motd'); |
| var jsCookie = require('js-cookie'); |
| var SimpleCook = require('./simplecook'); |
| var History = require('./history'); |
| var HistoryWidget = require('./history-widget').HistoryWidget; |
| var presentation = require('./presentation'); |
| |
| //css |
| require('bootstrap/dist/css/bootstrap.min.css'); |
| require('golden-layout/src/css/goldenlayout-base.css'); |
| require('selectize/dist/css/selectize.bootstrap2.css'); |
| require('bootstrap-slider/dist/css/bootstrap-slider.css'); |
| require('./colours.scss'); |
| require('./explorer.scss'); |
| |
| // Check to see if the current unload is a UI reset. |
| // Forgive me the global usage here |
| var hasUIBeenReset = false; |
| var simpleCooks = new SimpleCook(); |
| var historyWidget = new HistoryWidget(); |
| |
| // Polyfill includes for IE11 - From MDN |
| if (!String.prototype.includes) { |
| String.prototype.includes = function (search, start) { |
| if (search instanceof RegExp) { |
| throw TypeError('first argument must not be a RegExp'); |
| } |
| if (start === undefined) { |
| start = 0; |
| } |
| return this.indexOf(search, start) !== -1; |
| }; |
| } |
| |
| function setupSettings(hub) { |
| var eventHub = hub.layout.eventHub; |
| var defaultSettings = { |
| defaultLanguage: hub.defaultLangId, |
| }; |
| var currentSettings = JSON.parse(local.get('settings', null)) || defaultSettings; |
| |
| function onChange(newSettings) { |
| if (currentSettings.theme !== newSettings.theme) { |
| analytics.proxy('send', { |
| hitType: 'event', |
| eventCategory: 'ThemeChange', |
| eventAction: newSettings.theme, |
| }); |
| } |
| if (currentSettings.colourScheme !== newSettings.colourScheme) { |
| analytics.proxy('send', { |
| hitType: 'event', |
| eventCategory: 'ColourSchemeChange', |
| eventAction: newSettings.colourScheme, |
| }); |
| } |
| currentSettings = newSettings; |
| local.set('settings', JSON.stringify(newSettings)); |
| eventHub.emit('settingsChange', newSettings); |
| } |
| |
| new themer.Themer(eventHub, currentSettings); |
| |
| eventHub.on('requestSettings', function () { |
| eventHub.emit('settingsChange', currentSettings); |
| }); |
| |
| var setSettings = settings($('#settings'), currentSettings, onChange, hub.subdomainLangId); |
| eventHub.on('modifySettings', function (newSettings) { |
| setSettings(_.extend(currentSettings, newSettings)); |
| }); |
| return currentSettings; |
| } |
| |
| function hasCookieConsented(options) { |
| return jsCookie.get(options.policies.cookies.key) === options.policies.cookies.hash; |
| } |
| |
| function isMobileViewer() { |
| return window.compilerExplorerOptions.mobileViewer; |
| } |
| |
| function setupButtons(options) { |
| var alertSystem = new Alert(); |
| |
| // I'd like for this to be the only function used, but it gets messy to pass the callback function around, |
| // so we instead trigger a click here when we want it to open with this effect. Sorry! |
| if (options.policies.privacy.enabled) { |
| $('#privacy').click(function (event, data) { |
| var modal = alertSystem.alert( |
| data && data.title ? data.title : 'Privacy policy', |
| require('./policies/privacy.html') |
| ); |
| var timestamp = modal.find('#changed-date'); |
| timestamp.text(new Date(timestamp.attr('datetime')).toLocaleString()); |
| // I can't remember why this check is here as it seems superfluous |
| if (options.policies.privacy.enabled) { |
| jsCookie.set(options.policies.privacy.key, options.policies.privacy.hash, {expires: 365}); |
| } |
| }); |
| } |
| |
| if (options.policies.cookies.enabled) { |
| var getCookieTitle = function () { |
| return 'Cookies & related technologies policy<br><p>Current consent status: <span style="color:' + |
| (hasCookieConsented(options) ? 'green' : 'red') + '">' + |
| (hasCookieConsented(options) ? 'Granted' : 'Denied') + '</span></p>'; |
| }; |
| $('#cookies').click(function () { |
| var modal = alertSystem.ask(getCookieTitle(), $(require('./policies/cookies.html')), { |
| yes: function () { |
| simpleCooks.callDoConsent.apply(simpleCooks); |
| }, |
| yesHtml: 'Consent', |
| no: function () { |
| simpleCooks.callDontConsent.apply(simpleCooks); |
| }, |
| noHtml: 'Do NOT consent', |
| }); |
| var timestamp = modal.find('#changed-date'); |
| timestamp.text(new Date(timestamp.attr('datetime')).toLocaleString()); |
| }); |
| } |
| |
| $('#ui-reset').click(function () { |
| local.remove('gl'); |
| hasUIBeenReset = true; |
| window.history.replaceState(null, null, window.httpRoot); |
| window.location.reload(); |
| }); |
| |
| $('#ui-duplicate').click(function () { |
| window.open('/', '_blank'); |
| }); |
| |
| $('#changes').click(function () { |
| alertSystem.alert('Changelog', $(require('./changelog.html'))); |
| }); |
| |
| $('#ces').click(function () { |
| $.get(window.location.origin + window.httpRoot + 'bits/sponsors.html') |
| .done(function (data) { |
| alertSystem.alert('Compiler Explorer Sponsors', data); |
| analytics.proxy('send', { |
| hitType: 'event', |
| eventCategory: 'Sponsors', |
| eventAction: 'open', |
| }); |
| }) |
| .fail(function (err) { |
| var result = err.responseText || JSON.stringify(err); |
| alertSystem.alert('Compiler Explorer Sponsors', |
| '<div>Unable to fetch sponsors:</div><div>' + result + '</div>'); |
| }); |
| }); |
| |
| $('#ui-history').click(function () { |
| historyWidget.run(function (data) { |
| local.set('gl', JSON.stringify(data.config)); |
| hasUIBeenReset = true; |
| window.history.replaceState(null, null, window.httpRoot); |
| window.location.reload(); |
| }); |
| |
| $('#history').modal(); |
| }); |
| |
| if (isMobileViewer() && window.compilerExplorerOptions.slides && window.compilerExplorerOptions.slides.length > 1) { |
| $('#share').remove(); |
| $('.ui-presentation-control').removeClass('d-none'); |
| $('.ui-presentation-first').click(presentation.first); |
| $('.ui-presentation-prev').click(presentation.prev); |
| $('.ui-presentation-next').click(presentation.next); |
| } |
| } |
| |
| function findConfig(defaultConfig, options) { |
| var config = null; |
| if (!options.embedded) { |
| if (options.slides) { |
| presentation.init(window.compilerExplorerOptions.slides.length); |
| var currentSlide = presentation.getCurrentSlide(); |
| if (currentSlide < options.slides.length) { |
| config = options.slides[currentSlide]; |
| } else { |
| presentation.setCurrentSlide(0); |
| config = options.slides[0]; |
| } |
| } else { |
| if (options.config) { |
| config = options.config; |
| } else { |
| config = url.deserialiseState(window.location.hash.substr(1)); |
| } |
| |
| if (config) { |
| // replace anything in the default config with that from the hash |
| config = _.extend(defaultConfig, config); |
| } |
| if (!config) { |
| var savedState = local.get('gl', null); |
| config = savedState !== null ? JSON.parse(savedState) : defaultConfig; |
| } |
| } |
| } else { |
| config = _.extend(defaultConfig, { |
| settings: { |
| showMaximiseIcon: false, |
| showCloseIcon: false, |
| hasHeaders: false, |
| }, |
| }, sharing.configFromEmbedded(window.location.hash.substr(1))); |
| } |
| return config; |
| } |
| |
| function initializeResetLayoutLink() { |
| var currentUrl = document.URL; |
| if (currentUrl.includes('/z/')) { |
| $('#ui-brokenlink').attr('href', currentUrl.replace('/z/', '/resetlayout/')); |
| $('#ui-brokenlink').show(); |
| } else { |
| $('#ui-brokenlink').hide(); |
| } |
| } |
| |
| function initPolicies(options) { |
| // Ensure old cookies are removed, to avoid user confusion |
| |
| jsCookie.remove('fs_uid'); |
| jsCookie.remove('cookieconsent_status'); |
| if (options.policies.privacy.enabled && |
| options.policies.privacy.hash !== jsCookie.get(options.policies.privacy.key)) { |
| $('#privacy').trigger('click', { |
| title: 'New Privacy Policy. Please take a moment to read it', |
| }); |
| } |
| simpleCooks.onDoConsent = function () { |
| jsCookie.set(options.policies.cookies.key, options.policies.cookies.hash, {expires: 365}); |
| analytics.toggle(true); |
| }; |
| simpleCooks.onDontConsent = function () { |
| analytics.toggle(false); |
| jsCookie.set(options.policies.cookies.key, ''); |
| }; |
| simpleCooks.onHide = function () { |
| $(window).trigger('resize'); |
| }; |
| // '' means no consent. Hash match means consent of old. Null means new user! |
| var storedCookieConsent = jsCookie.get(options.policies.cookies.key); |
| if (options.policies.cookies.enabled && storedCookieConsent !== '' && |
| options.policies.cookies.hash !== storedCookieConsent) { |
| simpleCooks.show(); |
| } else if (options.policies.cookies.enabled && hasCookieConsented(options)) { |
| analytics.initialise(); |
| } |
| } |
| |
| function filterComponentState(config, keysToRemove) { |
| function filterComponentStateImpl(component) { |
| if (component.content) { |
| for (var i = 0; i < component.content.length; i++) { |
| filterComponentStateImpl(component.content[i], keysToRemove); |
| } |
| } |
| |
| if (component.componentState) { |
| Object.keys(component.componentState) |
| .filter(function (key) { return keysToRemove.includes(key); }) |
| .forEach(function (key) { delete component.componentState[key]; }); |
| } |
| } |
| |
| config = cloneDeep(config); |
| filterComponentStateImpl(config); |
| return config; |
| } |
| |
| /* |
| * this nonsense works around a bug in goldenlayout where a config can be generated |
| * that contains a flag indicating there is a maximized item which does not correspond |
| * to any items that actually exist in the config. |
| * |
| * See https://github.com/compiler-explorer/compiler-explorer/issues/2056 |
| */ |
| function removeOrphanedMaximisedItemFromConfig(config) { |
| // nothing to do if the maximised item id is not set |
| if (config.maximisedItemId !== '__glMaximised') return; |
| |
| var found = false; |
| function impl(component) { |
| if (component.id === '__glMaximised') { |
| found = true; |
| return; |
| } |
| |
| if (component.content) { |
| for (var i = 0; i < component.content.length; i++) { |
| impl(component.content[i]); |
| if (found) return; |
| } |
| } |
| } |
| |
| impl(config); |
| |
| if (!found) { |
| config.maximisedItemId = null; |
| } |
| } |
| |
| // eslint-disable-next-line max-statements |
| function start() { |
| initializeResetLayoutLink(); |
| |
| var options = require('options'); |
| |
| var hostnameParts = window.location.hostname.split('.'); |
| var subLangId = undefined; |
| // Only set the subdomain lang id if it makes sense to do so |
| if (hostnameParts.length > 0) { |
| var subdomainPart = hostnameParts[0]; |
| var langBySubdomain = _.find(options.languages, function (lang) { |
| return lang.id === subdomainPart || lang.alias.indexOf(subdomainPart) !== -1; |
| }); |
| if (langBySubdomain) { |
| subLangId = langBySubdomain.id; |
| } |
| } |
| var defaultLangId = subLangId; |
| if (!defaultLangId) { |
| if (options.languages['c++']) { |
| defaultLangId = 'c++'; |
| } else { |
| defaultLangId = _.keys(options.languages)[0]; |
| } |
| } |
| |
| // Cookie domains are matched as a RE against the window location. This allows a flexible |
| // way that works across multiple domains (e.g. godbolt.org and compiler-explorer.com). |
| // We allow this to be configurable so that (for example), gcc.godbolt.org and d.godbolt.org |
| // share the same cookie domain for some settings. |
| var cookieDomain = new RegExp(options.cookieDomainRe).exec(window.location.hostname); |
| if (cookieDomain && cookieDomain[0]) { |
| cookieDomain = cookieDomain[0]; |
| jsCookie.defaults.domain = cookieDomain; |
| } |
| |
| var defaultConfig = { |
| settings: {showPopoutIcon: false}, |
| content: [{ |
| type: 'row', |
| content: [ |
| Components.getEditor(1, defaultLangId), |
| Components.getCompiler(1, defaultLangId), |
| ], |
| }], |
| }; |
| |
| $(window).bind('hashchange', function () { |
| // punt on hash events and just reload the page if there's a hash |
| if (window.location.hash.substr(1)) window.location.reload(); |
| }); |
| |
| // Which buttons act as a linkable popup |
| var linkablePopups = ['#ces', '#sponsors', '#changes', '#cookies', '#setting', '#privacy']; |
| var hashPart = linkablePopups.indexOf(window.location.hash) > -1 ? window.location.hash : null; |
| if (hashPart) { |
| window.location.hash = ''; |
| // Handle the time we renamed sponsors to ces to work around issues with blockers. |
| if (hashPart === '#sponsors') hashPart = '#ces'; |
| } |
| |
| var config = findConfig(defaultConfig, options); |
| removeOrphanedMaximisedItemFromConfig(config); |
| |
| var root = $('#root'); |
| |
| var layout; |
| var hub; |
| try { |
| layout = new GoldenLayout(config, root); |
| hub = new Hub(layout, subLangId, defaultLangId); |
| } catch (e) { |
| Sentry.captureException(e); |
| |
| if (document.URL.includes('/z/')) { |
| document.location = document.URL.replace('/z/', '/resetlayout/'); |
| } |
| |
| layout = new GoldenLayout(defaultConfig, root); |
| hub = new Hub(layout, subLangId, defaultLangId); |
| } |
| |
| var lastState = null; |
| var storedPaths = {}; // TODO maybe make this an LRU cache? |
| |
| layout.on('stateChanged', function () { |
| var config = filterComponentState(layout.toConfig(), ['selection']); |
| var stringifiedConfig = JSON.stringify(config); |
| if (stringifiedConfig !== lastState) { |
| if (storedPaths[stringifiedConfig]) { |
| window.history.replaceState(null, null, storedPaths[stringifiedConfig]); |
| } else if (window.location.pathname !== window.httpRoot) { |
| window.history.replaceState(null, null, window.httpRoot); |
| // TODO: Add this state to storedPaths, but with a upper bound on the stored state count |
| } |
| lastState = stringifiedConfig; |
| |
| History.push(stringifiedConfig); |
| } |
| if (options.embedded) { |
| var strippedToLast = window.location.pathname; |
| strippedToLast = strippedToLast.substr(0, strippedToLast.lastIndexOf('/') + 1); |
| $('a.link').attr('href', strippedToLast + '#' + url.serialiseState(config)); |
| } |
| }); |
| |
| function sizeRoot() { |
| var height = $(window).height() - (root.position().top || 0) - ($('#simplecook:visible').height() || 0); |
| root.height(height); |
| layout.updateSize(); |
| } |
| |
| $(window) |
| .resize(sizeRoot) |
| .on('beforeunload', function () { |
| // Only preserve state in localStorage in non-embedded mode. |
| var shouldSave = !window.hasUIBeenReset && !hasUIBeenReset; |
| if (!options.embedded && !isMobileViewer() && shouldSave) { |
| local.set('gl', JSON.stringify(layout.toConfig())); |
| } |
| }); |
| |
| new clipboard('.btn.clippy'); |
| |
| var settings = setupSettings(hub); |
| |
| // We assume no consent for embed users |
| if (!options.embedded) { |
| setupButtons(options); |
| } |
| |
| sharing.initShareButton($('#share'), layout, function (config, extra) { |
| window.history.pushState(null, null, extra); |
| storedPaths[JSON.stringify(config)] = extra; |
| }); |
| |
| function setupAdd(thing, func) { |
| layout.createDragSource(thing, func); |
| thing.click(function () { |
| hub.addAtRoot(func()); |
| }); |
| } |
| |
| setupAdd($('#add-editor'), function () { |
| return Components.getEditor(); |
| }); |
| setupAdd($('#add-diff'), function () { |
| return Components.getDiff(); |
| }); |
| |
| if (hashPart) { |
| var element = $(hashPart); |
| if (element) element.click(); |
| } |
| initPolicies(options); |
| |
| // Skip some steps if using embedded mode |
| if (!options.embedded) { |
| // Only fetch MOTD when not embedded. |
| motd.initialise(options.motdUrl, $('#motd'), subLangId, settings.enableCommunityAds, |
| function (data) { |
| var sendMotd = function () { |
| hub.layout.eventHub.emit('motd', data); |
| }; |
| hub.layout.eventHub.on('requestMotd', sendMotd); |
| sendMotd(); |
| }, |
| function () { |
| hub.layout.eventHub.emit('modifySettings', { |
| enableCommunityAds: false, |
| }); |
| }); |
| |
| // Don't try to update Version tree link |
| var release = window.compilerExplorerOptions.gitReleaseCommit; |
| var versionLink = 'https://github.com/compiler-explorer/compiler-explorer/'; |
| if (release) { |
| versionLink += 'tree/' + release; |
| } |
| $('#version-tree').prop('href', versionLink); |
| } |
| |
| if (options.hideEditorToolbars) { |
| $('[name="editor-btn-toolbar"]').addClass('d-none'); |
| } |
| |
| window.onSponsorClick = function (sponsor) { |
| analytics.proxy('send', { |
| hitType: 'event', |
| eventCategory: 'Sponsors', |
| eventAction: 'click', |
| eventLabel: sponsor.url, |
| transport: 'beacon', |
| }); |
| window.open(sponsor.url); |
| }; |
| |
| sizeRoot(); |
| var initialConfig = JSON.stringify(filterComponentState(layout.toConfig(), ['selection'])); |
| lastState = initialConfig; |
| storedPaths[initialConfig] = window.location.href; |
| } |
| |
| $(start); |