Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / view / form / control / PhabricatorRemarkupControl.php
blob74478c7d51a6b4a5e1c68ae7bcacedcdfa57b426
1 <?php
3 final class PhabricatorRemarkupControl
4 extends AphrontFormTextAreaControl {
6 private $disableMacro = false;
7 private $disableFullScreen = false;
8 private $canPin;
9 private $sendOnEnter = false;
10 private $remarkupMetadata = array();
12 public function setDisableMacros($disable) {
13 $this->disableMacro = $disable;
14 return $this;
17 public function setDisableFullScreen($disable) {
18 $this->disableFullScreen = $disable;
19 return $this;
22 public function setCanPin($can_pin) {
23 $this->canPin = $can_pin;
24 return $this;
27 public function getCanPin() {
28 return $this->canPin;
31 public function setSendOnEnter($soe) {
32 $this->sendOnEnter = $soe;
33 return $this;
36 public function getSendOnEnter() {
37 return $this->sendOnEnter;
40 public function setRemarkupMetadata(array $value) {
41 $this->remarkupMetadata = $value;
42 return $this;
45 public function getRemarkupMetadata() {
46 return $this->remarkupMetadata;
49 public function setValue($value) {
50 if ($value instanceof RemarkupValue) {
51 $this->setRemarkupMetadata($value->getMetadata());
52 $value = $value->getCorpus();
55 return parent::setValue($value);
58 protected function renderInput() {
59 $id = $this->getID();
60 if (!$id) {
61 $id = celerity_generate_unique_node_id();
62 $this->setID($id);
65 $viewer = $this->getUser();
66 if (!$viewer) {
67 throw new PhutilInvalidStateException('setUser');
70 // NOTE: Metadata is passed to Javascript in a structured way, and also
71 // dumped directly into the form as an encoded string. This makes it less
72 // likely that we'll lose server-provided metadata (for example, from a
73 // saved draft) if there is a client-side error.
75 $metadata_name = $this->getName().'_metadata';
76 $metadata_value = (object)$this->getRemarkupMetadata();
77 $metadata_string = phutil_json_encode($metadata_value);
79 $metadata_id = celerity_generate_unique_node_id();
80 $metadata_input = phutil_tag(
81 'input',
82 array(
83 'type' => 'hidden',
84 'id' => $metadata_id,
85 'name' => $metadata_name,
86 'value' => $metadata_string,
87 ));
89 // We need to have this if previews render images, since Ajax can not
90 // currently ship JS or CSS.
91 require_celerity_resource('phui-lightbox-css');
93 if (!$this->getDisabled()) {
94 Javelin::initBehavior(
95 'aphront-drag-and-drop-textarea',
96 array(
97 'target' => $id,
98 'remarkupMetadataID' => $metadata_id,
99 'remarkupMetadataValue' => $metadata_value,
100 'activatedClass' => 'aphront-textarea-drag-and-drop',
101 'uri' => '/file/dropupload/',
102 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(),
106 $root_id = celerity_generate_unique_node_id();
108 $user_datasource = new PhabricatorPeopleDatasource();
109 $emoji_datasource = new PhabricatorEmojiDatasource();
110 $proj_datasource = id(new PhabricatorProjectDatasource())
111 ->setParameters(
112 array(
113 'autocomplete' => 1,
116 $phriction_datasource = new PhrictionDocumentDatasource();
117 $phurl_datasource = new PhabricatorPhurlURLDatasource();
119 Javelin::initBehavior(
120 'phabricator-remarkup-assist',
121 array(
122 'pht' => array(
123 'bold text' => pht('bold text'),
124 'italic text' => pht('italic text'),
125 'monospaced text' => pht('monospaced text'),
126 'List Item' => pht('List Item'),
127 'Quoted Text' => pht('Quoted Text'),
128 'data' => pht('data'),
129 'name' => pht('name'),
130 'URL' => pht('URL'),
131 'key-help' => pht('Pin or unpin the comment form.'),
133 'canPin' => $this->getCanPin(),
134 'disabled' => $this->getDisabled(),
135 'sendOnEnter' => $this->getSendOnEnter(),
136 'rootID' => $root_id,
137 'autocompleteMap' => (object)array(
138 64 => array( // "@"
139 'datasourceURI' => $user_datasource->getDatasourceURI(),
140 'headerIcon' => 'fa-user',
141 'headerText' => pht('Find User:'),
142 'hintText' => $user_datasource->getPlaceholderText(),
144 35 => array( // "#"
145 'datasourceURI' => $proj_datasource->getDatasourceURI(),
146 'headerIcon' => 'fa-briefcase',
147 'headerText' => pht('Find Project:'),
148 'hintText' => $proj_datasource->getPlaceholderText(),
150 58 => array( // ":"
151 'datasourceURI' => $emoji_datasource->getDatasourceURI(),
152 'headerIcon' => 'fa-smile-o',
153 'headerText' => pht('Find Emoji:'),
154 'hintText' => $emoji_datasource->getPlaceholderText(),
156 // Cancel on emoticons like ":3".
157 'ignore' => array(
158 '3',
159 ')',
160 '(',
161 '-',
162 '/',
165 91 => array( // "["
166 'datasourceURI' => $phriction_datasource->getDatasourceURI(),
167 'headerIcon' => 'fa-book',
168 'headerText' => pht('Find Document:'),
169 'hintText' => $phriction_datasource->getPlaceholderText(),
170 'cancel' => array(
171 ':', // Cancel on "http:" and similar.
172 '|',
173 ']',
175 'prefix' => '^\\[',
177 40 => array( // "("
178 'datasourceURI' => $phurl_datasource->getDatasourceURI(),
179 'headerIcon' => 'fa-compress',
180 'headerText' => pht('Find Phurl:'),
181 'hintText' => $phurl_datasource->getPlaceholderText(),
182 'cancel' => array(
183 ')',
185 'prefix' => '^\\(',
189 Javelin::initBehavior('phabricator-tooltips', array());
191 $actions = array(
192 'fa-bold' => array(
193 'tip' => pht('Bold'),
194 'nodevice' => true,
196 'fa-italic' => array(
197 'tip' => pht('Italics'),
198 'nodevice' => true,
200 'fa-text-width' => array(
201 'tip' => pht('Monospaced'),
202 'nodevice' => true,
204 'fa-link' => array(
205 'tip' => pht('Link'),
206 'nodevice' => true,
208 array(
209 'spacer' => true,
210 'nodevice' => true,
212 'fa-list-ul' => array(
213 'tip' => pht('Bulleted List'),
214 'nodevice' => true,
216 'fa-list-ol' => array(
217 'tip' => pht('Numbered List'),
218 'nodevice' => true,
220 'fa-code' => array(
221 'tip' => pht('Code Block'),
222 'nodevice' => true,
224 'fa-quote-right' => array(
225 'tip' => pht('Quote'),
226 'nodevice' => true,
228 'fa-table' => array(
229 'tip' => pht('Table'),
230 'nodevice' => true,
232 'fa-cloud-upload' => array(
233 'tip' => pht('Upload File'),
237 $can_use_macros =
238 (!$this->disableMacro) &&
239 (function_exists('imagettftext'));
241 if ($can_use_macros) {
242 $can_use_macros = PhabricatorApplication::isClassInstalledForViewer(
243 'PhabricatorMacroApplication',
244 $viewer);
247 if ($can_use_macros) {
248 $actions[] = array(
249 'spacer' => true,
251 $actions['fa-meh-o'] = array(
252 'tip' => pht('Meme'),
256 $actions['fa-eye'] = array(
257 'tip' => pht('Preview'),
258 'align' => 'right',
261 $actions['fa-book'] = array(
262 'tip' => pht('Help'),
263 'align' => 'right',
264 'href' => PhabricatorEnv::getDoclink('Remarkup Reference'),
267 $mode_actions = array();
269 if (!$this->disableFullScreen) {
270 $mode_actions['fa-arrows-alt'] = array(
271 'tip' => pht('Fullscreen Mode'),
272 'align' => 'right',
276 if ($this->getCanPin()) {
277 $mode_actions['fa-thumb-tack'] = array(
278 'tip' => pht('Pin Form On Screen'),
279 'align' => 'right',
283 if ($mode_actions) {
284 $actions += $mode_actions;
287 $buttons = array();
288 foreach ($actions as $action => $spec) {
290 $classes = array();
292 if (idx($spec, 'align') == 'right') {
293 $classes[] = 'remarkup-assist-right';
296 if (idx($spec, 'nodevice')) {
297 $classes[] = 'remarkup-assist-nodevice';
300 if (idx($spec, 'spacer')) {
301 $classes[] = 'remarkup-assist-separator';
302 $buttons[] = phutil_tag(
303 'span',
304 array(
305 'class' => implode(' ', $classes),
307 '');
308 continue;
309 } else {
310 $classes[] = 'remarkup-assist-button';
313 if ($action == 'fa-cloud-upload') {
314 $classes[] = 'remarkup-assist-upload';
317 $href = idx($spec, 'href', '#');
318 if ($href == '#') {
319 $meta = array('action' => $action);
320 $mustcapture = true;
321 $target = null;
322 } else {
323 $meta = array();
324 $mustcapture = null;
325 $target = '_blank';
328 $content = null;
330 $tip = idx($spec, 'tip');
331 if ($tip) {
332 $meta['tip'] = $tip;
333 $content = javelin_tag(
334 'span',
335 array(
336 'aural' => true,
338 $tip);
341 $sigils = array();
342 $sigils[] = 'remarkup-assist';
343 if (!$this->getDisabled()) {
344 $sigils[] = 'has-tooltip';
347 $buttons[] = javelin_tag(
348 'a',
349 array(
350 'class' => implode(' ', $classes),
351 'href' => $href,
352 'sigil' => implode(' ', $sigils),
353 'meta' => $meta,
354 'mustcapture' => $mustcapture,
355 'target' => $target,
356 'tabindex' => -1,
358 phutil_tag(
359 'div',
360 array(
361 'class' =>
362 'remarkup-assist phui-icon-view phui-font-fa bluegrey '.$action,
364 $content));
367 $buttons = phutil_tag(
368 'div',
369 array(
370 'class' => 'remarkup-assist-bar',
372 $buttons);
374 $use_monospaced = $viewer->compareUserSetting(
375 PhabricatorMonospacedTextareasSetting::SETTINGKEY,
376 PhabricatorMonospacedTextareasSetting::VALUE_TEXT_MONOSPACED);
378 if ($use_monospaced) {
379 $monospaced_textareas_class = 'PhabricatorMonospaced';
380 } else {
381 $monospaced_textareas_class = null;
384 $this->setCustomClass(
385 'remarkup-assist-textarea '.$monospaced_textareas_class);
387 return javelin_tag(
388 'div',
389 array(
390 'sigil' => 'remarkup-assist-control',
391 'class' => $this->getDisabled() ? 'disabled-control' : null,
392 'id' => $root_id,
394 array(
395 $buttons,
396 parent::renderInput(),
397 $metadata_input,