1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
2// See the LICENCE file in the repository root for full licence text.
3
4import { snakeCase } from 'lodash';
5import * as React from 'react';
6import { classWithModifiers, Modifiers } from 'utils/css';
7import { trans } from 'utils/lang';
8import { wikiUrl } from 'utils/url';
9
10type ChangeType = 'cancel' | 'save';
11
12export interface OnChangeProps {
13 event?: React.SyntheticEvent;
14 hasChanged: boolean;
15 type: ChangeType;
16 value: string | undefined;
17}
18
19interface Props {
20 disabled: boolean;
21 ignoreEsc: boolean;
22 modifiers?: Modifiers;
23 onChange: (props: OnChangeProps) => void;
24 placeholder?: string;
25 rawValue: string;
26}
27
28export default class BbcodeEditor extends React.Component<Props> {
29 static readonly defaultProps = {
30 disabled: false,
31 ignoreEsc: false,
32 };
33
34 private readonly bodyRef = React.createRef<HTMLTextAreaElement>();
35 private readonly sizeSelectRef = React.createRef<HTMLSelectElement>();
36
37 readonly cancel = (event?: React.SyntheticEvent) => {
38 if (this.bodyRef.current?.value !== this.props.rawValue && !confirm(trans('common.confirmation_unsaved'))) {
39 return;
40 }
41
42 if (this.bodyRef.current != null) {
43 this.bodyRef.current.value = this.props.rawValue;
44 }
45 this.sendOnChange({ event, type: 'cancel' });
46 };
47
48 componentDidMount() {
49 if (this.sizeSelectRef.current != null) {
50 this.sizeSelectRef.current.value = '';
51 }
52
53 if (this.bodyRef.current != null) {
54 this.bodyRef.current.selectionEnd = 0;
55 this.bodyRef.current.focus();
56 }
57 }
58
59 render() {
60 let blockClass = classWithModifiers('bbcode-editor', this.props.modifiers);
61 blockClass += ' js-bbcode-preview--form';
62
63 return (
64 <form
65 className={blockClass}
66 data-state='write'
67 >
68 <div className='bbcode-editor__content'>
69 <textarea
70 ref={this.bodyRef}
71 className='bbcode-editor__body js-bbcode-preview--body'
72 defaultValue={this.props.rawValue}
73 disabled={this.props.disabled}
74 name='body'
75 onKeyDown={this.onKeyDown}
76 placeholder={this.props.placeholder}
77 />
78
79 <div className='bbcode-editor__preview'>
80 <div className='forum-post-content js-bbcode-preview--preview' />
81 </div>
82
83 <div className='bbcode-editor__buttons-bar'>
84 <div className='bbcode-editor__buttons bbcode-editor__buttons--toolbar'>
85 {this.renderToolbar()}
86 </div>
87
88 <div className='bbcode-editor__buttons bbcode-editor__buttons--actions'>
89 <div className='bbcode-editor__button bbcode-editor__button--cancel'>
90 {this.actionButton(this.cancel, trans('common.buttons.cancel'))}
91 </div>
92 <div className='bbcode-editor__button bbcode-editor__button--hide-on-write'>
93 {this.renderPreviewHideButton()}
94 </div>
95 <div className='bbcode-editor__button bbcode-editor__button--hide-on-preview'>
96 {this.renderPreviewShowButton()}
97 </div>
98 <div className='bbcode-editor__button'>
99 {this.actionButton(this.save, trans('common.buttons.save'), 'forum-primary')}
100 </div>
101 </div>
102 </div>
103 </div>
104 </form>
105 );
106 }
107
108 private actionButton(onClick: (event: React.MouseEvent<HTMLButtonElement>) => void, title: string, modifiers: Modifiers = 'forum-secondary') {
109 return (
110 <button
111 className={classWithModifiers('btn-osu-big', modifiers)}
112 disabled={this.props.disabled}
113 onClick={onClick}
114 type='button'
115 >
116 {title}
117 </button>
118 );
119 }
120
121 private readonly onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
122 if (!this.props.ignoreEsc && e.key === 'Escape') {
123 this.cancel();
124 }
125 };
126
127 private renderPreviewHideButton() {
128 return (
129 <button
130 className='js-bbcode-preview--hide btn-osu-big btn-osu-big--forum-secondary'
131 disabled={this.props.disabled}
132 type='button'
133 >
134 {trans('forum.topic.create.preview_hide')}
135 </button>
136 );
137 }
138
139 private renderPreviewShowButton() {
140 return (
141 <button
142 className='js-bbcode-preview--show btn-osu-big btn-osu-big--forum-secondary'
143 disabled={this.props.disabled}
144 type='button'
145 >
146 {trans('forum.topic.create.preview')}
147 </button>
148 );
149 }
150
151 private renderToolbar() {
152 return (
153 <div className='post-box-toolbar'>
154 {this.toolbarButton('bold', 'fas fa-bold')}
155 {this.toolbarButton('italic', 'fas fa-italic')}
156 {this.toolbarButton('strikethrough', 'fas fa-strikethrough')}
157 {this.toolbarButton('heading', 'fas fa-heading')}
158 {this.toolbarButton('link', 'fas fa-link')}
159 {this.toolbarButton('spoilerbox', 'fas fa-barcode')}
160 {this.toolbarButton('list-numbered', 'fas fa-list-ol')}
161 {this.toolbarButton('list', 'fas fa-list')}
162 {this.toolbarButton('image', 'fas fa-image')}
163 {this.toolbarButton('imagemap', 'fas fa-map')}
164
165 {this.toolbarSizeSelect()}
166
167 <a
168 className='post-box-toolbar__help'
169 href={wikiUrl('BBCode')}
170 rel="noreferrer"
171 target='_blank'
172 >
173 {trans('bbcode.help')}
174 </a>
175 </div>
176 );
177 }
178
179 private readonly save = (event?: React.SyntheticEvent) => {
180 this.sendOnChange({ event, type: 'save' });
181 };
182
183 private sendOnChange({ event, type }: { event?: React.SyntheticEvent; type: ChangeType }) {
184 this.props.onChange({
185 event,
186 hasChanged: this.bodyRef.current?.value !== this.props.rawValue,
187 type,
188 value: this.bodyRef.current?.value,
189 });
190 }
191
192 private toolbarButton(name: string, iconClass: string) {
193 return (
194 <button
195 className={`btn-circle btn-circle--bbcode js-bbcode-btn--${name}`}
196 disabled={this.props.disabled}
197 title={trans(`bbcode.${snakeCase(name)}`)}
198 type='button'
199 >
200 <span className='btn-circle__content'>
201 <span className={iconClass} />
202 </span>
203 </button>
204 );
205 }
206
207 private toolbarSizeSelect() {
208 return (
209 <label
210 className='bbcode-size-select'
211 title={trans('bbcode.size._')}
212 >
213 <span className='bbcode-size-select__label'>
214 {trans('bbcode.size._')}
215 </span>
216
217 <i className='fas fa-chevron-down' />
218
219 <select
220 ref={this.sizeSelectRef}
221 className='bbcode-size-select__select js-bbcode-btn--size'
222 disabled={this.props.disabled}
223 >
224 <option value='50'>{trans('bbcode.size.tiny')}</option>
225 <option value='85'>{trans('bbcode.size.small')}</option>
226 <option value='100'>{trans('bbcode.size.normal')}</option>
227 <option value='150'>{trans('bbcode.size.large')}</option>
228 </select>
229 </label>
230 );
231 }
232}