@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
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}