diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php
index b2335d302f..7466547fba 100644
--- a/src/view/form/control/PhabricatorRemarkupControl.php
+++ b/src/view/form/control/PhabricatorRemarkupControl.php
@@ -18,25 +18,112 @@
final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
- public function getCaption() {
+ protected function renderInput() {
- $caption = parent::getCaption();
- if ($caption) {
- $caption_suffix = '
'.$caption;
- } else {
- $caption_suffix = '';
+ Javelin::initBehavior('phabricator-remarkup-assist', array());
+
+ $actions = array(
+ 'b' => array(
+ 'text' => 'B',
+ ),
+ 'i' => array(
+ 'text' => 'I',
+ ),
+ 'tt' => array(
+ 'text' => 'T',
+ ),
+ 's' => array(
+ 'text' => 'S',
+ ),
+ array(
+ 'spacer' => true,
+ ),
+ 'ul' => array(
+ 'text' => "\xE2\x80\xA2",
+ ),
+ 'ol' => array(
+ 'text' => '1.',
+ ),
+ 'code' => array(
+ 'text' => '{}',
+ ),
+ array(
+ 'spacer' => true,
+ ),
+ 'mention' => array(
+ 'text' => '@',
+ ),
+ array(
+ 'spacer' => true,
+ ),
+ 'h1' => array(
+ 'text' => 'H',
+ ),
+ array(
+ 'spacer' => true,
+ ),
+ 'help' => array(
+ 'align' => 'right',
+ 'text' => '?',
+ 'href' => PhabricatorEnv::getDoclink(
+ 'article/Remarkup_Reference.html'),
+ ),
+ );
+
+ $buttons = array();
+ foreach ($actions as $action => $spec) {
+ if (idx($spec, 'spacer')) {
+ $buttons[] = ' ';
+ continue;
+ }
+
+ $classes = array();
+ $classes[] = 'button';
+ $classes[] = 'grey';
+ $classes[] = 'remarkup-assist-button';
+ if (idx($spec, 'align') == 'right') {
+ $classes[] = 'remarkup-assist-right';
+ }
+
+ $href = idx($spec, 'href', '#');
+ if ($href == '#') {
+ $meta = array('action' => $action);
+ $mustcapture = true;
+ $target = null;
+ } else {
+ $meta = null;
+ $mustcapture = null;
+ $target = '_blank';
+ }
+
+ $buttons[] = javelin_render_tag(
+ 'a',
+ array(
+ 'class' => implode(' ', $classes),
+ 'href' => $href,
+ 'sigil' => 'remarkup-assist',
+ 'meta' => $meta,
+ 'mustcapture' => $mustcapture,
+ 'target' => $target,
+ 'tabindex' => -1,
+ ),
+ phutil_render_tag(
+ 'div',
+ array(
+ 'class' => 'remarkup-assist remarkup-assist-'.$action,
+ ),
+ idx($spec, 'text', '')));
}
- return phutil_render_tag(
- 'a',
+ $buttons = implode('', $buttons);
+
+ return javelin_render_tag(
+ 'div',
array(
- 'href' => PhabricatorEnv::getDoclink(
- 'article/Remarkup_Reference.html'),
- 'tabindex' => '-1',
- 'target' => '_blank',
+ 'sigil' => 'remarkup-assist-control',
),
- 'Formatting Reference') .
- $caption_suffix;
+ $buttons.
+ parent::renderInput());
}
}
diff --git a/webroot/rsrc/css/aphront/form-view.css b/webroot/rsrc/css/aphront/form-view.css
index 242f979757..0b36bb4bb9 100644
--- a/webroot/rsrc/css/aphront/form-view.css
+++ b/webroot/rsrc/css/aphront/form-view.css
@@ -49,7 +49,9 @@
.aphront-form-input input,
.aphront-form-input textarea {
font-size: 12px;
+ display: block;
width: 100%;
+ box-sizing: border-box;
}
diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css
index ed6c88e570..527b3b6185 100644
--- a/webroot/rsrc/css/core/remarkup.css
+++ b/webroot/rsrc/css/core/remarkup.css
@@ -246,3 +246,64 @@ img.phabricator-remarkup-embed-image {
background: #ffffff;
padding: 3px 6px;
}
+
+.remarkup-assist-bar {
+ padding: 2px 0;
+}
+
+a.remarkup-assist-button {
+ padding-left: 4px;
+ padding-right: 4px;
+ padding-bottom: 4px;
+ margin-bottom: 3px;
+
+ position: relative;
+ overflow: hidden;
+ height: 16px;
+ width: 16px;
+}
+
+a.remarkup-assist-button + a.remarkup-assist-button {
+ border-left-width: 0px;
+}
+
+.remarkup-assist {
+ float: left;
+ height: 16px;
+ width: 16px;
+ font-weight: normal;
+}
+
+.remarkup-assist-right {
+ float: right;
+}
+
+.remarkup-assist-b {
+ font-family: "Georgia", serif;
+ font-weight: bold;
+}
+
+.remarkup-assist-i {
+ font-family: "Georgia", serif;
+ font-style: italic;
+}
+
+.remarkup-assist-code,
+.remarkup-assist-tt {
+ text-align: center;
+ font-family: monospace;
+}
+
+.remarkup-assist-s {
+ font-family: "Georgia", serif;
+ text-decoration: line-through;
+}
+
+.remarkup-assist-ol {
+ font-family: "Georgia", serif;
+}
+
+.remarkup-assist-h1 {
+ font-family: "Georgia", serif;
+ font-weight: bold;
+}
diff --git a/webroot/rsrc/js/application/core/TextAreaUtils.js b/webroot/rsrc/js/application/core/TextAreaUtils.js
new file mode 100644
index 0000000000..9beac8b1d0
--- /dev/null
+++ b/webroot/rsrc/js/application/core/TextAreaUtils.js
@@ -0,0 +1,49 @@
+/**
+ * @requires javelin-install
+ * @provides phabricator-textareautils
+ * @javelin
+ */
+
+JX.install('TextAreaUtils', {
+ statics : {
+ getSelectionRange : function(area) {
+ var v = area.value;
+
+ // NOTE: This works well in Safari, Firefox and Chrome. We'll probably get
+ // less-good behavior on IE.
+
+ var s = v.length;
+ var e = v.length;
+
+ if ('selectionStart' in area) {
+ s = area.selectionStart;
+ e = area.selectionEnd;
+ }
+
+ return {start: s, end: e};
+ },
+
+ getSelectionText : function(area) {
+ var v = area.value;
+ var r = JX.TextAreaUtils.getSelectionRange(area);
+ return v.substring(r.start, r.end);
+ },
+
+ setSelectionRange : function(area, start, end) {
+ if ('setSelectionRange' in area) {
+ area.focus();
+ area.setSelectionRange(start, end);
+ }
+ },
+
+ setSelectionText : function(area, text) {
+ var v = area.value;
+ var r = JX.TextAreaUtils.getSelectionRange(area);
+
+ v = v.substring(0, r.start) + text + v.substring(r.end, v.length);
+ area.value = v;
+
+ JX.TextAreaUtils.setSelectionRange(area, r.start, r.start + text.length);
+ }
+ }
+});
diff --git a/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js b/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js
index 2b7a88f875..8e9e02d442 100644
--- a/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js
+++ b/webroot/rsrc/js/application/core/behavior-drag-and-drop-textarea.js
@@ -4,6 +4,7 @@
* javelin-dom
* phabricator-drag-and-drop-file-upload
* phabricator-paste-file-upload
+ * phabricator-textareautils
*/
JX.behavior('aphront-drag-and-drop-textarea', function(config) {
@@ -11,34 +12,7 @@ JX.behavior('aphront-drag-and-drop-textarea', function(config) {
var target = JX.$(config.target);
function onupload(f) {
- var v = target.value;
- var insert = '{F' + f.id + '}';
-
- // NOTE: This works well in Safari, Firefox and Chrome. We'll probably get
- // less-good behavior on IE, but I think IE doesn't support drag-and-drop
- // or paste uploads anyway.
-
- // Set the insert position to the end of the text, so we get reasonable
- // default behavior.
- var s = v.length;
- var e = v.length;
-
- // If possible, refine the insert position based on the current selection.
- if ('selectionStart' in target) {
- s = target.selectionStart;
- e = target.selectionEnd;
- }
-
- // Build the new text.
- v = v.substring(0, s) + insert + v.substring(e, v.length);
- // Replace the current value with the new text.
- target.value = v;
-
- // If possible, place the cursor after the inserted text.
- if ('setSelectionRange' in target) {
- target.focus();
- target.setSelectionRange(s + insert.length, s + insert.length);
- }
+ JX.TextAreaUtils.setSelectionText(target, '{F' + f.id + '}');
}
if (JX.PhabricatorDragAndDropFileUpload.isSupported()) {
diff --git a/webroot/rsrc/js/application/core/behavior-phabricator-remarkup-assist.js b/webroot/rsrc/js/application/core/behavior-phabricator-remarkup-assist.js
new file mode 100644
index 0000000000..e5b56167d8
--- /dev/null
+++ b/webroot/rsrc/js/application/core/behavior-phabricator-remarkup-assist.js
@@ -0,0 +1,89 @@
+/**
+ * @provides javelin-behavior-phabricator-remarkup-assist
+ * @requires javelin-behavior
+ * javelin-stratcom
+ * javelin-dom
+ * phabricator-textareautils
+ */
+
+JX.behavior('phabricator-remarkup-assist', function(config) {
+
+ function update(area, l, m, r) {
+ // Replace the selection with the entire assisted text.
+ JX.TextAreaUtils.setSelectionText(area, l + m + r);
+
+ // Now, select just the middle part. For instance, if the user clicked
+ // "B" to create bold text, we insert '**bold**' but just select the word
+ // "bold" so if they type stuff they'll be editing the bold text.
+ var r = JX.TextAreaUtils.getSelectionRange(area);
+ JX.TextAreaUtils.setSelectionRange(
+ area,
+ r.start + l.length,
+ r.start + l.length + m.length);
+ }
+
+ function assist(area, action) {
+ // If the user has some text selected, we'll try to use that (for example,
+ // if they have a word selected and want to bold it). Otherwise we'll insert
+ // generic text.
+ var sel = JX.TextAreaUtils.getSelectionText(area);
+ var r = JX.TextAreaUtils.getSelectionRange(area);
+
+ switch (action) {
+ case 'b':
+ update(area, '**', sel || 'bold text', '**');
+ break;
+ case 'i':
+ update(area, '//', sel || 'italic text', '//');
+ break;
+ case 'tt':
+ update(area, '`', sel || 'monospaced text', '`');
+ break;
+ case 's':
+ update(area, '~~', sel || 'strikethrough text', '~~');
+ break;
+ case 'ul':
+ case 'ol':
+ var ch = (action == 'ol') ? ' # ' : ' - ';
+ if (sel) {
+ sel = sel.split("\n");
+ } else {
+ sel = ["List Item"];
+ }
+ sel = sel.join("\n" + ch);
+ update(area, ((r.start == 0) ? "" : "\n\n") + ch, sel, "\n\n");
+ break;
+ case 'code':
+ sel = sel || "foreach ($list as $item) {\n work_miracles($item);\n}";
+ sel = sel.split("\n");
+ sel = " " + sel.join("\n ");
+ update(area, ((r.start == 0) ? "" : "\n\n"), sel, "\n\n");
+ break;
+ case 'mention':
+ update(area, '@', sel || 'username', '');
+ break;
+ case 'h1':
+ sel = sel || 'Header';
+ update(area, ((r.start == 0) ? "" : "\n\n") + "= ", sel, " =\n\n");
+ break;
+ }
+ }
+
+ JX.Stratcom.listen(
+ ['click'],
+ 'remarkup-assist',
+ function(e) {
+ var data = e.getNodeData('remarkup-assist');
+ if (!data) {
+ return;
+ }
+
+ e.kill();
+
+ var root = e.getNode('remarkup-assist-control');
+ var area = JX.DOM.find(root, 'textarea');
+
+ assist(area, data.action);
+ });
+
+});