Merge pull request #617 from keepassxreboot/fix/icon_handling_refactor

Fix handling multiple icons
This commit is contained in:
Sami Vänttinen 2019-11-10 08:02:26 +02:00 committed by GitHub
commit b3ebe63c92
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 203 additions and 188 deletions

View file

@ -60,7 +60,7 @@ browser.runtime.onMessage.addListener(async function(req, sender) {
kpxc.settings = response;
kpxc.initCredentialFields(true);
} else if (req.action === 'show_password_generator') {
kpxcPassword.trigger();
kpxcPasswordDialog.trigger();
} else if (req.action === 'add_username_only_option') {
kpxc.addToSitePreferences();
}
@ -448,7 +448,7 @@ kpxcFields.getUsernameField = function(passwordId, checkDisabled) {
}
if (kpxc.settings.showLoginFormIcon) {
kpxcUsernameField.initField(usernameField);
kpxcUsernameFields.newIcon(usernameField);
}
usernameField = i;
}
@ -506,8 +506,7 @@ kpxcFields.getPasswordField = function(usernameId, checkDisabled) {
passwordField = null;
}
kpxcPassword.init(kpxc.settings.usePasswordGeneratorIcons);
kpxcPassword.initField(passwordField);
kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, passwordField);
} else {
// Search all inputs on page
const inputs = kpxcFields.getAllFields();
@ -564,7 +563,7 @@ kpxcFields.prepareCombinations = async function(combinations) {
});
if (kpxc.settings.showLoginFormIcon) {
kpxcUsernameField.initField(usernameField, res.databaseClosed);
kpxcUsernameFields.newIcon(usernameField, res.databaseClosed);
}
// Initialize form-submit for remembering credentials
@ -856,7 +855,7 @@ kpxc.clearAllFromPage = function() {
// Switch credentials if database is changed or closed
kpxc.detectDatabaseChange = async function(response) {
kpxc.clearAllFromPage();
kpxcUsernameField.switchIcon(true);
kpxcUsernameFields.switchIcon(true);
if (document.visibilityState !== 'hidden') {
if (response.new !== '' && response.new !== response.old) {
@ -866,7 +865,7 @@ kpxc.detectDatabaseChange = async function(response) {
});
kpxc.settings = settings;
await kpxc.initCredentialFields(true);
kpxcUsernameField.switchIcon(false); // Unlocked
kpxcUsernameFields.switchIcon(false); // Unlocked
// If user has requested a manual fill through context menu the actual credential filling
// is handled here when the opened database has been regognized. It's not a pretty hack.
@ -963,11 +962,9 @@ kpxc.initCredentialFields = async function(forceCall) {
};
kpxc.initPasswordGenerator = function(inputs) {
kpxcPassword.init(kpxc.settings.usePasswordGeneratorIcons);
for (let i = 0; i < inputs.length; i++) {
if (inputs[i] && inputs[i].getLowerCaseAttribute('type') === 'password') {
kpxcPassword.initField(inputs[i], inputs, i);
kpxcPasswordIcons.newIcon(kpxc.settings.usePasswordGeneratorIcons, inputs[i], inputs, i);
}
}
};
@ -1783,7 +1780,7 @@ kpxcEvents.triggerActivatedTab = async function() {
// Update username field lock state
const state = await browser.runtime.sendMessage({ action: 'check_database_hash' });
kpxcUsernameField.switchIcon(state === '');
kpxcUsernameFields.switchIcon(state === '');
// initCredentialFields calls also "retrieve_credentials", to prevent it
// check of init() was already called

View file

@ -1,36 +1,24 @@
'use strict';
const kpxcPassword = {};
kpxcPassword.created = false;
kpxcPassword.icon = null;
kpxcPassword.inputField = null;
kpxcPassword.selected = null;
kpxcPassword.startPosX = 0;
kpxcPassword.startPosY = 0;
kpxcPassword.diffX = 0;
kpxcPassword.diffY = 0;
kpxcPassword.dialog = null;
kpxcPassword.titleBar = null;
kpxcPassword.useIcons = false;
const kpxcPasswordIcons = {};
kpxcPasswordIcons.icons = [];
try {
kpxcPassword.observer = new IntersectionObserver((entries) => {
kpxcUI.updateFromIntersectionObserver(kpxcPassword, entries);
});
} catch (err) {
console.log(err);
}
kpxcPassword.init = function(useIcons) {
kpxcPassword.useIcons = useIcons;
if ('initPasswordGenerator' in _called) {
return;
}
_called.initPasswordGenerator = true;
kpxcPasswordIcons.newIcon = function(field, databaseClosed = true) {
kpxcPasswordIcons.icons.push(new PasswordIcon(field, databaseClosed));
};
kpxcPassword.initField = function(field, inputs, pos) {
class PasswordIcon extends Icon {
constructor(useIcons, field, inputs, pos) {
super();
this.useIcons = useIcons;
this.initField(field, inputs, pos);
kpxcUI.monitorIconPosition(this);
}
};
PasswordIcon.prototype.initField = function(field, inputs, pos) {
if (!field) {
return;
}
@ -41,15 +29,15 @@ kpxcPassword.initField = function(field, inputs, pos) {
field.setAttribute('kpxc-password-generator', true);
if (kpxcPassword.useIcons) {
if (this.useIcons) {
// Observer the visibility
if (kpxcPassword.observer) {
kpxcPassword.observer.observe(field);
if (this.observer) {
this.observer.observe(field);
}
kpxcPassword.createIcon(field);
this.createIcon(field);
}
kpxcPassword.inputField = field;
this.inputField = field;
let found = false;
if (inputs) {
@ -66,7 +54,7 @@ kpxcPassword.initField = function(field, inputs, pos) {
field.setAttribute('kpxc-pwgen-next-field-exists', found);
};
kpxcPassword.createIcon = function(field) {
PasswordIcon.prototype.createIcon = function(field) {
const className = (isFirefox() ? 'key-moz' : 'key');
const size = (field.offsetHeight > 28) ? 24 : 16;
let offset = Math.floor((field.offsetHeight - size) / 3);
@ -90,15 +78,27 @@ kpxcPassword.createIcon = function(field) {
}
e.preventDefault();
kpxcPassword.showDialog(field, icon);
kpxcPasswordDialog.showDialog(field, icon);
});
kpxcUI.setIconPosition(icon, field);
kpxcPassword.icon = icon;
this.icon = icon;
document.body.appendChild(icon);
};
kpxcPassword.removeIcon = function(field) {
const kpxcPasswordDialog = {};
kpxcPasswordDialog.created = false;
kpxcPasswordDialog.icon = null;
kpxcPasswordDialog.selected = null;
kpxcPasswordDialog.startPosX = 0;
kpxcPasswordDialog.startPosY = 0;
kpxcPasswordDialog.diffX = 0;
kpxcPasswordDialog.diffY = 0;
kpxcPasswordDialog.dialog = null;
kpxcPasswordDialog.titleBar = null;
kpxcPasswordDialog.removeIcon = function(field) {
if (field.getAttribute('kpxc-password-generator')) {
const pwgenIcons = document.querySelectorAll('.kpxc-pwgen-icon');
for (const i of pwgenIcons) {
@ -109,16 +109,16 @@ kpxcPassword.removeIcon = function(field) {
}
};
kpxcPassword.createDialog = function() {
if (kpxcPassword.created) {
kpxcPasswordDialog.createDialog = function() {
if (kpxcPasswordDialog.created) {
// If database is open again, generate a new password right away
const input = $('.kpxc-pwgen-input');
if (input.style.display === 'none') {
kpxcPassword.generate();
kpxcPasswordDialog.generate();
}
return;
}
kpxcPassword.created = true;
kpxcPasswordDialog.created = true;
const wrapper = kpxcUI.createElement('div', 'kpxc');
const dialog = kpxcUI.createElement('div', 'kpxc kpxc-pwgen-dialog');
@ -129,7 +129,7 @@ kpxcPassword.createDialog = function() {
return;
}
kpxcPassword.openDialog();
kpxcPasswordDialog.openDialog();
});
titleBar.append(closeButton);
@ -144,16 +144,16 @@ kpxcPassword.createDialog = function() {
const fillButton = kpxcUI.createElement('button', 'kpxc-button kpxc-green-button', { 'id': 'kpxc-pwgen-btn-fill' }, tr('passwordGeneratorFill'));
generateButton.addEventListener('click', function(e) {
kpxcPassword.generate(e);
kpxcPasswordDialog.generate(e);
});
fillButton.addEventListener('click', function(e) {
kpxcPassword.fill(e);
kpxcPassword.openDialog();
kpxcPasswordDialog.fill(e);
kpxcPasswordDialog.openDialog();
});
copyButton.addEventListener('click', function(e) {
kpxcPassword.copy(e);
kpxcPasswordDialog.copy(e);
});
buttonsRow.appendMultiple(generateButton, copyButton, fillButton);
@ -172,60 +172,60 @@ kpxcPassword.createDialog = function() {
document.body.append(wrapper);
kpxcPassword.dialog = dialog;
kpxcPassword.titleBar = titleBar;
kpxcPassword.titleBar.addEventListener('mousedown', function(e) {
kpxcPassword.mouseDown(e);
kpxcPasswordDialog.dialog = dialog;
kpxcPasswordDialog.titleBar = titleBar;
kpxcPasswordDialog.titleBar.addEventListener('mousedown', function(e) {
kpxcPasswordDialog.mouseDown(e);
});
kpxcPassword.generate();
kpxcPasswordDialog.generate();
};
kpxcPassword.mouseDown = function(e) {
kpxcPassword.selected = kpxcPassword.titleBar;
kpxcPassword.startPosX = e.clientX;
kpxcPassword.startPosY = e.clientY;
kpxcPassword.diffX = kpxcPassword.startPosX - kpxcPassword.dialog.offsetLeft;
kpxcPassword.diffY = kpxcPassword.startPosY - kpxcPassword.dialog.offsetTop;
kpxcPasswordDialog.mouseDown = function(e) {
kpxcPasswordDialog.selected = kpxcPasswordDialog.titleBar;
kpxcPasswordDialog.startPosX = e.clientX;
kpxcPasswordDialog.startPosY = e.clientY;
kpxcPasswordDialog.diffX = kpxcPasswordDialog.startPosX - kpxcPasswordDialog.dialog.offsetLeft;
kpxcPasswordDialog.diffY = kpxcPasswordDialog.startPosY - kpxcPasswordDialog.dialog.offsetTop;
return false;
};
kpxcPassword.openDialog = function() {
if (kpxcPassword.dialog.style.display === '' || kpxcPassword.dialog.style.display === 'none') {
kpxcPassword.dialog.style.display = 'block';
kpxcPasswordDialog.openDialog = function() {
if (kpxcPasswordDialog.dialog.style.display === '' || kpxcPasswordDialog.dialog.style.display === 'none') {
kpxcPasswordDialog.dialog.style.display = 'block';
} else {
kpxcPassword.dialog.style.display = 'none';
kpxcPasswordDialog.dialog.style.display = 'none';
}
};
kpxcPassword.trigger = function() {
kpxcPassword.showDialog(kpxcPassword.inputField || document.activeElement, kpxcPassword.icon);
kpxcPasswordDialog.trigger = function() {
kpxcPasswordDialog.showDialog(document.activeElement, kpxcPasswordDialog.icon);
};
kpxcPassword.showDialog = function(field, icon) {
kpxcPasswordDialog.showDialog = function(field, icon) {
if (!kpxcFields.isVisible(field)) {
document.body.removeChild(icon);
field.removeAttribute('kpxc-password-generator');
return;
}
kpxcPassword.createDialog();
kpxcPassword.openDialog();
kpxcPasswordDialog.createDialog();
kpxcPasswordDialog.openDialog();
// Adjust the dialog location
if (kpxcPassword.dialog) {
if (kpxcPasswordDialog.dialog) {
if (icon) {
kpxcPassword.dialog.style.top = Pixels(icon.offsetTop + icon.offsetHeight);
kpxcPassword.dialog.style.left = icon.style.left;
kpxcPasswordDialog.dialog.style.top = Pixels(icon.offsetTop + icon.offsetHeight);
kpxcPasswordDialog.dialog.style.left = icon.style.left;
} else {
const rect = document.activeElement.getBoundingClientRect();
kpxcPassword.dialog.style.top = Pixels(rect.top + rect.height);
kpxcPassword.dialog.style.left = Pixels(rect.left);
kpxcPasswordDialog.dialog.style.top = Pixels(rect.top + rect.height);
kpxcPasswordDialog.dialog.style.left = Pixels(rect.left);
}
kpxcPassword.dialog.setAttribute('kpxc-pwgen-field-id', field.getAttribute('data-kpxc-id'));
kpxcPassword.dialog.setAttribute('kpxc-pwgen-next-field-id', field.getAttribute('kpxc-pwgen-next-field-id'));
kpxcPassword.dialog.setAttribute('kpxc-pwgen-next-is-password-field', field.getAttribute('kpxc-pwgen-next-is-password-field'));
kpxcPasswordDialog.dialog.setAttribute('kpxc-pwgen-field-id', field.getAttribute('data-kpxc-id'));
kpxcPasswordDialog.dialog.setAttribute('kpxc-pwgen-next-field-id', field.getAttribute('kpxc-pwgen-next-field-id'));
kpxcPasswordDialog.dialog.setAttribute('kpxc-pwgen-next-is-password-field', field.getAttribute('kpxc-pwgen-next-is-password-field'));
const fieldExists = Boolean(field.getAttribute('kpxc-pwgen-next-field-exists'));
const checkbox = $('.kpxc-pwgen-checkbox');
@ -240,7 +240,7 @@ kpxcPassword.showDialog = function(field, icon) {
}
};
kpxcPassword.generate = async function(e) {
kpxcPasswordDialog.generate = async function(e) {
// This function can be also called from non-events
if (e) {
if (!e.isTrusted) {
@ -254,16 +254,16 @@ kpxcPassword.generate = async function(e) {
}));
};
kpxcPassword.copy = function(e) {
kpxcPasswordDialog.copy = function(e) {
if (!e.isTrusted) {
return;
}
e.preventDefault();
kpxcPassword.copyPasswordToClipboard();
kpxcPasswordDialog.copyPasswordToClipboard();
};
kpxcPassword.fill = function(e) {
kpxcPasswordDialog.fill = function(e) {
if (!e.isTrusted) {
return;
}
@ -271,7 +271,7 @@ kpxcPassword.fill = function(e) {
e.preventDefault();
// Use the active input field
const field = _f(kpxcPassword.dialog.getAttribute('kpxc-pwgen-field-id'));
const field = _f(kpxcPasswordDialog.dialog.getAttribute('kpxc-pwgen-field-id'));
if (field) {
const password = $('.kpxc-pwgen-input');
if (field.getAttribute('maxlength')) {
@ -296,7 +296,7 @@ kpxcPassword.fill = function(e) {
}
};
kpxcPassword.copyPasswordToClipboard = function() {
kpxcPasswordDialog.copyPasswordToClipboard = function() {
$('.kpxc-pwgen-input').select();
try {
return document.execCommand('copy');
@ -344,13 +344,3 @@ const disableButtons = function() {
$('#kpxc-pwgen-btn-copy').style.display = 'none';
$('#kpxc-pwgen-btn-fill').style.display = 'none';
};
// Handle icon position on window resize
window.addEventListener('resize', function(e) {
kpxcUI.updateIconPosition(kpxcPassword);
});
// Handle icon position on scroll
window.addEventListener('scroll', function(e) {
kpxcUI.updateIconPosition(kpxcPassword);
});

View file

@ -10,6 +10,19 @@ const Pixels = function(value) {
return String(value) + 'px';
};
// Basic icon class
class Icon {
constructor() {
try {
this.observer = new IntersectionObserver((entries) => {
kpxcUI.updateFromIntersectionObserver(this, entries);
});
} catch (err) {
console.log(err);
}
}
};
const kpxcUI = {};
// Wrapper for creating elements
@ -36,6 +49,18 @@ kpxcUI.createElement = function(type, classes, attributes, textContent) {
return element;
};
kpxcUI.monitorIconPosition = function(iconClass) {
// Handle icon position on resize
window.addEventListener('resize', function(e) {
kpxcUI.updateIconPosition(iconClass);
});
// Handle icon position on scroll
window.addEventListener('scroll', function(e) {
kpxcUI.updateIconPosition(iconClass);
});
};
kpxcUI.updateIconPosition = function(iconClass) {
if (iconClass.inputField && iconClass.icon) {
kpxcUI.setIconPosition(iconClass.icon, iconClass.inputField);
@ -62,6 +87,7 @@ kpxcUI.setIconPosition = function(icon, field) {
kpxcUI.updateFromIntersectionObserver = function(iconClass, entries) {
for (const entry of entries) {
const rect = DOMRectToArray(entry.boundingClientRect);
const temp = entry.target.closest('.kpxc-username-icon');
if ((entry.intersectionRatio === 0 && !entry.isIntersecting) || (rect.some(x => x < -10))) {
iconClass.icon.style.display = 'none';
@ -115,13 +141,13 @@ const DOMRectToArray = function(domRect) {
// Enables dragging
document.addEventListener('mousemove', function(e) {
if (kpxcPassword.selected === kpxcPassword.titleBar) {
const xPos = e.clientX - kpxcPassword.diffX;
const yPos = e.clientY - kpxcPassword.diffY;
if (kpxcPasswordDialog.selected === kpxcPasswordDialog.titleBar) {
const xPos = e.clientX - kpxcPasswordDialog.diffX;
const yPos = e.clientY - kpxcPasswordDialog.diffY;
if (kpxcPassword.selected !== null) {
kpxcPassword.dialog.style.left = Pixels(xPos);
kpxcPassword.dialog.style.top = Pixels(yPos);
if (kpxcPasswordDialog.selected !== null) {
kpxcPasswordDialog.dialog.style.left = Pixels(xPos);
kpxcPasswordDialog.dialog.style.top = Pixels(yPos);
}
}
@ -137,7 +163,7 @@ document.addEventListener('mousemove', function(e) {
});
document.addEventListener('mouseup', function() {
kpxcPassword.selected = null;
kpxcPasswordDialog.selected = null;
kpxcDefine.selected = null;
});

View file

@ -1,18 +1,45 @@
'use strict';
const kpxcUsernameField = {};
kpxcUsernameField.icon = null;
kpxcUsernameField.inputField = null;
const kpxcUsernameFields = {};
kpxcUsernameFields.icons = [];
try {
kpxcUsernameField.observer = new IntersectionObserver((entries) => {
kpxcUI.updateFromIntersectionObserver(kpxcUsernameField, entries);
});
} catch (err) {
console.log(err);
}
kpxcUsernameFields.newIcon = function(field, databaseClosed = true) {
kpxcUsernameFields.icons.push(new UsernameFieldIcon(field, databaseClosed));
};
kpxcUsernameField.initField = function(field, databaseClosed = true) {
kpxcUsernameFields.switchIcon = function(locked) {
kpxcUsernameFields.icons.forEach(u => u.switchIcon(locked));
};
class UsernameFieldIcon extends Icon {
constructor(field, databaseClosed = true) {
super();
this.icon = null;
this.inputField = null;
this.initField(field, databaseClosed);
kpxcUI.monitorIconPosition(this);
}
switchIcon(locked) {
if (!this.icon) {
return;
}
if (locked) {
this.icon.classList.remove(getIconClassName());
this.icon.classList.add(getIconClassName(true));
this.icon.title = tr('usernameLockedFieldText');
} else {
this.icon.classList.remove(getIconClassName(true));
this.icon.classList.add(getIconClassName());
this.icon.title = tr('usernameFieldText');
}
}
};
UsernameFieldIcon.prototype.initField = function(field, databaseClosed = true) {
if (!field || field.getAttribute('kpxc-username-field') === 'true') {
return;
}
@ -20,36 +47,18 @@ kpxcUsernameField.initField = function(field, databaseClosed = true) {
field.setAttribute('kpxc-username-field', 'true');
// Observer the visibility
if (kpxcUsernameField.observer) {
kpxcUsernameField.observer.observe(field);
if (this.observer) {
this.observer.observe(field);
}
createIcon(field, databaseClosed);
kpxcUsernameField.inputField = field;
this.createIcon(field, databaseClosed);
this.inputField = field;
};
kpxcUsernameField.switchIcon = function(locked) {
const icons = $('.kpxc-username-icon');
if (!icons || icons.length === 0) {
return;
}
const icon = icons;
if (locked) {
icon.classList.remove(getIconClassName());
icon.classList.add(getIconClassName(true));
icon.title = tr('usernameLockedFieldText');
} else {
icon.classList.remove(getIconClassName(true));
icon.classList.add(getIconClassName());
icon.title = tr('usernameFieldText');
}
};
const createIcon = function(target, databaseClosed) {
UsernameFieldIcon.prototype.createIcon = function(target, databaseClosed) {
// Remove any existing password generator icons from the input field
if (target.getAttribute('kpxc-password-generator')) {
kpxcPassword.removeIcon(target);
kpxcPasswordDialog.removeIcon(target);
}
const field = target;
@ -60,7 +69,7 @@ const createIcon = function(target, databaseClosed) {
// Don't create the icon if the input field is too small
if (field.offsetWidth < (size * 1.5) || field.offsetHeight < size) {
kpxcUsernameField.observer.unobserve(field);
this.observer.unobserve(field);
return;
}
@ -79,45 +88,48 @@ const createIcon = function(target, databaseClosed) {
icon.style.width = Pixels(size);
icon.style.height = Pixels(size);
icon.addEventListener('click', async function(e) {
icon.addEventListener('click', function(e) {
if (!e.isTrusted) {
return;
}
e.preventDefault();
if (!kpxcFields.isVisible(field)) {
document.body.removeChild(icon);
field.removeAttribute('kpxc-username-field');
return;
}
const connected = await browser.runtime.sendMessage({ action: 'is_connected' });
if (!connected) {
kpxcUI.createNotification('error', tr('errorNotConnected'));
return;
}
const databaseHash = await browser.runtime.sendMessage({ action: 'check_database_hash' });
if (databaseHash === '') {
// Triggers database unlock
_called.manualFillRequested = ManualFill.BOTH;
await browser.runtime.sendMessage({
action: 'get_database_hash',
args: [ false, true ] // Set triggerUnlock to true
});
}
if (icon.className.includes('unlock')) {
fillCredentials();
}
iconClicked(field, icon);
});
kpxcUI.setIconPosition(icon, field);
kpxcUsernameField.icon = icon;
this.icon = icon;
document.body.appendChild(icon);
};
const iconClicked = async function(field, icon) {
if (!kpxcFields.isVisible(field)) {
document.body.removeChild(icon);
field.removeAttribute('kpxc-username-field');
return;
}
const connected = await browser.runtime.sendMessage({ action: 'is_connected' });
if (!connected) {
kpxcUI.createNotification('error', tr('errorNotConnected'));
return;
}
const databaseHash = await browser.runtime.sendMessage({ action: 'check_database_hash' });
if (databaseHash === '') {
// Triggers database unlock
_called.manualFillRequested = ManualFill.BOTH;
await browser.runtime.sendMessage({
action: 'get_database_hash',
args: [ false, true ] // Set triggerUnlock to true
});
}
if (icon.className.includes('unlock')) {
fillCredentials(field);
}
};
const getIconClassName = function(locked = false) {
if (locked) {
return (isFirefox() ? 'lock-moz' : 'lock');
@ -125,22 +137,12 @@ const getIconClassName = function(locked = false) {
return (isFirefox() ? 'unlock-moz' : 'unlock');
};
const fillCredentials = function() {
const fieldId = kpxcUsernameField.inputField.getAttribute('data-kpxc-id');
const fillCredentials = function(field) {
const fieldId = field.getAttribute('data-kpxc-id');
kpxcFields.prepareId(fieldId);
const givenType = kpxcUsernameField.inputField.type === 'password' ? 'password' : 'username';
const givenType = field.type === 'password' ? 'password' : 'username';
const combination = kpxcFields.getCombination(givenType, fieldId);
kpxc.fillInCredentials(combination, givenType === 'password', false);
};
// Handle icon position on window resize
window.addEventListener('resize', function(e) {
kpxcUI.updateIconPosition(kpxcUsernameField);
});
// Handle icon position on scroll
window.addEventListener('scroll', function(e) {
kpxcUI.updateIconPosition(kpxcUsernameField);
});

View file

@ -50,15 +50,15 @@
],
"js": [
"browser-polyfill.min.js",
"global.js",
"content/ui.js",
"content/banner.js",
"content/autocomplete.js",
"content/define.js",
"content/keepassxc-browser.js",
"content/pwgen.js",
"content/sites.js",
"content/ui.js",
"content/username-field.js",
"global.js"
"content/username-field.js"
],
"css": [
"css/autocomplete.css",