mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-27 00:06:55 +00:00
[fix] CSS insertion in Edge browser
Edge browser throws `HierarchyRequestError` while inserting CSS rules into CSS Media Queries. Therefore, a different mechanism is required to control CSS order. This patch tracks the starting index of each group of CSS rules in the DOM style sheet. Fix #1300 Close #1302
This commit is contained in:
+4
-18
@@ -1,30 +1,20 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`AppRegistry getApplication "getStyleElement" produces styles that are a function of rendering "element" 1`] = `
|
exports[`AppRegistry getApplication "getStyleElement" produces styles that are a function of rendering "element" 1`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
|
||||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
|
||||||
body{margin:0;}
|
body{margin:0;}
|
||||||
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
|
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
|
||||||
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
|
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"0.1\\"]{}
|
[stylesheet-group=\\"0.1\\"]{}
|
||||||
:focus:not([data-focusvisible-polyfill]){outline: none;}
|
:focus:not([data-focusvisible-polyfill]){outline: none;}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"1\\"]{}
|
[stylesheet-group=\\"1\\"]{}
|
||||||
.css-view-1dbjc4n{-ms-flex-align:stretch;-ms-flex-direction:column;-ms-flex-negative:0;-ms-flex-preferred-size:auto;-webkit-align-items:stretch;-webkit-box-align:stretch;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-basis:auto;-webkit-flex-direction:column;-webkit-flex-shrink:0;align-items:stretch;border:0 solid black;box-sizing:border-box;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;margin-bottom:0px;margin-left:0px;margin-right:0px;margin-top:0px;min-height:0px;min-width:0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;position:relative;z-index:0;}
|
.css-view-1dbjc4n{-ms-flex-align:stretch;-ms-flex-direction:column;-ms-flex-negative:0;-ms-flex-preferred-size:auto;-webkit-align-items:stretch;-webkit-box-align:stretch;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-basis:auto;-webkit-flex-direction:column;-webkit-flex-shrink:0;align-items:stretch;border:0 solid black;box-sizing:border-box;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;margin-bottom:0px;margin-left:0px;margin-right:0px;margin-top:0px;min-height:0px;min-width:0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;position:relative;z-index:0;}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"2\\"]{}
|
[stylesheet-group=\\"2\\"]{}
|
||||||
.r-flex-13awgt0{-ms-flex-negative:1;-ms-flex-positive:1;-ms-flex-preferred-size:0%;-webkit-box-flex:1;-webkit-flex-basis:0%;-webkit-flex-grow:1;-webkit-flex-shrink:1;flex-basis:0%;flex-grow:1;flex-shrink:1;}
|
.r-flex-13awgt0{-ms-flex-negative:1;-ms-flex-positive:1;-ms-flex-preferred-size:0%;-webkit-box-flex:1;-webkit-flex-basis:0%;-webkit-flex-grow:1;-webkit-flex-shrink:1;flex-basis:0%;flex-grow:1;flex-shrink:1;}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"2.2\\"]{}
|
[stylesheet-group=\\"2.2\\"]{}
|
||||||
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
|
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
|
||||||
.r-pointerEvents-12vffkv{pointer-events:none!important;}
|
.r-pointerEvents-12vffkv{pointer-events:none!important;}"
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`] = `
|
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`] = `
|
||||||
@@ -36,15 +26,11 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
|
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
|
||||||
"<style id=\\"react-native-stylesheet\\">@media all {
|
"<style id=\\"react-native-stylesheet\\">[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
|
||||||
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
|
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
|
||||||
body{margin:0;}
|
body{margin:0;}
|
||||||
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
|
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
|
||||||
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
|
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"0.1\\"]{}
|
[stylesheet-group=\\"0.1\\"]{}
|
||||||
:focus:not([data-focusvisible-polyfill]){outline: none;}
|
:focus:not([data-focusvisible-polyfill]){outline: none;}</style>"
|
||||||
}</style>"
|
|
||||||
`;
|
`;
|
||||||
|
|||||||
+13
-37
@@ -3,81 +3,57 @@
|
|||||||
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 1`] = `""`;
|
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 1`] = `""`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 2`] = `
|
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 2`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
.one {}"
|
||||||
.one {}
|
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 3`] = `
|
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 3`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
.one {}"
|
||||||
.one {}
|
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert insertion order for different groups 1`] = `
|
exports[`createOrderedCSSStyleSheet #insert insertion order for different groups 1`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"1\\"]{}
|
||||||
[stylesheet-group=\\"1\\"]{}
|
|
||||||
.one {}
|
.one {}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"2.2\\"]{}
|
[stylesheet-group=\\"2.2\\"]{}
|
||||||
.two {}
|
.two {}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"3\\"]{}
|
[stylesheet-group=\\"3\\"]{}
|
||||||
.three {}
|
.three {}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"4\\"]{}
|
[stylesheet-group=\\"4\\"]{}
|
||||||
.four-1 {}
|
.four-1 {}
|
||||||
.four-2 {}
|
.four-2 {}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"9.9\\"]{}
|
[stylesheet-group=\\"9.9\\"]{}
|
||||||
.nine-1 {}
|
.nine-1 {}
|
||||||
.nine-2 {}
|
.nine-2 {}"
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 1`] = `""`;
|
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 1`] = `""`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 2`] = `
|
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 2`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
.one {}"
|
||||||
.one {}
|
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 3`] = `
|
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 3`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
|
||||||
.one {}
|
.one {}
|
||||||
.two {}
|
.two {}"
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 4`] = `
|
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 4`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"0\\"]{}
|
||||||
[stylesheet-group=\\"0\\"]{}
|
|
||||||
.one {}
|
.one {}
|
||||||
.two {}
|
.two {}
|
||||||
.three {}
|
.three {}"
|
||||||
}"
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`createOrderedCSSStyleSheet client-side hydration from SSR CSS 1`] = `
|
exports[`createOrderedCSSStyleSheet client-side hydration from SSR CSS 1`] = `
|
||||||
"@media all {
|
"[stylesheet-group=\\"1\\"] {}
|
||||||
[stylesheet-group=\\"1\\"] {}
|
|
||||||
.one {width: 10px;}
|
.one {width: 10px;}
|
||||||
}
|
|
||||||
@media all {
|
|
||||||
[stylesheet-group=\\"2\\"] {}
|
[stylesheet-group=\\"2\\"] {}
|
||||||
.two-1 {height: 20px;}
|
.two-1 {height: 20px;}
|
||||||
.two-2 {color: red;}
|
.two-2 {color: red;}
|
||||||
@keyframes anim {
|
@keyframes anim {
|
||||||
0% {opacity: 1;}
|
0% {opacity: 1;}
|
||||||
}
|
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|||||||
+67
-50
@@ -7,7 +7,7 @@
|
|||||||
* @flow strict-local
|
* @flow strict-local
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Groups = { [key: number]: Array<string> };
|
type Groups = { [key: number]: { start: ?number, rules: Array<string> } };
|
||||||
type Selectors = { [key: string]: boolean };
|
type Selectors = { [key: string]: boolean };
|
||||||
|
|
||||||
const slice = Array.prototype.slice;
|
const slice = Array.prototype.slice;
|
||||||
@@ -15,17 +15,17 @@ const slice = Array.prototype.slice;
|
|||||||
/**
|
/**
|
||||||
* Order-based insertion of CSS.
|
* Order-based insertion of CSS.
|
||||||
*
|
*
|
||||||
* Each rule can be inserted (appended) into a numerically defined group.
|
* Each rule is associated with a numerically defined group.
|
||||||
* Groups are ordered within the style sheet according to their number, with the
|
* Groups are ordered within the style sheet according to their number, with the
|
||||||
* lowest first.
|
* lowest first.
|
||||||
*
|
*
|
||||||
* Groups are implemented using Media Query blocks. CSSMediaRule implements the
|
* Groups are implemented using marker rules. The selector of the first rule of
|
||||||
* CSSGroupingRule, which includes 'insertRule', allowing groups to be treated as
|
* each group is used only to encode the group number for hydration. An
|
||||||
* a sub-sheet.
|
* alternative implementation could rely on CSSMediaRule, allowing groups to be
|
||||||
|
* treated as a sub-sheet, but the Edge implementation of CSSMediaRule is
|
||||||
|
* broken.
|
||||||
* https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
|
* https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
|
||||||
*
|
* https://gist.github.com/necolas/aa0c37846ad6bd3b05b727b959e82674
|
||||||
* The selector of the first rule of each group is used only to encode the group
|
|
||||||
* number for hydration.
|
|
||||||
*/
|
*/
|
||||||
export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
|
export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
|
||||||
const groups: Groups = {};
|
const groups: Groups = {};
|
||||||
@@ -35,59 +35,78 @@ export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
|
|||||||
* Hydrate approximate record from any existing rules in the sheet.
|
* Hydrate approximate record from any existing rules in the sheet.
|
||||||
*/
|
*/
|
||||||
if (sheet != null) {
|
if (sheet != null) {
|
||||||
slice.call(sheet.cssRules).forEach(mediaRule => {
|
let group;
|
||||||
if (mediaRule.media == null) {
|
slice.call(sheet.cssRules).forEach((cssRule, i) => {
|
||||||
throw new Error(
|
const cssText = cssRule.cssText;
|
||||||
'OrderedCSSStyleSheet: hydrating invalid stylesheet. Expected only @media rules.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create group number
|
|
||||||
const group = decodeGroupRule(mediaRule);
|
|
||||||
groups[group] = [];
|
|
||||||
|
|
||||||
// Create record of existing selectors and rules
|
// Create record of existing selectors and rules
|
||||||
slice.call(mediaRule.cssRules).forEach(rule => {
|
if (cssText.indexOf('stylesheet-group') > -1) {
|
||||||
const selectorText = getSelectorText(rule.cssText);
|
group = decodeGroupRule(cssRule);
|
||||||
|
groups[group] = { start: i, rules: [cssText] };
|
||||||
|
} else {
|
||||||
|
const selectorText = getSelectorText(cssText);
|
||||||
if (selectorText != null) {
|
if (selectorText != null) {
|
||||||
selectors[selectorText] = true;
|
selectors[selectorText] = true;
|
||||||
groups[group].push(rule.cssText);
|
groups[group].rules.push(cssText);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sheetInsert(sheet, group, text) {
|
||||||
|
const orderedGroups = getOrderedGroups(groups);
|
||||||
|
const groupIndex = orderedGroups.indexOf(group);
|
||||||
|
const nextGroupIndex = groupIndex + 1;
|
||||||
|
const nextGroup = orderedGroups[nextGroupIndex];
|
||||||
|
// Insert rule before the next group, or at the end of the stylesheet
|
||||||
|
const position =
|
||||||
|
nextGroup != null && groups[nextGroup].start != null
|
||||||
|
? groups[nextGroup].start
|
||||||
|
: sheet.cssRules.length;
|
||||||
|
const isInserted = insertRuleAt(sheet, text, position);
|
||||||
|
|
||||||
|
if (isInserted) {
|
||||||
|
// Set the starting index of the new group
|
||||||
|
if (groups[group].start == null) {
|
||||||
|
groups[group].start = position;
|
||||||
|
}
|
||||||
|
// Increment the starting index of all subsequent groups
|
||||||
|
for (let i = nextGroupIndex; i < orderedGroups.length; i += 1) {
|
||||||
|
const groupNumber = orderedGroups[i];
|
||||||
|
const previousStart = groups[groupNumber].start;
|
||||||
|
groups[groupNumber].start = previousStart + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isInserted;
|
||||||
|
}
|
||||||
|
|
||||||
const OrderedCSSStyleSheet = {
|
const OrderedCSSStyleSheet = {
|
||||||
/**
|
/**
|
||||||
* The textContent of the style sheet.
|
* The textContent of the style sheet.
|
||||||
* Each group's rules are wrapped in a media query.
|
|
||||||
*/
|
*/
|
||||||
getTextContent(): string {
|
getTextContent(): string {
|
||||||
return getOrderedGroups(groups)
|
return getOrderedGroups(groups)
|
||||||
.map(group => {
|
.map(group => {
|
||||||
const rules = groups[group];
|
const rules = groups[group].rules;
|
||||||
const str = rules.join('\n');
|
return rules.join('\n');
|
||||||
return createMediaRule(str);
|
|
||||||
})
|
})
|
||||||
.join('\n');
|
.join('\n');
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Insert a rule into a media query in the style sheet
|
* Insert a rule into the style sheet
|
||||||
*/
|
*/
|
||||||
insert(cssText: string, group: number) {
|
insert(cssText: string, groupValue: number) {
|
||||||
|
const group = Number(groupValue);
|
||||||
|
|
||||||
// Create a new group.
|
// Create a new group.
|
||||||
if (groups[group] == null) {
|
if (groups[group] == null) {
|
||||||
const markerRule = encodeGroupRule(group);
|
const markerRule = encodeGroupRule(group);
|
||||||
|
|
||||||
// Create the internal record.
|
// Create the internal record.
|
||||||
groups[group] = [];
|
groups[group] = { start: null, rules: [markerRule] };
|
||||||
groups[group].push(markerRule);
|
// Update CSSOM.
|
||||||
|
|
||||||
// Create CSSOM CSSMediaRule.
|
|
||||||
if (sheet != null) {
|
if (sheet != null) {
|
||||||
const groupIndex = getOrderedGroups(groups).indexOf(group);
|
sheetInsert(sheet, group, markerRule);
|
||||||
insertRuleAt(sheet, createMediaRule(markerRule), groupIndex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,14 +117,14 @@ export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
|
|||||||
if (selectorText != null && selectors[selectorText] == null) {
|
if (selectorText != null && selectors[selectorText] == null) {
|
||||||
// Update the internal records.
|
// Update the internal records.
|
||||||
selectors[selectorText] = true;
|
selectors[selectorText] = true;
|
||||||
groups[group].push(cssText);
|
groups[group].rules.push(cssText);
|
||||||
// Update CSSOM CSSMediaRule.
|
// Update CSSOM.
|
||||||
if (sheet != null) {
|
if (sheet != null) {
|
||||||
const groupIndex = getOrderedGroups(groups).indexOf(group);
|
const isInserted = sheetInsert(sheet, group, cssText);
|
||||||
const root = sheet.cssRules[groupIndex];
|
if (!isInserted) {
|
||||||
if (root != null) {
|
// Revert internal record change if a rule was rejected (e.g.,
|
||||||
// $FlowFixMe: Flow is missing CSSOM types
|
// unrecognized pseudo-selector)
|
||||||
insertRuleAt(root, cssText, root.cssRules.length);
|
groups[group].rules.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,16 +138,12 @@ export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
|
|||||||
* Helper functions
|
* Helper functions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function createMediaRule(content) {
|
|
||||||
return `@media all {\n${content}\n}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function encodeGroupRule(group) {
|
function encodeGroupRule(group) {
|
||||||
return `[stylesheet-group="${group}"]{}`;
|
return `[stylesheet-group="${group}"]{}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeGroupRule(mediaRule) {
|
function decodeGroupRule(cssRule) {
|
||||||
return mediaRule.cssRules[0].selectorText.split('"')[1];
|
return Number(cssRule.selectorText.split('"')[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrderedGroups(obj: { [key: number]: any }) {
|
function getOrderedGroups(obj: { [key: number]: any }) {
|
||||||
@@ -143,12 +158,14 @@ function getSelectorText(cssText) {
|
|||||||
return selector !== '' ? selector.replace(pattern, '$1') : null;
|
return selector !== '' ? selector.replace(pattern, '$1') : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertRuleAt(root, cssText: string, position: number) {
|
function insertRuleAt(root, cssText: string, position: number): boolean {
|
||||||
try {
|
try {
|
||||||
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'.
|
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'.
|
||||||
root.insertRule(cssText, position);
|
root.insertRule(cssText, position);
|
||||||
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// JSDOM doesn't support `CSSSMediaRule#insertRule`.
|
// JSDOM doesn't support `CSSSMediaRule#insertRule`.
|
||||||
// Also ignore errors that occur from attempting to insert vendor-prefixed selectors.
|
// Also ignore errors that occur from attempting to insert vendor-prefixed selectors.
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user