Input now uses CodeMirror editor

This commit is contained in:
n1474335 2022-06-29 18:02:49 +01:00
parent 54fdc05e3a
commit 85ffe48743
17 changed files with 666 additions and 182 deletions

197
package-lock.json generated
View File

@ -95,6 +95,11 @@
"@babel/plugin-transform-runtime": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/runtime": "^7.18.3",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.1.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.0.2",
"autoprefixer": "^10.4.7",
"babel-loader": "^8.2.5",
"babel-plugin-dynamic-import-node": "^2.3.3",
@ -1782,6 +1787,60 @@
"node": ">=6.9.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.0.tgz",
"integrity": "sha512-nVJDPiCQXWXj5AZxqNVXyIM3nOYauF4Dko9NGPSwgVdK+lXWJQhI5LGhS/AvdG5b7u7/pTQBkrQmzkLWRBF62A==",
"dev": true,
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.1.0.tgz",
"integrity": "sha512-CeqY80nvUFrJcXcBW115aNi06D0PS8NSW6nuJRSwbrYFkE0SfJnPfyLGrcM90AV95lqg5+4xUi99BCmzNaPGJg==",
"dev": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/search": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz",
"integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==",
"dev": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.0.1.tgz",
"integrity": "sha512-6vYgaXc4KjSY0BUfSVDJooGcoswg/RJZpq/ZGjsUYmY0KN1lmB8u03nv+jiG1ncUV5qoggyxFT5AGD5Ak+5Zrw==",
"dev": true
},
"node_modules/@codemirror/view": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz",
"integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==",
"dev": true,
"dependencies": {
"@codemirror/state": "^6.0.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -2370,6 +2429,30 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
"node_modules/@lezer/common": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
"integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==",
"dev": true
},
"node_modules/@lezer/highlight": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
"integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
"dev": true,
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.0.0.tgz",
"integrity": "sha512-k6DEqBh4HxqO/cVGedb6Ern6LS7K6IOzfydJ5WaqCR26v6UR9sIFyb6PS+5rPUs/mXgnBR/QQCW7RkyjSCMoQA==",
"dev": true,
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@nightwatch/chai": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@nightwatch/chai/-/chai-5.0.2.tgz",
@ -5059,6 +5142,12 @@
"sha.js": "^2.4.8"
}
},
"node_modules/crelt": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==",
"dev": true
},
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -14244,6 +14333,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
"dev": true
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -14982,6 +15077,12 @@
"resolved": "https://registry.npmjs.org/vkbeautify/-/vkbeautify-0.99.3.tgz",
"integrity": "sha512-2ozZEFfmVvQcHWoHLNuiKlUfDKlhh4KGsy54U0UrlLMR1SO+XKAIDqBxtBwHgNrekurlJwE8A9K6L49T78ZQ9Q=="
},
"node_modules/w3c-keyname": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
"dev": true
},
"node_modules/watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
@ -17001,6 +17102,60 @@
"to-fast-properties": "^2.0.0"
}
},
"@codemirror/commands": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.0.0.tgz",
"integrity": "sha512-nVJDPiCQXWXj5AZxqNVXyIM3nOYauF4Dko9NGPSwgVdK+lXWJQhI5LGhS/AvdG5b7u7/pTQBkrQmzkLWRBF62A==",
"dev": true,
"requires": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0"
}
},
"@codemirror/language": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.1.0.tgz",
"integrity": "sha512-CeqY80nvUFrJcXcBW115aNi06D0PS8NSW6nuJRSwbrYFkE0SfJnPfyLGrcM90AV95lqg5+4xUi99BCmzNaPGJg==",
"dev": true,
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"@codemirror/search": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.0.0.tgz",
"integrity": "sha512-rL0rd3AhI0TAsaJPUaEwC63KHLO7KL0Z/dYozXj6E7L3wNHRyx7RfE0/j5HsIf912EE5n2PCb4Vg0rGYmDv4UQ==",
"dev": true,
"requires": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"crelt": "^1.0.5"
}
},
"@codemirror/state": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.0.1.tgz",
"integrity": "sha512-6vYgaXc4KjSY0BUfSVDJooGcoswg/RJZpq/ZGjsUYmY0KN1lmB8u03nv+jiG1ncUV5qoggyxFT5AGD5Ak+5Zrw==",
"dev": true
},
"@codemirror/view": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.0.2.tgz",
"integrity": "sha512-mnVT/q1JvKPjpmjXJNeCi/xHyaJ3abGJsumIVpdQ1nE1MXAyHf7GHWt8QpWMUvDiqF0j+inkhVR2OviTdFFX7Q==",
"dev": true,
"requires": {
"@codemirror/state": "^6.0.0",
"style-mod": "^4.0.0",
"w3c-keyname": "^2.2.4"
}
},
"@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@ -17452,6 +17607,30 @@
"integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==",
"dev": true
},
"@lezer/common": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.0.0.tgz",
"integrity": "sha512-ohydQe+Hb+w4oMDvXzs8uuJd2NoA3D8YDcLiuDsLqH+yflDTPEpgCsWI3/6rH5C3BAedtH1/R51dxENldQceEA==",
"dev": true
},
"@lezer/highlight": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.0.0.tgz",
"integrity": "sha512-nsCnNtim90UKsB5YxoX65v3GEIw3iCHw9RM2DtdgkiqAbKh9pCdvi8AWNwkYf10Lu6fxNhXPpkpHbW6mihhvJA==",
"dev": true,
"requires": {
"@lezer/common": "^1.0.0"
}
},
"@lezer/lr": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.0.0.tgz",
"integrity": "sha512-k6DEqBh4HxqO/cVGedb6Ern6LS7K6IOzfydJ5WaqCR26v6UR9sIFyb6PS+5rPUs/mXgnBR/QQCW7RkyjSCMoQA==",
"dev": true,
"requires": {
"@lezer/common": "^1.0.0"
}
},
"@nightwatch/chai": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@nightwatch/chai/-/chai-5.0.2.tgz",
@ -19640,6 +19819,12 @@
"sha.js": "^2.4.8"
}
},
"crelt": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.5.tgz",
"integrity": "sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==",
"dev": true
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -26720,6 +26905,12 @@
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true
},
"style-mod": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.0.0.tgz",
"integrity": "sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==",
"dev": true
},
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -27303,6 +27494,12 @@
"resolved": "https://registry.npmjs.org/vkbeautify/-/vkbeautify-0.99.3.tgz",
"integrity": "sha512-2ozZEFfmVvQcHWoHLNuiKlUfDKlhh4KGsy54U0UrlLMR1SO+XKAIDqBxtBwHgNrekurlJwE8A9K6L49T78ZQ9Q=="
},
"w3c-keyname": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.4.tgz",
"integrity": "sha512-tOhfEwEzFLJzf6d1ZPkYfGj+FWhIpBux9ppoP3rlclw3Z0BZv3N7b7030Z1kYth+6rDuAsXUFr+d0VE6Ed1ikw==",
"dev": true
},
"watchpack": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",

View File

@ -45,6 +45,11 @@
"@babel/plugin-transform-runtime": "^7.18.2",
"@babel/preset-env": "^7.18.2",
"@babel/runtime": "^7.18.3",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.1.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.0.2",
"autoprefixer": "^10.4.7",
"babel-loader": "^8.2.5",
"babel-plugin-dynamic-import-node": "^2.3.3",

View File

@ -112,7 +112,7 @@ CMYK: ${cmyk}
useAlpha: true
}).on('colorpickerChange', function(e) {
var color = e.color.string('rgba');
document.getElementById('input-text').value = color;
window.app.manager.input.setInput(color);
window.app.manager.input.debounceInputChange(new Event("keyup"));
});
</script>`;

View File

@ -146,8 +146,7 @@ class Manager {
this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);
// Input
this.addMultiEventListener("#input-text", "keyup", this.input.debounceInputChange, this.input);
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
document.getElementById("input-text").addEventListener("keyup", this.input.debounceInputChange.bind(this.input));
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
this.addListeners("#clr-io,#btn-close-all-tabs", "click", this.input.clearAllIoClick, this.input);
this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, this.input);
@ -179,6 +178,7 @@ class Manager {
this.addDynamicListener(".input-filter-result", "click", this.input.filterItemClick, this.input);
document.getElementById("btn-open-file").addEventListener("click", this.input.inputOpenClick.bind(this.input));
document.getElementById("btn-open-folder").addEventListener("click", this.input.folderOpenClick.bind(this.input));
this.addDynamicListener(".eol-select a", "click", this.input.eolSelectClick, this.input);
// Output

View File

@ -0,0 +1,190 @@
/**
* A Status bar extension for CodeMirror
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2022
* @license Apache-2.0
*/
import {showPanel} from "@codemirror/view";
/**
* Counts the stats of a document
* @param {element} el
* @param {Text} doc
*/
function updateStats(el, doc) {
const length = el.querySelector("#stats-length-value"),
lines = el.querySelector("#stats-lines-value");
length.textContent = doc.length;
lines.textContent = doc.lines;
}
/**
* Gets the current selection info
* @param {element} el
* @param {EditorState} state
* @param {boolean} selectionSet
*/
function updateSelection(el, state, selectionSet) {
const selLen = state.selection && state.selection.main ?
state.selection.main.to - state.selection.main.from :
0;
const selInfo = el.querySelector("#sel-info"),
curOffsetInfo = el.querySelector("#cur-offset-info");
if (!selectionSet) {
selInfo.style.display = "none";
curOffsetInfo.style.display = "none";
return;
}
if (selLen > 0) { // Range
const start = el.querySelector("#sel-start-value"),
end = el.querySelector("#sel-end-value"),
length = el.querySelector("#sel-length-value");
selInfo.style.display = "inline-block";
curOffsetInfo.style.display = "none";
start.textContent = state.selection.main.from;
end.textContent = state.selection.main.to;
length.textContent = state.selection.main.to - state.selection.main.from;
} else { // Position
const offset = el.querySelector("#cur-offset-value");
selInfo.style.display = "none";
curOffsetInfo.style.display = "inline-block";
offset.textContent = state.selection.main.from;
}
}
/**
* Gets the current character encoding of the document
* @param {element} el
* @param {EditorState} state
*/
function updateCharEnc(el, state) {
// const charenc = el.querySelector("#char-enc-value");
// TODO
// charenc.textContent = "TODO";
}
/**
* Returns what the current EOL separator is set to
* @param {element} el
* @param {EditorState} state
*/
function updateEOL(el, state) {
const eolLookup = {
"\u000a": "LF",
"\u000b": "VT",
"\u000c": "FF",
"\u000d": "CR",
"\u000d\u000a": "CRLF",
"\u0085": "NEL",
"\u2028": "LS",
"\u2029": "PS"
};
const val = el.querySelector("#eol-value");
val.textContent = eolLookup[state.lineBreak];
}
/**
* Builds the Left-hand-side widgets
* @returns {string}
*/
function constructLHS() {
return `<span data-toggle="tooltip" title="Input length">
<i class="material-icons">abc</i>
<span id="stats-length-value"></span>
</span>
<span data-toggle="tooltip" title="Number of lines">
<i class="material-icons">sort</i>
<span id="stats-lines-value"></span>
</span>
<span id="sel-info" data-toggle="tooltip" title="Selection">
<i class="material-icons">highlight_alt</i>
<span id="sel-start-value"></span>\u279E<span id="sel-end-value"></span>
(<span id="sel-length-value"></span> selected)
</span>
<span id="cur-offset-info" data-toggle="tooltip" title="Cursor offset">
<i class="material-icons">location_on</i>
<span id="cur-offset-value"></span>
</span>`;
}
/**
* Builds the Right-hand-side widgets
* Event listener set up in Manager
* @returns {string}
*/
function constructRHS() {
return `<span data-toggle="tooltip" title="Input character encoding">
<i class="material-icons">language</i>
<span id="char-enc-value">UTF-16</span>
</span>
<div class="cm-status-bar-select eol-select">
<span class="cm-status-bar-select-btn" data-toggle="tooltip" data-placement="bottom" title="End of line sequence">
<i class="material-icons">keyboard_return</i> <span id="eol-value"></span>
</span>
<div class="cm-status-bar-select-content">
<a href="#" data-val="LF">Line Feed, U+000A</a>
<a href="#" data-val="VT">Vertical Tab, U+000B</a>
<a href="#" data-val="FF">Form Feed, U+000C</a>
<a href="#" data-val="CR">Carriage Return, U+000D</a>
<a href="#" data-val="CRLF">CR+LF, U+000D U+000A</a>
<!-- <a href="#" data-val="NL">Next Line, U+0085</a> This causes problems. -->
<a href="#" data-val="LS">Line Separator, U+2028</a>
<a href="#" data-val="PS">Paragraph Separator, U+2029</a>
</div>
</div>`;
}
/**
* A panel constructor building a panel that re-counts the stats every time the document changes.
* @param {EditorView} view
* @returns {Panel}
*/
function wordCountPanel(view) {
const dom = document.createElement("div");
const lhs = document.createElement("div");
const rhs = document.createElement("div");
dom.className = "cm-status-bar";
lhs.innerHTML = constructLHS();
rhs.innerHTML = constructRHS();
dom.appendChild(lhs);
dom.appendChild(rhs);
updateEOL(rhs, view.state);
updateCharEnc(rhs, view.state);
updateStats(lhs, view.state.doc);
updateSelection(lhs, view.state, false);
return {
dom,
update(update) {
updateEOL(rhs, update.state);
updateSelection(lhs, update.state, update.selectionSet);
updateCharEnc(rhs, update.state);
if (update.docChanged) {
updateStats(lhs, update.state.doc);
}
}
};
}
/**
* A function that build the extension that enables the panel in an editor.
* @returns {Extension}
*/
export function statusBar() {
return showPanel.of(wordCountPanel);
}

View File

@ -219,8 +219,6 @@
<label for="input-text">Input</label>
<span class="pane-controls">
<div class="io-info" id="input-files-info"></div>
<div class="io-info" id="input-selection-info"></div>
<div class="io-info" id="input-info"></div>
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab">
<i class="material-icons">add</i>
</button>
@ -267,7 +265,7 @@
</div>
<div class="textarea-wrapper no-select input-wrapper" id="input-wrapper">
<div id="input-highlighter" class="no-select"></div>
<textarea id="input-text" class="input-text" spellcheck="false" tabindex="1" autofocus></textarea>
<div id="input-text"></div>
<div class="input-file" id="input-file">
<div class="file-overlay" id="file-overlay"></div>
<div style="position: relative; height: 100%;">

Binary file not shown.

View File

@ -6,7 +6,24 @@
* @license Apache-2.0
*/
#input-text,
#input-text {
position: relative;
width: 100%;
height: 100%;
margin: 0;
background-color: transparent;
}
.cm-editor {
height: 100%;
}
.cm-editor .cm-content {
font-family: var(--fixed-width-font-family);
font-size: var(--fixed-width-font-size);
color: var(--fixed-width-font-colour);
}
#output-text,
#output-html {
position: relative;
@ -163,14 +180,14 @@
#input-wrapper,
#output-wrapper,
#input-wrapper > * ,
#input-wrapper > :not(#input-text),
#output-wrapper > .textarea-wrapper > div,
#output-wrapper > .textarea-wrapper > textarea {
height: calc(100% - var(--title-height));
}
#input-wrapper.show-tabs,
#input-wrapper.show-tabs > *,
#input-wrapper.show-tabs > :not(#input-text),
#output-wrapper.show-tabs,
#output-wrapper.show-tabs > .textarea-wrapper > div,
#output-wrapper.show-tabs > .textarea-wrapper > textarea {
@ -193,7 +210,9 @@
}
.textarea-wrapper textarea,
.textarea-wrapper>div {
.textarea-wrapper #output-text,
.textarea-wrapper #output-html,
.textarea-wrapper #output-highlighter {
font-family: var(--fixed-width-font-family);
font-size: var(--fixed-width-font-size);
color: var(--fixed-width-font-colour);
@ -292,10 +311,6 @@
align-items: center;
}
#input-info {
line-height: 15px;
}
.dropping-file {
border: 5px dashed var(--drop-file-border-colour) !important;
}
@ -458,3 +473,73 @@
cursor: pointer;
filter: brightness(98%);
}
/* Status bar */
.cm-status-bar {
font-family: var(--fixed-width-font-family);
font-weight: normal;
font-size: 8pt;
margin: 0 5px;
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
}
.cm-status-bar i {
font-size: 12pt;
vertical-align: middle;
margin-left: 8px;
}
.cm-status-bar>div>span:first-child i {
margin-left: 0;
}
/* Dropup Button */
.cm-status-bar-select-btn {
border: none;
cursor: pointer;
}
/* The container <div> - needed to position the dropup content */
.cm-status-bar-select {
position: relative;
display: inline-block;
}
/* Dropup content (Hidden by Default) */
.cm-status-bar-select-content {
display: none;
position: absolute;
bottom: 20px;
right: 0;
background-color: #f1f1f1;
min-width: 200px;
box-shadow: 0px 4px 4px 0px rgba(0,0,0,0.2);
z-index: 1;
}
/* Links inside the dropup */
.cm-status-bar-select-content a {
color: black;
padding: 2px 5px;
text-decoration: none;
display: block;
}
/* Change color of dropup links on hover */
.cm-status-bar-select-content a:hover {
background-color: #ddd
}
/* Show the dropup menu on hover */
.cm-status-bar-select:hover .cm-status-bar-select-content {
display: block;
}
/* Change the background color of the dropup button when the dropup content is shown */
.cm-status-bar-select:hover .cm-status-bar-select-btn {
background-color: #f1f1f1;
}

View File

@ -13,7 +13,7 @@
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url("../static/fonts/MaterialIcons-Regular.woff2") format('woff2');
src: url("../static/fonts/MaterialIcons-Regular.ttf") format('truetype');
}
.material-icons {

View File

@ -140,7 +140,7 @@ class ControlsWaiter {
const params = [
includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput ? ["input", Utils.escapeHtml(input)] : undefined,
includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined,
];
const hash = params

View File

@ -155,12 +155,11 @@ class HighlighterWaiter {
this.mouseTarget = INPUT;
this.removeHighlights();
const el = e.target;
const start = el.selectionStart;
const end = el.selectionEnd;
const sel = document.getSelection();
const start = sel.baseOffset;
const end = sel.extentOffset;
if (start !== 0 || end !== 0) {
document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end);
this.highlightOutput([{start: start, end: end}]);
}
}
@ -248,12 +247,11 @@ class HighlighterWaiter {
this.mouseTarget !== INPUT)
return;
const el = e.target;
const start = el.selectionStart;
const end = el.selectionEnd;
const sel = document.getSelection();
const start = sel.baseOffset;
const end = sel.extentOffset;
if (start !== 0 || end !== 0) {
document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end);
this.highlightOutput([{start: start, end: end}]);
}
}
@ -328,7 +326,6 @@ class HighlighterWaiter {
removeHighlights() {
document.getElementById("input-highlighter").innerHTML = "";
document.getElementById("output-highlighter").innerHTML = "";
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
}

View File

@ -7,9 +7,19 @@
import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js";
import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs";
import Utils, { debounce } from "../../core/Utils.mjs";
import { toBase64 } from "../../core/lib/Base64.mjs";
import { isImage } from "../../core/lib/FileType.mjs";
import Utils, {debounce} from "../../core/Utils.mjs";
import {toBase64} from "../../core/lib/Base64.mjs";
import {isImage} from "../../core/lib/FileType.mjs";
import {
EditorView, keymap, highlightSpecialChars, drawSelection, rectangularSelection, crosshairCursor
} from "@codemirror/view";
import {EditorState, Compartment} from "@codemirror/state";
import {defaultKeymap, insertTab, insertNewline, history, historyKeymap} from "@codemirror/commands";
import {bracketMatching} from "@codemirror/language";
import {search, searchKeymap, highlightSelectionMatches} from "@codemirror/search";
import {statusBar} from "../extensions/statusBar.mjs";
/**
@ -27,6 +37,9 @@ class InputWaiter {
this.app = app;
this.manager = manager;
this.inputTextEl = document.getElementById("input-text");
this.initEditor();
// Define keys that don't change the input so we don't have to autobake when they are pressed
this.badKeys = [
16, // Shift
@ -61,6 +74,135 @@ class InputWaiter {
}
}
/**
* Sets up the CodeMirror Editor and returns the view
*/
initEditor() {
this.inputEditorConf = {
eol: new Compartment,
lineWrapping: new Compartment
};
const initialState = EditorState.create({
doc: null,
extensions: [
history(),
highlightSpecialChars({render: this.renderSpecialChar}),
drawSelection(),
rectangularSelection(),
crosshairCursor(),
bracketMatching(),
highlightSelectionMatches(),
search({top: true}),
statusBar(this.inputEditorConf),
this.inputEditorConf.lineWrapping.of(EditorView.lineWrapping),
this.inputEditorConf.eol.of(EditorState.lineSeparator.of("\n")),
EditorState.allowMultipleSelections.of(true),
keymap.of([
// Explicitly insert a tab rather than indenting the line
{ key: "Tab", run: insertTab },
// Explicitly insert a new line (using the current EOL char) rather
// than messing around with indenting, which does not respect EOL chars
{ key: "Enter", run: insertNewline },
...historyKeymap,
...defaultKeymap,
...searchKeymap
]),
]
});
this.inputEditorView = new EditorView({
state: initialState,
parent: this.inputTextEl
});
}
/**
* Override for rendering special characters.
* Should mirror the toDOM function in
* https://github.com/codemirror/view/blob/main/src/special-chars.ts#L150
* But reverts the replacement of line feeds with newline control pictures.
* @param {number} code
* @param {string} desc
* @param {string} placeholder
* @returns {element}
*/
renderSpecialChar(code, desc, placeholder) {
const s = document.createElement("span");
// CodeMirror changes 0x0a to "NL" instead of "LF". We change it back.
s.textContent = code === 0x0a ? "\u240a" : placeholder;
s.title = desc;
s.setAttribute("aria-label", desc);
s.className = "cm-specialChar";
return s;
}
/**
* Handler for EOL Select clicks
* Sets the line separator
* @param {Event} e
*/
eolSelectClick(e) {
e.preventDefault();
const eolLookup = {
"LF": "\u000a",
"VT": "\u000b",
"FF": "\u000c",
"CR": "\u000d",
"CRLF": "\u000d\u000a",
"NEL": "\u0085",
"LS": "\u2028",
"PS": "\u2029"
};
const eolval = eolLookup[e.target.getAttribute("data-val")];
const oldInputVal = this.getInput();
// Update the EOL value
this.inputEditorView.dispatch({
effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolval))
});
// Reset the input so that lines are recalculated, preserving the old EOL values
this.setInput(oldInputVal);
}
/**
* Sets word wrap on the input editor
* @param {boolean} wrap
*/
setWordWrap(wrap) {
this.inputEditorView.dispatch({
effects: this.inputEditorConf.lineWrapping.reconfigure(
wrap ? EditorView.lineWrapping : []
)
});
}
/**
* Gets the value of the current input
* @returns {string}
*/
getInput() {
const doc = this.inputEditorView.state.doc;
const eol = this.inputEditorView.state.lineBreak;
return doc.sliceString(0, doc.length, eol);
}
/**
* Sets the value of the current input
* @param {string} data
*/
setInput(data) {
this.inputEditorView.dispatch({
changes: {
from: 0,
to: this.inputEditorView.state.doc.length,
insert: data
}
});
}
/**
* Calculates the maximum number of tabs to display
*/
@ -339,10 +481,8 @@ class InputWaiter {
const activeTab = this.manager.tabs.getActiveInputTab();
if (inputData.inputNum !== activeTab) return;
const inputText = document.getElementById("input-text");
if (typeof inputData.input === "string") {
inputText.value = inputData.input;
this.setInput(inputData.input);
const fileOverlay = document.getElementById("input-file"),
fileName = document.getElementById("input-file-name"),
fileSize = document.getElementById("input-file-size"),
@ -355,17 +495,11 @@ class InputWaiter {
fileType.textContent = "";
fileLoaded.textContent = "";
inputText.style.overflow = "auto";
inputText.classList.remove("blur");
inputText.scroll(0, 0);
const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ?
inputData.input.count("\n") + 1 : null;
this.setInputInfo(inputData.input.length, lines);
this.inputTextEl.classList.remove("blur");
// Set URL to current input
const inputStr = toBase64(inputData.input, "A-Za-z0-9+/");
if (inputStr.length > 0 && inputStr.length <= 68267) {
if (inputStr.length >= 0 && inputStr.length <= 68267) {
this.setUrl({
includeInput: true,
input: inputStr
@ -414,7 +548,6 @@ class InputWaiter {
fileLoaded.textContent = inputData.progress + "%";
}
this.setInputInfo(inputData.size, null);
this.displayFilePreview(inputData);
if (!silent) window.dispatchEvent(this.manager.statechange);
@ -488,12 +621,10 @@ class InputWaiter {
*/
displayFilePreview(inputData) {
const activeTab = this.manager.tabs.getActiveInputTab(),
input = inputData.input,
inputText = document.getElementById("input-text");
input = inputData.input;
if (inputData.inputNum !== activeTab) return;
inputText.style.overflow = "hidden";
inputText.classList.add("blur");
inputText.value = Utils.printable(Utils.arrayBufferToStr(input.slice(0, 4096)));
this.inputTextEl.classList.add("blur");
this.setInput(Utils.arrayBufferToStr(input.slice(0, 4096)));
this.renderFileThumb();
@ -576,7 +707,7 @@ class InputWaiter {
*/
async getInputValue(inputNum) {
return await new Promise(resolve => {
this.getInput(inputNum, false, r => {
this.getInputFromWorker(inputNum, false, r => {
resolve(r.data);
});
});
@ -590,7 +721,7 @@ class InputWaiter {
*/
async getInputObj(inputNum) {
return await new Promise(resolve => {
this.getInput(inputNum, true, r => {
this.getInputFromWorker(inputNum, true, r => {
resolve(r.data);
});
});
@ -604,7 +735,7 @@ class InputWaiter {
* @param {Function} callback - The callback to execute when the input is returned
* @returns {ArrayBuffer | string | object}
*/
getInput(inputNum, getObj, callback) {
getInputFromWorker(inputNum, getObj, callback) {
const id = this.callbackID++;
this.callbacks[id] = callback;
@ -647,29 +778,6 @@ class InputWaiter {
});
}
/**
* Displays information about the input.
*
* @param {number} length - The length of the current input string
* @param {number} lines - The number of the lines in the current input string
*/
setInputInfo(length, lines) {
let width = length.toString().length.toLocaleString();
width = width < 2 ? 2 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("input-info").innerHTML = msg;
}
/**
* Handler for input change events.
* Debounces the input so we don't call autobake too often.
@ -696,17 +804,13 @@ class InputWaiter {
// Remove highlighting from input and output panes as the offsets might be different now
this.manager.highlighter.removeHighlights();
const textArea = document.getElementById("input-text");
const value = (textArea.value !== undefined) ? textArea.value : "";
const value = this.getInput();
const activeTab = this.manager.tabs.getActiveInputTab();
this.app.progress = 0;
const lines = value.length < (this.app.options.ioDisplayThreshold * 1024) ?
(value.count("\n") + 1) : null;
this.setInputInfo(value.length, lines);
this.updateInputValue(activeTab, value);
this.manager.tabs.updateInputTabHeader(activeTab, value.replace(/[\n\r]/g, "").slice(0, 100));
this.manager.tabs.updateInputTabHeader(activeTab, value.slice(0, 100).replace(/[\n\r]/g, ""));
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
@ -714,62 +818,6 @@ class InputWaiter {
}
}
/**
* Handler for input paste events
* Checks that the size of the input is below the display limit, otherwise treats it as a file/blob
*
* @param {event} e
*/
async inputPaste(e) {
e.preventDefault();
e.stopPropagation();
const self = this;
/**
* Triggers the input file/binary data overlay
*
* @param {string} pastedData
*/
function triggerOverlay(pastedData) {
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
self.loadUIFiles([file]);
}
const pastedData = e.clipboardData.getData("Text");
const inputText = document.getElementById("input-text");
const selStart = inputText.selectionStart;
const selEnd = inputText.selectionEnd;
const startVal = inputText.value.slice(0, selStart);
const endVal = inputText.value.slice(selEnd);
const val = startVal + pastedData + endVal;
if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) {
// Data too large to display, use overlay
triggerOverlay(val);
return false;
} else if (await this.preserveCarriageReturns(val)) {
// Data contains a carriage return and the user doesn't wish to edit it, use overlay
// We check this in a separate condition to make sure it is not run unless absolutely
// necessary.
triggerOverlay(val);
return false;
} else {
// Pasting normally fires the inputChange() event before
// changing the value, so instead change it here ourselves
// and manually fire inputChange()
inputText.value = val;
inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
// Don't debounce here otherwise the keyup event for the Ctrl key will cancel an autobake
// (at least for large inputs)
this.inputChange(e, true);
}
}
/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
@ -818,7 +866,7 @@ class InputWaiter {
if (text) {
// Append the text to the current input and fire inputChange()
document.getElementById("input-text").value += text;
this.setInput(this.getInput() + text);
this.inputChange(e);
return;
}
@ -843,44 +891,6 @@ class InputWaiter {
}
}
/**
* Checks if an input contains carriage returns.
* If a CR is detected, checks if the preserve CR option has been set,
* and if not, asks the user for their preference.
*
* @param {string} input - The input to be checked
* @returns {boolean} - If true, the input contains a CR which should be
* preserved, so display an overlay so it can't be edited
*/
async preserveCarriageReturns(input) {
if (input.indexOf("\r") < 0) return false;
const optionsStr = "This behaviour can be changed in the <a href='#' onclick='document.getElementById(\"options\").click()'>Options pane</a>";
const preserveStr = `A carriage return (\\r, 0x0d) was detected in your input. To preserve it, editing has been disabled.<br>${optionsStr}`;
const dontPreserveStr = `A carriage return (\\r, 0x0d) was detected in your input. It has not been preserved.<br>${optionsStr}`;
switch (this.app.options.preserveCR) {
case "always":
this.app.alert(preserveStr, 6000);
return true;
case "never":
this.app.alert(dontPreserveStr, 6000);
return false;
}
// Only preserve for high-entropy inputs
const data = Utils.strToArrayBuffer(input);
const entropy = Utils.calculateShannonEntropy(data);
if (entropy > 6) {
this.app.alert(preserveStr, 6000);
return true;
}
this.app.alert(dontPreserveStr, 6000);
return false;
}
/**
* Load files from the UI into the inputWorker
*
@ -1080,6 +1090,9 @@ class InputWaiter {
this.manager.worker.setupChefWorker();
this.addInput(true);
this.bakeAll();
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
/**

View File

@ -53,6 +53,9 @@ class OptionsWaiter {
selects[i].selectedIndex = 0;
}
}
// Initialise options
this.setWordWrap();
}
@ -136,14 +139,13 @@ class OptionsWaiter {
* Sets or unsets word wrap on the input and output depending on the wordWrap option value.
*/
setWordWrap() {
document.getElementById("input-text").classList.remove("word-wrap");
this.manager.input.setWordWrap(this.app.options.wordWrap);
document.getElementById("output-text").classList.remove("word-wrap");
document.getElementById("output-html").classList.remove("word-wrap");
document.getElementById("input-highlighter").classList.remove("word-wrap");
document.getElementById("output-highlighter").classList.remove("word-wrap");
if (!this.app.options.wordWrap) {
document.getElementById("input-text").classList.add("word-wrap");
document.getElementById("output-text").classList.add("word-wrap");
document.getElementById("output-html").classList.add("word-wrap");
document.getElementById("input-highlighter").classList.add("word-wrap");

View File

@ -1019,7 +1019,6 @@ class OutputWaiter {
}
document.getElementById("output-info").innerHTML = msg;
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
}
@ -1292,9 +1291,7 @@ class OutputWaiter {
if (this.outputs[activeTab].data.type === "string" &&
active.byteLength <= this.app.options.ioDisplayThreshold * 1024) {
const dishString = await this.getDishStr(this.getOutputDish(activeTab));
if (!await this.manager.input.preserveCarriageReturns(dishString)) {
active = dishString;
}
active = dishString;
} else {
transferable.push(active);
}

View File

@ -82,7 +82,7 @@ module.exports = {
// Enter input
browser
.useCss()
.setValue("#input-text", "Don't Panic.")
.setValue("#input-text", "Don't Panic.") // TODO
.pause(1000)
.click("#bake");

View File

@ -409,16 +409,16 @@ function bakeOp(browser, opName, input, args=[]) {
.click("#clr-recipe")
.click("#clr-io")
.waitForElementNotPresent("#rec-list li.operation")
.expect.element("#input-text").to.have.property("value").that.equals("");
.expect.element("#input-text").to.have.property("value").that.equals(""); // TODO
browser
.perform(function() {
console.log(`Current test: ${opName}`);
})
.urlHash("recipe=" + recipeConfig)
.setValue("#input-text", input)
.setValue("#input-text", input) // TODO
.waitForElementPresent("#rec-list li.operation")
.expect.element("#input-text").to.have.property("value").that.equals(input);
.expect.element("#input-text").to.have.property("value").that.equals(input); // TODO
browser
.waitForElementVisible("#stale-indicator", 5000)