mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 08:52:39 +01:00
Add assistance buttons to Remarkup text areas
Summary: Unblocker for D3547. Adds markup assist UI (buttons which generate remarkup for you -- not WYSIWYG) to Remarkup text areas. Test Plan: See screenshot. Clicked the buttons a bunch with selected/unselcted text. Results seem broadly reasonable. Reviewers: btrahan, vrana, teisenbe Reviewed By: btrahan CC: aran Maniphest Tasks: T337 Differential Revision: https://secure.phabricator.com/D3594
This commit is contained in:
parent
f182d735e9
commit
8763d0ca93
6 changed files with 304 additions and 42 deletions
|
@ -18,25 +18,112 @@
|
|||
|
||||
final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl {
|
||||
|
||||
public function getCaption() {
|
||||
protected function renderInput() {
|
||||
|
||||
$caption = parent::getCaption();
|
||||
if ($caption) {
|
||||
$caption_suffix = '<br />'.$caption;
|
||||
} else {
|
||||
$caption_suffix = '';
|
||||
}
|
||||
Javelin::initBehavior('phabricator-remarkup-assist', array());
|
||||
|
||||
return phutil_render_tag(
|
||||
'a',
|
||||
$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'),
|
||||
'tabindex' => '-1',
|
||||
'target' => '_blank',
|
||||
),
|
||||
'Formatting Reference') .
|
||||
$caption_suffix;
|
||||
);
|
||||
|
||||
$buttons = array();
|
||||
foreach ($actions as $action => $spec) {
|
||||
if (idx($spec, 'spacer')) {
|
||||
$buttons[] = '<span> </span>';
|
||||
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', '')));
|
||||
}
|
||||
|
||||
$buttons = implode('', $buttons);
|
||||
|
||||
return javelin_render_tag(
|
||||
'div',
|
||||
array(
|
||||
'sigil' => 'remarkup-assist-control',
|
||||
),
|
||||
$buttons.
|
||||
parent::renderInput());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -49,7 +49,9 @@
|
|||
.aphront-form-input input,
|
||||
.aphront-form-input textarea {
|
||||
font-size: 12px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
49
webroot/rsrc/js/application/core/TextAreaUtils.js
Normal file
49
webroot/rsrc/js/application/core/TextAreaUtils.js
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in a new issue