mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-08-07 17:49:17 +00:00
Merged 3.1 to HEAD
13275: updated web-client to use tinymce v3 13276: overlay display fix for when field has large content git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@13585 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
692
source/web/scripts/tiny_mce/classes/dom/Selection.js
vendored
Executable file
692
source/web/scripts/tiny_mce/classes/dom/Selection.js
vendored
Executable file
@@ -0,0 +1,692 @@
|
||||
/**
|
||||
* $Id: Selection.js 906 2008-08-24 16:47:29Z spocke $
|
||||
*
|
||||
* @author Moxiecode
|
||||
* @copyright Copyright <20> 2004-2008, Moxiecode Systems AB, All rights reserved.
|
||||
*/
|
||||
|
||||
(function() {
|
||||
function trimNl(s) {
|
||||
return s.replace(/[\n\r]+/g, '');
|
||||
};
|
||||
|
||||
// Shorten names
|
||||
var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;
|
||||
|
||||
/**#@+
|
||||
* @class This class handles text and control selection it's an crossbrowser utility class.
|
||||
* Consult the TinyMCE Wiki API for more details and examples on how to use this class.
|
||||
* @member tinymce.dom.Selection
|
||||
*/
|
||||
tinymce.create('tinymce.dom.Selection', {
|
||||
/**
|
||||
* Constructs a new selection instance.
|
||||
*
|
||||
* @constructor
|
||||
* @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
|
||||
* @param {Window} win Window to bind the selection object to.
|
||||
* @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
|
||||
*/
|
||||
Selection : function(dom, win, serializer) {
|
||||
var t = this;
|
||||
|
||||
t.dom = dom;
|
||||
t.win = win;
|
||||
t.serializer = serializer;
|
||||
|
||||
// Add events
|
||||
each([
|
||||
'onBeforeSetContent',
|
||||
'onBeforeGetContent',
|
||||
'onSetContent',
|
||||
'onGetContent'
|
||||
], function(e) {
|
||||
t[e] = new tinymce.util.Dispatcher(t);
|
||||
});
|
||||
|
||||
// Prevent leaks
|
||||
tinymce.addUnload(t.destroy, t);
|
||||
},
|
||||
|
||||
/**#@+
|
||||
* @method
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the selected contents using the DOM serializer passed in to this class.
|
||||
*
|
||||
* @param {Object} s Optional settings class with for example output format text or html.
|
||||
* @return {String} Selected contents in for example HTML format.
|
||||
*/
|
||||
getContent : function(s) {
|
||||
var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;
|
||||
|
||||
s = s || {};
|
||||
wb = wa = '';
|
||||
s.get = true;
|
||||
s.format = s.format || 'html';
|
||||
t.onBeforeGetContent.dispatch(t, s);
|
||||
|
||||
if (s.format == 'text')
|
||||
return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));
|
||||
|
||||
if (r.cloneContents) {
|
||||
n = r.cloneContents();
|
||||
|
||||
if (n)
|
||||
e.appendChild(n);
|
||||
} else if (is(r.item) || is(r.htmlText))
|
||||
e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText;
|
||||
else
|
||||
e.innerHTML = r.toString();
|
||||
|
||||
// Keep whitespace before and after
|
||||
if (/^\s/.test(e.innerHTML))
|
||||
wb = ' ';
|
||||
|
||||
if (/\s+$/.test(e.innerHTML))
|
||||
wa = ' ';
|
||||
|
||||
s.getInner = true;
|
||||
|
||||
s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
|
||||
t.onGetContent.dispatch(t, s);
|
||||
|
||||
return s.content;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the current selection to the specified content. If any contents is selected it will be replaced
|
||||
* with the contents passed in to this function. If there is no selection the contents will be inserted
|
||||
* where the caret is placed in the editor/page.
|
||||
*
|
||||
* @param {String} h HTML contents to set could also be other formats depending on settings.
|
||||
* @param {Object} s Optional settings object with for example data format.
|
||||
*/
|
||||
setContent : function(h, s) {
|
||||
var t = this, r = t.getRng(), c, d = t.win.document;
|
||||
|
||||
s = s || {format : 'html'};
|
||||
s.set = true;
|
||||
h = s.content = t.dom.processHTML(h);
|
||||
|
||||
// Dispatch before set content event
|
||||
t.onBeforeSetContent.dispatch(t, s);
|
||||
h = s.content;
|
||||
|
||||
if (r.insertNode) {
|
||||
// Make caret marker since insertNode places the caret in the beginning of text after insert
|
||||
h += '<span id="__caret">_</span>';
|
||||
|
||||
// Delete and insert new node
|
||||
r.deleteContents();
|
||||
r.insertNode(t.getRng().createContextualFragment(h));
|
||||
|
||||
// Move to caret marker
|
||||
c = t.dom.get('__caret');
|
||||
|
||||
// Make sure we wrap it compleatly, Opera fails with a simple select call
|
||||
r = d.createRange();
|
||||
r.setStartBefore(c);
|
||||
r.setEndAfter(c);
|
||||
t.setRng(r);
|
||||
|
||||
// Delete the marker, and hopefully the caret gets placed in the right location
|
||||
d.execCommand('Delete', false, null);
|
||||
|
||||
// In case it's still there
|
||||
t.dom.remove('__caret');
|
||||
} else {
|
||||
if (r.item) {
|
||||
// Delete content and get caret text selection
|
||||
d.execCommand('Delete', false, null);
|
||||
r = t.getRng();
|
||||
}
|
||||
|
||||
r.pasteHTML(h);
|
||||
}
|
||||
|
||||
// Dispatch set content event
|
||||
t.onSetContent.dispatch(t, s);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the start element of a selection range. If the start is in a text
|
||||
* node the parent element will be returned.
|
||||
*
|
||||
* @return {Element} Start element of selection range.
|
||||
*/
|
||||
getStart : function() {
|
||||
var t = this, r = t.getRng(), e;
|
||||
|
||||
if (isIE) {
|
||||
if (r.item)
|
||||
return r.item(0);
|
||||
|
||||
r = r.duplicate();
|
||||
r.collapse(1);
|
||||
e = r.parentElement();
|
||||
|
||||
if (e && e.nodeName == 'BODY')
|
||||
return e.firstChild;
|
||||
|
||||
return e;
|
||||
} else {
|
||||
e = r.startContainer;
|
||||
|
||||
if (e.nodeName == 'BODY')
|
||||
return e.firstChild;
|
||||
|
||||
return t.dom.getParent(e, function(n) {return n.nodeType == 1;});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the end element of a selection range. If the end is in a text
|
||||
* node the parent element will be returned.
|
||||
*
|
||||
* @return {Element} End element of selection range.
|
||||
*/
|
||||
getEnd : function() {
|
||||
var t = this, r = t.getRng(), e;
|
||||
|
||||
if (isIE) {
|
||||
if (r.item)
|
||||
return r.item(0);
|
||||
|
||||
r = r.duplicate();
|
||||
r.collapse(0);
|
||||
e = r.parentElement();
|
||||
|
||||
if (e && e.nodeName == 'BODY')
|
||||
return e.lastChild;
|
||||
|
||||
return e;
|
||||
} else {
|
||||
e = r.endContainer;
|
||||
|
||||
if (e.nodeName == 'BODY')
|
||||
return e.lastChild;
|
||||
|
||||
return t.dom.getParent(e, function(n) {return n.nodeType == 1;});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a bookmark location for the current selection. This bookmark object
|
||||
* can then be used to restore the selection after some content modification to the document.
|
||||
*
|
||||
* @param {bool} si Optional state if the bookmark should be simple or not. Default is complex.
|
||||
* @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
|
||||
*/
|
||||
getBookmark : function(si) {
|
||||
var t = this, r = t.getRng(), tr, sx, sy, vp = t.dom.getViewPort(t.win), e, sp, bp, le, c = -0xFFFFFF, s, ro = t.dom.getRoot(), wb = 0, wa = 0, nv;
|
||||
sx = vp.x;
|
||||
sy = vp.y;
|
||||
|
||||
// Simple bookmark fast but not as persistent
|
||||
if (si == 'simple')
|
||||
return {rng : r, scrollX : sx, scrollY : sy};
|
||||
|
||||
// Handle IE
|
||||
if (isIE) {
|
||||
// Control selection
|
||||
if (r.item) {
|
||||
e = r.item(0);
|
||||
|
||||
each(t.dom.select(e.nodeName), function(n, i) {
|
||||
if (e == n) {
|
||||
sp = i;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
tag : e.nodeName,
|
||||
index : sp,
|
||||
scrollX : sx,
|
||||
scrollY : sy
|
||||
};
|
||||
}
|
||||
|
||||
// Text selection
|
||||
tr = t.dom.doc.body.createTextRange();
|
||||
tr.moveToElementText(ro);
|
||||
tr.collapse(true);
|
||||
bp = Math.abs(tr.move('character', c));
|
||||
|
||||
tr = r.duplicate();
|
||||
tr.collapse(true);
|
||||
sp = Math.abs(tr.move('character', c));
|
||||
|
||||
tr = r.duplicate();
|
||||
tr.collapse(false);
|
||||
le = Math.abs(tr.move('character', c)) - sp;
|
||||
|
||||
return {
|
||||
start : sp - bp,
|
||||
length : le,
|
||||
scrollX : sx,
|
||||
scrollY : sy
|
||||
};
|
||||
}
|
||||
|
||||
// Handle W3C
|
||||
e = t.getNode();
|
||||
s = t.getSel();
|
||||
|
||||
if (!s)
|
||||
return null;
|
||||
|
||||
// Image selection
|
||||
if (e && e.nodeName == 'IMG') {
|
||||
return {
|
||||
scrollX : sx,
|
||||
scrollY : sy
|
||||
};
|
||||
}
|
||||
|
||||
// Text selection
|
||||
|
||||
function getPos(r, sn, en) {
|
||||
var w = t.dom.doc.createTreeWalker(r, NodeFilter.SHOW_TEXT, null, false), n, p = 0, d = {};
|
||||
|
||||
while ((n = w.nextNode()) != null) {
|
||||
if (n == sn)
|
||||
d.start = p;
|
||||
|
||||
if (n == en) {
|
||||
d.end = p;
|
||||
return d;
|
||||
}
|
||||
|
||||
p += trimNl(n.nodeValue || '').length;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Caret or selection
|
||||
if (s.anchorNode == s.focusNode && s.anchorOffset == s.focusOffset) {
|
||||
e = getPos(ro, s.anchorNode, s.focusNode);
|
||||
|
||||
if (!e)
|
||||
return {scrollX : sx, scrollY : sy};
|
||||
|
||||
// Count whitespace before
|
||||
trimNl(s.anchorNode.nodeValue || '').replace(/^\s+/, function(a) {wb = a.length;});
|
||||
|
||||
return {
|
||||
start : Math.max(e.start + s.anchorOffset - wb, 0),
|
||||
end : Math.max(e.end + s.focusOffset - wb, 0),
|
||||
scrollX : sx,
|
||||
scrollY : sy,
|
||||
beg : s.anchorOffset - wb == 0
|
||||
};
|
||||
} else {
|
||||
e = getPos(ro, r.startContainer, r.endContainer);
|
||||
|
||||
// Count whitespace before start and end container
|
||||
//(r.startContainer.nodeValue || '').replace(/^\s+/, function(a) {wb = a.length;});
|
||||
//(r.endContainer.nodeValue || '').replace(/^\s+/, function(a) {wa = a.length;});
|
||||
|
||||
if (!e)
|
||||
return {scrollX : sx, scrollY : sy};
|
||||
|
||||
return {
|
||||
start : Math.max(e.start + r.startOffset - wb, 0),
|
||||
end : Math.max(e.end + r.endOffset - wa, 0),
|
||||
scrollX : sx,
|
||||
scrollY : sy,
|
||||
beg : r.startOffset - wb == 0
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores the selection to the specified bookmark.
|
||||
*
|
||||
* @param {Object} bookmark Bookmark to restore selection from.
|
||||
* @return {bool} true/false if it was successful or not.
|
||||
*/
|
||||
moveToBookmark : function(b) {
|
||||
var t = this, r = t.getRng(), s = t.getSel(), ro = t.dom.getRoot(), sd, nvl, nv;
|
||||
|
||||
function getPos(r, sp, ep) {
|
||||
var w = t.dom.doc.createTreeWalker(r, NodeFilter.SHOW_TEXT, null, false), n, p = 0, d = {}, o, v, wa, wb;
|
||||
|
||||
while ((n = w.nextNode()) != null) {
|
||||
wa = wb = 0;
|
||||
|
||||
nv = n.nodeValue || '';
|
||||
//nv.replace(/^\s+[^\s]/, function(a) {wb = a.length - 1;});
|
||||
//nv.replace(/[^\s]\s+$/, function(a) {wa = a.length - 1;});
|
||||
|
||||
nvl = trimNl(nv).length;
|
||||
p += nvl;
|
||||
|
||||
if (p >= sp && !d.startNode) {
|
||||
o = sp - (p - nvl);
|
||||
|
||||
// Fix for odd quirk in FF
|
||||
if (b.beg && o >= nvl)
|
||||
continue;
|
||||
|
||||
d.startNode = n;
|
||||
d.startOffset = o + wb;
|
||||
}
|
||||
|
||||
if (p >= ep) {
|
||||
d.endNode = n;
|
||||
d.endOffset = ep - (p - nvl) + wb;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (!b)
|
||||
return false;
|
||||
|
||||
t.win.scrollTo(b.scrollX, b.scrollY);
|
||||
|
||||
// Handle explorer
|
||||
if (isIE) {
|
||||
// Handle simple
|
||||
if (r = b.rng) {
|
||||
try {
|
||||
r.select();
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
t.win.focus();
|
||||
|
||||
// Handle control bookmark
|
||||
if (b.tag) {
|
||||
r = ro.createControlRange();
|
||||
|
||||
each(t.dom.select(b.tag), function(n, i) {
|
||||
if (i == b.index)
|
||||
r.addElement(n);
|
||||
});
|
||||
} else {
|
||||
// Try/catch needed since this operation breaks when TinyMCE is placed in hidden divs/tabs
|
||||
try {
|
||||
// Incorrect bookmark
|
||||
if (b.start < 0)
|
||||
return true;
|
||||
|
||||
r = s.createRange();
|
||||
r.moveToElementText(ro);
|
||||
r.collapse(true);
|
||||
r.moveStart('character', b.start);
|
||||
r.moveEnd('character', b.length);
|
||||
} catch (ex2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
r.select();
|
||||
} catch (ex) {
|
||||
// Needed for some odd IE bug #1843306
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle W3C
|
||||
if (!s)
|
||||
return false;
|
||||
|
||||
// Handle simple
|
||||
if (b.rng) {
|
||||
s.removeAllRanges();
|
||||
s.addRange(b.rng);
|
||||
} else {
|
||||
if (is(b.start) && is(b.end)) {
|
||||
try {
|
||||
sd = getPos(ro, b.start, b.end);
|
||||
|
||||
if (sd) {
|
||||
r = t.dom.doc.createRange();
|
||||
r.setStart(sd.startNode, sd.startOffset);
|
||||
r.setEnd(sd.endNode, sd.endOffset);
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
}
|
||||
|
||||
if (!tinymce.isOpera)
|
||||
t.win.focus();
|
||||
} catch (ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects the specified element. This will place the start and end of the selection range around the element.
|
||||
*
|
||||
* @param {Element} n HMTL DOM element to select.
|
||||
* @param {} c Bool state if the contents should be selected or not on non IE browser.
|
||||
* @return {Element} Selected element the same element as the one that got passed in.
|
||||
*/
|
||||
select : function(n, c) {
|
||||
var t = this, r = t.getRng(), s = t.getSel(), b, fn, ln, d = t.win.document;
|
||||
|
||||
function first(n) {
|
||||
return n ? d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false).nextNode() : null;
|
||||
};
|
||||
|
||||
function last(n) {
|
||||
var c, o, w;
|
||||
|
||||
if (!n)
|
||||
return null;
|
||||
|
||||
w = d.createTreeWalker(n, NodeFilter.SHOW_TEXT, null, false);
|
||||
while (c = w.nextNode())
|
||||
o = c;
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
if (isIE) {
|
||||
try {
|
||||
b = d.body;
|
||||
|
||||
if (/^(IMG|TABLE)$/.test(n.nodeName)) {
|
||||
r = b.createControlRange();
|
||||
r.addElement(n);
|
||||
} else {
|
||||
r = b.createTextRange();
|
||||
r.moveToElementText(n);
|
||||
}
|
||||
|
||||
r.select();
|
||||
} catch (ex) {
|
||||
// Throws illigal agrument in IE some times
|
||||
}
|
||||
} else {
|
||||
if (c) {
|
||||
fn = first(n);
|
||||
ln = last(n);
|
||||
|
||||
if (fn && ln) {
|
||||
//console.debug(fn, ln);
|
||||
r = d.createRange();
|
||||
r.setStart(fn, 0);
|
||||
r.setEnd(ln, ln.nodeValue.length);
|
||||
} else
|
||||
r.selectNode(n);
|
||||
} else
|
||||
r.selectNode(n);
|
||||
|
||||
t.setRng(r);
|
||||
}
|
||||
|
||||
return n;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
|
||||
*
|
||||
* @return {bool} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
|
||||
*/
|
||||
isCollapsed : function() {
|
||||
var t = this, r = t.getRng(), s = t.getSel();
|
||||
|
||||
if (!r || r.item)
|
||||
return false;
|
||||
|
||||
return !s || r.boundingWidth == 0 || r.collapsed;
|
||||
},
|
||||
|
||||
/**
|
||||
* Collapse the selection to start or end of range.
|
||||
*
|
||||
* @param {bool} b Optional boolean state if to collapse to end or not. Defaults to start.
|
||||
*/
|
||||
collapse : function(b) {
|
||||
var t = this, r = t.getRng(), n;
|
||||
|
||||
// Control range on IE
|
||||
if (r.item) {
|
||||
n = r.item(0);
|
||||
r = this.win.document.body.createTextRange();
|
||||
r.moveToElementText(n);
|
||||
}
|
||||
|
||||
r.collapse(!!b);
|
||||
t.setRng(r);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the browsers internal selection object.
|
||||
*
|
||||
* @return {Selection} Internal browser selection object.
|
||||
*/
|
||||
getSel : function() {
|
||||
var t = this, w = this.win;
|
||||
|
||||
return w.getSelection ? w.getSelection() : w.document.selection;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the browsers internal range object.
|
||||
*
|
||||
* @return {Range} Internal browser range object.
|
||||
*/
|
||||
getRng : function() {
|
||||
var t = this, s = t.getSel(), r;
|
||||
|
||||
try {
|
||||
if (s)
|
||||
r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : t.win.document.createRange());
|
||||
} catch (ex) {
|
||||
// IE throws unspecified error here if TinyMCE is placed in a frame/iframe
|
||||
}
|
||||
|
||||
// No range found then create an empty one
|
||||
// This can occur when the editor is placed in a hidden container element on Gecko
|
||||
// Or on IE when there was an exception
|
||||
if (!r)
|
||||
r = isIE ? t.win.document.body.createTextRange() : t.win.document.createRange();
|
||||
|
||||
return r;
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes the selection to the specified DOM range.
|
||||
*
|
||||
* @param {Range} r Range to select.
|
||||
*/
|
||||
setRng : function(r) {
|
||||
var s;
|
||||
|
||||
if (!isIE) {
|
||||
s = this.getSel();
|
||||
|
||||
if (s) {
|
||||
s.removeAllRanges();
|
||||
s.addRange(r);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
r.select();
|
||||
} catch (ex) {
|
||||
// Needed for some odd IE bug #1843306
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the current selection to the specified DOM element.
|
||||
*
|
||||
* @param {Element} n Element to set as the contents of the selection.
|
||||
* @return {Element} Returns the element that got passed in.
|
||||
*/
|
||||
setNode : function(n) {
|
||||
var t = this;
|
||||
|
||||
t.setContent(t.dom.getOuterHTML(n));
|
||||
|
||||
return n;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the currently selected element or the common ancestor element for both start and end of the selection.
|
||||
*
|
||||
* @return {Element} Currently selected element or common ancestor element.
|
||||
*/
|
||||
getNode : function() {
|
||||
var t = this, r = t.getRng(), s = t.getSel(), e;
|
||||
|
||||
if (!isIE) {
|
||||
// Range maybe lost after the editor is made visible again
|
||||
if (!r)
|
||||
return t.dom.getRoot();
|
||||
|
||||
e = r.commonAncestorContainer;
|
||||
|
||||
// Handle selection a image or other control like element such as anchors
|
||||
if (!r.collapsed) {
|
||||
// If the anchor node is a element instead of a text node then return this element
|
||||
if (tinymce.isWebKit && s.anchorNode && s.anchorNode.nodeType == 1)
|
||||
return s.anchorNode.childNodes[s.anchorOffset];
|
||||
|
||||
if (r.startContainer == r.endContainer) {
|
||||
if (r.startOffset - r.endOffset < 2) {
|
||||
if (r.startContainer.hasChildNodes())
|
||||
e = r.startContainer.childNodes[r.startOffset];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return t.dom.getParent(e, function(n) {
|
||||
return n.nodeType == 1;
|
||||
});
|
||||
}
|
||||
|
||||
return r.item ? r.item(0) : r.parentElement();
|
||||
},
|
||||
|
||||
destroy : function(s) {
|
||||
var t = this;
|
||||
|
||||
t.win = null;
|
||||
|
||||
// Manual destroy then remove unload handler
|
||||
if (!s)
|
||||
tinymce.removeUnload(t.destroy);
|
||||
}
|
||||
|
||||
/**#@-*/
|
||||
});
|
||||
})();
|
Reference in New Issue
Block a user