(refs #1241) Add new extension point to add completion proposals provider for the textarea

This commit is contained in:
Naoki Takezoe
2016-07-12 19:38:42 +09:00
parent 1496591244
commit d5a9c2c15d
5 changed files with 87 additions and 39 deletions

View File

@@ -0,0 +1,33 @@
package gitbucket.core.plugin
import gitbucket.core.controller.Context
import gitbucket.core.util.EmojiUtil
trait CompletionProposalProvider {
val id: String
val prefix: String
val suffix: String = " "
val values: Seq[String]
def template(implicit context: Context): String = "value"
def additionalScript(implicit context: Context): String = ""
}
class EmojiCompletionProposalProvider extends CompletionProposalProvider {
override val id: String = "emoji"
override val values: Seq[String] = EmojiUtil.emojis.toSeq
override val prefix: String = ":"
override val suffix: String = ": "
override def template(implicit context: Context): String =
s"""'<img src=\"${context.path}/assets/common/images/emojis/' + value + '.png\" class=\"emoji\"></img>' + value"""
}
class UserCompletionProposalProvider extends CompletionProposalProvider {
override val id: String = "user"
override val values: Seq[String] = Nil
override val prefix: String = "@"
override def template(implicit context: Context): String = "'@' + value"
override def additionalScript(implicit context: Context): String =
s"""$$.get('${context.path}/_user/proposals', { query: '' }, function (data) { user = data.options; });"""
}

View File

@@ -169,6 +169,16 @@ abstract class Plugin {
*/
def textDecorators(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[TextDecorator] = Nil
/**
* Override to add completion proposal provider.
*/
val completionProposalProvider: Seq[CompletionProposalProvider] = Nil
/**
* Override to add completion proposal provider.
*/
def completionProposalProvider(registry: PluginRegistry, context: ServletContext, settings: SystemSettings): Seq[CompletionProposalProvider] = Nil
/**
* This method is invoked in initialization of plugin system.
* Register plugin functionality to PluginRegistry.
@@ -219,6 +229,9 @@ abstract class Plugin {
(textDecorators ++ textDecorators(registry, context, settings)).foreach { textDecorator =>
registry.addTextDecorator(textDecorator)
}
(completionProposalProvider ++ completionProposalProvider(registry, context, settings)).foreach { completionProposalProvider =>
registry.addCompletionProposalProvider(completionProposalProvider)
}
}
/**

View File

@@ -43,9 +43,13 @@ class PluginRegistry {
private val dashboardTabs = new ListBuffer[(Context) => Option[Link]]
private val assetsMappings = new ListBuffer[(String, String, ClassLoader)]
private val textDecorators = new ListBuffer[TextDecorator]
// TODO
textDecorators += new TextDecorator {
override def decorate(text: String)(implicit context: Context): String = EmojiUtil.convertEmojis(text)
}
private val completionProposalProviders = new ListBuffer[CompletionProposalProvider]
completionProposalProviders += new EmojiCompletionProposalProvider()
completionProposalProviders += new UserCompletionProposalProvider()
def addPlugin(pluginInfo: PluginInfo): Unit = plugins += pluginInfo
@@ -136,6 +140,10 @@ class PluginRegistry {
def addTextDecorator(textDecorator: TextDecorator): Unit = textDecorators += textDecorator
def getTextDecorators: Seq[TextDecorator] = textDecorators.toSeq
def addCompletionProposalProvider(completionProposalProvider: CompletionProposalProvider): Unit = completionProposalProviders += completionProposalProvider
def getCompletionProposalProviders: Seq[CompletionProposalProvider] = completionProposalProviders.toSeq
}
/**

View File

@@ -315,6 +315,14 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache
case CommitState.FAILURE => "Failed"
}
/**
* Render a given object as the JSON string.
*/
def json(obj: AnyRef): String = {
implicit val formats = org.json4s.DefaultFormats
org.json4s.jackson.Serialization.write(obj)
}
// This pattern comes from: http://stackoverflow.com/a/4390768/1771641 (extract-url-from-string)
private[this] val detectAndRenderLinksRegex = """(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,13}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))""".r

View File

@@ -1,5 +1,6 @@
@(owner: String, repository: String, completion: Seq[String], generateScript: Boolean = true)(textarea: Html)(implicit context: gitbucket.core.controller.Context)
@import gitbucket.core.util.{FileUtil, EmojiUtil}
@import gitbucket.core.view.helpers
<div class="muted attachable">
@textarea
<div class="clickable">Attach images or documents by dragging &amp; dropping, or selecting them.</div>
@@ -7,47 +8,32 @@
@defining("(id=\")([\\w\\-]*)(\")".r.findFirstMatchIn(textarea.body).map(_.group(2))){ textareaId =>
<script>
$(function(){
@if(completion.contains("emoji")){
var emojis = @Html(EmojiUtil.emojis.map("\"" + _ + "\"").mkString("[", ", ", "]"));
}
@if(completion.contains("user")){
var users = [];
$.get('@context.path/_user/proposals', { query: '' }, function (data) { users = data.options; });
}
$('#@textareaId').textcomplete([
@if(completion.contains("emoji")){
{
id: 'emoji',
match: /\B:([\-+\w]*)$/,
search: function (term, callback) {
callback($.map(emojis, function (emoji) {
return emoji.indexOf(term) === 0 ? emoji : null;
}));
},
template: function (value) {
return '<img src="@context.path/assets/common/images/emojis/' + value + '.png" class="emoji"></img>' + value;
},
replace: function (value) {
return ':' + value + ': ';
},
index: 1
},
@gitbucket.core.plugin.PluginRegistry().getCompletionProposalProviders.map { provider =>
@if(completion.contains(provider.id)){ // TODO Filter by context
var @provider.id = @Html(helpers.json(provider.values));
@Html(provider.additionalScript)
}
@if(completion.contains("user")){
{
id: 'user',
match: /\B@@([\-+\w]*)$/,
search: function (term, callback) {
callback($.map(users, function (word) {
return word.indexOf(term) === 0 ? word : null;
}));
}
$('#@textareaId').textcomplete([
@gitbucket.core.plugin.PluginRegistry().getCompletionProposalProviders.map { provider =>
@if(completion.contains(provider.id)){ // TODO Filter by context
{
id: '@{provider.id}',
match: /\B@{provider.prefix}([\-+\w]*)$/,
search: function (term, callback) {
callback($.map(@{provider.id}, function (proposal) {
return proposal.indexOf(term) === 0 ? proposal : null;
}));
},
template: function (value) {
return @{Html(provider.template)};
},
replace: function (value) {
return '@{provider.prefix}' + value + '@{provider.suffix}';
},
index: 1
},
index: 1,
replace: function (word) {
return '@@' + word + ' ';
}
},
}
}
], {
onKeydown: function (e, commands) {