diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 479a173a7..5229e4a11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,3 +23,17 @@ jobs: - name: Run spell check run: cspell + + quality: + name: Linting and formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + + - name: Run Biome + run: biome ci . diff --git a/biome.json b/biome.json new file mode 100644 index 000000000..b991ec0f2 --- /dev/null +++ b/biome.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noStaticOnlyClass": "error", + "noUselessSwitchCase": "error", + "useFlatMap": "error" + }, + "style": { + "noNegationElse": "off", + "useForOf": "error", + "useNodejsImportProtocol": "error", + "useNumberNamespace": "error" + }, + "suspicious": { + "noDoubleEquals": "error", + "noThenProperty": "error", + "useIsArray": "error" + } + } + }, + "files": { + "include": ["src/**/*", "utils/**/*.js", "www/**/*.js", "www/res/**/*.css"], + "ignore": [ + "www/js/**/*.js", + "www/css/**/*.css", + "src/plugins/**/*", + "plugins/**/*", + "hooks/**/*", + "fastlane/**/*", + "res/**/*", + "platforms/**/*" + ] + } +} diff --git a/package.json b/package.json index c67631470..f5ad13929 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "clean": "sh utils/scripts/clean.sh android android", "plugin": "sh utils/scripts/plugin.sh", "setup": "node ./utils/setup.js", - "spellcheck": "npx cspell" + "spellcheck": "npx cspell", + "lint": "biome lint --write", + "format": "biome format --write", + "check": "biome check --write" }, "keywords": [ "ecosystem:cordova" @@ -50,6 +53,7 @@ "@babel/preset-env": "^7.24.6", "@babel/runtime": "^7.24.6", "@babel/runtime-corejs3": "^7.24.6", + "@biomejs/biome": "1.8.3", "@types/ace": "^0.0.52", "@types/url-parse": "^1.4.11", "autoprefixer": "^10.4.19", @@ -102,4 +106,4 @@ "yargs": "^17.7.2" }, "browserslist": "cover 100%,not android < 5" -} \ No newline at end of file +} diff --git a/readme.md b/readme.md index 2bb19fdca..75ef8d45b 100644 --- a/readme.md +++ b/readme.md @@ -63,6 +63,27 @@ yarn setup yarn build ``` +## • Contributing + +Acode Editor is an open-source project, and we welcome contributions from the community. To contribute, follow these steps: + +1. Fork the repository. +2. Make your changes and commit them. +3. Push your changes to your fork. +4. Create a pull request and Wait for review. + +Please ensure that your code is clean, well-formatted, and follows the project's coding standards. Acode uses [Biomejs](https://biomejs.dev/) for formatting and linting. You can use following commands to lints/format your code locally: +```shell +yarn lint # for linting +yarn format # for formatting +yarn spellcheck # for spellchecking +``` +Also, ensure that your code is well-documented and includes comments where necessary. + +> [!Note] +> You can use any package manager like npm or yarn or pnpm or bun. +> You can use your editor specific Biomejs plugin for auto-formatting and linting based on Acode's configs. + ## • Developing a Plugin for Acode For comprehensive documentation on creating plugins for Acode Editor, visit the [repository](https://github.com/deadlyjack/acode-plugin). diff --git a/src/ace/colorView.js b/src/ace/colorView.js index ef8f0380f..ad329f92c 100644 --- a/src/ace/colorView.js +++ b/src/ace/colorView.js @@ -3,10 +3,10 @@ * @see https://github.com/easylogic/ace-colorpicker/blob/main/src/extension/ace/colorview.js */ -import Color from 'utils/color'; -import { HEX, HSL, HSLA, RGB, RGBA, isValidColor } from 'utils/color/regex'; +import Color from "utils/color"; +import { HEX, HSL, HSLA, RGB, RGBA, isValidColor } from "utils/color/regex"; -const COLORPICKER_TOKEN_CLASS = '.ace_color'; +const COLORPICKER_TOKEN_CLASS = ".ace_color"; const changedRules = []; let editor = null; @@ -17,139 +17,140 @@ let editor = null; * @param {boolean} [force=false] Force update color view */ export default function initColorView(e, force = false) { - editor = e; - const { renderer } = editor; + editor = e; + const { renderer } = editor; - editor.on('changeMode', onChangeMode); - renderer.on('afterRender', afterRender); + editor.on("changeMode", onChangeMode); + renderer.on("afterRender", afterRender); - if (force) { - const { files } = editorManager; + if (force) { + const { files } = editorManager; - if (Array.isArray(files)) { - files.forEach(file => { - if (file.session) { - file.session._addedColorRule = false; - } - }); - } + if (Array.isArray(files)) { + files.forEach((file) => { + if (file.session) { + file.session._addedColorRule = false; + } + }); + } - onChangeMode(); - } + onChangeMode(); + } } export function deactivateColorView() { - const { renderer } = editor; + const { renderer } = editor; - changedRules.forEach((rule) => rule.shift()); - changedRules.length = 0; - forceTokenizer(); + changedRules.forEach((rule) => rule.shift()); + changedRules.length = 0; + forceTokenizer(); - editor.off('changeMode', onChangeMode); - renderer.off('afterRender', afterRender); + editor.off("changeMode", onChangeMode); + renderer.off("afterRender", afterRender); } /** * Checks if the session supports color - * @param {AceAjax.IEditSession} session - * @returns + * @param {AceAjax.IEditSession} session + * @returns */ function sessionSupportsColor(session) { - const mode = session.getMode().$id.split('/').pop(); - return /css|less|scss|sass|stylus|html|dart/.test(mode) - ? mode - : false; + const mode = session.getMode().$id.split("/").pop(); + return /css|less|scss|sass|stylus|html|dart/.test(mode) ? mode : false; } function onChangeMode() { - const session = editor.session; - let forceUpdate = false; - - // if mode is not css, scss, sass, less, stylus, or html, return - const mode = sessionSupportsColor(session); - if (session._addedColorRule || !mode) { - return; - } - - let rules = session.$mode.$highlightRules.getRules(); - - if (mode === 'css') { - rules = { 'ruleset': rules['ruleset'] }; - } else if (mode === 'html') { - rules = { 'css-ruleset': rules['css-ruleset'] }; - } - - Object.keys(rules).forEach((key) => { - const rule = rules[key]; - if (rule instanceof Array) { - const ruleExists = rule.some((r) => r.token === 'color'); - if (ruleExists) return; - forceUpdate = true; - rule.unshift({ - token: "color", - regex: `${HEX}|${RGB}|${RGBA}|${HSL}|${HSLA}`, - }); - changedRules.push(rule); - return; - } - }); - - if (!forceUpdate) return; - - forceTokenizer(); + const session = editor.session; + let forceUpdate = false; + + // if mode is not css, scss, sass, less, stylus, or html, return + const mode = sessionSupportsColor(session); + if (session._addedColorRule || !mode) { + return; + } + + let rules = session.$mode.$highlightRules.getRules(); + + if (mode === "css") { + rules = { ruleset: rules["ruleset"] }; + } else if (mode === "html") { + rules = { "css-ruleset": rules["css-ruleset"] }; + } + + Object.keys(rules).forEach((key) => { + const rule = rules[key]; + if (Array.isArray(rule)) { + const ruleExists = rule.some((r) => r.token === "color"); + if (ruleExists) return; + forceUpdate = true; + rule.unshift({ + token: "color", + regex: `${HEX}|${RGB}|${RGBA}|${HSL}|${HSLA}`, + }); + changedRules.push(rule); + return; + } + }); + + if (!forceUpdate) return; + + forceTokenizer(); } function afterRender() { - const { session, renderer } = editor; - const { content } = renderer; - let classes = COLORPICKER_TOKEN_CLASS; - - // if session is css, scss, less, sass, stylus, or html (with css mode), continue - - const mode = sessionSupportsColor(session); - if (!mode) { - return; - } - - if (mode === 'scss') { - classes += ',.ace_function'; - } - - content.getAll(COLORPICKER_TOKEN_CLASS).forEach(( /**@type {HTMLElement} */ el, i, els) => { - let content = el.textContent; - const previousContent = els[i - 1]?.textContent; - const nextContent = els[i + 1]?.textContent; - const multiLinePrev = previousContent + content; - const multiLineNext = content + nextContent; - - if (el.dataset.modified === 'true') return; - el.dataset.modified = 'true'; - - if (!isValidColor(content)) { - if (isValidColor(multiLinePrev)) { - content = multiLinePrev; - } else if (isValidColor(multiLineNext)) { - content = multiLineNext; - } else { - return; - } - } - - try { - const fontColorString = Color(content).luminance > 0.5 ? "#000" : "#fff"; - el.classList.add('ace_color'); - el.style.cssText = `background-color: ${content}; color: ${fontColorString}; pointer-events: all;`; - } catch (error) { - console.log("Invalid color", content); - } - }); + const { session, renderer } = editor; + const { content } = renderer; + let classes = COLORPICKER_TOKEN_CLASS; + + // if session is css, scss, less, sass, stylus, or html (with css mode), continue + + const mode = sessionSupportsColor(session); + if (!mode) { + return; + } + + if (mode === "scss") { + classes += ",.ace_function"; + } + + content + .getAll(COLORPICKER_TOKEN_CLASS) + .forEach((/**@type {HTMLElement} */ el, i, els) => { + let content = el.textContent; + const previousContent = els[i - 1]?.textContent; + const nextContent = els[i + 1]?.textContent; + const multiLinePrev = previousContent + content; + const multiLineNext = content + nextContent; + + if (el.dataset.modified === "true") return; + el.dataset.modified = "true"; + + if (!isValidColor(content)) { + if (isValidColor(multiLinePrev)) { + content = multiLinePrev; + } else if (isValidColor(multiLineNext)) { + content = multiLineNext; + } else { + return; + } + } + + try { + const fontColorString = + Color(content).luminance > 0.5 ? "#000" : "#fff"; + el.classList.add("ace_color"); + el.style.cssText = `background-color: ${content}; color: ${fontColorString}; pointer-events: all;`; + } catch (error) { + console.log("Invalid color", content); + } + }); } function forceTokenizer() { - const { session } = editor; - // force recreation of tokenizer - session.$mode.$tokenizer = null; - session.bgTokenizer.setTokenizer(session.$mode.getTokenizer()); - // force re-highlight whole document - session.bgTokenizer.start(0); + const { session } = editor; + // force recreation of tokenizer + session.$mode.$tokenizer = null; + session.bgTokenizer.setTokenizer(session.$mode.getTokenizer()); + // force re-highlight whole document + session.bgTokenizer.start(0); } diff --git a/src/ace/commands.js b/src/ace/commands.js index cd23ad7db..544800dbd 100644 --- a/src/ace/commands.js +++ b/src/ace/commands.js @@ -1,270 +1,270 @@ -import fsOperation from 'fileSystem'; -import actions from 'handlers/quickTools'; -import keyBindings from 'lib/keyBindings'; -import Url from 'utils/Url'; +import fsOperation from "fileSystem"; +import actions from "handlers/quickTools"; +import keyBindings from "lib/keyBindings"; +import Url from "utils/Url"; const commands = [ - { - name: 'focusEditor', - description: 'Focus editor', - exec() { - editorManager.editor.focus(); - } - }, - { - name: 'findFile', - description: 'Find file in workspace', - exec() { - acode.exec('find-file'); - }, - }, - { - name: 'closeCurrentTab', - description: 'Close current tab', - exec() { - acode.exec('close-current-tab'); - }, - }, - { - name: 'closeAllTabs', - description: 'Close all tabs', - exec() { - acode.exec('close-all-tabs'); - }, - }, - { - name: 'newFile', - description: 'Create new file', - exec() { - acode.exec('new-file'); - }, - readOnly: true, - }, - { - name: 'openFile', - description: 'Open a file', - exec() { - acode.exec('open-file'); - }, - readOnly: true, - }, - { - name: 'openFolder', - description: 'Open a folder', - exec() { - acode.exec('open-folder'); - }, - readOnly: true, - }, - { - name: 'saveFile', - description: 'Save current file', - exec() { - acode.exec('save'); - }, - readOnly: true, - }, - { - name: 'saveFileAs', - description: 'Save as current file', - exec() { - acode.exec('save-as'); - }, - readOnly: true, - }, - { - name: 'saveAllChanges', - description: 'Save all changes', - exec() { - acode.exec('save-all-changes'); - }, - readOnly: true, - }, - { - name: 'nextFile', - description: 'Open next file tab', - exec() { - acode.exec('next-file'); - }, - }, - { - name: 'prevFile', - description: 'Open previous file tab', - exec() { - acode.exec('prev-file'); - }, - }, - { - name: 'showSettingsMenu', - description: 'Show settings menu', - exec() { - acode.exec('open', 'settings'); - }, - readOnly: true, - }, - { - name: 'renameFile', - description: 'Rename active file', - exec() { - acode.exec('rename'); - }, - readOnly: true, - }, - { - name: 'run', - description: 'Preview HTML and MarkDown', - exec() { - acode.exec('run'); - }, - readOnly: true, - }, - { - name: 'toggleFullscreen', - description: 'Toggle full screen mode', - exec() { - acode.exec('toggle-fullscreen'); - }, - }, - { - name: 'toggleSidebar', - description: 'Toggle sidebar', - exec() { - acode.exec('toggle-sidebar'); - }, - }, - { - name: 'toggleMenu', - description: 'Toggle main menu', - exec() { - acode.exec('toggle-menu'); - }, - }, - { - name: 'toggleEditMenu', - description: 'Toggle edit menu', - exec() { - acode.exec('toggle-editmenu'); - }, - }, - { - name: 'selectall', - description: 'Select all', - exec(editor) { - editor.selectAll(); - }, - readOnly: true, - }, - { - name: 'gotoline', - description: 'Go to line...', - exec() { - acode.exec('goto'); - }, - readOnly: true, - }, - { - name: 'find', - description: 'Find', - exec() { - acode.exec('find'); - }, - readOnly: true, - }, - { - name: 'copy', - description: 'Copy', - exec(editor) { - const { clipboard } = cordova.plugins; - const copyText = editor.getCopyText(); - clipboard.copy(copyText); - toast(strings['copied to clipboard']); - }, - readOnly: true, - }, - { - name: 'cut', - description: 'Cut', - exec(editor) { - let cutLine = - editor.$copyWithEmptySelection && editor.selection.isEmpty(); - let range = cutLine - ? editor.selection.getLineRange() - : editor.selection.getRange(); - editor._emit('cut', range); - if (!range.isEmpty()) { - const { clipboard } = cordova.plugins; - const copyText = editor.session.getTextRange(range); - clipboard.copy(copyText); - toast(strings['copied to clipboard']); - editor.session.remove(range); - } - editor.clearSelection(); - }, - scrollIntoView: 'cursor', - multiSelectAction: 'forEach', - }, - { - name: 'paste', - description: 'Paste', - exec() { - const { clipboard } = cordova.plugins; - clipboard.paste((text) => { - editorManager.editor.$handlePaste(text); - }); - }, - scrollIntoView: 'cursor', - }, - { - name: 'problems', - description: 'Show errors and warnings', - exec() { - acode.exec('open', 'problems'); - }, - }, - { - name: 'replace', - description: 'Replace', - exec() { - acode.exec('replace'); - }, - }, - { - name: 'openCommandPalette', - description: 'Open command palette', - exec() { - acode.exec('command-palette'); - }, - readOnly: true, - }, - { - name: 'modeSelect', - description: 'Change language mode...', - exec() { - acode.exec('syntax'); - }, - readOnly: true, - }, - { - name: 'toggleQuickTools', - description: 'Toggle quick tools', - exec() { - actions('toggle'); - }, - }, - { - name: 'selectWord', - description: 'Select current word', - exec(editor) { - editor.selection.selectAWord(); - editor._emit('select-word'); - } - } + { + name: "focusEditor", + description: "Focus editor", + exec() { + editorManager.editor.focus(); + }, + }, + { + name: "findFile", + description: "Find file in workspace", + exec() { + acode.exec("find-file"); + }, + }, + { + name: "closeCurrentTab", + description: "Close current tab", + exec() { + acode.exec("close-current-tab"); + }, + }, + { + name: "closeAllTabs", + description: "Close all tabs", + exec() { + acode.exec("close-all-tabs"); + }, + }, + { + name: "newFile", + description: "Create new file", + exec() { + acode.exec("new-file"); + }, + readOnly: true, + }, + { + name: "openFile", + description: "Open a file", + exec() { + acode.exec("open-file"); + }, + readOnly: true, + }, + { + name: "openFolder", + description: "Open a folder", + exec() { + acode.exec("open-folder"); + }, + readOnly: true, + }, + { + name: "saveFile", + description: "Save current file", + exec() { + acode.exec("save"); + }, + readOnly: true, + }, + { + name: "saveFileAs", + description: "Save as current file", + exec() { + acode.exec("save-as"); + }, + readOnly: true, + }, + { + name: "saveAllChanges", + description: "Save all changes", + exec() { + acode.exec("save-all-changes"); + }, + readOnly: true, + }, + { + name: "nextFile", + description: "Open next file tab", + exec() { + acode.exec("next-file"); + }, + }, + { + name: "prevFile", + description: "Open previous file tab", + exec() { + acode.exec("prev-file"); + }, + }, + { + name: "showSettingsMenu", + description: "Show settings menu", + exec() { + acode.exec("open", "settings"); + }, + readOnly: true, + }, + { + name: "renameFile", + description: "Rename active file", + exec() { + acode.exec("rename"); + }, + readOnly: true, + }, + { + name: "run", + description: "Preview HTML and MarkDown", + exec() { + acode.exec("run"); + }, + readOnly: true, + }, + { + name: "toggleFullscreen", + description: "Toggle full screen mode", + exec() { + acode.exec("toggle-fullscreen"); + }, + }, + { + name: "toggleSidebar", + description: "Toggle sidebar", + exec() { + acode.exec("toggle-sidebar"); + }, + }, + { + name: "toggleMenu", + description: "Toggle main menu", + exec() { + acode.exec("toggle-menu"); + }, + }, + { + name: "toggleEditMenu", + description: "Toggle edit menu", + exec() { + acode.exec("toggle-editmenu"); + }, + }, + { + name: "selectall", + description: "Select all", + exec(editor) { + editor.selectAll(); + }, + readOnly: true, + }, + { + name: "gotoline", + description: "Go to line...", + exec() { + acode.exec("goto"); + }, + readOnly: true, + }, + { + name: "find", + description: "Find", + exec() { + acode.exec("find"); + }, + readOnly: true, + }, + { + name: "copy", + description: "Copy", + exec(editor) { + const { clipboard } = cordova.plugins; + const copyText = editor.getCopyText(); + clipboard.copy(copyText); + toast(strings["copied to clipboard"]); + }, + readOnly: true, + }, + { + name: "cut", + description: "Cut", + exec(editor) { + let cutLine = + editor.$copyWithEmptySelection && editor.selection.isEmpty(); + let range = cutLine + ? editor.selection.getLineRange() + : editor.selection.getRange(); + editor._emit("cut", range); + if (!range.isEmpty()) { + const { clipboard } = cordova.plugins; + const copyText = editor.session.getTextRange(range); + clipboard.copy(copyText); + toast(strings["copied to clipboard"]); + editor.session.remove(range); + } + editor.clearSelection(); + }, + scrollIntoView: "cursor", + multiSelectAction: "forEach", + }, + { + name: "paste", + description: "Paste", + exec() { + const { clipboard } = cordova.plugins; + clipboard.paste((text) => { + editorManager.editor.$handlePaste(text); + }); + }, + scrollIntoView: "cursor", + }, + { + name: "problems", + description: "Show errors and warnings", + exec() { + acode.exec("open", "problems"); + }, + }, + { + name: "replace", + description: "Replace", + exec() { + acode.exec("replace"); + }, + }, + { + name: "openCommandPalette", + description: "Open command palette", + exec() { + acode.exec("command-palette"); + }, + readOnly: true, + }, + { + name: "modeSelect", + description: "Change language mode...", + exec() { + acode.exec("syntax"); + }, + readOnly: true, + }, + { + name: "toggleQuickTools", + description: "Toggle quick tools", + exec() { + actions("toggle"); + }, + }, + { + name: "selectWord", + description: "Select current word", + exec(editor) { + editor.selection.selectAWord(); + editor._emit("select-word"); + }, + }, ]; export function setCommands(editor) { - commands.forEach((command) => { - editor.commands.addCommand(command); - }); + commands.forEach((command) => { + editor.commands.addCommand(command); + }); } /** @@ -272,49 +272,48 @@ export function setCommands(editor) { * @param {AceAjax.Editor} editor Ace editor */ export async function setKeyBindings({ commands }) { - let keyboardShortcuts = keyBindings; - try { - const bindingsFile = fsOperation(KEYBINDING_FILE); - if (await bindingsFile.exists()) { - const bindings = await bindingsFile.readFile('json'); - // keyboardShortcuts = compareAndFixKeyBindings(keyboardShortcuts, bindings); - keyboardShortcuts = bindings; - } else { - throw new Error('Key binding file not found'); - } - } catch (error) { - await resetKeyBindings(); - } + let keyboardShortcuts = keyBindings; + try { + const bindingsFile = fsOperation(KEYBINDING_FILE); + if (await bindingsFile.exists()) { + const bindings = await bindingsFile.readFile("json"); + // keyboardShortcuts = compareAndFixKeyBindings(keyboardShortcuts, bindings); + keyboardShortcuts = bindings; + } else { + throw new Error("Key binding file not found"); + } + } catch (error) { + await resetKeyBindings(); + } - Object.keys(commands.byName).forEach((name) => { - const shortcut = keyboardShortcuts[name]; - const command = commands.byName[name]; + Object.keys(commands.byName).forEach((name) => { + const shortcut = keyboardShortcuts[name]; + const command = commands.byName[name]; - if (shortcut?.description) { - command.description = shortcut.description; - } + if (shortcut?.description) { + command.description = shortcut.description; + } - // not chekiang if shortcut is empty because it can be used to remove shortcut - command.bindKey = { win: shortcut?.key ?? null }; - commands.addCommand(command); - }); + // not chekiang if shortcut is empty because it can be used to remove shortcut + command.bindKey = { win: shortcut?.key ?? null }; + commands.addCommand(command); + }); } /** * Resets key binding */ export async function resetKeyBindings() { - try { - const fs = fsOperation(KEYBINDING_FILE); - const fileName = Url.basename(KEYBINDING_FILE); - const content = JSON.stringify(keyBindings, undefined, 2); - if (!(await fs.exists())) { - await fsOperation(DATA_STORAGE) - .createFile(fileName, content); - return; - } - await fs.writeFile(content); - } catch (error) { - console.error(error); - } + try { + const fs = fsOperation(KEYBINDING_FILE); + const fileName = Url.basename(KEYBINDING_FILE); + const content = JSON.stringify(keyBindings, undefined, 2); + if (!(await fs.exists())) { + await fsOperation(DATA_STORAGE).createFile(fileName, content); + return; + } + await fs.writeFile(content); + } catch (error) { + console.error(error); + } } diff --git a/src/ace/modelist.js b/src/ace/modelist.js index 4e335db74..c6f70d8ca 100644 --- a/src/ace/modelist.js +++ b/src/ace/modelist.js @@ -2,92 +2,96 @@ const modesByName = {}; const modes = []; export function initModes() { - ace.define("ace/ext/modelist", ["require", "exports", "module"], function (require, exports, module) { - module.exports = { - getModeForPath(path) { - let mode = modesByName.text; - let fileName = path.split(/[\/\\]/).pop(); - for (let i = 0; i < modes.length; i++) { - const iMode = modes[i]; - if (iMode.supportsFile?.(fileName)) { - mode = iMode; - break; - } - } - return mode; - }, - get modesByName() { - return modesByName; - }, - get modes() { - return modes; - }, - }; - }); + ace.define( + "ace/ext/modelist", + ["require", "exports", "module"], + function (require, exports, module) { + module.exports = { + getModeForPath(path) { + let mode = modesByName.text; + let fileName = path.split(/[\/\\]/).pop(); + for (const iMode of modes) { + if (iMode.supportsFile?.(fileName)) { + mode = iMode; + break; + } + } + return mode; + }, + get modesByName() { + return modesByName; + }, + get modes() { + return modes; + }, + }; + }, + ); } /** * Add language mode to ace editor * @param {string} name name of the mode - * @param {string|Array} extensions extensions of the mode + * @param {string|Array} extensions extensions of the mode * @param {string} [caption] display name of the mode */ export function addMode(name, extensions, caption) { - const filename = name.toLowerCase(); - const mode = new Mode(filename, caption, extensions); - modesByName[filename] = mode; - modes.push(mode); + const filename = name.toLowerCase(); + const mode = new Mode(filename, caption, extensions); + modesByName[filename] = mode; + modes.push(mode); } /** * Remove language mode from ace editor - * @param {string} name + * @param {string} name */ export function removeMode(name) { - const filename = name.toLowerCase(); - delete modesByName[filename]; - const modeIndex = modes.findIndex(mode => mode.name === filename); - if (modeIndex >= 0) { - modes.splice(modeIndex, 1); - } + const filename = name.toLowerCase(); + delete modesByName[filename]; + const modeIndex = modes.findIndex((mode) => mode.name === filename); + if (modeIndex >= 0) { + modes.splice(modeIndex, 1); + } } class Mode { - extensions; - displayName; - name; - mode; - extRe; + extensions; + displayName; + name; + mode; + extRe; - /** - * Create a new mode - * @param {string} name - * @param {string} caption - * @param {string|Array} extensions - */ - constructor(name, caption, extensions) { - if (Array.isArray(extensions)) { - extensions = extensions.join('|'); - } + /** + * Create a new mode + * @param {string} name + * @param {string} caption + * @param {string|Array} extensions + */ + constructor(name, caption, extensions) { + if (Array.isArray(extensions)) { + extensions = extensions.join("|"); + } - this.name = name; - this.mode = "ace/mode/" + name; - this.extensions = extensions; - this.caption = caption || this.name.replace(/_/g, " "); - let re; + this.name = name; + this.mode = "ace/mode/" + name; + this.extensions = extensions; + this.caption = caption || this.name.replace(/_/g, " "); + let re; - if (/\^/.test(extensions)) { - re = extensions.replace(/\|(\^)?/g, function (a, b) { - return "$|" + (b ? "^" : "^.*\\."); - }) + "$"; - } else { - re = "^.*\\.(" + extensions + ")$"; - } + if (/\^/.test(extensions)) { + re = + extensions.replace(/\|(\^)?/g, function (a, b) { + return "$|" + (b ? "^" : "^.*\\."); + }) + "$"; + } else { + re = "^.*\\.(" + extensions + ")$"; + } - this.extRe = new RegExp(re, "i"); - } + this.extRe = new RegExp(re, "i"); + } - supportsFile(filename) { - return this.extRe.test(filename); - } + supportsFile(filename) { + return this.extRe.test(filename); + } } diff --git a/src/ace/supportedModes.js b/src/ace/supportedModes.js index 9c382a09b..7a358916e 100644 --- a/src/ace/supportedModes.js +++ b/src/ace/supportedModes.js @@ -1,216 +1,216 @@ import { addMode } from "./modelist"; const modeList = { - ABAP: "abap", - ABC: "abc", - ActionScript: "as", - ADA: "ada|adb", - Alda: "alda", - Apache_Conf: "^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd", - Apex: "apex|cls|trigger|tgr", - AQL: "aql", - AsciiDoc: "asciidoc|adoc", - ASL: "dsl|asl|asl.json", - Assembly_x86: "asm|a", - Assembly_arm32: "s", - Astro: "astro", - AutoHotKey: "ahk", - BatchFile: "bat|cmd", - BibTeX: "bib", - C_Cpp: "cpp|c|cc|cxx|h|hh|hpp|ino", - C9Search: "c9search_results", - Cirru: "cirru|cr", - Clojure: "clj|cljs", - Cobol: "CBL|COB", - coffee: "coffee|cf|cson|^Cakefile", - ColdFusion: "cfm|cfc", - Crystal: "cr", - CSharp: "cs", - Csound_Document: "csd", - Csound_Orchestra: "orc", - Csound_Score: "sco", - CSS: "css", - Curly: "curly", - Cuttlefish: "conf", - D: "d|di", - Dart: "dart", - Diff: "diff|patch", - Dockerfile: "^Dockerfile", - Dot: "dot", - Drools: "drl", - Edifact: "edi", - Eiffel: "e|ge", - EJS: "ejs", - Elixir: "ex|exs", - Elm: "elm", - Erlang: "erl|hrl", - Forth: "frt|fs|ldr|fth|4th", - Fortran: "f|f90", - FSharp: "fsi|fs|ml|mli|fsx|fsscript", - FSL: "fsl", - FTL: "ftl", - Flix: "flix", - Gcode: "gcode", - Gherkin: "feature", - Gitignore: "^.gitignore", - Glsl: "glsl|frag|vert", - Gobstones: "gbs", - golang: "go", - GraphQLSchema: "gql", - Groovy: "groovy", - HAML: "haml", - Handlebars: "hbs|handlebars|tpl|mustache", - Haskell: "hs", - Haskell_Cabal: "cabal", - haXe: "hx", - Hjson: "hjson", - HTML: "html|htm|xhtml|we|wpy", - HTML_Elixir: "eex|html.eex", - HTML_Ruby: "erb|rhtml|html.erb", - INI: "ini|conf|cfg|prefs", - Io: "io", - Ion: "ion", - Jack: "jack", - Jade: "jade|pug", - Java: "java", - JavaScript: "js|jsm|jsx|cjs|mjs", - JEXL: "jexl", - JSON: "json", - JSON5: "json5", - JSONiq: "jq", - JSP: "jsp", - JSSM: "jssm|jssm_state", - JSX: "jsx", - Julia: "jl", - Kotlin: "kt|kts", - LaTeX: "tex|latex|ltx|bib", - Latte: "latte", - LESS: "less", - Liquid: "liquid", - Lisp: "lisp", - LiveScript: "ls", - Log: "log", - LogiQL: "logic|lql", - Logtalk: "lgt", - LSL: "lsl", - Lua: "lua", - LuaPage: "lp", - Lucene: "lucene", - Makefile: "^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make", - Markdown: "md|markdown", - Mask: "mask", - MATLAB: "matlab", - Maze: "mz", - MediaWiki: "wiki|mediawiki", - MEL: "mel", - MIPS: "s|asm", - MIXAL: "mixal", - MUSHCode: "mc|mush", - MySQL: "mysql", - Nasal: "nas", - Nginx: "nginx|conf", - Nim: "nim", - Nix: "nix", - NSIS: "nsi|nsh", - Nunjucks: "nunjucks|nunjs|nj|njk", - ObjectiveC: "m|mm", - OCaml: "ml|mli", - Odin: "odin", - PartiQL: "partiql|pql", - Pascal: "pas|p", - Perl: "pl|pm", - pgSQL: "pgsql", - PHP: "php|inc|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module", - PHP_Laravel_blade: "blade.php", - Pig: "pig", - PLSQL: "plsql", - Powershell: "ps1", - Praat: "praat|praatscript|psc|proc", - Prisma: "prisma", - Prolog: "plg|prolog", - Properties: "properties", - Protobuf: "proto", - Puppet: "epp|pp", - Python: "py", - PRQL: "prql", - QML: "qml", - R: "r", - Raku: "raku|rakumod|rakutest|p6|pl6|pm6", - Razor: "cshtml|asp", - RDoc: "Rd", - Red: "red|reds", - RHTML: "Rhtml", - Robot: "robot|resource", - RST: "rst", - Ruby: "rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile", - Rust: "rs", - SaC: "sac", - SASS: "sass", - SCAD: "scad", - Scala: "scala|sbt", - Scheme: "scm|sm|rkt|oak|scheme", - Scrypt: "scrypt", - SCSS: "scss", - SH: "sh|bash|^.bashrc", - SJS: "sjs", - Slim: "slim|skim", - Smarty: "smarty|tpl", - Smithy: "smithy", - snippets: "snippets", - Soy_Template: "soy", - Space: "space", - SPARQL: "rq", - SQL: "sql", - SQLServer: "sqlserver", - Stylus: "styl|stylus", - SVG: "svg", - Swift: "swift", - Tcl: "tcl", - Terraform: "tf|tfvars|terragrunt", - Tex: "tex", - Text: "txt", - Textile: "textile", - Toml: "toml", - TSX: "tsx", - Turtle: "ttl", - Twig: "twig|swig", - Typescript: "ts|typescript|str", - Vala: "vala", - VBScript: "vbs|vb", - Velocity: "vm", - Verilog: "v|vh|sv|svh", - VHDL: "vhd|vhdl", - Visualforce: "vfp|component|page", - Vue: "vue", - Wollok: "wlk|wpgm|wtest", - XML: "xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml", - XQuery: "xq", - YAML: "yaml|yml", - Zeek: "zeek|bro", - Zig: "zig", - Django: "html", + ABAP: "abap", + ABC: "abc", + ActionScript: "as", + ADA: "ada|adb", + Alda: "alda", + Apache_Conf: "^htaccess|^htgroups|^htpasswd|^conf|htaccess|htgroups|htpasswd", + Apex: "apex|cls|trigger|tgr", + AQL: "aql", + AsciiDoc: "asciidoc|adoc", + ASL: "dsl|asl|asl.json", + Assembly_x86: "asm|a", + Assembly_arm32: "s", + Astro: "astro", + AutoHotKey: "ahk", + BatchFile: "bat|cmd", + BibTeX: "bib", + C_Cpp: "cpp|c|cc|cxx|h|hh|hpp|ino", + C9Search: "c9search_results", + Cirru: "cirru|cr", + Clojure: "clj|cljs", + Cobol: "CBL|COB", + coffee: "coffee|cf|cson|^Cakefile", + ColdFusion: "cfm|cfc", + Crystal: "cr", + CSharp: "cs", + Csound_Document: "csd", + Csound_Orchestra: "orc", + Csound_Score: "sco", + CSS: "css", + Curly: "curly", + Cuttlefish: "conf", + D: "d|di", + Dart: "dart", + Diff: "diff|patch", + Dockerfile: "^Dockerfile", + Dot: "dot", + Drools: "drl", + Edifact: "edi", + Eiffel: "e|ge", + EJS: "ejs", + Elixir: "ex|exs", + Elm: "elm", + Erlang: "erl|hrl", + Forth: "frt|fs|ldr|fth|4th", + Fortran: "f|f90", + FSharp: "fsi|fs|ml|mli|fsx|fsscript", + FSL: "fsl", + FTL: "ftl", + Flix: "flix", + Gcode: "gcode", + Gherkin: "feature", + Gitignore: "^.gitignore", + Glsl: "glsl|frag|vert", + Gobstones: "gbs", + golang: "go", + GraphQLSchema: "gql", + Groovy: "groovy", + HAML: "haml", + Handlebars: "hbs|handlebars|tpl|mustache", + Haskell: "hs", + Haskell_Cabal: "cabal", + haXe: "hx", + Hjson: "hjson", + HTML: "html|htm|xhtml|we|wpy", + HTML_Elixir: "eex|html.eex", + HTML_Ruby: "erb|rhtml|html.erb", + INI: "ini|conf|cfg|prefs", + Io: "io", + Ion: "ion", + Jack: "jack", + Jade: "jade|pug", + Java: "java", + JavaScript: "js|jsm|jsx|cjs|mjs", + JEXL: "jexl", + JSON: "json", + JSON5: "json5", + JSONiq: "jq", + JSP: "jsp", + JSSM: "jssm|jssm_state", + JSX: "jsx", + Julia: "jl", + Kotlin: "kt|kts", + LaTeX: "tex|latex|ltx|bib", + Latte: "latte", + LESS: "less", + Liquid: "liquid", + Lisp: "lisp", + LiveScript: "ls", + Log: "log", + LogiQL: "logic|lql", + Logtalk: "lgt", + LSL: "lsl", + Lua: "lua", + LuaPage: "lp", + Lucene: "lucene", + Makefile: "^Makefile|^GNUmakefile|^makefile|^OCamlMakefile|make", + Markdown: "md|markdown", + Mask: "mask", + MATLAB: "matlab", + Maze: "mz", + MediaWiki: "wiki|mediawiki", + MEL: "mel", + MIPS: "s|asm", + MIXAL: "mixal", + MUSHCode: "mc|mush", + MySQL: "mysql", + Nasal: "nas", + Nginx: "nginx|conf", + Nim: "nim", + Nix: "nix", + NSIS: "nsi|nsh", + Nunjucks: "nunjucks|nunjs|nj|njk", + ObjectiveC: "m|mm", + OCaml: "ml|mli", + Odin: "odin", + PartiQL: "partiql|pql", + Pascal: "pas|p", + Perl: "pl|pm", + pgSQL: "pgsql", + PHP: "php|inc|phtml|shtml|php3|php4|php5|phps|phpt|aw|ctp|module", + PHP_Laravel_blade: "blade.php", + Pig: "pig", + PLSQL: "plsql", + Powershell: "ps1", + Praat: "praat|praatscript|psc|proc", + Prisma: "prisma", + Prolog: "plg|prolog", + Properties: "properties", + Protobuf: "proto", + Puppet: "epp|pp", + Python: "py", + PRQL: "prql", + QML: "qml", + R: "r", + Raku: "raku|rakumod|rakutest|p6|pl6|pm6", + Razor: "cshtml|asp", + RDoc: "Rd", + Red: "red|reds", + RHTML: "Rhtml", + Robot: "robot|resource", + RST: "rst", + Ruby: "rb|ru|gemspec|rake|^Guardfile|^Rakefile|^Gemfile", + Rust: "rs", + SaC: "sac", + SASS: "sass", + SCAD: "scad", + Scala: "scala|sbt", + Scheme: "scm|sm|rkt|oak|scheme", + Scrypt: "scrypt", + SCSS: "scss", + SH: "sh|bash|^.bashrc", + SJS: "sjs", + Slim: "slim|skim", + Smarty: "smarty|tpl", + Smithy: "smithy", + snippets: "snippets", + Soy_Template: "soy", + Space: "space", + SPARQL: "rq", + SQL: "sql", + SQLServer: "sqlserver", + Stylus: "styl|stylus", + SVG: "svg", + Swift: "swift", + Tcl: "tcl", + Terraform: "tf|tfvars|terragrunt", + Tex: "tex", + Text: "txt", + Textile: "textile", + Toml: "toml", + TSX: "tsx", + Turtle: "ttl", + Twig: "twig|swig", + Typescript: "ts|typescript|str", + Vala: "vala", + VBScript: "vbs|vb", + Velocity: "vm", + Verilog: "v|vh|sv|svh", + VHDL: "vhd|vhdl", + Visualforce: "vfp|component|page", + Vue: "vue", + Wollok: "wlk|wpgm|wtest", + XML: "xml|rdf|rss|wsdl|xslt|atom|mathml|mml|xul|xbl|xaml", + XQuery: "xq", + YAML: "yaml|yml", + Zeek: "zeek|bro", + Zig: "zig", + Django: "html", }; const languageNames = { - ObjectiveC: "Objective-C", - CSharp: "C#", - golang: "Go", - C_Cpp: "C/C++", - Csound_Document: "Csound Document", - Csound_Orchestra: "Csound", - Csound_Score: "Csound Score", - coffee: "CoffeeScript", - HTML_Ruby: "HTML (Ruby)", - HTML_Elixir: "HTML (Elixir)", - FTL: "FreeMarker", - PHP_Laravel_blade: "PHP (Blade Template)", - Perl6: "Perl 6", - AutoHotKey: "AutoHotkey/AutoIt", + ObjectiveC: "Objective-C", + CSharp: "C#", + golang: "Go", + C_Cpp: "C/C++", + Csound_Document: "Csound Document", + Csound_Orchestra: "Csound", + Csound_Score: "Csound Score", + coffee: "CoffeeScript", + HTML_Ruby: "HTML (Ruby)", + HTML_Elixir: "HTML (Elixir)", + FTL: "FreeMarker", + PHP_Laravel_blade: "PHP (Blade Template)", + Perl6: "Perl 6", + AutoHotKey: "AutoHotkey/AutoIt", }; Object.keys(modeList).forEach((key) => { - const extensions = modeList[key]; - const caption = languageNames[key]; + const extensions = modeList[key]; + const caption = languageNames[key]; - addMode(key, extensions, caption); + addMode(key, extensions, caption); }); diff --git a/src/ace/touchHandler.js b/src/ace/touchHandler.js index 84885b468..6c9eed374 100644 --- a/src/ace/touchHandler.js +++ b/src/ace/touchHandler.js @@ -1,1079 +1,1077 @@ -import tag from 'html-tag-js'; -import constants from 'lib/constants'; -import appSettings from 'lib/settings'; -import { key } from 'handlers/quickTools'; -import selectionMenu from 'lib/selectionMenu'; -import { getColorRange } from 'utils/color/regex'; +import { key } from "handlers/quickTools"; +import tag from "html-tag-js"; +import constants from "lib/constants"; +import selectionMenu from "lib/selectionMenu"; +import appSettings from "lib/settings"; +import { getColorRange } from "utils/color/regex"; export let scrollAnimationFrame; // scroll animation frame id const SCROLL_SPEED = { - FAST_X2: 0.99, - FAST: 0.97, - NORMAL: 0.95, - SLOW: 0.9, + FAST_X2: 0.99, + FAST: 0.97, + NORMAL: 0.95, + SLOW: 0.9, }; /** * Handler for touch events * @param {AceAjax.Editor} editor Ace editor instance * @param {boolean} minimal if true, disable selection, menu and cursor -*/ + */ export default function addTouchListeners(editor, minimal, onclick) { - const { renderer, container: $el } = editor; - const { $gutter } = renderer; - const { Range } = ace.require('ace/range'); - - let { - diagonalScrolling, - reverseScrolling, - teardropSize, - teardropTimeout, - scrollSpeed, - } = appSettings.value; - - if (minimal) { - diagonalScrolling = false; - reverseScrolling = false; - teardropSize = 0; - } - - /** - * Selection controller start - */ - const $start = tag('span', { - className: 'cursor start', - dataset: { - size: teardropSize, - }, - size: teardropSize, - }); - - /** - * Selection controller end - */ - const $end = tag('span', { - className: 'cursor end', - dataset: { - size: teardropSize, - }, - size: teardropSize, - }); - - /** - * Tear drop cursor - */ - const $cursor = tag('span', { - className: 'cursor single', - dataset: { - size: teardropSize, - }, - get size() { - const widthSq = teardropSize * teardropSize * 2; - const actualWidth = Math.sqrt(widthSq); - delete this.size; - this.size = actualWidth; - return actualWidth; - }, - startHide() { - clearTimeout($cursor.dataset.timeout); - $cursor.dataset.timeout = setTimeout(() => { - $cursor.remove(); - hideMenu(); - }, teardropTimeout); - }, - }); - - /** - * Text menu for touch devices - */ - const $menu = ; - const RESET_CLICK_COUNT_TIME = 500; // ms - const config = { passive: false }; // event listener config - const ACE_NO_CURSOR = '.ace_gutter,.ace_gutter *,.ace_fold,.ace_inline_button'; - - let LOCK_X = appSettings.value.textWrap; - - let scrollTimeout; // timeout to check if scrolling is finished - let menuActive; // true if menu is active - let mode; // cursor, selection or scroll - let moveY; // touch difference in vertical direction - let moveX; // touch difference in horizontal direction - let lastX; // last x - let lastY; // last y - let directionX; // direction in x - let directionY; // direction in y - let initialX; // initial x - let initialY; // initial y - let initialTimeX; // initial time - let initialTimeY; // initial time - let lockX; // lock x for prevent scrolling in horizontal direction - let lockY; // lock y for prevent scrolling in vertical direction - let clickCount = 0; // number of clicks - let selectionActive; // true if selection is active - let lastClickPos = null; - let teardropDoesShowMenu = true; // teardrop handler - let teardropTouchEnded = false; // teardrop handler - let teardropMoveTimeout; // teardrop handler - let forceCursorMode = false; // force to show cursor - let $activeTeardrop; // active teardrop - let timeTouchStart; // time of touch start - let touchEnded = true; // true if touch ended - let threshold = appSettings.value.touchMoveThreshold; - - $el.addEventListener('touchstart', touchStart, config, true); - $el.addEventListener('contextmenu', contextmenu, config, true); - - editor.setSelection = (value) => { - selectionActive = value; - }; - - editor.setMenu = (value) => { - menuActive = value; - }; - - if (!minimal) { - editor.on('change', onupdate); - editor.on('changeSession', onchangesession); - editor.on('scroll', onscroll); - editor.on('fold', onfold); - editor.on('select-word', () => { - selectionMode($end); - }); - editor.on('scroll-intoview', () => { - if (selectionActive) { - selectionMode($end); - } else { - cursorMode(); - } - }); - - appSettings.on('update:diagonalScrolling', (value) => { - diagonalScrolling = value; - }); - appSettings.on('update:reverseScrolling', (value) => { - reverseScrolling = value; - }); - appSettings.on('update:teardropSize', (value) => { - teardropSize = value; - $start.dataset.size = value; - $end.dataset.size = value; - $cursor.dataset.size = value; - }); - appSettings.on('update:textWrap', (value) => { - LOCK_X = value; - onupdate(); - }); - appSettings.on('update:scrollSpeed', (value) => { - scrollSpeed = value; - }); - appSettings.on('update:touchMoveThreshold', (value) => { - threshold = value; - }); - } - - /** - * Editor container on touch start - * @param {TouchEvent} e Touch event - */ - function touchStart(e) { - /**@type {HTMLElement} */ - const $target = e.target; - - editor.textInput.onContextMenu = null; - cancelAnimationFrame(scrollAnimationFrame); - const { clientX, clientY } = e.touches[0]; - - if (minimal && clientX <= constants.SIDEBAR_SLIDE_START_THRESHOLD_PX) { - return; - } - - if (isIn($start, clientX, clientY)) { - e.preventDefault(); - teardropHandler($start); - return; - } - - if (isIn($end, clientX, clientY)) { - e.preventDefault(); - teardropHandler($end); - return; - } - - if (isIn($cursor, clientX, clientY)) { - e.preventDefault(); - teardropHandler($cursor); - return; - } - - if ($target.matches(ACE_NO_CURSOR)) { - moveCursorTo(0, clientY); - return; - } - - touchEnded = false; - lastX = clientX; - lastY = clientY; - initialX = clientX; - initialY = clientY; - initialTimeX = e.timeStamp; - initialTimeY = e.timeStamp; - moveY = 0; - moveX = 0; - lockX = LOCK_X; - lockY = false; - mode = 'wait'; - - setTimeout(() => { - clickCount = 0; - lastClickPos = null; - }, RESET_CLICK_COUNT_TIME); - - document.addEventListener('touchmove', touchMove, config); - document.addEventListener('touchend', touchEnd, config); - } - - /** - * Editor container on touch move - * @param {TouchEvent} e Event - */ - function touchMove(e) { - if (mode === 'selection') { - removeListeners(); - return; - } - - let currentDirectionX; // direction in x - let currentDirectionY; // direction in y - const { clientX, clientY } = e.touches[0]; - - moveX = clientX - lastX; - moveY = clientY - lastY; - currentDirectionX = moveX > 0 ? 1 : -1; - currentDirectionY = moveY > 0 ? 1 : -1; - - if (directionX !== currentDirectionX) { - initialX = clientX; - initialTimeX = e.timeStamp; - } - - if (directionY !== currentDirectionY) { - initialY = clientY; - initialTimeY = e.timeStamp; - } - - directionX = currentDirectionX; - directionY = currentDirectionY; - lastX = clientX; - lastY = clientY; - - if (!moveX && !moveY) { - return; - } - - if (!diagonalScrolling && !lockX && !lockY) { - if (Math.abs(moveX) > Math.abs(moveY)) { - lockY = true; - } else { - lockX = true; - } - } - - if (lockX || Math.abs(moveX) < threshold) { - moveX = 0; - } - - if (lockY || Math.abs(moveY) < threshold) { - moveY = 0; - } - - if (moveX || moveY) { - e.preventDefault(); - [moveX, moveY] = testScroll(moveX, moveY); - mode = 'scroll'; - scroll(moveX, moveY); - } - } - - /** - * Editor container on touch end - * @param {TouchEvent} e Event - */ - function touchEnd(e) { - const { clientX, clientY } = e.changedTouches[0]; - // why I was using e.preventDefault() ? 🤔 - // because select word and select line misbehave without - // preventDefault - removeListeners(); - touchEnded = true; - - if (mode === 'scroll') { - const deltaTimeX = e.timeStamp - initialTimeX; - const deltaTimeY = e.timeStamp - initialTimeY; - const deltaX = clientX - initialX; - const deltaY = clientY - initialY; - const velocityX = lockX ? 0 : Math.round(deltaX / deltaTimeX); // in px/ms - const velocityY = lockY ? 0 : Math.round(deltaY / deltaTimeY); // in px/ms - scrollAnimation(velocityX, velocityY); - return; - } - - - if (mode === 'wait') { - if (lastClickPos) { - const { - clientX: clickXThen, - clientY: clickYThen, - } = lastClickPos; - const { - row: rowNow, - column: columnNow, - } = renderer.screenToTextCoordinates(clientX, clientY); - const { - row: rowThen, - column: columnThen, - } = renderer.screenToTextCoordinates(clickXThen, clickYThen); - - const rowDiff = Math.abs(rowNow - rowThen); - const columnDiff = Math.abs(columnNow - columnThen); - if (!rowDiff && columnDiff <= 2) { - clickCount += 1; - } - } else { - clickCount = 1; - } - - lastClickPos = { clientX, clientY }; - - if (clickCount === 2) { - mode = 'selection'; - } else if (clickCount >= 3) { - mode = 'select-line'; - } else { - mode = 'cursor'; - } - } - - if (minimal && mode === 'cursor') { - moveCursorTo(clientX, clientY); - if (onclick) onclick(); - return; - } else if (minimal) { - return; - } - - if (mode === 'cursor') { - e.preventDefault(); - const shiftKey = key.shift || e.shiftKey; - const ctrlKey = key.ctrl || e.ctrlKey; - moveCursorTo(clientX, clientY, shiftKey, ctrlKey); - if (!ctrlKey && !shiftKey) { - forceCursorMode = true; - cursorMode(); - } - if (!editor.isFocused()) editor.focus(); - return; - } - - if (mode === 'selection') { - e.preventDefault(); - moveCursorTo(clientX, clientY); - select(); - vibrate(); - return; - } - - if (mode === 'select-line') { - e.preventDefault(); - moveCursorTo(clientX, clientY); - editor.selection.selectLine(); - selectionMode($end); - vibrate(); - } - } - - /** - * Checks if given element is in the touch area - * @param {Element} $el - * @param {number} cX - * @param {number} cY - * @returns - */ - function isIn($el, cX, cY) { - const { - x, - y, - left, - top, - width: sWidth, - height: sHeight, - } = $el.getBoundingClientRect(); - - const sx = x || left; - const sy = y || top; - - return (cX > sx && cX < sx + sWidth - && cY > sy && cY < sy + sHeight); - } - - /** - * Vibrate device - * @returns {void} - */ - function vibrate() { - if (appSettings.value.vibrateOnTap) { - navigator.vibrate(constants.VIBRATION_TIME); - } - } - - /** - * Callback for contextmenu event - * @param {MouseEvent} e Event - */ - function contextmenu(e) { - e.preventDefault(); - e.stopPropagation(); - if (minimal) return; - const { clientX, clientY } = e; - moveCursorTo(clientX, clientY); - select(); - touchEnded = true; - editor.focus(); - } - - /** - * Select word at cursor position - * @returns {void} - */ - function select() { - removeListeners(); - const range = getColorRange() || editor.selection.getWordRange(); - if (!range || range?.isEmpty()) return; - editor.selection.setSelectionRange(range); - selectionMode($end); - } - - /** - * Scrolls the editor with smooth animation - * @param {number} velocityX velocity in x direction - * @param {number} velocityY velocity in y direction - * @param {number} [timeThen] - * @returns {void} - */ - function scrollAnimation(velocityX, velocityY, timeThen = 0) { - if (!velocityX && !velocityY) { - onscrollend(); - return; - } - - const timeNow = Date.now(); - - if (!timeThen) { - scrollAnimationFrame = requestAnimationFrame( - scrollAnimation.bind(null, velocityX, velocityY, timeNow), - ); - return; - } - - const timeElapsed = timeNow - timeThen; - const FRICTION = SCROLL_SPEED[scrollSpeed]; - const nextX = velocityX * timeElapsed; - const nextY = velocityY * timeElapsed; - - let scrollX = parseInt(nextX * 100) / 100; - let scrollY = parseInt(nextY * 100) / 100; - - const [canScrollX, canScrollY] = testScroll(scrollX, scrollY); - - if (!canScrollX) { - velocityX = 0; - scrollX = 0; - } - - if (!canScrollY) { - velocityY = 0; - scrollY = 0; - } - - if (!scrollX && !scrollY) { - cancelAnimationFrame(scrollAnimationFrame); - return; - } - - scroll(scrollX, scrollY); - - velocityX *= FRICTION; - velocityY *= FRICTION; - - scrollAnimationFrame = requestAnimationFrame( - scrollAnimation.bind(null, velocityX, velocityY, timeNow), - ); - } - - /** - * Test if scrolling is possible - * @param {number} moveX move in x direction - * @param {number} moveY move in y direction - * @returns {[number, number]} - */ - function testScroll(moveX, moveY) { - const UP = reverseScrolling ? 'down' : 'up'; - const DOWN = reverseScrolling ? 'up' : 'down'; - const LEFT = reverseScrolling ? 'right' : 'left'; - const RIGHT = reverseScrolling ? 'left' : 'right'; - - const vDirection = moveY > 0 ? DOWN : UP; - const hDirection = moveX > 0 ? RIGHT : LEFT; - - const { getEditorHeight, getEditorWidth } = editorManager; - const scrollLeft = editor.renderer.getScrollLeft(); - const scrollTop = editor.renderer.getScrollTop(); - const [editorWidth, editorHeight] = [getEditorWidth(editor), getEditorHeight(editor)]; - - if ( - (vDirection === 'down' && scrollTop <= 0) - || (vDirection === 'up' && scrollTop >= editorHeight) - ) { - moveY = 0; - } - - if ( - (hDirection === 'right' && scrollLeft <= 0) - || (hDirection === 'left' && scrollLeft >= editorWidth) - ) { - moveX = 0; - } - - - return [moveX, moveY]; - } - - /** - * Scroll to given position - * @param {number} x - * @param {number} y - */ - function scroll(x, y) { - let direction = reverseScrolling ? 1 : -1; - let scrollX = direction * x; - let scrollY = direction * y; - - renderer.scrollBy(scrollX, scrollY); - } - - /** - * Remove all listeners - */ - function removeListeners() { - document.removeEventListener('touchmove', touchMove, config); - document.removeEventListener('touchend', touchEnd, config); - } - - /** - * Compare two ranges - * @param {AceAjax.Range} r1 - * @param {AceAjax.Range} r2 - * @returns {boolean} - */ - function compareRanges(r1, r2) { - return r1.start.row === r2.start.row - && r1.start.column === r2.start.column - && r1.end.row === r2.end.row - && r1.end.column === r2.end.column; - } - - /** - * Moves cursor to given position - * @param {number} x - * @param {number} y - * @param {boolean} [shiftKey] - * @param {boolean} [ctrlKey] - */ - function moveCursorTo(x, y, shiftKey = false, ctrlKey = false) { - const pos = renderer.screenToTextCoordinates(x, y); - - hideTooltip(); - - if (shiftKey) { - const anchor = editor.selection.getSelectionAnchor() || editor.getCursorPosition(); - editor.selection.setRange({ start: anchor, end: pos }); - selectionMode($end); - return; - } - - if (ctrlKey) { - const range = new Range(pos.row, pos.column, pos.row, pos.column); - const ranges = editor.selection.getAllRanges(); - const exists = ranges.some((r) => compareRanges(r, range)); - if (exists) { - editor.selection.clearSelection(); - ranges.splice(ranges.indexOf(exists), 1); - ranges.forEach((r) => editor.selection.addRange(r)); - return; - } - - editor.selection.addRange(range); - return; - } - - editor.selection.moveToPosition(pos); - } - - /** - * Shows teardrop - * @returns {void} - */ - function cursorMode() { - if ((!teardropSize || !editor.isFocused()) && !forceCursorMode) { - $cursor.remove(); - return; - } - - forceCursorMode = false; - clearTimeout($cursor.dataset.timeout); - clearSelectionMode(); - - const { pageX, pageY } = renderer.textToScreenCoordinates( - editor.getCursorPosition(), - ); - const { lineHeight } = renderer; - const actualHeight = lineHeight; - const [x, y] = relativePosition(pageX, pageY + actualHeight); - $cursor.style.left = `${x}px`; - $cursor.style.top = `${y}px`; - if (!$cursor.isConnected) $el.append($cursor); - $cursor.startHide(); - - editor.selection.on('changeCursor', clearCursorMode); - } - - /** - * Remove cursor mode - * @returns {void} - */ - function clearCursorMode() { - if (!$el.contains($cursor)) return; - if ($cursor.dataset.immortal === 'true') return; - $cursor.remove(); - clearTimeout($cursor.dataset.timeout); - - editor.selection.off('changeCursor', clearCursorMode); - } - - /** - * Shows both teardrops - * @param {HTMLElement} $trigger - * @returns {void} - */ - function selectionMode($trigger) { - if (!teardropSize) return; - - clearCursorMode(); - selectionActive = true; - positionEnd(); - positionStart(); - if ($trigger) showMenu($trigger); - - setTimeout(() => { - editor.selection.on('changeSelection', clearSelectionMode); - editor.selection.on('changeCursor', clearSelectionMode); - }, 0); - } - - /** - * Positions the start teardrop - */ - function positionStart() { - const range = editor.getSelectionRange(); - const { pageX, pageY } = renderer.textToScreenCoordinates(range.start); - const { lineHeight } = renderer; - const [x, y] = relativePosition(pageX - teardropSize, pageY + lineHeight); - - $start.style.left = `${x}px`; - $start.style.top = `${y}px`; - - if (!$start.isConnected) $el.append($start); - } - - /** - * Positions the end teardrop - */ - function positionEnd() { - const range = editor.getSelectionRange(); - const { pageX, pageY } = renderer.textToScreenCoordinates(range.end); - const { lineHeight } = renderer; - const [x, y] = relativePosition(pageX, pageY + lineHeight); - - $end.style.left = `${x}px`; - $end.style.top = `${y}px`; - - if (!$end.isConnected) $el.append($end); - } - - /** - * Remove selection mode - * @param {Event} e Event - * @param {boolean} clearActive whether to clear selectionActive - * @returns {void} - */ - function clearSelectionMode(e, clearActive = true) { - const $els = [$start.dataset.immortal, $end.dataset.immortal]; - if ($els.includes('true')) return; - if ($el.contains($start)) $start.remove(); - if ($el.contains($end)) $end.remove(); - if (clearActive) { - selectionActive = false; - } - - editor.selection.off('changeSelection', clearSelectionMode); - editor.selection.off('changeCursor', clearSelectionMode); - } - - /** - * Shows the edit context menu - * @param {HTMLElement} [$trigger] A trigger element that triggered the menu, if not provided, menu will be shown at the current cursor position - */ - function showMenu($trigger) { - menuActive = true; - const rect = $trigger?.getBoundingClientRect(); - const { bottom, left } = rect; - const readOnly = editor.getReadOnly(); - const [x, y] = relativePosition(left, bottom); - if (readOnly) { - populateMenuItems('read-only'); - } else { - populateMenuItems(); - } - - $menu.style.left = `${x}px`; - $menu.style.top = `${y}px`; - - if (!$menu.isConnected) $el.parentElement.append($menu); - if ($trigger) positionMenu($trigger); - - editor.selection.on('changeCursor', hideMenu); - editor.selection.on('changeSelection', hideMenu); - } - - /** - * @param {boolean} clearActive whether to clear menuActive - * @returns {void} - */ - function hideMenu(clearActive = true) { - if (!$el.parentElement.contains($menu)) return; - $menu.remove(); - editor.selection.off('changeCursor', hideMenu); - editor.selection.off('changeSelection', hideMenu); - if (clearActive) menuActive = false; - } - - /** - * Populates the menu items - * @param {HTMLElement} $trigger - * @returns - */ - function positionMenu($trigger) { - const getProp = ($el, prop) => $el.getBoundingClientRect()[prop]; - const containerRight = getProp($el, 'right'); - const containerLeft = getProp($el, 'left'); - const containerBottom = getProp($el, 'bottom'); - const { lineHeight } = editor.renderer; - const margin = 10; - - - // if menu is positioned off screen horizontally from the right - const menuRight = getProp($menu, 'right'); - if (menuRight + margin > containerRight) { - const menuLeft = getProp($menu, 'left'); - const [x] = relativePosition(menuLeft - Math.abs(menuRight - containerRight)); - $menu.style.left = `${x - margin}px`; - } - - // if menu is positioned off screen horizontally from the left - const menuLeft = getProp($menu, 'left'); - if (menuLeft - margin < containerLeft) { - const [x] = relativePosition(menuLeft + Math.abs(menuLeft - containerLeft)); - $menu.style.left = `${x + margin}px`; - } - - if (shrink()) return; - - // if menu is positioned off screen vertically from the bottom - const menuBottom = getProp($menu, 'bottom'); - if (menuBottom > containerBottom) { - const range = editor.getSelectionRange(); - let pos; - - if ($trigger === $start) { - pos = range.start; - } else { - pos = range.end; - } - - const { pageY } = renderer.textToScreenCoordinates(pos); - const [, y] = relativePosition(null, pageY - lineHeight * 1.8); - $menu.style.top = `${y}px`; - } - - function shrink() { - const [left, right] = [getProp($menu, 'left'), getProp($menu, 'right')]; - const tooLeft = left < containerLeft; - const tooRight = right > containerRight; - if (tooLeft || tooRight) { - const { scale = 1 } = $menu.dataset; - $menu.dataset.scale = parseFloat(scale - 0.1); - $menu.style.transform = `scale(${$menu.dataset.scale})`; - positionMenu($trigger); - return true; - } - return false; - } - } - - /** - * Handles teardrop - * @param {HTMLDivElement} $teardrop Teardrop element to handle - */ - function teardropHandler($teardrop) { - $activeTeardrop = $teardrop; - $activeTeardrop.dataset.immortal = true; - teardropDoesShowMenu = true; - teardropTouchEnded = false; - - if (mode === 'cursor') { - const timeout = parseInt($cursor.dataset.timeout, 10); - clearTimeout(timeout); - } - - timeTouchStart = Date.now(); - document.addEventListener('touchmove', teardropTouchMoveHandler, config); - document.addEventListener('touchend', teardropTouchEndHandler, config); - } - - /** - * Touch event handler for teardrop - * @param {Event} e - */ - function teardropTouchMoveHandler(e) { - const { clientX, clientY } = e.touches[0]; - const { lineHeight } = renderer; - const { start, end } = editor.selection.getRange(); - let y = clientY - (lineHeight * 1.8); - let x = clientX; - - if (timeTouchStart) { - timeTouchStart = null; - - // Prevents accidental touchmove - if (diffX < threshold && diffY < threshold) return; - - const diffX = Math.abs(lastX - clientX); - const diffY = Math.abs(lastY - clientY); - const timeDiff = Date.now() - timeTouchStart; - - // Prevents accidental touchmove or highly sensitive touchmove - if (timeDiff < 50) return; - return; - } - - if ($activeTeardrop === $cursor) { - const { row, column } = renderer.screenToTextCoordinates(x, y); - editor.gotoLine(row + 1, column); - } else if ($activeTeardrop === $start) { - x = clientX + teardropSize; - - const { pageX, pageY } = renderer.textToScreenCoordinates(end); - if (pageY <= y) { - y = pageY; - } - - if (pageY <= y && pageX < x) { - x = pageX; - } - - let { row, column } = renderer.screenToTextCoordinates(x, y); - - if (column === end.column) { - --column; - } - - editor.selection.setSelectionAnchor(row, column); - positionEnd(); - } else { - const { pageX, pageY } = renderer.textToScreenCoordinates(start); - if (pageY >= y) { - y = pageY; - } - - if (pageY >= y && pageX > x) { - x = pageX; - } - - let { row, column } = renderer.screenToTextCoordinates(x, y); - - if (column === start.column) { - ++column; - } - - editor.selection.moveCursorToPosition({ row, column }); - positionStart(); - } - - clearTimeout(teardropMoveTimeout); - const parent = $el.getBoundingClientRect(); - let deltaX = 0; - if (clientY < parent.top) deltaX = -lineHeight; - if (clientY > parent.bottom) deltaX = lineHeight; - - if (deltaX) { - teardropMoveTimeout = setTimeout(() => { - const top = editor.session.getScrollTop(); - editor.session.setScrollTop(top + deltaX); - if (teardropTouchEnded) return; - teardropTouchMoveHandler(e); - }, 100); - } - - const [left, top] = relativePosition(clientX, clientY - lineHeight); - $activeTeardrop.style.left = `${left}px`; - $activeTeardrop.style.top = `${top}px`; - } - - /** - * Touch event handler for teardrop - */ - function teardropTouchEndHandler() { - teardropTouchEnded = true; - if ($activeTeardrop === $cursor) { - cursorMode(); - } else { - selectionMode($activeTeardrop); - } - - $activeTeardrop.dataset.immortal = false; - document.removeEventListener('touchmove', teardropTouchMoveHandler, config); - document.removeEventListener('touchend', teardropTouchEndHandler, config); - if (teardropDoesShowMenu) { - showMenu($activeTeardrop); - } - editor.focus(); - } - - /** - * Editor container on scroll - */ - function onscroll() { - clearTimeout(scrollTimeout); - clearCursorMode(); - clearSelectionMode(null, false); - hideMenu(false); - - hideTooltip(); - scrollTimeout = setTimeout(onscrollend, 100); - } - - /** - * Hides tooltip in the gutter - */ - function hideTooltip() { - $gutter.dispatchEvent(new MouseEvent('mouseout')); - } - - /** - * Editor container on scroll end - */ - function onscrollend() { - scrollTimeout = null; - editor._emit('scroll-end'); - if (!touchEnded) return; - - if (selectionActive) { - selectionMode(); - } - - if (menuActive) { - showMenu($end); - } - } - - /** - * Editor container on update - */ - function onupdate() { - clearSelectionMode(); - clearCursorMode(); - hideMenu(); - } - - /** - * Editor container on change session - */ - function onchangesession() { - if (scrollTimeout) { - clearTimeout(scrollTimeout); - onscrollend(); - } - - cancelAnimationFrame(scrollAnimationFrame); - setTimeout(() => { - const copyText = editor.session.getTextRange(editor.getSelectionRange()); - if (copyText) { - selectionMode($end); - return; - } - - clearSelectionMode(); - cursorMode(); - hideMenu(); - }, 0); - } - - /** - * Editor container on fold - */ - function onfold() { - if (selectionActive) { - positionEnd(); - positionStart(); - hideMenu(); - showMenu($end); - } else { - clearCursorMode(); - } - } - - /** - * Populates the menu items - * @param {'regular'|'read-only'|'select'} mode - */ - function populateMenuItems(mode = 'regular') { - $menu.innerHTML = ''; - const copyText = editor.getCopyText(); - const items = []; - - selectionMenu().forEach((item) => { - if (mode === 'read-only' && !item.readOnly) return; - if (copyText && !['selected', 'all'].includes(item.mode)) return; - if (!copyText && item.mode === 'selected') return; - - items.push(item); - }); - - items.forEach(({ onclick, text }) => { - $menu.append( -
{text}
- ); - }); - } - - /** - * Returns relative position of given coordinates - * @param {number} x x coordinate - * @param {number} y y coordinate - * @returns {[number, number]} - */ - function relativePosition(x, y) { - const { top, left } = $el.getBoundingClientRect(); - return [x - left, y - top]; - } + const { renderer, container: $el } = editor; + const { $gutter } = renderer; + const { Range } = ace.require("ace/range"); + + let { + diagonalScrolling, + reverseScrolling, + teardropSize, + teardropTimeout, + scrollSpeed, + } = appSettings.value; + + if (minimal) { + diagonalScrolling = false; + reverseScrolling = false; + teardropSize = 0; + } + + /** + * Selection controller start + */ + const $start = tag("span", { + className: "cursor start", + dataset: { + size: teardropSize, + }, + size: teardropSize, + }); + + /** + * Selection controller end + */ + const $end = tag("span", { + className: "cursor end", + dataset: { + size: teardropSize, + }, + size: teardropSize, + }); + + /** + * Tear drop cursor + */ + const $cursor = tag("span", { + className: "cursor single", + dataset: { + size: teardropSize, + }, + get size() { + const widthSq = teardropSize * teardropSize * 2; + const actualWidth = Math.sqrt(widthSq); + delete this.size; + this.size = actualWidth; + return actualWidth; + }, + startHide() { + clearTimeout($cursor.dataset.timeout); + $cursor.dataset.timeout = setTimeout(() => { + $cursor.remove(); + hideMenu(); + }, teardropTimeout); + }, + }); + + /** + * Text menu for touch devices + */ + const $menu = ; + const RESET_CLICK_COUNT_TIME = 500; // ms + const config = { passive: false }; // event listener config + const ACE_NO_CURSOR = + ".ace_gutter,.ace_gutter *,.ace_fold,.ace_inline_button"; + + let LOCK_X = appSettings.value.textWrap; + + let scrollTimeout; // timeout to check if scrolling is finished + let menuActive; // true if menu is active + let mode; // cursor, selection or scroll + let moveY; // touch difference in vertical direction + let moveX; // touch difference in horizontal direction + let lastX; // last x + let lastY; // last y + let directionX; // direction in x + let directionY; // direction in y + let initialX; // initial x + let initialY; // initial y + let initialTimeX; // initial time + let initialTimeY; // initial time + let lockX; // lock x for prevent scrolling in horizontal direction + let lockY; // lock y for prevent scrolling in vertical direction + let clickCount = 0; // number of clicks + let selectionActive; // true if selection is active + let lastClickPos = null; + let teardropDoesShowMenu = true; // teardrop handler + let teardropTouchEnded = false; // teardrop handler + let teardropMoveTimeout; // teardrop handler + let forceCursorMode = false; // force to show cursor + let $activeTeardrop; // active teardrop + let timeTouchStart; // time of touch start + let touchEnded = true; // true if touch ended + let threshold = appSettings.value.touchMoveThreshold; + + $el.addEventListener("touchstart", touchStart, config, true); + $el.addEventListener("contextmenu", contextmenu, config, true); + + editor.setSelection = (value) => { + selectionActive = value; + }; + + editor.setMenu = (value) => { + menuActive = value; + }; + + if (!minimal) { + editor.on("change", onupdate); + editor.on("changeSession", onchangesession); + editor.on("scroll", onscroll); + editor.on("fold", onfold); + editor.on("select-word", () => { + selectionMode($end); + }); + editor.on("scroll-intoview", () => { + if (selectionActive) { + selectionMode($end); + } else { + cursorMode(); + } + }); + + appSettings.on("update:diagonalScrolling", (value) => { + diagonalScrolling = value; + }); + appSettings.on("update:reverseScrolling", (value) => { + reverseScrolling = value; + }); + appSettings.on("update:teardropSize", (value) => { + teardropSize = value; + $start.dataset.size = value; + $end.dataset.size = value; + $cursor.dataset.size = value; + }); + appSettings.on("update:textWrap", (value) => { + LOCK_X = value; + onupdate(); + }); + appSettings.on("update:scrollSpeed", (value) => { + scrollSpeed = value; + }); + appSettings.on("update:touchMoveThreshold", (value) => { + threshold = value; + }); + } + + /** + * Editor container on touch start + * @param {TouchEvent} e Touch event + */ + function touchStart(e) { + /**@type {HTMLElement} */ + const $target = e.target; + + editor.textInput.onContextMenu = null; + cancelAnimationFrame(scrollAnimationFrame); + const { clientX, clientY } = e.touches[0]; + + if (minimal && clientX <= constants.SIDEBAR_SLIDE_START_THRESHOLD_PX) { + return; + } + + if (isIn($start, clientX, clientY)) { + e.preventDefault(); + teardropHandler($start); + return; + } + + if (isIn($end, clientX, clientY)) { + e.preventDefault(); + teardropHandler($end); + return; + } + + if (isIn($cursor, clientX, clientY)) { + e.preventDefault(); + teardropHandler($cursor); + return; + } + + if ($target.matches(ACE_NO_CURSOR)) { + moveCursorTo(0, clientY); + return; + } + + touchEnded = false; + lastX = clientX; + lastY = clientY; + initialX = clientX; + initialY = clientY; + initialTimeX = e.timeStamp; + initialTimeY = e.timeStamp; + moveY = 0; + moveX = 0; + lockX = LOCK_X; + lockY = false; + mode = "wait"; + + setTimeout(() => { + clickCount = 0; + lastClickPos = null; + }, RESET_CLICK_COUNT_TIME); + + document.addEventListener("touchmove", touchMove, config); + document.addEventListener("touchend", touchEnd, config); + } + + /** + * Editor container on touch move + * @param {TouchEvent} e Event + */ + function touchMove(e) { + if (mode === "selection") { + removeListeners(); + return; + } + + let currentDirectionX; // direction in x + let currentDirectionY; // direction in y + const { clientX, clientY } = e.touches[0]; + + moveX = clientX - lastX; + moveY = clientY - lastY; + currentDirectionX = moveX > 0 ? 1 : -1; + currentDirectionY = moveY > 0 ? 1 : -1; + + if (directionX !== currentDirectionX) { + initialX = clientX; + initialTimeX = e.timeStamp; + } + + if (directionY !== currentDirectionY) { + initialY = clientY; + initialTimeY = e.timeStamp; + } + + directionX = currentDirectionX; + directionY = currentDirectionY; + lastX = clientX; + lastY = clientY; + + if (!moveX && !moveY) { + return; + } + + if (!diagonalScrolling && !lockX && !lockY) { + if (Math.abs(moveX) > Math.abs(moveY)) { + lockY = true; + } else { + lockX = true; + } + } + + if (lockX || Math.abs(moveX) < threshold) { + moveX = 0; + } + + if (lockY || Math.abs(moveY) < threshold) { + moveY = 0; + } + + if (moveX || moveY) { + e.preventDefault(); + [moveX, moveY] = testScroll(moveX, moveY); + mode = "scroll"; + scroll(moveX, moveY); + } + } + + /** + * Editor container on touch end + * @param {TouchEvent} e Event + */ + function touchEnd(e) { + const { clientX, clientY } = e.changedTouches[0]; + // why I was using e.preventDefault() ? 🤔 + // because select word and select line misbehave without + // preventDefault + removeListeners(); + touchEnded = true; + + if (mode === "scroll") { + const deltaTimeX = e.timeStamp - initialTimeX; + const deltaTimeY = e.timeStamp - initialTimeY; + const deltaX = clientX - initialX; + const deltaY = clientY - initialY; + const velocityX = lockX ? 0 : Math.round(deltaX / deltaTimeX); // in px/ms + const velocityY = lockY ? 0 : Math.round(deltaY / deltaTimeY); // in px/ms + scrollAnimation(velocityX, velocityY); + return; + } + + if (mode === "wait") { + if (lastClickPos) { + const { clientX: clickXThen, clientY: clickYThen } = lastClickPos; + const { row: rowNow, column: columnNow } = + renderer.screenToTextCoordinates(clientX, clientY); + const { row: rowThen, column: columnThen } = + renderer.screenToTextCoordinates(clickXThen, clickYThen); + + const rowDiff = Math.abs(rowNow - rowThen); + const columnDiff = Math.abs(columnNow - columnThen); + if (!rowDiff && columnDiff <= 2) { + clickCount += 1; + } + } else { + clickCount = 1; + } + + lastClickPos = { clientX, clientY }; + + if (clickCount === 2) { + mode = "selection"; + } else if (clickCount >= 3) { + mode = "select-line"; + } else { + mode = "cursor"; + } + } + + if (minimal && mode === "cursor") { + moveCursorTo(clientX, clientY); + if (onclick) onclick(); + return; + } else if (minimal) { + return; + } + + if (mode === "cursor") { + e.preventDefault(); + const shiftKey = key.shift || e.shiftKey; + const ctrlKey = key.ctrl || e.ctrlKey; + moveCursorTo(clientX, clientY, shiftKey, ctrlKey); + if (!ctrlKey && !shiftKey) { + forceCursorMode = true; + cursorMode(); + } + if (!editor.isFocused()) editor.focus(); + return; + } + + if (mode === "selection") { + e.preventDefault(); + moveCursorTo(clientX, clientY); + select(); + vibrate(); + return; + } + + if (mode === "select-line") { + e.preventDefault(); + moveCursorTo(clientX, clientY); + editor.selection.selectLine(); + selectionMode($end); + vibrate(); + } + } + + /** + * Checks if given element is in the touch area + * @param {Element} $el + * @param {number} cX + * @param {number} cY + * @returns + */ + function isIn($el, cX, cY) { + const { + x, + y, + left, + top, + width: sWidth, + height: sHeight, + } = $el.getBoundingClientRect(); + + const sx = x || left; + const sy = y || top; + + return cX > sx && cX < sx + sWidth && cY > sy && cY < sy + sHeight; + } + + /** + * Vibrate device + * @returns {void} + */ + function vibrate() { + if (appSettings.value.vibrateOnTap) { + navigator.vibrate(constants.VIBRATION_TIME); + } + } + + /** + * Callback for contextmenu event + * @param {MouseEvent} e Event + */ + function contextmenu(e) { + e.preventDefault(); + e.stopPropagation(); + if (minimal) return; + const { clientX, clientY } = e; + moveCursorTo(clientX, clientY); + select(); + touchEnded = true; + editor.focus(); + } + + /** + * Select word at cursor position + * @returns {void} + */ + function select() { + removeListeners(); + const range = getColorRange() || editor.selection.getWordRange(); + if (!range || range?.isEmpty()) return; + editor.selection.setSelectionRange(range); + selectionMode($end); + } + + /** + * Scrolls the editor with smooth animation + * @param {number} velocityX velocity in x direction + * @param {number} velocityY velocity in y direction + * @param {number} [timeThen] + * @returns {void} + */ + function scrollAnimation(velocityX, velocityY, timeThen = 0) { + if (!velocityX && !velocityY) { + onscrollend(); + return; + } + + const timeNow = Date.now(); + + if (!timeThen) { + scrollAnimationFrame = requestAnimationFrame( + scrollAnimation.bind(null, velocityX, velocityY, timeNow), + ); + return; + } + + const timeElapsed = timeNow - timeThen; + const FRICTION = SCROLL_SPEED[scrollSpeed]; + const nextX = velocityX * timeElapsed; + const nextY = velocityY * timeElapsed; + + let scrollX = Number.parseInt(nextX * 100) / 100; + let scrollY = Number.parseInt(nextY * 100) / 100; + + const [canScrollX, canScrollY] = testScroll(scrollX, scrollY); + + if (!canScrollX) { + velocityX = 0; + scrollX = 0; + } + + if (!canScrollY) { + velocityY = 0; + scrollY = 0; + } + + if (!scrollX && !scrollY) { + cancelAnimationFrame(scrollAnimationFrame); + return; + } + + scroll(scrollX, scrollY); + + velocityX *= FRICTION; + velocityY *= FRICTION; + + scrollAnimationFrame = requestAnimationFrame( + scrollAnimation.bind(null, velocityX, velocityY, timeNow), + ); + } + + /** + * Test if scrolling is possible + * @param {number} moveX move in x direction + * @param {number} moveY move in y direction + * @returns {[number, number]} + */ + function testScroll(moveX, moveY) { + const UP = reverseScrolling ? "down" : "up"; + const DOWN = reverseScrolling ? "up" : "down"; + const LEFT = reverseScrolling ? "right" : "left"; + const RIGHT = reverseScrolling ? "left" : "right"; + + const vDirection = moveY > 0 ? DOWN : UP; + const hDirection = moveX > 0 ? RIGHT : LEFT; + + const { getEditorHeight, getEditorWidth } = editorManager; + const scrollLeft = editor.renderer.getScrollLeft(); + const scrollTop = editor.renderer.getScrollTop(); + const [editorWidth, editorHeight] = [ + getEditorWidth(editor), + getEditorHeight(editor), + ]; + + if ( + (vDirection === "down" && scrollTop <= 0) || + (vDirection === "up" && scrollTop >= editorHeight) + ) { + moveY = 0; + } + + if ( + (hDirection === "right" && scrollLeft <= 0) || + (hDirection === "left" && scrollLeft >= editorWidth) + ) { + moveX = 0; + } + + return [moveX, moveY]; + } + + /** + * Scroll to given position + * @param {number} x + * @param {number} y + */ + function scroll(x, y) { + let direction = reverseScrolling ? 1 : -1; + let scrollX = direction * x; + let scrollY = direction * y; + + renderer.scrollBy(scrollX, scrollY); + } + + /** + * Remove all listeners + */ + function removeListeners() { + document.removeEventListener("touchmove", touchMove, config); + document.removeEventListener("touchend", touchEnd, config); + } + + /** + * Compare two ranges + * @param {AceAjax.Range} r1 + * @param {AceAjax.Range} r2 + * @returns {boolean} + */ + function compareRanges(r1, r2) { + return ( + r1.start.row === r2.start.row && + r1.start.column === r2.start.column && + r1.end.row === r2.end.row && + r1.end.column === r2.end.column + ); + } + + /** + * Moves cursor to given position + * @param {number} x + * @param {number} y + * @param {boolean} [shiftKey] + * @param {boolean} [ctrlKey] + */ + function moveCursorTo(x, y, shiftKey = false, ctrlKey = false) { + const pos = renderer.screenToTextCoordinates(x, y); + + hideTooltip(); + + if (shiftKey) { + const anchor = + editor.selection.getSelectionAnchor() || editor.getCursorPosition(); + editor.selection.setRange({ start: anchor, end: pos }); + selectionMode($end); + return; + } + + if (ctrlKey) { + const range = new Range(pos.row, pos.column, pos.row, pos.column); + const ranges = editor.selection.getAllRanges(); + const exists = ranges.some((r) => compareRanges(r, range)); + if (exists) { + editor.selection.clearSelection(); + ranges.splice(ranges.indexOf(exists), 1); + ranges.forEach((r) => editor.selection.addRange(r)); + return; + } + + editor.selection.addRange(range); + return; + } + + editor.selection.moveToPosition(pos); + } + + /** + * Shows teardrop + * @returns {void} + */ + function cursorMode() { + if ((!teardropSize || !editor.isFocused()) && !forceCursorMode) { + $cursor.remove(); + return; + } + + forceCursorMode = false; + clearTimeout($cursor.dataset.timeout); + clearSelectionMode(); + + const { pageX, pageY } = renderer.textToScreenCoordinates( + editor.getCursorPosition(), + ); + const { lineHeight } = renderer; + const actualHeight = lineHeight; + const [x, y] = relativePosition(pageX, pageY + actualHeight); + $cursor.style.left = `${x}px`; + $cursor.style.top = `${y}px`; + if (!$cursor.isConnected) $el.append($cursor); + $cursor.startHide(); + + editor.selection.on("changeCursor", clearCursorMode); + } + + /** + * Remove cursor mode + * @returns {void} + */ + function clearCursorMode() { + if (!$el.contains($cursor)) return; + if ($cursor.dataset.immortal === "true") return; + $cursor.remove(); + clearTimeout($cursor.dataset.timeout); + + editor.selection.off("changeCursor", clearCursorMode); + } + + /** + * Shows both teardrops + * @param {HTMLElement} $trigger + * @returns {void} + */ + function selectionMode($trigger) { + if (!teardropSize) return; + + clearCursorMode(); + selectionActive = true; + positionEnd(); + positionStart(); + if ($trigger) showMenu($trigger); + + setTimeout(() => { + editor.selection.on("changeSelection", clearSelectionMode); + editor.selection.on("changeCursor", clearSelectionMode); + }, 0); + } + + /** + * Positions the start teardrop + */ + function positionStart() { + const range = editor.getSelectionRange(); + const { pageX, pageY } = renderer.textToScreenCoordinates(range.start); + const { lineHeight } = renderer; + const [x, y] = relativePosition(pageX - teardropSize, pageY + lineHeight); + + $start.style.left = `${x}px`; + $start.style.top = `${y}px`; + + if (!$start.isConnected) $el.append($start); + } + + /** + * Positions the end teardrop + */ + function positionEnd() { + const range = editor.getSelectionRange(); + const { pageX, pageY } = renderer.textToScreenCoordinates(range.end); + const { lineHeight } = renderer; + const [x, y] = relativePosition(pageX, pageY + lineHeight); + + $end.style.left = `${x}px`; + $end.style.top = `${y}px`; + + if (!$end.isConnected) $el.append($end); + } + + /** + * Remove selection mode + * @param {Event} e Event + * @param {boolean} clearActive whether to clear selectionActive + * @returns {void} + */ + function clearSelectionMode(e, clearActive = true) { + const $els = [$start.dataset.immortal, $end.dataset.immortal]; + if ($els.includes("true")) return; + if ($el.contains($start)) $start.remove(); + if ($el.contains($end)) $end.remove(); + if (clearActive) { + selectionActive = false; + } + + editor.selection.off("changeSelection", clearSelectionMode); + editor.selection.off("changeCursor", clearSelectionMode); + } + + /** + * Shows the edit context menu + * @param {HTMLElement} [$trigger] A trigger element that triggered the menu, if not provided, menu will be shown at the current cursor position + */ + function showMenu($trigger) { + menuActive = true; + const rect = $trigger?.getBoundingClientRect(); + const { bottom, left } = rect; + const readOnly = editor.getReadOnly(); + const [x, y] = relativePosition(left, bottom); + if (readOnly) { + populateMenuItems("read-only"); + } else { + populateMenuItems(); + } + + $menu.style.left = `${x}px`; + $menu.style.top = `${y}px`; + + if (!$menu.isConnected) $el.parentElement.append($menu); + if ($trigger) positionMenu($trigger); + + editor.selection.on("changeCursor", hideMenu); + editor.selection.on("changeSelection", hideMenu); + } + + /** + * @param {boolean} clearActive whether to clear menuActive + * @returns {void} + */ + function hideMenu(clearActive = true) { + if (!$el.parentElement.contains($menu)) return; + $menu.remove(); + editor.selection.off("changeCursor", hideMenu); + editor.selection.off("changeSelection", hideMenu); + if (clearActive) menuActive = false; + } + + /** + * Populates the menu items + * @param {HTMLElement} $trigger + * @returns + */ + function positionMenu($trigger) { + const getProp = ($el, prop) => $el.getBoundingClientRect()[prop]; + const containerRight = getProp($el, "right"); + const containerLeft = getProp($el, "left"); + const containerBottom = getProp($el, "bottom"); + const { lineHeight } = editor.renderer; + const margin = 10; + + // if menu is positioned off screen horizontally from the right + const menuRight = getProp($menu, "right"); + if (menuRight + margin > containerRight) { + const menuLeft = getProp($menu, "left"); + const [x] = relativePosition( + menuLeft - Math.abs(menuRight - containerRight), + ); + $menu.style.left = `${x - margin}px`; + } + + // if menu is positioned off screen horizontally from the left + const menuLeft = getProp($menu, "left"); + if (menuLeft - margin < containerLeft) { + const [x] = relativePosition( + menuLeft + Math.abs(menuLeft - containerLeft), + ); + $menu.style.left = `${x + margin}px`; + } + + if (shrink()) return; + + // if menu is positioned off screen vertically from the bottom + const menuBottom = getProp($menu, "bottom"); + if (menuBottom > containerBottom) { + const range = editor.getSelectionRange(); + let pos; + + if ($trigger === $start) { + pos = range.start; + } else { + pos = range.end; + } + + const { pageY } = renderer.textToScreenCoordinates(pos); + const [, y] = relativePosition(null, pageY - lineHeight * 1.8); + $menu.style.top = `${y}px`; + } + + function shrink() { + const [left, right] = [getProp($menu, "left"), getProp($menu, "right")]; + const tooLeft = left < containerLeft; + const tooRight = right > containerRight; + if (tooLeft || tooRight) { + const { scale = 1 } = $menu.dataset; + $menu.dataset.scale = Number.parseFloat(scale - 0.1); + $menu.style.transform = `scale(${$menu.dataset.scale})`; + positionMenu($trigger); + return true; + } + return false; + } + } + + /** + * Handles teardrop + * @param {HTMLDivElement} $teardrop Teardrop element to handle + */ + function teardropHandler($teardrop) { + $activeTeardrop = $teardrop; + $activeTeardrop.dataset.immortal = true; + teardropDoesShowMenu = true; + teardropTouchEnded = false; + + if (mode === "cursor") { + const timeout = Number.parseInt($cursor.dataset.timeout, 10); + clearTimeout(timeout); + } + + timeTouchStart = Date.now(); + document.addEventListener("touchmove", teardropTouchMoveHandler, config); + document.addEventListener("touchend", teardropTouchEndHandler, config); + } + + /** + * Touch event handler for teardrop + * @param {Event} e + */ + function teardropTouchMoveHandler(e) { + const { clientX, clientY } = e.touches[0]; + const { lineHeight } = renderer; + const { start, end } = editor.selection.getRange(); + let y = clientY - lineHeight * 1.8; + let x = clientX; + + if (timeTouchStart) { + timeTouchStart = null; + + // Prevents accidental touchmove + if (diffX < threshold && diffY < threshold) return; + + const diffX = Math.abs(lastX - clientX); + const diffY = Math.abs(lastY - clientY); + const timeDiff = Date.now() - timeTouchStart; + + // Prevents accidental touchmove or highly sensitive touchmove + if (timeDiff < 50) return; + return; + } + + if ($activeTeardrop === $cursor) { + const { row, column } = renderer.screenToTextCoordinates(x, y); + editor.gotoLine(row + 1, column); + } else if ($activeTeardrop === $start) { + x = clientX + teardropSize; + + const { pageX, pageY } = renderer.textToScreenCoordinates(end); + if (pageY <= y) { + y = pageY; + } + + if (pageY <= y && pageX < x) { + x = pageX; + } + + let { row, column } = renderer.screenToTextCoordinates(x, y); + + if (column === end.column) { + --column; + } + + editor.selection.setSelectionAnchor(row, column); + positionEnd(); + } else { + const { pageX, pageY } = renderer.textToScreenCoordinates(start); + if (pageY >= y) { + y = pageY; + } + + if (pageY >= y && pageX > x) { + x = pageX; + } + + let { row, column } = renderer.screenToTextCoordinates(x, y); + + if (column === start.column) { + ++column; + } + + editor.selection.moveCursorToPosition({ row, column }); + positionStart(); + } + + clearTimeout(teardropMoveTimeout); + const parent = $el.getBoundingClientRect(); + let deltaX = 0; + if (clientY < parent.top) deltaX = -lineHeight; + if (clientY > parent.bottom) deltaX = lineHeight; + + if (deltaX) { + teardropMoveTimeout = setTimeout(() => { + const top = editor.session.getScrollTop(); + editor.session.setScrollTop(top + deltaX); + if (teardropTouchEnded) return; + teardropTouchMoveHandler(e); + }, 100); + } + + const [left, top] = relativePosition(clientX, clientY - lineHeight); + $activeTeardrop.style.left = `${left}px`; + $activeTeardrop.style.top = `${top}px`; + } + + /** + * Touch event handler for teardrop + */ + function teardropTouchEndHandler() { + teardropTouchEnded = true; + if ($activeTeardrop === $cursor) { + cursorMode(); + } else { + selectionMode($activeTeardrop); + } + + $activeTeardrop.dataset.immortal = false; + document.removeEventListener("touchmove", teardropTouchMoveHandler, config); + document.removeEventListener("touchend", teardropTouchEndHandler, config); + if (teardropDoesShowMenu) { + showMenu($activeTeardrop); + } + editor.focus(); + } + + /** + * Editor container on scroll + */ + function onscroll() { + clearTimeout(scrollTimeout); + clearCursorMode(); + clearSelectionMode(null, false); + hideMenu(false); + + hideTooltip(); + scrollTimeout = setTimeout(onscrollend, 100); + } + + /** + * Hides tooltip in the gutter + */ + function hideTooltip() { + $gutter.dispatchEvent(new MouseEvent("mouseout")); + } + + /** + * Editor container on scroll end + */ + function onscrollend() { + scrollTimeout = null; + editor._emit("scroll-end"); + if (!touchEnded) return; + + if (selectionActive) { + selectionMode(); + } + + if (menuActive) { + showMenu($end); + } + } + + /** + * Editor container on update + */ + function onupdate() { + clearSelectionMode(); + clearCursorMode(); + hideMenu(); + } + + /** + * Editor container on change session + */ + function onchangesession() { + if (scrollTimeout) { + clearTimeout(scrollTimeout); + onscrollend(); + } + + cancelAnimationFrame(scrollAnimationFrame); + setTimeout(() => { + const copyText = editor.session.getTextRange(editor.getSelectionRange()); + if (copyText) { + selectionMode($end); + return; + } + + clearSelectionMode(); + cursorMode(); + hideMenu(); + }, 0); + } + + /** + * Editor container on fold + */ + function onfold() { + if (selectionActive) { + positionEnd(); + positionStart(); + hideMenu(); + showMenu($end); + } else { + clearCursorMode(); + } + } + + /** + * Populates the menu items + * @param {'regular'|'read-only'|'select'} mode + */ + function populateMenuItems(mode = "regular") { + $menu.innerHTML = ""; + const copyText = editor.getCopyText(); + const items = []; + + selectionMenu().forEach((item) => { + if (mode === "read-only" && !item.readOnly) return; + if (copyText && !["selected", "all"].includes(item.mode)) return; + if (!copyText && item.mode === "selected") return; + + items.push(item); + }); + + items.forEach(({ onclick, text }) => { + $menu.append(
{text}
); + }); + } + + /** + * Returns relative position of given coordinates + * @param {number} x x coordinate + * @param {number} y y coordinate + * @returns {[number, number]} + */ + function relativePosition(x, y) { + const { top, left } = $el.getBoundingClientRect(); + return [x - left, y - top]; + } } diff --git a/src/components/WebComponents/index.js b/src/components/WebComponents/index.js index c03324993..77c2e513a 100644 --- a/src/components/WebComponents/index.js +++ b/src/components/WebComponents/index.js @@ -1,4 +1,4 @@ -import '@ungap/custom-elements'; -import WcPage from './wcPage'; +import "@ungap/custom-elements"; +import WcPage from "./wcPage"; -customElements.define('wc-page', WcPage); +customElements.define("wc-page", WcPage); diff --git a/src/components/WebComponents/wcPage.js b/src/components/WebComponents/wcPage.js index 5346473b2..fa4e2cc71 100644 --- a/src/components/WebComponents/wcPage.js +++ b/src/components/WebComponents/wcPage.js @@ -1,272 +1,272 @@ -import tile from '../tile'; +import tile from "../tile"; export default class WCPage extends HTMLElement { - #leadBtn; - #header; - #on = { - hide: [], - show: [], - willconnect: [], - willdisconnect: [], - }; - #append; - handler; - onhide; - onconnect; - ondisconnect; - onwillconnect; - onwilldisconnect; - - constructor() { - super(); - const title = this.getAttribute('data-title'); - - this.handler = new PageHandler(this); - this.#append = super.append.bind(this); - this.append = this.appendBody.bind(this); - this.hide = this.hide.bind(this); - this.settitle = this.settitle.bind(this); - this.on = this.on.bind(this); - this.off = this.off.bind(this); - - this.handler.onReplace = () => { - if (typeof this.onwilldisconnect === 'function') { - this.onwilldisconnect(); - } - - this.#on.willdisconnect.forEach(cb => cb.call(this)); - }; - - this.handler.onRestore = () => { - if (typeof this.onwillconnect === 'function') { - this.onwillconnect(); - } - - this.#on.willconnect.forEach(cb => cb.call(this)); - }; - - this.#leadBtn = this.hide.call(this)} - attr-action='go-back' - >; - - this.#header = tile({ - type: 'header', - text: title || 'Page', - lead: this.#leadBtn, - }); - } - - appendBody(...$els) { - let $main = this.body; - if (!$main) return; - for (const $el of $els) { - $main.append($el); - } - } - - appendOuter(...$els) { - this.#append(...$els); - } - - attributeChangedCallback(name, oldValue, newValue) { - if (name === 'data-title') { - this.settitle = newValue; - } - } - - connectedCallback() { - this.classList.remove('hide'); - if (typeof this.onconnect === 'function') this.onconnect(); - this.#on.show.forEach(cb => cb.call(this)); - } - - disconnectedCallback() { - if (typeof this.ondisconnect === 'function') this.ondisconnect(); - this.#on.hide.forEach(cb => cb.call(this)); - } - - /** - * Adds event listener to the page - * @param {'hide' | 'show'} event - * @param {function(this: WCPage):void} cb - */ - on(event, cb) { - if (event in this.#on) { - this.#on[event].push(cb); - } - } - - /** - * Removes event listener from the page - * @param {'hide' | 'show'} event - * @param {function(this: WCPage):void} cb - */ - off(event, cb) { - if (event in this.#on) { - this.#on[event] = this.#on[event].filter(fn => fn !== cb); - } - } - - /** - * Sets the title of the page - * @param {string} title - */ - settitle(title) { - this.header.text = title; - } - - hide() { - this.classList.add('hide'); - if (typeof this.onhide === 'function') this.onhide(); - setTimeout(() => { - this.remove(); - this.handler.remove(); - }, 150); - } - - get body() { - return this.get('.main') || this.get('main'); - } - - set body($el) { - if (this.body) this.replaceChild($el, this.body); - - const headerAdjacent = this.header.nextElementSibling; - if (headerAdjacent) { - this.insertBefore($el, headerAdjacent); - return; - } - - this.appendChild($el); - } - - get innerHTML() { - return this.body?.innerHTML; - } - - set innerHTML(html) { - if (this.body) this.body.innerHTML = html; - } - - get textContent() { - return this.body?.textContent; - } - - set textContent(text) { - if (this.body) this.body.textContent = text; - } - - get lead() { - return this.#leadBtn; - } - - set lead($el) { - this.header.replaceChild($el, this.#leadBtn); - this.#leadBtn = $el; - } - - get header() { - return this.#header; - } - - set header($el) { - this.#header.replaceChild($el, this.#header); - this.#header = $el; - } - - initializeIfNotAlreadyInitialized() { - if (!this.#header.isConnected) { - this.#addHeaderOrAssignHeader(); - } - } - - #addHeaderOrAssignHeader() { - if (!this.classList.contains('primary')) { - this.#append(this.#header); - this.#append(
); - } else { - this.#header = this.get('header'); - if (this.#header) { - this.#leadBtn = this.#header.firstChild; - } - } - } + #leadBtn; + #header; + #on = { + hide: [], + show: [], + willconnect: [], + willdisconnect: [], + }; + #append; + handler; + onhide; + onconnect; + ondisconnect; + onwillconnect; + onwilldisconnect; + + constructor() { + super(); + const title = this.getAttribute("data-title"); + + this.handler = new PageHandler(this); + this.#append = super.append.bind(this); + this.append = this.appendBody.bind(this); + this.hide = this.hide.bind(this); + this.settitle = this.settitle.bind(this); + this.on = this.on.bind(this); + this.off = this.off.bind(this); + + this.handler.onReplace = () => { + if (typeof this.onwilldisconnect === "function") { + this.onwilldisconnect(); + } + + this.#on.willdisconnect.forEach((cb) => cb.call(this)); + }; + + this.handler.onRestore = () => { + if (typeof this.onwillconnect === "function") { + this.onwillconnect(); + } + + this.#on.willconnect.forEach((cb) => cb.call(this)); + }; + + this.#leadBtn = ( + this.hide.call(this)} + attr-action="go-back" + > + ); + + this.#header = tile({ + type: "header", + text: title || "Page", + lead: this.#leadBtn, + }); + } + + appendBody(...$els) { + let $main = this.body; + if (!$main) return; + for (const $el of $els) { + $main.append($el); + } + } + + appendOuter(...$els) { + this.#append(...$els); + } + + attributeChangedCallback(name, oldValue, newValue) { + if (name === "data-title") { + this.settitle = newValue; + } + } + + connectedCallback() { + this.classList.remove("hide"); + if (typeof this.onconnect === "function") this.onconnect(); + this.#on.show.forEach((cb) => cb.call(this)); + } + + disconnectedCallback() { + if (typeof this.ondisconnect === "function") this.ondisconnect(); + this.#on.hide.forEach((cb) => cb.call(this)); + } + + /** + * Adds event listener to the page + * @param {'hide' | 'show'} event + * @param {function(this: WCPage):void} cb + */ + on(event, cb) { + if (event in this.#on) { + this.#on[event].push(cb); + } + } + + /** + * Removes event listener from the page + * @param {'hide' | 'show'} event + * @param {function(this: WCPage):void} cb + */ + off(event, cb) { + if (event in this.#on) { + this.#on[event] = this.#on[event].filter((fn) => fn !== cb); + } + } + + /** + * Sets the title of the page + * @param {string} title + */ + settitle(title) { + this.header.text = title; + } + + hide() { + this.classList.add("hide"); + if (typeof this.onhide === "function") this.onhide(); + setTimeout(() => { + this.remove(); + this.handler.remove(); + }, 150); + } + + get body() { + return this.get(".main") || this.get("main"); + } + + set body($el) { + if (this.body) this.replaceChild($el, this.body); + + const headerAdjacent = this.header.nextElementSibling; + if (headerAdjacent) { + this.insertBefore($el, headerAdjacent); + return; + } + + this.appendChild($el); + } + + get innerHTML() { + return this.body?.innerHTML; + } + + set innerHTML(html) { + if (this.body) this.body.innerHTML = html; + } + + get textContent() { + return this.body?.textContent; + } + + set textContent(text) { + if (this.body) this.body.textContent = text; + } + + get lead() { + return this.#leadBtn; + } + + set lead($el) { + this.header.replaceChild($el, this.#leadBtn); + this.#leadBtn = $el; + } + + get header() { + return this.#header; + } + + set header($el) { + this.#header.replaceChild($el, this.#header); + this.#header = $el; + } + + initializeIfNotAlreadyInitialized() { + if (!this.#header.isConnected) { + this.#addHeaderOrAssignHeader(); + } + } + + #addHeaderOrAssignHeader() { + if (!this.classList.contains("primary")) { + this.#append(this.#header); + this.#append(
); + } else { + this.#header = this.get("header"); + if (this.#header) { + this.#leadBtn = this.#header.firstChild; + } + } + } } class PageHandler { - $el; - $replacement; - onRestore; - onReplace; - - /** - * - * @param {HTMLElement} $el - */ - constructor($el) { - this.$el = $el; - - this.onhide = this.onhide.bind(this); - this.onshow = this.onshow.bind(this); - - this.$replacement = ; - this.$replacement.handler = this; - - this.$el.on('hide', this.onhide); - this.$el.on('show', this.onshow); - } - - /** - * Replace current element with a replacement element - */ - replaceEl() { - this.$el.off('hide', this.onhide); - if (!this.$el.isConnected || this.$replacement.isConnected) return; - if (typeof this.onReplace === 'function') this.onReplace(); - this.$el.parentElement.replaceChild(this.$replacement, this.$el); - this.$el.classList.add('no-transition'); - } - - /** - * Restore current element from a replacement element - */ - restoreEl() { - if (this.$el.isConnected || !this.$replacement.isConnected) return; - if (typeof this.onRestore === 'function') this.onRestore(); - this.$el.off('hide', this.onhide); - this.$replacement.parentElement.replaceChild(this.$el, this.$replacement); - this.$el.on('hide', this.onhide); - } - - onhide() { - this.$el.off('hide', this.onhide); - handlePagesForSmoothExprienceBack(); - } - - onshow() { - this.$el.off('show', this.onshow); - handlePagesForSmoothExprience(); - } - - remove() { - this.$replacement.remove(); - } + $el; + $replacement; + onRestore; + onReplace; + + /** + * + * @param {HTMLElement} $el + */ + constructor($el) { + this.$el = $el; + + this.onhide = this.onhide.bind(this); + this.onshow = this.onshow.bind(this); + + this.$replacement = ; + this.$replacement.handler = this; + + this.$el.on("hide", this.onhide); + this.$el.on("show", this.onshow); + } + + /** + * Replace current element with a replacement element + */ + replaceEl() { + this.$el.off("hide", this.onhide); + if (!this.$el.isConnected || this.$replacement.isConnected) return; + if (typeof this.onReplace === "function") this.onReplace(); + this.$el.parentElement.replaceChild(this.$replacement, this.$el); + this.$el.classList.add("no-transition"); + } + + /** + * Restore current element from a replacement element + */ + restoreEl() { + if (this.$el.isConnected || !this.$replacement.isConnected) return; + if (typeof this.onRestore === "function") this.onRestore(); + this.$el.off("hide", this.onhide); + this.$replacement.parentElement.replaceChild(this.$el, this.$replacement); + this.$el.on("hide", this.onhide); + } + + onhide() { + this.$el.off("hide", this.onhide); + handlePagesForSmoothExprienceBack(); + } + + onshow() { + this.$el.off("show", this.onshow); + handlePagesForSmoothExprience(); + } + + remove() { + this.$replacement.remove(); + } } /** * Remove invisible pages from DOM and add them to the stack */ function handlePagesForSmoothExprience() { - const $pages = [...tag.getAll('wc-page')]; - for (let $page of $pages.slice(0, -1)) { - $page.handler.replaceEl(); - } + const $pages = [...tag.getAll("wc-page")]; + for (let $page of $pages.slice(0, -1)) { + $page.handler.replaceEl(); + } } function handlePagesForSmoothExprienceBack() { - [ - ...tag.getAll('.page-replacement') - ].pop()?.handler.restoreEl(); + [...tag.getAll(".page-replacement")].pop()?.handler.restoreEl(); } diff --git a/src/components/checkbox/index.js b/src/components/checkbox/index.js index 147eb33e1..c473b0ea0 100644 --- a/src/components/checkbox/index.js +++ b/src/components/checkbox/index.js @@ -1,5 +1,5 @@ -import './styles.scss'; -import Ref from 'html-tag-js/ref'; +import "./styles.scss"; +import Ref from "html-tag-js/ref"; /** * @typedef {Object} Checkbox @@ -24,61 +24,68 @@ import Ref from 'html-tag-js/ref'; * @returns {Checkbox} */ function Checkbox(text, checked, name, id, type, ref, size) { - if (typeof text === 'object') { - ({ text, checked, name, id, type, ref, size } = text); - } + if (typeof text === "object") { + ({ text, checked, name, id, type, ref, size } = text); + } - size = size || '1rem'; + size = size || "1rem"; - const $input = ref || new Ref(); - const $checkbox = ; + const $input = ref || new Ref(); + const $checkbox = ( + + ); + Object.defineProperties($checkbox, { + checked: { + get() { + return !!$input.el.checked; + }, + set(value) { + $input.el.checked = value; + }, + }, + onclick: { + get() { + return $input.el.onclick; + }, + set(onclick) { + $input.el.onclick = onclick; + }, + }, + onchange: { + get() { + return $input.el.onchange; + }, + set(onchange) { + $input.el.onchange = onchange; + }, + }, + value: { + get() { + return this.checked; + }, + set(value) { + this.checked = value; + }, + }, + toggle: { + value() { + this.checked = !this.checked; + }, + }, + }); - Object.defineProperties($checkbox, { - checked: { - get() { - return !!$input.el.checked; - }, - set(value) { - $input.el.checked = value; - }, - }, - onclick: { - get() { - return $input.el.onclick; - }, - set(onclick) { - $input.el.onclick = onclick; - }, - }, - onchange: { - get() { - return $input.el.onchange; - }, - set(onchange) { - $input.el.onchange = onchange; - }, - }, - value: { - get() { - return this.checked; - }, - set(value) { - this.checked = value; - }, - }, - toggle: { - value() { - this.checked = !this.checked; - } - } - }); - - return $checkbox; + return $checkbox; } export default Checkbox; diff --git a/src/components/collapsableList.js b/src/components/collapsableList.js index 7e4cc3415..db3af4258 100644 --- a/src/components/collapsableList.js +++ b/src/components/collapsableList.js @@ -1,5 +1,5 @@ -import tag from 'html-tag-js'; -import tile from './tile'; +import tag from "html-tag-js"; +import tile from "./tile"; /** * @typedef {object} CollapsibleBase @@ -28,122 +28,126 @@ import tile from './tile'; * @param {function(this:Collapsible):void} [options.ontoggle] Called when the list is toggled * @returns {Collapsible} */ -export default function collapsableList(titleText, type = 'indicator', options = {}) { - let onscroll = null; - const $ul = tag('ul', { - className: 'scroll', - onscroll: onUlScroll, - }); - const $collapseIndicator = tag('span', { - className: `icon ${type}`, - }); - const $title = tile({ - lead: $collapseIndicator, - type: 'div', - text: options.allCaps ? titleText.toUpperCase() : titleText, - tail: options.tail, - }); - const $mainWrapper = tag(options.type || 'div', { - className: 'list collapsible hidden', - children: [$title, $ul], - }); +export default function collapsableList( + titleText, + type = "indicator", + options = {}, +) { + let onscroll = null; + const $ul = tag("ul", { + className: "scroll", + onscroll: onUlScroll, + }); + const $collapseIndicator = tag("span", { + className: `icon ${type}`, + }); + const $title = tile({ + lead: $collapseIndicator, + type: "div", + text: options.allCaps ? titleText.toUpperCase() : titleText, + tail: options.tail, + }); + const $mainWrapper = tag(options.type || "div", { + className: "list collapsible hidden", + children: [$title, $ul], + }); - let collapse = () => { - $mainWrapper.classList.add('hidden'); - if ($mainWrapper.ontoggle) $mainWrapper.ontoggle.call($mainWrapper); - delete $ul.dataset.scrollTop; - }; + let collapse = () => { + $mainWrapper.classList.add("hidden"); + if ($mainWrapper.ontoggle) $mainWrapper.ontoggle.call($mainWrapper); + delete $ul.dataset.scrollTop; + }; - let expand = () => { - $mainWrapper.classList.remove('hidden'); - if ($mainWrapper.ontoggle) $mainWrapper.ontoggle.call($mainWrapper); - }; + let expand = () => { + $mainWrapper.classList.remove("hidden"); + if ($mainWrapper.ontoggle) $mainWrapper.ontoggle.call($mainWrapper); + }; - $title.classList.add('light'); - $title.addEventListener('click', toggle); + $title.classList.add("light"); + $title.addEventListener("click", toggle); - [$title, $mainWrapper].forEach(defineProperties); + [$title, $mainWrapper].forEach(defineProperties); - return $mainWrapper; + return $mainWrapper; - function onUlScroll() { - if (onscroll) onscroll.call($ul); - $ul.dataset.scrollTop = $ul.scrollTop; - } + function onUlScroll() { + if (onscroll) onscroll.call($ul); + $ul.dataset.scrollTop = $ul.scrollTop; + } - function toggle() { - if ($title.collapsed) { - expand(); - } else { - collapse(); - } - } + function toggle() { + if ($title.collapsed) { + expand(); + } else { + collapse(); + } + } - function defineProperties($el) { - Object.defineProperties($el, { - $title: { - get() { - return $title; - }, - }, - $ul: { - get() { - return $ul; - }, - }, - ontoggle: { - get() { - return options.ontoggle; - }, - set(fun) { - if (typeof fun === 'function') options.ontoggle = fun; - }, - }, - collapse: { - get() { - return collapse || (() => { }); - }, - set(fun) { - if (typeof fun === 'function') collapse = fun; - }, - }, - expand: { - get() { - return expand || (() => { }); - }, - set(fun) { - if (typeof fun === 'function') expand = fun; - }, - }, - collapsed: { - get() { - return $mainWrapper.classList.contains('hidden'); - } - }, - unclasped: { - get() { - return !this.collapsed; - } - }, - onscroll: { - get() { - return onscroll; - }, - set(fun) { - if (typeof fun === 'function') { - onscroll = fun; - } - } - }, - scrollTop: { - get() { - return $ul.dataset.scrollTop || 0; - }, - set(val) { - $ul.dataset.scrollTop = val; - $ul.scrollTop = val; - } - } - }); - } + function defineProperties($el) { + Object.defineProperties($el, { + $title: { + get() { + return $title; + }, + }, + $ul: { + get() { + return $ul; + }, + }, + ontoggle: { + get() { + return options.ontoggle; + }, + set(fun) { + if (typeof fun === "function") options.ontoggle = fun; + }, + }, + collapse: { + get() { + return collapse || (() => {}); + }, + set(fun) { + if (typeof fun === "function") collapse = fun; + }, + }, + expand: { + get() { + return expand || (() => {}); + }, + set(fun) { + if (typeof fun === "function") expand = fun; + }, + }, + collapsed: { + get() { + return $mainWrapper.classList.contains("hidden"); + }, + }, + unclasped: { + get() { + return !this.collapsed; + }, + }, + onscroll: { + get() { + return onscroll; + }, + set(fun) { + if (typeof fun === "function") { + onscroll = fun; + } + }, + }, + scrollTop: { + get() { + return $ul.dataset.scrollTop || 0; + }, + set(val) { + $ul.dataset.scrollTop = val; + $ul.scrollTop = val; + }, + }, + }); + } } diff --git a/src/components/contextmenu/index.js b/src/components/contextmenu/index.js index 2727fe0f1..025977f2f 100644 --- a/src/components/contextmenu/index.js +++ b/src/components/contextmenu/index.js @@ -1,5 +1,5 @@ -import './style.scss'; -import actionStack from 'lib/actionStack'; +import "./style.scss"; +import actionStack from "lib/actionStack"; /** * @typedef {object} ContextMenuObj @@ -32,115 +32,113 @@ import actionStack from 'lib/actionStack'; * @returns {ContextMenuObj} */ export default function Contextmenu(content, options) { - if (!options && typeof content === 'object') { - options = content; - content = null; - } else if (!options) { - options = {}; - } - - const $el = tag('ul', { - className: 'context-menu scroll', - innerHTML: content || '', - onclick(e) { - if (options.onclick) options.onclick.call(this, e); - if (options.onselect) { - const $target = e.target; - const { action } = $target.dataset; - if (!action) return; - hide(); - options.onselect.call(this, action); - } - }, - style: { - top: options.top || 'auto', - left: options.left || 'auto', - right: options.right || 'auto', - bottom: options.bottom || 'auto', - transformOrigin: options.transformOrigin, - }, - }); - const $mask = tag('span', { - className: 'mask', - ontouchstart: hide, - onmousedown: hide, - }); - - if (Array.isArray(options.items)) { - options.items.forEach(([text, action]) => { - $el.append( -
  • {text}
  • - ); - }); - } - - if (!options.innerHTML) addTabindex(); - - function show() { - actionStack.push({ - id: 'main-menu', - action: hide, - }); - $el.onshow(); - $el.classList.remove('hide'); - - if (options.innerHTML) { - $el.innerHTML = options.innerHTML.call($el); - addTabindex(); - } - - if (options.toggler) { - const client = options.toggler.getBoundingClientRect(); - if (!options.top && !options.bottom) { - $el.style.top = client.top + 'px'; - } - if (!options.left && !options.right) { - $el.style.right = innerWidth - client.right + 'px'; - } - } - - app.append($el, $mask); - - const $firstChild = $el.firstChild; - if ($firstChild && $firstChild.focus) $firstChild.focus(); - } - - function hide() { - actionStack.remove('main-menu'); - $el.onhide(); - $el.classList.add('hide'); - setTimeout(() => { - $mask.remove(); - $el.remove(); - }, 100); - } - - function toggle() { - if ($el.parentElement) return hide(); - show(); - } - - function addTabindex() { - /**@type {Array} */ - const children = [...$el.children]; - for (let $el of children) $el.tabIndex = '0'; - } - - function destroy() { - $el.remove(); - $mask.remove(); - options.toggler?.removeEventListener('click', toggle); - } - - if (options.toggler) { - options.toggler.addEventListener('click', toggle); - } - - $el.hide = hide; - $el.show = show; - $el.destroy = destroy; - $el.onshow = options.onshow || (() => { }); - $el.onhide = options.onhide || (() => { }); - - return $el; + if (!options && typeof content === "object") { + options = content; + content = null; + } else if (!options) { + options = {}; + } + + const $el = tag("ul", { + className: "context-menu scroll", + innerHTML: content || "", + onclick(e) { + if (options.onclick) options.onclick.call(this, e); + if (options.onselect) { + const $target = e.target; + const { action } = $target.dataset; + if (!action) return; + hide(); + options.onselect.call(this, action); + } + }, + style: { + top: options.top || "auto", + left: options.left || "auto", + right: options.right || "auto", + bottom: options.bottom || "auto", + transformOrigin: options.transformOrigin, + }, + }); + const $mask = tag("span", { + className: "mask", + ontouchstart: hide, + onmousedown: hide, + }); + + if (Array.isArray(options.items)) { + options.items.forEach(([text, action]) => { + $el.append(
  • {text}
  • ); + }); + } + + if (!options.innerHTML) addTabindex(); + + function show() { + actionStack.push({ + id: "main-menu", + action: hide, + }); + $el.onshow(); + $el.classList.remove("hide"); + + if (options.innerHTML) { + $el.innerHTML = options.innerHTML.call($el); + addTabindex(); + } + + if (options.toggler) { + const client = options.toggler.getBoundingClientRect(); + if (!options.top && !options.bottom) { + $el.style.top = client.top + "px"; + } + if (!options.left && !options.right) { + $el.style.right = innerWidth - client.right + "px"; + } + } + + app.append($el, $mask); + + const $firstChild = $el.firstChild; + if ($firstChild && $firstChild.focus) $firstChild.focus(); + } + + function hide() { + actionStack.remove("main-menu"); + $el.onhide(); + $el.classList.add("hide"); + setTimeout(() => { + $mask.remove(); + $el.remove(); + }, 100); + } + + function toggle() { + if ($el.parentElement) return hide(); + show(); + } + + function addTabindex() { + /**@type {Array} */ + const children = [...$el.children]; + for (let $el of children) $el.tabIndex = "0"; + } + + function destroy() { + $el.remove(); + $mask.remove(); + options.toggler?.removeEventListener("click", toggle); + } + + if (options.toggler) { + options.toggler.addEventListener("click", toggle); + } + + $el.hide = hide; + $el.show = show; + $el.destroy = destroy; + $el.onshow = options.onshow || (() => {}); + $el.onhide = options.onhide || (() => {}); + + return $el; } diff --git a/src/components/inputhints/index.js b/src/components/inputhints/index.js index 4f7ab7f39..08d15a45d 100644 --- a/src/components/inputhints/index.js +++ b/src/components/inputhints/index.js @@ -1,15 +1,14 @@ -import './style.scss'; - +import "./style.scss"; /** * @typedef {Object} HintObj * @property {string} value * @property {string} text -*/ + */ /** * @typedef {HintObj|string} Hint -*/ + */ /** * @typedef {Object} HintModification @@ -20,8 +19,7 @@ import './style.scss'; /** * @typedef {(setHints:(hints:Array)=>void, modification: HintModification) => void} HintCallback -*/ - + */ /** * Generate a list of hints for an input field @@ -29,303 +27,305 @@ import './style.scss'; * @param {Array|HintCallback} hints Hints or a callback to generate hints * @param {(value: string) => void} onSelect Callback to call when a hint is selected * @returns {{getSelected: ()=>HTMLLIElement, container: HTMLUListElement}} -*/ + */ export default function inputhints($input, hints, onSelect) { - /**@type {HTMLUListElement} */ - const $ul =
      ; - const LIMIT = 100; - - let preventUpdate = false; - let updateUlTimeout; - let pages = 0; - let currentHints = []; - - $input.addEventListener('focus', onfocus); - - if (typeof hints === 'function') { - const cb = hints; - hints = []; - $ul.content = []; - cb(setHints, hintModification()); - } else { - setHints(hints); - } - - /** - * Retain the focus on the input field - */ - function handleMouseDown() { - preventUpdate = true; - } - - function handleMouseUp() { - $input.focus(); - preventUpdate = false; - } - - /** - * Handle click event - * @param {MouseEvent} e Event - */ - function handleClick(e) { - const $el = e.target; - const action = $el.getAttribute('action'); - if (action !== 'hint') return; - const value = $el.getAttribute('value'); - if (!value) return; - $input.value = $el.textContent; - if (onSelect) onSelect(value); - preventUpdate = false; - onblur(); - } - - /** - * Handle keypress event - * @param {KeyboardEvent} e Event - */ - function handleKeypress(e) { - if (e.key !== 'Enter') return; - - e.preventDefault(); - e.stopPropagation(); - const activeHint = $ul.get('.active'); - if (!activeHint) return; - const value = activeHint.getAttribute('value'); - if (onSelect) onSelect(value); - else $input.value = value; - } - - /** - * Handle keydown event - * @param {KeyboardEvent} e Event - */ - function handleKeydown(e) { - const code = e.key; - if (code === 'ArrowUp' || code === 'ArrowDown') { - e.preventDefault(); - e.stopPropagation(); - } - updateHintFocus(code); - } - - /** - * Moves the active hint up or down - * @param {"ArrowDown" | "ArrowUp"} key Direction to move - */ - function updateHintFocus(key) { - let nextHint; - let activeHint = $ul.get('.active'); - if (!activeHint) activeHint = $ul.firstChild; - - if (key === 'ArrowDown') { - nextHint = activeHint.nextElementSibling; - if (!nextHint) nextHint = $ul.firstElementChild; - } else if (key === 'ArrowUp') { - nextHint = activeHint.previousElementSibling; - if (!nextHint) nextHint = $ul.lastElementChild; - } - - if (nextHint) { - activeHint.classList.remove('active'); - nextHint.classList.add('active'); - nextHint.scrollIntoView(); - } - } - - /** - * @this {HTMLInputElement} - */ - function oninput() { - const { value: toTest } = this; - const matched = []; - const regexp = new RegExp(toTest, 'i'); - hints.forEach((hint) => { - const { value, text } = hint; - if ( - regexp.test(value) - || regexp.test(text) - ) { - matched.push(hint); - } - }); - updateUl(matched); - } - - function onfocus() { - if (preventUpdate) return; - - $input.addEventListener('keypress', handleKeypress); - $input.addEventListener('keydown', handleKeydown); - $input.addEventListener('blur', onblur); - $input.addEventListener('input', oninput); - window.addEventListener('resize', position); - ulAddEventListeners(); - app.append($ul); - position(); - } - - /** - * Event listener for blur - * @returns - */ - function onblur() { - if (preventUpdate) return; - - clearTimeout(updateUlTimeout); - $input.removeEventListener('keypress', handleKeypress); - $input.removeEventListener('keydown', handleKeydown); - $input.removeEventListener('blur', onblur); - $input.removeEventListener('input', oninput); - window.removeEventListener('resize', position); - ulRemoveEventListeners(); - $ul.remove(); - } - - /** - * Update the position of the hint list - * @param {boolean} append Append the list to the body or not - */ - function position() { - const activeHint = $ul.get('.active'); - const { firstElementChild } = $ul; - if (!activeHint && firstElementChild) firstElementChild.classList.add('active'); - const client = $input.getBoundingClientRect(); - const inputTop = client.top - 5; - const inputBottom = client.bottom + 5; - const inputLeft = client.left; - const bottomHeight = window.innerHeight - inputBottom; - const mid = window.innerHeight / 2; - - if (bottomHeight >= mid) { - $ul.classList.remove('bottom'); - $ul.style.top = `${inputBottom}px`; - $ul.style.bottom = 'auto'; - } else { - $ul.classList.add('bottom'); - $ul.style.top = 'auto'; - $ul.style.bottom = `${inputTop}px`; - } - - $ul.style.left = `${inputLeft}px`; - $ul.style.width = `${client.width}px`; - } - - /** - * Set hint items - * @param {Array} list Hint items - */ - function setHints(list) { - if (Array.isArray(list)) { - hints = list; - } else { - hints = []; - } - updateUl(hints); - $ul.classList.remove('loading'); - } - - function hintModification() { - return { - add(item, index) { - if (index) { - hints.splice(index, 0, item); - const child = $ul.children[index]; - if (child) { - $ul.insertBefore(child, $ul.children[index]); - } - return; - } - - hints.push(item); - }, - remove(item) { - const index = hints.indexOf(item); - if (index > -1) { - hints.splice(index, 1); - } - }, - removeIndex(index) { - hints.splice(index, 1); - } - }; - } - - function ulAddEventListeners() { - window.addEventListener('resize', position); - $ul.addEventListener('click', handleClick); - $ul.addEventListener('mousedown', handleMouseDown); - $ul.addEventListener('mouseup', handleMouseUp); - $ul.addEventListener('touchstart', handleMouseDown); - $ul.addEventListener('touchend', handleMouseUp); - $ul.addEventListener('scroll', updatePage); - } - - function ulRemoveEventListeners() { - window.removeEventListener('resize', position); - $ul.removeEventListener('click', handleClick); - $ul.removeEventListener('mousedown', handleMouseDown); - $ul.removeEventListener('mouseup', handleMouseUp); - $ul.removeEventListener('touchstart', handleMouseDown); - $ul.removeEventListener('touchend', handleMouseUp); - $ul.removeEventListener('scroll', updatePage); - } - - function updatePage() { - // if the scroll is at the bottom - if ($ul.scrollTop + $ul.clientHeight >= $ul.scrollHeight) { - pages++; - updateUlNow(currentHints, pages); - } - } - - /** - * First time updates the hint instantly, then debounce - * @param {Array} hints - */ - function updateUl(hints) { - updateUlNow(hints); - updateUl = updateUlDebounce; - } - - /** - * Update the hint list after a delay - * @param {Array} hints - */ - function updateUlDebounce(hints) { - clearTimeout(updateUlTimeout); - updateUlTimeout = setTimeout(updateUlNow, 300, hints); - } - - /** - * Update the hint list instantly - * @param {Array} hints - * @param {number} page - */ - function updateUlNow(hints, page = 0) { - // render only first 500 hints - currentHints = hints; - const offset = page * LIMIT; - const end = offset + LIMIT; - const list = hints.slice(offset, end); - let scrollTop = $ul.scrollTop; - if (!list.length) return; - - $ul.remove(); - if (!page) { - scrollTop = 0; - $ul.content = list.map((hint) => ); - } else { - $ul.append(...list.map((hint) => )); - } - app.append($ul); - $ul.scrollTop = scrollTop; - position(); // Update the position of the new list - } - - return { - getSelected() { $ul.get('.active'); }, - get container() { return $ul; }, - }; + /**@type {HTMLUListElement} */ + const $ul =
        ; + const LIMIT = 100; + + let preventUpdate = false; + let updateUlTimeout; + let pages = 0; + let currentHints = []; + + $input.addEventListener("focus", onfocus); + + if (typeof hints === "function") { + const cb = hints; + hints = []; + $ul.content = []; + cb(setHints, hintModification()); + } else { + setHints(hints); + } + + /** + * Retain the focus on the input field + */ + function handleMouseDown() { + preventUpdate = true; + } + + function handleMouseUp() { + $input.focus(); + preventUpdate = false; + } + + /** + * Handle click event + * @param {MouseEvent} e Event + */ + function handleClick(e) { + const $el = e.target; + const action = $el.getAttribute("action"); + if (action !== "hint") return; + const value = $el.getAttribute("value"); + if (!value) return; + $input.value = $el.textContent; + if (onSelect) onSelect(value); + preventUpdate = false; + onblur(); + } + + /** + * Handle keypress event + * @param {KeyboardEvent} e Event + */ + function handleKeypress(e) { + if (e.key !== "Enter") return; + + e.preventDefault(); + e.stopPropagation(); + const activeHint = $ul.get(".active"); + if (!activeHint) return; + const value = activeHint.getAttribute("value"); + if (onSelect) onSelect(value); + else $input.value = value; + } + + /** + * Handle keydown event + * @param {KeyboardEvent} e Event + */ + function handleKeydown(e) { + const code = e.key; + if (code === "ArrowUp" || code === "ArrowDown") { + e.preventDefault(); + e.stopPropagation(); + } + updateHintFocus(code); + } + + /** + * Moves the active hint up or down + * @param {"ArrowDown" | "ArrowUp"} key Direction to move + */ + function updateHintFocus(key) { + let nextHint; + let activeHint = $ul.get(".active"); + if (!activeHint) activeHint = $ul.firstChild; + + if (key === "ArrowDown") { + nextHint = activeHint.nextElementSibling; + if (!nextHint) nextHint = $ul.firstElementChild; + } else if (key === "ArrowUp") { + nextHint = activeHint.previousElementSibling; + if (!nextHint) nextHint = $ul.lastElementChild; + } + + if (nextHint) { + activeHint.classList.remove("active"); + nextHint.classList.add("active"); + nextHint.scrollIntoView(); + } + } + + /** + * @this {HTMLInputElement} + */ + function oninput() { + const { value: toTest } = this; + const matched = []; + const regexp = new RegExp(toTest, "i"); + hints.forEach((hint) => { + const { value, text } = hint; + if (regexp.test(value) || regexp.test(text)) { + matched.push(hint); + } + }); + updateUl(matched); + } + + function onfocus() { + if (preventUpdate) return; + + $input.addEventListener("keypress", handleKeypress); + $input.addEventListener("keydown", handleKeydown); + $input.addEventListener("blur", onblur); + $input.addEventListener("input", oninput); + window.addEventListener("resize", position); + ulAddEventListeners(); + app.append($ul); + position(); + } + + /** + * Event listener for blur + * @returns + */ + function onblur() { + if (preventUpdate) return; + + clearTimeout(updateUlTimeout); + $input.removeEventListener("keypress", handleKeypress); + $input.removeEventListener("keydown", handleKeydown); + $input.removeEventListener("blur", onblur); + $input.removeEventListener("input", oninput); + window.removeEventListener("resize", position); + ulRemoveEventListeners(); + $ul.remove(); + } + + /** + * Update the position of the hint list + * @param {boolean} append Append the list to the body or not + */ + function position() { + const activeHint = $ul.get(".active"); + const { firstElementChild } = $ul; + if (!activeHint && firstElementChild) + firstElementChild.classList.add("active"); + const client = $input.getBoundingClientRect(); + const inputTop = client.top - 5; + const inputBottom = client.bottom + 5; + const inputLeft = client.left; + const bottomHeight = window.innerHeight - inputBottom; + const mid = window.innerHeight / 2; + + if (bottomHeight >= mid) { + $ul.classList.remove("bottom"); + $ul.style.top = `${inputBottom}px`; + $ul.style.bottom = "auto"; + } else { + $ul.classList.add("bottom"); + $ul.style.top = "auto"; + $ul.style.bottom = `${inputTop}px`; + } + + $ul.style.left = `${inputLeft}px`; + $ul.style.width = `${client.width}px`; + } + + /** + * Set hint items + * @param {Array} list Hint items + */ + function setHints(list) { + if (Array.isArray(list)) { + hints = list; + } else { + hints = []; + } + updateUl(hints); + $ul.classList.remove("loading"); + } + + function hintModification() { + return { + add(item, index) { + if (index) { + hints.splice(index, 0, item); + const child = $ul.children[index]; + if (child) { + $ul.insertBefore(child, $ul.children[index]); + } + return; + } + + hints.push(item); + }, + remove(item) { + const index = hints.indexOf(item); + if (index > -1) { + hints.splice(index, 1); + } + }, + removeIndex(index) { + hints.splice(index, 1); + }, + }; + } + + function ulAddEventListeners() { + window.addEventListener("resize", position); + $ul.addEventListener("click", handleClick); + $ul.addEventListener("mousedown", handleMouseDown); + $ul.addEventListener("mouseup", handleMouseUp); + $ul.addEventListener("touchstart", handleMouseDown); + $ul.addEventListener("touchend", handleMouseUp); + $ul.addEventListener("scroll", updatePage); + } + + function ulRemoveEventListeners() { + window.removeEventListener("resize", position); + $ul.removeEventListener("click", handleClick); + $ul.removeEventListener("mousedown", handleMouseDown); + $ul.removeEventListener("mouseup", handleMouseUp); + $ul.removeEventListener("touchstart", handleMouseDown); + $ul.removeEventListener("touchend", handleMouseUp); + $ul.removeEventListener("scroll", updatePage); + } + + function updatePage() { + // if the scroll is at the bottom + if ($ul.scrollTop + $ul.clientHeight >= $ul.scrollHeight) { + pages++; + updateUlNow(currentHints, pages); + } + } + + /** + * First time updates the hint instantly, then debounce + * @param {Array} hints + */ + function updateUl(hints) { + updateUlNow(hints); + updateUl = updateUlDebounce; + } + + /** + * Update the hint list after a delay + * @param {Array} hints + */ + function updateUlDebounce(hints) { + clearTimeout(updateUlTimeout); + updateUlTimeout = setTimeout(updateUlNow, 300, hints); + } + + /** + * Update the hint list instantly + * @param {Array} hints + * @param {number} page + */ + function updateUlNow(hints, page = 0) { + // render only first 500 hints + currentHints = hints; + const offset = page * LIMIT; + const end = offset + LIMIT; + const list = hints.slice(offset, end); + let scrollTop = $ul.scrollTop; + if (!list.length) return; + + $ul.remove(); + if (!page) { + scrollTop = 0; + $ul.content = list.map((hint) => ); + } else { + $ul.append(...list.map((hint) => )); + } + app.append($ul); + $ul.scrollTop = scrollTop; + position(); // Update the position of the new list + } + + return { + getSelected() { + $ul.get(".active"); + }, + get container() { + return $ul; + }, + }; } /** @@ -335,18 +335,18 @@ export default function inputhints($input, hints, onSelect) { * @returns {HTMLLIElement} */ function Hint({ hint }) { - let value = ''; - let text = ''; - - if (typeof hint === 'string') { - value = hint; - text = hint; - } else { - value = hint.value; - text = hint.text; - } - - return
      • ; + let value = ""; + let text = ""; + + if (typeof hint === "string") { + value = hint; + text = hint; + } else { + value = hint.value; + text = hint.text; + } + + return
      • ; } /** @@ -356,7 +356,11 @@ function Hint({ hint }) { * @returns {HTMLUListElement} */ function Ul({ hints = [] }) { - return
          - {hints.map((hint) => )} -
        ; + return ( +
          + {hints.map((hint) => ( + + ))} +
        + ); } diff --git a/src/components/page.js b/src/components/page.js index 1bbd96190..9b3b13857 100644 --- a/src/components/page.js +++ b/src/components/page.js @@ -9,19 +9,19 @@ import WCPage from "./WebComponents/wcPage"; * @returns {WCPage} */ function Page(title, options = {}) { - let page = ; - page.append = page.appendBody; - page.initializeIfNotAlreadyInitialized(); - page.settitle(title); + let page = ; + page.append = page.appendBody; + page.initializeIfNotAlreadyInitialized(); + page.settitle(title); - if (options.tail) { - page.header.append(options.tail); - } - if (options.lead) { - page.lead = options.lead; - } + if (options.tail) { + page.header.append(options.tail); + } + if (options.lead) { + page.lead = options.lead; + } - return page; + return page; } export default Page; diff --git a/src/components/palette/index.js b/src/components/palette/index.js index 383bff25e..33df7c144 100644 --- a/src/components/palette/index.js +++ b/src/components/palette/index.js @@ -1,8 +1,8 @@ -import './style.scss'; -import restoreTheme from 'lib/restoreTheme'; -import inputhints from 'components/inputhints'; -import actionStack from 'lib/actionStack'; -import keyboardHandler from 'handlers/keyboard'; +import "./style.scss"; +import inputhints from "components/inputhints"; +import keyboardHandler from "handlers/keyboard"; +import actionStack from "lib/actionStack"; +import restoreTheme from "lib/restoreTheme"; /** * @typedef {import('./inputhints').HintCallback} HintCallback @@ -48,91 +48,97 @@ This shows that using keyboardHideStart event is faster than not using it. * @returns {void} */ export default function palette(getList, onsSelectCb, placeholder, onremove) { - /**@type {HTMLInputElement} */ - const $input = ; - /**@type {HTMLElement} */ - const $mask =
        ; - /**@type {HTMLDivElement} */ - const $palette =
        {$input}
        ; - - - // Create a palette with input and hints - inputhints($input, generateHints, onSelect); - - // Removes the darkened color from status bar and navigation bar - restoreTheme(true); - - // Remove palette when input is blurred - $input.addEventListener('blur', remove); - // Don't wait for input to blur when keyboard hides, remove is - // as soon as keyboard starts to hide - keyboardHandler.on('keyboardHideStart', remove); - - // Add to DOM - app.append($palette, $mask); - - // Focus input to show options - $input.focus(); - - // Add to action stack to remove on back button - actionStack.push({ - id: 'palette', - action: remove, - }); - - /** - * On select callback for inputhints - * @param {string} value - */ - function onSelect(value) { - onsSelectCb(value); - remove(); - } - - /** - * Keydown event handler for input - * @param {KeyboardEvent} e - */ - function onkeydown(e) { - if (e.key !== 'Escape') return; - remove(); - } - - /** - * Generates hint for inputhints - * @param {HintCallback} setHints Set hints callback - * @param {HintModification} hintModification Hint modification object - */ - async function generateHints(setHints, hintModification) { - const list = getList(hintModification); - let data = list instanceof Promise ? await list : list; - setHints(data); - } - - /** - * Removes the palette - */ - function remove() { - actionStack.remove('palette'); - keyboardHandler.off('keyboardHideStart', remove); - $input.removeEventListener('blur', remove); - - restoreTheme(); - $palette.remove(); - $mask.remove(); - - if (typeof onremove === 'function') { - onremove(); - return; - } - - const { activeFile, editor } = editorManager; - if (activeFile.wasFocused) { - editor.focus(); - } - - remove = () => { - console.error('Palette already removed'); - }; - } -} \ No newline at end of file + /**@type {HTMLInputElement} */ + const $input = ( + + ); + /**@type {HTMLElement} */ + const $mask =
        ; + /**@type {HTMLDivElement} */ + const $palette =
        {$input}
        ; + + // Create a palette with input and hints + inputhints($input, generateHints, onSelect); + + // Removes the darkened color from status bar and navigation bar + restoreTheme(true); + + // Remove palette when input is blurred + $input.addEventListener("blur", remove); + // Don't wait for input to blur when keyboard hides, remove is + // as soon as keyboard starts to hide + keyboardHandler.on("keyboardHideStart", remove); + + // Add to DOM + app.append($palette, $mask); + + // Focus input to show options + $input.focus(); + + // Add to action stack to remove on back button + actionStack.push({ + id: "palette", + action: remove, + }); + + /** + * On select callback for inputhints + * @param {string} value + */ + function onSelect(value) { + onsSelectCb(value); + remove(); + } + + /** + * Keydown event handler for input + * @param {KeyboardEvent} e + */ + function onkeydown(e) { + if (e.key !== "Escape") return; + remove(); + } + + /** + * Generates hint for inputhints + * @param {HintCallback} setHints Set hints callback + * @param {HintModification} hintModification Hint modification object + */ + async function generateHints(setHints, hintModification) { + const list = getList(hintModification); + let data = list instanceof Promise ? await list : list; + setHints(data); + } + + /** + * Removes the palette + */ + function remove() { + actionStack.remove("palette"); + keyboardHandler.off("keyboardHideStart", remove); + $input.removeEventListener("blur", remove); + + restoreTheme(); + $palette.remove(); + $mask.remove(); + + if (typeof onremove === "function") { + onremove(); + return; + } + + const { activeFile, editor } = editorManager; + if (activeFile.wasFocused) { + editor.focus(); + } + + remove = () => { + console.error("Palette already removed"); + }; + } +} diff --git a/src/components/quickTools/footer.js b/src/components/quickTools/footer.js index af10a8fe0..d94d2411f 100644 --- a/src/components/quickTools/footer.js +++ b/src/components/quickTools/footer.js @@ -2,8 +2,8 @@ * @typedef {import('html-tag-js/ref')} Ref */ -import items, { ref } from './items'; -import settings from 'lib/settings'; +import settings from "lib/settings"; +import items, { ref } from "./items"; /** * Create a row with common buttons @@ -11,74 +11,89 @@ import settings from 'lib/settings'; * @param {number} [param0.row] Row number */ export const Row = ({ row }) => { - const startIndex = (row - 1) * settings.QUICKTOOLS_GROUP_CAPACITY * settings.QUICKTOOLS_GROUPS; - return
        { - (() => { - const sections = []; - for (let i = 0; i < settings.QUICKTOOLS_GROUPS; ++i) { - const section = []; - for (let j = 0; j < settings.QUICKTOOLS_GROUP_CAPACITY; ++j) { - const index = startIndex + (i * settings.QUICKTOOLS_GROUP_CAPACITY + j); - const itemIndex = settings.value.quicktoolsItems[index]; // saved item index - const item = items[itemIndex]; // item object - section.push(); - } - sections.push(
        {section}
        ); - } - return sections; - })() - }
        ; + const startIndex = + (row - 1) * settings.QUICKTOOLS_GROUP_CAPACITY * settings.QUICKTOOLS_GROUPS; + return ( +
        + {(() => { + const sections = []; + for (let i = 0; i < settings.QUICKTOOLS_GROUPS; ++i) { + const section = []; + for (let j = 0; j < settings.QUICKTOOLS_GROUP_CAPACITY; ++j) { + const index = + startIndex + (i * settings.QUICKTOOLS_GROUP_CAPACITY + j); + const itemIndex = settings.value.quicktoolsItems[index]; // saved item index + const item = items[itemIndex]; // item object + section.push(); + } + sections.push(
        {section}
        ); + } + return sections; + })()} +
        + ); }; /** * Create a search row with search input and buttons * @returns {Element} */ -export const SearchRow1 = ({ inputRef }) =>
        - - - - -
        ; +export const SearchRow1 = ({ inputRef }) => ( +
        + + + + +
        +); /** * Create a search row with replace input and buttons * @returns {Element} */ -export const SearchRow2 = ({ inputRef, posRef, totalRef }) =>
        - - - -
        - 0 - of - 0 -
        -
        ; +export const SearchRow2 = ({ inputRef, posRef, totalRef }) => ( +
        + + + +
        + 0 + of + 0 +
        +
        +); /**@type {HTMLElement} */ -export const $footer =
        ; +export const $footer =
        ; /**@type {HTMLElement} */ -export const $toggler = ; +export const $toggler = ( + +); /**@type {HTMLTextAreaElement} */ -export const $input = ; +export const $input = ( + +); /** - * + * * @param {RowItem} param0 Attributes * @param {string} param0.id Button id * @param {string} param0.icon Icon name @@ -90,22 +105,24 @@ export const $input = ); + return autosize( + , + ); } /** @@ -628,90 +685,94 @@ function Textarea({ name, placeholder, ref }) { * @returns {RegExp} - The regular expression created from the search string and options. */ function toRegex(search, options) { - const { caseSensitive = false, wholeWord = false, regExp = false } = options; - - let flags = caseSensitive ? 'gm' : 'gim'; - let regexString = regExp ? search : escapeStringRegexp(search); - - if (wholeWord) { - const wordBoundary = '\\b'; - regexString = `${wordBoundary}${regexString}${wordBoundary}`; - } - - try { - return new RegExp(regexString, flags); - } catch (error) { - const [, message] = error.message.split(/:(.*)/); - $resultOverview.classList.add('error'); - $resultOverview.textContent = strings['invalid regex'].replace('{message}', message || error.message); - return null; - } + const { caseSensitive = false, wholeWord = false, regExp = false } = options; + + let flags = caseSensitive ? "gm" : "gim"; + let regexString = regExp ? search : escapeStringRegexp(search); + + if (wholeWord) { + const wordBoundary = "\\b"; + regexString = `${wordBoundary}${regexString}${wordBoundary}`; + } + + try { + return new RegExp(regexString, flags); + } catch (error) { + const [, message] = error.message.split(/:(.*)/); + $resultOverview.classList.add("error"); + $resultOverview.textContent = strings["invalid regex"].replace( + "{message}", + message || error.message, + ); + return null; + } } /** * On cursor change event handler */ async function onCursorChange() { - const line = searchResult.selection.getCursor().row; - const result = results[line]; - if (!result) return; - const { file, position } = result; - if (!position) { // fold the file - searchResult.execCommand('toggleFoldWidget'); - return; - } - - Sidebar.hide(); - const { url } = filesSearched[file]; - await openFile(url, { render: true }); - const { editor } = editorManager; - editor.moveCursorTo(position.start.row, position.start.column, false); - editor.selection.setRange(position); - editor.centerSelection(); - editor.focus(); + const line = searchResult.selection.getCursor().row; + const result = results[line]; + if (!result) return; + const { file, position } = result; + if (!position) { + // fold the file + searchResult.execCommand("toggleFoldWidget"); + return; + } + + Sidebar.hide(); + const { url } = filesSearched[file]; + await openFile(url, { render: true }); + const { editor } = editorManager; + editor.moveCursorTo(position.start.row, position.start.column, false); + editor.selection.setRange(position); + editor.centerSelection(); + editor.focus(); } /** * When a file is added or removed from the file list - * @param {import('lib/fileList').Tree} tree + * @param {import('lib/fileList').Tree} tree */ function onFileUpdate(tree) { - if (!tree || tree?.children) return; - onInput(); + if (!tree || tree?.children) return; + onInput(); } /** * Add event listeners to file changes */ function addEvents() { - files.on('add-file', onFileUpdate); - files.on('remove-file', onFileUpdate); - files.on('add-folder', onInput); - files.on('remove-folder', onInput); - files.on('refresh', onInput); - editorManager.on('rename-file', onInput); - editorManager.on('file-content-changed', onInput); + files.on("add-file", onFileUpdate); + files.on("remove-file", onFileUpdate); + files.on("add-folder", onInput); + files.on("remove-folder", onInput); + files.on("refresh", onInput); + editorManager.on("rename-file", onInput); + editorManager.on("file-content-changed", onInput); } /** * Remove event listeners to file changes */ function removeEvents() { - files.off('add-file', onFileUpdate); - files.off('remove-file', onFileUpdate); - files.off('add-folder', onInput); - files.off('remove-folder', onInput); - files.off('refresh', onInput); - editorManager.off('rename-file', onInput); - editorManager.off('file-content-changed', onInput); + files.off("add-file", onFileUpdate); + files.off("remove-file", onFileUpdate); + files.off("add-folder", onInput); + files.off("remove-folder", onInput); + files.off("refresh", onInput); + editorManager.off("rename-file", onInput); + editorManager.off("file-content-changed", onInput); } function forceTokenizer() { - const { session } = searchResult; - // force recreation of tokenizer - session.$mode.$tokenizer = null; - session.bgTokenizer.setTokenizer(session.$mode.getTokenizer()); - // force re-highlight whole document - const row = session.getLength() - 1; - session.bgTokenizer.start(row); + const { session } = searchResult; + // force recreation of tokenizer + session.$mode.$tokenizer = null; + session.bgTokenizer.setTokenizer(session.$mode.getTokenizer()); + // force re-highlight whole document + const row = session.getLength() - 1; + session.bgTokenizer.start(row); } diff --git a/src/sidebarApps/searchInFiles/searchResultMode.js b/src/sidebarApps/searchInFiles/searchResultMode.js index 61391c277..2260c502f 100644 --- a/src/sidebarApps/searchInFiles/searchResultMode.js +++ b/src/sidebarApps/searchInFiles/searchResultMode.js @@ -1,207 +1,223 @@ export const words = []; export const fileNames = []; -ace.define("ace/mode/search_result_highlight_rules", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function (require, exports, module) { - const oop = require("../lib/oop"); - const { TextHighlightRules } = require("./text_highlight_rules"); - - function SearchHighlightRules() { - this.$rules = { - start: [ - { - token: "file_name", - get regex() { - return fileNames.join('|'); - } - }, - { - token: 'highlight', - get regex() { - return words.join('|'); - }, - }, - { - token: "string", // multi line string start - regex: /[|>][-+\d]*(?:$|\s+(?:$|#))/, - onMatch: function (val, state, stack, line) { - line = line.replace(/ #.*/, ""); - var indent = /^ *((:\s*)?-(\s*[^|>])?)?/.exec(line)[0] - .replace(/\S\s*$/, "").length; - var indentationIndicator = parseInt(/\d+[\s+-]*$/.exec(line)); +ace.define( + "ace/mode/search_result_highlight_rules", + [ + "require", + "exports", + "module", + "ace/lib/oop", + "ace/mode/text_highlight_rules", + ], + function (require, exports, module) { + const oop = require("../lib/oop"); + const { TextHighlightRules } = require("./text_highlight_rules"); - if (indentationIndicator) { - indent += indentationIndicator - 1; - this.next = "mlString"; - } else { - this.next = "mlStringPre"; - } - if (!stack.length) { - stack.push(this.next); - stack.push(indent); - } else { - stack[0] = this.next; - stack[1] = indent; - } - return this.token; - }, - next: "mlString" - } - ], - mlStringPre: [ - { - token: "indent", - regex: /^ *$/ - }, { - token: "indent", - regex: /^ */, - onMatch: function (val, state, stack) { - var curIndent = stack[1]; + function SearchHighlightRules() { + this.$rules = { + start: [ + { + token: "file_name", + get regex() { + return fileNames.join("|"); + }, + }, + { + token: "highlight", + get regex() { + return words.join("|"); + }, + }, + { + token: "string", // multi line string start + regex: /[|>][-+\d]*(?:$|\s+(?:$|#))/, + onMatch: function (val, state, stack, line) { + line = line.replace(/ #.*/, ""); + var indent = /^ *((:\s*)?-(\s*[^|>])?)?/ + .exec(line)[0] + .replace(/\S\s*$/, "").length; + var indentationIndicator = Number.parseInt( + /\d+[\s+-]*$/.exec(line), + ); - if (curIndent >= val.length) { - this.next = "start"; - stack.shift(); - stack.shift(); - } - else { - stack[1] = val.length - 1; - this.next = stack[0] = "mlString"; - } - return this.token; - }, - next: "mlString" - } - ], - mlString: [ - { - token: "indent", - regex: /^ *$/ - }, { - token: "indent", - regex: /^ */, - onMatch: function (val, state, stack) { - var curIndent = stack[1]; + if (indentationIndicator) { + indent += indentationIndicator - 1; + this.next = "mlString"; + } else { + this.next = "mlStringPre"; + } + if (!stack.length) { + stack.push(this.next); + stack.push(indent); + } else { + stack[0] = this.next; + stack[1] = indent; + } + return this.token; + }, + next: "mlString", + }, + ], + mlStringPre: [ + { + token: "indent", + regex: /^ *$/, + }, + { + token: "indent", + regex: /^ */, + onMatch: function (val, state, stack) { + var curIndent = stack[1]; - if (curIndent >= val.length) { - this.next = "start"; - stack.splice(0); - } - else { - this.next = "mlString"; - } - return this.token; - }, - next: "mlString" - } - ] - }; - this.normalizeRules(); - } - oop.inherits(SearchHighlightRules, TextHighlightRules); - exports.SearchHighlightRules = SearchHighlightRules; -}); + if (curIndent >= val.length) { + this.next = "start"; + stack.shift(); + stack.shift(); + } else { + stack[1] = val.length - 1; + this.next = stack[0] = "mlString"; + } + return this.token; + }, + next: "mlString", + }, + ], + mlString: [ + { + token: "indent", + regex: /^ *$/, + }, + { + token: "indent", + regex: /^ */, + onMatch: function (val, state, stack) { + var curIndent = stack[1]; -define( - "ace/mode/folding/search_result_fold", - [ - "require", "exports", "module", "ace/lib/oop", - "ace/mode/folding/fold_mode", "ace/range" - ], - function (require, exports, module) { - const oop = require("ace/lib/oop"); - const { FoldMode: BaseFoldMode } = require("./fold_mode"); - const { Range } = require("ace/range"); + if (curIndent >= val.length) { + this.next = "start"; + stack.splice(0); + } else { + this.next = "mlString"; + } + return this.token; + }, + next: "mlString", + }, + ], + }; + this.normalizeRules(); + } + oop.inherits(SearchHighlightRules, TextHighlightRules); + exports.SearchHighlightRules = SearchHighlightRules; + }, +); - function FoldMode() { }; - oop.inherits(FoldMode, BaseFoldMode); - exports.FoldMode = FoldMode; +define("ace/mode/folding/search_result_fold", [ + "require", + "exports", + "module", + "ace/lib/oop", + "ace/mode/folding/fold_mode", + "ace/range", +], function (require, exports, module) { + const oop = require("ace/lib/oop"); + const { FoldMode: BaseFoldMode } = require("./fold_mode"); + const { Range } = require("ace/range"); - (function () { - this.getFoldWidgetRange = function (session, foldStyle, row) { - var range = this.indentationBlock(session, row); - if (range) - return range; - var re = /\S/; - var line = session.getLine(row); - var startLevel = line.search(re); - if (startLevel == -1 || line[startLevel] != "#") - return; - var startColumn = line.length; - var maxRow = session.getLength(); - var startRow = row; - var endRow = row; - while (++row < maxRow) { - line = session.getLine(row); - var level = line.search(re); - if (level == -1) - continue; - if (line[level] != "#") - break; - endRow = row; - } - if (endRow > startRow) { - var endColumn = session.getLine(endRow).length; - return new Range(startRow, startColumn, endRow, endColumn); - } - }; - this.getFoldWidget = function (session, foldStyle, row) { - var line = session.getLine(row); - var indent = line.search(/\S/); - var next = session.getLine(row + 1); - var prev = session.getLine(row - 1); - var prevIndent = prev.search(/\S/); - var nextIndent = next.search(/\S/); - if (indent == -1) { - session.foldWidgets[row - 1] = prevIndent != -1 && prevIndent < nextIndent ? "start" : ""; - return ""; - } - if (prevIndent == -1) { - if (indent == nextIndent && line[indent] == "#" && next[indent] == "#") { - session.foldWidgets[row - 1] = ""; - session.foldWidgets[row + 1] = ""; - return "start"; - } - } - else if (prevIndent == indent && line[indent] == "#" && prev[indent] == "#") { - if (session.getLine(row - 2).search(/\S/) == -1) { - session.foldWidgets[row - 1] = "start"; - session.foldWidgets[row + 1] = ""; - return ""; - } - } - if (prevIndent != -1 && prevIndent < indent) - session.foldWidgets[row - 1] = "start"; - else - session.foldWidgets[row - 1] = ""; - if (indent < nextIndent) - return "start"; - else - return ""; - }; - }).call(FoldMode.prototype); - } -); + function FoldMode() {} + oop.inherits(FoldMode, BaseFoldMode); + exports.FoldMode = FoldMode; + (function () { + this.getFoldWidgetRange = function (session, foldStyle, row) { + var range = this.indentationBlock(session, row); + if (range) return range; + var re = /\S/; + var line = session.getLine(row); + var startLevel = line.search(re); + if (startLevel === -1 || line[startLevel] !== "#") return; + var startColumn = line.length; + var maxRow = session.getLength(); + var startRow = row; + var endRow = row; + while (++row < maxRow) { + line = session.getLine(row); + var level = line.search(re); + if (level === -1) continue; + if (line[level] !== "#") break; + endRow = row; + } + if (endRow > startRow) { + var endColumn = session.getLine(endRow).length; + return new Range(startRow, startColumn, endRow, endColumn); + } + }; + this.getFoldWidget = function (session, foldStyle, row) { + var line = session.getLine(row); + var indent = line.search(/\S/); + var next = session.getLine(row + 1); + var prev = session.getLine(row - 1); + var prevIndent = prev.search(/\S/); + var nextIndent = next.search(/\S/); + if (indent === -1) { + session.foldWidgets[row - 1] = + prevIndent !== -1 && prevIndent < nextIndent ? "start" : ""; + return ""; + } + if (prevIndent === -1) { + if ( + indent === nextIndent && + line[indent] === "#" && + next[indent] === "#" + ) { + session.foldWidgets[row - 1] = ""; + session.foldWidgets[row + 1] = ""; + return "start"; + } + } else if ( + prevIndent === indent && + line[indent] === "#" && + prev[indent] === "#" + ) { + if (session.getLine(row - 2).search(/\S/) === -1) { + session.foldWidgets[row - 1] = "start"; + session.foldWidgets[row + 1] = ""; + return ""; + } + } + if (prevIndent !== -1 && prevIndent < indent) + session.foldWidgets[row - 1] = "start"; + else session.foldWidgets[row - 1] = ""; + if (indent < nextIndent) return "start"; + else return ""; + }; + }).call(FoldMode.prototype); +}); ace.define( - "ace/mode/search_result", - [ - "require", "exports", "module", - "ace/lib/oop", "ace/mode/text", - "ace/mode/folding/search_result_fold", - "ace/search_result_highlight_rules" - ], - function (require, exports, module) { - const oop = require("ace/lib/oop"); - const { Mode: TextMode } = require("./text"); - const { SearchHighlightRules } = require("./search_result_highlight_rules"); - const { FoldMode } = require("./folding/search_result_fold"); + "ace/mode/search_result", + [ + "require", + "exports", + "module", + "ace/lib/oop", + "ace/mode/text", + "ace/mode/folding/search_result_fold", + "ace/search_result_highlight_rules", + ], + function (require, exports, module) { + const oop = require("ace/lib/oop"); + const { Mode: TextMode } = require("./text"); + const { SearchHighlightRules } = require("./search_result_highlight_rules"); + const { FoldMode } = require("./folding/search_result_fold"); - function Mode() { - this.$id = "ace/mode/search_result"; - this.HighlightRules = SearchHighlightRules; - this.foldingRules = new FoldMode(); - } - oop.inherits(Mode, TextMode); - exports.Mode = Mode; - } + function Mode() { + this.$id = "ace/mode/search_result"; + this.HighlightRules = SearchHighlightRules; + this.foldingRules = new FoldMode(); + } + oop.inherits(Mode, TextMode); + exports.Mode = Mode; + }, ); diff --git a/src/sidebarApps/searchInFiles/worker.js b/src/sidebarApps/searchInFiles/worker.js index 0ef5023ed..3798715fc 100644 --- a/src/sidebarApps/searchInFiles/worker.js +++ b/src/sidebarApps/searchInFiles/worker.js @@ -1,30 +1,30 @@ -import 'core-js/stable'; -import { minimatch } from 'minimatch'; +import "core-js/stable"; +import { minimatch } from "minimatch"; const resolvers = {}; self.onmessage = (ev) => { - const { action, data, error, id } = ev.data; - switch (action) { - case 'search-files': - processFiles(data, 'search'); - break; + const { action, data, error, id } = ev.data; + switch (action) { + case "search-files": + processFiles(data, "search"); + break; - case 'replace-files': - processFiles(data, 'replace'); - break; + case "replace-files": + processFiles(data, "replace"); + break; - case 'get-file': { - if (!resolvers[id]) return; - const cb = resolvers[id]; - cb(data, error); - delete resolvers[id]; - break; - } + case "get-file": { + if (!resolvers[id]) return; + const cb = resolvers[id]; + cb(data, error); + delete resolvers[id]; + break; + } - default: - return false; - } + default: + return false; + } }; /** @@ -33,40 +33,39 @@ self.onmessage = (ev) => { * @param {object} data - The data containing files, search, replace, and options. * @param {'search' | 'replace'} [mode='search'] - The mode of operation (search or replace). */ -function processFiles(data, mode = 'search') { - const process = mode === 'search' ? searchInFile : replaceInFile; - const { files, search, replace, options } = data; - const { test: skip } = Skip(options); - const total = files.length; - let count = 0; +function processFiles(data, mode = "search") { + const process = mode === "search" ? searchInFile : replaceInFile; + const { files, search, replace, options } = data; + const { test: skip } = Skip(options); + const total = files.length; + let count = 0; - files.forEach(processFile); + files.forEach(processFile); - /** - * Process a file for search or replace operation. - * - * @param {object} file - The file object to process. - * @param {string} file.url - The URL of the file. - */ - function processFile(file) { - if (skip(file)) { - done(++count / total, mode); - return; - } + /** + * Process a file for search or replace operation. + * + * @param {object} file - The file object to process. + * @param {string} file.url - The URL of the file. + */ + function processFile(file) { + if (skip(file)) { + done(++count / total, mode); + return; + } - getFile(file.url, (res, err) => { - if (err) { - done(++count / total, mode); - throw err; - } + getFile(file.url, (res, err) => { + if (err) { + done(++count / total, mode); + throw err; + } - process({ file, content: res, search, replace, options }); - done(++count / total, mode); - }); - } + process({ file, content: res, search, replace, options }); + done(++count / total, mode); + }); + } } - /** * Search for a string in the content of a file. * @param {object} arg - The content of the file to search. @@ -75,36 +74,36 @@ function processFiles(data, mode = 'search') { * @param {RegExp} arg.search - The string to search for. */ function searchInFile({ file, content, search }) { - const matches = []; + const matches = []; - let text = `${file.name}`; - let match; + let text = `${file.name}`; + let match; - if (text.length > 30) { - text = `...${text.slice(-30)}`; - } + if (text.length > 30) { + text = `...${text.slice(-30)}`; + } - while ((match = search.exec(content))) { - const [word] = match; - const start = match.index; - const end = start + word.length; - const position = { - start: getLineColumn(content, start), - end: getLineColumn(content, end) - }; - const [line, renderText] = getSurrounding(content, word, start, end); - text += `\n\t${line.trim()}`; - matches.push({ match: word, position, renderText }); - } + while ((match = search.exec(content))) { + const [word] = match; + const start = match.index; + const end = start + word.length; + const position = { + start: getLineColumn(content, start), + end: getLineColumn(content, end), + }; + const [line, renderText] = getSurrounding(content, word, start, end); + text += `\n\t${line.trim()}`; + matches.push({ match: word, position, renderText }); + } - self.postMessage({ - action: 'search-result', - data: { - file, - matches, - text, - }, - }); + self.postMessage({ + action: "search-result", + data: { + file, + matches, + text, + }, + }); } /** @@ -116,40 +115,40 @@ function searchInFile({ file, content, search }) { * @param {string} arg.replace - The string to replace with. */ function replaceInFile({ file, content, search, replace }) { - const text = content.replace(search, replace); + const text = content.replace(search, replace); - self.postMessage({ - action: 'replace-result', - data: { file, text }, - }); + self.postMessage({ + action: "replace-result", + data: { file, text }, + }); } /** * Gets surrounding text of a match. - * @param {string} content - * @param {string} word - * @param {number} start - * @param {number} end + * @param {string} content + * @param {string} word + * @param {number} start + * @param {number} end */ function getSurrounding(content, word, start, end) { - const max = 50; - const remaining = max - (end - start); - let result = []; + const max = 50; + const remaining = max - (end - start); + let result = []; - if (remaining <= 0) { - word = word.slice(-max); - result = [`...${word}`, word]; - } else { - let left = Math.floor(remaining / 2); - let right = left; + if (remaining <= 0) { + word = word.slice(-max); + result = [`...${word}`, word]; + } else { + let left = Math.floor(remaining / 2); + let right = left; - let leftText = content.substring(start - left, start); - let rightText = content.substring(end, end + right); + let leftText = content.substring(start - left, start); + let rightText = content.substring(end, end + right); - result = [`${leftText}${word}${rightText}`, word]; - } + result = [`${leftText}${word}${rightText}`, word]; + } - return result.map((text) => text.replace(/[\r\n]+/g, ' ⏎ ')); + return result.map((text) => text.replace(/[\r\n]+/g, " ⏎ ")); } /** @@ -167,29 +166,29 @@ function getSurrounding(content, word, start, end) { * const file = 'Hello, this is a test.\nAnother test is here.'; * const position = 15; * const lineColumn = getLineColumn(file, position); - * + * * // lineColumn: { line: 1, column: 16 } */ function getLineColumn(file, position) { - const lines = file.substring(0, position).split('\n'); - const lineNumber = lines.length - 1; - const columnNumber = lines[lineNumber].length; - return { row: lineNumber, column: columnNumber }; + const lines = file.substring(0, position).split("\n"); + const lineNumber = lines.length - 1; + const columnNumber = lines[lineNumber].length; + return { row: lineNumber, column: columnNumber }; } /** * Retrieves the contents of a file from the main thread. - * @param {string} url - * @param {function} cb + * @param {string} url + * @param {function} cb */ function getFile(url, cb) { - const id = parseInt(Date.now() + Math.random() * 1000000); - resolvers[id] = cb; - self.postMessage({ - action: 'get-file', - data: url, - id, - }); + const id = Number.parseInt(Date.now() + Math.random() * 1000000); + resolvers[id] = cb; + self.postMessage({ + action: "get-file", + data: url, + id, + }); } /** @@ -199,23 +198,22 @@ function getFile(url, cb) { * @param {'search'|'replace'} mode */ function done(ratio, mode) { - if (ratio === 1) { - self.postMessage({ - action: 'progress', - data: 100, - }); - self.postMessage({ - action: `done-${mode === 'search' ? 'searching' : 'replacing'}`, - }); - } else { - self.postMessage({ - action: 'progress', - data: Math.floor(ratio * 100), - }); - } + if (ratio === 1) { + self.postMessage({ + action: "progress", + data: 100, + }); + self.postMessage({ + action: `done-${mode === "search" ? "searching" : "replacing"}`, + }); + } else { + self.postMessage({ + action: "progress", + data: Math.floor(ratio * 100), + }); + } } - /** * Creates a skip function that filters files based on exclusion and inclusion patterns. * @@ -224,25 +222,28 @@ function done(ratio, mode) { * @param {string} arg.include - The inclusion patterns separated by commas. */ function Skip({ exclude, include }) { - const excludeFiles = (exclude ? exclude.split(',') : []).map((p) => p.trim()); - const includeFiles = (include ? include.split(',') : ['**']).map((p) => p.trim()); + const excludeFiles = (exclude ? exclude.split(",") : []).map((p) => p.trim()); + const includeFiles = (include ? include.split(",") : ["**"]).map((p) => + p.trim(), + ); - /** - * Tests whether a file should be skipped based on exclusion and inclusion patterns. - * - * @param {object} file - The file to be tested. - * @param {string} file.path - The relative URL of the file. - * @returns {boolean} - Returns true if the file should be skipped, false otherwise. - */ - function test(file) { - if (!file.path) return false; - const match = (pattern) => minimatch(file.path, pattern, { matchBase: true }); - return excludeFiles.some(match) || !includeFiles.some(match); - } + /** + * Tests whether a file should be skipped based on exclusion and inclusion patterns. + * + * @param {object} file - The file to be tested. + * @param {string} file.path - The relative URL of the file. + * @returns {boolean} - Returns true if the file should be skipped, false otherwise. + */ + function test(file) { + if (!file.path) return false; + const match = (pattern) => + minimatch(file.path, pattern, { matchBase: true }); + return excludeFiles.some(match) || !includeFiles.some(match); + } - return { - test - }; + return { + test, + }; } /** diff --git a/src/sidebarApps/sidebarApp.js b/src/sidebarApps/sidebarApp.js index 77f910265..95409f0e6 100644 --- a/src/sidebarApps/sidebarApp.js +++ b/src/sidebarApps/sidebarApp.js @@ -6,133 +6,135 @@ let $sidebar; let $contaienr; export default class SidebarApp { - /**@type {HTMLSpanElement} */ - #icon; - /**@type {string} */ - #id; - /**@type {string} */ - #init; - /**@type {string} */ - #title; - /**@type {boolean} */ - #active; - /**@type {(el:HTMLElement)=>void} */ - #onselect; - /**@type {HTMLElement} */ - #container; - - /** - * Creates a new sidebar app. - * @param {string} icon - * @param {string} id - * @param {string} title - * @param {(el:HTMLElement)=>void} init - * @param {(el:HTMLElement)=>void} onselect - */ - constructor(icon, id, title, init, onselect) { - const emptyFunc = () => { }; - this.#container =
        ; - this.#icon = ; - this.#id = id; - this.#title = title; - this.#init = init || emptyFunc; - this.#onselect = onselect || emptyFunc; - this.#init(this.#container); - } - - /** - * Installs the app in the sidebar. - * @param {boolean} prepend - * @returns {void} - */ - install(prepend = false) { - if (prepend) { - $apps.prepend(this.#icon); - return; - } - - $apps.append(this.#icon); - } - - /** - * Initialize the sidebar element. - * @param {HTMLElement} $el sidebar element - * @param {HTMLElement} $el2 apps element - */ - static init($el, $el2) { - $sidebar = $el; - $apps = $el2; - } - - /**@type {HTMLSpanElement} */ - get icon() { - return this.#icon; - } - - /**@type {string} */ - get id() { - return this.#id; - } - - /**@type {string} */ - get title() { - return this.#title; - } - - /**@type {boolean} */ - get active() { - return !!this.#active; - } - - /**@param {boolean} value */ - set active(value) { - this.#active = !!value; - this.#icon.classList.toggle('active', this.#active); - if (this.#active) { - const child = getContainer(this.#container); - $sidebar.replaceChild($contaienr, child); - this.#onselect(this.#container); - } - } - - /**@type {HTMLElement} */ - get container() { - return this.#container; - } - - /**@type {(el:HTMLElement)=>void} */ - get init() { - return this.#init; - } - - /**@type {(el:HTMLElement)=>void} */ - get onselect() { - return this.#onselect; - } - - remove() { - this.#icon.remove(); - this.#container.remove(); - this.#icon = null; - this.#container = null; - } + /**@type {HTMLSpanElement} */ + #icon; + /**@type {string} */ + #id; + /**@type {string} */ + #init; + /**@type {string} */ + #title; + /**@type {boolean} */ + #active; + /**@type {(el:HTMLElement)=>void} */ + #onselect; + /**@type {HTMLElement} */ + #container; + + /** + * Creates a new sidebar app. + * @param {string} icon + * @param {string} id + * @param {string} title + * @param {(el:HTMLElement)=>void} init + * @param {(el:HTMLElement)=>void} onselect + */ + constructor(icon, id, title, init, onselect) { + const emptyFunc = () => {}; + this.#container =
        ; + this.#icon = ; + this.#id = id; + this.#title = title; + this.#init = init || emptyFunc; + this.#onselect = onselect || emptyFunc; + this.#init(this.#container); + } + + /** + * Installs the app in the sidebar. + * @param {boolean} prepend + * @returns {void} + */ + install(prepend = false) { + if (prepend) { + $apps.prepend(this.#icon); + return; + } + + $apps.append(this.#icon); + } + + /** + * Initialize the sidebar element. + * @param {HTMLElement} $el sidebar element + * @param {HTMLElement} $el2 apps element + */ + static init($el, $el2) { + $sidebar = $el; + $apps = $el2; + } + + /**@type {HTMLSpanElement} */ + get icon() { + return this.#icon; + } + + /**@type {string} */ + get id() { + return this.#id; + } + + /**@type {string} */ + get title() { + return this.#title; + } + + /**@type {boolean} */ + get active() { + return !!this.#active; + } + + /**@param {boolean} value */ + set active(value) { + this.#active = !!value; + this.#icon.classList.toggle("active", this.#active); + if (this.#active) { + const child = getContainer(this.#container); + $sidebar.replaceChild($contaienr, child); + this.#onselect(this.#container); + } + } + + /**@type {HTMLElement} */ + get container() { + return this.#container; + } + + /**@type {(el:HTMLElement)=>void} */ + get init() { + return this.#init; + } + + /**@type {(el:HTMLElement)=>void} */ + get onselect() { + return this.#onselect; + } + + remove() { + this.#icon.remove(); + this.#container.remove(); + this.#icon = null; + this.#container = null; + } } /** * Creates a icon element for a sidebar app. - * @param {object} param0 + * @param {object} param0 * @param {string} param0.icon * @param {string} param0.id * @returns {HTMLElement} */ function Icon({ icon, id, title }) { - const className = `icon ${icon}`; - return ; + const className = `icon ${icon}`; + return ( + + ); } /** @@ -141,11 +143,11 @@ function Icon({ icon, id, title }) { * @returns {HTMLElement} */ function getContainer($el) { - const res = $contaienr; + const res = $contaienr; - if ($el) { - $contaienr = $el; - } + if ($el) { + $contaienr = $el; + } - return res || $sidebar.get('.container'); + return res || $sidebar.get(".container"); } diff --git a/src/theme/builder.js b/src/theme/builder.js index 45e15ab63..93e5642d4 100644 --- a/src/theme/builder.js +++ b/src/theme/builder.js @@ -1,340 +1,349 @@ -import Color from 'utils/color'; +import Color from "utils/color"; export default class ThemeBuilder { - #theme = { - "--popup-border-radius": "4px", - "--active-color": "rgb(51, 153, 255)", - "--active-text-color": "rgb(255, 215, 0)", - "--active-icon-color": "rgba(0, 0, 0, 0.2)", - "--border-color": "rgba(122, 122, 122, 0.2)", - "--box-shadow-color": "rgba(0, 0, 0, 0.2)", - "--button-active-color": "rgb(44, 142, 240)", - "--button-background-color": "rgb(51, 153, 255)", - "--button-text-color": "rgb(255, 255, 255)", - "--error-text-color": "rgb(255, 185, 92)", - "--primary-color": "rgb(153, 153, 255)", - "--primary-text-color": "rgb(255, 255, 255)", - "--secondary-color": "rgb(255, 255, 255)", - "--secondary-text-color": "rgb(37, 37, 37)", - "--link-text-color": "rgb(97, 94, 253)", - "--scrollbar-color": "rgba(0, 0, 0, 0.3)", - "--popup-border-color": "rgba(0, 0, 0, 0)", - "--popup-icon-color": "rgb(153, 153, 255)", - "--popup-background-color": "rgb(255, 255, 255)", - "--popup-text-color": "rgb(37, 37, 37)", - "--popup-active-color": "rgb(169, 0, 0)", - "--danger-color": "rgb(160, 51, 0)", - "--danger-text-color": "rgb(255, 255, 255)", - "--file-tab-width": "120px", - }; - - version = 'free'; - name = 'Default'; - type = 'light'; - darkenedPrimaryColor = 'rgb(92, 92, 153)'; - /** Whether Auto update darkened primary color when primary color is updated. */ - autoDarkened = true; - preferredEditorTheme = null; - preferredFont = null; - - /** - * Creates a theme - * @param {string} [name] name of the theme - * @param {'dark'|'light'} [type] type of the theme - * @param {'free'|'paid'} [version] version of the theme - */ - constructor(name = '', type = 'dark', version = 'free') { - this.name = name; - this.type = type; - this.version = version; - } - - get id() { - return this.name.toLowerCase(); - } - - get popupBorderRadius() { - return this.#theme['--popup-border-radius']; - } - - set popupBorderRadius(value) { - this.#theme['--popup-border-radius'] = value; - } - - get activeColor() { - return this.#theme['--active-color']; - } - - set activeColor(value) { - this.#theme['--active-color'] = value; - } - - get activeIconColor() { - return this.#theme['--active-icon-color']; - } - - set activeIconColor(value) { - this.#theme['--active-icon-color'] = value; - } - - get borderColor() { - return this.#theme['--border-color']; - } - - set borderColor(value) { - this.#theme['--border-color'] = value; - } - - get boxShadowColor() { - return this.#theme['--box-shadow-color']; - } - - set boxShadowColor(value) { - this.#theme['--box-shadow-color'] = value; - } - - get buttonActiveColor() { - return this.#theme['--button-active-color']; - } - - set buttonActiveColor(value) { - this.#theme['--button-active-color'] = value; - } - - get buttonBackgroundColor() { - return this.#theme['--button-background-color']; - } - - set buttonBackgroundColor(value) { - this.#theme['--button-background-color'] = value; - } - - get buttonTextColor() { - return this.#theme['--button-text-color']; - } - - set buttonTextColor(value) { - this.#theme['--button-text-color'] = value; - } - - get errorTextColor() { - return this.#theme['--error-text-color']; - } - - set errorTextColor(value) { - this.#theme['--error-text-color'] = value; - } - - get primaryColor() { - return this.#theme['--primary-color']; - } - - set primaryColor(value) { - if (this.autoDarkened) { - this.darkenedPrimaryColor = Color(value).darken(0.4).hex.toString(); - } - this.#theme['--primary-color'] = value; - } - - get primaryTextColor() { - return this.#theme['--primary-text-color']; - } - - set primaryTextColor(value) { - this.#theme['--primary-text-color'] = value; - } - - get secondaryColor() { - return this.#theme['--secondary-color']; - } - - set secondaryColor(value) { - this.#theme['--secondary-color'] = value; - } - - get secondaryTextColor() { - return this.#theme['--secondary-text-color']; - } - - set secondaryTextColor(value) { - this.#theme['--secondary-text-color'] = value; - } - - get linkTextColor() { - return this.#theme['--link-text-color']; - } - - set linkTextColor(value) { - this.#theme['--link-text-color'] = value; - } - - get scrollbarColor() { - return this.#theme['--scrollbar-color']; - } - - set scrollbarColor(value) { - this.#theme['--scrollbar-color'] = value; - } - - get popupBorderColor() { - return this.#theme['--popup-border-color']; - } - - set popupBorderColor(value) { - this.#theme['--popup-border-color'] = value; - } - - get popupIconColor() { - return this.#theme['--popup-icon-color']; - } - - set popupIconColor(value) { - this.#theme['--popup-icon-color'] = value; - } - - get popupBackgroundColor() { - return this.#theme['--popup-background-color']; - } - - set popupBackgroundColor(value) { - this.#theme['--popup-background-color'] = value; - } - - get popupTextColor() { - return this.#theme['--popup-text-color']; - } - - set popupTextColor(value) { - this.#theme['--popup-text-color'] = value; - } - - get popupActiveColor() { - return this.#theme['--popup-active-color']; - } - - set popupActiveColor(value) { - this.#theme['--popup-active-color'] = value; - } - - get dangerColor() { - return this.#theme['--danger-color']; - } - - set dangerColor(value) { - this.#theme['--danger-color'] = value; - } - - get fileTabWidth() { - return this.#theme['--file-tab-width']; - } - - set fileTabWidth(value) { - this.#theme['--file-tab-width'] = value; - } - - get activeTextColor() { - return this.#theme['--active-text-color']; - } - - set activeTextColor(value) { - this.#theme['--active-text-color'] = value; - } - - get css() { - let css = ''; - Object.keys(this.#theme).forEach(key => { - css += `${key}: ${this.#theme[key]};`; - }); - return `:root {${css}}`; - } - - /** - * Gets the theme as an object - * @param {'rgba'|'hex' | 'none'} colorType - * @returns {Object} - */ - toJSON(colorType = 'none') { - const res = { - name: this.name, - type: this.type, - version: this.version, - }; - Object.keys(this.#theme).forEach((key) => { - const color = colorType === 'hex' - ? Color(this.#theme[key]).hex.toString() - : colorType === 'rgba' - ? Color(this.#theme[key]).rgba.toString() - : this.#theme[key]; - res[ThemeBuilder.#toPascal(key)] = color; - }); - return res; - } - - toString() { - return JSON.stringify(this.toJSON()); - } - - /** - * This method is used to set a darkened primary color. - */ - darkenPrimaryColor() { - this.darkenedPrimaryColor = Color(this.primaryColor).darken(0.4).hex.toString(); - } - - /** - * Creates a theme from a CSS string - * @param {string} name - * @param {string} css - * @returns {ThemeBuilder} - */ - static fromCSS(name, css) { - const themeBuilder = new ThemeBuilder(name); - - // get rules using regex - const rules = css.match(/:root\s*{([^}]*)}/); - if (!rules) throw new Error('Invalid CSS string'); - - // get variables using regex - const variables = rules[1].match(/--[\w-]+:\s*[^;]+/g); - if (!variables) throw new Error('Invalid CSS string'); - - // set variables - variables.forEach((variable) => { - const [key, value] = variable.split(':'); - themeBuilder(ThemeBuilder.#toPascal(key.trim()), value.trim()); - }); - - return themeBuilder; - } - - static fromJSON(theme) { - if (!theme) throw new Error('Theme is required'); - if (typeof theme !== 'object') throw new Error('Theme must be an object'); - if (!theme.name) throw new Error('Theme name is required'); - if (!theme.type) throw new Error('Theme type is required'); - if (!theme.version) throw new Error('Theme version is required'); - const themeBuilder = new ThemeBuilder(theme.name, theme.type, theme.version); - - Object.keys(theme).forEach((key) => { - if (!Object.getOwnPropertyDescriptor(ThemeBuilder.prototype, key)) return; - themeBuilder[key] = theme[key]; - }); - - return themeBuilder; - } - - /** - * - * @param {string} str - */ - static #toPascal(str) { - // e.g. '--primary-color' => 'PrimaryColor' - return str.replace(/^--/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase()); - } + #theme = { + "--popup-border-radius": "4px", + "--active-color": "rgb(51, 153, 255)", + "--active-text-color": "rgb(255, 215, 0)", + "--active-icon-color": "rgba(0, 0, 0, 0.2)", + "--border-color": "rgba(122, 122, 122, 0.2)", + "--box-shadow-color": "rgba(0, 0, 0, 0.2)", + "--button-active-color": "rgb(44, 142, 240)", + "--button-background-color": "rgb(51, 153, 255)", + "--button-text-color": "rgb(255, 255, 255)", + "--error-text-color": "rgb(255, 185, 92)", + "--primary-color": "rgb(153, 153, 255)", + "--primary-text-color": "rgb(255, 255, 255)", + "--secondary-color": "rgb(255, 255, 255)", + "--secondary-text-color": "rgb(37, 37, 37)", + "--link-text-color": "rgb(97, 94, 253)", + "--scrollbar-color": "rgba(0, 0, 0, 0.3)", + "--popup-border-color": "rgba(0, 0, 0, 0)", + "--popup-icon-color": "rgb(153, 153, 255)", + "--popup-background-color": "rgb(255, 255, 255)", + "--popup-text-color": "rgb(37, 37, 37)", + "--popup-active-color": "rgb(169, 0, 0)", + "--danger-color": "rgb(160, 51, 0)", + "--danger-text-color": "rgb(255, 255, 255)", + "--file-tab-width": "120px", + }; + + version = "free"; + name = "Default"; + type = "light"; + darkenedPrimaryColor = "rgb(92, 92, 153)"; + /** Whether Auto update darkened primary color when primary color is updated. */ + autoDarkened = true; + preferredEditorTheme = null; + preferredFont = null; + + /** + * Creates a theme + * @param {string} [name] name of the theme + * @param {'dark'|'light'} [type] type of the theme + * @param {'free'|'paid'} [version] version of the theme + */ + constructor(name = "", type = "dark", version = "free") { + this.name = name; + this.type = type; + this.version = version; + } + + get id() { + return this.name.toLowerCase(); + } + + get popupBorderRadius() { + return this.#theme["--popup-border-radius"]; + } + + set popupBorderRadius(value) { + this.#theme["--popup-border-radius"] = value; + } + + get activeColor() { + return this.#theme["--active-color"]; + } + + set activeColor(value) { + this.#theme["--active-color"] = value; + } + + get activeIconColor() { + return this.#theme["--active-icon-color"]; + } + + set activeIconColor(value) { + this.#theme["--active-icon-color"] = value; + } + + get borderColor() { + return this.#theme["--border-color"]; + } + + set borderColor(value) { + this.#theme["--border-color"] = value; + } + + get boxShadowColor() { + return this.#theme["--box-shadow-color"]; + } + + set boxShadowColor(value) { + this.#theme["--box-shadow-color"] = value; + } + + get buttonActiveColor() { + return this.#theme["--button-active-color"]; + } + + set buttonActiveColor(value) { + this.#theme["--button-active-color"] = value; + } + + get buttonBackgroundColor() { + return this.#theme["--button-background-color"]; + } + + set buttonBackgroundColor(value) { + this.#theme["--button-background-color"] = value; + } + + get buttonTextColor() { + return this.#theme["--button-text-color"]; + } + + set buttonTextColor(value) { + this.#theme["--button-text-color"] = value; + } + + get errorTextColor() { + return this.#theme["--error-text-color"]; + } + + set errorTextColor(value) { + this.#theme["--error-text-color"] = value; + } + + get primaryColor() { + return this.#theme["--primary-color"]; + } + + set primaryColor(value) { + if (this.autoDarkened) { + this.darkenedPrimaryColor = Color(value).darken(0.4).hex.toString(); + } + this.#theme["--primary-color"] = value; + } + + get primaryTextColor() { + return this.#theme["--primary-text-color"]; + } + + set primaryTextColor(value) { + this.#theme["--primary-text-color"] = value; + } + + get secondaryColor() { + return this.#theme["--secondary-color"]; + } + + set secondaryColor(value) { + this.#theme["--secondary-color"] = value; + } + + get secondaryTextColor() { + return this.#theme["--secondary-text-color"]; + } + + set secondaryTextColor(value) { + this.#theme["--secondary-text-color"] = value; + } + + get linkTextColor() { + return this.#theme["--link-text-color"]; + } + + set linkTextColor(value) { + this.#theme["--link-text-color"] = value; + } + + get scrollbarColor() { + return this.#theme["--scrollbar-color"]; + } + + set scrollbarColor(value) { + this.#theme["--scrollbar-color"] = value; + } + + get popupBorderColor() { + return this.#theme["--popup-border-color"]; + } + + set popupBorderColor(value) { + this.#theme["--popup-border-color"] = value; + } + + get popupIconColor() { + return this.#theme["--popup-icon-color"]; + } + + set popupIconColor(value) { + this.#theme["--popup-icon-color"] = value; + } + + get popupBackgroundColor() { + return this.#theme["--popup-background-color"]; + } + + set popupBackgroundColor(value) { + this.#theme["--popup-background-color"] = value; + } + + get popupTextColor() { + return this.#theme["--popup-text-color"]; + } + + set popupTextColor(value) { + this.#theme["--popup-text-color"] = value; + } + + get popupActiveColor() { + return this.#theme["--popup-active-color"]; + } + + set popupActiveColor(value) { + this.#theme["--popup-active-color"] = value; + } + + get dangerColor() { + return this.#theme["--danger-color"]; + } + + set dangerColor(value) { + this.#theme["--danger-color"] = value; + } + + get fileTabWidth() { + return this.#theme["--file-tab-width"]; + } + + set fileTabWidth(value) { + this.#theme["--file-tab-width"] = value; + } + + get activeTextColor() { + return this.#theme["--active-text-color"]; + } + + set activeTextColor(value) { + this.#theme["--active-text-color"] = value; + } + + get css() { + let css = ""; + Object.keys(this.#theme).forEach((key) => { + css += `${key}: ${this.#theme[key]};`; + }); + return `:root {${css}}`; + } + + /** + * Gets the theme as an object + * @param {'rgba'|'hex' | 'none'} colorType + * @returns {Object} + */ + toJSON(colorType = "none") { + const res = { + name: this.name, + type: this.type, + version: this.version, + }; + Object.keys(this.#theme).forEach((key) => { + const color = + colorType === "hex" + ? Color(this.#theme[key]).hex.toString() + : colorType === "rgba" + ? Color(this.#theme[key]).rgba.toString() + : this.#theme[key]; + res[ThemeBuilder.#toPascal(key)] = color; + }); + return res; + } + + toString() { + return JSON.stringify(this.toJSON()); + } + + /** + * This method is used to set a darkened primary color. + */ + darkenPrimaryColor() { + this.darkenedPrimaryColor = Color(this.primaryColor) + .darken(0.4) + .hex.toString(); + } + + /** + * Creates a theme from a CSS string + * @param {string} name + * @param {string} css + * @returns {ThemeBuilder} + */ + static fromCSS(name, css) { + const themeBuilder = new ThemeBuilder(name); + + // get rules using regex + const rules = css.match(/:root\s*{([^}]*)}/); + if (!rules) throw new Error("Invalid CSS string"); + + // get variables using regex + const variables = rules[1].match(/--[\w-]+:\s*[^;]+/g); + if (!variables) throw new Error("Invalid CSS string"); + + // set variables + variables.forEach((variable) => { + const [key, value] = variable.split(":"); + themeBuilder(ThemeBuilder.#toPascal(key.trim()), value.trim()); + }); + + return themeBuilder; + } + + static fromJSON(theme) { + if (!theme) throw new Error("Theme is required"); + if (typeof theme !== "object") throw new Error("Theme must be an object"); + if (!theme.name) throw new Error("Theme name is required"); + if (!theme.type) throw new Error("Theme type is required"); + if (!theme.version) throw new Error("Theme version is required"); + const themeBuilder = new ThemeBuilder( + theme.name, + theme.type, + theme.version, + ); + + Object.keys(theme).forEach((key) => { + if (!Object.getOwnPropertyDescriptor(ThemeBuilder.prototype, key)) return; + themeBuilder[key] = theme[key]; + }); + + return themeBuilder; + } + + /** + * + * @param {string} str + */ + static #toPascal(str) { + // e.g. '--primary-color' => 'PrimaryColor' + return str + .replace(/^--/, "") + .replace(/-([a-z])/g, (g) => g[1].toUpperCase()); + } } -export function createBuiltInTheme(name, type, version = 'paid') { - const theme = new ThemeBuilder(name, type, version); - theme.autoDarkened = false; - return theme; +export function createBuiltInTheme(name, type, version = "paid") { + const theme = new ThemeBuilder(name, type, version); + theme.autoDarkened = false; + return theme; } diff --git a/src/theme/list.js b/src/theme/list.js index 37afa991b..83fa2e779 100644 --- a/src/theme/list.js +++ b/src/theme/list.js @@ -1,153 +1,155 @@ -import Url from 'utils/Url'; -import color from 'utils/color'; -import fsOperation from 'fileSystem'; -import fonts from '../lib/fonts'; -import themes from './preInstalled'; -import settings from '../lib/settings'; -import ThemeBuilder from './builder'; - -/** @type {Map} */ -const appThemes = new Map(); -let themeApplied = false; - -function init() { - themes.forEach((theme) => add(theme)); -} - -/** - * @typedef {object} Theme - * @property {string} id - * @property {string} name - * @property {string} type - * @property {string} version - * @property {string} primaryColor - */ - -/** - * Returns a list of all themes - * @returns {Theme[]} - */ -function list() { - return Array.from(appThemes.keys()).map((name) => { - const { id, type, primaryColor, version } = appThemes.get(name); - return { - id, - type, - version, - primaryColor, - name: name.capitalize(), - }; - }); -} - -/** - * - * @param {string} name - * @returns {ThemeBuilder} - */ -function get(name) { - return appThemes.get(name.toLowerCase()); -} - -/** - * - * @param {ThemeBuilder} theme - * @returns - */ -function add(theme) { - if (!(theme instanceof ThemeBuilder)) return; - if (appThemes.has(theme.id)) return; - appThemes.set(theme.id, theme); - if (settings.value.appTheme === theme.id) { - apply(theme.id); - } -} - -/** - * Apply a theme - * @param {string} id The name of the theme to apply - * @param {boolean} init Whether or not this is the first time the theme is being applied - */ -async function apply(id, init) { - if (!DOES_SUPPORT_THEME) { - id = 'default'; - } - - themeApplied = true; - const loaderFile = Url.join(ASSETS_DIRECTORY, 'res/tail-spin.svg'); - const svgName = '__tail-spin__.svg'; - const img = Url.join(DATA_STORAGE, svgName); - const theme = get(id); - const $style = document.head.get('style#app-theme') ?? ; - const update = { - appTheme: id, - }; - - if (id === 'custom') { - update.customTheme = theme.toJSON(); - } - - if (init && theme.preferredEditorTheme) { - update.editorTheme = theme.preferredEditorTheme; - editorManager.editor.setTheme(theme.preferredEditorTheme); - } - - if (init && theme.preferredFont) { - update.editorFont = theme.preferredFont; - fonts.setFont(theme.preferredFont); - } - - settings.update(update, false); - localStorage.__primary_color = theme.primaryColor; - document.body.setAttribute('theme-type', theme.type); - $style.textContent = theme.css; - document.head.append($style); - - // Set status bar and navigation bar color - system.setUiTheme( - color(theme.primaryColor).hex.toString(), - theme.toJSON('hex'), - ); - - try { - let fs = fsOperation(loaderFile); - const svg = await fs.readFile('utf8'); - - fs = fsOperation(img); - if (!(await fs.exists())) { - await fsOperation(DATA_STORAGE).createFile(svgName); - } - await fs.writeFile( - svg.replace(/#fff/g, theme.primaryColor), - ); - } catch (error) { - console.error(error); - } -} - -/** - * Update a theme - * @param {ThemeBuilder} theme - */ -function update(theme) { - if (!(theme instanceof ThemeBuilder)) return; - const oldTheme = get(theme.id); - if (!oldTheme) { - add(theme); - return; - } - const json = theme.toJSON(); - Object.keys(json).forEach((key) => { - oldTheme[key] = json[key]; - }); -} - -export default { - get applied() { return themeApplied; }, - init, - list, - get, - add, - apply, - update, -}; +import fsOperation from "fileSystem"; +import Url from "utils/Url"; +import color from "utils/color"; +import fonts from "../lib/fonts"; +import settings from "../lib/settings"; +import ThemeBuilder from "./builder"; +import themes from "./preInstalled"; + +/** @type {Map} */ +const appThemes = new Map(); +let themeApplied = false; + +function init() { + themes.forEach((theme) => add(theme)); +} + +/** + * @typedef {object} Theme + * @property {string} id + * @property {string} name + * @property {string} type + * @property {string} version + * @property {string} primaryColor + */ + +/** + * Returns a list of all themes + * @returns {Theme[]} + */ +function list() { + return Array.from(appThemes.keys()).map((name) => { + const { id, type, primaryColor, version } = appThemes.get(name); + return { + id, + type, + version, + primaryColor, + name: name.capitalize(), + }; + }); +} + +/** + * + * @param {string} name + * @returns {ThemeBuilder} + */ +function get(name) { + return appThemes.get(name.toLowerCase()); +} + +/** + * + * @param {ThemeBuilder} theme + * @returns + */ +function add(theme) { + if (!(theme instanceof ThemeBuilder)) return; + if (appThemes.has(theme.id)) return; + appThemes.set(theme.id, theme); + if (settings.value.appTheme === theme.id) { + apply(theme.id); + } +} + +/** + * Apply a theme + * @param {string} id The name of the theme to apply + * @param {boolean} init Whether or not this is the first time the theme is being applied + */ +async function apply(id, init) { + if (!DOES_SUPPORT_THEME) { + id = "default"; + } + + themeApplied = true; + const loaderFile = Url.join(ASSETS_DIRECTORY, "res/tail-spin.svg"); + const svgName = "__tail-spin__.svg"; + const img = Url.join(DATA_STORAGE, svgName); + const theme = get(id); + const $style = document.head.get("style#app-theme") ?? ( + + ); + const update = { + appTheme: id, + }; + + if (id === "custom") { + update.customTheme = theme.toJSON(); + } + + if (init && theme.preferredEditorTheme) { + update.editorTheme = theme.preferredEditorTheme; + editorManager.editor.setTheme(theme.preferredEditorTheme); + } + + if (init && theme.preferredFont) { + update.editorFont = theme.preferredFont; + fonts.setFont(theme.preferredFont); + } + + settings.update(update, false); + localStorage.__primary_color = theme.primaryColor; + document.body.setAttribute("theme-type", theme.type); + $style.textContent = theme.css; + document.head.append($style); + + // Set status bar and navigation bar color + system.setUiTheme( + color(theme.primaryColor).hex.toString(), + theme.toJSON("hex"), + ); + + try { + let fs = fsOperation(loaderFile); + const svg = await fs.readFile("utf8"); + + fs = fsOperation(img); + if (!(await fs.exists())) { + await fsOperation(DATA_STORAGE).createFile(svgName); + } + await fs.writeFile(svg.replace(/#fff/g, theme.primaryColor)); + } catch (error) { + console.error(error); + } +} + +/** + * Update a theme + * @param {ThemeBuilder} theme + */ +function update(theme) { + if (!(theme instanceof ThemeBuilder)) return; + const oldTheme = get(theme.id); + if (!oldTheme) { + add(theme); + return; + } + const json = theme.toJSON(); + Object.keys(json).forEach((key) => { + oldTheme[key] = json[key]; + }); +} + +export default { + get applied() { + return themeApplied; + }, + init, + list, + get, + add, + apply, + update, +}; diff --git a/src/theme/preInstalled.js b/src/theme/preInstalled.js index 49c8ab8d2..a3acefe5a 100644 --- a/src/theme/preInstalled.js +++ b/src/theme/preInstalled.js @@ -1,183 +1,183 @@ -import { createBuiltInTheme } from './builder'; +import { createBuiltInTheme } from "./builder"; -const WHITE = 'rgb(255, 255, 255)'; -const BLACK = 'rgb(0, 0, 0)'; +const WHITE = "rgb(255, 255, 255)"; +const BLACK = "rgb(0, 0, 0)"; -const dark = createBuiltInTheme('Dark', 'dark', 'free'); -dark.primaryColor = 'rgb(49, 49, 49)'; +const dark = createBuiltInTheme("Dark", "dark", "free"); +dark.primaryColor = "rgb(49, 49, 49)"; dark.primaryTextColor = WHITE; -dark.darkenedPrimaryColor = 'rgb(29, 29, 29)'; -dark.secondaryColor = 'rgb(37, 37, 37)'; +dark.darkenedPrimaryColor = "rgb(29, 29, 29)"; +dark.secondaryColor = "rgb(37, 37, 37)"; dark.secondaryTextColor = WHITE; -dark.activeColor = 'rgb(51, 153, 255)'; -dark.linkTextColor = 'rgb(181, 180, 233)'; -dark.borderColor = 'rgba(230, 230, 230, 0.2)'; +dark.activeColor = "rgb(51, 153, 255)"; +dark.linkTextColor = "rgb(181, 180, 233)"; +dark.borderColor = "rgba(230, 230, 230, 0.2)"; dark.popupIconColor = WHITE; -dark.popupBackgroundColor = 'rgb(49, 49, 49)'; +dark.popupBackgroundColor = "rgb(49, 49, 49)"; dark.popupTextColor = WHITE; -dark.popupActiveColor = 'rgb(255, 215, 0)'; +dark.popupActiveColor = "rgb(255, 215, 0)"; -const oled = createBuiltInTheme('OLED'); -oled.primaryColor = 'rgb(0, 0, 0)'; +const oled = createBuiltInTheme("OLED"); +oled.primaryColor = "rgb(0, 0, 0)"; oled.primaryTextColor = WHITE; -oled.darkenedPrimaryColor = 'rgb(0, 0, 0)'; -oled.secondaryColor = 'rgb(0, 0, 0)'; +oled.darkenedPrimaryColor = "rgb(0, 0, 0)"; +oled.secondaryColor = "rgb(0, 0, 0)"; oled.secondaryTextColor = WHITE; -oled.activeColor = 'rgb(56, 56, 56)'; -oled.activeIconColor = 'rgba(255, 255, 255, 0.2)'; -oled.linkTextColor = 'rgb(181, 180, 233)'; -oled.borderColor = 'rgb(124, 124, 124)'; +oled.activeColor = "rgb(56, 56, 56)"; +oled.activeIconColor = "rgba(255, 255, 255, 0.2)"; +oled.linkTextColor = "rgb(181, 180, 233)"; +oled.borderColor = "rgb(124, 124, 124)"; oled.popupIconColor = WHITE; -oled.popupBackgroundColor = 'rgb(0, 0, 0)'; +oled.popupBackgroundColor = "rgb(0, 0, 0)"; oled.popupTextColor = WHITE; -oled.popupActiveColor = 'rgb(121, 103, 0)'; -oled.popupBorderColor = 'rgba(255, 255, 255, 0.4)'; +oled.popupActiveColor = "rgb(121, 103, 0)"; +oled.popupBorderColor = "rgba(255, 255, 255, 0.4)"; oled.boxShadowColor = BLACK; -const ocean = createBuiltInTheme('Ocean'); -ocean.darkenedPrimaryColor = 'rgb(19, 19, 26)'; -ocean.primaryColor = 'rgb(32, 32, 44)'; +const ocean = createBuiltInTheme("Ocean"); +ocean.darkenedPrimaryColor = "rgb(19, 19, 26)"; +ocean.primaryColor = "rgb(32, 32, 44)"; ocean.primaryTextColor = WHITE; -ocean.secondaryColor = 'rgb(38, 38, 53)'; +ocean.secondaryColor = "rgb(38, 38, 53)"; ocean.secondaryTextColor = WHITE; -ocean.activeColor = 'rgb(51, 153, 255)'; -ocean.linkTextColor = 'rgb(181, 180, 233)'; -ocean.borderColor = 'rgb(122, 122, 163)'; +ocean.activeColor = "rgb(51, 153, 255)"; +ocean.linkTextColor = "rgb(181, 180, 233)"; +ocean.borderColor = "rgb(122, 122, 163)"; ocean.popupIconColor = WHITE; -ocean.popupBackgroundColor = 'rgb(32, 32, 44)'; +ocean.popupBackgroundColor = "rgb(32, 32, 44)"; ocean.popupTextColor = WHITE; -ocean.popupActiveColor = 'rgb(255, 215, 0)'; -ocean.boxShadowColor = 'rgba(0, 0, 0, 0.5)'; -ocean.preferredEditorTheme = 'ace/theme/solarized_dark'; -ocean.preferredFont = 'Fira Code'; +ocean.popupActiveColor = "rgb(255, 215, 0)"; +ocean.boxShadowColor = "rgba(0, 0, 0, 0.5)"; +ocean.preferredEditorTheme = "ace/theme/solarized_dark"; +ocean.preferredFont = "Fira Code"; -const bump = createBuiltInTheme('Bump'); -bump.darkenedPrimaryColor = 'rgb(28, 33, 38)'; -bump.primaryColor = 'rgb(48, 56, 65)'; -bump.primaryTextColor = 'rgb(236, 236, 236)'; -bump.secondaryColor = 'rgb(48, 71, 94)'; -bump.secondaryTextColor = 'rgb(236, 236, 236)'; -bump.activeColor = 'rgb(242, 163, 101)'; -bump.linkTextColor = 'rgb(181, 180, 233)'; -bump.borderColor = 'rgb(107, 120, 136)'; -bump.popupIconColor = 'rgb(236, 236, 236)'; -bump.popupBackgroundColor = 'rgb(48, 56, 65)'; -bump.popupTextColor = 'rgb(236, 236, 236)'; -bump.popupActiveColor = 'rgb(255, 215, 0)'; -bump.buttonBackgroundColor = 'rgb(242, 163, 101)'; -bump.buttonTextColor = 'rgb(236, 236, 236)'; -bump.buttonActiveColor = 'rgb(212, 137, 79)'; +const bump = createBuiltInTheme("Bump"); +bump.darkenedPrimaryColor = "rgb(28, 33, 38)"; +bump.primaryColor = "rgb(48, 56, 65)"; +bump.primaryTextColor = "rgb(236, 236, 236)"; +bump.secondaryColor = "rgb(48, 71, 94)"; +bump.secondaryTextColor = "rgb(236, 236, 236)"; +bump.activeColor = "rgb(242, 163, 101)"; +bump.linkTextColor = "rgb(181, 180, 233)"; +bump.borderColor = "rgb(107, 120, 136)"; +bump.popupIconColor = "rgb(236, 236, 236)"; +bump.popupBackgroundColor = "rgb(48, 56, 65)"; +bump.popupTextColor = "rgb(236, 236, 236)"; +bump.popupActiveColor = "rgb(255, 215, 0)"; +bump.buttonBackgroundColor = "rgb(242, 163, 101)"; +bump.buttonTextColor = "rgb(236, 236, 236)"; +bump.buttonActiveColor = "rgb(212, 137, 79)"; -const bling = createBuiltInTheme('Bling'); -bling.darkenedPrimaryColor = 'rgb(19, 19, 38)'; -bling.primaryColor = 'rgb(32, 32, 64)'; -bling.primaryTextColor = 'rgb(255, 189, 105)'; -bling.secondaryColor = 'rgb(84, 56, 100)'; -bling.secondaryTextColor = 'rgb(255, 189, 105)'; -bling.activeColor = 'rgb(255, 99, 99)'; -bling.linkTextColor = 'rgb(181, 180, 233)'; -bling.borderColor = 'rgb(93, 93, 151)'; -bling.popupIconColor = 'rgb(255, 189, 105)'; -bling.popupBackgroundColor = 'rgb(32, 32, 64)'; -bling.popupTextColor = 'rgb(255, 189, 105)'; -bling.popupActiveColor = 'rgb(51, 153, 255)'; -bling.buttonBackgroundColor = 'rgb(255, 99, 99)'; -bling.buttonTextColor = 'rgb(255, 189, 105)'; -bling.buttonActiveColor = 'rgb(160, 99, 52)'; +const bling = createBuiltInTheme("Bling"); +bling.darkenedPrimaryColor = "rgb(19, 19, 38)"; +bling.primaryColor = "rgb(32, 32, 64)"; +bling.primaryTextColor = "rgb(255, 189, 105)"; +bling.secondaryColor = "rgb(84, 56, 100)"; +bling.secondaryTextColor = "rgb(255, 189, 105)"; +bling.activeColor = "rgb(255, 99, 99)"; +bling.linkTextColor = "rgb(181, 180, 233)"; +bling.borderColor = "rgb(93, 93, 151)"; +bling.popupIconColor = "rgb(255, 189, 105)"; +bling.popupBackgroundColor = "rgb(32, 32, 64)"; +bling.popupTextColor = "rgb(255, 189, 105)"; +bling.popupActiveColor = "rgb(51, 153, 255)"; +bling.buttonBackgroundColor = "rgb(255, 99, 99)"; +bling.buttonTextColor = "rgb(255, 189, 105)"; +bling.buttonActiveColor = "rgb(160, 99, 52)"; -const moon = createBuiltInTheme('Moon'); -moon.darkenedPrimaryColor = 'rgb(20, 24, 29)'; -moon.primaryColor = 'rgb(34, 40, 49)'; -moon.primaryTextColor = 'rgb(0, 255, 245)'; -moon.secondaryColor = 'rgb(57, 62, 70)'; -moon.secondaryTextColor = 'rgb(0, 255, 245)'; -moon.activeColor = 'rgb(0, 173, 181)'; -moon.linkTextColor = 'rgb(181, 180, 233)'; -moon.borderColor = 'rgb(90, 101, 117)'; -moon.popupIconColor = 'rgb(0, 255, 245)'; -moon.popupBackgroundColor = 'rgb(34, 40, 49)'; -moon.popupTextColor = 'rgb(0, 255, 245)'; -moon.popupActiveColor = 'rgb(51, 153, 255)'; -moon.buttonBackgroundColor = 'rgb(0, 173, 181)'; -moon.buttonTextColor = 'rgb(0, 142, 149)'; -moon.buttonActiveColor = 'rgb(0, 173, 181)'; +const moon = createBuiltInTheme("Moon"); +moon.darkenedPrimaryColor = "rgb(20, 24, 29)"; +moon.primaryColor = "rgb(34, 40, 49)"; +moon.primaryTextColor = "rgb(0, 255, 245)"; +moon.secondaryColor = "rgb(57, 62, 70)"; +moon.secondaryTextColor = "rgb(0, 255, 245)"; +moon.activeColor = "rgb(0, 173, 181)"; +moon.linkTextColor = "rgb(181, 180, 233)"; +moon.borderColor = "rgb(90, 101, 117)"; +moon.popupIconColor = "rgb(0, 255, 245)"; +moon.popupBackgroundColor = "rgb(34, 40, 49)"; +moon.popupTextColor = "rgb(0, 255, 245)"; +moon.popupActiveColor = "rgb(51, 153, 255)"; +moon.buttonBackgroundColor = "rgb(0, 173, 181)"; +moon.buttonTextColor = "rgb(0, 142, 149)"; +moon.buttonActiveColor = "rgb(0, 173, 181)"; -const atticus = createBuiltInTheme('Atticus'); -atticus.darkenedPrimaryColor = 'rgb(32, 30, 30)'; -atticus.primaryColor = 'rgb(54, 51, 51)'; -atticus.primaryTextColor = 'rgb(246, 233, 233)'; -atticus.secondaryColor = 'rgb(39, 33, 33)'; -atticus.secondaryTextColor = 'rgb(246, 233, 233)'; -atticus.activeColor = 'rgb(225, 100, 40)'; -atticus.linkTextColor = 'rgb(181, 180, 233)'; -atticus.borderColor = 'rgb(117, 111, 111)'; -atticus.popupIconColor = 'rgb(246, 233, 233)'; -atticus.popupBackgroundColor = 'rgb(54, 51, 51)'; -atticus.popupTextColor = 'rgb(246, 233, 233)'; -atticus.popupActiveColor = 'rgb(51, 153, 255)'; -atticus.buttonBackgroundColor = 'rgb(225, 100, 40)'; -atticus.buttonTextColor = 'rgb(246, 233, 233)'; -atticus.buttonActiveColor = 'rgb(0, 145, 153)'; +const atticus = createBuiltInTheme("Atticus"); +atticus.darkenedPrimaryColor = "rgb(32, 30, 30)"; +atticus.primaryColor = "rgb(54, 51, 51)"; +atticus.primaryTextColor = "rgb(246, 233, 233)"; +atticus.secondaryColor = "rgb(39, 33, 33)"; +atticus.secondaryTextColor = "rgb(246, 233, 233)"; +atticus.activeColor = "rgb(225, 100, 40)"; +atticus.linkTextColor = "rgb(181, 180, 233)"; +atticus.borderColor = "rgb(117, 111, 111)"; +atticus.popupIconColor = "rgb(246, 233, 233)"; +atticus.popupBackgroundColor = "rgb(54, 51, 51)"; +atticus.popupTextColor = "rgb(246, 233, 233)"; +atticus.popupActiveColor = "rgb(51, 153, 255)"; +atticus.buttonBackgroundColor = "rgb(225, 100, 40)"; +atticus.buttonTextColor = "rgb(246, 233, 233)"; +atticus.buttonActiveColor = "rgb(0, 145, 153)"; -const tomyris = createBuiltInTheme('Tomyris'); -tomyris.darkenedPrimaryColor = 'rgb(32, 30, 30)'; -tomyris.primaryColor = 'rgb(59, 9, 68)'; -tomyris.primaryTextColor = 'rgb(241, 187, 213)'; -tomyris.secondaryColor = 'rgb(95, 24, 84)'; -tomyris.secondaryTextColor = 'rgb(144, 184, 248)'; -tomyris.activeColor = 'rgb(161, 37, 89)'; -tomyris.linkTextColor = 'rgb(181, 180, 233)'; -tomyris.borderColor = 'rgb(140, 58, 155)'; -tomyris.popupIconColor = 'rgb(241, 187, 213)'; -tomyris.popupBackgroundColor = 'rgb(59, 9, 68)'; -tomyris.popupTextColor = 'rgb(241, 187, 213)'; -tomyris.popupActiveColor = 'rgb(51, 153, 255)'; -tomyris.buttonBackgroundColor = 'rgb(161, 37, 89)'; -tomyris.buttonTextColor = 'rgb(241, 187, 213)'; -tomyris.buttonActiveColor = 'rgb(0, 145, 153)'; +const tomyris = createBuiltInTheme("Tomyris"); +tomyris.darkenedPrimaryColor = "rgb(32, 30, 30)"; +tomyris.primaryColor = "rgb(59, 9, 68)"; +tomyris.primaryTextColor = "rgb(241, 187, 213)"; +tomyris.secondaryColor = "rgb(95, 24, 84)"; +tomyris.secondaryTextColor = "rgb(144, 184, 248)"; +tomyris.activeColor = "rgb(161, 37, 89)"; +tomyris.linkTextColor = "rgb(181, 180, 233)"; +tomyris.borderColor = "rgb(140, 58, 155)"; +tomyris.popupIconColor = "rgb(241, 187, 213)"; +tomyris.popupBackgroundColor = "rgb(59, 9, 68)"; +tomyris.popupTextColor = "rgb(241, 187, 213)"; +tomyris.popupActiveColor = "rgb(51, 153, 255)"; +tomyris.buttonBackgroundColor = "rgb(161, 37, 89)"; +tomyris.buttonTextColor = "rgb(241, 187, 213)"; +tomyris.buttonActiveColor = "rgb(0, 145, 153)"; -const menes = createBuiltInTheme('Menes'); -menes.darkenedPrimaryColor = 'rgb(31, 34, 38)'; -menes.primaryColor = 'rgb(53, 57, 65)'; -menes.primaryTextColor = 'rgb(144, 184, 248)'; -menes.secondaryColor = 'rgb(38, 40, 43)'; -menes.secondaryTextColor = 'rgb(144, 184, 248)'; -menes.activeColor = 'rgb(95, 133, 219)'; -menes.linkTextColor = 'rgb(181, 180, 233)'; -menes.borderColor = 'rgb(117, 123, 134)'; -menes.popupIconColor = 'rgb(144, 184, 248)'; -menes.popupBackgroundColor = 'rgb(54, 59, 78)'; -menes.popupTextColor = 'rgb(144, 184, 248)'; -menes.popupActiveColor = 'rgb(51, 153, 255)'; -menes.buttonBackgroundColor = 'rgb(95, 133, 219)'; -menes.buttonTextColor = 'rgb(144, 184, 248)'; -menes.buttonActiveColor = 'rgb(0, 145, 153)'; +const menes = createBuiltInTheme("Menes"); +menes.darkenedPrimaryColor = "rgb(31, 34, 38)"; +menes.primaryColor = "rgb(53, 57, 65)"; +menes.primaryTextColor = "rgb(144, 184, 248)"; +menes.secondaryColor = "rgb(38, 40, 43)"; +menes.secondaryTextColor = "rgb(144, 184, 248)"; +menes.activeColor = "rgb(95, 133, 219)"; +menes.linkTextColor = "rgb(181, 180, 233)"; +menes.borderColor = "rgb(117, 123, 134)"; +menes.popupIconColor = "rgb(144, 184, 248)"; +menes.popupBackgroundColor = "rgb(54, 59, 78)"; +menes.popupTextColor = "rgb(144, 184, 248)"; +menes.popupActiveColor = "rgb(51, 153, 255)"; +menes.buttonBackgroundColor = "rgb(95, 133, 219)"; +menes.buttonTextColor = "rgb(144, 184, 248)"; +menes.buttonActiveColor = "rgb(0, 145, 153)"; -const light = createBuiltInTheme('Light', 'light'); -light.darkenedPrimaryColor = 'rgb(153, 153, 153)'; +const light = createBuiltInTheme("Light", "light"); +light.darkenedPrimaryColor = "rgb(153, 153, 153)"; light.primaryColor = WHITE; -light.primaryTextColor = 'rgb(51, 62, 89)'; +light.primaryTextColor = "rgb(51, 62, 89)"; light.secondaryColor = WHITE; -light.secondaryTextColor = 'rgb(51, 62, 89)'; -light.activeColor = 'rgb(51, 153, 255)'; -light.linkTextColor = 'rgb(104, 103, 149)'; -light.borderColor = 'rgb(153, 153, 153)'; -light.popupIconColor = 'rgb(51, 62, 89)'; +light.secondaryTextColor = "rgb(51, 62, 89)"; +light.activeColor = "rgb(51, 153, 255)"; +light.linkTextColor = "rgb(104, 103, 149)"; +light.borderColor = "rgb(153, 153, 153)"; +light.popupIconColor = "rgb(51, 62, 89)"; -const custom = createBuiltInTheme('Custom'); +const custom = createBuiltInTheme("Custom"); custom.autoDarkened = true; export default [ - createBuiltInTheme('default', 'dark', 'free'), - dark, - oled, - ocean, - bump, - bling, - moon, - atticus, - tomyris, - menes, - light, - custom, -]; \ No newline at end of file + createBuiltInTheme("default", "dark", "free"), + dark, + oled, + ocean, + bump, + bling, + moon, + atticus, + tomyris, + menes, + light, + custom, +]; diff --git a/src/utils/Path.js b/src/utils/Path.js index e73f00fad..dd5d9f126 100644 --- a/src/utils/Path.js +++ b/src/utils/Path.js @@ -1,191 +1,191 @@ export default { - /** - * The path.dirname() method returns the directory name of a path, - * similar to the Unix dirname command. - * Trailing directory separators are ignored. - * @param {string} path - * @returns {string} - */ - dirname(path) { - if (path.endsWith('/')) path = path.slice(0, -1); - const parts = path.split('/').slice(0, -1); - if (!/^(\.|\.\.|)$/.test(parts[0])) parts.unshift('.'); - const res = parts.join('/'); - - if (!res) return '/'; - else return res; - }, - - /** - * The path.basename() methods returns the last portion of a path, - * similar to the Unix basename command. - * Trailing directory separators are ignored, see path.sep. - * @param {string} path - * @returns {string} - */ - basename(path, ext = '') { - ext = ext || ''; - if (path === '' || path === '/') return path; - const ar = path.split('/'); - const last = ar.slice(-1)[0]; - if (!last) return ar.slice(-2)[0]; - let res = decodeURI(last.split('?')[0] || ''); - if (this.extname(res) === ext) res = res.replace(new RegExp(ext + '$'), ''); - return res; - }, - - /** - * returns the extension of the path, from the last occurrence of the . (period) - * character to end of string in the last portion of the path. - * If there is no . in the last portion of the path, or if there are no . characters - * other than the first character of the basename of path (see path.basename()) , an - * empty string is returned. - * @param {string} path - */ - extname(path) { - const filename = path.split('/').slice(-1)[0]; - if (/.+\..*$/.test(filename)) { - return /(?:\.([^.]*))?$/.exec(filename)[0] || ''; - } - - return ''; - }, - - /** - * returns a path string from an object. - * @param {PathObject} pathObject - */ - format(pathObject) { - let { root, dir, ext, name, base } = pathObject; - - if (base || !ext.startsWith('.')) { - ext = ''; - if (base) name = ''; - } - - dir = dir || root; - - if (!dir.endsWith('/')) dir += '/'; - - return dir + (base || name) + ext; - }, - - /** - * The path.isAbsolute() method determines if path is an absolute path. - * @param {string} path - */ - isAbsolute(path) { - return path.startsWith('/'); - }, - - /** - * Joins the given number of paths - * @param {...string} paths - */ - join(...paths) { - let res = paths.join('/'); - return this.normalize(res); - }, - - /** - * Normalizes the given path, resolving '..' and '.' segments. - * @param {string} path - */ - normalize(path) { - path = path.replace(/\.\/+/g, './'); - path = path.replace(/\/+/g, '/'); - - const resolved = []; - const pathAr = path.split('/'); - - pathAr.forEach((dir) => { - if (dir === '..') { - if (resolved.length) resolved.pop(); - } else if (dir === '.') { - return; - } else { - resolved.push(dir); - } - }); - - return resolved.join('/'); - }, - - /** - * - * @param {string} path - * @returns {PathObject} - */ - parse(path) { - const root = path.startsWith('/') ? '/' : ''; - const dir = this.dirname(path); - const ext = this.extname(path); - const name = this.basename(path, ext); - const base = this.basename(path); - - return { - root, - dir, - base, - ext, - name, - }; - }, - - /** + /** + * The path.dirname() method returns the directory name of a path, + * similar to the Unix dirname command. + * Trailing directory separators are ignored. + * @param {string} path + * @returns {string} + */ + dirname(path) { + if (path.endsWith("/")) path = path.slice(0, -1); + const parts = path.split("/").slice(0, -1); + if (!/^(\.|\.\.|)$/.test(parts[0])) parts.unshift("."); + const res = parts.join("/"); + + if (!res) return "/"; + else return res; + }, + + /** + * The path.basename() methods returns the last portion of a path, + * similar to the Unix basename command. + * Trailing directory separators are ignored, see path.sep. + * @param {string} path + * @returns {string} + */ + basename(path, ext = "") { + ext = ext || ""; + if (path === "" || path === "/") return path; + const ar = path.split("/"); + const last = ar.slice(-1)[0]; + if (!last) return ar.slice(-2)[0]; + let res = decodeURI(last.split("?")[0] || ""); + if (this.extname(res) === ext) res = res.replace(new RegExp(ext + "$"), ""); + return res; + }, + + /** + * returns the extension of the path, from the last occurrence of the . (period) + * character to end of string in the last portion of the path. + * If there is no . in the last portion of the path, or if there are no . characters + * other than the first character of the basename of path (see path.basename()) , an + * empty string is returned. + * @param {string} path + */ + extname(path) { + const filename = path.split("/").slice(-1)[0]; + if (/.+\..*$/.test(filename)) { + return /(?:\.([^.]*))?$/.exec(filename)[0] || ""; + } + + return ""; + }, + + /** + * returns a path string from an object. + * @param {PathObject} pathObject + */ + format(pathObject) { + let { root, dir, ext, name, base } = pathObject; + + if (base || !ext.startsWith(".")) { + ext = ""; + if (base) name = ""; + } + + dir = dir || root; + + if (!dir.endsWith("/")) dir += "/"; + + return dir + (base || name) + ext; + }, + + /** + * The path.isAbsolute() method determines if path is an absolute path. + * @param {string} path + */ + isAbsolute(path) { + return path.startsWith("/"); + }, + + /** + * Joins the given number of paths + * @param {...string} paths + */ + join(...paths) { + let res = paths.join("/"); + return this.normalize(res); + }, + + /** + * Normalizes the given path, resolving '..' and '.' segments. + * @param {string} path + */ + normalize(path) { + path = path.replace(/\.\/+/g, "./"); + path = path.replace(/\/+/g, "/"); + + const resolved = []; + const pathAr = path.split("/"); + + pathAr.forEach((dir) => { + if (dir === "..") { + if (resolved.length) resolved.pop(); + } else if (dir === ".") { + return; + } else { + resolved.push(dir); + } + }); + + return resolved.join("/"); + }, + + /** + * + * @param {string} path + * @returns {PathObject} + */ + parse(path) { + const root = path.startsWith("/") ? "/" : ""; + const dir = this.dirname(path); + const ext = this.extname(path); + const name = this.basename(path, ext); + const base = this.basename(path); + + return { + root, + dir, + base, + ext, + name, + }; + }, + + /** * Resolve the path eg. ```js resolvePath('path/to/some/dir/', '../../dir') //returns 'path/to/dir' ``` * @param {...string} paths */ - resolve(...paths) { - if (!paths.length) throw new Error('resolve(...path) : Arguments missing!'); + resolve(...paths) { + if (!paths.length) throw new Error("resolve(...path) : Arguments missing!"); - let result = ''; + let result = ""; - paths.forEach((path) => { - if (path.startsWith('/')) { - result = path; - return; - } + paths.forEach((path) => { + if (path.startsWith("/")) { + result = path; + return; + } - result = this.normalize(this.join(result, path)); - }); + result = this.normalize(this.join(result, path)); + }); - if (result.startsWith('/')) return result; - else return '/' + result; - }, + if (result.startsWith("/")) return result; + else return "/" + result; + }, - /** - * Gets path for path2 relative to path1 - * @param {String} path1 - * @param {String} path2 - */ - convertToRelative(path1, path2) { - path1 = this.normalize(path1).split('/'); - path2 = this.normalize(path2).split('/'); + /** + * Gets path for path2 relative to path1 + * @param {String} path1 + * @param {String} path2 + */ + convertToRelative(path1, path2) { + path1 = this.normalize(path1).split("/"); + path2 = this.normalize(path2).split("/"); - const p1len = path1.length; - const p2len = path2.length; + const p1len = path1.length; + const p2len = path2.length; - let flag = false; - let path = []; + let flag = false; + let path = []; - path1.forEach((dir, i) => { - if (dir === path2[i] && !flag) return; + path1.forEach((dir, i) => { + if (dir === path2[i] && !flag) return; - path.push(path2[i]); - if (!flag) { - flag = true; - return; - } + path.push(path2[i]); + if (!flag) { + flag = true; + return; + } - if (flag) path.unshift('..'); - }); + if (flag) path.unshift(".."); + }); - if (p2len > p1len) path.push(...path2.slice(p1len)); + if (p2len > p1len) path.push(...path2.slice(p1len)); - return path.join('/'); - }, + return path.join("/"); + }, }; diff --git a/src/utils/Uri.js b/src/utils/Uri.js index 5689d8110..f7600603e 100644 --- a/src/utils/Uri.js +++ b/src/utils/Uri.js @@ -1,122 +1,124 @@ -import escapeStringRegexp from 'escape-string-regexp'; -import path from './Path'; +import escapeStringRegexp from "escape-string-regexp"; +import path from "./Path"; export default { - /** - * Parse content uri to rootUri and docID - * - * eg. - *```js - * parse("content://.../AA98-181D%3A::.../index.html") - *``` - * `returns` {rootUri: "content://.../AA98-181D%3A", docId: "...index.html"} - * - * @param {string} contentUri - * @returns {{rootUri: string, docId: string, isFileUri: boolean}} - */ - parse(contentUri) { - let rootUri, - docId = ''; + /** + * Parse content uri to rootUri and docID + * + * eg. + *```js + * parse("content://.../AA98-181D%3A::.../index.html") + *``` + * `returns` {rootUri: "content://.../AA98-181D%3A", docId: "...index.html"} + * + * @param {string} contentUri + * @returns {{rootUri: string, docId: string, isFileUri: boolean}} + */ + parse(contentUri) { + let rootUri, + docId = ""; - const DOC_PROVIDER = - /^content:\/\/com\.((?![:<>"\/\\\|\?\*]).)*\.documents\//; - const TREE_URI = - /^content:\/\/com\.((?![:<>"\/\\\|\?\*]).)*\.documents\/tree\//; - const SINGLE_URI = - /^content:\/\/com\.(((?![:<>"\/\\\|\?\*]).)*)\.documents\/document/; + const DOC_PROVIDER = + /^content:\/\/com\.((?![:<>"\/\\\|\?\*]).)*\.documents\//; + const TREE_URI = + /^content:\/\/com\.((?![:<>"\/\\\|\?\*]).)*\.documents\/tree\//; + const SINGLE_URI = + /^content:\/\/com\.(((?![:<>"\/\\\|\?\*]).)*)\.documents\/document/; - if (DOC_PROVIDER.test(contentUri)) { - if (TREE_URI.test(contentUri)) { - if (/::/.test(contentUri)) { - [rootUri, docId] = contentUri.split('::'); - } else { - rootUri = contentUri; - docId = decodeURIComponent(contentUri.split('/').slice(-1)[0]); - } - } else if (SINGLE_URI.test(contentUri)) { - const [provider, providerId] = SINGLE_URI.exec(contentUri); - docId = decodeURIComponent(contentUri); //DecodUri - docId = docId.replace(provider, ''); //replace single to tree - docId = path.normalize(docId); //normalize docid + if (DOC_PROVIDER.test(contentUri)) { + if (TREE_URI.test(contentUri)) { + if (/::/.test(contentUri)) { + [rootUri, docId] = contentUri.split("::"); + } else { + rootUri = contentUri; + docId = decodeURIComponent(contentUri.split("/").slice(-1)[0]); + } + } else if (SINGLE_URI.test(contentUri)) { + const [provider, providerId] = SINGLE_URI.exec(contentUri); + docId = decodeURIComponent(contentUri); //DecodUri + docId = docId.replace(provider, ""); //replace single to tree + docId = path.normalize(docId); //normalize docid - if (docId.startsWith('/')) docId = docId.slice(1); // remove leading '/' + if (docId.startsWith("/")) docId = docId.slice(1); // remove leading '/' - rootUri = - `content://com.${providerId}.documents/tree/` + - docId.split(':')[0] + - '%3A'; - } + rootUri = + `content://com.${providerId}.documents/tree/` + + docId.split(":")[0] + + "%3A"; + } - return { - rootUri, - docId, - isFileUri: /^file:\/\/\//.test(rootUri), - }; - } else { - throw new Error('Invalid uri format.'); - } - }, - /** - * Formats the five contentUri object to string - * @param {{rootUri: string, docId: string} | String} contentUriObject or rootId - * @param {string} [docId] - * @returns {string} - */ - format(contentUriObject, docId) { - let rootUri; + return { + rootUri, + docId, + isFileUri: /^file:\/\/\//.test(rootUri), + }; + } else { + throw new Error("Invalid uri format."); + } + }, + /** + * Formats the five contentUri object to string + * @param {{rootUri: string, docId: string} | String} contentUriObject or rootId + * @param {string} [docId] + * @returns {string} + */ + format(contentUriObject, docId) { + let rootUri; - if (typeof contentUriObject === 'string') { - rootUri = contentUriObject; - } else { - rootUri = contentUriObject.rootUri; - docId = contentUriObject.docId; - } + if (typeof contentUriObject === "string") { + rootUri = contentUriObject; + } else { + rootUri = contentUriObject.rootUri; + docId = contentUriObject.docId; + } - if (docId) return [rootUri, docId].join('::'); - else return rootUri; - }, - /** - * Gets virtual address by replacing root with name i.e. added in file explorer - * @param {string} url - */ - getVirtualAddress(url) { - try { - const storageList = JSON.parse(localStorage.storageList || '[]'); + if (docId) return [rootUri, docId].join("::"); + else return rootUri; + }, + /** + * Gets virtual address by replacing root with name i.e. added in file explorer + * @param {string} url + */ + getVirtualAddress(url) { + try { + const storageList = JSON.parse(localStorage.storageList || "[]"); - const matches = []; - for (let storage of storageList) { - const regex = new RegExp('^' + escapeStringRegexp(storage.uri ?? storage.url)); - matches.push({ - regex, - charMatched: url.length - url.replace(regex, '').length, - storage, - }); - } + const matches = []; + for (let storage of storageList) { + const regex = new RegExp( + "^" + escapeStringRegexp(storage.uri ?? storage.url), + ); + matches.push({ + regex, + charMatched: url.length - url.replace(regex, "").length, + storage, + }); + } - const matched = matches.sort((a, b) => { - return b.charMatched - a.charMatched; - })[0]; + const matched = matches.sort((a, b) => { + return b.charMatched - a.charMatched; + })[0]; - if (matched) { - const { storage, regex } = matched; - const { name } = storage; - const [base, paths] = url.split('::') - url = base + '/' + paths.split('/').slice(1).join('/'); - return url.replace(regex, name).replace(/\/+/g, '/'); - } + if (matched) { + const { storage, regex } = matched; + const { name } = storage; + const [base, paths] = url.split("::"); + url = base + "/" + paths.split("/").slice(1).join("/"); + return url.replace(regex, name).replace(/\/+/g, "/"); + } - return url; - } catch (e) { - return url; - } - }, - /** - * Gets primary address of a content url. - * @param {string} url - * @returns {string} - */ - getPrimaryAddress(url) { - const [, primary] = url.split('::primary:'); - return primary; - }, -}; \ No newline at end of file + return url; + } catch (e) { + return url; + } + }, + /** + * Gets primary address of a content url. + * @param {string} url + * @returns {string} + */ + getPrimaryAddress(url) { + const [, primary] = url.split("::primary:"); + return primary; + }, +}; diff --git a/src/utils/Url.js b/src/utils/Url.js index d26c944a4..34bca3043 100644 --- a/src/utils/Url.js +++ b/src/utils/Url.js @@ -1,322 +1,322 @@ -import URLParse from 'url-parse'; -import path from './Path'; -import Uri from './Uri'; +import URLParse from "url-parse"; +import path from "./Path"; +import Uri from "./Uri"; export default { - /** - * Returns basename from a url eg. 'index.html' from 'ftp://localhost/foo/bar/index.html' - * @param {string} url - * @returns {string} - */ - basename(url) { - url = this.parse(url).url; - const protocol = this.getProtocol(url); - if (protocol === 'content:') { - try { - let { rootUri, docId, isFileUri } = Uri.parse(url); - - if (isFileUri) return this.basename(rootUri); - - if (docId.endsWith('/')) docId = docId.slice(0, -1); - docId = docId.split(':').pop(); - return this.pathname(docId).split('/').pop(); - } catch (error) { - return null; - } - } else { - if (url.endsWith('/')) url = url.slice(0, -1); - return this.pathname(url).split('/').pop(); - } - }, - - /** - * Checks if given urls are same or not - * @param {...String} urls - * @returns {Boolean} - */ - areSame(...urls) { - let firstUrl = urls[0]; - if (firstUrl.endsWith('/')) firstUrl = firstUrl.slice(0, -1); - return urls.every(url => { - if (url.endsWith('/')) url = url.slice(0, -1); - return firstUrl === url; - }); - }, - - /** - * - * @param {String} url - * returns the extension of the path, from the last occurrence of the . (period) - * character to end of string in the last portion of the path. - * If there is no . in the last portion of the path, or if there are no . - * characters other than the first character of the basename of path (see path.basename()), - * an empty string is returned. - * @returns {String} - */ - extname(url) { - const name = this.basename(url); - if (name) return path.extname(name); - else return null; - }, - /** - * Join all arguments together and normalize the resulting path. - * @param {...string} pathnames - * @returns {String} - */ - join(...pathnames) { - if (pathnames.length < 2) - throw new Error('Join(), requires at least two parameters'); - - let { url, query } = this.parse(pathnames[0]); - - const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ''; - - if (protocol === 'content://') { - try { - if (pathnames[1].startsWith('/')) pathnames[1] = pathnames[1].slice(1); - const contentUri = Uri.parse(url); - let [root, pathname] = contentUri.docId.split(':'); - const newDocId = path.join(pathname, ...pathnames.slice(1)); - if (/^content:\/\/com.termux/.test(url)) { - const rootCondition = root.endsWith('/'); - const newDocIdCondition = newDocId.startsWith('/'); - if (rootCondition === newDocIdCondition) { - root = root.slice(0, -1); - } else if (!rootCondition === !newDocIdCondition) { - root += '/'; - } - return `${contentUri.rootUri}::${root}${newDocId}${query}`; - } - return `${contentUri.rootUri}::${root}:${newDocId}${query}`; - } catch (error) { - return null; - } - } else if (protocol) { - url = url.replace(new RegExp('^' + protocol), ''); - pathnames[0] = url; - return protocol + path.join(...pathnames) + query; - } else { - return path.join(url, ...pathnames.slice(1)) + query; - } - }, - /** - * Make url safe by encoding url components - * @param {string} url - * @returns {string} - */ - safe(url) { - let { url: uri, query } = this.parse(url); - url = uri; - const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ''; - if (protocol) url = url.replace(new RegExp('^' + protocol), ''); - const parts = url.split('/').map((part, i) => { - if (i === 0) return part; - return fixedEncodeURIComponent(part); - }); - return protocol + parts.join('/') + query; - - function fixedEncodeURIComponent(str) { - return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { - return '%' + c.charCodeAt(0).toString(16); - }); - } - }, - /** - * Gets pathname from url eg. gets '/foo/bar' from 'ftp://myhost.com/foo/bar' - * @param {string} url - * @returns {string} - */ - pathname(url) { - if (typeof url !== 'string' || !this.PROTOCOL_PATTERN.test(url)) return url; - - url = url.split('?')[0]; - const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ''; - - if (protocol === 'content://') { - try { - const { rootUri, docId, isFileUri } = Uri.parse(url); - if (isFileUri) return this.pathname(rootUri); - else return '/' + (docId.split(':')[1] || docId); - } catch (error) { - return null; - } - } else { - if (protocol) url = url.replace(new RegExp('^' + protocol), ''); - - if (protocol !== 'file:///') - return '/' + url.split('/').slice(1).join('/'); - - return '/' + url; - } - }, - - /** - * Returns dirname from url eg. 'ftp://localhost/foo/' from 'ftp://localhost/foo/bar' - * @param {string} url - * @returns {string} - */ - dirname(url) { - if (typeof url !== 'string') throw new Error('URL must be string'); - - const urlObj = this.parse(url); - url = urlObj.url; - const protocol = this.getProtocol(url); - - if (protocol === 'content:') { - try { - let { rootUri, docId, isFileUri } = Uri.parse(url); - - if (isFileUri) return this.dirname(rootUri); - else { - if (docId.endsWith('/')) docId = docId.slice(0, -1); - docId = [...docId.split('/').slice(0, -1), ''].join('/'); - return Uri.format(rootUri, docId); - } - } catch (error) { - return null; - } - } else { - if (url.endsWith('/')) url = url.slice(0, -1); - return [...url.split('/').slice(0, -1), ''].join('/') + urlObj.query; - } - }, - - /** - * Parse given url into url and query - * @param {string} url - * @returns {{url:string, query:string}}} - */ - parse(url) { - const [uri, query = ''] = url.split(/(?=\?)/); - return { - url: uri, - query, - }; - }, - - /** - * Formate Url object to string - * @param {object} urlObj - * @param {"ftp:"|"sftp:"|"http:"|"https:"} urlObj.protocol - * @param {string|number} urlObj.hostname - * @param {string} [urlObj.path] - * @param {string} [urlObj.username] - * @param {string} [urlObj.password] - * @param {string|number} [urlObj.port] - * @param {object} [urlObj.query] - * @returns {string} - */ - formate(urlObj) { - let { protocol, hostname, username, password, path, port, query } = urlObj; - - const enc = (str) => encodeURIComponent(str); - - if (!protocol || !hostname) - throw new Error("Cannot formate url. Missing 'protocol' and 'hostname'."); - - let string = `${protocol}//`; - - if (username && password) string += `${enc(username)}:${enc(password)}@`; - else if (username) string += `${username}@`; - - string += hostname; - - if (port) string += `:${port}`; - - if (path) { - if (!path.startsWith('/')) path = '/' + path; - - string += path; - } - - if (query && typeof query === 'object') { - string += '?'; - - for (let key in query) string += `${enc(key)}=${enc(query[key])}&`; - - string = string.slice(0, -1); - } - - return string; - }, - /** - * Returns protocol of a url e.g. 'ftp:' from 'ftp://localhost/foo/bar' - * @param {string} url - * @returns {"ftp:"|"sftp:"|"http:"|"https:"} - */ - getProtocol(url) { - return (/^([a-z]+:)\/\/\/?/i.exec(url) || [])[1] || ''; - }, - /** - * - * @param {string} url - * @returns {string} - */ - hidePassword(url) { - const { protocol, username, hostname, pathname } = URLParse(url); - if (protocol === 'file:') { - return url; - } else { - return `${protocol}//${username}@${hostname}${pathname}`; - } - }, - /** - * Decodes url and returns username, password, hostname, pathname, port and query - * @param {string} url - * @returns {URLObject} - */ - decodeUrl(url) { - const uuid = 'uuid' + Math.floor(Math.random() + Date.now() * 1000000); - - if (/#/.test(url)) { - url = url.replace(/#/g, uuid); - } - - let { username, password, hostname, pathname, port, query } = URLParse( - url, - true, - ); - - if (pathname) { - pathname = decodeURIComponent(pathname); - pathname = pathname.replace(new RegExp(uuid, 'g'), '#'); - } - - if (username) { - username = decodeURIComponent(username); - } - - if (password) { - password = decodeURIComponent(password); - } - - if (port) { - port = parseInt(port); - } - - let { keyFile, passPhrase } = query; - - if (keyFile) { - query.keyFile = decodeURIComponent(keyFile); - } - - if (passPhrase) { - query.passPhrase = decodeURIComponent(passPhrase); - } - - return { username, password, hostname, pathname, port, query }; - }, - /** - * Removes trailing slash from url - * @param {string} url - * @returns - */ - trimSlash(url) { - const parsed = this.parse(url); - if (parsed.url.endsWith('/')) { - parsed.url = parsed.url.slice(0, -1); - } - return this.join(parsed.url, parsed.query); - }, - PROTOCOL_PATTERN: /^[a-z]+:\/\/\/?/i, + /** + * Returns basename from a url eg. 'index.html' from 'ftp://localhost/foo/bar/index.html' + * @param {string} url + * @returns {string} + */ + basename(url) { + url = this.parse(url).url; + const protocol = this.getProtocol(url); + if (protocol === "content:") { + try { + let { rootUri, docId, isFileUri } = Uri.parse(url); + + if (isFileUri) return this.basename(rootUri); + + if (docId.endsWith("/")) docId = docId.slice(0, -1); + docId = docId.split(":").pop(); + return this.pathname(docId).split("/").pop(); + } catch (error) { + return null; + } + } else { + if (url.endsWith("/")) url = url.slice(0, -1); + return this.pathname(url).split("/").pop(); + } + }, + + /** + * Checks if given urls are same or not + * @param {...String} urls + * @returns {Boolean} + */ + areSame(...urls) { + let firstUrl = urls[0]; + if (firstUrl.endsWith("/")) firstUrl = firstUrl.slice(0, -1); + return urls.every((url) => { + if (url.endsWith("/")) url = url.slice(0, -1); + return firstUrl === url; + }); + }, + + /** + * + * @param {String} url + * returns the extension of the path, from the last occurrence of the . (period) + * character to end of string in the last portion of the path. + * If there is no . in the last portion of the path, or if there are no . + * characters other than the first character of the basename of path (see path.basename()), + * an empty string is returned. + * @returns {String} + */ + extname(url) { + const name = this.basename(url); + if (name) return path.extname(name); + else return null; + }, + /** + * Join all arguments together and normalize the resulting path. + * @param {...string} pathnames + * @returns {String} + */ + join(...pathnames) { + if (pathnames.length < 2) + throw new Error("Join(), requires at least two parameters"); + + let { url, query } = this.parse(pathnames[0]); + + const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ""; + + if (protocol === "content://") { + try { + if (pathnames[1].startsWith("/")) pathnames[1] = pathnames[1].slice(1); + const contentUri = Uri.parse(url); + let [root, pathname] = contentUri.docId.split(":"); + const newDocId = path.join(pathname, ...pathnames.slice(1)); + if (/^content:\/\/com.termux/.test(url)) { + const rootCondition = root.endsWith("/"); + const newDocIdCondition = newDocId.startsWith("/"); + if (rootCondition === newDocIdCondition) { + root = root.slice(0, -1); + } else if (!rootCondition === !newDocIdCondition) { + root += "/"; + } + return `${contentUri.rootUri}::${root}${newDocId}${query}`; + } + return `${contentUri.rootUri}::${root}:${newDocId}${query}`; + } catch (error) { + return null; + } + } else if (protocol) { + url = url.replace(new RegExp("^" + protocol), ""); + pathnames[0] = url; + return protocol + path.join(...pathnames) + query; + } else { + return path.join(url, ...pathnames.slice(1)) + query; + } + }, + /** + * Make url safe by encoding url components + * @param {string} url + * @returns {string} + */ + safe(url) { + let { url: uri, query } = this.parse(url); + url = uri; + const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ""; + if (protocol) url = url.replace(new RegExp("^" + protocol), ""); + const parts = url.split("/").map((part, i) => { + if (i === 0) return part; + return fixedEncodeURIComponent(part); + }); + return protocol + parts.join("/") + query; + + function fixedEncodeURIComponent(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, function (c) { + return "%" + c.charCodeAt(0).toString(16); + }); + } + }, + /** + * Gets pathname from url eg. gets '/foo/bar' from 'ftp://myhost.com/foo/bar' + * @param {string} url + * @returns {string} + */ + pathname(url) { + if (typeof url !== "string" || !this.PROTOCOL_PATTERN.test(url)) return url; + + url = url.split("?")[0]; + const protocol = (this.PROTOCOL_PATTERN.exec(url) || [])[0] || ""; + + if (protocol === "content://") { + try { + const { rootUri, docId, isFileUri } = Uri.parse(url); + if (isFileUri) return this.pathname(rootUri); + else return "/" + (docId.split(":")[1] || docId); + } catch (error) { + return null; + } + } else { + if (protocol) url = url.replace(new RegExp("^" + protocol), ""); + + if (protocol !== "file:///") + return "/" + url.split("/").slice(1).join("/"); + + return "/" + url; + } + }, + + /** + * Returns dirname from url eg. 'ftp://localhost/foo/' from 'ftp://localhost/foo/bar' + * @param {string} url + * @returns {string} + */ + dirname(url) { + if (typeof url !== "string") throw new Error("URL must be string"); + + const urlObj = this.parse(url); + url = urlObj.url; + const protocol = this.getProtocol(url); + + if (protocol === "content:") { + try { + let { rootUri, docId, isFileUri } = Uri.parse(url); + + if (isFileUri) return this.dirname(rootUri); + else { + if (docId.endsWith("/")) docId = docId.slice(0, -1); + docId = [...docId.split("/").slice(0, -1), ""].join("/"); + return Uri.format(rootUri, docId); + } + } catch (error) { + return null; + } + } else { + if (url.endsWith("/")) url = url.slice(0, -1); + return [...url.split("/").slice(0, -1), ""].join("/") + urlObj.query; + } + }, + + /** + * Parse given url into url and query + * @param {string} url + * @returns {{url:string, query:string}}} + */ + parse(url) { + const [uri, query = ""] = url.split(/(?=\?)/); + return { + url: uri, + query, + }; + }, + + /** + * Formate Url object to string + * @param {object} urlObj + * @param {"ftp:"|"sftp:"|"http:"|"https:"} urlObj.protocol + * @param {string|number} urlObj.hostname + * @param {string} [urlObj.path] + * @param {string} [urlObj.username] + * @param {string} [urlObj.password] + * @param {string|number} [urlObj.port] + * @param {object} [urlObj.query] + * @returns {string} + */ + formate(urlObj) { + let { protocol, hostname, username, password, path, port, query } = urlObj; + + const enc = (str) => encodeURIComponent(str); + + if (!protocol || !hostname) + throw new Error("Cannot formate url. Missing 'protocol' and 'hostname'."); + + let string = `${protocol}//`; + + if (username && password) string += `${enc(username)}:${enc(password)}@`; + else if (username) string += `${username}@`; + + string += hostname; + + if (port) string += `:${port}`; + + if (path) { + if (!path.startsWith("/")) path = "/" + path; + + string += path; + } + + if (query && typeof query === "object") { + string += "?"; + + for (let key in query) string += `${enc(key)}=${enc(query[key])}&`; + + string = string.slice(0, -1); + } + + return string; + }, + /** + * Returns protocol of a url e.g. 'ftp:' from 'ftp://localhost/foo/bar' + * @param {string} url + * @returns {"ftp:"|"sftp:"|"http:"|"https:"} + */ + getProtocol(url) { + return (/^([a-z]+:)\/\/\/?/i.exec(url) || [])[1] || ""; + }, + /** + * + * @param {string} url + * @returns {string} + */ + hidePassword(url) { + const { protocol, username, hostname, pathname } = URLParse(url); + if (protocol === "file:") { + return url; + } else { + return `${protocol}//${username}@${hostname}${pathname}`; + } + }, + /** + * Decodes url and returns username, password, hostname, pathname, port and query + * @param {string} url + * @returns {URLObject} + */ + decodeUrl(url) { + const uuid = "uuid" + Math.floor(Math.random() + Date.now() * 1000000); + + if (/#/.test(url)) { + url = url.replace(/#/g, uuid); + } + + let { username, password, hostname, pathname, port, query } = URLParse( + url, + true, + ); + + if (pathname) { + pathname = decodeURIComponent(pathname); + pathname = pathname.replace(new RegExp(uuid, "g"), "#"); + } + + if (username) { + username = decodeURIComponent(username); + } + + if (password) { + password = decodeURIComponent(password); + } + + if (port) { + port = Number.parseInt(port); + } + + let { keyFile, passPhrase } = query; + + if (keyFile) { + query.keyFile = decodeURIComponent(keyFile); + } + + if (passPhrase) { + query.passPhrase = decodeURIComponent(passPhrase); + } + + return { username, password, hostname, pathname, port, query }; + }, + /** + * Removes trailing slash from url + * @param {string} url + * @returns + */ + trimSlash(url) { + const parsed = this.parse(url); + if (parsed.url.endsWith("/")) { + parsed.url = parsed.url.slice(0, -1); + } + return this.join(parsed.url, parsed.query); + }, + PROTOCOL_PATTERN: /^[a-z]+:\/\/\/?/i, }; diff --git a/src/utils/color/hex.js b/src/utils/color/hex.js index cc4e96fb0..d297c6aa1 100644 --- a/src/utils/color/hex.js +++ b/src/utils/color/hex.js @@ -1,64 +1,64 @@ -import Rgb from './rgb'; +import Rgb from "./rgb"; export default class Hex { - r = 0; - g = 0; - b = 0; - a = 1; + r = 0; + g = 0; + b = 0; + a = 1; - /** - * Hex color constructor - * @param {number} r Red value in hexadecimals - * @param {number} g Green value in hexadecimals - * @param {number} b Blue value in hexadecimals - * @param {number} a Alpha value between 0 and 1 - */ - constructor(r, g, b, a = 1) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } + /** + * Hex color constructor + * @param {number} r Red value in hexadecimals + * @param {number} g Green value in hexadecimals + * @param {number} b Blue value in hexadecimals + * @param {number} a Alpha value between 0 and 1 + */ + constructor(r, g, b, a = 1) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } - /** - * Creates a Hex color from an RGB color - * @param {Rgb} rgb - */ - static fromRgb(rgb) { - const { r, g, b, a } = rgb; - return new Hex(r, g, b, a * 255); - } + /** + * Creates a Hex color from an RGB color + * @param {Rgb} rgb + */ + static fromRgb(rgb) { + const { r, g, b, a } = rgb; + return new Hex(r, g, b, a * 255); + } - /** - * Gets the color as a string - * @param {boolean} alpha Whether to include alpha - */ - toString(alpha) { - let r = this.r.toString(16); - let g = this.g.toString(16); - let b = this.b.toString(16); - let a = this.a.toString(16); + /** + * Gets the color as a string + * @param {boolean} alpha Whether to include alpha + */ + toString(alpha) { + let r = this.r.toString(16); + let g = this.g.toString(16); + let b = this.b.toString(16); + let a = this.a.toString(16); - if (r.length === 1) r = `0${r}`; - if (g.length === 1) g = `0${g}`; - if (b.length === 1) b = `0${b}`; - if (a.length === 1) a = `0${a}`; + if (r.length === 1) r = `0${r}`; + if (g.length === 1) g = `0${g}`; + if (b.length === 1) b = `0${b}`; + if (a.length === 1) a = `0${a}`; - const hex = () => `#${r}${g}${b}`.toUpperCase(); - const hexA = () => `#${r}${g}${b}${a}`.toUpperCase(); + const hex = () => `#${r}${g}${b}`.toUpperCase(); + const hexA = () => `#${r}${g}${b}${a}`.toUpperCase(); - if (alpha === undefined) { - return this.a === 255 ? hex() : hexA(); - } + if (alpha === undefined) { + return this.a === 255 ? hex() : hexA(); + } - return alpha ? hexA() : hex(); - } + return alpha ? hexA() : hex(); + } - /** - * Gets the color as an RGB object - * @returns {Rgb} - */ - get rgb() { - return new Rgb(this.r, this.g, this.b, this.a); - } -} \ No newline at end of file + /** + * Gets the color as an RGB object + * @returns {Rgb} + */ + get rgb() { + return new Rgb(this.r, this.g, this.b, this.a); + } +} diff --git a/src/utils/color/hsl.js b/src/utils/color/hsl.js index 9d0af2996..ddb7769bd 100644 --- a/src/utils/color/hsl.js +++ b/src/utils/color/hsl.js @@ -1,140 +1,147 @@ -import Rgb from './rgb'; +import Rgb from "./rgb"; export default class Hsl { - h = 0; - s = 0; - l = 0; - a = 1; - - /** - * HSL color constructor - * @param {number} h Hue value between 0 and 1 - * @param {number} s Saturation value between 0 and 1 - * @param {number} l Lightness value between 0 and 1 - * @param {number} a Alpha value between 0 and 1 - */ - constructor(h, s, l, a = 1) { - this.h = h; - this.s = s; - this.l = l; - this.a = a; - } - - /** - * Creates an HSL color from an RGB color - * @param {Rgb} rgb - * @returns - */ - static fromRgb(rgb) { - let { r, g, b, a } = rgb; - r /= 255; - g /= 255; - b /= 255; - - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (max + min) / 2; - let h = 0; - let s = 0; - - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - switch (max) { - case r: h = (g - b) / d + (g < b ? 6 : 0); break; - case g: h = (b - r) / d + 2; break; - case b: h = (r - g) / d + 4; break; - } - h /= 6; - } - - return new Hsl(h, s, l, a); - } - - /** - * Gets the color as a string - * @param {boolean} [alpha] Whether to include alpha - * @returns - */ - toString(alpha) { - const hsl = () => `hsl(${this.hValue}, ${this.sValue}%, ${this.lValue}%)`; - const hsla = () => `hsla(${this.hValue}, ${this.sValue}%, ${this.lValue}%, ${this.a})`; - if (alpha === undefined) { - return this.a === 1 ? hsl() : hsla(); - } - - return alpha ? hsla() : hsl(); - } - - get hValue() { - return this.h * 360; - } - - get sValue() { - return this.s * 100; - } - - get lValue() { - return this.l * 100; - } - - get lightness() { - return this.lValue; - } - - get hue() { - return this.hValue; - } - - get saturation() { - return this.sValue; - } - - /** - * Gets the color as an rgb object - * @returns {Rgb} - */ - get rgb() { - if (this.l === 0) { - return new Rgb(0, 0, 0, this.a); - } - - if (this.l === 1) { - return new Rgb(255, 255, 255, this.a); - } - - // now convert hsl value to rgb - let c = (1 - Math.abs(2 * this.l - 1)) * this.s; - let x = c * (1 - Math.abs((this.h * 6) % 2 - 1)); - let m = this.l - c / 2; - let r = 0; - let g = 0; - let b = 0; - - if (this.h < 1 / 6) { - r = c; - g = x; - } else if (this.h < 2 / 6) { - r = x; - g = c; - } else if (this.h < 3 / 6) { - g = c; - b = x; - } else if (this.h < 4 / 6) { - g = x; - b = c; - } else if (this.h < 5 / 6) { - r = x; - b = c; - } else { - r = c; - b = x; - } - - r = Math.round((r + m) * 255); - g = Math.round((g + m) * 255); - b = Math.round((b + m) * 255); - - return new Rgb(r, g, b, this.a); - } + h = 0; + s = 0; + l = 0; + a = 1; + + /** + * HSL color constructor + * @param {number} h Hue value between 0 and 1 + * @param {number} s Saturation value between 0 and 1 + * @param {number} l Lightness value between 0 and 1 + * @param {number} a Alpha value between 0 and 1 + */ + constructor(h, s, l, a = 1) { + this.h = h; + this.s = s; + this.l = l; + this.a = a; + } + + /** + * Creates an HSL color from an RGB color + * @param {Rgb} rgb + * @returns + */ + static fromRgb(rgb) { + let { r, g, b, a } = rgb; + r /= 255; + g /= 255; + b /= 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const l = (max + min) / 2; + let h = 0; + let s = 0; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + case g: + h = (b - r) / d + 2; + break; + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return new Hsl(h, s, l, a); + } + + /** + * Gets the color as a string + * @param {boolean} [alpha] Whether to include alpha + * @returns + */ + toString(alpha) { + const hsl = () => `hsl(${this.hValue}, ${this.sValue}%, ${this.lValue}%)`; + const hsla = () => + `hsla(${this.hValue}, ${this.sValue}%, ${this.lValue}%, ${this.a})`; + if (alpha === undefined) { + return this.a === 1 ? hsl() : hsla(); + } + + return alpha ? hsla() : hsl(); + } + + get hValue() { + return this.h * 360; + } + + get sValue() { + return this.s * 100; + } + + get lValue() { + return this.l * 100; + } + + get lightness() { + return this.lValue; + } + + get hue() { + return this.hValue; + } + + get saturation() { + return this.sValue; + } + + /** + * Gets the color as an rgb object + * @returns {Rgb} + */ + get rgb() { + if (this.l === 0) { + return new Rgb(0, 0, 0, this.a); + } + + if (this.l === 1) { + return new Rgb(255, 255, 255, this.a); + } + + // now convert hsl value to rgb + let c = (1 - Math.abs(2 * this.l - 1)) * this.s; + let x = c * (1 - Math.abs(((this.h * 6) % 2) - 1)); + let m = this.l - c / 2; + let r = 0; + let g = 0; + let b = 0; + + if (this.h < 1 / 6) { + r = c; + g = x; + } else if (this.h < 2 / 6) { + r = x; + g = c; + } else if (this.h < 3 / 6) { + g = c; + b = x; + } else if (this.h < 4 / 6) { + g = x; + b = c; + } else if (this.h < 5 / 6) { + r = x; + b = c; + } else { + r = c; + b = x; + } + + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + + return new Rgb(r, g, b, this.a); + } } diff --git a/src/utils/color/index.js b/src/utils/color/index.js index e9ca67d5b..027c6e526 100644 --- a/src/utils/color/index.js +++ b/src/utils/color/index.js @@ -1,75 +1,75 @@ -import Hex from './hex'; -import Hsl from './hsl'; -import Rgb from './rgb'; +import Hex from "./hex"; +import Hsl from "./hsl"; +import Rgb from "./rgb"; /**@type {CanvasRenderingContext2D} */ -const ctx = ().getContext('2d', { - willReadFrequently: true, +const ctx = ().getContext("2d", { + willReadFrequently: true, }); -export default (/**@type {string}*/color) => { - return new Color(color); +export default (/**@type {string}*/ color) => { + return new Color(color); }; class Color { - rgb = new Rgb(0, 0, 0, 1); + rgb = new Rgb(0, 0, 0, 1); - /** - * Create a color from a string - * @param {string} color - */ - constructor(color) { - const { canvas } = ctx; - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = color; - ctx.fillRect(0, 0, 1, 1); - const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; - this.rgb = new Rgb(r, g, b, a / 255); - } + /** + * Create a color from a string + * @param {string} color + */ + constructor(color) { + const { canvas } = ctx; + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = color; + ctx.fillRect(0, 0, 1, 1); + const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; + this.rgb = new Rgb(r, g, b, a / 255); + } - darken(ratio) { - const hsl = Hsl.fromRgb(this.rgb); - hsl.l = Math.max(0, hsl.l - (ratio * hsl.l)); - this.rgb = hsl.rgb; - return this; - } + darken(ratio) { + const hsl = Hsl.fromRgb(this.rgb); + hsl.l = Math.max(0, hsl.l - ratio * hsl.l); + this.rgb = hsl.rgb; + return this; + } - lighten(ratio) { - const hsl = Hsl.fromRgb(this.rgb); - hsl.l = Math.min(1, hsl.l + (ratio * hsl.l)); - this.rgb = hsl.rgb; - return this; - } + lighten(ratio) { + const hsl = Hsl.fromRgb(this.rgb); + hsl.l = Math.min(1, hsl.l + ratio * hsl.l); + this.rgb = hsl.rgb; + return this; + } - get isDark() { - return this.luminance < 0.5; - } + get isDark() { + return this.luminance < 0.5; + } - get isLight() { - return this.luminance >= 0.5; - } + get isLight() { + return this.luminance >= 0.5; + } - get lightness() { - return this.hsl.l; - } + get lightness() { + return this.hsl.l; + } - /** - * Get the luminance of the color - * Returns a value between 0 and 1 - */ - get luminance() { - let { r, g, b } = this.rgb; - r /= 255; - g /= 255; - b /= 255; - return 0.2126 * r + 0.7152 * g + 0.0722 * b; - } + /** + * Get the luminance of the color + * Returns a value between 0 and 1 + */ + get luminance() { + let { r, g, b } = this.rgb; + r /= 255; + g /= 255; + b /= 255; + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + } - get hex() { - return Hex.fromRgb(this.rgb); - } + get hex() { + return Hex.fromRgb(this.rgb); + } - get hsl() { - return Hsl.fromRgb(this.rgb); - } -}; \ No newline at end of file + get hsl() { + return Hsl.fromRgb(this.rgb); + } +} diff --git a/src/utils/color/regex.js b/src/utils/color/regex.js index cae7554ca..5f3fa5061 100644 --- a/src/utils/color/regex.js +++ b/src/utils/color/regex.js @@ -1,6 +1,7 @@ -export const MAX_16_BASE = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'; -export const HUE_VALUE = '(360(\\.0+)?|(3[0-5][0-9]|([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9])(\\.\\d+)?)|(\\.\\d+))'; -export const MAX_PERCENTAGE = '((100(\\.0+)?|[1-9]?[0-9](\\.\\d+)?|\\.\\d+)%)'; +export const MAX_16_BASE = "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"; +export const HUE_VALUE = + "(360(\\.0+)?|(3[0-5][0-9]|([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-9][0-9])(\\.\\d+)?)|(\\.\\d+))"; +export const MAX_PERCENTAGE = "((100(\\.0+)?|[1-9]?[0-9](\\.\\d+)?|\\.\\d+)%)"; export const MAX_ALPHA = `((0(\\.\\d+)?|1(\\.0+)?|\\.\\d+)|${MAX_PERCENTAGE})`; export const RGB_VALUE = `(\\s*${MAX_16_BASE}|${MAX_PERCENTAGE}\\s*)`; export const RGB_VALUES = `${RGB_VALUE},${RGB_VALUE},${RGB_VALUE}`; @@ -11,232 +12,238 @@ export const HSL_VALUE = `\\s*${HUE_VALUE}\\s*,\\s*${MAX_PERCENTAGE}\\s*,\\s*${M export const HSL_VALUE_NO_COMMA = `\\s*${HUE_VALUE}\\s+${MAX_PERCENTAGE}\\s+${MAX_PERCENTAGE}\\s*`; export const HSL = `(hsl\\s*\\(${HSL_VALUE}\\)|hsl\\s*\\(${HSL_VALUE_NO_COMMA}\\))`; export const HSLA = `(hsla\\s*\\(${HSL_VALUE}\\s*,\\s*${MAX_ALPHA}\\s*\\)|hsla\\s*\\(${HSL_VALUE_NO_COMMA}\\s+\/\/\\s+${MAX_ALPHA}\\s*\\)|hsla\\s*\\(${HSL_VALUE_NO_COMMA}\\s+${MAX_ALPHA}\\s*\\))`; -export const HEX = '(#[0-9a-f]{3,8})'; +export const HEX = "(#[0-9a-f]{3,8})"; export const NAMED_COLORS = { - 'black': 'rgb(0, 0, 0)', - 'silver': 'rgb(192, 192, 192)', - 'gray': 'rgb(128, 128, 128)', - 'white': 'rgb(255, 255, 255)', - 'maroon': 'rgb(128, 0, 0)', - 'red': 'rgb(255, 0, 0)', - 'purple': 'rgb(128, 0, 128)', - 'fuchsia': 'rgb(255, 0, 255)', - 'green': 'rgb(0, 128, 0)', - 'lime': 'rgb(0, 255, 0)', - 'olive': 'rgb(128, 128, 0)', - 'yellow': 'rgb(255, 255, 0)', - 'navy': 'rgb(0, 0, 128)', - 'blue': 'rgb(0, 0, 255)', - 'teal': 'rgb(0, 128, 128)', - 'aqua': 'rgb(0, 255, 255)', - 'aliceblue': 'rgb(240, 248, 255)', - 'antiquewhite': 'rgb(250, 235, 215)', - 'aquamarine': 'rgb(127, 255, 212)', - 'azure': 'rgb(240, 255, 255)', - 'beige': 'rgb(245, 245, 220)', - 'bisque': 'rgb(255, 228, 196)', - 'blanchedalmond': 'rgb(255, 235, 205)', - 'blueviolet': 'rgb(138, 43, 226)', - 'brown': 'rgb(165, 42, 42)', - 'burlywood': 'rgb(222, 184, 135)', - 'cadetblue': 'rgb(95, 158, 160)', - 'chartreuse': 'rgb(127, 255, 0)', - 'chocolate': 'rgb(210, 105, 30)', - 'coral': 'rgb(255, 127, 80)', - 'cornflowerblue': 'rgb(100, 149, 237)', - 'cornsilk': 'rgb(255, 248, 220)', - 'crimson': 'rgb(220, 20, 60)', - 'cyan': 'rgb(0, 255, 255)', - 'darkblue': 'rgb(0, 0, 139)', - 'darkcyan': 'rgb(0, 139, 139)', - 'darkgoldenrod': 'rgb(184, 134, 11)', - 'darkgray': 'rgb(169, 169, 169)', - 'darkgreen': 'rgb(0, 100, 0)', - 'darkgrey': 'rgb(169, 169, 169)', - 'darkkhaki': 'rgb(189, 183, 107)', - 'darkmagenta': 'rgb(139, 0, 139)', - 'darkolivegreen': 'rgb(85, 107, 47)', - 'darkorange': 'rgb(255, 140, 0)', - 'darkorchid': 'rgb(153, 50, 204)', - 'darkred': 'rgb(139, 0, 0)', - 'darksalmon': 'rgb(233, 150, 122)', - 'darkseagreen': 'rgb(143, 188, 143)', - 'darkslateblue': 'rgb(72, 61, 139)', - 'darkslategray': 'rgb(47, 79, 79)', - 'darkslategrey': 'rgb(47, 79, 79)', - 'darkturquoise': 'rgb(0, 206, 209)', - 'darkviolet': 'rgb(148, 0, 211)', - 'deeppink': 'rgb(255, 20, 147)', - 'deepskyblue': 'rgb(0, 191, 255)', - 'dimgray': 'rgb(105, 105, 105)', - 'dimgrey': 'rgb(105, 105, 105)', - 'dodgerblue': 'rgb(30, 144, 255)', - 'firebrick': 'rgb(178, 34, 34)', - 'floralwhite': 'rgb(255, 250, 240)', - 'forestgreen': 'rgb(34, 139, 34)', - 'gainsboro': 'rgb(220, 220, 220)', - 'ghostwhite': 'rgb(248, 248, 255)', - 'gold': 'rgb(255, 215, 0)', - 'goldenrod': 'rgb(218, 165, 32)', - 'greenyellow': 'rgb(173, 255, 47)', - 'grey': 'rgb(128, 128, 128)', - 'honeydew': 'rgb(240, 255, 240)', - 'hotpink': 'rgb(255, 105, 180)', - 'indianred': 'rgb(205, 92, 92)', - 'indigo': 'rgb(75, 0, 130)', - 'ivory': 'rgb(255, 255, 240)', - 'khaki': 'rgb(240, 230, 140)', - 'lavender': 'rgb(230, 230, 250)', - 'lavenderblush': 'rgb(255, 240, 245)', - 'lawngreen': 'rgb(124, 252, 0)', - 'lemonchiffon': 'rgb(255, 250, 205)', - 'lightblue': 'rgb(173, 216, 230)', - 'lightcoral': 'rgb(240, 128, 128)', - 'lightcyan': 'rgb(224, 255, 255)', - 'lightgoldenrodyellow': 'rgb(250, 250, 210)', - 'lightgray': 'rgb(211, 211, 211)', - 'lightgreen': 'rgb(144, 238, 144)', - 'lightgrey': 'rgb(211, 211, 211)', - 'lightpink': 'rgb(255, 182, 193)', - 'lightsalmon': 'rgb(255, 160, 122)', - 'lightseagreen': 'rgb(32, 178, 170)', - 'lightskyblue': 'rgb(135, 206, 250)', - 'lightslategray': 'rgb(119, 136, 153)', - 'lightslategrey': 'rgb(119, 136, 153)', - 'lightsteelblue': 'rgb(176, 196, 222)', - 'lightyellow': 'rgb(255, 255, 224)', - 'limegreen': 'rgb(50, 205, 50)', - 'linen': 'rgb(250, 240, 230)', - 'magenta': 'rgb(255, 0, 255)', - 'mediumaquamarine': 'rgb(102, 205, 170)', - 'mediumblue': 'rgb(0, 0, 205)', - 'mediumorchid': 'rgb(186, 85, 211)', - 'mediumpurple': 'rgb(147, 112, 219)', - 'mediumseagreen': 'rgb(60, 179, 113)', - 'mediumslateblue': 'rgb(123, 104, 238)', - 'mediumspringgreen': 'rgb(0, 250, 154)', - 'mediumturquoise': 'rgb(72, 209, 204)', - 'mediumvioletred': 'rgb(199, 21, 133)', - 'midnightblue': 'rgb(25, 25, 112)', - 'mintcream': 'rgb(245, 255, 250)', - 'mistyrose': 'rgb(255, 228, 225)', - 'moccasin': 'rgb(255, 228, 181)', - 'navajowhite': 'rgb(255, 222, 173)', - 'oldlace': 'rgb(253, 245, 230)', - 'olivedrab': 'rgb(107, 142, 35)', - 'orangered': 'rgb(255, 69, 0)', - 'orchid': 'rgb(218, 112, 214)', - 'palegoldenrod': 'rgb(238, 232, 170)', - 'palegreen': 'rgb(152, 251, 152)', - 'paleturquoise': 'rgb(175, 238, 238)', - 'palevioletred': 'rgb(219, 112, 147)', - 'papayawhip': 'rgb(255, 239, 213)', - 'peachpuff': 'rgb(255, 218, 185)', - 'peru': 'rgb(205, 133, 63)', - 'pink': 'rgb(255, 192, 203)', - 'plum': 'rgb(221, 160, 221)', - 'powderblue': 'rgb(176, 224, 230)', - 'rebeccapurple': 'rgb(102, 51, 153)', - 'rosybrown': 'rgb(188, 143, 143)', - 'royalblue': 'rgb(65, 105, 225)', - 'saddlebrown': 'rgb(139, 69, 19)', - 'salmon': 'rgb(250, 128, 114)', - 'sandybrown': 'rgb(244, 164, 96)', - 'seagreen': 'rgb(46, 139, 87)', - 'seashell': 'rgb(255, 245, 238)', - 'sienna': 'rgb(160, 82, 45)', - 'skyblue': 'rgb(135, 206, 235)', - 'slateblue': 'rgb(106, 90, 205)', - 'slategray': 'rgb(112, 128, 144)', - 'slategrey': 'rgb(112, 128, 144)', - 'snow': 'rgb(255, 250, 250)', - 'springgreen': 'rgb(0, 255, 127)', - 'steelblue': 'rgb(70, 130, 180)', - 'tan': 'rgb(210, 180, 140)', - 'thistle': 'rgb(216, 191, 216)', - 'tomato': 'rgb(255, 99, 71)', - 'turquoise': 'rgb(64, 224, 208)', - 'violet': 'rgb(238, 130, 238)', - 'wheat': 'rgb(245, 222, 179)', - 'whitesmoke': 'rgb(245, 245, 245)', - 'yellowgreen': 'rgb(154, 205, 50)', + black: "rgb(0, 0, 0)", + silver: "rgb(192, 192, 192)", + gray: "rgb(128, 128, 128)", + white: "rgb(255, 255, 255)", + maroon: "rgb(128, 0, 0)", + red: "rgb(255, 0, 0)", + purple: "rgb(128, 0, 128)", + fuchsia: "rgb(255, 0, 255)", + green: "rgb(0, 128, 0)", + lime: "rgb(0, 255, 0)", + olive: "rgb(128, 128, 0)", + yellow: "rgb(255, 255, 0)", + navy: "rgb(0, 0, 128)", + blue: "rgb(0, 0, 255)", + teal: "rgb(0, 128, 128)", + aqua: "rgb(0, 255, 255)", + aliceblue: "rgb(240, 248, 255)", + antiquewhite: "rgb(250, 235, 215)", + aquamarine: "rgb(127, 255, 212)", + azure: "rgb(240, 255, 255)", + beige: "rgb(245, 245, 220)", + bisque: "rgb(255, 228, 196)", + blanchedalmond: "rgb(255, 235, 205)", + blueviolet: "rgb(138, 43, 226)", + brown: "rgb(165, 42, 42)", + burlywood: "rgb(222, 184, 135)", + cadetblue: "rgb(95, 158, 160)", + chartreuse: "rgb(127, 255, 0)", + chocolate: "rgb(210, 105, 30)", + coral: "rgb(255, 127, 80)", + cornflowerblue: "rgb(100, 149, 237)", + cornsilk: "rgb(255, 248, 220)", + crimson: "rgb(220, 20, 60)", + cyan: "rgb(0, 255, 255)", + darkblue: "rgb(0, 0, 139)", + darkcyan: "rgb(0, 139, 139)", + darkgoldenrod: "rgb(184, 134, 11)", + darkgray: "rgb(169, 169, 169)", + darkgreen: "rgb(0, 100, 0)", + darkgrey: "rgb(169, 169, 169)", + darkkhaki: "rgb(189, 183, 107)", + darkmagenta: "rgb(139, 0, 139)", + darkolivegreen: "rgb(85, 107, 47)", + darkorange: "rgb(255, 140, 0)", + darkorchid: "rgb(153, 50, 204)", + darkred: "rgb(139, 0, 0)", + darksalmon: "rgb(233, 150, 122)", + darkseagreen: "rgb(143, 188, 143)", + darkslateblue: "rgb(72, 61, 139)", + darkslategray: "rgb(47, 79, 79)", + darkslategrey: "rgb(47, 79, 79)", + darkturquoise: "rgb(0, 206, 209)", + darkviolet: "rgb(148, 0, 211)", + deeppink: "rgb(255, 20, 147)", + deepskyblue: "rgb(0, 191, 255)", + dimgray: "rgb(105, 105, 105)", + dimgrey: "rgb(105, 105, 105)", + dodgerblue: "rgb(30, 144, 255)", + firebrick: "rgb(178, 34, 34)", + floralwhite: "rgb(255, 250, 240)", + forestgreen: "rgb(34, 139, 34)", + gainsboro: "rgb(220, 220, 220)", + ghostwhite: "rgb(248, 248, 255)", + gold: "rgb(255, 215, 0)", + goldenrod: "rgb(218, 165, 32)", + greenyellow: "rgb(173, 255, 47)", + grey: "rgb(128, 128, 128)", + honeydew: "rgb(240, 255, 240)", + hotpink: "rgb(255, 105, 180)", + indianred: "rgb(205, 92, 92)", + indigo: "rgb(75, 0, 130)", + ivory: "rgb(255, 255, 240)", + khaki: "rgb(240, 230, 140)", + lavender: "rgb(230, 230, 250)", + lavenderblush: "rgb(255, 240, 245)", + lawngreen: "rgb(124, 252, 0)", + lemonchiffon: "rgb(255, 250, 205)", + lightblue: "rgb(173, 216, 230)", + lightcoral: "rgb(240, 128, 128)", + lightcyan: "rgb(224, 255, 255)", + lightgoldenrodyellow: "rgb(250, 250, 210)", + lightgray: "rgb(211, 211, 211)", + lightgreen: "rgb(144, 238, 144)", + lightgrey: "rgb(211, 211, 211)", + lightpink: "rgb(255, 182, 193)", + lightsalmon: "rgb(255, 160, 122)", + lightseagreen: "rgb(32, 178, 170)", + lightskyblue: "rgb(135, 206, 250)", + lightslategray: "rgb(119, 136, 153)", + lightslategrey: "rgb(119, 136, 153)", + lightsteelblue: "rgb(176, 196, 222)", + lightyellow: "rgb(255, 255, 224)", + limegreen: "rgb(50, 205, 50)", + linen: "rgb(250, 240, 230)", + magenta: "rgb(255, 0, 255)", + mediumaquamarine: "rgb(102, 205, 170)", + mediumblue: "rgb(0, 0, 205)", + mediumorchid: "rgb(186, 85, 211)", + mediumpurple: "rgb(147, 112, 219)", + mediumseagreen: "rgb(60, 179, 113)", + mediumslateblue: "rgb(123, 104, 238)", + mediumspringgreen: "rgb(0, 250, 154)", + mediumturquoise: "rgb(72, 209, 204)", + mediumvioletred: "rgb(199, 21, 133)", + midnightblue: "rgb(25, 25, 112)", + mintcream: "rgb(245, 255, 250)", + mistyrose: "rgb(255, 228, 225)", + moccasin: "rgb(255, 228, 181)", + navajowhite: "rgb(255, 222, 173)", + oldlace: "rgb(253, 245, 230)", + olivedrab: "rgb(107, 142, 35)", + orangered: "rgb(255, 69, 0)", + orchid: "rgb(218, 112, 214)", + palegoldenrod: "rgb(238, 232, 170)", + palegreen: "rgb(152, 251, 152)", + paleturquoise: "rgb(175, 238, 238)", + palevioletred: "rgb(219, 112, 147)", + papayawhip: "rgb(255, 239, 213)", + peachpuff: "rgb(255, 218, 185)", + peru: "rgb(205, 133, 63)", + pink: "rgb(255, 192, 203)", + plum: "rgb(221, 160, 221)", + powderblue: "rgb(176, 224, 230)", + rebeccapurple: "rgb(102, 51, 153)", + rosybrown: "rgb(188, 143, 143)", + royalblue: "rgb(65, 105, 225)", + saddlebrown: "rgb(139, 69, 19)", + salmon: "rgb(250, 128, 114)", + sandybrown: "rgb(244, 164, 96)", + seagreen: "rgb(46, 139, 87)", + seashell: "rgb(255, 245, 238)", + sienna: "rgb(160, 82, 45)", + skyblue: "rgb(135, 206, 235)", + slateblue: "rgb(106, 90, 205)", + slategray: "rgb(112, 128, 144)", + slategrey: "rgb(112, 128, 144)", + snow: "rgb(255, 250, 250)", + springgreen: "rgb(0, 255, 127)", + steelblue: "rgb(70, 130, 180)", + tan: "rgb(210, 180, 140)", + thistle: "rgb(216, 191, 216)", + tomato: "rgb(255, 99, 71)", + turquoise: "rgb(64, 224, 208)", + violet: "rgb(238, 130, 238)", + wheat: "rgb(245, 222, 179)", + whitesmoke: "rgb(245, 245, 245)", + yellowgreen: "rgb(154, 205, 50)", }; -const namedColors = Object.keys(NAMED_COLORS).join('|'); +const namedColors = Object.keys(NAMED_COLORS).join("|"); export const colorRegex = { - /** - * Regular expression to match rgb colors - * @type {RegExp} - */ - get rgb() { - delete this.rgb; - this.rgb = new RegExp(`^${RGB}$`); - return this.rgb; - }, - /** - * Regular expression to match rgba colors - * @type {RegExp} - */ - get rgba() { - delete this.rgba; - this.rgba = new RegExp(`^${RGBA}$`); - return this.rgba; - }, - /** - * Regular expression to match hsl colors - * @type {RegExp} - */ - get hsl() { - delete this.hsl; - this.hsl = new RegExp(`^${HSL}$`); - return this.hsl; - }, - /** - * Regular expression to match hsla colors - * @type {RegExp} - */ - get hsla() { - delete this.hsla; - this.hsla = new RegExp(`^${HSLA}$`); - return this.hsla; - }, - /** - * Regular expression to match hex colors - * @type {RegExp} - */ - get hex() { - delete this.hex; - this.hex = new RegExp(`^${HEX}$`); - return this.hex; - }, - /** - * Regular expression to match any color - * @type {RegExp} - */ - get named() { - delete this.named; - this.named = new RegExp(`^(${namedColors})$`); - return this.named; - }, - /** - * Regular expression to match any color - * @type {RegExp} - */ - get anyStrict() { - delete this.any; - this.any = new RegExp(`^(${namedColors}|${RGB}|${RGBA}|${HSL}|${HSLA}|${HEX})$`, 'i'); - return this.any; - }, - /** - * Regular expression to match any color - * Always return a new RegExp instance - * @type {RegExp} - */ - get anyGlobal() { - // Negative lookbehind is not supported in older browsers - return new RegExp(`(^|\\W)(${namedColors}|${RGB}|${RGBA}|${HSL}|${HSLA}|${HEX})($|\\W)`, 'gi'); - } + /** + * Regular expression to match rgb colors + * @type {RegExp} + */ + get rgb() { + delete this.rgb; + this.rgb = new RegExp(`^${RGB}$`); + return this.rgb; + }, + /** + * Regular expression to match rgba colors + * @type {RegExp} + */ + get rgba() { + delete this.rgba; + this.rgba = new RegExp(`^${RGBA}$`); + return this.rgba; + }, + /** + * Regular expression to match hsl colors + * @type {RegExp} + */ + get hsl() { + delete this.hsl; + this.hsl = new RegExp(`^${HSL}$`); + return this.hsl; + }, + /** + * Regular expression to match hsla colors + * @type {RegExp} + */ + get hsla() { + delete this.hsla; + this.hsla = new RegExp(`^${HSLA}$`); + return this.hsla; + }, + /** + * Regular expression to match hex colors + * @type {RegExp} + */ + get hex() { + delete this.hex; + this.hex = new RegExp(`^${HEX}$`); + return this.hex; + }, + /** + * Regular expression to match any color + * @type {RegExp} + */ + get named() { + delete this.named; + this.named = new RegExp(`^(${namedColors})$`); + return this.named; + }, + /** + * Regular expression to match any color + * @type {RegExp} + */ + get anyStrict() { + delete this.any; + this.any = new RegExp( + `^(${namedColors}|${RGB}|${RGBA}|${HSL}|${HSLA}|${HEX})$`, + "i", + ); + return this.any; + }, + /** + * Regular expression to match any color + * Always return a new RegExp instance + * @type {RegExp} + */ + get anyGlobal() { + // Negative lookbehind is not supported in older browsers + return new RegExp( + `(^|\\W)(${namedColors}|${RGB}|${RGBA}|${HSL}|${HSLA}|${HEX})($|\\W)`, + "gi", + ); + }, }; /** @@ -244,38 +251,38 @@ export const colorRegex = { * @returns {AceAjax.Range} */ export function getColorRange() { - const { editor } = editorManager; - const copyText = editor.getCopyText(); + const { editor } = editorManager; + const copyText = editor.getCopyText(); - if (copyText) { - if (!isValidColor(copyText)) return null; - return editor.selection.getRange(); - } + if (copyText) { + if (!isValidColor(copyText)) return null; + return editor.selection.getRange(); + } - const { Range } = ace.require('ace/range'); - let range; + const { Range } = ace.require("ace/range"); + let range; - const cursorPos = editor.selection.getCursor(); - /**@type {string} */ - const line = editor.session.getLine(cursorPos.row); + const cursorPos = editor.selection.getCursor(); + /**@type {string} */ + const line = editor.session.getLine(cursorPos.row); - // match color in current line and get range - const regex = colorRegex.anyGlobal; - let match; + // match color in current line and get range + const regex = colorRegex.anyGlobal; + let match; - while (match = regex.exec(line)) { - const start = match.index + match[1].length; - const end = start + match[2].length; + while ((match = regex.exec(line))) { + const start = match.index + match[1].length; + const end = start + match[2].length; - if (cursorPos.column >= start && cursorPos.column <= end) { - range = new Range(cursorPos.row, start, cursorPos.row, end); - break; - } - } + if (cursorPos.column >= start && cursorPos.column <= end) { + range = new Range(cursorPos.row, start, cursorPos.row, end); + break; + } + } - return range; + return range; } export function isValidColor(value) { - return colorRegex.anyStrict.test(value); -} \ No newline at end of file + return colorRegex.anyStrict.test(value); +} diff --git a/src/utils/color/rgb.js b/src/utils/color/rgb.js index d91e02054..535ff2860 100644 --- a/src/utils/color/rgb.js +++ b/src/utils/color/rgb.js @@ -1,27 +1,27 @@ export default class Rgb { - r = 0; - g = 0; - b = 0; - a = 1; + r = 0; + g = 0; + b = 0; + a = 1; - constructor(r, g, b, a = 1) { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } + constructor(r, g, b, a = 1) { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } - /** - * Get the color as a string - * @param {boolean} alpha Whether to include alpha channel - * @returns - */ - toString(alpha) { - const rgb = () => `rgb(${this.r}, ${this.g}, ${this.b})`; - const rgba = () => `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; - if (alpha === undefined) { - return this.a === 1 ? rgb() : rgba(); - } - return alpha ? rgba() : rgb(); - } -} \ No newline at end of file + /** + * Get the color as a string + * @param {boolean} alpha Whether to include alpha channel + * @returns + */ + toString(alpha) { + const rgb = () => `rgb(${this.r}, ${this.g}, ${this.b})`; + const rgba = () => `rgba(${this.r}, ${this.g}, ${this.b}, ${this.a})`; + if (alpha === undefined) { + return this.a === 1 ? rgb() : rgba(); + } + return alpha ? rgba() : rgb(); + } +} diff --git a/src/utils/encodings.js b/src/utils/encodings.js index 75c80afb5..fbe122df6 100644 --- a/src/utils/encodings.js +++ b/src/utils/encodings.js @@ -1,9 +1,8 @@ -import settings from 'lib/settings'; -import alert from 'dialogs/alert'; +import alert from "dialogs/alert"; +import settings from "lib/settings"; let encodings = {}; - /** * @typedef {Object} Encoding * @property {string} label @@ -13,30 +12,32 @@ let encodings = {}; /** * Get the encoding label from the charset - * @param {string} charset + * @param {string} charset * @returns {Encoding|undefined} */ export function getEncoding(charset) { - charset = charset.toLowerCase(); + charset = charset.toLowerCase(); - const found = Object.keys(encodings).find((key) => { - if (key.toLowerCase() === charset) { - return true; - } + const found = Object.keys(encodings).find((key) => { + if (key.toLowerCase() === charset) { + return true; + } - const alias = encodings[key].aliases.find((alias) => alias.toLowerCase() === charset); - if (alias) { - return true; - } + const alias = encodings[key].aliases.find( + (alias) => alias.toLowerCase() === charset, + ); + if (alias) { + return true; + } - return false; - }); + return false; + }); - if (found) { - return encodings[found]; - } + if (found) { + return encodings[found]; + } - return encodings['UTF-8']; + return encodings["UTF-8"]; } /** @@ -46,87 +47,105 @@ export function getEncoding(charset) { * @returns {Promise} */ export async function decode(buffer, charset) { - let isJson = false; + let isJson = false; - if (charset === 'json') { - charset = null; - isJson = true; - } + if (charset === "json") { + charset = null; + isJson = true; + } - if (!charset) { - charset = settings.value.defaultFileEncoding; - } + if (!charset) { + charset = settings.value.defaultFileEncoding; + } - charset = getEncoding(charset).name; - const text = await execDecode(buffer, charset); + charset = getEncoding(charset).name; + const text = await execDecode(buffer, charset); - if (isJson) { - return JSON.parse(text); - } + if (isJson) { + return JSON.parse(text); + } - return text; + return text; } /** * Encodes text to ArrayBuffer according given encoding type - * @param {string} text - * @param {string} charset + * @param {string} text + * @param {string} charset * @returns {Promise} */ export function encode(text, charset) { - if (!charset) { - charset = settings.value.defaultFileEncoding; - } + if (!charset) { + charset = settings.value.defaultFileEncoding; + } - charset = getEncoding(charset).name; - return execEncode(text, charset); + charset = getEncoding(charset).name; + return execEncode(text, charset); } export async function initEncodings() { - return new Promise((resolve, reject) => { - cordova.exec((map) => { - Object.keys(map).forEach((key) => { - const encoding = map[key]; - encodings[key] = encoding; - }); - resolve(); - }, (error) => { - alert(strings.error, error.message || error); - reject(error); - }, "System", "get-available-encodings", []); - }); + return new Promise((resolve, reject) => { + cordova.exec( + (map) => { + Object.keys(map).forEach((key) => { + const encoding = map[key]; + encodings[key] = encoding; + }); + resolve(); + }, + (error) => { + alert(strings.error, error.message || error); + reject(error); + }, + "System", + "get-available-encodings", + [], + ); + }); } /** * Decodes arrayBuffer to String according given encoding type - * @param {ArrayBuffer} buffer - * @param {string} charset + * @param {ArrayBuffer} buffer + * @param {string} charset * @returns {Promise} */ function execDecode(buffer, charset) { - return new Promise((resolve, reject) => { - cordova.exec((text) => { - resolve(text); - }, (error) => { - reject(error); - }, "System", "decode", [buffer, charset]); - }); + return new Promise((resolve, reject) => { + cordova.exec( + (text) => { + resolve(text); + }, + (error) => { + reject(error); + }, + "System", + "decode", + [buffer, charset], + ); + }); } /** * Encodes text to ArrayBuffer according given encoding type - * @param {string} text - * @param {string} charset + * @param {string} text + * @param {string} charset * @returns {Promise} */ function execEncode(text, charset) { - return new Promise((resolve, reject) => { - cordova.exec((buffer) => { - resolve(buffer); - }, (error) => { - reject(error); - }, "System", "encode", [text, charset]); - }); + return new Promise((resolve, reject) => { + cordova.exec( + (buffer) => { + resolve(buffer); + }, + (error) => { + reject(error); + }, + "System", + "encode", + [text, charset], + ); + }); } export default encodings; diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 28190e814..798489c08 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -2,11 +2,11 @@ import ajax from "@deadlyjack/ajax"; import escapeStringRegexp from "escape-string-regexp"; import fsOperation from "fileSystem"; -import Url from "./Url"; -import Uri from "./Uri"; -import path from "./Path"; import alert from "dialogs/alert"; import constants from "lib/constants"; +import path from "./Path"; +import Uri from "./Uri"; +import Url from "./Url"; /** * Gets programming language name according to filename @@ -14,468 +14,464 @@ import constants from "lib/constants"; * @returns */ function getFileType(filename) { - const regex = { - babel: /\.babelrc$/i, - jsmap: /\.js\.map$/i, - yarn: /^yarn\.lock$/i, - testjs: /\.test\.js$/i, - testts: /\.test\.ts$/i, - cssmap: /\.css\.map$/i, - typescriptdef: /\.d\.ts$/i, - clojurescript: /\.cljs$/i, - cppheader: /\.(hh|hpp)$/i, - jsconfig: /^jsconfig.json$/i, - tsconfig: /^tsconfig.json$/i, - android: /\.(apk|aab|slim)$/i, - jsbeautify: /^\.jsbeautifyrc$/i, - webpack: /^webpack\.config\.js$/i, - audio: /\.(mp3|wav|ogg|flac|aac)$/i, - git: /(^\.gitignore$)|(^\.gitmodules$)/i, - video: /\.(mp4|m4a|mov|3gp|wmv|flv|avi)$/i, - image: /\.(png|jpg|jpeg|gif|bmp|ico|webp)$/i, - npm: /(^package\.json$)|(^package\-lock\.json$)/i, - compressed: /\.(zip|rar|7z|tar|gz|gzip|dmg|iso)$/i, - eslint: - /(^\.eslintrc(\.(json5?|ya?ml|toml))?$|eslint\.config\.(c?js|json)$)/i, - postcssconfig: - /(^\.postcssrc(\.(json5?|ya?ml|toml))?$|postcss\.config\.(c?js|json)$)/i, - prettier: - /(^\.prettierrc(\.(json5?|ya?ml|toml))?$|prettier\.config\.(c?js|json)$)/i, - }; - - const fileType = Object.keys(regex).find((type) => - regex[type].test(filename), - ); - if (fileType) return fileType; - - return Url.extname(filename).substring(1); + const regex = { + babel: /\.babelrc$/i, + jsmap: /\.js\.map$/i, + yarn: /^yarn\.lock$/i, + testjs: /\.test\.js$/i, + testts: /\.test\.ts$/i, + cssmap: /\.css\.map$/i, + typescriptdef: /\.d\.ts$/i, + clojurescript: /\.cljs$/i, + cppheader: /\.(hh|hpp)$/i, + jsconfig: /^jsconfig.json$/i, + tsconfig: /^tsconfig.json$/i, + android: /\.(apk|aab|slim)$/i, + jsbeautify: /^\.jsbeautifyrc$/i, + webpack: /^webpack\.config\.js$/i, + audio: /\.(mp3|wav|ogg|flac|aac)$/i, + git: /(^\.gitignore$)|(^\.gitmodules$)/i, + video: /\.(mp4|m4a|mov|3gp|wmv|flv|avi)$/i, + image: /\.(png|jpg|jpeg|gif|bmp|ico|webp)$/i, + npm: /(^package\.json$)|(^package\-lock\.json$)/i, + compressed: /\.(zip|rar|7z|tar|gz|gzip|dmg|iso)$/i, + eslint: + /(^\.eslintrc(\.(json5?|ya?ml|toml))?$|eslint\.config\.(c?js|json)$)/i, + postcssconfig: + /(^\.postcssrc(\.(json5?|ya?ml|toml))?$|postcss\.config\.(c?js|json)$)/i, + prettier: + /(^\.prettierrc(\.(json5?|ya?ml|toml))?$|prettier\.config\.(c?js|json)$)/i, + }; + + const fileType = Object.keys(regex).find((type) => + regex[type].test(filename), + ); + if (fileType) return fileType; + + return Url.extname(filename).substring(1); } export default { - /** - * @deprecated This method is deprecated, use 'encodings.decode' instead. - * Decodes arrayBuffer to String according given encoding type - * @param {ArrayBuffer} arrayBuffer - * @param {String} [encoding='utf-8'] - */ - decodeText(arrayBuffer, encoding = "utf-8") { - const isJson = encoding === "json"; - if (isJson) encoding = "utf-8"; - - const uint8Array = new Uint8Array(arrayBuffer); - const result = new TextDecoder(encoding).decode(uint8Array); - if (isJson) { - return this.parseJSON(result); - } - return result; - }, - /** - * Gets icon according to filename - * @param {string} filename - */ - getIconForFile(filename) { - const { getModeForPath } = ace.require("ace/ext/modelist"); - const type = getFileType(filename); - const { name } = getModeForPath(filename); - - const iconForMode = `file_type_${name}`; - const iconForType = `file_type_${type}`; - - return `file file_type_default ${iconForMode} ${iconForType}`; - }, - /** - * - * @param {FileEntry[]} list - * @param {object} fileBrowser settings - * @param {'both'|'file'|'folder'} - */ - sortDir(list, fileBrowser, mode = "both") { - const dir = []; - const file = []; - const sortByName = fileBrowser.sortByName; - const showHiddenFile = fileBrowser.showHiddenFiles; - - list.forEach((item) => { - let hidden; - - item.name = item.name || path.basename(item.url || ""); - hidden = item.name[0] === "."; - - if (typeof item.isDirectory !== "boolean") { - if (this.isDir(item.type)) item.isDirectory = true; - } - if (!item.type) item.type = item.isDirectory ? "dir" : "file"; - if (!item.url) item.url = item.url || item.uri; - if ((hidden && showHiddenFile) || !hidden) { - if (item.isDirectory) { - dir.push(item); - } else if (item.isFile) { - file.push(item); - } - } - if (item.isDirectory) { - item.icon = "folder"; - } else { - if (mode === "folder") { - item.disabled = true; - } - item.icon = this.getIconForFile(item.name); - } - }); - - if (sortByName) { - dir.sort(compare); - file.sort(compare); - } - - return dir.concat(file); - - function compare(a, b) { - return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; - } - }, - /** - * Gets error message from error object - * @param {Error} err - * @param {...string} args - */ - errorMessage(err, ...args) { - args.forEach((arg, i) => { - if (/^(content|file|ftp|sftp|https?):/.test(arg)) { - args[i] = this.getVirtualPath(arg); - } - }); - - const extra = args.join("
        "); - let msg; - - if (typeof err === "string" && err) { - msg = err; - } else if (err instanceof Error) { - msg = err.message; - } else { - msg = strings["an error occurred"]; - } - - return msg + (extra ? "
        " + extra : ""); - }, - /** - * - * @param {Error} err - * @param {...string} args - * @returns {PromiseLike} - */ - error(err, ...args) { - if (err.code === 0) { - toast(err); - return; - } - - let hide = null; - const onhide = () => { - if (hide) hide(); - }; - const promise = { - then(fun) { - if (typeof fun === "function") { - hide = fun; - } - }, - }; - - const msg = this.errorMessage(err, ...args); - alert(strings.error, msg, onhide); - return promise; - }, - /** - * Returns unique ID - * @returns {string} - */ - uuid() { - return ( - new Date().getTime() + parseInt(Math.random() * 100000000000) - ).toString(36); - }, - /** - * Parses JSON string, if fails returns null - * @param {Object|Array} string - */ - parseJSON(string) { - if (!string) return null; - try { - return JSON.parse(string); - } catch (e) { - return null; - } - }, - /** - * Checks whether given type is directory or not - * @param {'dir'|'directory'|'folder'} type - * @returns {Boolean} - */ - isDir(type) { - return /^(dir|directory|folder)$/.test(type); - }, - /** - * Checks whether given type is file or not - * @param {'file'|'link'} type - * @returns {Boolean} - */ - isFile(type) { - return /^(file|link)$/.test(type); - }, - /** - * Replace matching part of url to alias name by which storage is added - * @param {String} url - * @returns {String} - */ - getVirtualPath(url) { - url = Url.parse(url).url; - - if (/^content:/.test(url)) { - const primary = Uri.getPrimaryAddress(url); - if (primary) { - return primary; - } - } - - /**@type {string[]} */ - const storageList = JSON.parse(localStorage.storageList || "[]"); - const storageListLen = storageList.length; - - for (let i = 0; i < storageListLen; ++i) { - const uuid = storageList[i]; - let storageUrl = Url.parse(uuid.uri || uuid.url || "").url; - if (!storageUrl) continue; - if (storageUrl.endsWith("/")) { - storageUrl = storageUrl.slice(0, -1); - } - const regex = new RegExp("^" + escapeStringRegexp(storageUrl)); - if (regex.test(url)) { - url = url.replace(regex, uuid.name); - break; - } - } - - return url; - }, - /** - * Updates uri of all active which matches the oldUrl as location - * of the file - * @param {String} oldUrl - * @param {String} newUrl - */ - updateUriOfAllActiveFiles(oldUrl, newUrl) { - const files = editorManager.files; - const { url } = Url.parse(oldUrl); - - for (let file of files) { - if (!file.uri) continue; - const fileUrl = Url.parse(file.uri).url; - if (new RegExp("^" + escapeStringRegexp(url)).test(fileUrl)) { - if (newUrl) { - file.uri = Url.join(newUrl, file.filename); - } else { - file.uri = null; - } - } - } - - editorManager.onupdate("file-delete"); - editorManager.emit("update", "file-delete"); - }, - /** - * Displays ad on the current page - */ - showAd() { - const { ad } = window; - if (IS_FREE_VERSION && innerHeight * devicePixelRatio > 600 && ad) { - const $page = tag.getAll("wc-page:not(#root)").pop(); - if ($page) { - ad.active = true; - ad.show(); - } - } - }, - /** - * Hides the ad - * @param {Boolean} [force=false] - */ - hideAd(force = false) { - const { ad } = window; - if (IS_FREE_VERSION && ad?.active) { - const $pages = tag.getAll(".page-replacement"); - const hide = $pages.length === 1; - - if (force || hide) { - ad.active = false; - ad.hide(); - } - } - }, - async toInternalUri(uri) { - return new Promise((resolve, reject) => { - window.resolveLocalFileSystemURL( - uri, - (entry) => { - resolve(entry.toInternalURL()); - }, - reject, - ); - }); - }, - promisify(func, ...args) { - return new Promise((resolve, reject) => { - func(...args, resolve, reject); - }); - }, - async checkAPIStatus() { - try { - const { status } = await ajax.get(Url.join(constants.API_BASE, "status")); - return status === "ok"; - } catch (error) { - return false; - } - }, - fixFilename(name) { - if (!name) return name; - return name.replace(/(\r\n)+|\r+|\n+|\t+/g, "").trim(); - }, - /** - * Creates a debounced function that delays invoking the input function until after 'wait' milliseconds have elapsed - * since the last time the debounced function was invoked. Useful for implementing behavior that should only happen - * after the input is complete. - * - * @param {Function} func - The function to debounce. - * @param {number} wait - The number of milliseconds to delay. - * @returns {Function} The new debounced function. - * @example - * window.addEventListener('resize', debounce(myFunction, 200)); - */ - debounce(func, wait) { - let timeout; - return function debounced(...args) { - const later = () => { - clearTimeout(timeout); - func.apply(this, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - }, - defineDeprecatedProperty(obj, name, getter, setter) { - Object.defineProperty(obj, name, { - get: function () { - console.warn(`Property '${name}' is deprecated.`); - return getter.call(this); - }, - set: function (value) { - console.warn(`Property '${name}' is deprecated.`); - setter.call(this, value); - }, - }); - }, - parseHTML(html) { - const parser = new DOMParser(); - const doc = parser.parseFromString(html, "text/html"); - const children = doc.body.children; - if (children.length === 1) { - return children[0]; - } - return Array.from(children); - }, - async createFileStructure(uri, pathString, isFile = true) { - const parts = pathString.split("/").filter(Boolean); - let currentUri = uri; - - // Determine if it's a special case URI - const isSpecialCase = currentUri.includes("::"); - let baseFolder; - - if (currentUri.includes("com.android.externalstorage.documents")) { - baseFolder = decodeURIComponent(currentUri.split("%3A")[1].split("/")[0]); - } else if ( - !( - currentUri.includes("com.android.externalstorage.documents") || - currentUri.includes("com.termux.documents") - ) - ) { - if (isFile) { - await fsOperation(uri).createFile(pathString); - } else { - await fsOperation(uri).createDirectory(pathString); - } - return { uri: uri, type: isFile ? "file" : "folder" }; - } - - for (let i = 0; i < parts.length; i++) { - const isLastElement = i === parts.length - 1; - const name = parts[i]; - let fullUri = currentUri; - - // Adjust URI for special cases - if (currentUri.includes("com.android.externalstorage.documents")) { - if (!isSpecialCase && i === 0) { - fullUri += `::primary:${baseFolder}/${name}`; - } else { - fullUri += `/${name}`; - } - } else if (currentUri.includes("com.termux.documents")) { - if (!isSpecialCase && i === 0) { - fullUri += `::/data/data/com.termux/files/home/${name}`; - } else { - fullUri += `/${name}`; - } - } - - if (isLastElement && isFile) { - // Create file if it's the last element and isFile is true - if (!(await fsOperation(fullUri).exists())) { - await fsOperation(currentUri).createFile(name); - } else { - return; - } - } else { - // Create directory - if (!(await fsOperation(fullUri).exists())) { - await fsOperation(currentUri).createDirectory(name); - } else { - return; - } - } - currentUri = fullUri; - } - let tileType; - if (isFile && parts.length === 1) { - tileType = "file"; - } else { - const urlParts = currentUri.split("/"); - const pathParts = pathString.split("/"); - const pathStartIndex = urlParts.findIndex( - (part) => part === pathParts[0], - ); - if (pathStartIndex !== -1) { - const pathEndIndex = pathStartIndex + pathParts.length; - urlParts.splice(pathStartIndex + 1, pathEndIndex - pathStartIndex - 1); - } - currentUri = urlParts.join("/"); - tileType = "folder"; - } - return { uri: currentUri, type: tileType }; - }, - formatDownloadCount(downloadCount) { - const units = ["", "K", "M", "B", "T"]; - let index = 0; - - while (downloadCount >= 1000 && index < units.length - 1) { - downloadCount /= 1000; - index++; - } - - const countStr = - downloadCount < 10 ? downloadCount.toFixed(2) : downloadCount.toFixed(1); - const trimmedCountStr = countStr.replace(/\.?0+$/, ""); - - return `${trimmedCountStr}${units[index]}`; - }, - }; + /** + * @deprecated This method is deprecated, use 'encodings.decode' instead. + * Decodes arrayBuffer to String according given encoding type + * @param {ArrayBuffer} arrayBuffer + * @param {String} [encoding='utf-8'] + */ + decodeText(arrayBuffer, encoding = "utf-8") { + const isJson = encoding === "json"; + if (isJson) encoding = "utf-8"; + + const uint8Array = new Uint8Array(arrayBuffer); + const result = new TextDecoder(encoding).decode(uint8Array); + if (isJson) { + return this.parseJSON(result); + } + return result; + }, + /** + * Gets icon according to filename + * @param {string} filename + */ + getIconForFile(filename) { + const { getModeForPath } = ace.require("ace/ext/modelist"); + const type = getFileType(filename); + const { name } = getModeForPath(filename); + + const iconForMode = `file_type_${name}`; + const iconForType = `file_type_${type}`; + + return `file file_type_default ${iconForMode} ${iconForType}`; + }, + /** + * + * @param {FileEntry[]} list + * @param {object} fileBrowser settings + * @param {'both'|'file'|'folder'} + */ + sortDir(list, fileBrowser, mode = "both") { + const dir = []; + const file = []; + const sortByName = fileBrowser.sortByName; + const showHiddenFile = fileBrowser.showHiddenFiles; + + list.forEach((item) => { + let hidden; + + item.name = item.name || path.basename(item.url || ""); + hidden = item.name[0] === "."; + + if (typeof item.isDirectory !== "boolean") { + if (this.isDir(item.type)) item.isDirectory = true; + } + if (!item.type) item.type = item.isDirectory ? "dir" : "file"; + if (!item.url) item.url = item.url || item.uri; + if ((hidden && showHiddenFile) || !hidden) { + if (item.isDirectory) { + dir.push(item); + } else if (item.isFile) { + file.push(item); + } + } + if (item.isDirectory) { + item.icon = "folder"; + } else { + if (mode === "folder") { + item.disabled = true; + } + item.icon = this.getIconForFile(item.name); + } + }); + + if (sortByName) { + dir.sort(compare); + file.sort(compare); + } + + return dir.concat(file); + + function compare(a, b) { + return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1; + } + }, + /** + * Gets error message from error object + * @param {Error} err + * @param {...string} args + */ + errorMessage(err, ...args) { + args.forEach((arg, i) => { + if (/^(content|file|ftp|sftp|https?):/.test(arg)) { + args[i] = this.getVirtualPath(arg); + } + }); + + const extra = args.join("
        "); + let msg; + + if (typeof err === "string" && err) { + msg = err; + } else if (err instanceof Error) { + msg = err.message; + } else { + msg = strings["an error occurred"]; + } + + return msg + (extra ? "
        " + extra : ""); + }, + /** + * + * @param {Error} err + * @param {...string} args + * @returns {PromiseLike} + */ + error(err, ...args) { + if (err.code === 0) { + toast(err); + return; + } + + let hide = null; + const onhide = () => { + if (hide) hide(); + }; + + const msg = this.errorMessage(err, ...args); + alert(strings.error, msg, onhide); + + return new Promise((resolve) => { + hide = resolve; + }); + }, + /** + * Returns unique ID + * @returns {string} + */ + uuid() { + return ( + new Date().getTime() + Number.parseInt(Math.random() * 100000000000) + ).toString(36); + }, + /** + * Parses JSON string, if fails returns null + * @param {Object|Array} string + */ + parseJSON(string) { + if (!string) return null; + try { + return JSON.parse(string); + } catch (e) { + return null; + } + }, + /** + * Checks whether given type is directory or not + * @param {'dir'|'directory'|'folder'} type + * @returns {Boolean} + */ + isDir(type) { + return /^(dir|directory|folder)$/.test(type); + }, + /** + * Checks whether given type is file or not + * @param {'file'|'link'} type + * @returns {Boolean} + */ + isFile(type) { + return /^(file|link)$/.test(type); + }, + /** + * Replace matching part of url to alias name by which storage is added + * @param {String} url + * @returns {String} + */ + getVirtualPath(url) { + url = Url.parse(url).url; + + if (/^content:/.test(url)) { + const primary = Uri.getPrimaryAddress(url); + if (primary) { + return primary; + } + } + + /**@type {string[]} */ + const storageList = JSON.parse(localStorage.storageList || "[]"); + const storageListLen = storageList.length; + + for (let i = 0; i < storageListLen; ++i) { + const uuid = storageList[i]; + let storageUrl = Url.parse(uuid.uri || uuid.url || "").url; + if (!storageUrl) continue; + if (storageUrl.endsWith("/")) { + storageUrl = storageUrl.slice(0, -1); + } + const regex = new RegExp("^" + escapeStringRegexp(storageUrl)); + if (regex.test(url)) { + url = url.replace(regex, uuid.name); + break; + } + } + + return url; + }, + /** + * Updates uri of all active which matches the oldUrl as location + * of the file + * @param {String} oldUrl + * @param {String} newUrl + */ + updateUriOfAllActiveFiles(oldUrl, newUrl) { + const files = editorManager.files; + const { url } = Url.parse(oldUrl); + + for (let file of files) { + if (!file.uri) continue; + const fileUrl = Url.parse(file.uri).url; + if (new RegExp("^" + escapeStringRegexp(url)).test(fileUrl)) { + if (newUrl) { + file.uri = Url.join(newUrl, file.filename); + } else { + file.uri = null; + } + } + } + + editorManager.onupdate("file-delete"); + editorManager.emit("update", "file-delete"); + }, + /** + * Displays ad on the current page + */ + showAd() { + const { ad } = window; + if (IS_FREE_VERSION && innerHeight * devicePixelRatio > 600 && ad) { + const $page = tag.getAll("wc-page:not(#root)").pop(); + if ($page) { + ad.active = true; + ad.show(); + } + } + }, + /** + * Hides the ad + * @param {Boolean} [force=false] + */ + hideAd(force = false) { + const { ad } = window; + if (IS_FREE_VERSION && ad?.active) { + const $pages = tag.getAll(".page-replacement"); + const hide = $pages.length === 1; + + if (force || hide) { + ad.active = false; + ad.hide(); + } + } + }, + async toInternalUri(uri) { + return new Promise((resolve, reject) => { + window.resolveLocalFileSystemURL( + uri, + (entry) => { + resolve(entry.toInternalURL()); + }, + reject, + ); + }); + }, + promisify(func, ...args) { + return new Promise((resolve, reject) => { + func(...args, resolve, reject); + }); + }, + async checkAPIStatus() { + try { + const { status } = await ajax.get(Url.join(constants.API_BASE, "status")); + return status === "ok"; + } catch (error) { + return false; + } + }, + fixFilename(name) { + if (!name) return name; + return name.replace(/(\r\n)+|\r+|\n+|\t+/g, "").trim(); + }, + /** + * Creates a debounced function that delays invoking the input function until after 'wait' milliseconds have elapsed + * since the last time the debounced function was invoked. Useful for implementing behavior that should only happen + * after the input is complete. + * + * @param {Function} func - The function to debounce. + * @param {number} wait - The number of milliseconds to delay. + * @returns {Function} The new debounced function. + * @example + * window.addEventListener('resize', debounce(myFunction, 200)); + */ + debounce(func, wait) { + let timeout; + return function debounced(...args) { + const later = () => { + clearTimeout(timeout); + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + }, + defineDeprecatedProperty(obj, name, getter, setter) { + Object.defineProperty(obj, name, { + get: function () { + console.warn(`Property '${name}' is deprecated.`); + return getter.call(this); + }, + set: function (value) { + console.warn(`Property '${name}' is deprecated.`); + setter.call(this, value); + }, + }); + }, + parseHTML(html) { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const children = doc.body.children; + if (children.length === 1) { + return children[0]; + } + return Array.from(children); + }, + async createFileStructure(uri, pathString, isFile = true) { + const parts = pathString.split("/").filter(Boolean); + let currentUri = uri; + + // Determine if it's a special case URI + const isSpecialCase = currentUri.includes("::"); + let baseFolder; + + if (currentUri.includes("com.android.externalstorage.documents")) { + baseFolder = decodeURIComponent(currentUri.split("%3A")[1].split("/")[0]); + } else if ( + !( + currentUri.includes("com.android.externalstorage.documents") || + currentUri.includes("com.termux.documents") + ) + ) { + if (isFile) { + await fsOperation(uri).createFile(pathString); + } else { + await fsOperation(uri).createDirectory(pathString); + } + return { uri: uri, type: isFile ? "file" : "folder" }; + } + + for (let i = 0; i < parts.length; i++) { + const isLastElement = i === parts.length - 1; + const name = parts[i]; + let fullUri = currentUri; + + // Adjust URI for special cases + if (currentUri.includes("com.android.externalstorage.documents")) { + if (!isSpecialCase && i === 0) { + fullUri += `::primary:${baseFolder}/${name}`; + } else { + fullUri += `/${name}`; + } + } else if (currentUri.includes("com.termux.documents")) { + if (!isSpecialCase && i === 0) { + fullUri += `::/data/data/com.termux/files/home/${name}`; + } else { + fullUri += `/${name}`; + } + } + + if (isLastElement && isFile) { + // Create file if it's the last element and isFile is true + if (!(await fsOperation(fullUri).exists())) { + await fsOperation(currentUri).createFile(name); + } else { + return; + } + } else { + // Create directory + if (!(await fsOperation(fullUri).exists())) { + await fsOperation(currentUri).createDirectory(name); + } else { + return; + } + } + currentUri = fullUri; + } + let tileType; + if (isFile && parts.length === 1) { + tileType = "file"; + } else { + const urlParts = currentUri.split("/"); + const pathParts = pathString.split("/"); + const pathStartIndex = urlParts.findIndex( + (part) => part === pathParts[0], + ); + if (pathStartIndex !== -1) { + const pathEndIndex = pathStartIndex + pathParts.length; + urlParts.splice(pathStartIndex + 1, pathEndIndex - pathStartIndex - 1); + } + currentUri = urlParts.join("/"); + tileType = "folder"; + } + return { uri: currentUri, type: tileType }; + }, + formatDownloadCount(downloadCount) { + const units = ["", "K", "M", "B", "T"]; + let index = 0; + + while (downloadCount >= 1000 && index < units.length - 1) { + downloadCount /= 1000; + index++; + } + + const countStr = + downloadCount < 10 ? downloadCount.toFixed(2) : downloadCount.toFixed(1); + const trimmedCountStr = countStr.replace(/\.?0+$/, ""); + + return `${trimmedCountStr}${units[index]}`; + }, +}; diff --git a/src/utils/keyboardEvent.js b/src/utils/keyboardEvent.js index 516dc1940..3eabdd098 100644 --- a/src/utils/keyboardEvent.js +++ b/src/utils/keyboardEvent.js @@ -13,191 +13,279 @@ */ const keys = { - // arrow keys - 37: "ArrowLeft", - 38: "ArrowUp", - 39: "ArrowRight", - 40: "ArrowDown", - // special keys - 8: "Backspace", - 9: "Tab", - 13: "Enter", - 16: "ShiftLeft", - 17: "ControlLeft", - 18: "AltLeft", - 19: "Pause", - 20: "CapsLock", - 27: "Escape", - 32: " ", - 33: "PageUp", - 34: "PageDown", - 35: "End", - 36: "Home", - 45: "Insert", - 46: "Delete", + // arrow keys + 37: "ArrowLeft", + 38: "ArrowUp", + 39: "ArrowRight", + 40: "ArrowDown", + // special keys + 8: "Backspace", + 9: "Tab", + 13: "Enter", + 16: "ShiftLeft", + 17: "ControlLeft", + 18: "AltLeft", + 19: "Pause", + 20: "CapsLock", + 27: "Escape", + 32: " ", + 33: "PageUp", + 34: "PageDown", + 35: "End", + 36: "Home", + 45: "Insert", + 46: "Delete", }; - const initKeyboardEventType = (function (event) { - try { - event.initKeyboardEvent( - "keyup" // in DOMString typeArg - , false // in boolean canBubbleArg - , false // in boolean cancelableArg - , window // in views::AbstractView viewArg - , "+" // [test]in DOMString keyIdentifierArg | webkit event.keyIdentifier | IE9 event.key - , 3 // [test]in unsigned long keyLocationArg | webkit event.keyIdentifier | IE9 event.location - , true // [test]in boolean ctrlKeyArg | webkit event.shiftKey | old webkit event.ctrlKey | IE9 event.modifiersList - , false // [test]shift | alt - , true // [test]shift | alt - , false // meta - , false // altGraphKey - ); - - return ((event["keyIdentifier"] || event["key"]) == "+" && (event["location"]) || event["keyLocation"] == 3) && ( - event.ctrlKey ? - event.altKey ? // webkit - 1 : - 3 : - event.shiftKey ? - 2 : // webkit - 4 // IE9 - ) || 9; // FireFox|w3c - } catch (error) { - initKeyboardEventType = 0; - } + try { + event.initKeyboardEvent( + "keyup", // in DOMString typeArg + false, // in boolean canBubbleArg + false, // in boolean cancelableArg + window, // in views::AbstractView viewArg + "+", // [test]in DOMString keyIdentifierArg | webkit event.keyIdentifier | IE9 event.key + 3, // [test]in unsigned long keyLocationArg | webkit event.keyIdentifier | IE9 event.location + true, // [test]in boolean ctrlKeyArg | webkit event.shiftKey | old webkit event.ctrlKey | IE9 event.modifiersList + false, // [test]shift | alt + true, // [test]shift | alt + false, // meta + false, // altGraphKey + ); + + return ( + ((((event["keyIdentifier"] || event["key"]) === "+" && + event["location"]) || + event["keyLocation"] === 3) && + (event.ctrlKey + ? event.altKey + ? // webkit + 1 + : 3 + : event.shiftKey + ? 2 + : // webkit + 4)) || // IE9 + 9 + ); // FireFox|w3c + } catch (error) { + initKeyboardEventType = 0; + } })(document.createEvent("KeyboardEvent")); const keyboardEventPropertiesDictionary = { - "char": "", - "key": "", - "location": 0, - "ctrlKey": false, - "shiftKey": false, - "altKey": false, - "metaKey": false, - "repeat": false, - "locale": "", - - "detail": 0, - "bubbles": false, - "cancelable": false, - - //legacy properties - "keyCode": 0, - "charCode": 0, - "which": 0 + char: "", + key: "", + location: 0, + ctrlKey: false, + shiftKey: false, + altKey: false, + metaKey: false, + repeat: false, + locale: "", + + detail: 0, + bubbles: false, + cancelable: false, + + //legacy properties + keyCode: 0, + charCode: 0, + which: 0, }; const own = Function.prototype.call.bind(Object.prototype.hasOwnProperty); -const ObjectDefineProperty = Object.defineProperty || function (obj, prop, val) { - if ("value" in val) { - obj[prop] = val["value"]; - } -}; +const ObjectDefineProperty = + Object.defineProperty || + function (obj, prop, val) { + if ("value" in val) { + obj[prop] = val["value"]; + } + }; /** * Creates a keyboard event * @param {'keydown' | 'keyup'} type type of the event * @param {KeyEvent} dict - * @returns + * @returns */ export default function KeyboardEvent(type, dict) { - let event; - - if (initKeyboardEventType) { - event = document.createEvent("KeyboardEvent"); - } else { - event = document.createEvent("Event"); - } - - let propName; - let localDict = {}; - - if (!dict.key && (dict.keyCode || dict.which)) { - let key = keys[dict.keyCode || dict.which]; - if (!key) key = String.fromCharCode(dict.keyCode || dict.which); - dict.key = key; - } else if (dict.key && !dict.which && !dict.keyCode) { - let keyCode = Object.keys(keys).find(key => keys[key] === dict.key); - if (!keyCode) keyCode = dict.key.charCodeAt(0); - dict.keyCode = keyCode; - dict.which = keyCode; - } - - for (propName in keyboardEventPropertiesDictionary) - if (own(keyboardEventPropertiesDictionary, propName)) { - localDict[propName] = (own(dict, propName) && dict || keyboardEventPropertiesDictionary)[propName]; - } - - const ctrlKey = localDict["ctrlKey"]; - const shiftKey = localDict["shiftKey"]; - const altKey = localDict["altKey"]; - const metaKey = localDict["metaKey"]; - const altGraphKey = localDict["altGraphKey"]; - - const modifiersListArg = initKeyboardEventType > 3 ? ( - (ctrlKey ? "Control" : "") + - (shiftKey ? " Shift" : "") + - (altKey ? " Alt" : "") + - (metaKey ? " Meta" : "") + - (altGraphKey ? " AltGraph" : "") - ).trim() : null; - - const key = localDict["key"] + ""; - const char = localDict["char"] + ""; - const location = localDict["location"]; - const keyCode = localDict["keyCode"] || (localDict["keyCode"] = key && key.charCodeAt(0) || 0); - const charCode = localDict["charCode"] || (localDict["charCode"] = char && char.charCodeAt(0) || 0); - const bubbles = localDict["bubbles"]; - const cancelable = localDict["cancelable"]; - const repeat = localDict["repeat"]; - const locale = localDict["locale"]; - const view = window; - - localDict["which"] || (localDict["which"] = localDict["keyCode"]); - - if ("initKeyEvent" in event) { //FF - //https://developer.mozilla.org/en/DOM/event.initKeyEvent - event.initKeyEvent(type, bubbles, cancelable, view, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode); - } else if (initKeyboardEventType && "initKeyboardEvent" in event) { //https://developer.mozilla.org/en/DOM/KeyboardEvent#initKeyboardEvent() - if (initKeyboardEventType == 1) { // webkit - //http://stackoverflow.com/a/8490774/1437207 - //https://bugs.webkit.org/show_bug.cgi?id=13368 - event.initKeyboardEvent(type, bubbles, cancelable, view, key, location, ctrlKey, shiftKey, altKey, metaKey, altGraphKey); - } else if (initKeyboardEventType == 2) { // old webkit - //http://code.google.com/p/chromium/issues/detail?id=52408 - event.initKeyboardEvent(type, bubbles, cancelable, view, ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode); - } else if (initKeyboardEventType == 3) { // webkit - event.initKeyboardEvent(type, bubbles, cancelable, view, key, location, ctrlKey, altKey, shiftKey, metaKey, altGraphKey); - } else if (initKeyboardEventType == 4) { // IE9 - //http://msdn.microsoft.com/en-us/library/ie/ff975297(v=vs.85).aspx - event.initKeyboardEvent(type, bubbles, cancelable, view, key, location, modifiersListArg, repeat, locale); - } else { // FireFox|w3c - //http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent-initKeyboardEvent - //https://developer.mozilla.org/en/DOM/KeyboardEvent#initKeyboardEvent() - event.initKeyboardEvent(type, bubbles, cancelable, view, char, key, location, modifiersListArg, repeat, locale); - } - } else { - event.initEvent(type, bubbles, cancelable); - } - - for (propName in keyboardEventPropertiesDictionary) - if (own(keyboardEventPropertiesDictionary, propName)) { - if (event[propName] != localDict[propName]) { - try { - delete event[propName]; - ObjectDefineProperty(event, propName, { - writable: true, - "value": localDict[propName] - }); - } catch (error) { - //Some properties is read-only - } - - } - } - - return event; + let event; + + if (initKeyboardEventType) { + event = document.createEvent("KeyboardEvent"); + } else { + event = document.createEvent("Event"); + } + + let propName; + let localDict = {}; + + if (!dict.key && (dict.keyCode || dict.which)) { + let key = keys[dict.keyCode || dict.which]; + if (!key) key = String.fromCharCode(dict.keyCode || dict.which); + dict.key = key; + } else if (dict.key && !dict.which && !dict.keyCode) { + let keyCode = Object.keys(keys).find((key) => keys[key] === dict.key); + if (!keyCode) keyCode = dict.key.charCodeAt(0); + dict.keyCode = keyCode; + dict.which = keyCode; + } + + for (propName in keyboardEventPropertiesDictionary) + if (own(keyboardEventPropertiesDictionary, propName)) { + localDict[propName] = ((own(dict, propName) && dict) || + keyboardEventPropertiesDictionary)[propName]; + } + + const ctrlKey = localDict["ctrlKey"]; + const shiftKey = localDict["shiftKey"]; + const altKey = localDict["altKey"]; + const metaKey = localDict["metaKey"]; + const altGraphKey = localDict["altGraphKey"]; + + const modifiersListArg = + initKeyboardEventType > 3 + ? ( + (ctrlKey ? "Control" : "") + + (shiftKey ? " Shift" : "") + + (altKey ? " Alt" : "") + + (metaKey ? " Meta" : "") + + (altGraphKey ? " AltGraph" : "") + ).trim() + : null; + + const key = localDict["key"] + ""; + const char = localDict["char"] + ""; + const location = localDict["location"]; + const keyCode = + localDict["keyCode"] || + (localDict["keyCode"] = (key && key.charCodeAt(0)) || 0); + const charCode = + localDict["charCode"] || + (localDict["charCode"] = (char && char.charCodeAt(0)) || 0); + const bubbles = localDict["bubbles"]; + const cancelable = localDict["cancelable"]; + const repeat = localDict["repeat"]; + const locale = localDict["locale"]; + const view = window; + + localDict["which"] || (localDict["which"] = localDict["keyCode"]); + + if ("initKeyEvent" in event) { + //FF + //https://developer.mozilla.org/en/DOM/event.initKeyEvent + event.initKeyEvent( + type, + bubbles, + cancelable, + view, + ctrlKey, + altKey, + shiftKey, + metaKey, + keyCode, + charCode, + ); + } else if (initKeyboardEventType && "initKeyboardEvent" in event) { + //https://developer.mozilla.org/en/DOM/KeyboardEvent#initKeyboardEvent() + if (initKeyboardEventType === 1) { + // webkit + //http://stackoverflow.com/a/8490774/1437207 + //https://bugs.webkit.org/show_bug.cgi?id=13368 + event.initKeyboardEvent( + type, + bubbles, + cancelable, + view, + key, + location, + ctrlKey, + shiftKey, + altKey, + metaKey, + altGraphKey, + ); + } else if (initKeyboardEventType === 2) { + // old webkit + //http://code.google.com/p/chromium/issues/detail?id=52408 + event.initKeyboardEvent( + type, + bubbles, + cancelable, + view, + ctrlKey, + altKey, + shiftKey, + metaKey, + keyCode, + charCode, + ); + } else if (initKeyboardEventType === 3) { + // webkit + event.initKeyboardEvent( + type, + bubbles, + cancelable, + view, + key, + location, + ctrlKey, + altKey, + shiftKey, + metaKey, + altGraphKey, + ); + } else if (initKeyboardEventType === 4) { + // IE9 + //http://msdn.microsoft.com/en-us/library/ie/ff975297(v=vs.85).aspx + event.initKeyboardEvent( + type, + bubbles, + cancelable, + view, + key, + location, + modifiersListArg, + repeat, + locale, + ); + } else { + // FireFox|w3c + //http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent-initKeyboardEvent + //https://developer.mozilla.org/en/DOM/KeyboardEvent#initKeyboardEvent() + event.initKeyboardEvent( + type, + bubbles, + cancelable, + view, + char, + key, + location, + modifiersListArg, + repeat, + locale, + ); + } + } else { + event.initEvent(type, bubbles, cancelable); + } + + for (propName in keyboardEventPropertiesDictionary) + if (own(keyboardEventPropertiesDictionary, propName)) { + if (event[propName] !== localDict[propName]) { + try { + delete event[propName]; + ObjectDefineProperty(event, propName, { + writable: true, + value: localDict[propName], + }); + } catch (error) { + //Some properties is read-only + } + } + } + + return event; } diff --git a/src/utils/polyfill.js b/src/utils/polyfill.js index 405937bf1..695c56ce2 100644 --- a/src/utils/polyfill.js +++ b/src/utils/polyfill.js @@ -1,90 +1,90 @@ export default function loadPolyFill() { - if (!('isConnected' in Node.prototype)) { - Object.defineProperty(Node.prototype, 'isConnected', { - get() { - return ( - !this.ownerDocument || - !( - this.ownerDocument.compareDocumentPosition(this) & - this.DOCUMENT_POSITION_DISCONNECTED - ) - ); - }, - }); - } + if (!("isConnected" in Node.prototype)) { + Object.defineProperty(Node.prototype, "isConnected", { + get() { + return ( + !this.ownerDocument || + !( + this.ownerDocument.compareDocumentPosition(this) & + this.DOCUMENT_POSITION_DISCONNECTED + ) + ); + }, + }); + } - if (!DOMTokenList.prototype.replace) { - DOMTokenList.prototype.replace = function (a, b) { - if (this.contains(a)) { - this.add(b); - this.remove(a); - return true; - } - return false; - }; - } + if (!DOMTokenList.prototype.replace) { + DOMTokenList.prototype.replace = function (a, b) { + if (this.contains(a)) { + this.add(b); + this.remove(a); + return true; + } + return false; + }; + } - if (!HTMLElement.prototype.append) { - HTMLElement.prototype.append = function (...nodes) { - nodes.map((node) => this.appendChild(node)); - }; - } + if (!HTMLElement.prototype.append) { + HTMLElement.prototype.append = function (...nodes) { + nodes.map((node) => this.appendChild(node)); + }; + } - if (!HTMLElement.prototype.remove) { - HTMLElement.prototype.remove = function () { - this.parentElement.removeChild(this); - }; - } + if (!HTMLElement.prototype.remove) { + HTMLElement.prototype.remove = function () { + this.parentElement.removeChild(this); + }; + } - if (!HTMLElement.prototype.getParent) { - HTMLElement.prototype.getParent = function (queryString) { - const $$ = [...document.querySelectorAll(queryString)]; - for (let $ of $$) if ($.contains(this)) return $; - return null; - }; - } + if (!HTMLElement.prototype.getParent) { + HTMLElement.prototype.getParent = function (queryString) { + const $$ = [...document.querySelectorAll(queryString)]; + for (let $ of $$) if ($.contains(this)) return $; + return null; + }; + } - if (!String.prototype.hashCode) { - Object.defineProperty(String.prototype, 'hashCode', { - value: function () { - let hash = 0; - for (let i = 0; i < this.length; i++) { - const chr = this.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; // Convert to 32bit integer - } - return Math.abs(hash) + (hash < 0 ? 'N' : ''); - }, - }); - } + if (!String.prototype.hashCode) { + Object.defineProperty(String.prototype, "hashCode", { + value: function () { + let hash = 0; + for (let i = 0; i < this.length; i++) { + const chr = this.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer + } + return Math.abs(hash) + (hash < 0 ? "N" : ""); + }, + }); + } - if (!String.prototype.subtract) { - Object.defineProperty(String.prototype, 'subtract', { - value: function (str) { - return this.replace(new RegExp('^' + str), ''); - }, - }); - } + if (!String.prototype.subtract) { + Object.defineProperty(String.prototype, "subtract", { + value: function (str) { + return this.replace(new RegExp("^" + str), ""); + }, + }); + } - if (!String.prototype.capitalize) { - Object.defineProperty(String.prototype, 'capitalize', { - value: function (index) { - if (typeof index === 'number' && index >= 0) { - const strs = [ - this.slice(0, index), - this.slice(index, index + 1), - this.slice(index + 1), - ]; - return strs[0] + (strs[1] ? strs[1].toUpperCase() : '') + strs[2]; - } else { - let strs = this.split(' '); - strs = strs.map((str) => { - if (str.length > 0) return str[0].toUpperCase() + str.slice(1); - return ''; - }); - return strs.join(' '); - } - }, - }); - } + if (!String.prototype.capitalize) { + Object.defineProperty(String.prototype, "capitalize", { + value: function (index) { + if (typeof index === "number" && index >= 0) { + const strs = [ + this.slice(0, index), + this.slice(index, index + 1), + this.slice(index + 1), + ]; + return strs[0] + (strs[1] ? strs[1].toUpperCase() : "") + strs[2]; + } else { + let strs = this.split(" "); + strs = strs.map((str) => { + if (str.length > 0) return str[0].toUpperCase() + str.slice(1); + return ""; + }); + return strs.join(" "); + } + }, + }); + } } diff --git a/src/utils/taskManager.js b/src/utils/taskManager.js index 6283a8130..076403219 100644 --- a/src/utils/taskManager.js +++ b/src/utils/taskManager.js @@ -1,79 +1,79 @@ export default class TaskManager { - /** - * @typedef {'linear'|'parallel'} TaskManagerMode - */ + /** + * @typedef {'linear'|'parallel'} TaskManagerMode + */ - /** - * @type {Array<()=>Promise>} - */ - #queue = []; - /** - * @type {TaskManagerMode} - */ - #mode = 'linear'; - /** - * @type {boolean} - */ - #busy = false; - /** - * @type {TaskCallback[]} - */ - #listeners = []; - #count = 0; + /** + * @type {Array<()=>Promise>} + */ + #queue = []; + /** + * @type {TaskManagerMode} + */ + #mode = "linear"; + /** + * @type {boolean} + */ + #busy = false; + /** + * @type {TaskCallback[]} + */ + #listeners = []; + #count = 0; - /** - * Create new TaskManager - * @param {TaskManagerMode} mode - */ - constructor(mode) { - this.#mode = mode; + /** + * Create new TaskManager + * @param {TaskManagerMode} mode + */ + constructor(mode) { + this.#mode = mode; - this.queueTask = this.queueTask.bind(this); - } + this.queueTask = this.queueTask.bind(this); + } - /** - * Add task to queue - * @param {()=>Promise} task - */ - async queueTask(task) { - this.#queue.push(task); - this.#execNext(); - return new Promise((resolve, reject) => { - const listener = (t, result, error) => { - if (t !== task) return; + /** + * Add task to queue + * @param {()=>Promise} task + */ + async queueTask(task) { + this.#queue.push(task); + this.#execNext(); + return new Promise((resolve, reject) => { + const listener = (t, result, error) => { + if (t !== task) return; - this.#listeners = this.#listeners.filter((l) => l !== listener); + this.#listeners = this.#listeners.filter((l) => l !== listener); - if (error) reject(error); - else resolve(result); - }; + if (error) reject(error); + else resolve(result); + }; - this.#listeners.push(listener); - }); - } + this.#listeners.push(listener); + }); + } - async #execNext() { - if (this.#mode === 'linear' && this.#busy) { - return; - } + async #execNext() { + if (this.#mode === "linear" && this.#busy) { + return; + } - const task = this.#queue.shift(); - if (!task) return; + const task = this.#queue.shift(); + if (!task) return; - let result; - let error; + let result; + let error; - try { - this.#busy = true; - const id = this.#count++; - result = await task(id); - } catch (err) { - error = err; - } finally { - this.#busy = false; - } + try { + this.#busy = true; + const id = this.#count++; + result = await task(id); + } catch (err) { + error = err; + } finally { + this.#busy = false; + } - this.#listeners.forEach((l) => l(task, result, error)); - if (this.#mode === 'linear') this.#execNext(); - } -} \ No newline at end of file + this.#listeners.forEach((l) => l(task, result, error)); + if (this.#mode === "linear") this.#execNext(); + } +} diff --git a/utils/config.js b/utils/config.js index 82332ee64..93448eb87 100755 --- a/utils/config.js +++ b/utils/config.js @@ -1,109 +1,113 @@ -const path = require('path'); -const fs = require('fs'); -const { promisify } = require('util'); -const exec = promisify(require('child_process').exec); +const path = require("node:path"); +const fs = require("node:fs"); +const { promisify } = require("node:util"); +const exec = promisify(require("node:child_process").exec); (async () => { - const AD_APP_ID = 'ca-app-pub-5911839694379275~4255791238'; - const CONFIG_ID = /id="([a-z.]+")/; - const HTML_ID = /[a-z.]+<\/title>/; - const ID_PAID = 'com.foxdebug.acode'; - const ID_FREE = 'com.foxdebug.acodefree'; - const arg = process.argv[2]; - const arg2 = process.argv[3]; - const platformsDir = path.resolve(__dirname, '../platforms/'); - const babelrcpath = path.resolve(__dirname, '../.babelrc'); - const configpath = path.resolve(__dirname, '../config.xml'); - const htmlpath = path.resolve(__dirname, '../www/index.html'); - const logopath = path.resolve( - __dirname, - '../res/android/values/ic_launcher_background.xml', - ); - - const logoTextPaid = `<?xml version="1.0" encoding="utf-8"?> + const AD_APP_ID = "ca-app-pub-5911839694379275~4255791238"; + const CONFIG_ID = /id="([a-z.]+")/; + const HTML_ID = /<title>[a-z.]+<\/title>/; + const ID_PAID = "com.foxdebug.acode"; + const ID_FREE = "com.foxdebug.acodefree"; + const arg = process.argv[2]; + const arg2 = process.argv[3]; + const platformsDir = path.resolve(__dirname, "../platforms/"); + const babelrcpath = path.resolve(__dirname, "../.babelrc"); + const configpath = path.resolve(__dirname, "../config.xml"); + const htmlpath = path.resolve(__dirname, "../www/index.html"); + const logopath = path.resolve( + __dirname, + "../res/android/values/ic_launcher_background.xml", + ); + + const logoTextPaid = `<?xml version="1.0" encoding="utf-8"?> <resources> <color name="ic_launcher_background">#3a3e54</color> <color name="ic_splash_background">#3a3e54</color> </resources>`; - const logoTextFree = `<?xml version="1.0" encoding="utf-8"?> + const logoTextFree = `<?xml version="1.0" encoding="utf-8"?> <resources> <color name="ic_launcher_background">#ffffff</color> <color name="ic_splash_background">#313131</color> </resources>`; - try { - let babelrcText = fs.readFileSync(babelrcpath, 'utf-8'); - let config = fs.readFileSync(configpath, 'utf-8'); - let html = fs.readFileSync(htmlpath, 'utf-8'); - let platforms = fs.readdirSync(platformsDir).filter((file) => !file.startsWith('.')); - let logo, id, currentId; - - currentId = /id="([a-z.]+)"/.exec(config)[1]; - babelrc = JSON.parse(babelrcText); - - if (arg === 'd') { - babelrc.compact = false; - } else if (arg === 'p') { - babelrc.compact = true; - } - - if (arg2 === 'free') { - logo = logoTextFree; - id = ID_FREE; - } else { - logo = logoTextPaid; - id = ID_PAID; - } - - fs.writeFileSync(babelrcpath, babelrcText, 'utf8'); - - if (currentId !== id) { - const promises = []; - - html = html.replace(HTML_ID, `<title>${id}`); - config = config.replace(CONFIG_ID, `id="${id}"`); - babelrcText = JSON.stringify(babelrc, undefined, 2); - - fs.writeFileSync(htmlpath, html, 'utf8'); - fs.writeFileSync(logopath, logo, 'utf8'); - fs.writeFileSync(configpath, config, 'utf8'); - - - for (let platform of platforms) { - if (!platform) continue; - - promises.push( - (async () => { - console.log( - `|--- Preparing platform ${platform.toUpperCase()} ---|`, - ); - - if (id === ID_FREE) { - console.log(`|--- Installing Admob ---|`); - await exec(`cordova plugin add cordova-plugin-consent@2.4.0 --save`); - await exec(`cordova plugin add admob-plus-cordova@1.28.0 --save --variable APP_ID_ANDROID="${AD_APP_ID}" --variable PLAY_SERVICES_VERSION="21.5.0"`); - console.log('DONE! Installing admob-plus-cordova'); - } else { - console.log(`|--- Removing Admob ---|`); - await exec(`cordova plugin remove cordova-plugin-consent --save`); - await exec(`cordova plugin remove admob-plus-cordova --save`); - console.log('DONE! Removing admob-plus-cordova'); - } - - console.log(`|--- Reinstalling platform ---|`); - const { stderr } = await exec(`yarn clean`); - if (stderr) console.error(stderr); - else console.log('DONE! Reinstalling platform'); - - })(), - ); - } - - await Promise.all(promises); - } - process.exit(0); - } catch (error) { - console.error(error); - process.exit(1); - } + try { + let babelrcText = fs.readFileSync(babelrcpath, "utf-8"); + let config = fs.readFileSync(configpath, "utf-8"); + let html = fs.readFileSync(htmlpath, "utf-8"); + let platforms = fs + .readdirSync(platformsDir) + .filter((file) => !file.startsWith(".")); + let logo, id, currentId; + + currentId = /id="([a-z.]+)"/.exec(config)[1]; + babelrc = JSON.parse(babelrcText); + + if (arg === "d") { + babelrc.compact = false; + } else if (arg === "p") { + babelrc.compact = true; + } + + if (arg2 === "free") { + logo = logoTextFree; + id = ID_FREE; + } else { + logo = logoTextPaid; + id = ID_PAID; + } + + fs.writeFileSync(babelrcpath, babelrcText, "utf8"); + + if (currentId !== id) { + const promises = []; + + html = html.replace(HTML_ID, `${id}`); + config = config.replace(CONFIG_ID, `id="${id}"`); + babelrcText = JSON.stringify(babelrc, undefined, 2); + + fs.writeFileSync(htmlpath, html, "utf8"); + fs.writeFileSync(logopath, logo, "utf8"); + fs.writeFileSync(configpath, config, "utf8"); + + for (let platform of platforms) { + if (!platform) continue; + + promises.push( + (async () => { + console.log( + `|--- Preparing platform ${platform.toUpperCase()} ---|`, + ); + + if (id === ID_FREE) { + console.log(`|--- Installing Admob ---|`); + await exec( + `cordova plugin add cordova-plugin-consent@2.4.0 --save`, + ); + await exec( + `cordova plugin add admob-plus-cordova@1.28.0 --save --variable APP_ID_ANDROID="${AD_APP_ID}" --variable PLAY_SERVICES_VERSION="21.5.0"`, + ); + console.log("DONE! Installing admob-plus-cordova"); + } else { + console.log(`|--- Removing Admob ---|`); + await exec(`cordova plugin remove cordova-plugin-consent --save`); + await exec(`cordova plugin remove admob-plus-cordova --save`); + console.log("DONE! Removing admob-plus-cordova"); + } + + console.log(`|--- Reinstalling platform ---|`); + const { stderr } = await exec(`yarn clean`); + if (stderr) console.error(stderr); + else console.log("DONE! Reinstalling platform"); + })(), + ); + } + + await Promise.all(promises); + } + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } })(); diff --git a/utils/lang.js b/utils/lang.js index 99a5c0a31..b6af02506 100755 --- a/utils/lang.js +++ b/utils/lang.js @@ -1,15 +1,15 @@ -const path = require('path'); -const fs = require('fs'); -const yargs = require('yargs'); -const readline = require('readline'); -const args = yargs.alias('a', 'all').argv; +const path = require("node:path"); +const fs = require("node:fs"); +const yargs = require("yargs"); +const readline = require("node:readline"); +const args = yargs.alias("a", "all").argv; -const dir = path.resolve(__dirname, '../src/lang'); +const dir = path.resolve(__dirname, "../src/lang"); const read = readline.createInterface({ - input: process.stdin, - output: process.stdout + input: process.stdin, + output: process.stdout, }); -const enLang = path.join(dir, 'en-us.json'); +const enLang = path.join(dir, "en-us.json"); const list = fs.readdirSync(dir); const len = list.length; let command = ""; @@ -17,272 +17,265 @@ let arg = ""; let val = ""; if (args._.length > 3) { - console.error("Invalid arguments", args._); - process.exit(0); + console.error("Invalid arguments", args._); + process.exit(0); } else { - command = args._[0]; - arg = args._[1]; - val = args._[2]; + command = args._[0]; + arg = args._[1]; + val = args._[2]; } switch (command) { - case 'add': - case 'remove': - case 'update': - case 'update-key': - case 'search': - case 'check': - update(); - break; - default: - console.error(`Missing/Invalid arguments. + case "add": + case "remove": + case "update": + case "update-key": + case "search": + case "check": + update(); + break; + default: + console.error(`Missing/Invalid arguments. use 'add' to add a new string use 'remove' to remove a string use 'search' to search a string use 'update' to update a string use 'update-key' to update a key use 'check' to check a string`); - process.exit(); + process.exit(); } async function update() { - let key; - - if (command === "check") { - let total = 0; - let done = 0; - - fs.readFile(enLang, 'utf-8', (err, data) => { - if (err) { - console.error(err); - process.exit(0); - return; - } - - let error = false; - - const fix = arg === 'fix'; - const enLangData = JSON.parse(data); - - list.forEach((file, i) => { - if (file === 'en-us.json') return; - - let flagError = false; - let langFile = path.join(dir, file); - const exit = (i, len) => { - if (i + 1 === len) { - if (!error) { - console.log("\nGOOD NEWS! No Error Found\n"); - } - process.exit(0); - } - }; - - fs.readFile(langFile, 'utf-8', (err, data) => { - if (err) { - console.error(err); - process.exit(1); - return; - } - - let langError = () => { - if (!flagError) { - error = true; - flagError = true; - console.log(`-------------- ${file}`); - } - }; - const langData = JSON.parse(data); - flagError = false; - - for (let enKey in enLangData) { - - const key = Object.keys(langData).find((k) => { - try { - if ((new RegExp(`^${escapeRegExp(k)}$`, 'i')).test(enKey)) { - return true; - } - return false; - } catch (e) { - console.log({ e, k }); - return false; - } - }); - - if (!key) { - langError(); - if (fix) { - langData[enKey] = enLangData[enKey]; - } - - console.log(`Missing: ${enKey} ${fix ? '✔' : ''}`); - } else if (key !== enKey) { - langError(); - console.log(`Fix: "${key} --> ${enKey}" ${fix ? '✔' : ''}`); - - if (fix) { - const val = langData[key]; - delete langData[key]; - langData[enKey] = val; - } - } - } - - if (flagError) { - if (fix) { - total += 1; - const langJSONData = JSON.stringify(langData, undefined, 2); - fs.writeFile(langFile, langJSONData, (err) => { - if (err) { - console.error(err); - process.exit(1); - } - done += 1; - exit(done, total); - }); - } - console.log('\n'); - } - - if (!fix) { - exit(i, len); - } - }); - }); - }); - return; - } - - if (!arg) { - getStr("string: ") - .then(res => { - key = res.toLowerCase(); - arg = res; - askTranslation(); - }); - return; - } - - key = arg.toLowerCase(); - let newKey = val; - askTranslation(); - - if (command === 'update-key' && !newKey) { - newKey = await getStr("new key: "); - } - - function askTranslation(i = 0) { - const lang = list[i]; - const langName = lang.split('.')[0]; - if (command === "add") { - if (!args.a) { - getStr(`${langName}: `) - .then(addString); - return; - } - - addString(); - } else if (command === "remove") { - update(strings => { - if (key in strings) { - delete strings[key]; - console.log(`Removed: ${key}`); - return strings; - } else { - console.error("String not exists"); - } - }); - } else if (command === "update-key") { - update(strings => { - const val = strings[key]; - delete strings[key]; - strings[newKey] = val; - return strings; - }); - } else if (command === "update") { - if (val) { - update(strings => { - strings[key] = val; - return strings; - }); - } else { - getStr(`${langName}: `) - .then(res => { - - res = res || arg; - update(strings => { - strings[key] = res; - return strings; - }); - - }); - } - } else if (command === "search") { - update(string => { - if (key in string) console.log(`${key}(${langName}): ${string[key]}`); - else { - console.log(`${key} not exists`); - process.exit(); - } - }); - } - - function update(modify) { - const file = path.resolve(dir, lang); - const text = fs.readFileSync(file, "utf8"); - const strings = modify(JSON.parse(text)); - if (strings) { - const newText = JSON.stringify(strings, undefined, 2); - fs.writeFile(file, newText, "utf8", err => { - if (err) { - console.error(err); - process.exit(1); - } - - next(); - }); - } else { - next(); - } - - function next() { - if (i === list.length - 1) { - process.exit(); - } else { - askTranslation(++i); - } - } - } - - function addString(string) { - string = string || arg; - update(strings => { - if (key in strings) { - console.error("String already exists"); - process.exit(1); - } else { - strings[key] = string; - return strings; - } - }); - } - } - + let key; + + if (command === "check") { + let total = 0; + let done = 0; + + fs.readFile(enLang, "utf-8", (err, data) => { + if (err) { + console.error(err); + process.exit(0); + return; + } + + let error = false; + + const fix = arg === "fix"; + const enLangData = JSON.parse(data); + + list.forEach((file, i) => { + if (file === "en-us.json") return; + + let flagError = false; + let langFile = path.join(dir, file); + const exit = (i, len) => { + if (i + 1 === len) { + if (!error) { + console.log("\nGOOD NEWS! No Error Found\n"); + } + process.exit(0); + } + }; + + fs.readFile(langFile, "utf-8", (err, data) => { + if (err) { + console.error(err); + process.exit(1); + return; + } + + let langError = () => { + if (!flagError) { + error = true; + flagError = true; + console.log(`-------------- ${file}`); + } + }; + const langData = JSON.parse(data); + flagError = false; + + for (let enKey in enLangData) { + const key = Object.keys(langData).find((k) => { + try { + if (new RegExp(`^${escapeRegExp(k)}$`, "i").test(enKey)) { + return true; + } + return false; + } catch (e) { + console.log({ e, k }); + return false; + } + }); + + if (!key) { + langError(); + if (fix) { + langData[enKey] = enLangData[enKey]; + } + + console.log(`Missing: ${enKey} ${fix ? "✔" : ""}`); + } else if (key !== enKey) { + langError(); + console.log(`Fix: "${key} --> ${enKey}" ${fix ? "✔" : ""}`); + + if (fix) { + const val = langData[key]; + delete langData[key]; + langData[enKey] = val; + } + } + } + + if (flagError) { + if (fix) { + total += 1; + const langJSONData = JSON.stringify(langData, undefined, 2); + fs.writeFile(langFile, langJSONData, (err) => { + if (err) { + console.error(err); + process.exit(1); + } + done += 1; + exit(done, total); + }); + } + console.log("\n"); + } + + if (!fix) { + exit(i, len); + } + }); + }); + }); + return; + } + + if (!arg) { + getStr("string: ").then((res) => { + key = res.toLowerCase(); + arg = res; + askTranslation(); + }); + return; + } + + key = arg.toLowerCase(); + let newKey = val; + askTranslation(); + + if (command === "update-key" && !newKey) { + newKey = await getStr("new key: "); + } + + function askTranslation(i = 0) { + const lang = list[i]; + const langName = lang.split(".")[0]; + if (command === "add") { + if (!args.a) { + getStr(`${langName}: `).then(addString); + return; + } + + addString(); + } else if (command === "remove") { + update((strings) => { + if (key in strings) { + delete strings[key]; + console.log(`Removed: ${key}`); + return strings; + } else { + console.error("String not exists"); + } + }); + } else if (command === "update-key") { + update((strings) => { + const val = strings[key]; + delete strings[key]; + strings[newKey] = val; + return strings; + }); + } else if (command === "update") { + if (val) { + update((strings) => { + strings[key] = val; + return strings; + }); + } else { + getStr(`${langName}: `).then((res) => { + res = res || arg; + update((strings) => { + strings[key] = res; + return strings; + }); + }); + } + } else if (command === "search") { + update((string) => { + if (key in string) console.log(`${key}(${langName}): ${string[key]}`); + else { + console.log(`${key} not exists`); + process.exit(); + } + }); + } + + function update(modify) { + const file = path.resolve(dir, lang); + const text = fs.readFileSync(file, "utf8"); + const strings = modify(JSON.parse(text)); + if (strings) { + const newText = JSON.stringify(strings, undefined, 2); + fs.writeFile(file, newText, "utf8", (err) => { + if (err) { + console.error(err); + process.exit(1); + } + + next(); + }); + } else { + next(); + } + + function next() { + if (i === list.length - 1) { + process.exit(); + } else { + askTranslation(++i); + } + } + } + + function addString(string) { + string = string || arg; + update((strings) => { + if (key in strings) { + console.error("String already exists"); + process.exit(1); + } else { + strings[key] = string; + return strings; + } + }); + } + } } function getStr(str) { - return new Promise((resolve, reject) => { - if (val) { - resolve(val); - return; - } - - read.question(str, res => { - resolve(res); - }); - }); + return new Promise((resolve, reject) => { + if (val) { + resolve(val); + return; + } + + read.question(str, (res) => { + resolve(res); + }); + }); } function escapeRegExp(text) { - return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); } diff --git a/utils/loadStyles.js b/utils/loadStyles.js index 78ca6a22c..cfe038dab 100644 --- a/utils/loadStyles.js +++ b/utils/loadStyles.js @@ -1,23 +1,23 @@ -const fs = require('fs'); -const path = require('path'); +const fs = require("node:fs"); +const path = require("node:path"); -const WWW = path.resolve(__dirname, '../www'); -const CSS = path.resolve(WWW, 'css/build'); -const CSS_PATH = './css/build/'; +const WWW = path.resolve(__dirname, "../www"); +const CSS = path.resolve(WWW, "css/build"); +const CSS_PATH = "./css/build/"; -const cssFiles = fs.readdirSync(CSS).filter((file) => file.endsWith('.css')); -const htmlFiles = fs.readdirSync(WWW).filter((file) => file.endsWith('.html')); +const cssFiles = fs.readdirSync(CSS).filter((file) => file.endsWith(".css")); +const htmlFiles = fs.readdirSync(WWW).filter((file) => file.endsWith(".html")); try { - for (let htmlFile of htmlFiles) { - loadStyles(path.resolve(WWW, htmlFile)); - } + for (let htmlFile of htmlFiles) { + loadStyles(path.resolve(WWW, htmlFile)); + } } catch (error) { - console.error(error); - process.exit(1); + console.error(error); + process.exit(1); } -console.log('Styles loaded'); +console.log("Styles loaded"); process.exit(0); /** @@ -25,18 +25,18 @@ process.exit(0); * @param {String} htmlFile */ function loadStyles(htmlFile) { - let styles = ''; + let styles = ""; - for (let cssFile of cssFiles) { - styles += `\n`; - } + for (let cssFile of cssFiles) { + styles += `\n`; + } - styles += '\n\n'; + styles += "\n\n"; - const rgx = - /([^<]*(?:<(?!!--styles_end-->)[^<]*)*)\n*/gim; - let html = fs.readFileSync(htmlFile, 'utf8'); - html = html.replace(rgx, ''); - html = html.replace('', styles + ''); - fs.writeFileSync(htmlFile, html); + const rgx = + /([^<]*(?:<(?!!--styles_end-->)[^<]*)*)\n*/gim; + let html = fs.readFileSync(htmlFile, "utf8"); + html = html.replace(rgx, ""); + html = html.replace("", styles + ""); + fs.writeFileSync(htmlFile, html); } diff --git a/utils/setup.js b/utils/setup.js index 178e3f599..9eaafa923 100644 --- a/utils/setup.js +++ b/utils/setup.js @@ -1,36 +1,39 @@ // setup acode for the first time // 1. install dependencies // 2. add cordova platform android@10.2 -// 3. install cordova plugins +// 3. install cordova plugins // cordova-plugin-buildinfo // cordova-plugin-device // cordova-plugin-file // all the plugins in ./src/plugins -const { execSync } = require('child_process'); -const fs = require('fs'); -const path = require('path'); -const PLATFORM_FILES = ['.DS_Store']; +const { execSync } = require("node:child_process"); +const fs = require("node:fs"); +const path = require("node:path"); +const PLATFORM_FILES = [".DS_Store"]; -execSync('npm install', { stdio: 'inherit' }); +execSync("npm install", { stdio: "inherit" }); try { - execSync('cordova platform add android', { stdio: 'inherit' }); + execSync("cordova platform add android", { stdio: "inherit" }); } catch (error) { - // ignore + // ignore } try { - execSync('mkdir -p www/css/build www/js/build', { stdio: 'inherit' }) + execSync("mkdir -p www/css/build www/js/build", { stdio: "inherit" }); } catch (error) { - console.log("Failed to create www/css/build & www/js/build directories (You may Try after reading The Error)", error) + console.log( + "Failed to create www/css/build & www/js/build directories (You may Try after reading The Error)", + error, + ); } -execSync('cordova plugin add cordova-plugin-buildinfo', { stdio: 'inherit' }); -execSync('cordova plugin add cordova-plugin-device', { stdio: 'inherit' }); -execSync('cordova plugin add cordova-plugin-file', { stdio: 'inherit' }); +execSync("cordova plugin add cordova-plugin-buildinfo", { stdio: "inherit" }); +execSync("cordova plugin add cordova-plugin-device", { stdio: "inherit" }); +execSync("cordova plugin add cordova-plugin-file", { stdio: "inherit" }); -const plugins = fs.readdirSync(path.join(__dirname, '../src/plugins')); -plugins.forEach(plugin => { - if (PLATFORM_FILES.includes(plugin) || plugin.startsWith('.')) return; - execSync(`cordova plugin add ./src/plugins/${plugin}`, { stdio: 'inherit' }); +const plugins = fs.readdirSync(path.join(__dirname, "../src/plugins")); +plugins.forEach((plugin) => { + if (PLATFORM_FILES.includes(plugin) || plugin.startsWith(".")) return; + execSync(`cordova plugin add ./src/plugins/${plugin}`, { stdio: "inherit" }); });