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); + }); + +});