the browser-facing portion of osu!
at master 7.1 kB view raw
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}