Skip to content

Commit

Permalink
Merge pull request #801 from City-of-Helsinki/UHF-10464
Browse files Browse the repository at this point in the history
UHF-10464 Cookie banner Admin UI
  • Loading branch information
Arkkimaagi authored Sep 5, 2024
2 parents 95764bf + 019b6c8 commit 32c796a
Show file tree
Hide file tree
Showing 10 changed files with 1,748 additions and 1 deletion.
1 change: 1 addition & 0 deletions modules/hdbt_cookie_banner/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
73 changes: 73 additions & 0 deletions modules/hdbt_cookie_banner/assets/css/cookie-banner-admin-ui.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* Set max width to form */
.hdbt-cookie-banner {
max-width: 1200px;
}

/* Not all form controls should be 100% wide, for example select elements are better at auto */
.form-control {
width: auto;
}

/* Only inputs should be 100% wide */
input.form-control {
width: 100%;
}

/* Hide unused cruft */
.json-editor-btntype-deletelast,
.json-editor-btntype-deleteall,
.h3.je-object__title:has([style*="display: none;"]+.sr-only),
.btn-group.je-object__controls {
display: none;
}

/* First level wells should not have grey background */
[data-schemapath="root"]>.well {
background: transparent;
border: 0 none;
padding: 0;
box-shadow: none;
}

/* Handle button width with grandparent grid that is inherited with subgrid */
div:has( > .je-object__container) {
display: grid;
grid-template-columns: [column-1] 1fr [column-2] auto;
}

/* Add separator line between elements */
.je-object__container + .je-object__container {
border-top: 1px solid #ccc;
}

/* Inherit the grid from grandparent and set grid rows here */
:not([data-schemaid="root"]).je-object__container {
grid-column: span 2;
display: grid;
grid-template-columns: subgrid;
grid-template-rows: [row-1] 1fr [row-2] auto;
}

/* By default, take two columns on all elements */
.je-object__container > * {
grid-column: 1 / span 2;
}

/* Title should be 1 column wide */
.je-object__container > .je-object__title {
grid-column: 1 / span 1;
grid-row: 1;
}

/* Btn group should be 1 column wide and on the first row */
.je-object__container > .btn-group {
grid-column: 2 / span 1;
grid-row: 1;
margin-top: 18px;
}

/* JSON Textarea size */
textarea {
width: 100%;
height: 90dvh;
}
207 changes: 207 additions & 0 deletions modules/hdbt_cookie_banner/assets/js/cookie-banner-admin-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
(function (Drupal, drupalSettings) {
Drupal.behaviors.cookieBannerAdminUi = {
attach: function attach() {

// Small schema to handle languages
const languageSchema = {
"title": "Supported languages",
"description": "List all languages you wish banner to support.",
"type": "array",
"format": "table",
"options": {
"disable_collapse": true
},
"items": {
"type": "object",
"properties": {
"code": {
"type": "string",
"minLength": 2,
"title": "Code (eg. \"fi\")"
},
"name": {
"type": "string",
"minLength": 1,
"title": "Name (ex. \"Finnish\")"
}
},
"required": [
"code",
"name"
],
"title": "Language"
},
"uniqueItems": true,
"minItems": 1
}
const defaultLanguages = [
{ "code": "fi", "name": "Finnish" },
{ "code": "sv", "name": "Swedish" },
{ "code": "en", "name": "English" }
];

// JSON editor options for both forms
const editorOptions = {
theme: 'bootstrap3',
iconlib: 'bootstrap',
show_opt_in: true,
disable_edit_json: true,
disable_properties: true,
disable_array_delete_all_rows: true,
disable_array_delete_last_row: true,
keep_oneof_values: false,
prompt_before_delete: true,
}

/**
* Gets the schema object for site settings
* @returns {Promise} A promise that resolves with the schema object
*/
function getSchema(){
try {
const schema = JSON.parse(drupalSettings.cookieBannerAdminUi.siteSettingsSchema);
return schema;
} catch (error) {
console.error('Error fetching the schema:', error);
}
return {};
}

/**
* Initializes the language editor and returns a reference to it
* @param {object} defaultLanguages that contains code and name for each language
* @param {object} languageSchema JSON schema for the language editor
* @param {object} editorOptions for the JSON editor
* @returns reference to the language editor
*/
function initializeLanguageEditor(defaultLanguages, languageSchema, editorOptions){
const langOptions = {
...editorOptions,
schema: languageSchema,
startval: defaultLanguages
};

const languageElement = document.getElementById('language_holder');
const languageEditor = new JSONEditor(languageElement, langOptions);
return languageEditor;
}

/**
* Initializes the banner editor and returns a reference to it
* @param {object} schema JSON schema of siteSettings.json for the banner editor
* @param {object} editorOptions for the JSON editor
* @returns reference to the banner editor
*/
function initializeBannerEditor(schema, editorOptions){
let isUpdatingFromEditor = false; // Flag to prevent loop
let startval = {};
let textarea = null;
try {
textarea = document.getElementById('edit-site-settings');
startval = JSON.parse(textarea.value);
} catch (error) {
console.error('Error parsing the textarea value:', error);
}

const bannerElement = document.getElementById('editor_holder');
const bannerEditor = new JSONEditor(bannerElement, {
...editorOptions,
schema,
startval
});

// Listen for changes in the JSON editor
bannerEditor.on('change', function() {
if (!isUpdatingFromEditor) {
const updatedData = bannerEditor.getValue();
textarea.value = JSON.stringify(updatedData, null, 2);
}
});

// Listen for manual changes in the textarea
textarea.addEventListener('input', function() {
try {
const updatedTextareaData = JSON.parse(textarea.value);

// Prevent triggering the editor change event
isUpdatingFromEditor = true;
bannerEditor.setValue(updatedTextareaData);
isUpdatingFromEditor = false;
} catch (e) {
console.error('Invalid JSON in textarea:', e);
}
});

return bannerEditor;
}

/**
* Updates the schema with the new languages
* @param {object} languages JSON generated by the language editor
* @param {object} schema JSON schema of siteSettings.json
* @returns {object} updated schema with the new languages
*/
function updateSchema(languages, schema){
const newSchema = schema;

const localisedText = {
"type": "object",
"title": "Localised text",
"properties": {},
"required": [],
"additionalProperties": false
};

const fallbackLanguageEnum = languages.map(lang => lang.code);
const fallbackLanguageEnumTitles = languages.map(lang => lang.code + " (" + lang.name + ")");

languages.forEach(lang => {
localisedText.properties[lang.code] = {
"type": "string",
"title": lang.code + " (" + lang.name + ")"
};
localisedText.required.push(lang.code);
});

newSchema["$defs"].LocalisedText = localisedText;
newSchema.properties.fallbackLanguage.enum = fallbackLanguageEnum;
newSchema.properties.fallbackLanguage.options.enum_titles = fallbackLanguageEnumTitles;

return newSchema;
}

/**
* Initializes the language and banner editors
*/
async function initializeEditor(){
const languageEditor = initializeLanguageEditor(defaultLanguages, languageSchema, editorOptions);
let schema = getSchema();
let bannerEditor = initializeBannerEditor(schema, editorOptions);

const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(null, args);
}, delay);
};
};

languageEditor.on('change', debounce(function() {
const errors = languageEditor.validate();
if (!errors.length) {
const languages = languageEditor.getValue();
schema = updateSchema(languages, schema);
bannerEditor.destroy();
bannerEditor = initializeBannerEditor(schema, editorOptions);
}
}, 300));
}

// Initialize the editor once the page has loaded
window.onload = initializeEditor;

}
};
})(Drupal, drupalSettings);
2 changes: 2 additions & 0 deletions modules/hdbt_cookie_banner/assets/js/jsoneditor.js

Large diffs are not rendered by default.

Loading

0 comments on commit 32c796a

Please sign in to comment.