jenkins-bot has submitted this change and it was merged.
Change subject: Refactor and implement mention inspector
......................................................................
Refactor and implement mention inspector
* Refactor to add a target (inheriting from sa); this allows customizing
the toolbar. It is now limited to three items, plus the menu.
* Simplify module handling. The main module is still lazy loaded, and
that just uses normal dependencies.
Also, dropped handling the experimental flag for now. Most of the
experiments would probably not be applicable, and this is still
experimental itself.
* Implement the initial inspector, with:
* Toolbar item
* Auto-complete based on topic authors (case-insensitive, anywhere
in the string)
* Full edit and remove support (for remove, both via back-spacing
and by button)
* Validation that user exists, with appropriate error messages
if not.
Depends on:
* https://gerrit.wikimedia.org/r/#/c/197464/ (mw.util.isIPAddress)
* https://gerrit.wikimedia.org/r/197475 (suggestions when blank)
* https://gerrit.wikimedia.org/r/197862 (Jenkins depend on VE)
* https://gerrit.wikimedia.org/r/#/c/197835/ (Update OOjs to v1.1.6),
for OO.unique
Bug: T90764
Bug: T92588
Change-Id: I0a14f4779c73996d4cb063b27f3330def0baae6d
---
M Hooks.php
M Resources.php
M i18n/en.json
M i18n/qqq.json
R modules/editor/editors/visualeditor/ext.flow.editors.visualeditor.js
A modules/editor/editors/visualeditor/mw.flow.ve.CommandRegistry.js
A modules/editor/editors/visualeditor/mw.flow.ve.Target.js
A modules/editor/editors/visualeditor/mw.flow.ve.Target.less
A
modules/editor/editors/visualeditor/ui/contextitem/mw.flow.ve.ui.MentionContextItem.js
A modules/editor/editors/visualeditor/ui/images/icons/flow-mention.svg
A
modules/editor/editors/visualeditor/ui/inspectors/mw.flow.ve.ui.MentionInspector.js
A modules/editor/editors/visualeditor/ui/mw.flow.ve.ui.Icons.less
A
modules/editor/editors/visualeditor/ui/tools/mw.flow.ve.ui.MentionInspectorTool.js
A
modules/editor/editors/visualeditor/ui/widgets/mw.flow.ve.ui.MentionTargetInputWidget.js
A modules/engine/components/board/features/flow-board-visualeditor.js
M modules/engine/components/board/flow-board.js
16 files changed, 928 insertions(+), 67 deletions(-)
Approvals:
EBernhardson: Looks good to me, approved
jenkins-bot: Verified
diff --git a/Hooks.php b/Hooks.php
index 43080da..442d465 100644
--- a/Hooks.php
+++ b/Hooks.php
@@ -578,11 +578,13 @@
return true;
}
+ // Static variables that do not vary by request
public static function onResourceLoaderGetConfigVars( &$vars ) {
global $wgFlowEditorList;
$vars['wgFlowEditorList'] = $wgFlowEditorList;
$vars['wgFlowMaxTopicLength'] =
Flow\Model\PostRevision::MAX_TOPIC_LENGTH;
+ $vars['wgFlowMentionTemplate'] = wfMessage(
'flow-ve-mention-template' )->inContentLanguage()->plain();
return true;
}
diff --git a/Resources.php b/Resources.php
index 9431315..c1483df 100644
--- a/Resources.php
+++ b/Resources.php
@@ -355,6 +355,8 @@
'engine/components/board/features/flow-board-navigation.js',
// Feature: Table of Contents
'engine/components/board/features/flow-board-toc.js',
+ // Feature: VisualEditor
+
'engine/components/board/features/flow-board-visualeditor.js',
// Component: FlowBoardHistoryComponent
'engine/components/board/flow-boardhistory.js',
@@ -425,8 +427,50 @@
'editor/editors/ext.flow.editors.none.js',
),
) + $mobile,
+
+ // Basically this is just all the Flow-specific VE stuff, except
ext.flow.editors.visualeditor.js,
+ // That needs to register itself even if the browser doesn't support VE
(so we can tell
+ // the editor dispatcher that). But we want to reduce what we load if
the browser can't actually
+ // use VE.
+ 'ext.flow.visualEditor' => $flowResourceTemplate + array(
+ 'scripts' => array(
+ 'editor/editors/visualeditor/mw.flow.ve.Target.js',
+
'editor/editors/visualeditor/ui/inspectors/mw.flow.ve.ui.MentionInspector.js',
+
'editor/editors/visualeditor/ui/tools/mw.flow.ve.ui.MentionInspectorTool.js',
+ // MentionInspectorTool must be after MentionInspector
and before MentionContextItem.
+
'editor/editors/visualeditor/ui/contextitem/mw.flow.ve.ui.MentionContextItem.js',
+
'editor/editors/visualeditor/ui/widgets/mw.flow.ve.ui.MentionTargetInputWidget.js',
+
'editor/editors/visualeditor/mw.flow.ve.CommandRegistry.js',
+ ),
+ 'styles' => array(
+ 'editor/editors/visualeditor/mw.flow.ve.Target.less',
+
'editor/editors/visualeditor/ui/mw.flow.ve.ui.Icons.less',
+ ),
+ 'dependencies' => array(
+ 'ext.visualEditor.core',
+ 'ext.visualEditor.core.desktop',
+ 'ext.visualEditor.data',
+ 'ext.visualEditor.icons',
+ // See comment at bottom of mw.flow.ve.Target.js.
+ 'ext.visualEditor.mediawiki',
+ 'ext.visualEditor.mwlink',
+ 'ext.visualEditor.mwtransclusion',
+ 'ext.visualEditor.standalone',
+ 'site',
+ 'user',
+ 'mediawiki.api',
+ ),
+ 'messages' => array(
+ 'flow-ve-mention-context-item-label',
+ 'flow-ve-mention-inspector-title',
+ 'flow-ve-mention-inspector-remove-label',
+ 'flow-ve-mention-inspector-invalid-user',
+ 'flow-ve-mention-tool-title',
+ ),
+ ),
+
'ext.flow.editors.visualeditor' => $flowResourceTemplate + array(
- 'scripts' => 'editor/editors/ext.flow.editors.visualeditor.js',
+ 'scripts' =>
'editor/editors/visualeditor/ext.flow.editors.visualeditor.js',
'dependencies' => array(
'jquery.spinner',
// ve dependencies will be loaded via JS
diff --git a/i18n/en.json b/i18n/en.json
index 41f14bb..3eff608 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -532,5 +532,11 @@
"flow-undo-edit-failure": "The edit could not be undone due to
conflicting intermediate edits.",
"group-flow-bot": "Flow bots",
"group-flow-bot-member": "Flow bot",
- "grouppage-flow-bot": "Project:Flow bots"
+ "grouppage-flow-bot": "Project:Flow bots",
+ "flow-ve-mention-context-item-label": "Mention",
+ "flow-ve-mention-inspector-title": "Mention",
+ "flow-ve-mention-inspector-remove-label": "Remove",
+ "flow-ve-mention-tool-title": "Mention a user",
+ "flow-ve-mention-template": "ping",
+ "flow-ve-mention-inspector-invalid-user": "The username '$1' is not
registered."
}
diff --git a/i18n/qqq.json b/i18n/qqq.json
index a21d048..e606049 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -534,5 +534,11 @@
"flow-undo-edit-failure": "Error message shown on undo pages when the
revision cannot be directly undone.",
"group-flow-bot": "{{doc-group|flow-bot}}",
"group-flow-bot-member": "{{doc-group|flow-bot|member}}",
- "grouppage-flow-bot": "{{doc-group|flow-bot|page}}"
+ "grouppage-flow-bot": "{{doc-group|flow-bot|page}}",
+ "flow-ve-mention-context-item-label": "Label of context items for user
mentions in Flow's VisualEditor",
+ "flow-ve-mention-inspector-title": "Title for the user mention
inspector in Flow's VisualEditor",
+ "flow-ve-mention-inspector-remove-label": "Text of remove button on
Flow's user mention inspector in the VisualEditor",
+ "flow-ve-mention-tool-title": "Title text for the user mention tool on
Flow's VisualEditor toolbar",
+ "flow-ve-mention-template": "Name of on-wiki template used for user
mentions. The template should accept a call in the form
{{templatename|Username]], to mention Username. It will use content language.",
+ "flow-ve-mention-inspector-invalid-user": "Error shown when the poster
attempts to mention a user that does not exist. Parameters:\n$1: Username.
The username is not registered; thus, gender is unknown."
}
diff --git a/modules/editor/editors/ext.flow.editors.visualeditor.js
b/modules/editor/editors/visualeditor/ext.flow.editors.visualeditor.js
similarity index 74%
rename from modules/editor/editors/ext.flow.editors.visualeditor.js
rename to modules/editor/editors/visualeditor/ext.flow.editors.visualeditor.js
index 0548f82..97499a6 100644
--- a/modules/editor/editors/ext.flow.editors.visualeditor.js
+++ b/modules/editor/editors/visualeditor/ext.flow.editors.visualeditor.js
@@ -1,9 +1,8 @@
-
-( function ( $, mw, ve ) {
+( function ( $, mw, OO, ve ) {
'use strict';
/**
- * @param {jQuery} $node
+ * @param {jQuery} $node Node to replace with a VisualEditor
* @param {string} [content='']
*/
mw.flow.editors.visualeditor = function ( $node, content ) {
@@ -19,7 +18,7 @@
} );
// load dependencies & init editor
- mw.loader.using( this.getModules(), $.proxy( this.init, this,
content || '' ) );
+ mw.loader.using( 'ext.flow.visualEditor', $.proxy( this.init,
this, content || '' ) );
};
OO.inheritClass( mw.flow.editors.visualeditor,
mw.flow.editors.AbstractEditor );
@@ -48,20 +47,7 @@
$.removeSpinner( 'flow-editor-loading' );
- // init ve, save target object
- //
- // We need at least some MW-specific stuff (e.g. links,
MW-style files).
- //
- // But for now we're using standalone, since
- // ve.init.mw.Target has some stuff that is not applicable
- // to us (e.g. submitUrl, this.$checkboxes), and there
- // were glitches when I tried to use it.
- //
- // However, we will have to look at this later.
- target = this.target = new ve.init.sa.Target(
- 'desktop',
- { floatable: false }
- );
+ target = this.target = new mw.flow.ve.Target();
htmlDoc = ve.createDocumentFromHtml( content ); // HTMLDocument
@@ -86,7 +72,6 @@
'mw-content-' + mw.config.get( 'wgVisualEditor'
).pageLanguageDir
);
- target.setSurface( surface );
setTimeout( function () {
// focus VE instance if textarea had focus
if ( !$focusedElement.length ||
flowEditor.$node.is( $focusedElement ) ) {
@@ -114,53 +99,13 @@
};
mw.flow.editors.visualeditor.prototype.destroy = function () {
- this.target.surface.destroy();
- this.target.toolbar.destroy();
+ if ( this.target ) {
+ this.target.surface.destroy();
+ this.target.toolbar.destroy();
+ }
// re-display original node
this.$node.show();
- };
-
- /**
- * Get all resourceloader modules that should be loaded.
- *
- * @return {array}
- */
- mw.flow.editors.visualeditor.prototype.getModules = function () {
- var core, standalone, messages, icons, specific;
-
- // core setup
- core = ['ext.visualEditor.core.desktop'];
- if ( mw.config.get( 'wgVisualEditorConfig'
).enableExperimentalCode ) {
- core.push( 'ext.visualEditor.experimental' );
- }
-
- // Standalone
- standalone = ['ext.visualEditor.standalone'];
-
- // data module
- messages = ['ext.visualEditor.data'];
-
- // icons
- icons = ['ext.visualEditor.icons'];
-
- // plugins
- //
- // No plugins supported for now. Later, we can figure out
which plugins we want.
-
- // plugins = mw.config.get( 'wgVisualEditorConfig'
).pluginModules || [],
-
- // site & user
- specific = ['site', 'user'];
-
- return [].concat(
- core,
- standalone,
- messages,
- icons,
- // plugins,
- specific
- );
};
/**
@@ -237,6 +182,14 @@
mw.flow.editors.visualeditor.static.isSupported = function () {
return !!(
mw.user.options.get( 'visualeditor-enable' ) &&
+
+ // ES5 support, from es5-skip.js
+ ( function () {
+ // This test is based on 'use strict',
+ // which is inherited from the top-leve
function.
+ return !this && !!Function.prototype.bind;
+ }() ) &&
+
// Since VE commit
e2fab2f1ebf2a28f18b8ead08c478c4fc95cd64e, SVG is required
document.createElementNS &&
document.createElementNS( 'http://www.w3.org/2000/svg',
'svg' ).createSVGRect
@@ -246,4 +199,4 @@
mw.flow.editors.visualeditor.static.usesPreview = function () {
return false;
};
-} ( jQuery, mediaWiki, ve ) );
+} ( jQuery, mediaWiki, OO, ve ) );
diff --git a/modules/editor/editors/visualeditor/mw.flow.ve.CommandRegistry.js
b/modules/editor/editors/visualeditor/mw.flow.ve.CommandRegistry.js
new file mode 100644
index 0000000..bd0d452
--- /dev/null
+++ b/modules/editor/editors/visualeditor/mw.flow.ve.CommandRegistry.js
@@ -0,0 +1,13 @@
+( function ( ve ) {
+ 'use strict';
+
+ ve.ui.commandRegistry.register(
+ new ve.ui.Command(
+ 'flowMention',
+ 'window',
+ 'open',
+ { args: ['flowMention'] },
+ { supportedSelections: ['linear'] }
+ )
+ );
+} ( ve ) );
diff --git a/modules/editor/editors/visualeditor/mw.flow.ve.Target.js
b/modules/editor/editors/visualeditor/mw.flow.ve.Target.js
new file mode 100644
index 0000000..928b4f4
--- /dev/null
+++ b/modules/editor/editors/visualeditor/mw.flow.ve.Target.js
@@ -0,0 +1,53 @@
+( function ( mw, OO, ve ) {
+ 'use strict';
+
+ mw.flow.ve = {
+ ui: {}
+ };
+
+ /**
+ * Flow-specific target, inheriting from the stand-alone target
+ *
+ * @class
+ * @extends ve.init.sa.Target
+ */
+ mw.flow.ve.Target = function FlowVeTarget() {
+ mw.flow.ve.Target.parent.call(
+ this,
+ 'desktop',
+ { floatable: false }
+ );
+ };
+
+ OO.inheritClass( mw.flow.ve.Target, ve.init.sa.Target );
+
+ // Static
+
+ mw.flow.ve.Target.static.toolbarGroups = [
+ {
+ type: 'list',
+ icon: 'text-style',
+ title: OO.ui.deferMsg(
'visualeditor-toolbar-style-tooltip' ),
+ include: [ 'bold', 'italic' ],
+ forceExpand: [ 'bold', 'italic' ]
+ },
+
+ { include: [ 'link' ] },
+
+ { include: [ 'flowMention' ] }
+ ];
+
+ // This is a workaround.
+ //
+ // We need to make sure the MW platform wins (we need it for e.g.
linkCache), because our
+ // dependencies do not agree.
+ //
+ // ext.visualEditor.data depends on ext.visualEditor.mediawiki, which
provides
+ // ve.init.mw.Platform.js. However, we also use
ext.visualEditor.standalone, which
+ // provides ve.init.sa.Platform. Both of these self-initialize
ve.init.platform.
+ ve.init.platform = new ve.init.mw.Platform();
+
+ OO.ui.getUserLanguages = ve.init.platform.getUserLanguages.bind(
ve.init.platform );
+
+ OO.ui.msg = ve.init.platform.getMessage.bind( ve.init.platform );
+} ( mediaWiki, OO, ve ) );
diff --git a/modules/editor/editors/visualeditor/mw.flow.ve.Target.less
b/modules/editor/editors/visualeditor/mw.flow.ve.Target.less
new file mode 100644
index 0000000..73b5ccb
--- /dev/null
+++ b/modules/editor/editors/visualeditor/mw.flow.ve.Target.less
@@ -0,0 +1,17 @@
+@import 'mediawiki.mixins';
+
+// Override another * rule in common.less back to browser default
+// box-sizing is not inherited.
+.flow-component .ve-init-target {
+ .box-sizing(content-box);
+
+ * {
+ .box-sizing(content-box);
+ }
+
+ // VE and OOjs UI do use other models in some cases, so we
+ // have to re-override them again as needed.
+ .oo-ui-textInputWidget input {
+ .box-sizing(border-box);
+ }
+}
diff --git
a/modules/editor/editors/visualeditor/ui/contextitem/mw.flow.ve.ui.MentionContextItem.js
b/modules/editor/editors/visualeditor/ui/contextitem/mw.flow.ve.ui.MentionContextItem.js
new file mode 100644
index 0000000..b3f7907
--- /dev/null
+++
b/modules/editor/editors/visualeditor/ui/contextitem/mw.flow.ve.ui.MentionContextItem.js
@@ -0,0 +1,58 @@
+( function ( mw, OO, ve ) {
+ 'use strict';
+
+ /**
+ * Context item for user mentions
+ *
+ * @class
+ * @extends ve.ui.ContextItem
+ *
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model item is related to
+ * @param {Object} config Configuration options
+ */
+ mw.flow.ve.ui.MentionContextItem = function FlowVeMentionContextItem(
context, model, config ) {
+ mw.flow.ve.ui.MentionContextItem.parent.call( this, context,
model, config );
+
+ this.$element.addClass( 'flow-ve-ui-mentionContextItem' );
+ };
+
+ OO.inheritClass( mw.flow.ve.ui.MentionContextItem,
ve.ui.MWTransclusionContextItem );
+
+ // Static
+ mw.flow.ve.ui.MentionContextItem.static.name = 'flowMention';
+
+ mw.flow.ve.ui.MentionContextItem.static.icon = 'flow-mention';
+
+ mw.flow.ve.ui.MentionContextItem.static.label = OO.ui.deferMsg(
'flow-ve-mention-context-item-label' );
+
+ mw.flow.ve.ui.MentionContextItem.static.commandName = 'flowMention';
+
+ // Make sure the inspector uses an arrow, rather than trying to fit in
the template.
+ // Wouldn't fit anyway, though, most likely.
+
+ mw.flow.ve.ui.MentionContextItem.static.embeddable = false;
+ /**
+ * @static
+ * @localdoc Sharing implementation with
mw.flow.ve.ui.MentionInspectorTool
+ */
+ mw.flow.ve.ui.MentionContextItem.static.isCompatibleWith =
+ mw.flow.ve.ui.MentionInspectorTool.static.isCompatibleWith;
+
+
+ // Instance Methods
+
+ /**
+ * Returns a short description emphasizing the relevant data (currently
just the user name)
+ *
+ * @return string User name
+ */
+ mw.flow.ve.ui.MentionContextItem.prototype.getDescription = function ()
{
+ var key =
mw.flow.ve.ui.MentionInspector.static.templateParameterKey;
+
+ // Is there a more intuitive way to do this?
+ return
this.model.element.attributes.mw.parts[0].template.params[key].wt;
+ };
+
+ ve.ui.contextItemFactory.register( mw.flow.ve.ui.MentionContextItem );
+} ( mediaWiki, OO, ve ) );
diff --git
a/modules/editor/editors/visualeditor/ui/images/icons/flow-mention.svg
b/modules/editor/editors/visualeditor/ui/images/icons/flow-mention.svg
new file mode 100644
index 0000000..b5a70db
--- /dev/null
+++ b/modules/editor/editors/visualeditor/ui/images/icons/flow-mention.svg
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="24"
+ height="24"
+ id="svg2">
+ <defs
+ id="defs4" />
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ transform="translate(0,-1028.3622)"
+ id="layer1">
+ <g
+ transform="translate(0,-4)"
+ id="g3048">
+ <g
+ transform="translate(-0.263648,0)"
+ id="g3061">
+ <g
+ transform="translate(-13.472704,1038.1992)"
+ id="g3">
+ <polygon
+ points="20,11 20,7 24,7 24,5 20,5 20,1 18,1 18,5 14,5 14,7 18,7
18,11 "
+ id="polygon5" />
+ </g>
+ <g
+ transform="translate(7.5,1035.8622)"
+ id="g5-1">
+ <g
+ id="g7"
+ style="fill:#000000;fill-opacity:1">
+ <g
+ id="g9"
+ style="fill:#000000;fill-opacity:1">
+ <g
+ id="g11"
+ style="fill:#000000;fill-opacity:1">
+ <path
+ d="M 9,9 C 6.7,9 4.8,7.1 4.8,4.7 4.8,2.3 6.7,0.5 9,0.5 c
2.3,0 4.2,1.9 4.2,4.2 C 13.2,7 11.4,9 9,9 z"
+ id="path13"
+ style="fill:#000000;fill-opacity:1" />
+ </g>
+ </g>
+ </g>
+ <g
+ id="g15"
+ style="fill:#000000;fill-opacity:1">
+ <g
+ id="g17"
+ style="fill:#000000;fill-opacity:1">
+ <g
+ id="g19"
+ style="fill:#000000;fill-opacity:1">
+ <path
+ d="m 16.5,16.5 -15,0 0,-0.6 c 0,-1.1 0.2,-2 0.5,-2.8
0.3,-0.8 0.8,-1.4 1.4,-2 C 4,10.6 4.8,10.2 5.7,9.9 L 6,9.8 6.4,10 c 0.8,0.5
1.7,0.8 2.7,0.8 0.9,0 1.9,-0.3 2.7,-0.8 L 12,9.8 12.3,9.9 c 0.9,0.3 1.6,0.7
2.3,1.2 0.6,0.5 1.1,1.2 1.4,2 0.3,0.8 0.5,1.7 0.5,2.8 z"
+ id="path21"
+ style="fill:#000000;fill-opacity:1" />
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git
a/modules/editor/editors/visualeditor/ui/inspectors/mw.flow.ve.ui.MentionInspector.js
b/modules/editor/editors/visualeditor/ui/inspectors/mw.flow.ve.ui.MentionInspector.js
new file mode 100644
index 0000000..e5d74c7
--- /dev/null
+++
b/modules/editor/editors/visualeditor/ui/inspectors/mw.flow.ve.ui.MentionInspector.js
@@ -0,0 +1,372 @@
+( function ( $, mw, OO, ve ) {
+ 'use strict';
+
+ // Based partly on ve.ui.MWTemplateDialog
+ /**
+ * Inspector for editing Flow mentions. This is a friendly
+ * UI for a transclusion (e.g. {{ping}}, template varies by wiki).
+ *
+ * @class
+ * @extends ve.ui.NodeInspector
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+ mw.flow.ve.ui.MentionInspector = function FlowVeMentionInspector(
config ) {
+ mw.flow.ve.ui.MentionInspector.parent.call( this, config );
+
+ // this.selectedNode is the ve.dm.MWTransclusionNode, which we
inherit
+ // from ve.ui.NodeInspector.
+ //
+ // The templateModel (used locally some places) is a sub-part
of the transclusion
+ // model.
+ this.transclusionModel = null;
+ this.loaded = false;
+ this.altered = false;
+
+ this.targetInput = null;
+ this.errorWidget = null;
+ this.errorFieldsetLayout = null;
+ };
+
+ OO.inheritClass( mw.flow.ve.ui.MentionInspector, ve.ui.NodeInspector );
+
+ // Static
+
+ mw.flow.ve.ui.MentionInspector.static.name = 'flowMention';
+ mw.flow.ve.ui.MentionInspector.static.icon = 'flow-mention';
+ mw.flow.ve.ui.MentionInspector.static.title = OO.ui.deferMsg(
'flow-ve-mention-inspector-title' );
+ mw.flow.ve.ui.MentionInspector.static.modelClasses = [
ve.dm.MWTransclusionNode ];
+
+ mw.flow.ve.ui.MentionInspector.static.template = mw.config.get(
'wgFlowMentionTemplate' );
+ mw.flow.ve.ui.MentionInspector.static.templateParameterKey = '1'; //
1-indexed positional parameter
+
+ // Buttons
+ mw.flow.ve.ui.MentionInspector.static.actions = [
+ {
+ action: 'remove',
+ label: OO.ui.deferMsg(
'flow-ve-mention-inspector-remove-label' ),
+ flags: ['destructive'],
+ modes: 'edit'
+ }
+ ].concat( mw.flow.ve.ui.MentionInspector.parent.static.actions );
+
+ // Instance Methods
+
+ /**
+ * Handle changes to the input widget
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.onTargetInputChange = function
() {
+ var templateModel, parameterModel, key, value, inspector;
+
+ this.hideErrors();
+
+ key =
mw.flow.ve.ui.MentionInspector.static.templateParameterKey;
+ value = this.targetInput.getValue();
+ inspector = this;
+
+ this.pushPending();
+ this.targetInput.isValid().done( function ( isValid ) {
+ if ( isValid ) {
+ // After the updates are done, we'll get
onTransclusionModelChange
+ templateModel =
inspector.transclusionModel.getParts()[0];
+ if ( templateModel.hasParameter( key ) ) {
+ parameterModel =
templateModel.getParameter( key );
+ parameterModel.setValue( value );
+ } else {
+ parameterModel = new
ve.dm.MWParameterModel(
+ templateModel,
+ key,
+ value
+ );
+ templateModel.addParameter(
parameterModel );
+ }
+ } else {
+ // Disable save button
+ inspector.setApplicableStatus();
+ }
+ } ).always( function () {
+ inspector.popPending();
+ } );
+ };
+
+ /**
+ * Handle the transclusion becoming ready
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.onTransclusionReady = function
() {
+ var templateModel, key;
+
+ key =
mw.flow.ve.ui.MentionInspector.static.templateParameterKey;
+
+ this.loaded = true;
+ this.$element.addClass( 'flow-ve-ui-mentionInspector-ready' );
+ this.popPending();
+
+ templateModel = this.transclusionModel.getParts()[0];
+ if ( templateModel.hasParameter( key ) ) {
+ this.targetInput.setValue( templateModel.getParameter(
key ).getValue() );
+ }
+ };
+
+ /**
+ * Handles the transclusion model changing. This should only happen
when we change
+ * the parameter, then get a callback.
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.onTransclusionModelChange =
function () {
+ if ( this.loaded ) {
+ this.altered = true;
+ this.setApplicableStatus();
+ }
+ };
+
+ /**
+ * Sets the abiliities based on the current status
+ *
+ * If it's empty or invalid, it can not be inserted or updated.
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.setApplicableStatus = function
() {
+ var parts = this.transclusionModel.getParts(),
+ templateModel = parts[0],
+ key =
mw.flow.ve.ui.MentionInspector.static.templateParameterKey,
+ inspector = this;
+
+ // The template should always be there; the question is whether
the first/only
+ // positional parameter is.
+ //
+ // If they edit an existing mention, and make it invalid, they
should be able
+ // to cancel, but not save.
+ if ( templateModel.hasParameter( key ) ) {
+ this.pushPending();
+ this.targetInput.isValid().done( function ( isValid ) {
+ inspector.actions.setAbilities( { done: isValid
} );
+ } ).always( function () {
+ inspector.popPending();
+ } );
+ } else {
+ inspector.actions.setAbilities( { done: false } );
+ }
+ };
+
+ /**
+ * Initialize UI of inspector
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.initialize = function () {
+ var flowBoard, overlay, indicatorWidget;
+
+
mw.flow.ve.ui.MentionInspector.parent.prototype.initialize.call( this );
+
+ // I would much prefer to use dependency injection to get the
list of topic posters
+ // into the inspector, but I haven't been able to figure out
how to pass it through
+ // yet.
+
+ flowBoard = mw.flow.getPrototypeMethod( 'board',
'getInstanceByElement' )(
+ this.$element
+ );
+
+ // Properties
+ overlay = this.manager.getOverlay();
+ this.targetInput = new mw.flow.ve.ui.MentionTargetInputWidget( {
+ $: this.$,
+ $overlay: overlay ? overlay.$element : this.$frame,
+ topicPosters: flowBoard.getTopicPosters( this.$element )
+ } );
+ indicatorWidget = new OO.ui.IndicatorWidget( {
+ indicator: 'alert'
+ } );
+ this.errorWidget = new OO.ui.FieldLayout( indicatorWidget, {
+ align: 'inline'
+ } );
+ this.errorFieldsetLayout = new OO.ui.FieldsetLayout( {
+ items: [
+ this.errorWidget
+ ]
+ } );
+
+ // Initialization
+ this.$content.addClass( 'flow-ve-ui-mentionInspector-content' );
+ this.errorFieldsetLayout.toggle( false );
+ this.form.addItems( [
+ this.errorFieldsetLayout
+ ] );
+ this.form.$element.append( this.targetInput.$element );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.getActionProcess = function (
action ) {
+ var surfaceModel = this.getFragment().getSurface(), dfd,
inspector;
+
+ if ( action === 'done' ) {
+ dfd = $.Deferred();
+ inspector = this;
+
+ this.targetInput.isValid().done( function ( isValid ) {
+ var transclusionModelPlain;
+
+ if ( isValid ) {
+ transclusionModelPlain =
inspector.transclusionModel.getPlainObject();
+
+ // Should be either null or the right
template
+ if ( inspector.selectedNode instanceof
ve.dm.MWTransclusionNode ) {
+
inspector.transclusionModel.updateTransclusionNode( surfaceModel,
inspector.selectedNode );
+ } else if ( transclusionModelPlain !==
null ) {
+ inspector.fragment =
inspector.getFragment().collapseToEnd();
+
inspector.transclusionModel.insertTransclusionNode( inspector.getFragment() );
+ surfaceModel.setSelection(
surfaceModel.getSelection().collapseToEnd() );
+ }
+
+ inspector.close( { action: action } );
+ dfd.resolve();
+ } else {
+ dfd.reject( new OO.ui.Error( OO.ui.msg(
'flow-ve-mention-inspector-invalid-user', inspector.targetInput.getValue() ) )
);
+ }
+ } );
+
+ return new OO.ui.Process( dfd.promise() );
+ } else if ( action === 'remove' ) {
+ return new OO.ui.Process( function () {
+ var doc, nodeRange;
+
+ doc = surfaceModel.getDocument();
+ nodeRange = this.selectedNode.getOuterRange();
+
+ surfaceModel.change(
+ ve.dm.Transaction.newFromRemoval( doc,
nodeRange )
+ );
+
+ this.close( { action: action } );
+ }, this );
+ }
+
+ return
mw.flow.ve.ui.MentionInspector.parent.prototype.getActionProcess.call( this,
action );
+ };
+
+ // Technically, these are private. However, it's necessary to override
them (and not call
+ // the parent), since otherwise this UI (which was probably designed
for dialogs) does not fit the inspector.
+ // Only handles on error at a time for now.
+ //
+ // It would be nice to implement a general solution for this that
covers all inspectors (or
+ // maybe a mixin for inline errors next to form elements).
+ /**
+ * @inherit
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.showErrors = function ( errors
) {
+ var errorText;
+
+ if ( errors instanceof OO.ui.Error ) {
+ errors = [errors];
+ }
+
+ errorText = errors[0].getMessageText();
+ this.errorWidget.setLabel( errorText );
+ this.errorFieldsetLayout.toggle( true );
+ this.setSize( 'large' );
+ };
+
+ /**
+ * @inherit
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.hideErrors = function () {
+ this.errorFieldsetLayout.toggle( false );
+ this.errorWidget.setLabel( '' );
+ this.setSize( 'medium' );
+ };
+
+ /**
+ * Pre-populate the username based on the node
+ *
+ * @param {Object} [data] Inspector initial data
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.getSetupProcess = function (
data ) {
+ return
mw.flow.ve.ui.MentionInspector.parent.prototype.getSetupProcess.call( this,
data )
+ .next( function () {
+ var templateModel, promise;
+
+ this.loaded = false;
+ this.altered = false;
+ // MWTransclusionModel has some unnecessary
behavior for our use
+ // case, mainly templatedata lookups.
+ this.transclusionModel = new
ve.dm.MWTransclusionModel();
+
+ // Events
+ this.transclusionModel.connect( this, {
+ change: 'onTransclusionModelChange'
+ } );
+
+ this.targetInput.connect( this, {
+ change: 'onTargetInputChange'
+ } );
+
+ // Initialization
+ if ( !this.selectedNode ) {
+ this.actions.setMode( 'insert' );
+ templateModel =
ve.dm.MWTemplateModel.newFromName(
+ this.transclusionModel,
+
mw.flow.ve.ui.MentionInspector.static.template
+ );
+ promise =
this.transclusionModel.addPart( templateModel );
+ } else {
+ this.actions.setMode( 'edit' );
+
+ // Load existing ping
+ promise = this.transclusionModel
+ .load( ve.copy(
this.selectedNode.getAttribute( 'mw' ) ) );
+ }
+
+ // Don't allow saving until we're sure it's
valid.
+ this.actions.setAbilities( { done: false } );
+ this.pushPending();
+ promise.always( this.onTransclusionReady.bind(
this ) );
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.getReadyProcess = function (
data ) {
+ return
mw.flow.ve.ui.MentionInspector.parent.prototype.getReadyProcess.call( this,
data )
+ .next( function () {
+ this.targetInput.focus();
+ }, this );
+ };
+
+ /**
+ * @inheritdoc
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.getTeardownProcess = function
( data ) {
+ data = data || {};
+ return
mw.flow.ve.ui.MentionInspector.parent.prototype.getTeardownProcess.call( this,
data )
+ .first( function () {
+ // Cleanup
+ this.$element.removeClass(
'flow-ve-ui-mentionInspector-ready' );
+ this.transclusionModel.disconnect( this );
+ this.transclusionModel.abortRequests();
+ this.transclusionModel = null;
+
+ this.targetInput.disconnect( this );
+
+ this.targetInput.setValue( '' );
+ }, this );
+ };
+
+ /**
+ * Gets the transclusion node representing this mention
+ *
+ * @param {Object} [data] Inspector opening data
+ * @return {ve.dm.Node} Selected node
+ */
+ mw.flow.ve.ui.MentionInspector.prototype.getSelectedNode = function () {
+ // Checks the model class
+ var node =
mw.flow.ve.ui.MentionInspector.parent.prototype.getSelectedNode.call( this );
+ if ( node !== null ) {
+ if ( node.isSingleTemplate(
mw.flow.ve.ui.MentionInspector.static.template ) ) {
+ return node;
+ }
+ }
+
+ return null;
+ };
+
+ ve.ui.windowFactory.register( mw.flow.ve.ui.MentionInspector );
+} ( jQuery, mediaWiki, OO, ve ) );
diff --git a/modules/editor/editors/visualeditor/ui/mw.flow.ve.ui.Icons.less
b/modules/editor/editors/visualeditor/ui/mw.flow.ve.ui.Icons.less
new file mode 100644
index 0000000..7312a4c
--- /dev/null
+++ b/modules/editor/editors/visualeditor/ui/mw.flow.ve.ui.Icons.less
@@ -0,0 +1,5 @@
+@import 'mediawiki.mixins';
+
+.oo-ui-icon-flow-mention {
+ .background-image('images/icons/flow-mention.svg');
+}
diff --git
a/modules/editor/editors/visualeditor/ui/tools/mw.flow.ve.ui.MentionInspectorTool.js
b/modules/editor/editors/visualeditor/ui/tools/mw.flow.ve.ui.MentionInspectorTool.js
new file mode 100644
index 0000000..641f9fb
--- /dev/null
+++
b/modules/editor/editors/visualeditor/ui/tools/mw.flow.ve.ui.MentionInspectorTool.js
@@ -0,0 +1,40 @@
+( function ( mw, OO, ve ) {
+ 'use strict';
+
+ /**
+ * Tool for user mentions
+ *
+ * @class
+ * @extends ve.ui.InspectorTool
+ *
+ * @constructor
+ * @param {OO.ui.ToolGroup} toolGroup
+ * @param {Object} [config] Configuration options
+ */
+
+ mw.flow.ve.ui.MentionInspectorTool = function
FlowVeMentionInspectorTool( toolGroup, config ) {
+ mw.flow.ve.ui.MentionInspectorTool.parent.call( this,
toolGroup, config );
+ };
+
+ OO.inheritClass( mw.flow.ve.ui.MentionInspectorTool,
ve.ui.InspectorTool );
+
+ // Static
+ mw.flow.ve.ui.MentionInspectorTool.static.commandName = 'flowMention';
+ mw.flow.ve.ui.MentionInspectorTool.static.name = 'flowMention';
+ mw.flow.ve.ui.MentionInspectorTool.static.icon = 'flow-mention';
+ mw.flow.ve.ui.MentionInspectorTool.static.title = OO.ui.deferMsg(
'flow-ve-mention-tool-title' );
+
+ mw.flow.ve.ui.MentionInspectorTool.static.template =
mw.flow.ve.ui.MentionInspector.static.template;
+
+ /**
+ * Checks whether the model represents a user mention
+ *
+ * @return boolean
+ */
+ mw.flow.ve.ui.MentionInspectorTool.static.isCompatibleWith = function (
model ) {
+ return model instanceof ve.dm.MWTransclusionNode &&
+ model.isSingleTemplate(
mw.flow.ve.ui.MentionInspectorTool.static.template );
+ };
+
+ ve.ui.toolFactory.register( mw.flow.ve.ui.MentionInspectorTool );
+} ( mediaWiki, OO, ve ) );
diff --git
a/modules/editor/editors/visualeditor/ui/widgets/mw.flow.ve.ui.MentionTargetInputWidget.js
b/modules/editor/editors/visualeditor/ui/widgets/mw.flow.ve.ui.MentionTargetInputWidget.js
new file mode 100644
index 0000000..eb9870e
--- /dev/null
+++
b/modules/editor/editors/visualeditor/ui/widgets/mw.flow.ve.ui.MentionTargetInputWidget.js
@@ -0,0 +1,170 @@
+( function ( $, mw, OO, ve ) {
+ 'use strict';
+
+ /**
+ * Creates an input widget with auto-completion for users to be
mentioned
+ *
+ * @class
+ * @extends oo.ui.TextInputWidget
+ * @mixins OO.ui.LookupElement
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ * @param {Array} [config.topicPosters] Array of usernames representing
posters to this thread,
+ * without duplicates.
+ */
+ mw.flow.ve.ui.MentionTargetInputWidget = function
FlowVeUiMentionTargetInputWidget( config ) {
+ mw.flow.ve.ui.MentionTargetInputWidget.parent.call( this,
config );
+
+ // Mixin constructor
+ config.allowSuggestionsWhenEmpty = true;
+ OO.ui.LookupElement.call( this, config );
+
+ // Properties
+ // Exclude anonymous users, since they do not receive pings.
+ this.loggedInTopicPosters = $.grep( config.topicPosters || [],
function ( poster ) {
+ return !mw.util.isIPAddress( poster, false );
+ } );
+ this.username = null;
+ // Username to validity promise (promise resolves with
true/false for existent/non-existent
+ this.isUsernameValidCache = {};
+
+ this.$element.addClass( 'flow-ve-ui-mentionTargetInputWidget' );
+ this.lookupMenu.$element.addClass(
'flow-ve-ui-mentionTargetInputWidget-menu' );
+ };
+
+ OO.inheritClass( mw.flow.ve.ui.MentionTargetInputWidget,
OO.ui.TextInputWidget );
+
+ OO.mixinClass( mw.flow.ve.ui.MentionTargetInputWidget,
OO.ui.LookupElement );
+
+ /**
+ * @inheritdoc
+ */
+ mw.flow.ve.ui.MentionTargetInputWidget.prototype.isValid = function () {
+ var api = new mw.Api(),
+ dfd = $.Deferred(),
+ promise = dfd.promise(),
+ username = this.getValue(),
+ widget = this,
+ isValid;
+
+ if ( $.trim( username ) === '' ) {
+ dfd.resolve( false );
+ return promise;
+ }
+
+ username = username[0].toUpperCase() + username.slice( 1 );
+ if ( this.isUsernameValidCache[username] !== undefined ) {
+ return this.isUsernameValidCache[username];
+ }
+
+ // Note that we delete this below if it turns out to get an
error.
+ this.isUsernameValidCache[username] = promise;
+
+ api.get( {
+ action: 'query',
+ list: 'users',
+ ususers: username
+ } ).done( function ( resp ) {
+ if (
+ resp &&
+ resp.query &&
+ resp.query.users &&
+ resp.query.users.length > 0
+ ) {
+ // This is the normal path for either existent
or non-existent users.
+ isValid = resp.query.users[0].missing ===
undefined;
+ dfd.resolve( isValid );
+ } else {
+ // This means part of the response is missing,
which again shouldn't
+ // happen (it could for empty string user, but
we're not supposed to
+ // send the request at all then). See
explanation under fail.
+ dfd.resolve( true );
+ delete widget.isUsernameValidCache[username];
+ }
+ } ).fail( function () {
+ // This should only happen on error cases. Even if the
user doesn't exist,
+ // we should still enter done. Since this is an
unforseen error, return true
+ // so we don't block submission, and evict cache.
+ dfd.resolve( true );
+ delete widget.isUsernameValidCache[username];
+ } );
+
+ return promise;
+ };
+
+ /**
+ * Gets a promise representing the auto-complete.
+ * Right now, the auto-complete is based on the users who have already
posted to the topic.
+ *
+ * It does a case-insensitive search for a string (anywhere in the
poster's username)
+ * matching what the user has typed in so far.
+ *
+ * E.g. if one of the posters is "Mary Jane Smith", that will be a
suggestion if the user has
+ * entered e.g. "Mary", "jane", or 'Smi'.
+ *
+ * @method
+ * @returns {jQuery.Promise}
+ */
+ mw.flow.ve.ui.MentionTargetInputWidget.prototype.getLookupRequest =
function () {
+ var abortObject = { abort: $.noop }, dfd = $.Deferred(),
+ lowerValue = this.value.toLowerCase(), matches;
+
+ matches = $.grep( this.loggedInTopicPosters, function ( poster
) {
+ return poster.toLowerCase().indexOf( lowerValue ) >= 0;
+ } );
+
+ dfd.resolve( matches );
+ return dfd.promise( abortObject );
+ };
+
+ /**
+ * @inheritdoc
+ */
+
mw.flow.ve.ui.MentionTargetInputWidget.prototype.getLookupCacheDataFromResponse
= function ( data ) {
+ return data;
+ };
+
+ /**
+ * Converts the raw data to UI objects
+ *
+ * @param Array list of users
+ * @return {OO.ui.MenuOptionWidget[]} Menu items
+ */
+
mw.flow.ve.ui.MentionTargetInputWidget.prototype.getLookupMenuOptionsFromData =
function ( users ) {
+ var items = [], user, i;
+
+ for ( i = 0; i < users.length; i++ ) {
+ user = users[i];
+
+ items.push( new OO.ui.MenuOptionWidget( {
+ $: this.lookupMenu.$,
+ data: user,
+ label: user
+ } ) );
+ }
+
+ return items;
+ };
+
+ // Based on
ve.ui.MWLinkTargetInputWidget.prototype.initializeLookupMenuSelection
+ /**
+ * @inheritdoc
+ */
+
mw.flow.ve.ui.MentionTargetInputWidget.prototype.initializeLookupMenuSelection
= function () {
+ var item;
+ if ( this.username ) {
+ this.lookupMenu.selectItem(
this.lookupMenu.getItemFromData( this.username ) );
+ }
+
+ item = this.lookupMenu.getSelectedItem();
+ if ( !item ) {
+
OO.ui.LookupElement.prototype.initializeLookupMenuSelection.call( this );
+ }
+
+ item = this.lookupMenu.getSelectedItem();
+ if ( item ) {
+ this.username = item.getData();
+ }
+ };
+} ( jQuery, mediaWiki, OO, ve ) );
diff --git
a/modules/engine/components/board/features/flow-board-visualeditor.js
b/modules/engine/components/board/features/flow-board-visualeditor.js
new file mode 100644
index 0000000..0fcb319
--- /dev/null
+++ b/modules/engine/components/board/features/flow-board-visualeditor.js
@@ -0,0 +1,37 @@
+/*!
+ * Expose some functionality on the board object that is needed for
VisualEditor.
+ */
+
+( function ( $, mw, OO ) {
+ /**
+ * FlowBoardComponentVisualEditorFeatureMixin
+ *
+ * @this FlowBoardComponent
+ * @constructor
+ *
+ */
+ function FlowBoardComponentVisualEditorFeatureMixin( $container ) {
+ }
+
+ // This is not really VE-specific, but I'm not sure where best to put
it.
+ // Also, should we pre-compute this in a loadHandler?
+ /**
+ * Finds topic authors for the given node
+ *
+ * @return Array List of usernames
+ */
+ function flowVisualEditorGetTopicPosters( $node ) {
+ var $topic = $node.closest( '.flow-topic' ),
+ duplicatedArray;
+
+ // Could use a data attribute to avoid trim.
+ duplicatedArray = $.map( $topic.find( '.flow-author
.mw-userlink' ).get(), function ( el ) {
+ return $.trim( $( el ).text() );
+ } );
+ return OO.unique( duplicatedArray );
+ }
+
+ FlowBoardComponentVisualEditorFeatureMixin.prototype.getTopicPosters =
flowVisualEditorGetTopicPosters;
+
+ mw.flow.mixinComponent( 'board',
FlowBoardComponentVisualEditorFeatureMixin );
+}( jQuery, mediaWiki, OO ) );
diff --git a/modules/engine/components/board/flow-board.js
b/modules/engine/components/board/flow-board.js
index e430c3c..6f3839a 100644
--- a/modules/engine/components/board/flow-board.js
+++ b/modules/engine/components/board/flow-board.js
@@ -16,6 +16,8 @@
* @mixins FlowBoardComponentLoadEventsMixin
* @mixins FlowBoardComponentMiscMixin
* @mixins FlowBoardComponentLoadMoreFeatureMixin
+ * @mixins FlowBoardComponentVisualEditorFeatureMixin
+ *
* @constructor
*/
function FlowBoardComponent( $container ) {
--
To view, visit https://gerrit.wikimedia.org/r/196866
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: I0a14f4779c73996d4cb063b27f3330def0baae6d
Gerrit-PatchSet: 13
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Mattflaschen <[email protected]>
Gerrit-Reviewer: Catrope <[email protected]>
Gerrit-Reviewer: EBernhardson <[email protected]>
Gerrit-Reviewer: Legoktm <[email protected]>
Gerrit-Reviewer: Mattflaschen <[email protected]>
Gerrit-Reviewer: Matthias Mullie <[email protected]>
Gerrit-Reviewer: SG <[email protected]>
Gerrit-Reviewer: Siebrand <[email protected]>
Gerrit-Reviewer: Trevor Parscal <[email protected]>
Gerrit-Reviewer: jenkins-bot <>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits