Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Move to Typescript #502

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dist/config.js
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ var klaroConfig = {

// You can customize the ID of the DIV element that Klaro will create
// when starting up. If undefined, Klaro will use 'klaro'.
elementID: 'klaro',
elementID: 'klaroRoot',

// You can override CSS style variables here. For IE11, Klaro will
// dynamically inject the variables into the CSS. If you still consider
2 changes: 1 addition & 1 deletion dist/configs/i18n.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
var klaroI18nConfig = {
version: 2,
cookieName: 'klaro-i18n',
elementID: 'klaro',
elementID: 'klaroRoot',
lang: 'en',
default: true,
noNotice: true,
2 changes: 1 addition & 1 deletion dist/ide.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/index.html
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@
console.debug("External tracker loaded!")
}
function showKlaro(config, modal){
var element = document.getElementById("klaro").children[0];
var element = document.getElementById("klaroRoot").children[0];
if (element !== undefined){
if (element.classList !== undefined)
element.classList.add("wiggle")
@@ -116,7 +116,7 @@
<body>
<!-- by default, klaro will be appended in the klaro div, or will create one at the end of the body if none exists.
it's recommended to have klaro at the top of your content so that screen reader users can be notified early -->
<div id="klaro"></div>
<div id="klaroRoot"></div>
<img data-hide=true data-title="We're watching you (for your own safety)" class="camera"
data-src="assets/Surveillance-camera-small.png" data-name="camera" />
<section class="hero is-info is-medium is-bold">
2 changes: 1 addition & 1 deletion dist/klaro-no-translations.js

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions dist/klaro-no-translations.js.LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
2 changes: 1 addition & 1 deletion dist/klaro.js

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions dist/klaro.js.LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
447 changes: 382 additions & 65 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -8,11 +8,6 @@
"bugs": {
"url": "https://github.com/KIProtect/klaro/issues"
},
"dependencies": {
"@babel/eslint-parser": "^7.23.10",
"sass": "^1.25.0",
"webpack-merge": "^5.10.0"
},
"description": "A simple but powerful consent manager.",
"devDependencies": {
"@babel/cli": "^7.23.9",
@@ -21,6 +16,7 @@
"@babel/plugin-proposal-object-rest-spread": "7.17.3",
"@babel/preset-env": "7.24.0",
"@babel/preset-react": "7.23.3",
"@babel/eslint-parser": "^7.23.10",
"autoprefixer": "^10.4.18",
"babel-loader": "9.1.3",
"classnames": "^2.5.1",
@@ -34,18 +30,24 @@
"mini-css-extract-plugin": "^2.8.1",
"postcss-loader": "^8.1.1",
"preact": "^10.19.6",
"sass-loader": "^14.1.1",
"style-loader": "^3.3.4",
"stylelint": "^16.2.1",
"stylelint-config-sass-guidelines": "^11.0.0",
"ts-loader": "^9.5.1",
"typescript": "^5.3.3",
"@types/react": "^18.2.61",
"@types/react-dom": "^18.2.19",
"url-loader": "^4.1.1",
"webpack": "^5.90.3",
"webpack-bundle-analyzer": "^4.10.1",
"webpack-bundle-size-analyzer": "^3.1.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.2",
"webpack-hot-middleware": "^2.26.1",
"yaml-loader": "^0.8.1"
"yaml-loader": "^0.8.1",
"sass": "^1.71.1",
"sass-loader": "^14.0.0",
"webpack-merge": "^5.10.0"
},
"files": [
"*.js",
@@ -82,5 +84,5 @@
"make-dev": "cross-env APP_ENV=dev webpack serve --mode development --config webpack.config.js",
"make-watch": "cross-env APP_ENV=dev webpack --mode development --watch --config webpack.config.js"
},
"version": "0.7.22"
}
"version": "0.7.18"
}
1 change: 0 additions & 1 deletion src/components/ide/globals.jsx
Original file line number Diff line number Diff line change
@@ -13,7 +13,6 @@ export const Globals = ({ config, disabled, controls, updateConfig, t }) => {
updateConfig={updateConfig}
config={config}
t={t}
key={globalField.name}
field={globalField}
{...(globalField.controlProps || {})}
/>
1 change: 0 additions & 1 deletion src/components/ide/services.jsx
Original file line number Diff line number Diff line change
@@ -61,7 +61,6 @@ export const ServiceConfig = ({ service, setState, updateServiceName, disabled,
updateConfig={updateServiceConfig}
config={service}
t={t}
key={serviceField.name}
field={serviceField}
{...(serviceField.controlProps || {})}
/>
344 changes: 0 additions & 344 deletions src/lib.js

This file was deleted.

355 changes: 355 additions & 0 deletions src/lib.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
import React from 'react';
import App from './components/app';
import ContextualConsentNotice from './components/contextual-consent-notice';
import ConsentManager from './consent-manager';
import KlaroApi from './utils/api';
import { injectStyles } from './utils/styling';
import { createRoot } from 'react-dom/client';
import { convertToMap, update } from './utils/maps';
import { t, language } from './utils/i18n';
import { themes } from './themes';
import { currentScript, dataset, applyDataset } from './utils/compat';
export { update as updateConfig } from './utils/config';
import './scss/klaro.scss';

let defaultConfig: any;
const defaultTranslations = new Map([]);
const eventHandlers: { [key: string]: Function[] } = {};
const events: { [key: string]: any[][] } = {};

export interface Translations {
apps: any;
services: any;
}

export interface Config {
elementID?: string;
translations?: Translations;
services?: Service[];
apps?: Service[];
fallbackLang?: string;
[key: string]: any;
}

export interface Service {
name: string;
[key: string]: any;
}

export function getElementID(config: Config, ide?: boolean): string {
return (config.elementID || 'klaro') + (ide ? '-ide' : '');
}

export function getElement(config: Config, ide?: boolean): HTMLElement {
const id = getElementID(config, ide);
let element = document.getElementById(id);
if (element === null) {
element = document.createElement('div');
element.id = id;
document.body.appendChild(element);
}
return element as HTMLElement;
}

export function addEventListener(eventType: string, handler: (...args: any[]) => boolean | void): void {
if (eventHandlers[eventType] === undefined) eventHandlers[eventType] = [handler];
else eventHandlers[eventType].push(handler);
if (events[eventType] !== undefined)
for (const event of events[eventType]) if (handler(...event) === false) break;
}

function executeEventHandlers(eventType: string, ...args: any[]): boolean | void {
const handlers = eventHandlers[eventType];
if (events[eventType] === undefined) events[eventType] = [args];
else events[eventType].push(args);
if (handlers !== undefined)
for (const handler of handlers) {
if (handler(...args) === true) return true;
}
}

const roots: any[] = [];
function getRoot(element: any): any {
let root: any;
for(const existingRoot of roots){
if (existingRoot.element === element)
return existingRoot.root;
}
root = createRoot(element)
roots.push({root: root, element: element})
return root;
}

export function getConfigTranslations(config: Config): Map<any, any> {
const trans = new Map([]);
update(trans, defaultTranslations);
update(trans, convertToMap(config.translations || {}));
return trans;
}


let cnt = 1;
export function render(config: Config, opts: any = {}): React.Component | void {

if (config === undefined) return;
config = validateConfig(config);

executeEventHandlers('render', config, opts);

let showCnt = 0;
if (opts.show) showCnt = cnt++;
const element = getElement(config);
const manager = getManager(config);

if (opts.api !== undefined) manager.watch(opts.api);

injectStyles(config, themes, element);

const lang = language(config);
const configTranslations = getConfigTranslations(config);
const tt = (...args: any[]) => t(configTranslations, lang, config.fallbackLang || 'zz', ...args);

const root = getRoot(element)
const app = root.render(
<App
t={tt}
lang={lang}
manager={manager}
config={config}
testing={opts.testing}
modal={opts.modal}
api={opts.api}
show={showCnt}
/>);


renderContextualConsentNotices(manager, tt, lang, config, opts);
return app;
}

export function renderContextualConsentNotices(
manager: any,
tt: (...args: any[]) => string,
lang: string,
config: Config,
opts: any
) {
for (const service of config.services || []) {
const consent = manager.getConsent(service.name) && manager.confirmed;
const elements = document.querySelectorAll("[data-name='" + service.name + "']");
for(let i=0;i < elements.length; i++){
const element = elements[i];
const ds: any = dataset(element as HTMLElement);
if (ds.type === 'placeholder') continue;
if (element.tagName === 'IFRAME' || element.tagName === 'DIV') {
let placeholderElement = element.previousElementSibling as HTMLElement | null;
if (placeholderElement !== null) {
const ds: any = dataset(placeholderElement);
if (ds.type !== 'placeholder' || ds.name !== service.name) placeholderElement = null;
}
if (placeholderElement === null) {
placeholderElement = document.createElement('DIV');
placeholderElement.style.maxWidth = element.getAttribute('width') + 'px';
placeholderElement.style.height = element.getAttribute('height') + 'px';
applyDataset({ type: 'placeholder', name: service.name }, placeholderElement);
if (consent) placeholderElement.style.display = 'none';
element.parentElement!.insertBefore(placeholderElement, element);
const root = getRoot(placeholderElement);
const notice = root.render(
<ContextualConsentNotice
t={tt}
lang={lang}
manager={manager}
config={config}
service={service}
style={ds.style}
/>);
}
if (element instanceof HTMLIFrameElement) {
ds['src'] = element.src;
}
if (ds['modified-by-klaro'] === undefined && (element.getAttribute('style')! as any || {}).display === undefined)
ds['original-display'] = (element.getAttribute('style')! as any || {}).display;
ds['modified-by-klaro'] = 'yes';
applyDataset(ds, element as HTMLElement);
if (!consent) {
element.setAttribute('src', '');
element.setAttribute('style', 'display: none;');
}
}
}
}
}

function showKlaroIDE(script: HTMLScriptElement): void {
const baseName = /^(.*)(\/[^/]+)$/.exec(script.src)?.[1] || '';
const element = document.createElement('script');
element.src = baseName !== '' ? baseName + '/ide.js' : 'ide.js';
element.type = "application/javascript";
document.head.appendChild(element);
}

function doOnceLoaded(handler: () => void): void {
if (/complete|interactive|loaded/.test(document.readyState)) {
handler();
} else {
window.addEventListener('DOMContentLoaded', handler);
}
}

function getKlaroId(script: HTMLScriptElement): string | null {
const klaroId = script.getAttribute('data-klaro-id');
if (klaroId !== null) return klaroId;
const regexMatch = /.*\/privacy-managers\/([a-f0-9]+)\/klaro.*\.js/.exec(script.src);
if (regexMatch !== null) return regexMatch[1];
return null;
}

function getKlaroApiUrl(script: HTMLScriptElement): string | null {
const klaroApiUrl = script.getAttribute('data-klaro-api-url');
if (klaroApiUrl !== null) return klaroApiUrl;
const regexMatch = /(http(?:s)?:\/\/[^/]+)\/v1\/privacy-managers\/([a-f0-9]+)\/klaro.*\.js/.exec(script.src);
if (regexMatch !== null) return regexMatch[1];
return null;
}

function getKlaroConfigName(hashParams: Map<string, string | boolean>, script: HTMLScriptElement): string {
if (hashParams.has('klaro-config')) {
return hashParams.get('klaro-config')! as string;
}
const klaroConfigName = script.getAttribute('data-klaro-config');
if (klaroConfigName !== null) return klaroConfigName;
return 'default';
}

function getHashParams(): Map<string, string | boolean> {
return new Map(decodeURI(location.hash.slice(1)).split("&").map(kv => kv.split("=")).map((kv) : [string, string | boolean] => (kv.length === 1 ? [kv[0], true] : [kv[0], kv[1]])));
}

export function validateConfig(config: Config): Config {
const validatedConfig: Config = { ...config };
if (validatedConfig.version === 2) return validatedConfig;
if (validatedConfig.apps !== undefined && validatedConfig.services === undefined) {
validatedConfig.services = validatedConfig.apps;
console.warn("Warning, your configuration file is outdated. Please change `apps` to `services`");
delete validatedConfig.apps;
}
if (validatedConfig.translations !== undefined) {
if (validatedConfig.translations.apps !== undefined && validatedConfig.translations.services === undefined) {
validatedConfig.translations.services = validatedConfig.translations.apps;
console.warn("Warning, your configuration file is outdated. Please change `apps` to `services` in the `translations` key");
delete validatedConfig.translations.apps;
}
}
return validatedConfig;
}

export function setup(config?: Config): void {
if (typeof window === 'undefined') return;
const script = currentScript("klaro") as HTMLScriptElement;
if (!script) return;


const hashParams = getHashParams();
const testing = hashParams.get('klaro-testing') === 'true';

const initialize = (opts: any) => {
const fullOpts = { ...opts, testing };
if (!defaultConfig.noAutoLoad && (!defaultConfig.testing || fullOpts.testing)) render(defaultConfig, fullOpts);
};


if (config !== undefined) {
defaultConfig = config;
doOnceLoaded(() => initialize({}));
} else if (script !== null) {
const klaroId = getKlaroId(script);
const klaroApiUrl = getKlaroApiUrl(script);
const klaroConfigName = getKlaroConfigName(hashParams, script);

if (klaroId !== null){
// we initialize with an API backend
const api = new KlaroApi(klaroApiUrl, klaroId, {testing: testing})
if ((window as any).klaroApiConfigs !== undefined){
// the configs were already supplied with the Klaro binary

if (executeEventHandlers("apiConfigsLoaded", (window as any).klaroApiConfigs, api) === true){
return
}

const config: any = (window as any).klaroApiConfigs.find((config: any): boolean => config.name === klaroConfigName && (config.status === 'active' || testing))

if (config !== undefined){
defaultConfig = config
doOnceLoaded(() => initialize({api: api}))
} else {
executeEventHandlers("apiConfigsFailed", {})
}

} else {
// we load the configs separately...
api.loadConfig(klaroConfigName).then((config) => {

// an event handler can interrupt the initialization, e.g. if it wants to perform
// its own initialization given the API configs
if (executeEventHandlers("apiConfigsLoaded", [config], api) === true){
return
}
defaultConfig = config
doOnceLoaded(() => initialize({api: api}))

}).catch((err) => {
console.error(err, "cannot load Klaro configs")
executeEventHandlers("apiConfigsFailed", err)
})
}
} else {
// we initialize with a local config instead
const configName = script.getAttribute('data-klaro-config') || "klaroConfig"
defaultConfig = (window as any)[configName];
if (defaultConfig !== undefined)
doOnceLoaded(() => initialize({}))
}
}

if (hashParams.has('klaro-ide')) {
showKlaroIDE(script);
}
}

export function show(config?: Config, modal?: boolean, api?: any): boolean {
config = config || defaultConfig;
render(config!, { show: true, modal, api });
return false;
}

// Consent Managers
interface Managers {
[key: string]: ConsentManager;
}

const managers: Managers = {};

export function resetManagers(): void {
for (const key of Object.keys(managers)) {
delete managers[key];
}
}

export function getManager(config?: Config): ConsentManager {
if (config === undefined)
config = defaultConfig!;
const name = config!.storageName || config!.cookieName || 'default'; // deprecated: cookieName
if (!managers[name]) {
managers[name] = new ConsentManager(validateConfig(config!));
}
return managers[name];
}

declare var VERSION: string;

export function version(): string {
return VERSION[0] === 'v' ? VERSION.slice(1) : VERSION;
}

export { language, defaultConfig, defaultTranslations };
18 changes: 18 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "es6",
"target": "es5",
"lib": [
"es6",
"dom"
],
"jsx": "react",
"strict": true,
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
}
}
7 changes: 6 additions & 1 deletion webpack.base.js
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ let config = {
devtool: 'inline-source-map',
resolve: {
symlinks: false,
extensions: ['.jsx', '.js'],
extensions: ['.jsx', '.js', '.tsx', '.ts'],
modules: [SRC_DIR, 'node_modules'],
alias: {
react: 'preact/compat',
@@ -38,6 +38,11 @@ let config = {
exclude: /node_modules/,
include: [SRC_DIR],
loader: 'babel-loader',
},
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}
],
},
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
@@ -12,4 +12,4 @@ module.exports = (env, argv) => {
default:
throw new Error('No matching configuration was found!');
}
};
};