Merge "Implement dark mode styles and use Codex CSS components in search panel"

This commit is contained in:
jenkins-bot 2024-09-03 15:59:15 +00:00 committed by Gerrit Code Review
commit 74aeccf9c6
19 changed files with 1336 additions and 218 deletions

View file

@ -165,13 +165,15 @@
"legacy/modules/ve-cm/ve.ui.CodeMirrorTool.js"
],
"styles": [
"legacy/modules/ve-cm/ve.ui.CodeMirror.less"
"legacy/modules/ve-cm/ve.ui.CodeMirror.less",
"legacy/ext.CodeMirror.less"
],
"messages": [
"codemirror-toggle-label"
]
},
"ext.CodeMirror.v6": {
"class": "MediaWiki\\ResourceLoader\\CodexModule",
"dependencies": [
"mediawiki.api",
"mediawiki.user",
@ -181,6 +183,9 @@
"packageFiles": [
"codemirror.js",
"codemirror.textSelection.js",
"codemirror.search.js",
"codemirror.gotoLine.js",
"codemirror.panel.js",
{
"name": "ext.CodeMirror.data.js",
"callback": "MediaWiki\\Extension\\CodeMirror\\DataScript::makeScript"
@ -189,44 +194,57 @@
"styles": [
"codemirror.less"
],
"codexStyleOnly": true,
"codexComponents": [
"CdxButton",
"CdxCheckbox",
"CdxLabel",
"CdxTextInput",
"CdxToggleButton",
"CdxToggleButtonGroup"
],
"messages": [
"codemirror-all",
"codemirror-all-tooltip",
"codemirror-by-word",
"codemirror-control-character",
"codemirror-done",
"codemirror-find",
"codemirror-fold-template",
"codemirror-folded-code",
"codemirror-goto-line",
"codemirror-goto-line-go",
"codemirror-match-case",
"codemirror-next",
"codemirror-previous",
"codemirror-all",
"codemirror-match-case",
"codemirror-regexp",
"codemirror-by-word",
"codemirror-replace",
"codemirror-replace-placeholder",
"codemirror-replace-all",
"codemirror-control-character",
"codemirror-special-char-null",
"codemirror-special-char-bell",
"codemirror-replace-placeholder",
"codemirror-special-char-backspace",
"codemirror-special-char-newline",
"codemirror-special-char-vertical-tab",
"codemirror-special-char-bell",
"codemirror-special-char-carriage-return",
"codemirror-special-char-escape",
"codemirror-special-char-nbsp",
"codemirror-special-char-zero-width-space",
"codemirror-special-char-zero-width-non-joiner",
"codemirror-special-char-zero-width-joiner",
"codemirror-special-char-left-to-right-mark",
"codemirror-special-char-right-to-left-mark",
"codemirror-special-char-line-separator",
"codemirror-special-char-left-to-right-override",
"codemirror-special-char-right-to-left-override",
"codemirror-special-char-narrow-nbsp",
"codemirror-special-char-left-to-right-isolate",
"codemirror-special-char-right-to-left-isolate",
"codemirror-special-char-pop-directional-isolate",
"codemirror-special-char-paragraph-separator",
"codemirror-special-char-zero-width-no-break-space",
"codemirror-special-char-left-to-right-mark",
"codemirror-special-char-left-to-right-override",
"codemirror-special-char-line-separator",
"codemirror-special-char-narrow-nbsp",
"codemirror-special-char-nbsp",
"codemirror-special-char-newline",
"codemirror-special-char-null",
"codemirror-special-char-object-replacement",
"codemirror-fold-template",
"codemirror-unfold",
"codemirror-folded-code"
"codemirror-special-char-paragraph-separator",
"codemirror-special-char-pop-directional-isolate",
"codemirror-special-char-right-to-left-isolate",
"codemirror-special-char-right-to-left-mark",
"codemirror-special-char-right-to-left-override",
"codemirror-special-char-vertical-tab",
"codemirror-special-char-zero-width-joiner",
"codemirror-special-char-zero-width-no-break-space",
"codemirror-special-char-zero-width-non-joiner",
"codemirror-special-char-zero-width-space",
"codemirror-unfold"
]
},
"ext.CodeMirror.v6.init": {
@ -266,6 +284,9 @@
"packageFiles": [
"codemirror.wikieditor.js"
],
"styles": [
"codemirror.wikieditor.less"
],
"messages": [
"codemirror-toggle-label",
"codemirror-toggle-label-short"

View file

@ -14,15 +14,19 @@
"codemirror-prefs-colorblind": "Enable colorblind-friendly scheme for syntax highlighting when editing wikitext",
"codemirror-prefs-colorblind-help": "If you use a gadget for syntax highlighting, this preference will not work.",
"codemirror-find": "Find",
"codemirror-next": "next",
"codemirror-previous": "previous",
"codemirror-all": "all",
"codemirror-match-case": "match case",
"codemirror-regexp": "regexp",
"codemirror-by-word": "by word",
"codemirror-replace": "replace",
"codemirror-next": "Find next",
"codemirror-previous": "Find previous",
"codemirror-all": "All",
"codemirror-all-tooltip": "Select all matches",
"codemirror-match-case": "Match case",
"codemirror-regexp": "Regular expression",
"codemirror-by-word": "By word",
"codemirror-replace": "Replace",
"codemirror-replace-placeholder": "Replace",
"codemirror-replace-all": "replace all",
"codemirror-replace-all": "Replace all",
"codemirror-done": "Done",
"codemirror-goto-line": "Go to line",
"codemirror-goto-line-go": "Go",
"codemirror-control-character": "Control character $1",
"codemirror-special-char-null": "Null character",
"codemirror-special-char-bell": "Bell character",

View file

@ -6,7 +6,8 @@
"Raymond",
"Shirayuki",
"SkyDaisy9",
"pastakhov"
"pastakhov",
"MusikAnimal"
]
},
"codemirror-desc": "{{desc|name=Code Mirror|url=https://www.mediawiki.org/wiki/Extension:CodeMirror}}\n\nAdditional info: Description of \"Syntax highlighting\" in wiki\n[[mw:Extension:SyntaxHighlight GeSHi]]",
@ -17,16 +18,20 @@
"codemirror-v6-prefs-colorblind": "Used in user preferences as label for enabling the colorblind-friendly option. This is a shorter version of {{msg-mw|codemirror-prefs-colorblind}} shown under section {{msg-mw|prefs-syntax-highlighting}} on wikis using CodeMirror 6.",
"codemirror-prefs-colorblind": "Used in user preferences as label for enabling the colorblind-friendly option.",
"codemirror-prefs-colorblind-help": "Used in user preferences as remark on the colorblind-friendly option.",
"codemirror-find": "Placeholder text for the input in the CodeMirror search dialog.",
"codemirror-next": "Label for the 'Next' button in the CodeMirror search dialog.",
"codemirror-previous": "Label for the 'Previous' button in the CodeMirror search dialog.",
"codemirror-all": "Label for the 'All' button in the CodeMirror search dialog.",
"codemirror-match-case": "Label for the 'match case' option in the CodeMirror search dialog.",
"codemirror-regexp": "Label for the 'regexp' button in the CodeMirror search dialog. This enables the user to search using regular expressions.",
"codemirror-by-word": "Label for the 'by word' button in the CodeMirror search dialog.",
"codemirror-replace": "Label for the 'replace' button in the CodeMirror search dialog.",
"codemirror-replace-placeholder": "Placeholder text for the 'Replace' input in the CodeMirror search dialog.",
"codemirror-replace-all": "Label for the 'replace all' button in the CodeMirror search dialog.",
"codemirror-find": "Placeholder text for the input in the CodeMirror search panel.",
"codemirror-next": "Tooltip text for the 'Find next' button in the CodeMirror search panel.",
"codemirror-previous": "Tooltip text for the 'Find previous' button in the CodeMirror search panel.",
"codemirror-all": "Label for the 'All' button in the CodeMirror search panel, which finds all the results. See also {{msg-mw|codemirror-all-tooltip}}.",
"codemirror-all-tooltip": "Tooltip shown when hovering over the 'All' button in the CodeMirror search panel.",
"codemirror-match-case": "Tooltip for the 'Match case' button in the CodeMirror search panel.",
"codemirror-regexp": "Tooltip for the 'Regular expression' button in the CodeMirror search panel.",
"codemirror-by-word": "Tooltip for the 'By word' button in the CodeMirror search panel.",
"codemirror-replace": "Label for the 'Replace' button in the CodeMirror search panel.",
"codemirror-replace-placeholder": "Placeholder text for the 'Replace' input in the CodeMirror search panel.",
"codemirror-replace-all": "Label for the 'Replace all' button in the CodeMirror search panel.",
"codemirror-done": "Label for the 'Done' button in CodeMirror panels.\n{{Identical|Done}}",
"codemirror-goto-line": "Label for the 'Go to line' input field.",
"codemirror-goto-line-go": "Label for the 'Go to line' submit button.\n{{Identical|Go}}",
"codemirror-control-character": "Tooltip text shown when hovering over special characters. $1 is the Unicode value of the special character.",
"codemirror-special-char-null": "Tooltip text shown when hovering over a null character. See [[wikidata:Q617945]] for possible translations.",
"codemirror-special-char-bell": "Tooltip text shown when hovering over a bell character. See [[wikidata:Q815674]] for possible translations.",

View file

@ -35,8 +35,12 @@
"Extension": "https://codemirror.net/docs/ref/#state.Extension",
"KeyBinding": "https://codemirror.net/docs/ref/#view.KeyBinding",
"LanguageSupport": "https://codemirror.net/docs/ref/#language.LanguageSupport",
"Panel": "https://codemirror.net/docs/ref/#view.Panel",
"PluginSpec": "https://codemirror.net/docs/ref/#view.PluginSpec",
"RangeSet": "https://codemirror.net/docs/ref/#state.RangeSet",
"SearchQuery": "https://codemirror.net/docs/ref/#search.SearchQuery",
"StateEffectType": "https://codemirror.net/docs/ref/#state.StateEffectType",
"StateField": "https://codemirror.net/docs/ref/#state.StateField",
"StreamParser": "https://codemirror.net/docs/ref/#language.StreamParser",
"StringStream": "https://codemirror.net/docs/ref/#language.StringStream",
"SyntaxNode": "https://lezer.codemirror.net/docs/ref/#common.SyntaxNode",

7
package-lock.json generated
View file

@ -8,7 +8,7 @@
"devDependencies": {
"@codemirror/commands": "6.2.5",
"@codemirror/language": "6.9.3",
"@codemirror/search": "6.5.4",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.22.2",
"@lezer/highlight": "1.2.0",
@ -524,9 +524,10 @@
}
},
"node_modules/@codemirror/search": {
"version": "6.5.4",
"version": "6.5.6",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
"integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",

View file

@ -20,7 +20,7 @@
"devDependencies": {
"@codemirror/commands": "6.2.5",
"@codemirror/language": "6.9.3",
"@codemirror/search": "6.5.4",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.2.1",
"@codemirror/view": "6.22.2",
"@lezer/highlight": "1.2.0",

View file

@ -17,7 +17,6 @@
"ve": "readonly"
},
"rules": {
"max-len": "off",
"es-x/no-array-prototype-includes": "off"
},
"overrides": [

View file

@ -0,0 +1,173 @@
const {
EditorSelection,
EditorView,
Prec,
StateEffect,
StateEffectType,
StateField,
keymap,
showPanel
} = require( 'ext.CodeMirror.v6.lib' );
const CodeMirrorPanel = require( './codemirror.panel.js' );
/**
* Custom goto line panel for CodeMirror using CSS-only Codex components.
*
* Using the Alt-g keybinding, this shows a panel asking the user for a line number,
* when a valid position is provided, moves the cursor to that line.
*
* This feature supports line numbers, relative line offsets prefixed with `+` or `-`,
* document percentages suffixed with `%`, and an optional column position by adding `:`
* and a second number after the line number.
*
* Based on the CodeMirror implementation (MIT).
*
* @see https://github.com/codemirror/search/blob/0d8af3e4cc/src/goto-line.ts
* @extends CodeMirrorPanel
*/
class CodeMirrorGotoLine extends CodeMirrorPanel {
constructor() {
super();
/**
* @type {StateEffectType}
*/
this.toggleEffect = StateEffect.define();
/**
* @type {StateField}
*/
this.panelStateField = StateField.define( {
create: () => true,
update: ( value, transaction ) => {
for ( const e of transaction.effects ) {
if ( e.is( this.toggleEffect ) ) {
value = e.value;
}
}
return value;
},
// eslint-disable-next-line arrow-body-style
provide: ( stateField ) => {
// eslint-disable-next-line arrow-body-style
return showPanel.from( stateField, ( on ) => {
return on ? () => this.panel : null;
} );
}
} );
/**
* @type {HTMLInputElement}
*/
this.input = undefined;
}
/**
* @inheritDoc
*/
get extension() {
// Use Prec.highest to ensure that this keymap is used before the default searchKeymap.
return Prec.highest(
keymap.of( {
key: 'Mod-Alt-g',
run: ( view ) => {
this.view = view;
const effects = [ this.toggleEffect.of( true ) ];
if ( !this.view.state.field( this.panelStateField, false ) ) {
effects.push( StateEffect.appendConfig.of( [ this.panelStateField ] ) );
}
this.view.dispatch( { effects } );
return true;
}
} )
);
}
/**
* @inheritDoc
*/
get panel() {
const container = document.createElement( 'div' );
container.className = 'cm-mw-goto-line-panel cm-mw-panel cm-mw-panel--row';
container.addEventListener( 'keydown', this.onKeydown.bind( this ) );
// Line input.
const [ inputWrapper, input ] = this.getTextInput( 'line', this.line );
this.input = input;
container.appendChild( inputWrapper );
// Go button.
const button = this.getButton( 'codemirror-goto-line-go' );
button.addEventListener( 'click', this.go.bind( this ) );
container.appendChild( button );
return {
dom: container,
top: true,
mount: () => {
this.input.value = String(
this.view.state.doc.lineAt(
this.view.state.selection.main.head
).number
);
this.input.focus();
this.input.select();
}
};
}
/**
* Respond to keydown events.
*
* @param {KeyboardEvent} event
*/
onKeydown( event ) {
if ( event.key === 'Escape' ) {
event.preventDefault();
this.view.dispatch( { effects: this.toggleEffect.of( false ) } );
this.view.focus();
} else if ( event.key === 'Enter' ) {
event.preventDefault();
this.go();
}
}
/**
* Go to the specified line.
*/
go() {
const match = /^([+-])?(\d+)?(:\d+)?(%)?$/.exec( this.input.value );
if ( !match ) {
return;
}
const { state } = this.view;
const startLine = state.doc.lineAt( state.selection.main.head );
const [ , sign, ln, cl, percent ] = match;
const col = cl ? +cl.slice( 1 ) : 0;
let line = ln ? +ln : startLine.number;
if ( ln && percent ) {
let pc = line / 100;
if ( sign ) {
pc = pc * ( sign === '-' ? -1 : 1 ) + ( startLine.number / state.doc.lines );
}
line = Math.round( state.doc.lines * pc );
} else if ( ln && sign ) {
line = line * ( sign === '-' ? -1 : 1 ) + startLine.number;
}
const docLine = state.doc.line( Math.max( 1, Math.min( state.doc.lines, line ) ) );
const selection = EditorSelection.cursor(
docLine.from + Math.max( 0, Math.min( col, docLine.length ) )
);
this.view.dispatch( {
effects: [
this.toggleEffect.of( false ),
EditorView.scrollIntoView( selection.from, { y: 'center' } )
],
selection
} );
this.view.focus();
}
}
module.exports = CodeMirrorGotoLine;

View file

@ -15,10 +15,11 @@ const {
keymap,
lineNumbers,
rectangularSelection,
redo,
searchKeymap
redo
} = require( 'ext.CodeMirror.v6.lib' );
const CodeMirrorTextSelection = require( './codemirror.textSelection.js' );
const CodeMirrorSearch = require( './codemirror.search.js' );
const CodeMirrorGotoLine = require( './codemirror.gotoLine.js' );
require( './ext.CodeMirror.data.js' );
/**
@ -125,6 +126,7 @@ class CodeMirror {
this.updateExtension,
this.bracketMatchingExtension,
this.dirExtension,
this.searchExtension,
EditorState.readOnly.of( this.readOnly ),
EditorView.domEventHandlers( {
blur: () => {
@ -135,10 +137,7 @@ class CodeMirror {
}
} ),
EditorView.lineWrapping,
keymap.of( [
...defaultKeymap,
...searchKeymap
] ),
keymap.of( defaultKeymap ),
EditorState.allowMultipleSelections.of( true ),
drawSelection(),
rectangularSelection(),
@ -171,6 +170,18 @@ class CodeMirror {
return extensions;
}
/**
* Extension for search and goto line functionality.
*
* @return {Extension|Extension[]}
*/
get searchExtension() {
return [
new CodeMirrorSearch().extension,
new CodeMirrorGotoLine().extension
];
}
/**
* This extension adds bracket matching to the CodeMirror editor.
*
@ -293,16 +304,6 @@ class CodeMirror {
*/
get phrasesExtension() {
return EditorState.phrases.of( {
Find: mw.msg( 'codemirror-find' ),
next: mw.msg( 'codemirror-next' ),
previous: mw.msg( 'codemirror-previous' ),
all: mw.msg( 'codemirror-all' ),
'match case': mw.msg( 'codemirror-match-case' ),
regexp: mw.msg( 'codemirror-regexp' ),
'by word': mw.msg( 'codemirror-by-word' ),
replace: mw.msg( 'codemirror-replace' ),
Replace: mw.msg( 'codemirror-replace-placeholder' ),
'replace all': mw.msg( 'codemirror-replace-all' ),
'Control character': mw.msg( 'codemirror-control-character' )
} );
}

View file

@ -1,7 +1,39 @@
@import 'mediawiki.skin.variables.less';
.cm-editor {
border: 1px solid #c8ccd1;
border: @border-width-base @border-style-base @border-color-subtle;
.cm-selectionBackground {
background: #d9d9d9;
@media screen {
html.skin-theme-clientpref-night & {
background: #222;
}
}
@media screen and ( prefers-color-scheme: dark ) {
html.skin-theme-clientpref-os & {
background: #222;
}
}
}
&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground {
background: #d7d4f0;
@media screen {
html.skin-theme-clientpref-night & {
background: #233;
}
}
@media screen and ( prefers-color-scheme: dark ) {
html.skin-theme-clientpref-os & {
background: #233;
}
}
}
}
.cm-matchingBracket,
@ -11,16 +43,26 @@
font-weight: bold;
}
.cm-editor .cm-specialChar {
color: @color-destructive--hover;
}
.cm-special-char-nbsp {
color: #888;
color: @color-placeholder;
}
.cm-tooltip-fold {
cursor: pointer;
cursor: @cursor-base--hover;
line-height: 1.2;
padding: 0 1px;
}
.cm-editor .cm-foldPlaceholder {
background-color: @background-color-interactive;
border-color: @border-color-subtle;
color: @color-base;
}
.cm-bidi-isolate {
/* @noflip */
direction: ltr;
@ -31,56 +73,92 @@
height: 100%;
}
// Overrides for WikiEditor.
.wikiEditor-ui-text .cm-editor {
border: inherit;
// The various .cm-editor prefixed styles are required to have higher
// specificity than CodeMirror's default styles, which are set by JS.
.cm-editor .cm-gutters {
background-color: @background-color-interactive-subtle;
border-right-color: @border-color-subtle;
color: @color-subtle;
}
.cm-mw-toggle-wikieditor {
.oo-ui-icon-syntax-highlight {
background-color: @color-base;
// The SVG is just barely over 300 bytes, and is also only temporary
// until an official icon has been established in Codex/OOUI (T174145).
/* @embed */
@url: url( codemirror.icon.svg );
-webkit-mask-image: @url;
mask-image: @url;
-webkit-mask-size: @size-icon-medium;
mask-size: @size-icon-medium;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
}
.cm-editor .cm-cursor {
border-left-color: @color-emphasized;
}
&:hover {
background-color: @background-color-interactive;
}
.cm-editor .cm-tooltip {
background-color: @background-color-neutral-subtle;
border-color: @border-color-base;
}
&.oo-ui-toggleWidget-on {
.oo-ui-labelElement-label {
color: @color-progressive;
}
.cm-editor .cm-panels {
background-color: @background-color-neutral-subtle;
border-bottom: 0;
color: @color-base;
z-index: @z-index-above-content;
.oo-ui-icon-syntax-highlight {
background-color: @color-progressive;
.cdx-button-group {
.cdx-button,
.cdx-toggle-button {
min-width: @min-width-toggle-switch;
}
}
&.oo-ui-buttonElement-frameless.oo-ui-labelElement.oo-ui-iconElement:first-child {
margin-left: 0;
.cm-mw-panel {
border-bottom: @border-style-base @border-width-base @border-color-subtle;
padding: @spacing-50;
}
.cm-mw-panel--text-input {
flex-basis: 0;
flex-grow: 1;
}
.cm-mw-panel--row {
align-items: center;
column-gap: @spacing-50;
display: flex;
&:not( :last-child ) {
margin-bottom: @spacing-50;
}
}
.cm-mw-panel--button {
margin-bottom: 0;
}
.cm-mw-panel--toggle-button.cdx-toggle-button--toggled-on {
&:enabled:active {
background-color: @color-progressive--active;
}
.cdx-icon {
background-color: @color-inverted;
}
}
}
// Hide all buttons except CodeMirror on read only pages (T301615)
// This is the same hack that CodeEditor uses to customize the toolbar.
// WikiEditor should be updated to better handle read only pages (T188817).
.ext-codemirror-readonly {
.wikiEditor-section-secondary,
.group:not( .group-codemirror ),
.tabs,
.sections {
display: none;
}
.cm-mw-icon--match-case {
background-color: @color-base;
.cdx-mixin-css-icon( @cdx-icon-search-case-sensitive, @color-base, @size-icon-medium, true );
}
.cm-mw-icon--regexp {
background-color: @color-base;
.cdx-mixin-css-icon( @cdx-icon-search-regular-expression, @color-base, @size-icon-medium, true );
}
.cm-mw-icon--quotes {
background-color: @color-base;
.cdx-mixin-css-icon( @cdx-icon-quotes, @color-base, @size-icon-medium, true );
}
.cm-mw-icon--previous {
background-color: @color-base;
.cdx-mixin-css-icon( @cdx-icon-previous, @color-base, @size-icon-medium, true );
}
.cm-mw-icon--next {
background-color: @color-base;
.cdx-mixin-css-icon( @cdx-icon-next, @color-base, @size-icon-medium, true );
}

View file

@ -1,16 +1,20 @@
@comment-color: #72777d;
@error-color: #d73333;
@link-color: #000aaa;
@parser-function-color: #d73333;
@table-color: #d08;
@template-color: #80c;
@template-variable-color: #ac6600;
@wikitext-formatting-color: #0076dd;
@xml-tag-color: #14866d;
@import 'mediawiki.skin.variables.less';
@error-color: @color-destructive;
@link-color: @color-progressive;
@parser-function-color: @color-destructive;
@table-color: #d08;
@table-color-dark: #ff5edd;
@template-color: #80c;
@template-color-dark: #af84e6;
@template-variable-color: #ac6600;
@wikitext-formatting-color: @color-progressive--focus;
@wikitext-formatting-color-dark: @color-progressive--hover;
@xml-tag-color: @color-content-added;
@template-background-color: #a11;
@ext-background-color: #70a;
@ext-background-color: #eee;
@link-background-color: #219;
@skip-formatting-color: #adf;
.ground( @template: 0, @ext: 0, @link: 0 ) {
@template-shade: fade( @template-background-color, 4% * @template );
@ -19,44 +23,76 @@
background-color: average( average( @template-shade, @ext-shade ), @link-shade );
}
/* stylelint-disable declaration-block-single-line-max-declarations */
/* stylelint-disable @stylistic/block-closing-brace-space-after */
/* stylelint-disable @stylistic/block-opening-brace-newline-after */
/* stylelint-disable @stylistic/block-opening-brace-newline-before */
/* stylelint-disable @stylistic/declaration-block-semicolon-newline-after */
/* stylelint-disable @stylistic/selector-list-comma-newline-after */
.darkmode( @prop, @value ) {
@media screen {
html.skin-theme-clientpref-night & {
@{prop}: @value;
}
}
// See T365311
.CodeMirror { color: inherit; }
@media screen and ( prefers-color-scheme: dark ) {
html.skin-theme-clientpref-os & {
@{prop}: @value;
}
}
}
.cm-mw-pagename { text-decoration: underline; }
.wikitext-formatting-color {
color: @wikitext-formatting-color;
.darkmode( color, @wikitext-formatting-color-dark );
}
// TODO: It appears like this was never used. Remove?
.cm-mw-matching { background-color: #ffd700; }
.cm-mw-pagename {
text-decoration: underline;
}
.cm-mw-skipformatting { background-color: #adf; }
.cm-mw-skipformatting {
background-color: @skip-formatting-color;
}
.cm-mw-list,
.cm-mw-indenting { color: @wikitext-formatting-color; font-weight: bold; }
.cm-mw-indenting {
.wikitext-formatting-color();
font-weight: bold;
}
// FIXME: Remove camelCase variant after CM6 upgrade is complete (also check Global Search)
.cm-mw-doubleUnderscore,
.cm-mw-double-underscore,
.cm-mw-signature, .cm-mw-hr { color: @wikitext-formatting-color; font-weight: bold; background-color: #eee; }
.cm-mw-signature,
.cm-mw-hr {
.wikitext-formatting-color();
font-weight: bold;
background-color: @background-color-disabled-subtle;
}
// TODO: Deprecate .cm-mw-mnemonic in favor of -html-entity
.cm-mw-mnemonic, .cm-mw-html-entity { color: @xml-tag-color; }
.cm-mw-comment { color: @comment-color; font-weight: normal; }
.cm-mw-apostrophes-bold, .cm-mw-apostrophes-italic { color: @wikitext-formatting-color; }
.cm-mw-strong { font-weight: bold; }
.cm-mw-mnemonic,
.cm-mw-html-entity {
color: @xml-tag-color;
}
.cm-mw-comment {
color: @color-subtle;
font-weight: normal;
}
.cm-mw-apostrophes-bold,
.cm-mw-apostrophes-italic {
.wikitext-formatting-color();
}
.cm-mw-strong {
font-weight: bold;
}
// FIXME: Remove .CodeMirror-line rules after CM6 upgrade
pre.CodeMirror-line.cm-mw-section-1,
pre.CodeMirror-line-like.cm-mw-section-1,
.cm-mw-section-1, .cm-mw-section-1 ~ * {
.cm-mw-section-1,
.cm-mw-section-1 ~ * {
font-size: 1.8em;
line-height: 1.2em;
}
pre.CodeMirror-line.cm-mw-section-2,
pre.CodeMirror-line-like.cm-mw-section-2,
.cm-mw-section-2, .cm-mw-section-2 ~ * {
.cm-mw-section-2,
.cm-mw-section-2 ~ * {
font-size: 1.5em;
line-height: 1.2em;
}
@ -76,48 +112,92 @@ span.cm-mw-section-6 ~ * {
font-weight: bold;
}
.cm-mw-template { color: @template-color; font-weight: normal; }
.cm-mw-template {
color: @template-color;
font-weight: normal;
html.skin-theme-clientpref-night & {
color: @template-color-dark;
}
@media ( prefers-color-scheme: dark ) {
html.skin-theme-clientpref-os & {
color: @template-color-dark;
}
}
}
// TODO: deprecate/remove after CM6 upgrade
.cm-mw-template-name-mnemonic { font-weight: normal; }
.cm-mw-template-name-mnemonic {
font-weight: normal;
}
.cm-mw-template-name,
.cm-mw-template-argument-name,
.cm-mw-template-delimiter,
.cm-mw-template-bracket { color: @template-color; font-weight: bold; }
.cm-mw-template-bracket {
color: @template-color;
font-weight: bold;
.darkmode( color, @template-color-dark );
}
.cm-mw-templatevariable,
.cm-mw-templatevariable-bracket { color: @template-variable-color; font-weight: normal; }
.cm-mw-templatevariable-bracket {
color: @template-variable-color;
font-weight: normal;
}
.cm-mw-templatevariable-name,
.cm-mw-templatevariable-delimiter { color: @template-variable-color; font-weight: bold; }
.cm-mw-templatevariable-delimiter {
color: @template-variable-color;
font-weight: bold;
}
.cm-mw-parserfunction { font-weight: normal; }
.cm-mw-parserfunction {
font-weight: normal;
}
.cm-mw-parserfunction-name,
.cm-mw-parserfunction-bracket,
.cm-mw-parserfunction-delimiter { color: @parser-function-color; font-weight: bold; }
.cm-mw-parserfunction-delimiter {
color: @parser-function-color;
font-weight: bold;
}
pre.CodeMirror-line.cm-mw-exttag,
pre.CodeMirror-line-like.cm-mw-exttag {
.ground( @ext: 0.5 );
}
.cm-mw-exttag { .ground( @ext: 1 ); }
.cm-mw-exttag {
.ground( @ext: 1 );
}
.cm-mw-exttag-name,
.cm-mw-htmltag-name { color: @xml-tag-color; font-weight: bold; }
.cm-mw-htmltag-name {
color: @xml-tag-color;
font-weight: bold;
}
.cm-mw-exttag-bracket,
.cm-mw-exttag-attribute,
.cm-mw-htmltag-bracket,
.cm-mw-htmltag-attribute { color: @xml-tag-color; font-weight: normal; }
.cm-mw-htmltag-attribute {
color: @xml-tag-color;
font-weight: normal;
}
.cm-mw-tag-pre,
.cm-mw-tag-nowiki,
pre.CodeMirror-line.cm-mw-tag-pre,
pre.CodeMirror-line-like.cm-mw-tag-pre,
.cm-mw-tag-pre,
pre.CodeMirror-line.cm-mw-tag-nowiki,
pre.CodeMirror-line-like.cm-mw-tag-nowiki,
.cm-mw-tag-nowiki {
pre.CodeMirror-line-like.cm-mw-tag-nowiki {
background-color: rgba( 0, 0, 0, 0.04 );
.darkmode( background-color, rgba( 255, 255, 255, 0.06 ) );
}
.cm-mw-link,
.cm-mw-link-tosection,
.cm-mw-section-header { color: @wikitext-formatting-color; font-weight: normal; }
.cm-mw-section-header {
.wikitext-formatting-color();
font-weight: normal;
}
.cm-mw-link-pagename,
.cm-mw-link-bracket,
.cm-mw-link-delimiter,
@ -130,52 +210,109 @@ pre.CodeMirror-line-like.cm-mw-tag-nowiki,
}
.cm-mw-extlink-protocol,
.cm-mw-free-extlink-protocol,
.cm-mw-extlink-bracket { color: @link-color; font-weight: bold; }
.cm-mw-table-bracket,
.cm-mw-table-delimiter { color: @table-color; font-weight: bold; }
.cm-mw-table-definition { color: @table-color; font-weight: normal; }
.cm-mw-table-caption { font-weight: bold; }
.cm-mw-template2-ground { .ground( @template: 1 ); }
.cm-mw-template3-ground { .ground( @template: 2 ); }
.cm-mw-ext-ground,
.cm-mw-template-ext-ground { .ground( @ext: 1 ); }
.cm-mw-ext2-ground,
.cm-mw-template-ext2-ground { .ground( @ext: 2 ); }
.cm-mw-ext3-ground,
.cm-mw-template-ext3-ground { .ground( @ext: 3 ); }
.cm-mw-link-ground,
.cm-mw-ext-link-ground,
.cm-mw-template-link-ground { .ground( @link: 1 ); }
.cm-mw-ext2-link-ground,
.cm-mw-template-ext-link-ground { .ground( @ext: 1, @link: 1 ); }
.cm-mw-ext3-link-ground,
.cm-mw-template-ext2-link-ground { .ground( @ext: 2, @link: 1 ); }
.cm-mw-template-ext3-link-ground { .ground( @ext: 3, @link: 1 ); }
.cm-mw-template2-ext-ground { .ground( @template: 1, @ext: 1 ); }
.cm-mw-template2-ext2-ground { .ground( @template: 1, @ext: 2 ); }
.cm-mw-template2-ext3-ground { .ground( @template: 1, @ext: 3 ); }
.cm-mw-template2-link-ground { .ground( @template: 1, @link: 1 ); }
.cm-mw-template2-ext-link-ground { .ground( @template: 1, @ext: 1, @link: 1 ); }
.cm-mw-template2-ext2-link-ground { .ground( @template: 1, @ext: 2, @link: 1 ); }
.cm-mw-template2-ext3-link-ground { .ground( @template: 1, @ext: 3, @link: 1 ); }
.cm-mw-template3-ext-ground { .ground( @template: 2, @ext: 1 ); }
.cm-mw-template3-ext2-ground { .ground( @template: 2, @ext: 2 ); }
.cm-mw-template3-ext3-ground { .ground( @template: 2, @ext: 3 ); }
.cm-mw-template3-link-ground { .ground( @template: 2, @link: 1 ); }
.cm-mw-template3-ext-link-ground { .ground( @template: 2, @ext: 1, @link: 1 ); }
.cm-mw-template3-ext2-link-ground { .ground( @template: 2, @ext: 2, @link: 1 ); }
.cm-mw-template3-ext3-link-ground { .ground( @template: 2, @ext: 3, @link: 1 ); }
.cm-mw-error { color: @error-color; }
.cm-mw-em { font-style: italic; }
.cm-mw-matchingbracket {
background-color: #eee;
box-shadow: inset 0 0 1px 1px #999;
.cm-mw-extlink-bracket {
color: @link-color;
font-weight: bold;
}
.cm-mw-table-bracket,
.cm-mw-table-delimiter {
color: @table-color;
font-weight: bold;
.darkmode( color, @table-color-dark );
}
.cm-mw-table-definition {
color: @table-color;
font-weight: normal;
.darkmode( color, @table-color-dark );
}
.cm-mw-table-caption {
font-weight: bold;
}
.cm-mw-template2-ground {
.ground( @template: 1 );
}
.cm-mw-template3-ground {
.ground( @template: 2 );
}
.cm-mw-ext-ground,
.cm-mw-template-ext-ground {
.ground( @ext: 1 );
}
.cm-mw-ext2-ground,
.cm-mw-template-ext2-ground {
.ground( @ext: 2 );
}
.cm-mw-ext3-ground,
.cm-mw-template-ext3-ground {
.ground( @ext: 3 );
}
.cm-mw-link-ground,
.cm-mw-ext-link-ground,
.cm-mw-template-link-ground {
.ground( @link: 1 );
}
.cm-mw-ext2-link-ground,
.cm-mw-template-ext-link-ground {
.ground( @ext: 1, @link: 1 );
}
.cm-mw-ext3-link-ground,
.cm-mw-template-ext2-link-ground {
.ground( @ext: 2, @link: 1 );
}
.cm-mw-template-ext3-link-ground {
.ground( @ext: 3, @link: 1 );
}
.cm-mw-template2-ext-ground {
.ground( @template: 1, @ext: 1 );
}
.cm-mw-template2-ext2-ground {
.ground( @template: 1, @ext: 2 );
}
.cm-mw-template2-ext3-ground {
.ground( @template: 1, @ext: 3 );
}
.cm-mw-template2-link-ground {
.ground( @template: 1, @link: 1 );
}
.cm-mw-template2-ext-link-ground {
.ground( @template: 1, @ext: 1, @link: 1 );
}
.cm-mw-template2-ext2-link-ground {
.ground( @template: 1, @ext: 2, @link: 1 );
}
.cm-mw-template2-ext3-link-ground {
.ground( @template: 1, @ext: 3, @link: 1 );
}
.cm-mw-template3-ext-ground {
.ground( @template: 2, @ext: 1 );
}
.cm-mw-template3-ext2-ground {
.ground( @template: 2, @ext: 2 );
}
.cm-mw-template3-ext3-ground {
.ground( @template: 2, @ext: 3 );
}
.cm-mw-template3-link-ground {
.ground( @template: 2, @link: 1 );
}
.cm-mw-template3-ext-link-ground {
.ground( @template: 2, @ext: 1, @link: 1 );
}
.cm-mw-template3-ext2-link-ground {
.ground( @template: 2, @ext: 2, @link: 1 );
}
.cm-mw-template3-ext3-link-ground {
.ground( @template: 2, @ext: 3, @link: 1 );
}
.cm-mw-error {
color: @error-color;
}
.cm-mw-em {
font-style: italic;
}

View file

@ -0,0 +1,205 @@
const { EditorView, Extension, Panel } = require( 'ext.CodeMirror.v6.lib' );
/**
* Abstract class for a panel that can be used with CodeMirror.
* This class provides methods to create CSS-only Codex components.
*
* @see https://codemirror.net/docs/ref/#h_panels
* @abstract
*/
class CodeMirrorPanel {
/**
* @constructor
*/
constructor() {
/**
* @type {EditorView}
*/
this.view = undefined;
}
/**
* Get the panel and any associated keymaps as a CodeMirror Extension.
*
* @abstract
* @type {Extension}
*/
// eslint-disable-next-line getter-return
get extension() {}
/**
* Get the Panel object.
*
* @abstract
* @type {Panel}
*/
// eslint-disable-next-line getter-return
get panel() {}
/**
* Get a CSS-only Codex TextInput.
*
* @param {string} name
* @param {string} [value='']
* @param {string} placeholder
* @return {Array<HTMLElement>} [HTMLDivElement, HTMLInputElement]
* @internal
*/
getTextInput( name, value = '', placeholder = '' ) {
const wrapper = document.createElement( 'div' );
wrapper.className = 'cdx-text-input cm-mw-panel--text-input';
const input = document.createElement( 'input' );
input.className = 'cdx-text-input__input';
input.type = 'text';
input.name = name;
// The following messages may be used here:
// * codemirror-find
// * codemirror-replace-placeholder
input.placeholder = placeholder ? mw.msg( placeholder ) : '';
input.value = value;
wrapper.appendChild( input );
return [ wrapper, input ];
}
/**
* Get a CSS-only Codex Button.
*
* @param {string} label
* @param {string|null} [icon=null]
* @param {boolean} [iconOnly=false]
* @return {HTMLButtonElement}
* @internal
*/
getButton( label, icon = null, iconOnly = false ) {
const button = document.createElement( 'button' );
button.className = 'cdx-button cm-mw-panel--button';
button.type = 'button';
if ( icon ) {
const iconSpan = document.createElement( 'span' );
// The following CSS classes may be used here:
// * cm-mw-icon--previous
// * cm-mw-icon--next
// * cm-mw-icon--all
// * cm-mw-icon--replace
// * cm-mw-icon--replace-all
// * cm-mw-icon--done
// * cm-mw-icon--goto-line-go
iconSpan.className = 'cdx-button__icon cm-mw-icon--' + icon;
if ( !iconOnly ) {
iconSpan.setAttribute( 'aria-hidden', 'true' );
}
button.appendChild( iconSpan );
}
// The following messages may be used here:
// * codemirror-next
// * codemirror-previous
// * codemirror-all
// * codemirror-replace
// * codemirror-replace-all
const message = mw.msg( label );
if ( iconOnly ) {
button.classList.add( 'cdx-button--icon-only' );
button.title = message;
button.setAttribute( 'aria-label', message );
} else {
button.append( message );
}
return button;
}
/**
* Get a CSS-only Codex Checkbox.
*
* @param {string} name
* @param {string} label
* @param {boolean} [checked=false]
* @return {Array<HTMLElement>} [HTMLSpanElement, HTMLInputElement]
* @internal
*/
getCheckbox( name, label, checked = false ) {
const wrapper = document.createElement( 'span' );
wrapper.className = 'cdx-checkbox cdx-checkbox--inline cm-mw-panel--checkbox';
const input = document.createElement( 'input' );
input.className = 'cdx-checkbox__input';
input.id = `cm-mw-panel--checkbox-${ name }`;
input.type = 'checkbox';
input.name = name;
input.checked = checked;
wrapper.appendChild( input );
const emptyIcon = document.createElement( 'span' );
emptyIcon.className = 'cdx-checkbox__icon';
wrapper.appendChild( emptyIcon );
const labelWrapper = document.createElement( 'div' );
labelWrapper.className = 'cdx-checkbox__label cdx-label';
const labelElement = document.createElement( 'label' );
labelElement.className = 'cdx-label__label';
labelElement.htmlFor = input.id;
const innerSpan = document.createElement( 'span' );
innerSpan.className = 'cdx-label__label__text';
// The following messages may be used here:
// * codemirror-match-case
// * codemirror-regexp
// * codemirror-by-word
innerSpan.textContent = mw.msg( label );
labelElement.appendChild( innerSpan );
labelWrapper.appendChild( labelElement );
wrapper.appendChild( labelWrapper );
return [ wrapper, input ];
}
/**
* Get a CSS-only Codex ToggleButton.
*
* @param {string} name
* @param {string} label
* @param {string} icon
* @param {boolean} [checked=false]
* @return {HTMLButtonElement}
* @internal
*/
getToggleButton( name, label, icon, checked = false ) {
const btn = document.createElement( 'button' );
// The following CSS classes may be used here:
// * cdx-toggle-button--toggled-on
// * cdx-toggle-button--toggled-off
btn.className = 'cdx-toggle-button cdx-toggle-button--framed ' +
`cdx-toggle-button--toggled-${ checked ? 'on' : 'off' } cm-mw-panel--toggle-button`;
btn.dataset.checked = String( checked );
btn.setAttribute( 'aria-pressed', checked );
// The following messages may be used here:
// * codemirror-match-case
// * codemirror-regexp
// * codemirror-by-word
const message = mw.msg( label );
btn.title = message;
btn.setAttribute( 'aria-label', message );
// Add the icon.
const iconWrapper = document.createElement( 'span' );
// The following CSS classes may be used here:
// * cm-mw-icon--match-case
// * cm-mw-icon--regexp
// * cm-mw-icon--quotes
iconWrapper.className = 'cdx-icon cdx-icon--medium cm-mw-icon--' + icon;
btn.appendChild( iconWrapper );
// Add the click handler.
btn.addEventListener( 'click', ( e ) => {
e.preventDefault();
const toggled = btn.dataset.checked === 'true';
btn.dataset.checked = String( !toggled );
btn.setAttribute( 'aria-pressed', String( !toggled ) );
btn.classList.toggle( 'cdx-toggle-button--toggled-on', !toggled );
btn.classList.toggle( 'cdx-toggle-button--toggled-off', toggled );
} );
return btn;
}
}
module.exports = CodeMirrorPanel;

View file

@ -0,0 +1,307 @@
const {
EditorView,
SearchQuery,
closeSearchPanel,
findNext,
findPrevious,
keymap,
replaceAll,
replaceNext,
runScopeHandlers,
search,
searchKeymap,
selectMatches,
setSearchQuery
} = require( 'ext.CodeMirror.v6.lib' );
const CodeMirrorPanel = require( './codemirror.panel.js' );
/**
* Custom search panel for CodeMirror using CSS-only Codex components.
*
* @extends CodeMirrorPanel
*/
class CodeMirrorSearch extends CodeMirrorPanel {
constructor() {
super();
/**
* @type {SearchQuery}
*/
this.searchQuery = {
search: ''
};
/**
* @type {HTMLInputElement}
*/
this.searchInput = undefined;
/**
* @type {HTMLInputElement}
*/
this.replaceInput = undefined;
/**
* @type {HTMLButtonElement}
*/
this.matchCaseButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.regexpButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.wholeWordButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.nextButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.prevButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.allButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.replaceButton = undefined;
/**
* @type {HTMLButtonElement}
*/
this.replaceAllButton = undefined;
}
/**
* @inheritDoc
*/
get extension() {
return [
search( {
createPanel: ( view ) => {
this.view = view;
return this.panel;
}
} ),
keymap.of( searchKeymap )
];
}
/**
* @inheritDoc
*/
get panel() {
const container = document.createElement( 'div' );
container.className = 'cm-mw-panel cm-mw-panel--search-panel';
container.addEventListener( 'keydown', this.onKeydown.bind( this ) );
const firstRow = document.createElement( 'div' );
firstRow.className = 'cm-mw-panel--row';
container.appendChild( firstRow );
// Search input.
const [ searchInputWrapper, searchInput ] = this.getTextInput(
'search',
this.searchQuery.search || '',
'codemirror-find'
);
this.searchInput = searchInput;
this.searchInput.setAttribute( 'main-field', 'true' );
firstRow.appendChild( searchInputWrapper );
this.appendPrevAndNextButtons( firstRow );
// "All" button.
this.allButton = this.getButton( 'codemirror-all' );
this.allButton.title = mw.msg( 'codemirror-all-tooltip' );
this.allButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
selectMatches( this.view );
} );
firstRow.appendChild( this.allButton );
this.appendSearchOptions( firstRow );
this.appendSecondRow( container );
return {
dom: container,
top: true,
mount: () => {
this.searchInput.focus();
this.searchInput.select();
}
};
}
/**
* @param {HTMLDivElement} firstRow
* @private
*/
appendPrevAndNextButtons( firstRow ) {
const buttonGroup = document.createElement( 'div' );
buttonGroup.className = 'cdx-button-group';
// "Previous" button.
this.prevButton = this.getButton( 'codemirror-previous', 'previous', true );
buttonGroup.appendChild( this.prevButton );
this.prevButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
findPrevious( this.view );
} );
// "Next" button.
this.nextButton = this.getButton( 'codemirror-next', 'next', true );
buttonGroup.appendChild( this.nextButton );
this.nextButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
findNext( this.view );
} );
firstRow.appendChild( buttonGroup );
}
/**
* @param {HTMLDivElement} firstRow
* @private
*/
appendSearchOptions( firstRow ) {
const buttonGroup = document.createElement( 'div' );
buttonGroup.className = 'cdx-toggle-button-group';
// "Match case" ToggleButton.
this.matchCaseButton = this.getToggleButton(
'case',
'codemirror-match-case',
'match-case',
this.searchQuery.caseSensitive
);
buttonGroup.appendChild( this.matchCaseButton );
// "Regexp" ToggleButton.
this.regexpButton = this.getToggleButton(
're',
'codemirror-regexp',
'regexp',
this.searchQuery.regexp
);
buttonGroup.appendChild( this.regexpButton );
// "Whole word" checkbox.
this.wholeWordButton = this.getToggleButton(
'word',
'codemirror-by-word',
'quotes',
this.searchQuery.wholeWord
);
buttonGroup.appendChild( this.wholeWordButton );
firstRow.appendChild( buttonGroup );
}
/**
* @param {HTMLDivElement} container
* @private
*/
appendSecondRow( container ) {
const shouldBeDisabled = this.view.state.readOnly;
const row = document.createElement( 'div' );
row.className = 'cm-mw-panel--row';
container.appendChild( row );
// Replace input.
const [ replaceInputWrapper, replaceInput ] = this.getTextInput(
'replace',
this.searchQuery.replace || '',
'codemirror-replace-placeholder'
);
this.replaceInput = replaceInput;
this.replaceInput.disabled = shouldBeDisabled;
row.appendChild( replaceInputWrapper );
// "Replace" button.
this.replaceButton = this.getButton( 'codemirror-replace' );
this.replaceButton.disabled = shouldBeDisabled;
row.appendChild( this.replaceButton );
this.replaceButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
replaceNext( this.view );
} );
// "Replace all" button.
this.replaceAllButton = this.getButton( 'codemirror-replace-all' );
this.replaceAllButton.disabled = shouldBeDisabled;
row.appendChild( this.replaceAllButton );
this.replaceAllButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
replaceAll( this.view );
} );
// "Done" button.
const doneButton = this.getButton( 'codemirror-done' );
row.appendChild( doneButton );
doneButton.addEventListener( 'click', ( e ) => {
e.preventDefault();
closeSearchPanel( this.view );
this.view.focus();
} );
}
/**
* Respond to keydown events.
*
* @param {KeyboardEvent} event
*/
onKeydown( event ) {
if ( runScopeHandlers( this.view, event, 'search-panel' ) ) {
event.preventDefault();
} else if ( event.key === 'Enter' && event.target === this.searchInput ) {
event.preventDefault();
( event.shiftKey ? findPrevious : findNext )( this.view );
} else if ( event.key === 'Enter' && event.target === this.replaceInput ) {
event.preventDefault();
replaceNext( this.view );
}
}
/**
* Create a new {@link SearchQuery} and dispatch it to the {@link EditorView}.
*/
commit() {
const query = new SearchQuery( {
search: this.searchInput.value,
caseSensitive: this.matchCaseButton.dataset.checked === 'true',
regexp: this.regexpButton.dataset.checked === 'true',
wholeWord: this.wholeWordButton.dataset.checked === 'true',
replace: this.replaceInput.value,
// Makes i.e. "\n" match the literal string "\n" instead of a newline.
literal: true
} );
if ( !this.searchQuery || !query.eq( this.searchQuery ) ) {
this.searchQuery = query;
this.view.dispatch( {
effects: setSearchQuery.of( query )
} );
}
}
/**
* @inheritDoc
*/
getTextInput( name, value = '', placeholder = '' ) {
const [ container, input ] = super.getTextInput( name, value, placeholder );
input.addEventListener( 'change', this.commit.bind( this ) );
input.addEventListener( 'keyup', this.commit.bind( this ) );
return [ container, input ];
}
/**
* @inheritDoc
*/
getToggleButton( name, label, icon, checked = false ) {
const button = super.getToggleButton( name, label, icon, checked );
button.addEventListener( 'click', this.commit.bind( this ) );
return button;
}
}
module.exports = CodeMirrorSearch;

View file

@ -0,0 +1,55 @@
@import 'mediawiki.skin.variables.less';
// Overrides for WikiEditor.
.wikiEditor-ui-text .cm-editor {
border: inherit;
}
.cm-mw-toggle-wikieditor {
.oo-ui-icon-syntax-highlight {
background-color: @color-base;
// The SVG is just barely over 300 bytes, and is also only temporary
// until an official icon has been established in Codex/OOUI (T174145).
/* @embed */
@url: url( codemirror.icon.svg );
-webkit-mask-image: @url;
mask-image: @url;
-webkit-mask-size: @size-icon-medium;
mask-size: @size-icon-medium;
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-position: center;
mask-position: center;
}
&:hover {
background-color: @background-color-interactive;
}
&.oo-ui-toggleWidget-on {
.oo-ui-labelElement-label {
color: @color-progressive;
}
.oo-ui-icon-syntax-highlight {
background-color: @color-progressive;
}
}
&.oo-ui-buttonElement-frameless.oo-ui-labelElement.oo-ui-iconElement:first-child {
margin-left: 0;
}
}
// Hide all buttons except CodeMirror on read only pages (T301615)
// This is the same hack that CodeEditor uses to customize the toolbar.
// WikiEditor should be updated to better handle read only pages (T188817).
.ext-codemirror-readonly {
.wikiEditor-section-secondary,
.group:not( .group-codemirror ),
.tabs,
.sections {
display: none;
}
}

View file

@ -236,9 +236,6 @@ function init() {
$codeMirror.addClass( 'cm-mw-colorblind-colors' );
}
// T365311: Apply color inversion until dark syntax styles are chosen
$codeMirror.addClass( 'notheme skin-invert' );
// T305333: Reload CodeMirror to fix a cursor caret issue.
codeMirror.refresh();

View file

@ -1,6 +1,13 @@
@import 'mediawiki.mixins';
@import 'mediawiki.skin.variables.less';
/* TODO: Replace with ext.CodeMirror.v6.less following CM6 upgrade */
@matching-bracket-border-color: #eee;
@matching-bracket-box-shadow-color: #999;
// CM5 dark mode fixes, see T365311
.mw-body-content .CodeMirror {
background-color: inherit;
color: inherit;
}
.wikiEditor-ui .CodeMirror {
line-height: 1.5em;
@ -13,3 +20,32 @@
padding: 0;
}
}
.CodeMirror-gutters {
background-color: @background-color-interactive-subtle;
border-right-color: @border-color-subtle;
}
.CodeMirror-line::selection,
.CodeMirror-line > span::selection,
.CodeMirror-line > span > span::selection {
background: #d9d9d9;
@media screen {
html.skin-theme-clientpref-night & {
background: #233;
}
}
@media screen and ( prefers-color-scheme: dark ) {
html.skin-theme-clientpref-os {
background: #233;
}
}
}
.cm-mw-matchingbracket {
background-color: @matching-bracket-border-color;
box-shadow: inset 0 0 1px 1px @matching-bracket-box-shadow-color;
font-weight: bold;
}

View file

@ -21477,7 +21477,7 @@ class SearchCursor {
let norm = this.normalize(str);
for (let i = 0, pos = start;; i++) {
let code = norm.charCodeAt(i);
let match = this.match(code, pos);
let match = this.match(code, pos, this.bufferPos + this.bufferStart);
if (i == norm.length - 1) {
if (match) {
this.value = match;
@ -21490,13 +21490,13 @@ class SearchCursor {
}
}
}
match(code, pos) {
match(code, pos, end) {
let match = null;
for (let i = 0; i < this.matches.length; i += 2) {
let index = this.matches[i], keep = false;
if (this.query.charCodeAt(index) == code) {
if (index == this.query.length - 1) {
match = { from: this.matches[i + 1], to: pos + 1 };
match = { from: this.matches[i + 1], to: end };
}
else {
this.matches[i]++;
@ -21510,7 +21510,7 @@ class SearchCursor {
}
if (this.query.charCodeAt(0) == code) {
if (this.query.length == 1)
match = { from: pos, to: pos + 1 };
match = { from: pos, to: end };
else
this.matches.push(1, pos);
}
@ -21858,12 +21858,12 @@ const matchHighlighter = /*@__PURE__*/ViewPlugin.fromClass(class {
if (conf.wholeWords) {
query = state.sliceDoc(range.from, range.to); // TODO: allow and include leading/trailing space?
check = state.charCategorizer(range.head);
if (!(insideWordBoundaries(check, state, range.from, range.to)
&& insideWord(check, state, range.from, range.to)))
if (!(insideWordBoundaries(check, state, range.from, range.to) &&
insideWord(check, state, range.from, range.to)))
return Decoration.none;
}
else {
query = state.sliceDoc(range.from, range.to).trim();
query = state.sliceDoc(range.from, range.to);
if (!query)
return Decoration.none;
}
@ -22126,10 +22126,10 @@ class RegExpQuery extends QueryType {
this.prevMatchInRange(state, curTo, state.doc.length);
}
getReplacement(result) {
return this.spec.unquote(this.spec.replace.replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
return this.spec.unquote(this.spec.replace).replace(/\$([$&\d+])/g, (m, i) => i == "$" ? "$"
: i == "&" ? result.match[0]
: i != "0" && +i < result.match.length ? result.match[i]
: m));
: m);
}
matchAll(state, limit) {
let cursor = regexpCursor(this.spec, state, 0, state.doc.length), ranges = [];
@ -22422,7 +22422,7 @@ Default search-related key bindings.
- Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel)
- F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext)
- Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious)
- Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine)
- Mod-Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine)
- Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence)
*/
const searchKeymap = [
@ -22431,7 +22431,7 @@ const searchKeymap = [
{ key: "Mod-g", run: findNext, shift: findPrevious, scope: "editor search-panel", preventDefault: true },
{ key: "Escape", run: closeSearchPanel, scope: "editor search-panel" },
{ key: "Mod-Shift-l", run: selectSelectionMatches },
{ key: "Alt-g", run: gotoLine },
{ key: "Mod-Alt-g", run: gotoLine },
{ key: "Mod-d", run: selectNextOccurrence, preventDefault: true },
];
class SearchPanel {

View file

@ -0,0 +1,94 @@
const CodeMirrorPanel = require( '../../resources/codemirror.panel.js' );
// CodeMirrorPanel is tagged as abstract, but being JavaScript it isn't a
// "real" abstract class, so we can instantiate it directly for testing purposes.
const cmPanel = new CodeMirrorPanel();
describe( 'CodeMirrorPanel', () => {
it( 'should create a Codex TextInput', () => {
const [ inputWrapper, input ] = cmPanel.getTextInput( 'foo', 'bar', 'codemirror-find' );
expect( inputWrapper.className ).toBe( 'cdx-text-input cm-mw-panel--text-input' );
expect( input.className ).toBe( 'cdx-text-input__input' );
expect( input.type ).toBe( 'text' );
expect( input.name ).toBe( 'foo' );
// No i18n in unit tests, so we only check for the key.
expect( input.placeholder ).toBe( 'codemirror-find' );
expect( input.value ).toBe( 'bar' );
} );
it( 'should create a Codex Button with no icon', () => {
const buttonNoIcon = cmPanel.getButton( 'foo' );
expect( buttonNoIcon.tagName ).toBe( 'BUTTON' );
expect( buttonNoIcon.className ).toBe( 'cdx-button cm-mw-panel--button' );
expect( buttonNoIcon.type ).toBe( 'button' );
expect( buttonNoIcon.children.length ).toBe( 0 );
} );
it( 'should create a Codex button with an icon and a label', () => {
const buttonWithIcon = cmPanel.getButton( 'foo', 'bar' );
expect( buttonWithIcon.tagName ).toBe( 'BUTTON' );
expect( buttonWithIcon.className ).toBe( 'cdx-button cm-mw-panel--button' );
expect( buttonWithIcon.type ).toBe( 'button' );
expect( buttonWithIcon.children.length ).toBe( 1 );
const iconSpan = buttonWithIcon.children[ 0 ];
expect( iconSpan.tagName ).toBe( 'SPAN' );
expect( iconSpan.className ).toBe( 'cdx-button__icon cm-mw-icon--bar' );
expect( iconSpan.getAttribute( 'aria-hidden' ) ).toBe( 'true' );
} );
it( 'should create an icon-only Codex button', () => {
const buttonIconOnly = cmPanel.getButton( 'foo', 'bar', true );
expect( buttonIconOnly.tagName ).toBe( 'BUTTON' );
expect( buttonIconOnly.className ).toBe(
'cdx-button cm-mw-panel--button cdx-button--icon-only'
);
expect( buttonIconOnly.type ).toBe( 'button' );
expect( buttonIconOnly.children.length ).toBe( 1 );
expect( buttonIconOnly.getAttribute( 'aria-label' ) ).toBe( 'foo' );
expect( buttonIconOnly.title ).toBe( 'foo' );
const iconSpan = buttonIconOnly.children[ 0 ];
expect( iconSpan.tagName ).toBe( 'SPAN' );
expect( iconSpan.className ).toBe( 'cdx-button__icon cm-mw-icon--bar' );
expect( iconSpan.getAttribute( 'aria-hidden' ) ).toBeNull();
} );
it( 'should create a Codex Checkbox', () => {
const [ checkboxWrapper, checkbox ] = cmPanel.getCheckbox( 'foo', 'bar', true );
expect( checkboxWrapper.className ).toBe( 'cdx-checkbox cdx-checkbox--inline cm-mw-panel--checkbox' );
expect( checkboxWrapper.children.length ).toBe( 3 );
const labelWrapper = checkboxWrapper.children[ 2 ];
expect( labelWrapper.tagName ).toBe( 'DIV' );
expect( labelWrapper.className ).toBe( 'cdx-checkbox__label cdx-label' );
const label = labelWrapper.children[ 0 ];
expect( label.tagName ).toBe( 'LABEL' );
expect( label.className ).toBe( 'cdx-label__label' );
expect( label.textContent ).toBe( 'bar' );
expect( checkbox.className ).toBe( 'cdx-checkbox__input' );
expect( checkbox.type ).toBe( 'checkbox' );
expect( checkbox.name ).toBe( 'foo' );
expect( checkbox.checked ).toBe( true );
} );
it( 'should create a Codex ToggleButton', () => {
const toggleButtonOn = cmPanel.getToggleButton( 'foo', 'bar', 'baz', true );
expect( toggleButtonOn.tagName ).toBe( 'BUTTON' );
expect( toggleButtonOn.className ).toBe(
'cdx-toggle-button cdx-toggle-button--framed cdx-toggle-button--toggled-on cm-mw-panel--toggle-button'
);
expect( toggleButtonOn.dataset.checked ).toBe( 'true' );
expect( toggleButtonOn.getAttribute( 'aria-pressed' ) ).toBe( 'true' );
expect( toggleButtonOn.title ).toBe( 'bar' );
expect( toggleButtonOn.getAttribute( 'aria-label' ) ).toBe( 'bar' );
expect( toggleButtonOn.children.length ).toBe( 1 );
const iconSpan = toggleButtonOn.children[ 0 ];
expect( iconSpan.tagName ).toBe( 'SPAN' );
expect( iconSpan.className ).toBe( 'cdx-icon cdx-icon--medium cm-mw-icon--baz' );
const toggleButtonOff = cmPanel.getToggleButton( 'foo', 'bar', 'baz', false );
expect( toggleButtonOff.className ).toBe(
'cdx-toggle-button cdx-toggle-button--framed cdx-toggle-button--toggled-off cm-mw-panel--toggle-button'
);
expect( toggleButtonOff.dataset.checked ).toBe( 'false' );
expect( toggleButtonOff.getAttribute( 'aria-pressed' ) ).toBe( 'false' );
} );
} );

View file

@ -19,3 +19,4 @@ mw.track = jest.fn();
mw.Api.prototype.saveOption = jest.fn();
global.$ = require( 'jquery' );
$.fn.textSelection = () => {};
window.matchMedia = jest.fn().mockReturnValue( { matches: false } );