mirror of
https://github.com/TriliumNext/Notes.git
synced 2026-04-24 13:48:53 -05:00
Implement plugin
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
|
||||
|
||||
import MathUI from './mathui';
|
||||
import MathEditing from './mathediting';
|
||||
|
||||
export default class Math extends Plugin {
|
||||
static get requires() {
|
||||
return [ MathEditing, MathUI, Widget ];
|
||||
}
|
||||
|
||||
static get pluginName() {
|
||||
return 'Math';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import Command from '@ckeditor/ckeditor5-core/src/command';
|
||||
import { getSelectedMathModelWidget } from './utils';
|
||||
|
||||
export default class MathCommand extends Command {
|
||||
execute( equation ) {
|
||||
const model = this.editor.model;
|
||||
const selection = model.document.selection;
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
|
||||
model.change( writer => {
|
||||
let mathtex;
|
||||
if ( selectedElement && selectedElement.is( 'mathtex' ) ) {
|
||||
// Update selected element
|
||||
const mode = selectedElement.getAttribute( 'mode' );
|
||||
const display = selectedElement.getAttribute( 'display' );
|
||||
mathtex = writer.createElement( 'mathtex', { equation, mode, display } );
|
||||
} else {
|
||||
// Create new model element
|
||||
mathtex = writer.createElement( 'mathtex', { equation, mode: 'script', display: true } );
|
||||
}
|
||||
model.insertContent( mathtex );
|
||||
writer.setSelection( mathtex, 'on' );
|
||||
} );
|
||||
}
|
||||
|
||||
refresh() {
|
||||
const model = this.editor.model;
|
||||
const selection = model.document.selection;
|
||||
|
||||
const isAllowed = model.schema.checkChild( selection.focus.parent, 'mathtex' );
|
||||
this.isEnabled = isAllowed;
|
||||
|
||||
const selectedEquation = getSelectedMathModelWidget( selection );
|
||||
this.value = selectedEquation ? selectedEquation.getAttribute( 'equation' ) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
|
||||
import { toWidget } from '@ckeditor/ckeditor5-widget/src/utils';
|
||||
import Widget from '@ckeditor/ckeditor5-widget/src/widget';
|
||||
|
||||
import MathCommand from './mathcommand';
|
||||
|
||||
import { renderEquation } from './utils';
|
||||
|
||||
export default class MathEditing extends Plugin {
|
||||
static get requires() {
|
||||
return [ Widget ];
|
||||
}
|
||||
|
||||
static get pluginName() {
|
||||
return 'MathEditing';
|
||||
}
|
||||
|
||||
init() {
|
||||
const editor = this.editor;
|
||||
editor.commands.add( 'math', new MathCommand( editor ) );
|
||||
|
||||
this._defineSchema();
|
||||
this._defineConverters();
|
||||
}
|
||||
|
||||
_defineSchema() {
|
||||
const schema = this.editor.model.schema;
|
||||
|
||||
schema.register( 'mathtex', {
|
||||
allowWhere: '$text',
|
||||
isInline: true,
|
||||
isObject: true,
|
||||
allowAttributes: [ 'equation', 'type', 'display' ]
|
||||
} );
|
||||
}
|
||||
|
||||
_defineConverters() {
|
||||
const conversion = this.editor.conversion;
|
||||
|
||||
|
||||
// View -> Model
|
||||
conversion.for( 'upcast' )
|
||||
// MathJax inline way (e.g. <script type="math/tex">\sqrt{\frac{a}{b}}</script>)
|
||||
.elementToElement( {
|
||||
view: {
|
||||
name: 'script',
|
||||
attributes: {
|
||||
type: 'math/tex'
|
||||
}
|
||||
},
|
||||
model: ( viewElement, modelWriter ) => {
|
||||
const equation = viewElement.getChild( 0 ).data.trim();
|
||||
return modelWriter.createElement( 'mathtex', { equation, type: 'script', display: false } );
|
||||
}
|
||||
} )
|
||||
// MathJax display way (e.g. <script type="math/tex; mode=display">\sqrt{\frac{a}{b}}</script>)
|
||||
.elementToElement( {
|
||||
view: {
|
||||
name: 'script',
|
||||
attributes: {
|
||||
type: 'math/tex; mode=display'
|
||||
}
|
||||
},
|
||||
model: ( viewElement, modelWriter ) => {
|
||||
const equation = viewElement.getChild( 0 ).data.trim();
|
||||
return modelWriter.createElement( 'mathtex', { equation, type: 'script', display: true } );
|
||||
}
|
||||
} )
|
||||
// CKEditor 4 way (e.g. <span class="math-tex">\( \sqrt{\frac{a}{b}} \)</span>)
|
||||
.elementToElement( {
|
||||
view: {
|
||||
name: 'span',
|
||||
classes: [ 'math-tex' ]
|
||||
},
|
||||
model: ( viewElement, modelWriter ) => {
|
||||
let equation = viewElement.getChild( 0 ).data.trim();
|
||||
|
||||
// Remove delimiters (e.g. \( \) or \[ \])
|
||||
const hasInlineDelimiters = equation.includes( '\\(' ) && equation.includes( '\\)' );
|
||||
const hasDisplayDelimiters = equation.includes( '\\[' ) && equation.includes( '\\]' );
|
||||
if ( hasInlineDelimiters || hasDisplayDelimiters ) {
|
||||
equation = equation.substring( 2, equation.length - 2 ).trim();
|
||||
}
|
||||
|
||||
return modelWriter.createElement( 'mathtex', { equation, type: 'span', display: hasDisplayDelimiters } );
|
||||
}
|
||||
} );
|
||||
|
||||
// Model -> View (element)
|
||||
conversion.for( 'editingDowncast' ).elementToElement( {
|
||||
model: 'mathtex',
|
||||
view: ( modelItem, viewWriter ) => {
|
||||
const widgetElement = createMathtexEditingView( modelItem, viewWriter );
|
||||
return toWidget( widgetElement, viewWriter );
|
||||
}
|
||||
} );
|
||||
|
||||
// Model -> Data
|
||||
conversion.for( 'dataDowncast' ).elementToElement( {
|
||||
model: 'mathtex',
|
||||
view: createMathtexView
|
||||
} );
|
||||
|
||||
// Create view for editor
|
||||
function createMathtexEditingView( modelItem, viewWriter ) {
|
||||
const equation = modelItem.getAttribute( 'equation' );
|
||||
const display = modelItem.getAttribute( 'display' );
|
||||
|
||||
|
||||
// CKEngine render multiple times if using span instead of div
|
||||
const mathtexView = viewWriter.createContainerElement( 'div', {
|
||||
style: display ? 'display: block;' : 'display: inline-block;',
|
||||
class: 'mathtex'
|
||||
} );
|
||||
|
||||
// Div is formatted as parent container
|
||||
const uiElement = viewWriter.createUIElement( 'div', null, function( domDocument ) {
|
||||
const domElement = this.toDomElement( domDocument );
|
||||
|
||||
renderEquation( equation, domElement, 'mathjax', display );
|
||||
|
||||
return domElement;
|
||||
} );
|
||||
|
||||
viewWriter.insert( viewWriter.createPositionAt( mathtexView, 0 ), uiElement );
|
||||
|
||||
return mathtexView;
|
||||
}
|
||||
|
||||
// Create view for data
|
||||
function createMathtexView( modelItem, viewWriter ) {
|
||||
const equation = modelItem.getAttribute( 'equation' );
|
||||
const type = modelItem.getAttribute( 'type' );
|
||||
const display = modelItem.getAttribute( 'display' );
|
||||
|
||||
if ( type === 'span' ) {
|
||||
const mathtexView = viewWriter.createContainerElement( 'span', {
|
||||
class: 'math-tex'
|
||||
} );
|
||||
|
||||
if ( display ) {
|
||||
viewWriter.insert( viewWriter.createPositionAt( mathtexView, 0 ), viewWriter.createText( '\\[' + equation + '\\]' ) );
|
||||
} else {
|
||||
viewWriter.insert( viewWriter.createPositionAt( mathtexView, 0 ), viewWriter.createText( '\\(' + equation + '\\)' ) );
|
||||
}
|
||||
|
||||
return mathtexView;
|
||||
} else {
|
||||
const mathtexView = viewWriter.createContainerElement( 'script', {
|
||||
type: display ? 'math/tex; mode=display': 'math/tex'
|
||||
} );
|
||||
|
||||
viewWriter.insert( viewWriter.createPositionAt( mathtexView, 0 ), viewWriter.createText( equation ) );
|
||||
|
||||
return mathtexView;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
|
||||
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
|
||||
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
|
||||
import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
|
||||
|
||||
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
|
||||
import MainFormView from './ui/mainformview';
|
||||
|
||||
// Need math commands from there
|
||||
import MathEditing from './mathediting';
|
||||
|
||||
import pluginIcon from '../theme/icons/icon.svg';
|
||||
|
||||
const mathKeystroke = 'Ctrl+M';
|
||||
|
||||
export default class MathUI extends Plugin {
|
||||
static get requires() {
|
||||
return [ ContextualBalloon, MathEditing ];
|
||||
}
|
||||
|
||||
static get pluginName() {
|
||||
return 'MathUI';
|
||||
}
|
||||
|
||||
init() {
|
||||
const editor = this.editor;
|
||||
editor.editing.view.addObserver( ClickObserver );
|
||||
|
||||
this._form = this._createFormView();
|
||||
|
||||
this._balloon = editor.plugins.get( ContextualBalloon );
|
||||
|
||||
this._createToolbarMathButton();
|
||||
|
||||
this._enableUserBalloonInteractions();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
|
||||
this._form.destroy();
|
||||
}
|
||||
|
||||
_showUI() {
|
||||
const editor = this.editor;
|
||||
const mathCommand = editor.commands.get( 'math' );
|
||||
|
||||
if ( !mathCommand.isEnabled ) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._addFormView();
|
||||
|
||||
this._balloon.showStack( 'main' );
|
||||
}
|
||||
|
||||
_createFormView() {
|
||||
const editor = this.editor;
|
||||
const mathCommand = editor.commands.get( 'math' );
|
||||
const engine = 'mathjax';
|
||||
|
||||
const formView = new MainFormView( editor.locale, engine );
|
||||
|
||||
formView.mathInputView.bind( 'value' ).to( mathCommand, 'value' );
|
||||
|
||||
// Listen to 'submit' button click
|
||||
this.listenTo( formView, 'submit', () => {
|
||||
editor.execute( 'math', formView.equation );
|
||||
this._closeFormView();
|
||||
} );
|
||||
|
||||
// Listen to cancel button click
|
||||
this.listenTo( formView, 'cancel', () => {
|
||||
this._closeFormView();
|
||||
} );
|
||||
|
||||
// Close plugin ui, if esc is pressed (while ui is focused)
|
||||
formView.keystrokes.set( 'esc', ( data, cancel ) => {
|
||||
this._closeFormView();
|
||||
cancel();
|
||||
} );
|
||||
|
||||
return formView;
|
||||
}
|
||||
|
||||
_addFormView() {
|
||||
if ( this._isFormInPanel ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = this.editor;
|
||||
const mathCommand = editor.commands.get( 'math' );
|
||||
|
||||
this._balloon.add( {
|
||||
view: this._form,
|
||||
position: this._getBalloonPositionData(),
|
||||
} );
|
||||
|
||||
if ( this._balloon.visibleView === this._form ) {
|
||||
this._form.mathInputView.select();
|
||||
}
|
||||
|
||||
this._form.equation = mathCommand.value || '';
|
||||
}
|
||||
|
||||
_hideUI() {
|
||||
if ( !this._isFormInPanel ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = this.editor;
|
||||
|
||||
this.stopListening( editor.ui, 'update' );
|
||||
this.stopListening( this._balloon, 'change:visibleView' );
|
||||
|
||||
editor.editing.view.focus();
|
||||
|
||||
// Remove form first because it's on top of the stack.
|
||||
this._removeFormView();
|
||||
}
|
||||
|
||||
_closeFormView() {
|
||||
const mathCommand = this.editor.commands.get( 'math' );
|
||||
if ( mathCommand.value !== undefined ) {
|
||||
this._removeFormView();
|
||||
} else {
|
||||
this._hideUI();
|
||||
}
|
||||
}
|
||||
|
||||
_removeFormView() {
|
||||
if ( this._isFormInPanel ) {
|
||||
this._form.saveButtonView.focus();
|
||||
|
||||
this._balloon.remove( this._form );
|
||||
|
||||
this.editor.editing.view.focus();
|
||||
}
|
||||
}
|
||||
|
||||
_getBalloonPositionData() {
|
||||
const view = this.editor.editing.view;
|
||||
const viewDocument = view.document;
|
||||
const target = view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
|
||||
return { target };
|
||||
}
|
||||
|
||||
_createToolbarMathButton() {
|
||||
const editor = this.editor;
|
||||
const mathCommand = editor.commands.get( 'math' );
|
||||
const t = editor.t;
|
||||
|
||||
// Handle the `Ctrl+M` keystroke and show the panel.
|
||||
editor.keystrokes.set( mathKeystroke, ( keyEvtData, cancel ) => {
|
||||
// Prevent focusing the search bar in FF and opening new tab in Edge. #153, #154.
|
||||
cancel();
|
||||
|
||||
if ( mathCommand.isEnabled ) {
|
||||
this._showUI();
|
||||
}
|
||||
} );
|
||||
|
||||
this.editor.ui.componentFactory.add( 'math', locale => {
|
||||
const button = new ButtonView( locale );
|
||||
|
||||
button.isEnabled = true;
|
||||
button.label = t( 'Insert math' );
|
||||
button.icon = pluginIcon;
|
||||
button.keystroke = mathKeystroke;
|
||||
button.tooltip = true;
|
||||
button.isToggleable = true;
|
||||
|
||||
button.bind( 'isEnabled' ).to( mathCommand, 'isEnabled' );
|
||||
|
||||
this.listenTo( button, 'execute', () => this._showUI() );
|
||||
|
||||
return button;
|
||||
} );
|
||||
}
|
||||
|
||||
_enableUserBalloonInteractions() {
|
||||
// Close the panel on the Esc key press when the editable has focus and the balloon is visible.
|
||||
this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
|
||||
if ( this._isUIVisible ) {
|
||||
this._hideUI();
|
||||
cancel();
|
||||
}
|
||||
} );
|
||||
|
||||
// Close on click outside of balloon panel element.
|
||||
clickOutsideHandler( {
|
||||
emitter: this._form,
|
||||
activator: () => this._isFormInPanel,
|
||||
contextElements: [ this._balloon.view.element ],
|
||||
callback: () => this._hideUI()
|
||||
} );
|
||||
}
|
||||
|
||||
get _isUIVisible() {
|
||||
const visibleView = this._balloon.visibleView;
|
||||
|
||||
return visibleView == this._form;
|
||||
}
|
||||
|
||||
get _isFormInPanel() {
|
||||
return this._balloon.hasView( this._form );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import View from '@ckeditor/ckeditor5-ui/src/view';
|
||||
import ViewCollection from '@ckeditor/ckeditor5-ui/src/viewcollection';
|
||||
|
||||
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
|
||||
import LabeledInputView from '@ckeditor/ckeditor5-ui/src/labeledinput/labeledinputview';
|
||||
import InputTextView from '@ckeditor/ckeditor5-ui/src/inputtext/inputtextview';
|
||||
import LabelView from '@ckeditor/ckeditor5-ui/src/label/labelview';
|
||||
|
||||
import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler';
|
||||
import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker';
|
||||
import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler';
|
||||
|
||||
import checkIcon from '@ckeditor/ckeditor5-core/theme/icons/check.svg';
|
||||
import cancelIcon from '@ckeditor/ckeditor5-core/theme/icons/cancel.svg';
|
||||
|
||||
import submitHandler from '@ckeditor/ckeditor5-ui/src/bindings/submithandler';
|
||||
|
||||
import MathView from './mathview';
|
||||
|
||||
import '../../theme/mathform.css';
|
||||
|
||||
export default class MainFormView extends View {
|
||||
constructor( locale, engine ) {
|
||||
super( locale );
|
||||
|
||||
const t = locale.t;
|
||||
|
||||
// Create key event & focus trackers
|
||||
this._createKeyAndFocusTrackers();
|
||||
|
||||
// Equation input
|
||||
this.mathInputView = this._createMathInput();
|
||||
|
||||
// Preview label
|
||||
this.previewLabel = new LabelView( locale );
|
||||
this.previewLabel.text = t( 'Equation preview' );
|
||||
|
||||
// Math element
|
||||
this.mathView = new MathView( engine, locale );
|
||||
|
||||
// Submit button
|
||||
this.saveButtonView = this._createButton( t( 'Save' ), checkIcon, 'ck-button-save', null );
|
||||
this.saveButtonView.type = 'submit';
|
||||
|
||||
// Cancel button
|
||||
this.cancelButtonView = this._createButton( t( 'Cancel' ), cancelIcon, 'ck-button-cancel', 'cancel' );
|
||||
|
||||
|
||||
// Add UI elements to template
|
||||
this.setTemplate( {
|
||||
tag: 'form',
|
||||
attributes: {
|
||||
class: [
|
||||
'ck',
|
||||
'ck-math-form',
|
||||
],
|
||||
tabindex: '-1'
|
||||
},
|
||||
children: [
|
||||
{
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [
|
||||
'ck-math-view'
|
||||
]
|
||||
},
|
||||
children: [
|
||||
this.mathInputView,
|
||||
this.previewLabel,
|
||||
this.mathView
|
||||
]
|
||||
},
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView,
|
||||
],
|
||||
} );
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render();
|
||||
|
||||
// Prevent default form submit event & trigger custom 'submit'
|
||||
submitHandler( {
|
||||
view: this,
|
||||
} );
|
||||
|
||||
// Register form elements to focusable elements
|
||||
const childViews = [
|
||||
this.mathInputView,
|
||||
this.saveButtonView,
|
||||
this.cancelButtonView,
|
||||
];
|
||||
|
||||
childViews.forEach( v => {
|
||||
this._focusables.add( v );
|
||||
this.focusTracker.add( v.element );
|
||||
} );
|
||||
|
||||
// Listen to keypresses inside form element
|
||||
this.keystrokes.listenTo( this.element );
|
||||
}
|
||||
|
||||
focus() {
|
||||
this._focusCycler.focusFirst();
|
||||
}
|
||||
|
||||
get equation() {
|
||||
return this.mathInputView.inputView.element.value;
|
||||
}
|
||||
|
||||
set equation( equation ) {
|
||||
this.mathInputView.inputView.element.value = equation;
|
||||
this.mathView.value = equation;
|
||||
}
|
||||
|
||||
_createKeyAndFocusTrackers() {
|
||||
this.focusTracker = new FocusTracker();
|
||||
this.keystrokes = new KeystrokeHandler();
|
||||
this._focusables = new ViewCollection();
|
||||
|
||||
this._focusCycler = new FocusCycler( {
|
||||
focusables: this._focusables,
|
||||
focusTracker: this.focusTracker,
|
||||
keystrokeHandler: this.keystrokes,
|
||||
actions: {
|
||||
focusPrevious: 'shift + tab',
|
||||
focusNext: 'tab'
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
_createMathInput() {
|
||||
const t = this.locale.t;
|
||||
|
||||
// Create equation input
|
||||
const mathInput = new LabeledInputView( this.locale, InputTextView );
|
||||
const inputView = mathInput.inputView;
|
||||
mathInput.infoText = t( 'Insert equation in TeX format.' );
|
||||
inputView.on( 'input', () => {
|
||||
this.mathView.value = inputView.element.value;
|
||||
} );
|
||||
|
||||
return mathInput;
|
||||
}
|
||||
|
||||
_createButton( label, icon, className, eventName ) {
|
||||
const button = new ButtonView( this.locale );
|
||||
|
||||
button.set( {
|
||||
label,
|
||||
icon,
|
||||
tooltip: true
|
||||
} );
|
||||
|
||||
button.extendTemplate( {
|
||||
attributes: {
|
||||
class: className
|
||||
}
|
||||
} );
|
||||
|
||||
if ( eventName ) {
|
||||
button.delegate( 'execute' ).to( this, eventName );
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import View from '@ckeditor/ckeditor5-ui/src/view';
|
||||
import { renderEquation } from '../utils';
|
||||
|
||||
export default class MathView extends View {
|
||||
constructor( engine, locale ) {
|
||||
super( locale );
|
||||
|
||||
this.engine = engine;
|
||||
|
||||
this.set( 'value', '' );
|
||||
|
||||
this.on( 'change:value', () => {
|
||||
this.updateMath();
|
||||
} );
|
||||
|
||||
this.setTemplate( {
|
||||
tag: 'div',
|
||||
attributes: {
|
||||
class: [
|
||||
'ck',
|
||||
'ck-math-preview'
|
||||
],
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
updateMath() {
|
||||
renderEquation( this.value, this.element, this.engine );
|
||||
}
|
||||
|
||||
render() {
|
||||
super.render();
|
||||
this.updateMath();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
export function renderEquation( equation, element, engine = 'katex', display = false ) {
|
||||
if ( engine === 'mathjax' && typeof katex !== 'mathjax' ) {
|
||||
if (display) {
|
||||
element.innerHTML = '\\[' + equation + '\\]';
|
||||
} else {
|
||||
element.innerHTML = '\\(' + equation + '\\)';
|
||||
}
|
||||
/* eslint-disable */
|
||||
MathJax.Hub.Queue( [ 'Typeset', MathJax.Hub, element ] );
|
||||
/* eslint-enable */
|
||||
}
|
||||
else if ( engine === 'katex' && typeof katex !== 'undefined' ) {
|
||||
/* eslint-disable */
|
||||
katex.render( equation, element, {
|
||||
throwOnError: false,
|
||||
displayMode: display
|
||||
} );
|
||||
/* eslint-enable */
|
||||
} else {
|
||||
element.innerHTML = equation;
|
||||
console.warn( 'math-tex-typesetting-missing: Missing the mathematical typesetting engine for tex.' );
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectedMathModelWidget( selection ) {
|
||||
const selectedElement = selection.getSelectedElement();
|
||||
|
||||
if ( selectedElement && selectedElement.is( 'mathtex' ) ) {
|
||||
return selectedElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user