Page MenuHomePhorge

behavior-phabricator-remarkup-assist.js
No OneTemporary

behavior-phabricator-remarkup-assist.js

/**
* @provides javelin-behavior-phabricator-remarkup-assist
* @requires javelin-behavior
* javelin-stratcom
* javelin-dom
* phabricator-phtize
* phabricator-textareautils
* phabricator-remarkup-metadata
* javelin-workflow
* javelin-vector
* phuix-autocomplete
* javelin-mask
*/
JX.behavior('phabricator-remarkup-assist', function(config) {
var pht = JX.phtize(config.pht);
var root = JX.$(config.rootID);
var area = JX.DOM.find(root, 'textarea');
var edit_mode = 'normal';
var edit_root = null;
var preview = null;
var pinned = false;
// When we pin the comment area to the bottom of the window, we need to put
// an extra spacer element at the bottom of the document so that it is
// possible to scroll down far enough to see content at the end. Otherwise,
// the last part of the document will be hidden behind the comment area when
// the document is fully scrolled.
var pinned_spacer = JX.$N(
'div',
{className: 'remarkup-assist-pinned-spacer'});
function set_edit_mode(root, mode) {
if (mode == edit_mode) {
return;
}
// First, disable any active mode.
if (edit_root) {
if (edit_mode == 'fullscreen') {
JX.DOM.alterClass(edit_root, 'remarkup-control-fullscreen-mode', false);
JX.DOM.alterClass(document.body, 'remarkup-fullscreen-mode', false);
JX.Mask.hide('jx-light-mask');
}
area.style.height = '';
// If we're in preview mode, kick the preview back down to default
// size.
if (preview) {
JX.DOM.show(area);
resize_preview();
JX.DOM.hide(area);
}
}
edit_root = root;
edit_mode = mode;
// Now, apply the new mode.
if (mode == 'fullscreen') {
JX.DOM.alterClass(edit_root, 'remarkup-control-fullscreen-mode', true);
JX.DOM.alterClass(document.body, 'remarkup-fullscreen-mode', true);
JX.Mask.show('jx-light-mask');
// If we're in preview mode, expand the preview to full-size.
if (preview) {
JX.DOM.show(area);
}
resizearea();
if (preview) {
resize_preview();
JX.DOM.hide(area);
}
}
JX.DOM.focus(area);
}
function set_pinned_mode(root, mode) {
if (mode === pinned) {
return;
}
pinned = mode;
var container = get_pinned_container(root);
JX.DOM.alterClass(container, 'remarkup-assist-pinned', pinned);
if (pinned) {
JX.DOM.appendContent(document.body, pinned_spacer);
} else {
JX.DOM.remove(pinned_spacer);
}
resizearea();
JX.DOM.focus(area);
}
function get_pinned_container(root) {
return JX.DOM.findAbove(root, 'div', 'phui-comment-form');
}
function resizearea() {
// If we're in the pinned comment mode, resize the pinned spacer to be the
// same size as the pinned form. This allows users to scroll to the bottom
// of the document by creating extra footer space to scroll through.
if (pinned) {
var container = get_pinned_container(root);
var d = JX.Vector.getDim(container);
d.x = null;
d.setDim(pinned_spacer);
}
if (!edit_root) {
return;
}
if (edit_mode != 'fullscreen') {
return;
}
// In Firefox, a textarea with position "absolute" or "fixed", anchored
// "top" and "bottom", and height "auto" renders as two lines high. Force
// it to the correct height with Javascript.
var v = JX.Vector.getViewport();
v.x = null;
v.y -= 26;
v.setDim(area);
}
JX.Stratcom.listen('resize', null, resizearea);
JX.Stratcom.listen('keydown', null, function(e) {
if (e.getSpecialKey() != 'esc') {
return;
}
if (edit_mode != 'fullscreen') {
return;
}
e.kill();
set_edit_mode(edit_root, 'normal');
set_pinned_mode(root, false);
});
function update(area, l, m, r) {
// Replace the selection with the entire assisted text.
JX.TextAreaUtils.setSelectionText(area, l + m + r, true);
// 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 range = JX.TextAreaUtils.getSelectionRange(area);
JX.TextAreaUtils.setSelectionRange(
area,
range.start + l.length,
range.start + l.length + m.length);
}
function prepend_char_to_lines(ch, sel, def) {
if (sel) {
sel = sel.split('\n');
} else {
sel = [def];
}
if (ch === '>') {
for(var i=0; i < sel.length; i++) {
if (sel[i][0] === '>') {
ch = '>';
} else {
ch = '> ';
}
sel[i] = ch + sel[i];
}
return sel.join('\n');
}
return sel.join('\n' + ch);
}
function assist(area, action, root, button) {
// 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);
var ch;
switch (action) {
case 'fa-bold':
update(area, '**', sel || pht('bold text'), '**');
break;
case 'fa-italic':
update(area, '//', sel || pht('italic text'), '//');
break;
case 'fa-link':
var name = pht('name');
if (/^https?:/i.test(sel)) {
update(area, '[[ ' + sel + ' | ', name, ' ]]');
} else {
update(area, '[[ ', pht('URL'), ' | ' + (sel || name) + ' ]]');
}
break;
case 'fa-text-width':
update(area, '`', sel || pht('monospaced text'), '`');
break;
case 'fa-list-ul':
case 'fa-list-ol':
ch = (action == 'fa-list-ol') ? ' # ' : ' - ';
sel = prepend_char_to_lines(ch, sel, pht('List Item'));
update(area, ((r.start === 0) ? '' : '\n\n') + ch, sel, '\n\n');
break;
case 'fa-code':
sel = sel || 'foreach ($list as $item) {\n work_miracles($item);\n}';
var code_prefix = (r.start === 0) ? '' : '\n';
update(area, code_prefix + '```\n', sel, '\n```');
break;
case 'fa-quote-right':
ch = '>';
sel = prepend_char_to_lines(ch, sel, pht('Quoted Text'));
update(area, ((r.start === 0) ? '' : '\n\n'), sel, '\n\n');
break;
case 'fa-table':
var table_prefix = (r.start === 0 ? '' : '\n\n');
update(area, table_prefix + '| ', sel || pht('data'), ' |');
break;
case 'fa-meh-o':
new JX.Workflow('/macro/meme/create/')
.setHandler(function(response) {
update(
area,
'',
sel,
(r.start === 0 ? '' : '\n\n') + response.text + '\n\n');
})
.start();
break;
case 'fa-cloud-upload':
new JX.Workflow('/file/uploaddialog/')
.setHandler(function(response) {
var files = response.files;
for (var ii = 0; ii < files.length; ii++) {
var file = files[ii];
var upload = new JX.PhabricatorFileUpload()
.setID(file.id)
.setPHID(file.phid)
.setURI(file.uri);
JX.TextAreaUtils.insertFileReference(area, upload);
var metadata = new JX.RemarkupMetadata(
config.remarkupMetadataValue, config.remarkupMetadataID);
var phids = metadata.getMetadata('attachedFilePHIDs', []);
phids.push(file.phid);
metadata.setMetadata('attachedFilePHIDs', phids);
}
})
.start();
break;
case 'fa-arrows-alt':
set_pinned_mode(root, false);
if (edit_mode == 'fullscreen') {
set_edit_mode(root, 'normal');
} else {
set_edit_mode(root, 'fullscreen');
}
break;
case 'fa-eye':
if (!preview) {
preview = JX.$N(
'div',
{
className: 'remarkup-inline-preview'
},
null);
area.parentNode.insertBefore(preview, area);
JX.DOM.alterClass(button, 'preview-active', true);
JX.DOM.alterClass(root, 'remarkup-preview-active', true);
resize_preview();
JX.DOM.hide(area);
update_preview();
} else {
JX.DOM.show(area);
resize_preview(true);
JX.DOM.remove(preview);
preview = null;
JX.DOM.alterClass(button, 'preview-active', false);
JX.DOM.alterClass(root, 'remarkup-preview-active', false);
}
break;
case 'fa-thumb-tack':
// If we're pinning, kick us out of fullscreen mode first.
set_edit_mode(edit_root, 'normal');
// Now pin or unpin the area.
set_pinned_mode(root, !pinned);
break;
}
}
function resize_preview(restore) {
if (!preview) {
return;
}
var src;
var dst;
if (restore) {
src = preview;
dst = area;
} else {
src = area;
dst = preview;
}
var d = JX.Vector.getDim(src);
d.x = null;
d.setDim(dst);
}
function update_preview() {
var value = area.value;
var data = {
text: value
};
var onupdate = function(r) {
if (area.value !== value) {
return;
}
if (!preview) {
return;
}
JX.DOM.setContent(preview, JX.$H(r.content).getFragment());
};
new JX.Workflow('/transactions/remarkuppreview/', data)
.setHandler(onupdate)
.start();
}
JX.DOM.listen(
root,
'click',
'remarkup-assist',
function(e) {
var data = e.getNodeData('remarkup-assist');
if (!data.action) {
return;
}
e.kill();
if (config.disabled) {
return;
}
assist(area, data.action, root, e.getNode('remarkup-assist'));
});
var autocomplete = new JX.PHUIXAutocomplete()
.setArea(area);
for (var k in config.autocompleteMap) {
autocomplete.addAutocomplete(k, config.autocompleteMap[k]);
}
autocomplete.start();
if (config.canPin) {
new JX.KeyboardShortcut('z', pht('key-help'))
.setGroup('xactions')
.setHandler(function() {
set_pinned_mode(root, !pinned);
})
.register();
}
if (config.sendOnEnter) {
// Send on enter if the shift key is not held.
JX.DOM.listen(area, 'keydown', null,
function(e) {
if (e.getSpecialKey() != 'return') {
return;
}
// Let other listeners (particularly the inline autocomplete) have a
// chance to handle this event.
if (JX.Stratcom.pass()) {
return;
}
var raw = e.getRawEvent();
if (raw.shiftKey) {
// If the shift key is pressed, let the browser write a newline into
// the textarea.
return;
}
if (edit_mode == 'fullscreen') {
// Don't send on enter in fullscreen
return;
}
// From here on, interpret this as a "send" action, not a literal
// newline.
e.kill();
// This allows 'workflow' and similar actions to take effect.
// Such as pontificate in Conpherence
var form = e.getNode('tag:form');
JX.DOM.invoke(form, 'didSyntheticSubmit');
});
}
});

File Metadata

Mime Type
text/plain
Expires
Jan 19 2025, 22:02 (6 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129204
Default Alt Text
behavior-phabricator-remarkup-assist.js (11 KB)

Event Timeline