Compare commits
3 Commits
master
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 35d2ef8580 | |||
| 5b69cecf9f | |||
| ef8b4e04ce |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
coverage/*
|
||||
package-lock.json
|
||||
|
||||
@@ -100,19 +100,19 @@ Build with the following commands, the output will be stored in "build/js/output
|
||||
For full not minified version
|
||||
|
||||
```sh
|
||||
npm build utils-build
|
||||
npm run utils-build
|
||||
```
|
||||
|
||||
For minified build (but keeps the function names as is)
|
||||
|
||||
```sh
|
||||
npm build utils-min-build
|
||||
npm run utils-min-build
|
||||
```
|
||||
|
||||
Or build both at the same time
|
||||
|
||||
```sh
|
||||
npm build utils-build-all
|
||||
npm run utils-build-all
|
||||
```
|
||||
|
||||
## Develop
|
||||
|
||||
3899
package-lock.json
generated
3899
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
117
src/utils.mjs
117
src/utils.mjs
@@ -10,6 +10,8 @@ import {
|
||||
isFunction as _isFunction,
|
||||
executeFunctionByName as _executeFunctionByName,
|
||||
isObject as _isObject,
|
||||
isArray as _isArray,
|
||||
isIterable as _isIterable,
|
||||
getObjectCount as _getObjectCount,
|
||||
keyInObject as _keyInObject,
|
||||
getKeyByValue as _getKeyByValue,
|
||||
@@ -61,7 +63,11 @@ import {
|
||||
} from './utils/FormatBytes.mjs';
|
||||
import {
|
||||
parseQueryString as _parseQueryString,
|
||||
getQueryStringParam as _getQueryStringParam
|
||||
getQueryStringParam as _getQueryStringParam,
|
||||
hasUrlParameter as _hasUrlParameter,
|
||||
getUrlParameter as _getUrlParameter,
|
||||
updateUrlParameter as _updateUrlParameter,
|
||||
removeUrlParameter as _removeUrlParameter,
|
||||
} from './utils/UrlParser.mjs';
|
||||
import {
|
||||
loginLogout as _loginLogout,
|
||||
@@ -81,6 +87,11 @@ import { l10nTranslation } from './utils/l10nTranslation.mjs';
|
||||
import { HtmlElementCreator } from './utils/HtmlElementCreator.mjs';
|
||||
import { ActionBox } from './utils/ActionBox.mjs';
|
||||
import { LoginNavMenu } from './utils/LoginNavMenu.mjs';
|
||||
import {
|
||||
isWebkit as _isWebkit,
|
||||
isMobileWebKit as _isMobileWebKit,
|
||||
isDesktopWebKit as _isDesktopWebKit
|
||||
} from './utils/BrowserDetect.mjs';
|
||||
|
||||
let aiob = new ActionIndicatorOverlayBox();
|
||||
let hec = new HtmlElementCreator();
|
||||
@@ -442,6 +453,28 @@ function isObject(val) // eslint-disable-line no-unused-vars
|
||||
return _isObject(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if variable is array
|
||||
* @param {any} val possible array
|
||||
* @returns {Boolean} true/false if it an array
|
||||
*/
|
||||
// @ts-ignore
|
||||
function isArray(val) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _isArray(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* checks if a variable is an iterable
|
||||
* @param {any} val possible iterable
|
||||
* @return {Boolean} true/false if it is an object or not
|
||||
*/
|
||||
// @ts-ignore
|
||||
function isIterable(val) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _isIterable(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the length of an object (entries)
|
||||
* @param {Object} object object to check
|
||||
@@ -937,16 +970,14 @@ function html_options_refill(name, data, sort = '') // eslint-disable-line no-un
|
||||
* ALTERNATIVE CODE
|
||||
* var url = new URL(window.location.href);
|
||||
* param_uid = url.searchParams.get('uid');
|
||||
* @param {String} [query=''] the query string to parse
|
||||
* if not set will auto fill
|
||||
* @param {String} [return_key=''] if set only returns this key entry
|
||||
* or empty for none
|
||||
* @return {Object|String} parameter entry list
|
||||
* @param {String} [query=''] the query string to parse, if not set will auto fill
|
||||
* @param {String} [return_key=''] if set only returns this key entry or empty for none
|
||||
* @param {Boolean} [single=false] if set to true then only the first found will be returned
|
||||
*/
|
||||
// @ts-ignore
|
||||
function parseQueryString(query = '', return_key = '') // eslint-disable-line no-unused-vars
|
||||
function parseQueryString(query = '', return_key = '', single = false) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _parseQueryString(query, return_key);
|
||||
return _parseQueryString(query, return_key, single);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -955,12 +986,9 @@ function parseQueryString(query = '', return_key = '') // eslint-disable-line no
|
||||
* if a parameter is set several times it will be returned as an array
|
||||
* if search parameter set and nothing found and empty string is returned
|
||||
* if no parametes exist and no serach is set and empty object is returned
|
||||
* @param {String} [search=''] if set searches for this entry, if empty
|
||||
* all parameters are returned
|
||||
* @param {String} [query=''] different query string to parse, if not
|
||||
* set (default) the current window href is used
|
||||
* @param {Boolean} [single=false] if set to true then only the first found
|
||||
* will be returned
|
||||
* @param {String} [search=''] if set searches for this entry, if empty all parameters are returned
|
||||
* @param {String} [query=''] different query string to parse, if not set (default) the current window href is used
|
||||
* @param {Boolean} [single=false] if set to true then only the first found will be returned
|
||||
* @return {Object|Array|String} if search is empty, object, if search is set
|
||||
* and only one entry, then string, else array
|
||||
* unless single is true
|
||||
@@ -971,6 +999,51 @@ function getQueryStringParam(search = '', query = '', single = false) // eslint-
|
||||
return _getQueryStringParam(search, query, single);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a query parameter in the current URL and update the browser's address bar
|
||||
* @param {string} key - The parameter name to add or update
|
||||
* @param {string} value - The value to set for the parameter
|
||||
* @param {boolean} [reload=false] - Whether to reload the page after updating the URL
|
||||
*/
|
||||
// @ts-ignore
|
||||
function updateUrlParameter(key, value, reload = false) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _updateUrlParameter(key, value, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a parameter from the current URL and update the browser's address bar
|
||||
* @param {string} key - The parameter name to remove
|
||||
* @param {boolean} [reload=false] - Whether to reload the page after updating the URL
|
||||
*/
|
||||
// @ts-ignore
|
||||
function removeUrlParameter(key, reload = false) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _removeUrlParameter(key, reload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key exists as URL parameter
|
||||
* @param {String} key URL parameter to search
|
||||
* @returns {Boolean} True if key exists
|
||||
*/
|
||||
// @ts-ignore
|
||||
function hasUrlParameter(key) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _hasUrlParameter(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return one value for a URL paramter or null if not found
|
||||
* @param {String} key Which URL parameter to get
|
||||
* @returns {String|Null} URL parameter content
|
||||
*/
|
||||
// @ts-ignore
|
||||
function getUrlParameter(key) // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _getUrlParameter(key);
|
||||
}
|
||||
|
||||
// MARK: ACL LOGIN
|
||||
// *** MASTER logout call
|
||||
/**
|
||||
@@ -1156,4 +1229,20 @@ function adjustActionBoxHeight(target_id = 'actionBox', override = 0, content_ov
|
||||
ab.adjustActionBoxHeight(target_id, override, content_override);
|
||||
}
|
||||
|
||||
// MARK: BROWSER DETECTION
|
||||
function isWebkit() // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _isWebkit();
|
||||
}
|
||||
|
||||
function isMobileWebKit() // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _isMobileWebKit();
|
||||
}
|
||||
|
||||
function isDesktopWebKit() // eslint-disable-line no-unused-vars
|
||||
{
|
||||
return _isDesktopWebKit();
|
||||
}
|
||||
|
||||
/* END */
|
||||
|
||||
31
src/utils/BrowserDetect.mjs
Normal file
31
src/utils/BrowserDetect.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Description: Browser detect
|
||||
Date: 2025/11.25
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
export { isWebkit, isMobileWebKit, isDesktopWebKit };
|
||||
|
||||
// Desktop Safari, all mobile WebKit browsers on iOS and webview iOS
|
||||
function isWebkit()
|
||||
{
|
||||
return "GestureEvent" in window;
|
||||
}
|
||||
|
||||
// all mobile webkit browsers and webview iOS
|
||||
function isMobileWebKit()
|
||||
{
|
||||
return "ongesturechange" in window;
|
||||
}
|
||||
|
||||
// Desktop Safari
|
||||
function isDesktopWebKit()
|
||||
{
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
'safari' in window &&
|
||||
'pushNotification' in window.safari
|
||||
);
|
||||
}
|
||||
|
||||
// __END__
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: Date Time functions
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: DOM Helpers
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: Byte string formatting
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: DOM Management and HTML builder
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@ export {
|
||||
// deprecated name
|
||||
HtmlElementCreator as DomManagement
|
||||
};
|
||||
import { deepCopyFunction, isObject } from './JavaScriptHelpers.mjs';
|
||||
import { deepCopyFunction, isObject, isArray } from './JavaScriptHelpers.mjs';
|
||||
|
||||
class HtmlElementCreator {
|
||||
/**
|
||||
@@ -50,7 +50,7 @@ class HtmlElementCreator {
|
||||
base.sub.push(deepCopyFunction(attach));
|
||||
} else {
|
||||
// sub check
|
||||
if (isObject(base.sub) && base.sub.length > 0) {
|
||||
if (isArray(base.sub) && base.sub.length > 0) {
|
||||
for (var i = 0; i < base.sub.length; i ++) {
|
||||
// recursive call to sub element
|
||||
this.ael(base.sub[i], attach, id);
|
||||
@@ -209,7 +209,7 @@ class HtmlElementCreator {
|
||||
}
|
||||
}
|
||||
// second CSS
|
||||
if (isObject(tree.css) && tree.css.length > 0) {
|
||||
if (isArray(tree.css) && tree.css.length > 0) {
|
||||
line += ' class="';
|
||||
for (i = 0; i < tree.css.length; i ++) {
|
||||
line += tree.css[i] + ' ';
|
||||
@@ -234,7 +234,7 @@ class HtmlElementCreator {
|
||||
// dive into sub tree to attach sub nodes
|
||||
// NOTES: we can have content (text) AND sub nodes at the same level
|
||||
// CONTENT (TEXT) takes preference over SUB NODE in order
|
||||
if (isObject(tree.sub) && tree.sub.length > 0) {
|
||||
if (isArray(tree.sub) && tree.sub.length > 0) {
|
||||
if (tree.content) {
|
||||
content.push(tree.content);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: HTML Helpers
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/*
|
||||
Description: JavaScript Helpers
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
export {
|
||||
errorCatch, isFunction,
|
||||
executeFunctionByName, runFunction, runFunctionArgsArray,
|
||||
isObject, getObjectCount,
|
||||
isObject, isIterable, isArray, getObjectCount,
|
||||
keyInObject, objectKeyExists,
|
||||
getKeyByValue, valueInObject, objectValueExists,
|
||||
deepCopyFunction
|
||||
@@ -114,10 +114,33 @@ function runFunctionArgsArray(name, args)
|
||||
*/
|
||||
function isObject(val)
|
||||
{
|
||||
if (val === null) {
|
||||
return false;
|
||||
return val !== null && typeof val === 'object' && !Array.isArray(val);
|
||||
}
|
||||
return ((typeof val === 'function') || (typeof val === 'object'));
|
||||
|
||||
/**
|
||||
* Check if variable is array
|
||||
* @param {any} val possible array
|
||||
* @returns {Boolean} true/false if it an array
|
||||
*/
|
||||
function isArray(val)
|
||||
{
|
||||
return val !== null && Array.isArray(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any iterable type
|
||||
* @param {any} val possible iteratable object/array
|
||||
* @returns {Boolean} true/false if it is iterable
|
||||
*/
|
||||
function isIterable(val)
|
||||
{
|
||||
if (val == null) return false;
|
||||
|
||||
// Check if it's iterable, but excldue strings
|
||||
if (typeof val[Symbol.iterator] === 'function' && typeof val !== 'string') return true;
|
||||
|
||||
// Check if it's a plain object
|
||||
return typeof val === 'object' && val.constructor === Object;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: Login access and menu
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: Login access and menu
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: Resize and Move Javascript
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: String Helpers
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
Description: HTML Helpers
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
export { parseQueryString, getQueryStringParam };
|
||||
export { parseQueryString, getQueryStringParam, hasUrlParameter, getUrlParameter, updateUrlParameter, removeUrlParameter };
|
||||
|
||||
/**
|
||||
* NOTE: this original code was wrong, now using URL and parsing through
|
||||
@@ -13,12 +13,9 @@ export { parseQueryString, getQueryStringParam };
|
||||
* ALTERNATIVE CODE
|
||||
* var url = new URL(window.location.href);
|
||||
* param_uid = url.searchParams.get('uid');
|
||||
* @param {String} [query=''] the query string to parse
|
||||
* if not set will auto fill
|
||||
* @param {String} [return_key=''] if set only returns this key entry
|
||||
* or empty for none
|
||||
* @param {Boolean} [single=false] if set to true then only the first found
|
||||
* will be returned
|
||||
* @param {String} [query=''] the query string to parse, if not set will auto fill
|
||||
* @param {String} [return_key=''] if set only returns this key entry or empty for none
|
||||
* @param {Boolean} [single=false] if set to true then only the first found will be returned
|
||||
* @return {Object|String} parameter entry list
|
||||
*/
|
||||
function parseQueryString(query = '', return_key = '', single = false)
|
||||
@@ -32,12 +29,9 @@ function parseQueryString(query = '', return_key = '', single = false)
|
||||
* if a parameter is set several times it will be returned as an array
|
||||
* if search parameter set and nothing found and empty string is returned
|
||||
* if no parametes exist and no serach is set and empty object is returned
|
||||
* @param {String} [search=''] if set searches for this entry, if empty
|
||||
* all parameters are returned
|
||||
* @param {String} [query=''] different query string to parse, if not
|
||||
* set (default) the current window href is used
|
||||
* @param {Boolean} [single=false] if set to true then only the first found
|
||||
* will be returned
|
||||
* @param {String} [search=''] if set searches for this entry, if empty all parameters are returned
|
||||
* @param {String} [query=''] different query string to parse, if not set (default) the current window href is used
|
||||
* @param {Boolean} [single=false] if set to true then only the first found will be returned
|
||||
* @return {Object|Array|String} if search is empty, object, if search is set
|
||||
* and only one entry, then string, else array
|
||||
* unless single is true
|
||||
@@ -75,4 +69,74 @@ function getQueryStringParam(search = '', query = '', single = false)
|
||||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key exists as URL parameter
|
||||
* @param {String} key URL parameter to search
|
||||
* @returns {Boolean} True if key exists
|
||||
*/
|
||||
function hasUrlParameter(key)
|
||||
{
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return one value for a URL paramter or null if not found
|
||||
* @param {String} key Which URL parameter to get
|
||||
* @returns {String|Null} URL parameter content
|
||||
*/
|
||||
function getUrlParameter(key)
|
||||
{
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or update a query parameter in the current URL and update the browser's address bar
|
||||
* @param {string} key - The parameter name to add or update
|
||||
* @param {string} value - The value to set for the parameter
|
||||
* @param {boolean} [reload=false] - Whether to reload the page after updating the URL
|
||||
*/
|
||||
function updateUrlParameter(key, value, reload = false)
|
||||
{
|
||||
// Create a URL object from the current URL
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// Set or update the parameter
|
||||
url.searchParams.set(key, value);
|
||||
|
||||
const newUrl = url.toString();
|
||||
|
||||
// Update the browser's address bar without reloading the page
|
||||
window.history.pushState({ path: newUrl }, '', newUrl);
|
||||
|
||||
// Optionally reload the page
|
||||
if (reload) {
|
||||
// window.location.href = newUrl;
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a parameter from the current URL and update the browser's address bar
|
||||
* @param {string} key - The parameter name to remove
|
||||
* @param {boolean} [reload=false] - Whether to reload the page after updating the URL
|
||||
*/
|
||||
function removeUrlParameter(key, reload = false)
|
||||
{
|
||||
// Create a URL object from the current URL
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
// Remove the parameter if it exists
|
||||
url.searchParams.delete(key);
|
||||
|
||||
// Update the browser's address bar without reloading the page
|
||||
window.history.pushState({}, '', url.toString());
|
||||
|
||||
// Optionally reload the page
|
||||
if (reload) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// __EMD__
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Description: Translation call
|
||||
Date: 2025//3/6
|
||||
Date: 2025/3/6
|
||||
Creator: Clemens Schwaighofer
|
||||
*/
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
// isFunction,
|
||||
// executeFunctionByName,
|
||||
isObject,
|
||||
isArray,
|
||||
isIterable,
|
||||
getObjectCount,
|
||||
keyInObject,
|
||||
objectKeyExists,
|
||||
@@ -30,15 +32,57 @@ describe("isObject", () => {
|
||||
let is_object = {
|
||||
"a": 1
|
||||
};
|
||||
let empty_array = [];
|
||||
let is_array = [1, 2, 3];
|
||||
let is_string = "";
|
||||
let is_number = 1;
|
||||
expect(isObject(empty_object)).toEqual(true);
|
||||
expect(isObject(is_object)).toEqual(true);
|
||||
expect(isObject(empty_array)).toEqual(false);
|
||||
expect(isObject(is_array)).toEqual(false);
|
||||
expect(isObject(is_string)).toEqual(false);
|
||||
expect(isObject(is_number)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isArray", () => {
|
||||
it('should return bool if array', () => {
|
||||
let empty_object = {};
|
||||
let is_object = {
|
||||
"a": 1
|
||||
};
|
||||
let empty_array = [];
|
||||
let is_array = [1, 2, 3];
|
||||
let is_string = "";
|
||||
let is_number = 1;
|
||||
expect(isArray(empty_object)).toEqual(false);
|
||||
expect(isArray(is_object)).toEqual(false);
|
||||
expect(isArray(empty_array)).toEqual(true);
|
||||
expect(isArray(is_array)).toEqual(true);
|
||||
expect(isArray(is_string)).toEqual(false);
|
||||
expect(isArray(is_number)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isIterable", () => {
|
||||
it('should return bool if iterable', () => {
|
||||
let empty_object = {};
|
||||
let is_object = {
|
||||
"a": 1
|
||||
};
|
||||
let empty_array = [];
|
||||
let is_array = [1, 2, 3];
|
||||
let is_string = "";
|
||||
let is_number = 1;
|
||||
expect(isIterable(empty_object)).toEqual(true);
|
||||
expect(isIterable(is_object)).toEqual(true);
|
||||
expect(isIterable(empty_array)).toEqual(true);
|
||||
expect(isIterable(is_array)).toEqual(true);
|
||||
expect(isIterable(is_string)).toEqual(false);
|
||||
expect(isIterable(is_number)).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getObjectCount", () => {
|
||||
it('should return count of objects', () => {
|
||||
let zero = {};
|
||||
|
||||
Reference in New Issue
Block a user