From cf237b882ea281be24ed30e845558e7cc7463d0d Mon Sep 17 00:00:00 2001 From: Moriel Schottlender Date: Fri, 18 Oct 2013 13:09:04 -0700 Subject: [PATCH] Insert special character tool A tool to add special characters and diacritics to text. Also added a new button type ve.ui.GroupButtonWidget that includes a group of PushButtonWidget objects and returna the individual button's value upon click Wikis can edit , a JSON string, to include their own desird special characters to insert through the tool. Bug: 50296 Change-Id: I26d1f437feef1c8b61ed3be5f74ef524b33baf49 --- VisualEditor.i18n.php | 52 ++++ VisualEditor.php | 6 + demos/ve/index.php | 4 +- modules/ve/init/ve.init.Target.js | 3 +- modules/ve/test/index.php | 4 +- .../ve.ui.SpecialCharacterInspector.js | 232 ++++++++++++++++++ modules/ve/ui/styles/images/spinner.gif | Bin 0 -> 2608 bytes modules/ve/ui/styles/ve.ui.Inspector.css | 17 ++ modules/ve/ui/styles/ve.ui.Widget.css | 6 + modules/ve/ui/tools/ve.ui.InspectorTool.js | 20 ++ .../ve/ui/widgets/ve.ui.GroupButtonWidget.js | 50 ++++ 11 files changed, 391 insertions(+), 3 deletions(-) create mode 100644 modules/ve/ui/inspectors/ve.ui.SpecialCharacterInspector.js create mode 100644 modules/ve/ui/styles/images/spinner.gif create mode 100644 modules/ve/ui/widgets/ve.ui.GroupButtonWidget.js diff --git a/VisualEditor.i18n.php b/VisualEditor.i18n.php index b2c2cea84c..605fe30618 100644 --- a/VisualEditor.i18n.php +++ b/VisualEditor.i18n.php @@ -155,6 +155,55 @@ $messages['en'] = array( 'visualeditor-linkinspector-title' => 'Hyperlink', 'visualeditor-listbutton-bullet-tooltip' => 'Bullet list', 'visualeditor-listbutton-number-tooltip' => 'Numbered list', + 'visualeditor-specialcharacter-button-tooltip' => 'Special character', + 'visualeditor-specialcharacterinspector-title' => 'Special character', + 'visualeditor-specialcharinspector-characterlist-insert' => '{ + "symbols": { + "−": "−", + "—": "—", + "°": "°", + "″": "″", + "′": "′", + "←": "←", + "→": "→", + "·": "·", + "§": "§" + }, + "accents": { + "à": "à", + "á": "á", + "â": "â", + "ä": "ä", + "ç": "ç", + "è": "è", + "é": "é", + "ê": "ê", + "ë": "ë", + "ì": "ì", + "í": "í", + "î": "î", + "ï": "ï", + "ò": "ò", + "ó": "ó", + "ô": "ô", + "ö": "ö", + "ø": "ø", + "ù": "ù", + "ú": "ú", + "û": "û", + "ü": "ü" + }, + "math": { + "−": "−", + "×": "×", + "÷": "÷", + "≈": "≈", + "≠": "≠", + "≤": "≤", + "≥": "≥", + "±": "±" + } +}', 'visualeditor-loadwarning' => 'Error loading data from server: $1. Would you like to retry?', 'visualeditor-loadwarning-token' => 'Error loading edit token from server: $1. Would you like to retry?', 'visualeditor-mainnamespacepagelink' => 'Project:Main namespace', @@ -555,6 +604,9 @@ Parameters: {{Identical|Hyperlink}}', 'visualeditor-listbutton-bullet-tooltip' => 'Tooltip text for the bullet list button', 'visualeditor-listbutton-number-tooltip' => 'Tooltip text for the numbered list button', + 'visualeditor-specialcharacter-button-tooltip' => 'Tooltip text for the insert character button', + 'visualeditor-specialcharacterinspector-title' => 'Used as title for special character inspector', + 'visualeditor-specialcharinspector-characterlist-insert' => 'This is a JSON string defining the special characters that can be inserted using the special character insertion tool. Please make sure it is a valid JSON string.', 'visualeditor-loadwarning' => 'Text (JavaScript confirm()) shown when the editor fails to load properly. Parameters: diff --git a/VisualEditor.php b/VisualEditor.php index a1e389f68e..c26ad521a0 100644 --- a/VisualEditor.php +++ b/VisualEditor.php @@ -511,6 +511,9 @@ $wgResourceModules += array( 've/ui/inspectors/ve.ui.LinkInspector.js', 've-mw/ui/inspectors/ve.ui.MWLinkInspector.js', 've-mw/ui/inspectors/ve.ui.MWExtensionInspector.js', + + 've/ui/widgets/ve.ui.GroupButtonWidget.js', + 've/ui/inspectors/ve.ui.SpecialCharacterInspector.js', ), 'styles' => array( // ce @@ -667,6 +670,9 @@ $wgResourceModules += array( 'visualeditor-savedialog-warning-dirty', 'visualeditor-saveerror', 'visualeditor-serializeerror', + 'visualeditor-specialcharacter-button-tooltip', + 'visualeditor-specialcharacterinspector-title', + 'visualeditor-specialcharinspector-characterlist-insert', 'visualeditor-toolbar-cancel', 'visualeditor-toolbar-savedialog', 'visualeditor-version-label', diff --git a/demos/ve/index.php b/demos/ve/index.php index 60dabfe7c1..982972c604 100644 --- a/demos/ve/index.php +++ b/demos/ve/index.php @@ -235,8 +235,9 @@ $html = file_get_contents( $page ); - + + @@ -247,6 +248,7 @@ $html = file_get_contents( $page ); + diff --git a/modules/ve/init/ve.init.Target.js b/modules/ve/init/ve.init.Target.js index abc7f9fb90..73c5cb78f2 100644 --- a/modules/ve/init/ve.init.Target.js +++ b/modules/ve/init/ve.init.Target.js @@ -39,7 +39,8 @@ ve.init.Target.static.toolbarGroups = [ }, { 'include': [ 'bold', 'italic', 'link', 'clear' ] }, { 'include': [ 'number', 'bullet', 'outdent', 'indent' ] }, - { 'include': '*' } + { 'include': '*', 'demote': [ 'specialcharacter' ] } + ]; ve.init.Target.static.surfaceCommands = [ diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php index 5e694177a9..bc2a5112a5 100644 --- a/modules/ve/test/index.php +++ b/modules/ve/test/index.php @@ -188,8 +188,9 @@ - + + @@ -200,6 +201,7 @@ + diff --git a/modules/ve/ui/inspectors/ve.ui.SpecialCharacterInspector.js b/modules/ve/ui/inspectors/ve.ui.SpecialCharacterInspector.js new file mode 100644 index 0000000000..81c687099a --- /dev/null +++ b/modules/ve/ui/inspectors/ve.ui.SpecialCharacterInspector.js @@ -0,0 +1,232 @@ +/*! + * VisualEditor UserInterface SpecialCharacterInspector class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * Special character inspector. + * + * @class + * @extends ve.ui.Inspector + * + * @constructor + * @param {ve.ui.WindowSet} windowSet Window set this inspector is part of + * @param {Object} [config] Configuration options + */ +ve.ui.SpecialCharacterInspector = function VeUiSpecialCharacterInspector( windowSet, config ) { + + // Parent constructor + ve.ui.Inspector.call( this, windowSet, config ); + + this.characters = null; + this.$buttonDomList = null; + this.initialSelection = null; + this.addedChar = null; + this.categories = null; + + // Fallback character list in case no list is found anywhere + this.minimalCharacterList = + { + 'accents': { + 'à': 'à', + 'á': 'á', + 'â': 'â', + 'ä': 'ä', + 'ç': 'ç', + 'è': 'è', + 'é': 'é', + 'ê': 'ê', + 'ë': 'ë', + 'ì': 'ì', + 'í': 'í', + 'î': 'î', + 'ï': 'ï', + 'ò': 'ò', + 'ó': 'ó', + 'ô': 'ô', + 'ö': 'ö', + 'ø': 'ø', + 'ù': 'ù', + 'ú': 'ú', + 'û': 'û', + 'ü': 'ü' + }, + 'symbols': { + '−': '−', + '—': '—', + '°': '°', + '″': '″', + '′': '′', + '←': '←', + '→': '→', + '·': '·', + '§': '§' + } + }; +}; + +/* Inheritance */ + +OO.inheritClass( ve.ui.SpecialCharacterInspector, ve.ui.Inspector ); + +/* Static properties */ + +ve.ui.SpecialCharacterInspector.static.name = 'specialcharacter'; + +ve.ui.SpecialCharacterInspector.static.icon = 'specialcharacter'; + +ve.ui.SpecialCharacterInspector.static.titleMessage = 'visualeditor-specialcharacterinspector-title'; + +ve.ui.SpecialCharacterInspector.static.removeable = false; + +/* Methods */ + +/** + * Handle frame ready events. + * + * @method + */ +ve.ui.SpecialCharacterInspector.prototype.initialize = function () { + // Parent method + ve.ui.Inspector.prototype.initialize.call( this ); + + this.$spinner = this.$( '
' ).addClass( 've-specialchar-spinner' ); + this.$form.append( this.$spinner ); +}; + +/** + * Handle the inspector being setup. + * + * @method + * @param {Object} [data] Inspector opening data + */ +ve.ui.SpecialCharacterInspector.prototype.setup = function ( data ) { + // Parent method + ve.ui.Inspector.prototype.setup.call( this, data ); + + // Preserve initial selection so we can collapse cursor position + // after we're done adding + this.initialSelection = this.surface.getModel().getSelection(); + + this.getCharList().done( ve.bind( function() { + // Now we can rebuild our button list + // We only want to rebuild the list if we don't already have it + if ( !this.$buttonDomList ) { + // Start with the spinner showing + this.$spinner.show(); + this.buildButtonList().done( ve.bind( function() { + // Append the new button list + this.$form.append( this.$buttonDomList ); + // Done, hide the spinner + this.$spinner.hide(); + + }, this ) ); + } + }, this ) ); + +}; + +/** + * Get the special character list object + * This can also be an AJAX call with default fallback + * + * @returns {jQuery.Promise} + */ +ve.ui.SpecialCharacterInspector.prototype.getCharList = function () { + var charslist, charobj, + deferred = $.Deferred(); + + // Don't request the character list again if we already have it + if ( !this.characters ) { + + // Get the character list + charslist = ve.msg( 'visualeditor-specialcharinspector-characterlist-insert' ); + try { + charobj = $.parseJSON( charslist ); + } catch ( err ) { + // There was no character list found, or the character list message is + // invalid json string. Force a fallback to the minimal character list + charobj = this.minimalCharacterList; + ve.log( 've.ui.SpecialCharacterInspector: Could not parse the Special Character list; using default.'); + ve.log( err.message ); + } finally { + this.characters = charobj; + deferred.resolve(); + } + } + + return deferred.promise(); +}; + +/** + * Builds the button DOM list based on the character list + * + * @returns {jQuery.Promise} + */ +ve.ui.SpecialCharacterInspector.prototype.buildButtonList = function () { + var category, categoryButtons, + deferred = $.Deferred(), + $widgetOutput = this.$( '
' ).addClass( 've-specialchar-list' ); + + if ( !this.characters ) { + deferred.reject(); + } + + for ( category in this.characters ) { + categoryButtons = new ve.ui.GroupButtonWidget( { + 'groupName': category, + 'group': this.characters[category], + 'aggregations': { 'click': 'click' } + } ); + + categoryButtons.connect( this, { 'click': 'onSpecialCharAdd' } ); + + $widgetOutput + .append( this.$( '

').text( category ) ) + .append( categoryButtons.$element ); + } + + this.$buttonDomList = $widgetOutput; + deferred.resolve(); + return deferred.promise(); +}; + +/** + * Handle the click event on the button groups. The value of the selection will be inserted + * into the text + * + * @param {OO.ui.PushButtonWidget} button The value attached to the clicked button + */ +ve.ui.SpecialCharacterInspector.prototype.onSpecialCharAdd = function ( button ) { + var fragment = this.surface.getModel().getFragment( null, true ), + value = button.returnValue; + + // Insert the character + if ( value !== undefined ) { + // get the insertion value (it could be in any category) + this.addedChar = value; + fragment.insertContent( value, false ); + } +}; + +/** + * @inheritdoc + */ +ve.ui.SpecialCharacterInspector.prototype.teardown = function ( data ) { + var selection; + // Collapse selection after the inserted content + if ( this.addedChar ) { + selection = new ve.Range( this.initialSelection.start + this.addedChar.length ); + this.surface.execute( 'content', 'select', selection ); + } + // reset + this.addedChar = null; + // Parent method + ve.ui.Inspector.prototype.teardown.call( this, data ); +}; + +/* Registration */ + +ve.ui.inspectorFactory.register( ve.ui.SpecialCharacterInspector ); diff --git a/modules/ve/ui/styles/images/spinner.gif b/modules/ve/ui/styles/images/spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..c478f8f498d9f82e51810e924cba8b04f2f18897 GIT binary patch literal 2608 zcmdVcdr(tX9tZGC-nquyye^O%9sz?gDvz~FvAP||<(U--LTT7&mvBi4SS8XZM7Ar) zLqMdIM^R`Uopq5vZP_hIt5od>j>$V-l-DQK_&d&a^-PxJ;kDkBJ z%sKO!-}!z|Xn2VKUWF^*3VaIy>+9=rad9gvD|2&m6B84|!^1d^FD@>Qj*fPBcP}k1 zSu7U4UfJ2En2GMQFaR|f|N7Zw(lmzQ-qU0PaNadGj~)KpzvT}MYpX=&;6=g(_u zYVz{(j7HjBDc ztN*JpEoZX5j&vO=*|z_X?}%zQGLS$-o^-x10DTZZCri$PGd4woY4jW!69QQP!GVvc z&ojWFq+B|iqYzM>3zVc-9Z-Anh>j|bQ?j`yQ7&6UtI%EUKsmY)4YN0zsR`9c^(dqX z(_FoGMH8+mHvlY+_*A_goh=& zHAUgqgM3jszU_B0B1@{4e4iB@KeE60Mg*RFiqBOSKRsK&Jun^-)|{a>h5Kh4JC7HY z;}SoN)N{3PEE0zNeb{|#DpC>WEz zZAwD}QyeE}bG4|8^T)6gKu}%!6s2^*5E1)hA1Xk)k}+sFh~MQ%##+Y#hc0^xc7FO+ z7jJ%gRz5e)!VXMXK`dg_3Sp7At>yBl&!;&mX`j&y=^WnP zILv(|&^BT}Hbnm~(22K#{?kXvNI`$lIV&T@=2WGosNMj+m*HNL(`+!c*HDfs1Y>qa zzbg;Lpxz(|3&R@qAQq05=*28dGC4^ZH{O6}XL;m1CI|DZeYI@R$bSSo5_^mSWSRac z_E|4w86Iqv$^QOy40oowQA4^UlcUvT+OxZlKPB}!6+kH1gO{tL8$$8+Oyazo#5Dku zNoU{M-wx#HP%2ubno7@t2hyl}( zJhI1x%c43aZ(`C`+11|b*e3wB;hIbK>r%VUZ$c)kH&b4T1$e1pmtU4~yy{Q5B&pShA z(2HAs>UZ`tj?w=z%5k~&A!akGY!Xw6g3q;US`R~+n7kv*P&QBCg_em9Zx{hZLX?1P zdHkEgGSARs(+vf7v`XP?$DA7VM9Kv?ss7owxZPQ(@);=-Ex-x+9ub$uekn#gcxFaw zyf3HNn`Ru0z?X8eqXp(K4#^Y0Mg&G&MufhGr+RN@L+D>!KJ>GBIP@X-%ImDy3C>`0eiQ; zb+s}dtYwQN7ySS7od@kFdu?}U)_bQh+bFpRVfggFBij~9Vpqq)!ry)$*>V!ftgQvA zU*wWgGwkf5MY;91V1n9ViLKXE1bRCnz=JB}#F}RQh%KB*_U#T6D2S1H?b5H_!TJ_P zC|KzN%Rm81%@{NW_?r?#iN6uZ9c!D8*G??5&>tdsGhD#O%2odgUAp{3?l) zN$~h{RLjw$A&Pjz<7m-ocdy|1>x<0bU7Km)ZNx=u)vr^HhP*Ae;~cp!`;3QOac{mH zd^DuYQ?Vd2RSW;C)9*#T_TayQJ9hQBX1i>s5VyKBCtIi$UTNKj&$#bT%C@zCO^nQ;UqRm08{fr7)%SCdd`7T|IcIFgkB z(9+r5kHy?ZwJpK3IG>@MECMl|i3h!>CcdwRhS87@$>}xYU^X4={EuF&e*#ssu!sNv literal 0 HcmV?d00001 diff --git a/modules/ve/ui/styles/ve.ui.Inspector.css b/modules/ve/ui/styles/ve.ui.Inspector.css index 05a9db04eb..2cbe046679 100644 --- a/modules/ve/ui/styles/ve.ui.Inspector.css +++ b/modules/ve/ui/styles/ve.ui.Inspector.css @@ -41,3 +41,20 @@ padding: 0; white-space: nowrap; } + +/* ve.ui.SpecialCharacterInspector.js */ + +.ve-specialchar-spinner { + /* There might be a better place for a spinner? */ + /* @embed */ + background-image: url(images/spinner.gif); + width: 31px; + height: 31px; + margin-left: auto; + margin-right: auto; +} + +.ve-specialchar-list h2 { + font-size: 1em; + text-transform: capitalize; +} diff --git a/modules/ve/ui/styles/ve.ui.Widget.css b/modules/ve/ui/styles/ve.ui.Widget.css index ec9e87824a..c4d5bf27af 100644 --- a/modules/ve/ui/styles/ve.ui.Widget.css +++ b/modules/ve/ui/styles/ve.ui.Widget.css @@ -108,3 +108,9 @@ border-bottom-left-radius: 0; border-bottom-width: 0; } + +/* ve.ui.GroupButtonWidget.js */ + +.ve-ui-groupButtonWidget { + white-space: normal; +} diff --git a/modules/ve/ui/tools/ve.ui.InspectorTool.js b/modules/ve/ui/tools/ve.ui.InspectorTool.js index c6a3c2bc09..533a5d32da 100644 --- a/modules/ve/ui/tools/ve.ui.InspectorTool.js +++ b/modules/ve/ui/tools/ve.ui.InspectorTool.js @@ -119,3 +119,23 @@ ve.ui.LinkInspectorTool.static.titleMessage = 'visualeditor-annotationbutton-lin ve.ui.LinkInspectorTool.static.inspector = 'link'; ve.ui.LinkInspectorTool.static.modelClasses = [ ve.dm.LinkAnnotation ]; ve.ui.toolFactory.register( ve.ui.LinkInspectorTool ); + +/** + * Insert characters tool. + * + * @class + * @extends ve.ui.InspectorTool + * @constructor + * @param {OO.ui.ToolGroup} toolGroup + * @param {Object} [config] Configuration options + */ +ve.ui.InsertCharacterInspectorTool = function VeUiInsertCharacterInspectorTool( toolGroup, config ) { + ve.ui.InspectorTool.call( this, toolGroup, config ); +}; +OO.inheritClass( ve.ui.InsertCharacterInspectorTool, ve.ui.InspectorTool ); +ve.ui.InsertCharacterInspectorTool.static.name = 'specialcharacter'; +ve.ui.InsertCharacterInspectorTool.static.group = 'insert'; +ve.ui.InsertCharacterInspectorTool.static.icon = 'special-character'; +ve.ui.InsertCharacterInspectorTool.static.titleMessage = 'visualeditor-specialcharacter-button-tooltip'; +ve.ui.InsertCharacterInspectorTool.static.inspector = 'specialcharacter'; +ve.ui.toolFactory.register( ve.ui.InsertCharacterInspectorTool ); diff --git a/modules/ve/ui/widgets/ve.ui.GroupButtonWidget.js b/modules/ve/ui/widgets/ve.ui.GroupButtonWidget.js new file mode 100644 index 0000000000..7ac770ae52 --- /dev/null +++ b/modules/ve/ui/widgets/ve.ui.GroupButtonWidget.js @@ -0,0 +1,50 @@ +/*! + * VisualEditor UserInterface GroupButtonWidget class. + * + * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt + * @license The MIT License (MIT); see LICENSE.txt + */ + +/** + * Creates an ve.ui.GroupButtonWidget object. + * + * @class + * @extends OO.ui.Widget + * + * @param {Object} [config] Configuration options + * @cfg {Object} [group] Button group parameters organized by { 'label': returnValue } + * where 'returnValue' is the value associated with the button + */ +ve.ui.GroupButtonWidget = function VeUiGroupButtonWidget( config ) { + var item, button, arrButtons = []; + + // Parent constructor + OO.ui.Widget.call( this, config ); + + // Mixin constructors + OO.ui.GroupElement.call( this, this.$( '
' ), config ); + + // Initialization + this.value = null; + this.group = config.group; + this.buttons = {}; + // Set up the buttons + for ( item in this.group ) { + button = new OO.ui.PushButtonWidget( { + 'label': item, + } ); + // store value + button.returnValue = this.group[item]; + arrButtons.push( button ); + } + + this.addItems( arrButtons ); + + this.$element.append( this.$group.addClass( 've-ui-groupButtonWidget' ) ); +}; + +/* Inheritance */ + +OO.inheritClass( ve.ui.GroupButtonWidget, OO.ui.Widget ); + +OO.mixinClass( ve.ui.GroupButtonWidget, OO.ui.GroupElement );