mirror of
https://github.com/gorhill/uBlock.git
synced 2024-11-02 00:42:45 +01:00
Use a CodeMirror editor instance in element picker
This allows to bring in all the benefits of syntax highlighting and enhanced editing features in the element picker, like auto- completion, etc. This is also a necessary step to possibly solve the following issue: - https://github.com/gorhill/uBlock/issues/2035 Additionally, incrementally improved the behavior of uBO's custom CodeMirror static filtering syntax mode when double-clicking somewhere in a static extended filter: - on a class/id string will cause the whole class/id string to be selected, including the prepending `.`/`#`. - somewhere in a hostname/entity will cause all the labels from the cursor position to the right-most label to be selected (subject to change/fine-tune as per feedback of filter list maintainers). Related feedback: - https://github.com/uBlockOrigin/uBlock-issues/issues/1134#issuecomment-679421316
This commit is contained in:
parent
9994033629
commit
a095b83250
@ -29,11 +29,6 @@ html#ublock0-epicker,
|
||||
#ublock0-epicker.paused:not(.zap) aside {
|
||||
display: block;
|
||||
}
|
||||
#ublock0-epicker ul,
|
||||
#ublock0-epicker li,
|
||||
#ublock0-epicker div {
|
||||
display: block;
|
||||
}
|
||||
#ublock0-epicker #toolbar {
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
@ -79,36 +74,31 @@ html#ublock0-epicker,
|
||||
width: 100%;
|
||||
}
|
||||
#ublock0-epicker section > div:first-child {
|
||||
border: 1px solid #aaa;
|
||||
border: 1px solid var(--default-surface-border);
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
#ublock0-epicker section.invalidFilter > div:first-child {
|
||||
border-color: red;
|
||||
}
|
||||
#ublock0-epicker section textarea {
|
||||
background-color: var(--default-surface);
|
||||
#ublock0-epicker section .codeMirrorContainer {
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: var(--default-ink);
|
||||
font: 11px monospace;
|
||||
height: 8em;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 2px 2px 1.2em 2px;
|
||||
resize: none;
|
||||
padding: 2px;
|
||||
width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
#ublock0-epicker section textarea + div {
|
||||
background-color: transparent;
|
||||
bottom: 0;
|
||||
.CodeMirror-lines,
|
||||
.CodeMirror pre {
|
||||
padding: 0;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#ublock0-epicker section .resultsetWidgets {
|
||||
display: flex;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
#resultsetModifiers {
|
||||
align-items: flex-end;
|
||||
@ -196,7 +186,7 @@ html#ublock0-epicker,
|
||||
overflow: hidden;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters {
|
||||
max-height: 16em;
|
||||
max-height: 14em;
|
||||
overflow-y: auto;
|
||||
}
|
||||
#ublock0-epicker #candidateFilters > li:first-of-type {
|
||||
|
@ -42,7 +42,9 @@ const cmEditor = new CodeMirror(document.getElementById('userFilters'), {
|
||||
lineWrapping: true,
|
||||
matchBrackets: true,
|
||||
maxScanLines: 1,
|
||||
styleActiveLine: true,
|
||||
styleActiveLine: {
|
||||
nonEmpty: true,
|
||||
},
|
||||
});
|
||||
|
||||
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
|
||||
|
@ -53,7 +53,9 @@
|
||||
matchBrackets: true,
|
||||
maxScanLines: 1,
|
||||
readOnly: true,
|
||||
styleActiveLine: true,
|
||||
styleActiveLine: {
|
||||
nonEmpty: true,
|
||||
},
|
||||
});
|
||||
|
||||
uBlockDashboard.patchCodeMirrorEditor(cmEditor);
|
||||
|
@ -318,10 +318,12 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
|
||||
return 'comment';
|
||||
}
|
||||
if ( parser.category === parser.CATStaticExtFilter ) {
|
||||
return colorExtSpan(stream);
|
||||
const style = colorExtSpan(stream);
|
||||
return style ? `ext ${style}` : 'ext';
|
||||
}
|
||||
if ( parser.category === parser.CATStaticNetFilter ) {
|
||||
return colorNetSpan(stream);
|
||||
const style = colorNetSpan(stream);
|
||||
return style ? `net ${style}` : 'net';
|
||||
}
|
||||
stream.skipToEnd();
|
||||
return null;
|
||||
@ -330,13 +332,14 @@ CodeMirror.defineMode('ubo-static-filtering', function() {
|
||||
return {
|
||||
lineComment: '!',
|
||||
token: function(stream) {
|
||||
let style = '';
|
||||
if ( stream.sol() ) {
|
||||
parser.analyze(stream.string);
|
||||
parser.analyzeExtra();
|
||||
parserSlot = 0;
|
||||
netOptionValueMode = false;
|
||||
}
|
||||
let style = colorSpan(stream) || '';
|
||||
style += colorSpan(stream) || '';
|
||||
if ( (parser.flavorBits & parser.BITFlavorError) !== 0 ) {
|
||||
style += ' line-background-error';
|
||||
}
|
||||
@ -615,24 +618,49 @@ CodeMirror.registerHelper('fold', 'ubo-static-filtering', (( ) => {
|
||||
|
||||
const s = cm.getLine(line);
|
||||
const token = cm.getTokenTypeAt(pos);
|
||||
let lmatch, rmatch;
|
||||
let select = false;
|
||||
let beg, end;
|
||||
|
||||
// Select URL in comments
|
||||
if ( token === 'comment link' ) {
|
||||
lmatch = /\S+$/.exec(s.slice(0, ch));
|
||||
rmatch = /^\S+/.exec(s.slice(ch));
|
||||
select = lmatch !== null && rmatch !== null &&
|
||||
/^https?:\/\//.test(s.slice(lmatch.index));
|
||||
if ( /\bcomment\b/.test(token) && /\blink\b/.test(token) ) {
|
||||
const l = /\S+$/.exec(s.slice(0, ch));
|
||||
if ( l && /^https?:\/\//.test(s.slice(l.index)) ) {
|
||||
const r = /^\S+/.exec(s.slice(ch));
|
||||
if ( r ) {
|
||||
beg = l.index;
|
||||
end = ch + r[0].length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Better word selection for cosmetic filters
|
||||
if ( /\bext\b/.test(token) ) {
|
||||
if ( /\bvalue\b/.test(token) ) {
|
||||
const l = /[^,.]*$/i.exec(s.slice(0, ch));
|
||||
const r = /^[^#,]*/i.exec(s.slice(ch));
|
||||
if ( l && r ) {
|
||||
beg = l.index;
|
||||
end = ch + r[0].length;
|
||||
}
|
||||
}
|
||||
if ( /\bvariable\b/.test(token) ) {
|
||||
const l = /[#.]?[a-z0-9_-]+$/i.exec(s.slice(0, ch));
|
||||
const r = /^[a-z0-9_-]+/i.exec(s.slice(ch));
|
||||
if ( l && r ) {
|
||||
beg = l.index;
|
||||
end = ch + r[0].length;
|
||||
if ( /\bdef\b/.test(cm.getTokenTypeAt({ line, ch: beg + 1 })) ) {
|
||||
beg += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add more convenient word-matching cases here
|
||||
// if ( select === false ) { ... }
|
||||
|
||||
if ( select === false ) { return Pass; }
|
||||
if ( beg === undefined ) { return Pass; }
|
||||
cm.setSelection(
|
||||
{ line, ch: lmatch.index },
|
||||
{ line, ch: ch + rmatch.index + rmatch[0].length }
|
||||
{ line, ch: beg },
|
||||
{ line, ch: end }
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -19,6 +19,8 @@
|
||||
Home: https://github.com/gorhill/uBlock
|
||||
*/
|
||||
|
||||
/* global CodeMirror */
|
||||
|
||||
'use strict';
|
||||
|
||||
/******************************************************************************/
|
||||
@ -36,7 +38,6 @@ const $storAll = selector => document.querySelectorAll(selector);
|
||||
|
||||
const pickerRoot = document.documentElement;
|
||||
const dialog = $stor('aside');
|
||||
const taCandidate = $stor('textarea');
|
||||
let staticFilteringParser;
|
||||
|
||||
const svgRoot = $stor('svg');
|
||||
@ -66,11 +67,40 @@ let needBody = false;
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const cmEditor = new CodeMirror(document.querySelector('.codeMirrorContainer'), {
|
||||
autoCloseBrackets: true,
|
||||
autofocus: true,
|
||||
extraKeys: {
|
||||
'Ctrl-Space': 'autocomplete',
|
||||
},
|
||||
lineWrapping: true,
|
||||
matchBrackets: true,
|
||||
maxScanLines: 1,
|
||||
});
|
||||
|
||||
vAPI.messaging.send('dashboard', {
|
||||
what: 'getAutoCompleteDetails'
|
||||
}).then(response => {
|
||||
if ( response instanceof Object === false ) { return; }
|
||||
const mode = cmEditor.getMode();
|
||||
if ( mode.setHints instanceof Function ) {
|
||||
mode.setHints(response);
|
||||
}
|
||||
});
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const rawFilterFromTextarea = function() {
|
||||
const text = cmEditor.getValue();
|
||||
const pos = text.indexOf('\n');
|
||||
return pos === -1 ? text : text.slice(0, pos);
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const filterFromTextarea = function() {
|
||||
const s = taCandidate.value.trim();
|
||||
if ( s === '' ) { return ''; }
|
||||
const pos = s.indexOf('\n');
|
||||
const filter = pos === -1 ? s.trim() : s.slice(0, pos).trim();
|
||||
const filter = rawFilterFromTextarea();
|
||||
if ( filter === '' ) { return ''; }
|
||||
const sfp = staticFilteringParser;
|
||||
sfp.analyze(filter);
|
||||
sfp.analyzeExtra();
|
||||
@ -256,7 +286,8 @@ const candidateFromFilterChoice = function(filterChoice) {
|
||||
const onCandidateOptimized = function(details) {
|
||||
$id('resultsetModifiers').classList.remove('hide');
|
||||
computedCandidate = details.filter;
|
||||
taCandidate.value = computedCandidate;
|
||||
cmEditor.setValue(computedCandidate);
|
||||
cmEditor.clearHistory();
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
@ -393,9 +424,9 @@ const onCandidateChanged = function() {
|
||||
$id('resultsetCount').textContent = 'E';
|
||||
$id('create').setAttribute('disabled', '');
|
||||
}
|
||||
const text = rawFilterFromTextarea();
|
||||
$id('resultsetModifiers').classList.toggle(
|
||||
'hide',
|
||||
taCandidate.value === '' || taCandidate.value !== computedCandidate
|
||||
'hide', text === '' || text !== computedCandidate
|
||||
);
|
||||
vAPI.MessagingConnection.sendTo(epickerConnectionId, {
|
||||
what: 'dialogSetFilter',
|
||||
@ -462,20 +493,22 @@ const onDepthChanged = function() {
|
||||
slot: max - value,
|
||||
});
|
||||
if ( text === undefined ) { return; }
|
||||
taCandidate.value = text;
|
||||
cmEditor.setValue(text);
|
||||
cmEditor.clearHistory();
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
/******************************************************************************/
|
||||
|
||||
const onSpecificityChanged = function() {
|
||||
if ( taCandidate.value !== computedCandidate ) { return; }
|
||||
if ( rawFilterFromTextarea() !== computedCandidate ) { return; }
|
||||
const text = candidateFromFilterChoice({
|
||||
filters: cosmeticFilterCandidates,
|
||||
slot: computedCandidateSlot,
|
||||
});
|
||||
if ( text === undefined ) { return; }
|
||||
taCandidate.value = text;
|
||||
cmEditor.setValue(text);
|
||||
cmEditor.clearHistory();
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
@ -496,7 +529,8 @@ const onCandidateClicked = function(ev) {
|
||||
}
|
||||
const text = candidateFromFilterChoice(choice);
|
||||
if ( text === undefined ) { return; }
|
||||
taCandidate.value = text;
|
||||
cmEditor.setValue(text);
|
||||
cmEditor.clearHistory();
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
@ -703,7 +737,7 @@ const showDialog = function(details) {
|
||||
// This is an issue which surfaced when the element picker code was
|
||||
// revisited to isolate the picker dialog DOM from the page DOM.
|
||||
if ( typeof filter !== 'object' || filter === null ) {
|
||||
taCandidate.value = '';
|
||||
cmEditor.setValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -714,7 +748,7 @@ const showDialog = function(details) {
|
||||
|
||||
const text = candidateFromFilterChoice(filterChoice);
|
||||
if ( text === undefined ) { return; }
|
||||
taCandidate.value = text;
|
||||
cmEditor.setValue(text);
|
||||
onCandidateChanged();
|
||||
};
|
||||
|
||||
@ -749,7 +783,8 @@ const startPicker = function() {
|
||||
|
||||
if ( pickerRoot.classList.contains('zap') ) { return; }
|
||||
|
||||
taCandidate.addEventListener('input', onCandidateChanged);
|
||||
cmEditor.on('changes', onCandidateChanged);
|
||||
|
||||
$id('preview').addEventListener('click', onPreviewClicked);
|
||||
$id('create').addEventListener('click', onCreateClicked);
|
||||
$id('pick').addEventListener('click', onPickClicked);
|
||||
|
@ -4,16 +4,20 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>uBlock Origin Element Picker</title>
|
||||
<link rel="stylesheet" href="../lib/codemirror/lib/codemirror.css">
|
||||
<link rel="stylesheet" href="../lib/codemirror/addon/hint/show-hint.css">
|
||||
|
||||
<link rel="stylesheet" href="../css/themes/default.css">
|
||||
<link rel="stylesheet" href="../css/epicker-ui.css">
|
||||
<link rel="stylesheet" href="../css/codemirror.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<aside>
|
||||
<section>
|
||||
<div>
|
||||
<textarea lang="en" dir="ltr" spellcheck="false"></textarea>
|
||||
<div>
|
||||
<div class="codeMirrorContainer codeMirrorBreakAll"></div>
|
||||
<div class="resultsetWidgets">
|
||||
<span id="resultsetModifiers">
|
||||
<span id="resultsetDepth" class="resultsetModifier">
|
||||
<span><span></span><span></span><span></span></span>
|
||||
@ -51,13 +55,20 @@
|
||||
</aside>
|
||||
<svg><path d></path><path d></path></svg>
|
||||
|
||||
<script src="../lib/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../lib/codemirror/addon/edit/closebrackets.js"></script>
|
||||
<script src="../lib/codemirror/addon/edit/matchbrackets.js"></script>
|
||||
<script src="../lib/codemirror/addon/hint/show-hint.js"></script>
|
||||
|
||||
<script src="../js/codemirror/ubo-static-filtering.js"></script>
|
||||
|
||||
<script src="../js/vapi.js"></script>
|
||||
<script src="../js/vapi-common.js"></script>
|
||||
<script src="../js/vapi-client.js"></script>
|
||||
<script src="../js/vapi-client-extra.js"></script>
|
||||
<script src="../js/i18n.js"></script>
|
||||
<script src="../js/epicker-ui.js"></script>
|
||||
<script src="../js/static-filtering-parser.js"></script>
|
||||
<script src="../js/epicker-ui.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user