@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 436 lines 12 kB view raw
1<?php 2 3final class PhabricatorRemarkupControl 4 extends AphrontFormTextAreaControl { 5 6 private $disableFullScreen = false; 7 private $canPin; 8 private $sendOnEnter = false; 9 private $remarkupMetadata = array(); 10 private $surroundingObject; 11 12 public function setDisableFullScreen($disable) { 13 $this->disableFullScreen = $disable; 14 return $this; 15 } 16 17 /** 18 * Set whether the form can be pinned on the screen 19 * @param bool $can_pin True if the form can be pinned on the screen by the 20 * user 21 * @return $this 22 */ 23 public function setCanPin($can_pin) { 24 $this->canPin = $can_pin; 25 return $this; 26 } 27 28 public function getCanPin() { 29 return $this->canPin; 30 } 31 32 public function setSendOnEnter($soe) { 33 $this->sendOnEnter = $soe; 34 return $this; 35 } 36 37 public function getSendOnEnter() { 38 return $this->sendOnEnter; 39 } 40 41 public function setRemarkupMetadata(array $value) { 42 $this->remarkupMetadata = $value; 43 return $this; 44 } 45 46 public function getRemarkupMetadata() { 47 return $this->remarkupMetadata; 48 } 49 50 /** 51 * Set the type of object in which the control is rendered 52 * @param $object Object class, e.g. 'ManiphestTask' 53 */ 54 public function setSurroundingObject($object) { 55 $this->surroundingObject = $object; 56 return $this; 57 } 58 59 /** 60 * Return the type of object in which this control is rendered 61 * @return object Object class, e.g. 'ManiphestTask' 62 */ 63 public function getSurroundingObject() { 64 return $this->surroundingObject; 65 } 66 67 public function setValue($value) { 68 if ($value instanceof RemarkupValue) { 69 $this->setRemarkupMetadata($value->getMetadata()); 70 $value = $value->getCorpus(); 71 } 72 73 return parent::setValue($value); 74 } 75 76 protected function renderInput() { 77 $id = $this->getID(); 78 if (!$id) { 79 $id = celerity_generate_unique_node_id(); 80 $this->setID($id); 81 } 82 83 $viewer = $this->getUser(); 84 if (!$viewer) { 85 throw new PhutilInvalidStateException('setUser'); 86 } 87 88 // NOTE: Metadata is passed to Javascript in a structured way, and also 89 // dumped directly into the form as an encoded string. This makes it less 90 // likely that we'll lose server-provided metadata (for example, from a 91 // saved draft) if there is a client-side error. 92 93 $metadata_name = $this->getName().'_metadata'; 94 $metadata_value = (object)$this->getRemarkupMetadata(); 95 $metadata_string = phutil_json_encode($metadata_value); 96 97 $metadata_id = celerity_generate_unique_node_id(); 98 $metadata_input = phutil_tag( 99 'input', 100 array( 101 'type' => 'hidden', 102 'id' => $metadata_id, 103 'name' => $metadata_name, 104 'value' => $metadata_string, 105 )); 106 107 // We need to have this if previews render images, since Ajax can not 108 // currently ship JS or CSS. 109 require_celerity_resource('phui-lightbox-css'); 110 111 if (!$this->getDisabled()) { 112 Javelin::initBehavior( 113 'aphront-drag-and-drop-textarea', 114 array( 115 'target' => $id, 116 'remarkupMetadataID' => $metadata_id, 117 'remarkupMetadataValue' => $metadata_value, 118 'activatedClass' => 'aphront-textarea-drag-and-drop', 119 'uri' => '/file/dropupload/', 120 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), 121 )); 122 } 123 124 $root_id = celerity_generate_unique_node_id(); 125 126 $user_datasource = new PhabricatorPeopleDatasource(); 127 $emoji_datasource = new PhabricatorEmojiDatasource(); 128 $proj_datasource = id(new PhabricatorProjectDatasource()) 129 ->setParameters( 130 array( 131 'autocomplete' => 1, 132 )); 133 134 $phriction_datasource = new PhrictionDocumentDatasource(); 135 $phurl_datasource = new PhabricatorPhurlURLDatasource(); 136 137 // Get users involved in surrounding object (if available) 138 $involved_users = null; 139 if ($this->getSurroundingObject() instanceof 140 PhabricatorInvolveeInterface) { 141 $involved_users = $this->getSurroundingObject()->getInvolvedUsers(); 142 } 143 144 Javelin::initBehavior( 145 'phabricator-remarkup-assist', 146 array( 147 'pht' => array( 148 'bold text' => pht('bold text'), 149 'italic text' => pht('italic text'), 150 'monospaced text' => pht('monospaced text'), 151 'List Item' => pht('List Item'), 152 'Quoted Text' => pht('Quoted Text'), 153 'data' => pht('data'), 154 'name' => pht('name'), 155 'URL' => pht('URL'), 156 'key-help' => pht('Pin or unpin the comment form.'), 157 ), 158 'canPin' => $this->getCanPin(), 159 'disabled' => $this->getDisabled(), 160 'sendOnEnter' => $this->getSendOnEnter(), 161 'rootID' => $root_id, 162 'remarkupMetadataID' => $metadata_id, 163 'remarkupMetadataValue' => $metadata_value, 164 'autocompleteMap' => (object)array( 165 64 => array( // "@" 166 'datasourceURI' => $user_datasource->getDatasourceURI(), 167 'headerIcon' => 'fa-user', 168 'headerText' => pht('Find User:'), 169 'hintText' => $user_datasource->getPlaceholderText(), 170 'involvedUsers' => $involved_users, 171 ), 172 35 => array( // "#" 173 'datasourceURI' => $proj_datasource->getDatasourceURI(), 174 'headerIcon' => 'fa-briefcase', 175 'headerText' => pht('Find Project:'), 176 'hintText' => $proj_datasource->getPlaceholderText(), 177 ), 178 58 => array( // ":" 179 'datasourceURI' => $emoji_datasource->getDatasourceURI(), 180 'headerIcon' => 'fa-smile-o', 181 'headerText' => pht('Find Emoji:'), 182 'hintText' => $emoji_datasource->getPlaceholderText(), 183 184 // Cancel on emoticons like ":3". 185 'ignore' => array( 186 '3', 187 '\'', // crying :'( 188 ')', 189 '(', 190 '-', 191 '/', 192 '<', 193 '>', 194 '[', 195 ']', 196 '|', 197 'D', 198// 'p', // Too risky, many emojis starting with lowercase p 199 'P', 200 ), 201 ), 202 91 => array( // "[" 203 'datasourceURI' => $phriction_datasource->getDatasourceURI(), 204 'headerIcon' => 'fa-book', 205 'headerText' => pht('Find Document:'), 206 'hintText' => $phriction_datasource->getPlaceholderText(), 207 'cancel' => array( 208 ':', // Cancel on "http:" and similar. 209 '|', 210 ']', 211 ), 212 'prefix' => '^\\[', 213 ), 214 40 => array( // "(" 215 'datasourceURI' => $phurl_datasource->getDatasourceURI(), 216 'headerIcon' => 'fa-compress', 217 'headerText' => pht('Find Phurl:'), 218 'hintText' => $phurl_datasource->getPlaceholderText(), 219 'cancel' => array( 220 ')', 221 ), 222 'prefix' => '^\\(', 223 ), 224 ), 225 )); 226 Javelin::initBehavior('phabricator-tooltips', array()); 227 228 $actions = array( 229 'fa-bold' => array( 230 'tip' => pht('Bold'), 231 'nodevice' => true, 232 ), 233 'fa-italic' => array( 234 'tip' => pht('Italics'), 235 'nodevice' => true, 236 ), 237 'fa-text-width' => array( 238 'tip' => pht('Monospaced'), 239 'nodevice' => true, 240 ), 241 'fa-link' => array( 242 'tip' => pht('Link'), 243 'nodevice' => true, 244 ), 245 array( 246 'spacer' => true, 247 'nodevice' => true, 248 ), 249 'fa-list-ul' => array( 250 'tip' => pht('Bulleted List'), 251 'nodevice' => true, 252 ), 253 'fa-list-ol' => array( 254 'tip' => pht('Numbered List'), 255 'nodevice' => true, 256 ), 257 'fa-code' => array( 258 'tip' => pht('Code Block'), 259 'nodevice' => true, 260 ), 261 'fa-quote-right' => array( 262 'tip' => pht('Quote'), 263 'nodevice' => true, 264 ), 265 'fa-table' => array( 266 'tip' => pht('Table'), 267 'nodevice' => true, 268 ), 269 'fa-cloud-upload' => array( 270 'tip' => pht('Upload File'), 271 ), 272 ); 273 274 $can_use_macros = function_exists('imagettftext'); 275 276 if ($can_use_macros) { 277 $can_use_macros = PhabricatorApplication::isClassInstalledForViewer( 278 PhabricatorMacroApplication::class, 279 $viewer); 280 } 281 282 if ($can_use_macros) { 283 $actions[] = array( 284 'spacer' => true, 285 ); 286 $actions['fa-meh-o'] = array( 287 'tip' => pht('Meme'), 288 ); 289 } 290 291 $actions['fa-eye'] = array( 292 'tip' => pht('Preview'), 293 'align' => 'right', 294 ); 295 296 $actions['fa-book'] = array( 297 'tip' => pht('Help'), 298 'align' => 'right', 299 'href' => '/reference/remarkup/', 300 ); 301 302 $mode_actions = array(); 303 304 if (!$this->disableFullScreen) { 305 $mode_actions['fa-arrows-alt'] = array( 306 'tip' => pht('Fullscreen Mode'), 307 'align' => 'right', 308 ); 309 } 310 311 if ($this->getCanPin()) { 312 $mode_actions['fa-thumb-tack'] = array( 313 'tip' => pht('Pin Form On Screen'), 314 'align' => 'right', 315 ); 316 } 317 318 if ($mode_actions) { 319 $actions += $mode_actions; 320 } 321 322 $buttons = array(); 323 foreach ($actions as $action => $spec) { 324 325 $classes = array(); 326 327 if (idx($spec, 'align') == 'right') { 328 $classes[] = 'remarkup-assist-right'; 329 } 330 331 if (idx($spec, 'nodevice')) { 332 $classes[] = 'remarkup-assist-nodevice'; 333 } 334 335 if (idx($spec, 'spacer')) { 336 $classes[] = 'remarkup-assist-separator'; 337 $buttons[] = phutil_tag( 338 'span', 339 array( 340 'class' => implode(' ', $classes), 341 ), 342 ''); 343 continue; 344 } else { 345 $classes[] = 'remarkup-assist-button'; 346 } 347 348 if ($action == 'fa-cloud-upload') { 349 $classes[] = 'remarkup-assist-upload'; 350 } 351 352 $href = idx($spec, 'href', '#'); 353 if ($href == '#') { 354 $meta = array('action' => $action); 355 $mustcapture = true; 356 $target = null; 357 } else { 358 $meta = array(); 359 $mustcapture = null; 360 $target = '_blank'; 361 } 362 363 $content = null; 364 365 $tip = idx($spec, 'tip'); 366 if ($tip) { 367 $meta['tip'] = $tip; 368 $content = javelin_tag( 369 'span', 370 array( 371 'aural' => true, 372 ), 373 $tip); 374 } 375 376 $sigils = array(); 377 $sigils[] = 'remarkup-assist'; 378 if (!$this->getDisabled()) { 379 $sigils[] = 'has-tooltip'; 380 } 381 382 $buttons[] = javelin_tag( 383 'a', 384 array( 385 'class' => implode(' ', $classes), 386 'href' => $href, 387 'sigil' => implode(' ', $sigils), 388 'meta' => $meta, 389 'mustcapture' => $mustcapture, 390 'target' => $target, 391 'tabindex' => -1, 392 ), 393 phutil_tag( 394 'div', 395 array( 396 'class' => 397 'remarkup-assist phui-icon-view phui-font-fa bluegrey '.$action, 398 ), 399 $content)); 400 } 401 402 $buttons = phutil_tag( 403 'div', 404 array( 405 'class' => 'remarkup-assist-bar', 406 ), 407 $buttons); 408 409 $use_monospaced = $viewer->compareUserSetting( 410 PhabricatorMonospacedTextareasSetting::SETTINGKEY, 411 PhabricatorMonospacedTextareasSetting::VALUE_TEXT_MONOSPACED); 412 413 if ($use_monospaced) { 414 $monospaced_textareas_class = 'PhabricatorMonospaced'; 415 } else { 416 $monospaced_textareas_class = null; 417 } 418 419 $this->setCustomClass( 420 'remarkup-assist-textarea '.$monospaced_textareas_class); 421 422 return javelin_tag( 423 'div', 424 array( 425 'sigil' => 'remarkup-assist-control', 426 'class' => $this->getDisabled() ? 'disabled-control' : null, 427 'id' => $root_id, 428 ), 429 array( 430 $buttons, 431 parent::renderInput(), 432 $metadata_input, 433 )); 434 } 435 436}