├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── workflows │ ├── code-lint.yml │ └── codeql-analysis.yml ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── convert-logs.js ├── gulpfile.js ├── package-lock.json ├── package.json ├── read-log.js ├── src ├── README.md ├── background.js ├── background │ ├── clipboard.js │ ├── commands.js │ ├── copy.js │ ├── helpers.js │ ├── index.js │ ├── menu.js │ └── paste.js ├── colors.sass ├── content.js ├── content.sass ├── content │ ├── capture.js │ ├── index.js │ ├── infobox.js │ ├── loader.js │ ├── scroller.js │ ├── selection.js │ └── table.js ├── ico128.png ├── ico16.png ├── ico19.png ├── ico32.png ├── ico38.png ├── ico48.png ├── lib │ ├── cell.js │ ├── css.js │ ├── dom.js │ ├── event.js │ ├── keyboard.js │ ├── matrix.js │ ├── message.js │ ├── number.js │ ├── preferences.js │ └── util.js ├── manifest.json ├── options.js ├── options.pug ├── options.sass ├── popup.js ├── popup.pug └── popup.sass └── test ├── jasmine.json ├── number_test.js └── server ├── base.html ├── frame.html ├── index.js ├── public └── img │ ├── a.png │ ├── b.png │ └── c.png └── templates ├── big.js ├── dynamic.html ├── forms.html ├── framea.html ├── frameb.html ├── frameset.html ├── frameset_content.html ├── hidden.html ├── nested.html ├── numbers.html ├── paste.html ├── script.html ├── scroll.js ├── simple.html ├── spans.html └── styled.html /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = off 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nirantak 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | - CopyTables version: 4 | - Firefox version: 5 | - Operating System: 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ```txt 13 | Paste the command(s) you ran and the output/screenshots. 14 | If there was a crash, please include any logs/traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/workflows/code-lint.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up NodeJS 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: "18.3.0" 20 | cache: "npm" 21 | 22 | - name: Install Dependencies 23 | run: npm install 24 | 25 | - name: Run Code Formatter 26 | run: npm run format -- --check 27 | 28 | - name: Run Firefox Extension Linter 29 | run: | 30 | npm run prod 31 | npm run lint 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | - cron: "30 6 * * 1" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: ["javascript"] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v2 28 | 29 | # Initializes the CodeQL tools for scanning 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v1 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java) 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v1 38 | 39 | - name: Perform CodeQL Analysis 40 | uses: github/codeql-action/analyze@v1 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | app 4 | npm-debug.log 5 | *.zip 6 | ._* 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nirantak Raghav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CopyTables 2 | 3 | ![CopyTables](https://raw.githubusercontent.com/nirantak/copytables/main/src/ico128.png) 4 | 5 | Firefox extension to select and copy table cells. 6 | 7 | [Install for Firefox](https://addons.mozilla.org/firefox/addon/copywebtables/) 8 | 9 | ## Usage 10 | 11 | - Hold Opt (macOS) or Alt (Windows) and drag to select cells. 12 | - Hold Opt+Cmd (macOS) or Alt+Ctrl (Windows) and drag to select columns. 13 | - Copy selection (or the whole table) as seen on the screen (for rich text editors) 14 | - Copy as CSV or TSV (for Spreadsheets). 15 | - Copy as HTML (for your website). 16 | 17 | Forked from [gebrkn/copytables](https://github.com/gebrkn/copytables) for Chrome ([Web Store](https://chrome.google.com/webstore/detail/copytables/ekdpkppgmlalfkphpibadldikjimijon)). 18 | 19 | ## Building the Extension 20 | 21 | Build tested using: `node` v18.3.0 (`npm` v8.11.0) on macOS 12.4 22 | 23 | ```bash 24 | # Clone the repo 25 | git clone https://github.com/nirantak/copytables.git 26 | cd copytables 27 | 28 | # Install dependencies 29 | npm install 30 | 31 | # Build zipped extension 32 | npm run deploy 33 | ## Output: copytables-0.1.1.zip 34 | 35 | # Test in development mode 36 | npm start 37 | ## This does the following: 38 | ## - Runs a dev server with dummy data on localhost:9876 39 | ## - Runs gulp to watch and rebuild for code changes 40 | ## - Opens Firefox with the extension loaded in debug mode 41 | ``` 42 | -------------------------------------------------------------------------------- /convert-logs.js: -------------------------------------------------------------------------------- 1 | /// In dev, rewrite console.logs to something more readable when logged to stdout 2 | /// In production, get rid of them 3 | 4 | function __dbg() { 5 | var nl = "\n", 6 | buf = []; 7 | 8 | function type(x) { 9 | try { 10 | return Object.prototype.toString 11 | .call(x) 12 | .replace("[object ", "") 13 | .replace("]", ""); 14 | } catch (e) { 15 | return ""; 16 | } 17 | } 18 | 19 | function props(x, depth) { 20 | var r = Array.isArray(x) ? [] : {}; 21 | try { 22 | Object.keys(x).forEach(function (k) { 23 | r[k] = inspect(x[k], depth + 1); 24 | }); 25 | return r; 26 | } catch (e) { 27 | return "error"; 28 | } 29 | } 30 | 31 | function inspect(x, depth) { 32 | if (depth > 5) return "..."; 33 | if (typeof x !== "object" || x === null) return x; 34 | var t = type(x), 35 | p = props(x, depth); 36 | if (t === "Object" || t === "Array") return p; 37 | var r = {}; 38 | r[t] = p; 39 | return r; 40 | } 41 | 42 | buf.push(location ? location.href : "??"); 43 | 44 | [].forEach.call(arguments, function (arg) { 45 | buf.push(inspect(arg, 0)); 46 | }); 47 | 48 | return "@@CT<<" + JSON.stringify(buf) + ">>CT@@"; 49 | } 50 | 51 | function handleLogging(content, path, isDev) { 52 | var dbg = String(__dbg); 53 | var out = []; 54 | 55 | if (isDev) { 56 | out.push(dbg.replace(/\s+/g, " ")); 57 | } 58 | 59 | content.split("\n").forEach(function (line, lnum) { 60 | var m = line.match(/(.*?)console\.log\((.*)\)(.*)/); 61 | 62 | if (m) { 63 | if (isDev) { 64 | out.push( 65 | [ 66 | m[1], 67 | "console.log(__dbg(", 68 | JSON.stringify(path), 69 | "," + (lnum + 1), 70 | "," + m[2] + "))", 71 | m[3], 72 | ].join("") 73 | ); 74 | } 75 | } else { 76 | if (isDev && line.match(/function/)) { 77 | out.push(" // " + path + ":" + (lnum + 1)); 78 | } 79 | out.push(line); 80 | } 81 | }); 82 | 83 | return out.join("\n"); 84 | } 85 | 86 | module.exports = function (content) { 87 | return handleLogging(content, this.resourcePath, this.query == "?dev"); 88 | }; 89 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const clip = require("gulp-clip-empty-files"), 4 | cp = require("child_process"), 5 | del = require("del"), 6 | fs = require("fs"), 7 | gulp = require("gulp"), 8 | named = require("vinyl-named"), 9 | pug = require("gulp-pug"), 10 | path = require("path"), 11 | sass = require("gulp-sass")(require("node-sass")), 12 | sassTypes = require("node-sass").types, 13 | webpack = require("webpack-stream"), 14 | uglify = require("gulp-uglify"), 15 | through = require("through2"); 16 | const DEST = "./app"; 17 | const TEST_URL = "http://localhost:9876/all"; 18 | const IS_DEV = process.env.NODE_ENV === "development"; 19 | 20 | // based on https://coderwall.com/p/fhgu_q/inlining-images-with-gulp-sass 21 | function sassInlineImage(file) { 22 | var filePath = path.resolve(process.cwd(), file.getValue()), 23 | ext = filePath.split(".").pop(), 24 | data = fs.readFileSync(filePath), 25 | buffer = Buffer.from(data), 26 | str = '"data:image/' + ext + ";base64," + buffer.toString("base64") + '"'; 27 | return sassTypes.String(str); 28 | } 29 | 30 | const webpackConfig = { 31 | mode: IS_DEV ? "development" : "production", 32 | devtool: false, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.js$/, 37 | use: [ 38 | { 39 | loader: __dirname + "/convert-logs?" + (IS_DEV ? "dev" : ""), 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | }; 46 | 47 | gulp.task("pug", function () { 48 | return gulp.src("./src/*.pug").pipe(pug()).pipe(gulp.dest(DEST)); 49 | }); 50 | 51 | gulp.task("sass", function () { 52 | return gulp 53 | .src("./src/*.sass") 54 | .pipe( 55 | sass({ 56 | functions: { 57 | "inline-image($file)": sassInlineImage, 58 | }, 59 | }) 60 | ) 61 | .pipe(clip()) 62 | .pipe(gulp.dest(DEST)); 63 | }); 64 | 65 | gulp.task("copy", function () { 66 | return gulp.src("./src/*.{png,css,json,svg}").pipe(gulp.dest(DEST)); 67 | }); 68 | 69 | gulp.task("js", function () { 70 | return gulp 71 | .src("./src/*.js") 72 | .pipe(named()) 73 | .pipe(webpack(webpackConfig)) 74 | .pipe(IS_DEV ? through.obj() : uglify()) 75 | .pipe(gulp.dest(DEST)); 76 | }); 77 | 78 | gulp.task("clean", function (done) { 79 | del.sync(DEST); 80 | done(); 81 | }); 82 | 83 | gulp.task("zip", function (done) { 84 | const m = require("./src/manifest.json"), 85 | fn = `copytables-${m.version}.zip`; 86 | 87 | cp.execSync(`rm -f ./${fn} && zip -j ./${fn} ./app/*`); 88 | done(); 89 | }); 90 | 91 | gulp.task("make", gulp.parallel("pug", "sass", "copy", "js")); 92 | gulp.task("build", gulp.series("clean", "make")); 93 | gulp.task("deploy", gulp.series("build", "zip")); 94 | 95 | gulp.task("watch", function () { 96 | gulp.watch("./src/**/*", gulp.series("make")); 97 | }); 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copytables", 3 | "version": "0.1.1", 4 | "description": "Firefox extension to select and copy table cells", 5 | "author": "Nirantak Raghav", 6 | "license": "MIT", 7 | "homepage": "https://github.com/nirantak/copytables", 8 | "private": true, 9 | "scripts": { 10 | "server": "node ./test/server/index.js", 11 | "watch": "NODE_ENV=development gulp watch", 12 | "ext": "web-ext run -s ./app -u http://localhost:9876/all", 13 | "start": "npm run dev && concurrently -k -c 'cyan,green,yellow' 'npm:watch' 'npm:server' 'npm:ext'", 14 | "clean": "rm -rf ./app ./copytables*.zip", 15 | "dev": "NODE_ENV=development gulp build", 16 | "prod": "NODE_ENV=production gulp build", 17 | "deploy": "NODE_ENV=production gulp deploy", 18 | "test": "jasmine --config=./test/jasmine.json", 19 | "lint": "web-ext lint -s ./app", 20 | "format": "prettier --write '!app/**/*' '**/*.{js,scss,html,json}'" 21 | }, 22 | "engines": { 23 | "node": "18.3.0", 24 | "npm": "8.11.0" 25 | }, 26 | "dependencies": { 27 | "del": "^6.1.1", 28 | "gulp": "^4.0.2", 29 | "gulp-clip-empty-files": "^0.1.2", 30 | "gulp-pug": "^5.0.0", 31 | "gulp-rename": "^2.0.0", 32 | "gulp-sass": "^5.1.0", 33 | "gulp-uglify": "^3.0.2", 34 | "node-sass": "^7.0.1", 35 | "vinyl-named": "^1.1.0", 36 | "webpack-stream": "^7.0.0" 37 | }, 38 | "devDependencies": { 39 | "concurrently": "^7.2.1", 40 | "express": "^4.18.1", 41 | "glob": "^8.0.3", 42 | "jasmine": "^4.1.0", 43 | "mustache": "^4.2.0", 44 | "prettier": "^2.6.2", 45 | "web-ext": "^6.8.0" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/nirantak/copytables.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/nirantak/copytables/issues" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /read-log.js: -------------------------------------------------------------------------------- 1 | var readline = require("readline"); 2 | 3 | function proc(line) { 4 | var m = line.match(/@@CT<<(.+?)>>CT@@/); 5 | if (!m) return ""; 6 | var data = JSON.parse(m[1]); 7 | 8 | var url = data.shift(); 9 | 10 | if (url.match(/^moz-extension/)) url = ""; 11 | 12 | var file = data.shift(); 13 | var line = data.shift(); 14 | 15 | var prefix = file.split("src/")[1].split(".")[0] + ":" + line + " "; 16 | 17 | var s = data 18 | .map(function (x) { 19 | return JSON.stringify(x); 20 | }) 21 | .join(" "); 22 | 23 | if (url) s += " " + url; 24 | 25 | console.log(prefix + s); 26 | } 27 | 28 | readline 29 | .createInterface({ 30 | input: process.stdin, 31 | output: process.stdout, 32 | terminal: false, 33 | }) 34 | .on("line", proc); 35 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## References 2 | 3 | - JS API differences: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#javascript_apis 4 | - Polyfill for Chrome: https://www.npmjs.com/package/webextension-polyfill 5 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | require("./background/index").main(); 3 | })(); 4 | -------------------------------------------------------------------------------- /src/background/clipboard.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var dom = require("../lib/dom"), 4 | util = require("../lib/util"); 5 | 6 | M.copyRich = function (text) { 7 | console.log(util.timeStart("copyRich")); 8 | 9 | var t = document.createElement("div"); 10 | document.body.appendChild(t); 11 | 12 | t.contentEditable = true; 13 | t.innerHTML = text; 14 | 15 | dom.select(t); 16 | document.execCommand("copy"); 17 | document.body.removeChild(t); 18 | 19 | console.log(util.timeEnd("copyRich")); 20 | }; 21 | 22 | M.copyText = function (text) { 23 | console.log(util.timeStart("copyText")); 24 | 25 | var t = document.createElement("textarea"); 26 | document.body.appendChild(t); 27 | 28 | t.value = text; 29 | t.focus(); 30 | t.select(); 31 | 32 | document.execCommand("copy"); 33 | document.body.removeChild(t); 34 | 35 | console.log(util.timeEnd("copyText")); 36 | }; 37 | 38 | M.content = function () { 39 | var cc = "", 40 | pasted = false; 41 | 42 | var pasteHandler = function (evt) { 43 | if (pasted) return; 44 | pasted = true; 45 | console.log("paste handler toggled"); 46 | cc = evt.clipboardData.getData("text/html"); 47 | evt.stopPropagation(); 48 | evt.preventDefault(); 49 | }; 50 | 51 | document.addEventListener("paste", pasteHandler, true); 52 | console.log("exec paste"); 53 | document.execCommand("paste"); 54 | document.removeEventListener("paste", pasteHandler); 55 | return cc; 56 | }; 57 | -------------------------------------------------------------------------------- /src/background/commands.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var message = require("../lib/message"), 4 | preferences = require("../lib/preferences"), 5 | util = require("../lib/util"), 6 | menu = require("./menu"), 7 | copy = require("./copy"), 8 | helpers = require("./helpers"); 9 | 10 | function findTableCommand(direction, sender) { 11 | console.log("findTableCommand", direction, sender); 12 | 13 | if (!sender) { 14 | return helpers.findTable(direction); 15 | } 16 | 17 | message.frame("tableIndexFromContextMenu", sender).then(function (res) { 18 | if (res.data !== null) { 19 | helpers.findTable(direction, { 20 | tabId: sender.tabId, 21 | frameId: sender.frameId, 22 | index: res.data, 23 | }); 24 | } else { 25 | helpers.findTable(direction); 26 | } 27 | }); 28 | } 29 | 30 | function copyCommand(format, sender) { 31 | console.log(util.timeStart("copyCommand")); 32 | 33 | console.log("copyCommand", format, sender); 34 | 35 | var msg = { 36 | name: "beginCopy", 37 | broadcast: !sender, 38 | options: copy.options(format), 39 | }; 40 | 41 | var ok = true; 42 | 43 | function doCopy(r) { 44 | if (r.data) { 45 | ok = copy.exec(format, r.data); 46 | return true; 47 | } 48 | } 49 | 50 | if (sender) { 51 | message.frame(msg, sender).then(function (r) { 52 | doCopy(r); 53 | message.frame(ok ? "endCopy" : "endCopyFailed", sender); 54 | console.log(util.timeEnd("copyCommand")); 55 | }); 56 | } else { 57 | message.allFrames(msg).then(function (res) { 58 | res.some(doCopy); 59 | message.allFrames(ok ? "endCopy" : "endCopyFailed"); 60 | console.log(util.timeEnd("copyCommand")); 61 | }); 62 | } 63 | } 64 | 65 | function captureCommand(mode) { 66 | if (mode === "zzz") { 67 | mode = ""; 68 | } 69 | 70 | var m = preferences.val("_captureMode"); 71 | if (m === mode) { 72 | mode = ""; 73 | } 74 | 75 | preferences.set("_captureMode", mode).then(function () { 76 | helpers.updateUI(); 77 | message.allFrames("preferencesUpdated"); 78 | }); 79 | } 80 | 81 | function selectCommand(mode, sender) { 82 | if (sender) { 83 | message.frame({ name: "selectFromContextMenu", mode: mode }, sender); 84 | } 85 | } 86 | 87 | M.exec = function (cmd, sender) { 88 | console.log("GOT COMMAND", cmd, sender); 89 | 90 | if (sender && typeof sender.tabId === "undefined") { 91 | sender = null; // this comes from the popup 92 | } 93 | 94 | if (cmd === "copy") { 95 | preferences.copyFormats().forEach(function (f) { 96 | if (f.default) { 97 | cmd = "copy_" + f.id; 98 | } 99 | }); 100 | } 101 | 102 | var m; 103 | 104 | m = cmd.match(/^copy_(\w+)/); 105 | if (m) { 106 | return copyCommand(m[1], sender); 107 | } 108 | 109 | m = cmd.match(/^capture_(\w+)/); 110 | if (m) { 111 | return captureCommand(m[1], sender); 112 | } 113 | 114 | m = cmd.match(/^select_(\w+)/); 115 | if (m) { 116 | return selectCommand(m[1], sender); 117 | } 118 | 119 | switch (cmd) { 120 | case "find_next": 121 | return findTableCommand(+1, sender); 122 | case "find_previous": 123 | return findTableCommand(-1, sender); 124 | case "open_options": 125 | return util.callBrowser("runtime.openOptionsPage"); 126 | } 127 | }; 128 | -------------------------------------------------------------------------------- /src/background/copy.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var paste = require("./paste"), 4 | dom = require("../lib/dom"), 5 | matrix = require("../lib/matrix"), 6 | util = require("../lib/util"), 7 | clipboard = require("./clipboard"); 8 | 9 | function trimTextMatrix(mat) { 10 | mat = matrix.map(mat, function (row, cell) { 11 | return util.strip(util.nobr(cell)); 12 | }); 13 | return matrix.trim(mat, Boolean); 14 | } 15 | 16 | function asTabs(mat) { 17 | return trimTextMatrix(mat) 18 | .map(function (row) { 19 | return row.join("\t"); 20 | }) 21 | .join("\n"); 22 | } 23 | 24 | function asCSV(mat) { 25 | return trimTextMatrix(mat) 26 | .map(function (row) { 27 | return row 28 | .map(function (cell) { 29 | if (cell.match(/^\w+$/) || cell.match(/^-?[0-9]+(\.[0-9]*)?$/)) 30 | return cell; 31 | return '"' + cell.replace(/"/g, '""') + '"'; 32 | }) 33 | .join(","); 34 | }) 35 | .join("\n"); 36 | } 37 | 38 | function asTextile(mat) { 39 | var rows = mat.map(function (row) { 40 | var cells = row 41 | .filter(function (node) { 42 | return node.td; 43 | }) 44 | .map(function (node) { 45 | var t = "", 46 | s = ""; 47 | 48 | t = dom.textContent(node.td); 49 | 50 | if (node.colSpan) s += "\\" + (node.colSpan + 1); 51 | if (node.rowSpan) s += "/" + (node.rowSpan + 1); 52 | if (s) s += "."; 53 | 54 | return "|" + s + " " + t.replace("|", "|"); 55 | }); 56 | 57 | return cells.join(" ") + " |"; 58 | }); 59 | 60 | return rows.join("\n"); 61 | } 62 | 63 | M.formats = {}; 64 | 65 | M.formats.richHTMLCSS = { 66 | opts: { 67 | method: "clipboard", 68 | withSelection: true, 69 | keepStyles: true, 70 | keepHidden: false, 71 | }, 72 | exec: function (t) { 73 | clipboard.copyRich(t.html()); 74 | }, 75 | }; 76 | 77 | M.formats.richHTML = { 78 | opts: { 79 | method: "clipboard", 80 | withSelection: true, 81 | keepStyles: false, 82 | keepHidden: false, 83 | }, 84 | exec: function (t) { 85 | clipboard.copyRich(t.html()); 86 | }, 87 | }; 88 | 89 | M.formats.textTabs = { 90 | opts: { 91 | method: "clipboard", 92 | withSelection: true, 93 | keepStyles: false, 94 | keepHidden: false, 95 | }, 96 | exec: function (t) { 97 | clipboard.copyText(asTabs(t.textMatrix())); 98 | }, 99 | }; 100 | 101 | M.formats.textTabsSwap = { 102 | opts: { 103 | method: "clipboard", 104 | withSelection: true, 105 | keepStyles: false, 106 | keepHidden: false, 107 | }, 108 | exec: function (t) { 109 | clipboard.copyText(asTabs(matrix.transpose(t.textMatrix()))); 110 | }, 111 | }; 112 | 113 | M.formats.textCSV = { 114 | opts: { 115 | method: "clipboard", 116 | withSelection: true, 117 | keepStyles: false, 118 | keepHidden: false, 119 | }, 120 | exec: function (t) { 121 | clipboard.copyText(asCSV(t.textMatrix())); 122 | }, 123 | }; 124 | 125 | M.formats.textCSVSwap = { 126 | opts: { 127 | method: "clipboard", 128 | withSelection: true, 129 | keepStyles: false, 130 | keepHidden: false, 131 | }, 132 | exec: function (t) { 133 | clipboard.copyText(asCSV(matrix.transpose(t.textMatrix()))); 134 | }, 135 | }; 136 | 137 | M.formats.textHTML = { 138 | opts: { 139 | method: "clipboard", 140 | withSelection: true, 141 | keepStyles: false, 142 | keepHidden: true, 143 | }, 144 | exec: function (t) { 145 | clipboard.copyText(util.reduceWhitespace(t.html())); 146 | }, 147 | }; 148 | 149 | M.formats.textHTMLCSS = { 150 | opts: { 151 | method: "clipboard", 152 | withSelection: true, 153 | keepStyles: true, 154 | keepHidden: true, 155 | }, 156 | exec: function (t) { 157 | clipboard.copyText(util.reduceWhitespace(t.html())); 158 | }, 159 | }; 160 | 161 | M.formats.textTextile = { 162 | opts: { 163 | method: "clipboard", 164 | withSelection: true, 165 | keepStyles: false, 166 | keepHidden: false, 167 | }, 168 | exec: function (t) { 169 | clipboard.copyText(asTextile(t.nodeMatrix())); 170 | }, 171 | }; 172 | 173 | // 174 | 175 | M.options = function (format) { 176 | return M.formats[format].opts; 177 | }; 178 | 179 | M.exec = function (format, data) { 180 | var ok = false, 181 | t = new paste.table(), 182 | fmt = M.formats[format]; 183 | 184 | if (t.init(data, fmt.opts)) { 185 | fmt.exec(t); 186 | ok = true; 187 | } 188 | 189 | t.destroy(); 190 | return ok; 191 | }; 192 | -------------------------------------------------------------------------------- /src/background/helpers.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var message = require("../lib/message"), 4 | preferences = require("../lib/preferences"), 5 | util = require("../lib/util"), 6 | menu = require("./menu"); 7 | var badgeColor = "#1e88ff"; 8 | 9 | M.updateUI = function () { 10 | preferences.load().then(function () { 11 | menu.create(); 12 | M.updateBadge(); 13 | }); 14 | }; 15 | 16 | M.setBadge = function (s) { 17 | util.callBrowser("browserAction.setBadgeText", { text: s }); 18 | util.callBrowser("browserAction.setBadgeBackgroundColor", { 19 | color: badgeColor, 20 | }); 21 | }; 22 | 23 | M.updateBadge = function () { 24 | var mode = preferences.val("_captureMode"); 25 | 26 | console.log("updateBadge mode=" + mode); 27 | 28 | switch (mode) { 29 | case "column": 30 | return M.setBadge("C"); 31 | case "row": 32 | return M.setBadge("R"); 33 | case "cell": 34 | return M.setBadge("E"); 35 | case "table": 36 | return M.setBadge("T"); 37 | default: 38 | M.setBadge(""); 39 | } 40 | }; 41 | 42 | M.enumTables = function () { 43 | return message.allFrames("enumTables").then(function (res) { 44 | var all = []; 45 | 46 | res.forEach(function (r) { 47 | all = all.concat( 48 | (r.data || []).map(function (t) { 49 | t.frame = { 50 | tabId: r.receiver.tabId, 51 | frameId: r.receiver.frameId, 52 | }; 53 | return t; 54 | }) 55 | ); 56 | }); 57 | 58 | return all.sort(function (a, b) { 59 | return a.frame.frameId - b.frame.frameId || a.index - b.index; 60 | }); 61 | }); 62 | }; 63 | 64 | M.findTable = function (direction, start) { 65 | M.enumTables().then(function (allTables) { 66 | if (!allTables.length) { 67 | return; 68 | } 69 | 70 | var curr = -1; 71 | 72 | allTables.some(function (t, n) { 73 | if ( 74 | start && 75 | t.frame.frameId == start.frameId && 76 | t.frame.tabId === start.tabId && 77 | t.index === start.index 78 | ) { 79 | curr = n; 80 | return true; 81 | } 82 | if (!start && t.selected) { 83 | curr = n; 84 | return true; 85 | } 86 | }); 87 | 88 | if (direction === +1) { 89 | if (curr === -1 || curr === allTables.length - 1) curr = 0; 90 | else curr += 1; 91 | } else { 92 | if (curr === -1 || curr === 0) curr = allTables.length - 1; 93 | else curr--; 94 | } 95 | 96 | var t = allTables[curr]; 97 | message.frame({ name: "selectTableByIndex", index: t.index }, t.frame); 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /src/background/index.js: -------------------------------------------------------------------------------- 1 | /// Background script main 2 | 3 | var M = (module.exports = {}); 4 | 5 | var message = require("../lib/message"), 6 | util = require("../lib/util"), 7 | preferences = require("../lib/preferences"), 8 | menu = require("./menu"), 9 | commands = require("./commands"), 10 | copy = require("./copy"), 11 | helpers = require("./helpers"); 12 | 13 | var messageListeners = { 14 | dropAllSelections: function (msg) { 15 | message.allFrames("dropSelection"); 16 | }, 17 | 18 | dropOtherSelections: function (msg) { 19 | message.enumFrames("active").then(function (frames) { 20 | frames.forEach(function (frame) { 21 | if (frame.frameId !== msg.sender.frameId) { 22 | message.frame("dropSelection", frame); 23 | } 24 | }); 25 | }); 26 | }, 27 | 28 | contextMenu: function (msg) { 29 | helpers.enumTables().then(function (ts) { 30 | menu.enable( 31 | ["select_row", "select_column", "select_table"], 32 | msg.selectable 33 | ); 34 | menu.enable(["copy"], msg.selectable); 35 | menu.enable(["find_previous", "find_next"], ts.length > 0); 36 | }); 37 | }, 38 | 39 | genericCopy: function (msg) { 40 | commands.exec("copy", msg.sender); 41 | }, 42 | 43 | preferencesUpdated: function (msg) { 44 | helpers.updateUI(); 45 | message.broadcast("preferencesUpdated"); 46 | }, 47 | 48 | command: function (msg) { 49 | console.log("messageListeners.command", msg.sender); 50 | commands.exec(msg.command, msg.sender); 51 | }, 52 | }; 53 | 54 | function init() { 55 | menu.create(); 56 | message.listen(messageListeners); 57 | 58 | util.callBrowser("contextMenus.onClicked.addListener", function (info, tab) { 59 | commands.exec(info.menuItemId, { tabId: tab.id, frameId: info.frameId }); 60 | }); 61 | 62 | util.callBrowser("commands.onCommand.addListener", function (cmd) { 63 | commands.exec(cmd, null); 64 | }); 65 | 66 | helpers.updateUI(); 67 | } 68 | 69 | M.main = function () { 70 | preferences.load().then(init); 71 | }; 72 | -------------------------------------------------------------------------------- /src/background/menu.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var preferences = require("../lib/preferences"), 4 | util = require("../lib/util"); 5 | 6 | var mainMenu = { 7 | id: "root", 8 | title: "Table...", 9 | children: [ 10 | { 11 | id: "select_row", 12 | title: "Select Row", 13 | }, 14 | { 15 | id: "select_column", 16 | title: "Select Column", 17 | }, 18 | { 19 | id: "select_table", 20 | title: "Select Table", 21 | }, 22 | "---", 23 | { 24 | id: "find_previous", 25 | title: "Previous Table", 26 | }, 27 | { 28 | id: "find_next", 29 | title: "Next Table", 30 | }, 31 | "---", 32 | { 33 | id: "copy", 34 | title: "Copy", 35 | }, 36 | { 37 | id: "copyAs", 38 | title: "Copy...", 39 | }, 40 | ], 41 | }; 42 | 43 | var uid = 0; 44 | 45 | function createMenu(menu, parent) { 46 | var desc = { 47 | enabled: true, 48 | contexts: ["page", "selection", "link", "editable"], 49 | }; 50 | 51 | if (parent) { 52 | desc.parentId = parent; 53 | } 54 | 55 | if (menu === "---") { 56 | desc.id = "uid" + ++uid; 57 | desc.type = "separator"; 58 | } else { 59 | desc.id = menu.id; 60 | desc.title = menu.title; 61 | } 62 | 63 | var sub = menu.children; 64 | 65 | if (menu.id === "copyAs") { 66 | var cf = preferences.copyFormats().filter(function (f) { 67 | return f.enabled; 68 | }); 69 | 70 | if (!cf.length) { 71 | return; 72 | } 73 | 74 | sub = cf.map(function (f) { 75 | return { 76 | id: "copy_" + f.id, 77 | title: f.name, 78 | }; 79 | }); 80 | } 81 | 82 | var mobj = util.callBrowser("contextMenus.create", desc); 83 | 84 | if (sub) { 85 | sub.forEach(function (subMenu) { 86 | createMenu(subMenu, mobj); 87 | }); 88 | } 89 | 90 | return mobj; 91 | } 92 | 93 | M.create = function () { 94 | util.callBrowserAsync("contextMenus.removeAll").then(function () { 95 | createMenu(mainMenu); 96 | }); 97 | }; 98 | 99 | M.enable = function (ids, enabled) { 100 | ids.forEach(function (id) { 101 | util.callBrowser("contextMenus.update", id, { enabled: enabled }); 102 | if (id === "copy") { 103 | var cf = preferences.copyFormats().filter(function (f) { 104 | return f.enabled; 105 | }); 106 | cf.forEach(function (f) { 107 | util.callBrowser("contextMenus.update", "copy_" + f.id, { 108 | enabled: enabled, 109 | }); 110 | }); 111 | } 112 | }); 113 | }; 114 | -------------------------------------------------------------------------------- /src/background/paste.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var dom = require("../lib/dom"), 4 | matrix = require("../lib/matrix"), 5 | util = require("../lib/util"), 6 | cell = require("../lib/cell"), 7 | css = require("../lib/css"), 8 | clipboard = require("./clipboard"); 9 | 10 | function toMatrix(tbl) { 11 | console.log(util.timeStart("toMatrix")); 12 | 13 | var tds = {}, 14 | rows = {}, 15 | cols = {}; 16 | 17 | dom.cells(tbl).forEach(function (td) { 18 | var bounds = dom.bounds(td); 19 | var c = bounds.x, 20 | r = bounds.y; 21 | cols[c] = rows[r] = 1; 22 | tds[r + "/" + c] = td; 23 | }); 24 | 25 | rows = Object.keys(rows).sort(util.numeric); 26 | cols = Object.keys(cols).sort(util.numeric); 27 | 28 | var mat = rows.map(function (r) { 29 | return cols.map(function (c) { 30 | var td = tds[r + "/" + c]; 31 | return td ? { td: td } : {}; 32 | }); 33 | }); 34 | 35 | matrix.each(mat, function (row, node, ri, ni) { 36 | if (!node.td) return; 37 | 38 | var rs = parseInt(dom.attr(node.td, "rowSpan")) || 1; 39 | var cs = parseInt(dom.attr(node.td, "colSpan")) || 1; 40 | 41 | for (var i = 1; i < cs; i++) { 42 | if (row[ni + i] && !row[ni + i].td) row[ni + i].colRef = node; 43 | } 44 | 45 | for (var i = 1; i < rs; i++) { 46 | if (mat[ri + i] && mat[ri + i][ni] && !mat[ri + i][ni].td) 47 | mat[ri + i][ni].rowRef = node; 48 | } 49 | }); 50 | 51 | console.log(util.timeEnd("toMatrix")); 52 | 53 | return mat; 54 | } 55 | 56 | function computeSpans(mat) { 57 | matrix.each(mat, function (_, node) { 58 | if (node.colRef) node.colRef.colSpan = (node.colRef.colSpan || 0) + 1; 59 | if (node.rowRef) node.rowRef.rowSpan = (node.rowRef.rowSpan || 0) + 1; 60 | }); 61 | } 62 | 63 | function trim(tbl) { 64 | console.log(util.timeStart("trim.filter")); 65 | 66 | var mat = matrix.trim(toMatrix(tbl), function (node) { 67 | return cell.selected(node.td); 68 | }); 69 | 70 | console.log(util.timeEnd("trim.filter")); 71 | 72 | console.log(util.timeStart("trim.remove")); 73 | 74 | var tds = []; 75 | 76 | matrix.each(mat, function (row, node) { 77 | if (node.td) { 78 | tds.push(node.td); 79 | } 80 | }); 81 | 82 | var junk = []; 83 | 84 | dom.cells(tbl).forEach(function (td) { 85 | if (tds.indexOf(td) < 0) junk.push(td); 86 | else if (!cell.selected(td)) td.innerHTML = ""; 87 | }); 88 | 89 | dom.remove(junk); 90 | junk = []; 91 | 92 | dom.find("tr", tbl).forEach(function (tr) { 93 | if (dom.find("td, th", tr).length === 0) { 94 | junk.push(tr); 95 | } 96 | }); 97 | 98 | dom.remove(junk); 99 | 100 | console.log(util.timeEnd("trim.remove")); 101 | 102 | computeSpans(mat); 103 | 104 | matrix.each(mat, function (_, node) { 105 | if (node.td) { 106 | dom.attr(node.td, "rowSpan", node.rowSpan ? node.rowSpan + 1 : null); 107 | dom.attr(node.td, "colSpan", node.colSpan ? node.colSpan + 1 : null); 108 | } 109 | }); 110 | } 111 | 112 | function fixRelativeLinks(where, fixCssUrls) { 113 | var aa = where.ownerDocument.createElement("A"); 114 | 115 | function fixUrl(url) { 116 | // since we've set BASE url, this works 117 | aa.href = url; 118 | return aa.href; 119 | } 120 | 121 | function fixTags(tags, attrs) { 122 | dom.find(tags, where).forEach(function (t) { 123 | attrs.forEach(function (attr) { 124 | var v = dom.attr(t, attr); 125 | if (v) { 126 | dom.attr(t, attr, fixUrl(v)); 127 | } 128 | }); 129 | }); 130 | } 131 | 132 | fixTags("A, AREA, LINK", ["href"]); 133 | fixTags("IMG, INPUT, SCRIPT", ["src", "longdesc", "usemap"]); 134 | fixTags("FORM", ["action"]); 135 | fixTags("Q, BLOCKQUOTE, INS, DEL", ["cite"]); 136 | fixTags("OBJECT", ["classid", "codebase", "data", "usemap"]); 137 | 138 | if (fixCssUrls) { 139 | dom.find("*", where).forEach(function (el) { 140 | var style = dom.attr(el, "style"); 141 | 142 | if (!style || style.toLowerCase().indexOf("url") < 0) return; 143 | 144 | var fixStyle = style.replace( 145 | /(\burl\s*\()([^()]+)/gi, 146 | function (_, pfx, url) { 147 | url = util.strip(url); 148 | if (url[0] === '"' || url[0] === "'") { 149 | return pfx + url[0] + fixUrl(url.slice(1, -1)) + url[0]; 150 | } 151 | return pfx + fixUrl(url); 152 | } 153 | ); 154 | 155 | if (fixStyle !== style) dom.attr(el, "style", fixStyle); 156 | }); 157 | } 158 | } 159 | 160 | function removeHiddenElements(node) { 161 | var hidden = []; 162 | 163 | dom.find("*", node).forEach(function (el) { 164 | if (!dom.visible(el)) { 165 | hidden.push(el); 166 | } 167 | }); 168 | 169 | if (hidden.length) { 170 | console.log("removeHidden: " + hidden.length); 171 | dom.remove(hidden); 172 | } 173 | } 174 | tcc = function (text) { 175 | console.log(util.timeStart("textCopy")); 176 | 177 | var t = document.createElement("textarea"); 178 | document.body.appendChild(t); 179 | 180 | t.value = text; 181 | t.focus(); 182 | t.select(); 183 | 184 | document.execCommand("copy"); 185 | document.body.removeChild(t); 186 | 187 | console.log(util.timeEnd("textCopy")); 188 | }; 189 | 190 | M.table = function () {}; 191 | 192 | M.table.prototype.init = function (data, options) { 193 | console.log(util.timeStart("paste.init")); 194 | 195 | this.frame = document.createElement("IFRAME"); 196 | this.frame.setAttribute("sandbox", "allow-same-origin"); 197 | document.body.appendChild(this.frame); 198 | 199 | this.doc = this.frame.contentDocument; 200 | this.body = this.doc.body; 201 | 202 | var base = this.doc.createElement("BASE"); 203 | dom.attr(base, "href", data.url); 204 | this.body.appendChild(base); 205 | 206 | // some cells (e.g. containing an image) could become width=0 207 | // after paste, breaking toMatrix calculations 208 | var css = this.doc.createElement("STYLE"); 209 | css.type = "text/css"; 210 | css.innerHTML = "td { min-width: 1px; }"; 211 | this.body.appendChild(css); 212 | 213 | this.div = this.doc.createElement("DIV"); 214 | this.div.contentEditable = true; 215 | this.body.appendChild(this.div); 216 | 217 | var ok = this.initTable(data, options); 218 | 219 | console.log("paste.init=" + ok + " method=" + options.method); 220 | console.log(util.timeEnd("paste.init")); 221 | 222 | return ok; 223 | }; 224 | 225 | M.table.prototype.initTable = function (data, options) { 226 | console.log(util.timeStart("paste.insertTable")); 227 | 228 | if (options.method === "clipboard") { 229 | this.div.focus(); 230 | 231 | // NB: just pasting the clipboard via `this.doc.execCommand('paste')` 232 | // is very slow for some reason. Instead, intercept paste 233 | // to obtain the clipboard and insert it via innerHTML which is waaay faster 234 | 235 | this.div.innerHTML = clipboard.content(); 236 | 237 | // destroy the clipboard to avoid pasting of intermediate results 238 | // this doesn't really fix that because they can hit paste before 239 | // .content() finishes, but still... 240 | clipboard.copyText(""); 241 | } 242 | 243 | if (options.method === "transfer") { 244 | this.div.innerHTML = data.html; 245 | } 246 | 247 | console.log(util.timeEnd("paste.insertTable")); 248 | 249 | this.table = dom.findOne("table", this.div); 250 | 251 | if (!this.table || dom.tag(this.table) !== "TABLE") return false; 252 | 253 | if (data.hasSelection) { 254 | console.log(util.timeStart("paste.trim")); 255 | trim(this.table); 256 | console.log(util.timeEnd("paste.trim")); 257 | } 258 | 259 | if (options.method === "transfer" && options.keepStyles) { 260 | console.log(util.timeStart("paste.restoreStyles")); 261 | 262 | dom.findSelf("*", this.table).forEach(function (el) { 263 | var uid = dom.attr(el, "data-copytables-uid"); 264 | if (uid && el.style) { 265 | dom.removeAttr(el, "style"); 266 | el.style.cssText = css.compute(css.read(el), data.css[uid] || {}); 267 | } 268 | dom.removeAttr(el, "data-copytables-uid"); 269 | }); 270 | 271 | console.log(util.timeEnd("paste.restoreStyles")); 272 | } 273 | 274 | if (options.method === "transfer" && !options.keepHidden) { 275 | console.log(util.timeStart("paste.removeHidden")); 276 | removeHiddenElements(this.div); 277 | console.log(util.timeEnd("paste.removeHidden")); 278 | } 279 | 280 | fixRelativeLinks(this.div, options.keepStyles); 281 | dom.cells(this.table).forEach(cell.reset); 282 | 283 | if (!options.keepStyles) { 284 | dom.findSelf("*", this.table).forEach(function (el) { 285 | dom.removeAttr(el, "style"); 286 | dom.removeAttr(el, "class"); 287 | }); 288 | } 289 | 290 | return true; 291 | }; 292 | 293 | M.table.prototype.html = function () { 294 | return this.table.outerHTML; 295 | }; 296 | 297 | M.table.prototype.nodeMatrix = function () { 298 | var mat = toMatrix(this.table); 299 | computeSpans(mat); 300 | return mat; 301 | }; 302 | 303 | M.table.prototype.textMatrix = function () { 304 | return matrix.map(toMatrix(this.table), function (_, node) { 305 | return dom.textContent(node.td); 306 | }); 307 | }; 308 | 309 | M.table.prototype.destroy = function () { 310 | if (this.frame) document.body.removeChild(this.frame); 311 | }; 312 | -------------------------------------------------------------------------------- /src/colors.sass: -------------------------------------------------------------------------------- 1 | $border: #f0f0f0 2 | $body: #777 3 | $link: #999 4 | $accent: #1e88ff 5 | $hot: #f0a340 6 | $button: #eaeaea 7 | $lite: #f5f5f5 8 | $cell-bg: #b5d5ff 9 | -------------------------------------------------------------------------------- /src/content.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | require("./content/index").main(); 3 | })(); 4 | -------------------------------------------------------------------------------- /src/content.sass: -------------------------------------------------------------------------------- 1 | @import "colors" 2 | 3 | $i: !important 4 | $no-trans: all 0s 0s $i 5 | 6 | body[data-copytables-wait], 7 | body[data-copytables-wait] * 8 | cursor: wait $i 9 | 10 | [data-copytables-selected]:not([data-copytables-locked]) 11 | background-color: $cell-bg $i 12 | transition: $no-trans 13 | 14 | [data-copytables-marked]:not([data-copytables-locked]) 15 | background-color: $cell-bg $i 16 | transition: $no-trans 17 | 18 | $infobox-border: 9px 19 | $infobox-color: $accent 20 | 21 | #__copytables_infobox__ 22 | 23 | position: fixed 24 | font-family: Arial, sans-serif 25 | font-size: 14px 26 | 27 | overflow: hidden 28 | white-space: nowrap 29 | z-index: 65535 30 | 31 | background-color: transparentize(darken($infobox-color, 1), 0.03) 32 | background-image: url(inline-image('src/ico16.png')) 33 | background-position: 8px center 34 | background-repeat: no-repeat 35 | 36 | padding: 0 0 0 32px 37 | transition: opacity 0.2s ease 38 | 39 | &.hidden 40 | opacity: 0 41 | 42 | &[data-position="0"] 43 | left: 0 44 | top: 0 45 | border-bottom-right-radius: $infobox-border 46 | &[data-position="1"] 47 | right: 0 48 | top: 0 49 | border-bottom-left-radius: $infobox-border 50 | &[data-position="2"] 51 | right: 0 52 | bottom: 0 53 | border-top-left-radius: $infobox-border 54 | &[data-position="3"] 55 | left: 0 56 | bottom: 0 57 | border-top-right-radius: $infobox-border 58 | 59 | b 60 | display: inline-block 61 | margin: 0 1px 62 | padding: 12px 12px 12px 0 63 | font-weight: 200 64 | color: lighten($infobox-color, 35) 65 | 66 | &:first-child 67 | border-left: none 68 | 69 | i 70 | font-style: normal 71 | padding-left: 6px 72 | font-weight: 700 73 | color: lighten($infobox-color, 45) 74 | 75 | span 76 | background-color: transparentize(darken($infobox-color, 5), 0.03) 77 | color: lighten($infobox-color, 45) 78 | padding: 12px 79 | cursor: pointer 80 | -------------------------------------------------------------------------------- /src/content/capture.js: -------------------------------------------------------------------------------- 1 | /// Handle selecting cells with mouse 2 | 3 | var M = (module.exports = {}); 4 | 5 | var dom = require("../lib/dom"), 6 | cell = require("../lib/cell"), 7 | event = require("../lib/event"), 8 | message = require("../lib/message"), 9 | preferences = require("../lib/preferences"), 10 | util = require("../lib/util"), 11 | infobox = require("./infobox"), 12 | table = require("./table"), 13 | scroller = require("./scroller"); 14 | 15 | M.Capture = function () { 16 | this.anchorPoint = null; 17 | this.table = null; 18 | }; 19 | 20 | M.Capture.prototype.markRect = function (evt) { 21 | var cx = evt.clientX, 22 | cy = evt.clientY; 23 | 24 | var p = this.scroller.adjustPoint(this.anchorPoint); 25 | 26 | var rect = [ 27 | Math.min(cx, p.x), 28 | Math.min(cy, p.y), 29 | Math.max(cx, p.x), 30 | Math.max(cy, p.y), 31 | ]; 32 | 33 | var big = 10e6; 34 | 35 | if (this.mode === "column" || this.mode === "table") { 36 | rect[1] = -big; 37 | rect[3] = +big; 38 | } 39 | 40 | if (this.mode === "row" || this.mode === "table") { 41 | rect[0] = -big; 42 | rect[2] = +big; 43 | } 44 | 45 | return rect; 46 | }; 47 | 48 | M.Capture.prototype.setCaptured = function (rect) { 49 | dom.cells(this.table).forEach(function (td) { 50 | cell.unmark(td); 51 | if (util.intersect(dom.bounds(td).rect, rect)) { 52 | cell.mark(td); 53 | } 54 | }); 55 | }; 56 | 57 | M.Capture.prototype.setLocked = function (rect, canSelect) { 58 | dom.cells(this.table).forEach(function (td) { 59 | cell.unlock(td); 60 | if (util.intersect(dom.bounds(td).rect, rect)) { 61 | if (!canSelect) { 62 | cell.unselect(td); 63 | cell.lock(td); 64 | } 65 | } 66 | }); 67 | }; 68 | 69 | M.Capture.prototype.selection = function () { 70 | var self = this, 71 | tds = cell.findSelected(self.table); 72 | 73 | if (!self.selectedCells) { 74 | return [true, (self.selectedCells = tds)]; 75 | } 76 | 77 | if (tds.length !== self.selectedCells.length) { 78 | return [true, (self.selectedCells = tds)]; 79 | } 80 | 81 | var eq = true; 82 | 83 | tds.forEach(function (td, i) { 84 | eq = eq && td === self.selectedCells[i]; 85 | }); 86 | 87 | if (!eq) { 88 | return [true, (self.selectedCells = tds)]; 89 | } 90 | 91 | return [false, self.selectedCells]; 92 | }; 93 | 94 | M.Capture.prototype.start = function (evt, mode, extend) { 95 | var t = table.locate(evt.target); 96 | 97 | this.table = t.table; 98 | this.scroller = new scroller.Scroller(t.td); 99 | this.mode = mode; 100 | this.extend = extend; 101 | 102 | if (!this.anchorPoint) extend = false; 103 | 104 | if (!extend) { 105 | this.anchorPoint = { 106 | x: dom.bounds(t.td).x + 1, 107 | y: dom.bounds(t.td).y + 1, 108 | }; 109 | this.setLocked(this.markRect(evt), !cell.selected(t.td)); 110 | } 111 | 112 | var self = this; 113 | 114 | var tracker = function (type, evt) { 115 | if (type === "move") self.scroller.reset(); 116 | self.scroller.scroll(evt); 117 | self.setCaptured(self.markRect(evt)); 118 | infobox.update(self.table); 119 | 120 | if (type === "up") { 121 | self.onDone(self.table); 122 | } 123 | }; 124 | 125 | event.trackMouse(evt, tracker); 126 | }; 127 | 128 | M.Capture.prototype.stop = function () { 129 | dom.find("td, th").forEach(function (td) { 130 | cell.unmark(td); 131 | cell.unlock(td); 132 | }); 133 | }; 134 | -------------------------------------------------------------------------------- /src/content/index.js: -------------------------------------------------------------------------------- 1 | /// Content script main 2 | 3 | var M = (module.exports = {}); 4 | 5 | var preferences = require("../lib/preferences"), 6 | keyboard = require("../lib/keyboard"), 7 | event = require("../lib/event"), 8 | message = require("../lib/message"), 9 | dom = require("../lib/dom"), 10 | util = require("../lib/util"), 11 | capture = require("./capture"), 12 | infobox = require("./infobox"), 13 | selection = require("./selection"), 14 | table = require("./table"), 15 | loader = require("./loader"); 16 | 17 | var mouseButton = 0, 18 | currentCapture = null; 19 | 20 | function parseEvent(evt) { 21 | var key = keyboard.key(evt), 22 | emod = preferences.int("modifier.extend"), 23 | mods = key.modifiers.code, 24 | kmods = mods & ~emod; 25 | 26 | if ( 27 | preferences.val("capture.enabled") && 28 | preferences.val("_captureMode") && 29 | !kmods 30 | ) { 31 | console.log("got capture", preferences.val("_captureMode"), mods & emod); 32 | return [preferences.val("_captureMode"), mods & emod]; 33 | } 34 | 35 | if (!key.scan.code && kmods) { 36 | var cap = util.first(preferences.captureModes(), function (m) { 37 | return kmods === preferences.int("modifier." + m.id); 38 | }); 39 | 40 | if (cap) { 41 | console.log("got modifier", cap.id, mods & emod); 42 | return [cap.id, mods & emod]; 43 | } 44 | } 45 | } 46 | 47 | function destroyCapture() { 48 | if (currentCapture) { 49 | currentCapture.stop(); 50 | currentCapture = null; 51 | } 52 | } 53 | 54 | function captureDone(tbl) { 55 | table.selectCaptured(tbl); 56 | if (preferences.val("capture.reset")) { 57 | preferences.set("_captureMode", "").then(function () { 58 | message.background("preferencesUpdated"); 59 | }); 60 | } 61 | } 62 | 63 | function startCapture(evt, mode, extend) { 64 | var t = table.locate(evt.target); 65 | 66 | if (!t) { 67 | destroyCapture(); 68 | return false; 69 | } 70 | 71 | if (currentCapture && currentCapture.table !== t.table) { 72 | destroyCapture(); 73 | extend = false; 74 | } 75 | 76 | if (currentCapture) { 77 | currentCapture.stop(); 78 | } 79 | 80 | currentCapture = currentCapture || new capture.Capture(); 81 | 82 | currentCapture.onDone = captureDone; 83 | 84 | selection.start(evt.target); 85 | currentCapture.start(evt, mode, extend); 86 | } 87 | 88 | var copyLock = false, 89 | copyWaitTimeout = 300, 90 | copyWaitTimer = 0; 91 | 92 | function beginCopy(msg) { 93 | var tbl = selection.table(), 94 | hasSelection = true; 95 | 96 | if (!tbl) { 97 | if (msg.broadcast) return null; 98 | var el = event.lastTarget(), 99 | t = table.locate(el); 100 | if (!t) return null; 101 | tbl = t.table; 102 | hasSelection = false; 103 | } 104 | 105 | copyLock = true; 106 | var data = table.copy(tbl, msg.options, hasSelection); 107 | 108 | copyWaitTimer = setTimeout(function () { 109 | dom.attr(document.body, "data-copytables-wait", 1); 110 | }, copyWaitTimeout); 111 | 112 | return data; 113 | } 114 | 115 | function endCopy() { 116 | copyLock = false; 117 | clearTimeout(copyWaitTimer); 118 | dom.removeAttr(document.body, "data-copytables-wait"); 119 | } 120 | 121 | var eventListeners = { 122 | mousedownCapture: function (evt) { 123 | event.register(evt); 124 | 125 | if (evt.button !== mouseButton) { 126 | return; 127 | } 128 | 129 | var p = parseEvent(evt); 130 | console.log("parseEvent=", p); 131 | 132 | if (!p || !selection.selectable(evt.target)) { 133 | message.background("dropAllSelections"); 134 | return; 135 | } 136 | 137 | window.focus(); 138 | startCapture(evt, p[0], p[1]); 139 | }, 140 | 141 | copy: function (evt) { 142 | if (selection.active() && !copyLock) { 143 | console.log("COPY MINE"); 144 | message.background("genericCopy"); 145 | event.reset(evt); 146 | } else if (copyLock) { 147 | console.log("COPY LOCKED"); 148 | event.reset(evt); 149 | } else { 150 | console.log("COPY PASS"); 151 | } 152 | }, 153 | 154 | contextmenu: function (evt) { 155 | event.register(evt); 156 | 157 | message.background({ 158 | name: "contextMenu", 159 | selectable: selection.selectable(evt.target), 160 | selected: selection.active(), 161 | }); 162 | }, 163 | }; 164 | 165 | var messageListeners = { 166 | dropSelection: function () { 167 | if (selection.table()) { 168 | selection.drop(); 169 | } 170 | // hide infobox once a selection is dropped 171 | // this means users won't be able to click and copy from the infobox 172 | infobox.remove(); 173 | }, 174 | 175 | preferencesUpdated: preferences.load, 176 | 177 | enumTables: function (msg) { 178 | return table.enum(selection.table()); 179 | }, 180 | 181 | selectTableByIndex: function (msg) { 182 | var tbl = table.byIndex(msg.index); 183 | if (tbl) { 184 | selection.select(tbl, "table"); 185 | tbl.scrollIntoView(true); 186 | infobox.update(selection.table()); 187 | } 188 | }, 189 | 190 | selectFromContextMenu: function (msg) { 191 | var el = event.lastTarget(), 192 | t = table.locate(el); 193 | 194 | if (t) { 195 | selection.toggle(t.td, msg.mode); 196 | } else if (msg.mode === "table") { 197 | selection.toggle(dom.closest(el, "table"), "table"); 198 | } 199 | 200 | infobox.update(selection.table()); 201 | }, 202 | 203 | tableIndexFromContextMenu: function () { 204 | var el = event.lastTarget(), 205 | tbl = dom.closest(el, "table"); 206 | return tbl ? table.indexOf(tbl) : null; 207 | }, 208 | 209 | beginCopy: beginCopy, 210 | endCopy: endCopy, 211 | 212 | endCopyFailed: function () { 213 | if (copyLock) { 214 | // inform the user that copy/paste failed 215 | console.error("Sorry, CopyTables was unable to copy this table."); 216 | } 217 | endCopy(); 218 | }, 219 | }; 220 | 221 | function init() { 222 | event.listen(document, eventListeners); 223 | message.listen(messageListeners); 224 | } 225 | 226 | M.main = function () { 227 | loader.load().then(function () { 228 | if (!document.body) { 229 | console.log("no body", document.URL); 230 | return; 231 | } 232 | preferences.load().then(init); 233 | }); 234 | }; 235 | -------------------------------------------------------------------------------- /src/content/infobox.js: -------------------------------------------------------------------------------- 1 | /// Display diverse functions when dragging over a table 2 | 3 | var M = (module.exports = {}); 4 | 5 | var cell = require("../lib/cell"), 6 | dom = require("../lib/dom"), 7 | event = require("../lib/event"), 8 | preferences = require("../lib/preferences"), 9 | number = require("../lib/number"); 10 | 11 | function getValue(td, fmt) { 12 | var val = { text: "", number: 0, isNumber: false }; 13 | 14 | dom.textContentItems(td).some(function (t) { 15 | var n = number.extract(t, fmt); 16 | if (n !== null) { 17 | return (val = { text: t, number: n, isNumber: true }); 18 | } 19 | }); 20 | 21 | return val; 22 | } 23 | 24 | function data(tbl) { 25 | if (!tbl) { 26 | return null; 27 | } 28 | 29 | var cells = cell.findSelected(tbl); 30 | 31 | if (!cells || !cells.length) { 32 | return null; 33 | } 34 | 35 | var fmt = preferences.numberFormat(); 36 | var values = []; 37 | 38 | cells.forEach(function (td) { 39 | values.push(getValue(td, fmt)); 40 | }); 41 | 42 | return preferences.infoFunctions().map(function (f) { 43 | return { 44 | title: f.name + ":", 45 | message: f.fn(values), 46 | }; 47 | }); 48 | } 49 | 50 | var boxId = "__copytables_infobox__", 51 | pendingContent = null, 52 | timer = 0, 53 | freq = 500; 54 | 55 | function getBox() { 56 | return dom.findOne("#" + boxId); 57 | } 58 | 59 | function setTimer() { 60 | if (!timer) timer = setInterval(draw, freq); 61 | } 62 | 63 | function clearTimer() { 64 | clearInterval(timer); 65 | timer = 0; 66 | } 67 | 68 | function html(items, sticky) { 69 | var h = []; 70 | 71 | items.forEach(function (item) { 72 | if (item.message !== null) 73 | h.push(" " + item.title + "" + item.message + ""); 74 | }); 75 | 76 | h = h.join(""); 77 | 78 | if (sticky) { 79 | h += "×"; 80 | } else { 81 | h += ""; 82 | } 83 | 84 | return h; 85 | } 86 | 87 | function init() { 88 | var box = dom.create("div", { 89 | id: boxId, 90 | "data-position": preferences.val("infobox.position") || "0", 91 | }); 92 | document.body.appendChild(box); 93 | 94 | box.addEventListener("click", function (e) { 95 | if (dom.tag(e.target) === "SPAN") hide(); 96 | }); 97 | 98 | return box; 99 | } 100 | 101 | function draw() { 102 | if (!pendingContent) { 103 | //console.log('no pendingContent'); 104 | clearTimer(); 105 | return; 106 | } 107 | 108 | if (pendingContent === "hide") { 109 | //console.log('removed'); 110 | dom.remove([getBox()]); 111 | clearTimer(); 112 | return; 113 | } 114 | 115 | var box = getBox() || init(); 116 | 117 | dom.removeClass(box, "hidden"); 118 | box.innerHTML = pendingContent; 119 | 120 | pendingContent = null; 121 | //console.log('drawn'); 122 | } 123 | 124 | function show(items) { 125 | var p = html(items, preferences.val("infobox.sticky")); 126 | 127 | if (p === pendingContent) { 128 | //console.log('same content'); 129 | return; 130 | } 131 | 132 | if (pendingContent) { 133 | //console.log('queued'); 134 | } 135 | 136 | pendingContent = p; 137 | setTimer(); 138 | } 139 | 140 | function hide() { 141 | //console.log('about to remove...'); 142 | pendingContent = "hide"; 143 | dom.addClass(getBox(), "hidden"); 144 | setTimer(); 145 | } 146 | 147 | M.update = function (tbl) { 148 | if (preferences.val("infobox.enabled")) { 149 | var d = data(tbl); 150 | if (d && d.length) show(d); 151 | } 152 | }; 153 | 154 | M.remove = function () { 155 | if (!preferences.val("infobox.sticky")) { 156 | hide(); 157 | } 158 | }; 159 | -------------------------------------------------------------------------------- /src/content/loader.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | M.load = function () { 4 | return new Promise(function (resolve) { 5 | function ready() { 6 | return ( 7 | document && 8 | (document.readyState === "interactive" || 9 | document.readyState === "complete") 10 | ); 11 | } 12 | 13 | function onload(e) { 14 | if (ready()) { 15 | console.log("loaded", document.readyState, document.URL); 16 | document.removeEventListener("readystatechange", onload); 17 | resolve(); 18 | } 19 | } 20 | 21 | if (ready()) { 22 | console.log("ready", document.readyState, document.URL); 23 | return resolve(); 24 | } 25 | 26 | console.log("not loaded", document.readyState, document.URL); 27 | document.addEventListener("readystatechange", onload); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/content/scroller.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var dom = require("../lib/dom"), 4 | event = require("../lib/event"), 5 | preferences = require("../lib/preferences"); 6 | 7 | function isScrollable(el) { 8 | var css = window.getComputedStyle(el); 9 | if ( 10 | !css.overflowX.match(/scroll|auto/) && 11 | !css.overflowY.match(/scroll|auto/) 12 | ) 13 | return false; 14 | return el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight; 15 | } 16 | 17 | function closestScrollable(el) { 18 | while (el && el !== document.body && el !== document.documentElement) { 19 | if (isScrollable(el)) return el; 20 | el = el.parentNode; 21 | } 22 | return null; 23 | } 24 | 25 | function position(base) { 26 | return base 27 | ? { x: base.scrollLeft, y: base.scrollTop } 28 | : { x: window.scrollX, y: window.scrollY }; 29 | } 30 | 31 | M.Scroller = function (el) { 32 | (this.base = closestScrollable(el.parentNode)), 33 | (this.anchor = position(this.base)); 34 | this.reset(); 35 | }; 36 | 37 | M.Scroller.prototype.reset = function () { 38 | this.amount = preferences.int("scroll.amount"); 39 | this.acceleration = preferences.int("scroll.acceleration"); 40 | }; 41 | 42 | M.Scroller.prototype.adjustPoint = function (pt) { 43 | var p = position(this.base); 44 | return { 45 | x: pt.x + this.anchor.x - p.x, 46 | y: pt.y + this.anchor.y - p.y, 47 | }; 48 | }; 49 | 50 | M.Scroller.prototype.scroll = function (e) { 51 | function adjust(a, sx, sy, ww, hh, cx, cy) { 52 | if (cx < a) sx -= a; 53 | if (cx > ww - a) sx += a; 54 | if (cy < a) sy -= a; 55 | if (cy > hh - a) sy += a; 56 | return { x: sx, y: sy }; 57 | } 58 | 59 | if (this.base) { 60 | var b = dom.bounds(this.base); 61 | var p = adjust( 62 | this.amount, 63 | this.base.scrollLeft, 64 | this.base.scrollTop, 65 | this.base.clientWidth, 66 | this.base.clientHeight, 67 | e.clientX - b.x, 68 | e.clientY - b.y 69 | ); 70 | 71 | this.base.scrollLeft = p.x; 72 | this.base.scrollTop = p.y; 73 | } else { 74 | var p = adjust( 75 | this.amount, 76 | window.scrollX, 77 | window.scrollY, 78 | window.innerWidth, 79 | window.innerHeight, 80 | e.clientX, 81 | e.clientY 82 | ); 83 | 84 | if (p.x != window.scrollX || p.y != window.scrollY) { 85 | window.scrollTo(p.x, p.y); 86 | } 87 | } 88 | 89 | this.amount = Math.max( 90 | 1, 91 | Math.min(100, this.amount + this.amount * (this.acceleration / 100)) 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /src/content/selection.js: -------------------------------------------------------------------------------- 1 | /// Selection tools. 2 | 3 | var M = (module.exports = {}); 4 | 5 | var dom = require("../lib/dom"), 6 | message = require("../lib/message"), 7 | cell = require("../lib/cell"), 8 | table = require("./table"), 9 | infobox = require("./infobox"); 10 | 11 | function cellsToSelect(el, mode) { 12 | if (mode === "table") { 13 | var tbl = dom.closest(el, "table"); 14 | return tbl ? dom.cells(tbl) : []; 15 | } 16 | 17 | var t = table.locate(el); 18 | 19 | if (!t) { 20 | return []; 21 | } 22 | 23 | if (mode === "cell") { 24 | return [t.td]; 25 | } 26 | 27 | var sel = dom.bounds(t.td); 28 | 29 | return dom.cells(t.table).filter(function (td) { 30 | var b = dom.bounds(td); 31 | 32 | switch (mode) { 33 | case "column": 34 | return sel.x === b.x; 35 | case "row": 36 | return sel.y === b.y; 37 | } 38 | }); 39 | } 40 | 41 | var excludeElements = "a, input, button, textarea, select, img"; 42 | 43 | M.selectable = function (el) { 44 | return !!( 45 | el && 46 | dom.closest(el, "table") && 47 | !dom.closest(el, excludeElements) 48 | ); 49 | }; 50 | 51 | M.selected = function (el) { 52 | var t = table.locate(el); 53 | return t && cell.selected(t.td); 54 | }; 55 | 56 | M.drop = function () { 57 | dom.find("td, th").forEach(cell.reset); 58 | }; 59 | 60 | M.active = function () { 61 | return !!cell.find("selected").length; 62 | }; 63 | 64 | M.table = function () { 65 | var cs = cell.find("selected"); 66 | return cs.length ? dom.closest(cs[0], "table") : null; 67 | }; 68 | 69 | M.start = function (el) { 70 | var t = table.locate(el); 71 | 72 | if (!t) { 73 | return false; 74 | } 75 | 76 | window.getSelection().removeAllRanges(); 77 | message.background("dropOtherSelections"); 78 | 79 | if (M.table() !== t.table) { 80 | M.drop(); 81 | } 82 | 83 | return true; 84 | }; 85 | 86 | M.select = function (el, mode) { 87 | if (dom.is(el, "table")) el = dom.cells(el)[0]; 88 | if (el && M.start(el)) { 89 | var tds = cellsToSelect(el, mode); 90 | tds.forEach(cell.select); 91 | } 92 | }; 93 | 94 | M.toggle = function (el, mode) { 95 | if (dom.is(el, "table")) el = dom.cells(el)[0]; 96 | if (el && M.start(el)) { 97 | var tds = cellsToSelect(el, mode), 98 | fn = tds.every(cell.selected) ? cell.reset : cell.select; 99 | tds.forEach(fn); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /src/content/table.js: -------------------------------------------------------------------------------- 1 | var M = (module.exports = {}); 2 | 3 | var dom = require("../lib/dom"), 4 | cell = require("../lib/cell"), 5 | css = require("../lib/css"), 6 | event = require("../lib/event"), 7 | util = require("../lib/util"); 8 | 9 | function listTables() { 10 | var all = []; 11 | 12 | dom.find("table").forEach(function (tbl, n) { 13 | if (!dom.cells(tbl).length || !dom.visible(tbl)) return; 14 | all.push({ 15 | index: n, 16 | table: tbl, 17 | }); 18 | }); 19 | 20 | return all; 21 | } 22 | 23 | M.locate = function (el) { 24 | var td = dom.closest(el, "td, th"), 25 | tbl = dom.closest(td, "table"); 26 | 27 | return td && tbl ? { td: td, table: tbl } : null; 28 | }; 29 | 30 | M.indexOf = function (tbl) { 31 | var res = -1; 32 | 33 | listTables().forEach(function (r) { 34 | if (tbl === r.table) res = r.index; 35 | }); 36 | 37 | return res; 38 | }; 39 | 40 | M.byIndex = function (index) { 41 | var res = null; 42 | 43 | listTables().forEach(function (r) { 44 | if (index === r.index) res = r.table; 45 | }); 46 | 47 | console.log("byIndex", res); 48 | return res; 49 | }; 50 | 51 | M.enum = function (selectedTable) { 52 | return listTables().map(function (r) { 53 | r.selected = r.table === selectedTable; 54 | delete r.table; 55 | return r; 56 | }); 57 | }; 58 | 59 | M.copy = function (tbl, options, hasSelection) { 60 | console.log(util.timeStart("table.copy")); 61 | 62 | var data = { 63 | hasSelection: hasSelection, 64 | url: document.location ? document.location.href : "", 65 | }; 66 | 67 | if (hasSelection) { 68 | // lock selected cells to remove highlighting with no animation 69 | dom.cells(tbl).forEach(function (td) { 70 | if (cell.selected(td)) { 71 | cell.lock(td); 72 | } 73 | }); 74 | } 75 | 76 | if (options.method === "transfer") { 77 | data.css = {}; 78 | 79 | if (options.keepStyles) { 80 | dom.findSelf("*", tbl).forEach(function (el, uid) { 81 | dom.attr(el, "data-copytables-uid", uid); 82 | data.css[uid] = css.read(el); 83 | }); 84 | } 85 | 86 | data.html = tbl.outerHTML; 87 | 88 | if (options.keepStyles) { 89 | dom.findSelf("*", tbl).forEach(function (el) { 90 | dom.removeAttr(el, "data-copytables-uid"); 91 | }); 92 | } 93 | } 94 | 95 | if (options.method === "clipboard") { 96 | // work around "unselectable" tables 97 | 98 | var style = document.createElement("STYLE"); 99 | style.type = "text/css"; 100 | style.innerHTML = 101 | "* { user-select: auto !important; -webkit-user-select: auto !important }"; 102 | document.body.appendChild(style); 103 | 104 | dom.select(tbl); 105 | 106 | // wrap copy in a capturing handler to work around copy-hijackers 107 | 108 | var copyHandler = function (evt) { 109 | console.log("COPY IN TABLE"); 110 | evt.stopPropagation(); 111 | }; 112 | 113 | document.addEventListener("copy", copyHandler, true); 114 | document.execCommand("copy"); 115 | document.removeEventListener("copy", copyHandler, true); 116 | 117 | dom.deselect(); 118 | document.body.removeChild(style); 119 | } 120 | 121 | if (hasSelection) { 122 | dom.cells(tbl).forEach(function (td) { 123 | if (cell.selected(td)) { 124 | cell.unlock(td); 125 | } 126 | }); 127 | } 128 | 129 | console.log("table.copy method=" + options.method); 130 | console.log(util.timeEnd("table.copy")); 131 | 132 | return data; 133 | }; 134 | 135 | M.selectCaptured = function (tbl) { 136 | dom.cells(tbl).forEach(function (td) { 137 | if (cell.locked(td)) { 138 | cell.reset(td); 139 | } else if (cell.marked(td)) { 140 | cell.unmark(td); 141 | cell.select(td); 142 | } 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /src/ico128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico128.png -------------------------------------------------------------------------------- /src/ico16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico16.png -------------------------------------------------------------------------------- /src/ico19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico19.png -------------------------------------------------------------------------------- /src/ico32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico32.png -------------------------------------------------------------------------------- /src/ico38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico38.png -------------------------------------------------------------------------------- /src/ico48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/src/ico48.png -------------------------------------------------------------------------------- /src/lib/cell.js: -------------------------------------------------------------------------------- 1 | /// Get/set table cell state 2 | 3 | var M = (module.exports = {}); 4 | 5 | var dom = require("../lib/dom"); 6 | 7 | var prefix = "data-copytables-"; 8 | 9 | M.set = function (td, state) { 10 | return td && td.setAttribute(prefix + state, "1"); 11 | }; 12 | 13 | M.is = function (td, state) { 14 | return td && td.hasAttribute(prefix + state); 15 | }; 16 | 17 | M.clear = function (td, state) { 18 | return td && td.removeAttribute(prefix + state); 19 | }; 20 | 21 | M.select = function (td) { 22 | return M.set(td, "selected"); 23 | }; 24 | 25 | M.selected = function (td) { 26 | return M.is(td, "selected"); 27 | }; 28 | 29 | M.unselect = function (td) { 30 | return M.clear(td, "selected"); 31 | }; 32 | 33 | M.mark = function (td) { 34 | return M.set(td, "marked"); 35 | }; 36 | 37 | M.marked = function (td) { 38 | return M.is(td, "marked"); 39 | }; 40 | 41 | M.unmark = function (td) { 42 | return M.clear(td, "marked"); 43 | }; 44 | 45 | M.lock = function (td) { 46 | return M.set(td, "locked"); 47 | }; 48 | 49 | M.locked = function (td) { 50 | return M.is(td, "locked"); 51 | }; 52 | 53 | M.unlock = function (td) { 54 | return M.clear(td, "locked"); 55 | }; 56 | 57 | M.find = function (states, where) { 58 | var sel = states 59 | .split(",") 60 | .map(function (s) { 61 | return "[" + prefix + s.trim() + "]"; 62 | }) 63 | .join(","); 64 | return dom.find(sel, where); 65 | }; 66 | 67 | M.findSelected = function (where) { 68 | var sel = "[{}selected]:not([{}locked]), [{}marked]:not([{}locked])".replace( 69 | /{}/g, 70 | prefix 71 | ); 72 | return dom.find(sel, where); 73 | }; 74 | 75 | M.reset = function (td) { 76 | td.removeAttribute(prefix + "selected"); 77 | td.removeAttribute(prefix + "marked"); 78 | td.removeAttribute(prefix + "locked"); 79 | }; 80 | -------------------------------------------------------------------------------- /src/lib/css.js: -------------------------------------------------------------------------------- 1 | /// CSS tools 2 | 3 | var M = (module.exports = {}); 4 | 5 | var ignore = /^(width|height|display)$|^(-webkit|animation|motion)|-origin$/, 6 | props = null; 7 | 8 | M.read = function (el) { 9 | var cs = window.getComputedStyle(el); 10 | 11 | if (!props) { 12 | props = [].filter.call(cs, function (p) { 13 | return !ignore.test(p); 14 | }); 15 | } 16 | 17 | var res = {}; 18 | 19 | props.forEach(function (p) { 20 | var val = cs.getPropertyValue(p); 21 | res[p] = val.replace(/\b(\d+\.\d+)(?=px\b)/g, function ($0, $1) { 22 | return Math.round(parseFloat($1)); 23 | }); 24 | }); 25 | 26 | if (cs.getPropertyValue("display") === "none") { 27 | res["display"] = "none"; 28 | } 29 | 30 | return res; 31 | }; 32 | 33 | M.compute = function (defaults, custom) { 34 | var rules = []; 35 | 36 | Object.keys(custom).forEach(function (k) { 37 | if (custom[k] !== defaults[k]) { 38 | rules.push(k + ":" + custom[k]); 39 | } 40 | }); 41 | 42 | return rules.join("; "); 43 | }; 44 | -------------------------------------------------------------------------------- /src/lib/dom.js: -------------------------------------------------------------------------------- 1 | /// Basic DOM library 2 | 3 | var M = (module.exports = {}); 4 | 5 | function toArray(coll) { 6 | return Array.prototype.slice.call(coll || [], 0); 7 | } 8 | 9 | function each(coll, fn) { 10 | Array.prototype.forEach.call(coll || [], fn); 11 | } 12 | 13 | M.is = function (el, sel) { 14 | return el && el.matches && el.matches(sel); 15 | }; 16 | 17 | M.visible = function (el) { 18 | return el && !!(el.offsetHeight || el.offsetWidth); 19 | }; 20 | 21 | M.findOne = function (sel, where) { 22 | return (where || document).querySelector(sel); 23 | }; 24 | 25 | M.find = function (sel, where) { 26 | return (where || document).querySelectorAll(sel); 27 | }; 28 | 29 | M.tag = function (el) { 30 | if (!el || !el.tagName) return ""; 31 | return String(el.tagName).toUpperCase(); 32 | }; 33 | 34 | M.indexOf = function (el, sel, where) { 35 | var idx = -1; 36 | M.find(sel, where).forEach(function (e, n) { 37 | if (e === el) { 38 | idx = n; 39 | } 40 | }); 41 | return idx; 42 | }; 43 | 44 | M.nth = function (n, sel, where) { 45 | return M.find(sel, where).item(n); 46 | }; 47 | 48 | M.attr = function (el, name, value) { 49 | if (!el || !el.getAttribute) { 50 | return null; 51 | } 52 | if (arguments.length === 2) { 53 | return el.getAttribute(name); 54 | } 55 | if (value === null) { 56 | return el.removeAttribute(name); 57 | } 58 | return el.setAttribute(name, value); 59 | }; 60 | 61 | M.removeAttr = function (el, name) { 62 | if (el && el.removeAttribute) { 63 | el.removeAttribute(name); 64 | } 65 | }; 66 | 67 | M.findSelf = function (sel, where) { 68 | return [where || document].concat(toArray(M.find(sel, where))); 69 | }; 70 | 71 | M.rows = function (tbl) { 72 | if (tbl && tbl.rows) { 73 | return toArray(tbl.rows); 74 | } 75 | return []; 76 | }; 77 | 78 | M.cells = function (tbl) { 79 | var ls = []; 80 | 81 | M.rows(tbl).forEach(function (tr) { 82 | ls = ls.concat(toArray(tr.cells)); 83 | }); 84 | 85 | return ls; 86 | }; 87 | 88 | M.remove = function (els) { 89 | els.forEach(function (el) { 90 | if (el && el.parentNode) el.parentNode.removeChild(el); 91 | }); 92 | }; 93 | 94 | M.closest = function (el, sel) { 95 | while (el && el.matches) { 96 | if (el.matches(sel)) return el; 97 | el = el.parentNode; 98 | } 99 | return null; 100 | }; 101 | 102 | M.contains = function (parent, el) { 103 | while (el) { 104 | if (el === parent) return true; 105 | el = el.parentNode; 106 | } 107 | return false; 108 | }; 109 | 110 | M.bounds = function (el) { 111 | var r = el.getBoundingClientRect(); 112 | return { 113 | x: r.left, 114 | y: r.top, 115 | right: r.right, 116 | bottom: r.bottom, 117 | rect: [r.left, r.top, r.right, r.bottom], 118 | }; 119 | }; 120 | 121 | M.offset = function (el) { 122 | var r = { x: 0, y: 0 }; 123 | while (el) { 124 | r.x += el.offsetLeft; 125 | r.y += el.offsetTop; 126 | el = el.offsetParent; 127 | } 128 | return r; 129 | }; 130 | 131 | M.addClass = function (el, cls) { 132 | return el && el.classList && el.classList.add(cls); 133 | }; 134 | 135 | M.removeClass = function (el, cls) { 136 | return el && el.classList && el.classList.remove(cls); 137 | }; 138 | 139 | M.hasClass = function (el, cls) { 140 | return el && el.classList && el.classList.contains(cls); 141 | }; 142 | 143 | function _strip(s) { 144 | return String(s || "").replace(/^\s+|\s+$/g, ""); 145 | } 146 | 147 | M.textContentItems = function (node) { 148 | var c = []; 149 | 150 | function walk(n) { 151 | if (!n) return; 152 | 153 | if (n.nodeType === 3) { 154 | var t = _strip(n.textContent); 155 | if (t.length) c.push(t); 156 | return; 157 | } 158 | 159 | if (!M.visible(n)) { 160 | return; 161 | } 162 | 163 | (n.childNodes || []).forEach(walk); 164 | } 165 | 166 | walk(node); 167 | return c; 168 | }; 169 | 170 | M.textContent = function (node) { 171 | return _strip(M.textContentItems(node).join(" ")); 172 | }; 173 | 174 | M.htmlContent = function (node) { 175 | if (!node) return ""; 176 | return _strip(node.innerHTML); 177 | }; 178 | 179 | M.deselect = function () { 180 | var selection = window.getSelection(); 181 | selection.removeAllRanges(); 182 | }; 183 | 184 | M.select = function (el) { 185 | var range = document.createRange(); 186 | var selection = window.getSelection(); 187 | selection.removeAllRanges(); 188 | 189 | range.selectNodeContents(el); 190 | selection.addRange(range); 191 | }; 192 | 193 | M.create = function (tag, atts) { 194 | var e = document.createElement(tag); 195 | if (atts) { 196 | Object.keys(atts).forEach(function (a) { 197 | e.setAttribute(a, atts[a]); 198 | }); 199 | } 200 | return e; 201 | }; 202 | -------------------------------------------------------------------------------- /src/lib/event.js: -------------------------------------------------------------------------------- 1 | /// Basic events library 2 | 3 | var M = (module.exports = {}); 4 | 5 | var lastEvent = null; 6 | 7 | M.register = function (evt) { 8 | lastEvent = evt; 9 | }; 10 | 11 | M.last = function () { 12 | return lastEvent; 13 | }; 14 | 15 | M.lastTarget = function () { 16 | return lastEvent ? lastEvent.target : null; 17 | }; 18 | 19 | M.reset = function (evt) { 20 | evt.stopPropagation(); 21 | evt.preventDefault(); 22 | }; 23 | 24 | M.listen = function (target, listeners) { 25 | Object.keys(listeners).forEach(function (key) { 26 | var m = key.match(/^(\w+?)(Capture)?$/); 27 | (target || document).addEventListener(m[1], listeners[key], !!m[2]); 28 | }); 29 | }; 30 | 31 | M.unlisten = function (target, listeners) { 32 | Object.keys(listeners).forEach(function (key) { 33 | var m = key.match(/^(\w+?)(Capture)?$/); 34 | (target || document).removeEventListener(m[1], listeners[key], !!m[2]); 35 | }); 36 | }; 37 | 38 | var tracker = { 39 | active: false, 40 | lastEvent: null, 41 | timer: 0, 42 | freq: 5, 43 | }; 44 | 45 | M.trackMouse = function (evt, fn) { 46 | function watch() { 47 | fn("tick", tracker.lastEvent); 48 | tracker.timer = setTimeout(watch, tracker.freq); 49 | } 50 | 51 | function reset() { 52 | clearInterval(tracker.timer); 53 | tracker.active = false; 54 | M.unlisten(document, listeners); 55 | } 56 | 57 | var listeners = { 58 | mousemove: function (evt) { 59 | M.reset(evt); 60 | tracker.lastEvent = evt; 61 | 62 | if (evt.buttons === 1) { 63 | clearTimeout(tracker.timer); 64 | fn("move", tracker.lastEvent); 65 | watch(); 66 | } else { 67 | reset(); 68 | fn("up", evt); 69 | } 70 | }, 71 | 72 | mouseup: function (evt) { 73 | M.reset(evt); 74 | tracker.lastEvent = evt; 75 | reset(); 76 | fn("up", evt); 77 | }, 78 | }; 79 | 80 | if (tracker.active) { 81 | console.log("mouse tracker already active"); 82 | reset(); 83 | } 84 | 85 | tracker.active = true; 86 | console.log("mouse tracker started"); 87 | 88 | M.listen(document, listeners); 89 | listeners.mousemove(evt); 90 | }; 91 | -------------------------------------------------------------------------------- /src/lib/keyboard.js: -------------------------------------------------------------------------------- 1 | /// Keyboard events and keys 2 | 3 | var M = (module.exports = {}); 4 | 5 | M.mac = navigator.userAgent.indexOf("Macintosh") > 0; 6 | M.win = navigator.userAgent.indexOf("Windows") > 0; 7 | 8 | M.modifiers = { 9 | SHIFT: 1 << 10, 10 | CTRL: 1 << 11, 11 | ALT: 1 << 12, 12 | META: 1 << 13, 13 | }; 14 | 15 | M.mouseModifiers = {}; 16 | 17 | if (M.mac) 18 | M.mouseModifiers = [M.modifiers.SHIFT, M.modifiers.ALT, M.modifiers.META]; 19 | else if (M.win) 20 | M.mouseModifiers = [M.modifiers.SHIFT, M.modifiers.ALT, M.modifiers.CTRL]; 21 | else 22 | M.mouseModifiers = [ 23 | M.modifiers.SHIFT, 24 | M.modifiers.ALT, 25 | M.modifiers.CTRL, 26 | M.modifiers.META, 27 | ]; 28 | 29 | M.modNames = {}; 30 | 31 | M.modNames[M.modifiers.CTRL] = "Ctrl"; 32 | M.modNames[M.modifiers.ALT] = M.mac ? "Opt" : "Alt"; 33 | M.modNames[M.modifiers.META] = M.mac ? "Cmd" : M.win ? "Win" : "Meta"; 34 | M.modNames[M.modifiers.SHIFT] = "Shift"; 35 | 36 | M.modHTMLNames = {}; 37 | 38 | M.modHTMLNames[M.modifiers.CTRL] = M.mac 39 | ? "⌃ control" 40 | : M.modNames[M.modifiers.CTRL]; 41 | M.modHTMLNames[M.modifiers.ALT] = M.mac 42 | ? "⌥ option" 43 | : M.modNames[M.modifiers.ALT]; 44 | M.modHTMLNames[M.modifiers.META] = M.mac 45 | ? "⌘ command" 46 | : M.modNames[M.modifiers.META]; 47 | M.modHTMLNames[M.modifiers.SHIFT] = M.mac 48 | ? "⇧ shift" 49 | : M.modNames[M.modifiers.SHIFT]; 50 | 51 | M.keyNames = { 52 | 8: "Backspace", 53 | 9: "Tab", 54 | 13: "Enter", 55 | 19: "Break", 56 | 20: "Caps", 57 | 27: "Esc", 58 | 32: "Space", 59 | 33: "PgUp", 60 | 34: "PgDn", 61 | 35: "End", 62 | 36: "Home", 63 | 37: "Left", 64 | 38: "Up", 65 | 39: "Right", 66 | 40: "Down", 67 | 45: "Ins", 68 | 46: "Del", 69 | 48: "0", 70 | 49: "1", 71 | 50: "2", 72 | 51: "3", 73 | 52: "4", 74 | 53: "5", 75 | 54: "6", 76 | 55: "7", 77 | 56: "8", 78 | 57: "9", 79 | 65: "A", 80 | 66: "B", 81 | 67: "C", 82 | 68: "D", 83 | 69: "E", 84 | 70: "F", 85 | 71: "G", 86 | 72: "H", 87 | 73: "I", 88 | 74: "J", 89 | 75: "K", 90 | 76: "L", 91 | 77: "M", 92 | 78: "N", 93 | 79: "O", 94 | 80: "P", 95 | 81: "Q", 96 | 82: "R", 97 | 83: "S", 98 | 84: "T", 99 | 85: "U", 100 | 86: "V", 101 | 87: "W", 102 | 88: "X", 103 | 89: "Y", 104 | 90: "Z", 105 | 93: "Select", 106 | 96: "Num0", 107 | 97: "Num1", 108 | 98: "Num2", 109 | 99: "Num3", 110 | 100: "Num4", 111 | 101: "Num5", 112 | 102: "Num6", 113 | 103: "Num7", 114 | 104: "Num8", 115 | 105: "Num9", 116 | 106: "Num*", 117 | 107: "Num+", 118 | 109: "Num-", 119 | 110: "Num.", 120 | 111: "Num/", 121 | 112: "F1", 122 | 113: "F2", 123 | 114: "F3", 124 | 115: "F4", 125 | 116: "F5", 126 | 117: "F6", 127 | 118: "F7", 128 | 119: "F8", 129 | 120: "F9", 130 | 121: "F10", 131 | 122: "F11", 132 | 123: "F12", 133 | 144: "NumLock", 134 | 145: "ScrollLock", 135 | 186: ";", 136 | 187: "=", 137 | 188: ",", 138 | 189: "-", 139 | 190: ".", 140 | 191: "/", 141 | 192: "`", 142 | 219: "(", 143 | 220: "\\", 144 | 221: ")", 145 | 222: "'", 146 | }; 147 | 148 | M.keyCode = function (name) { 149 | var code; 150 | 151 | Object.keys(M.keyNames).some(function (c) { 152 | if (M.keyNames[c] === name) { 153 | return (code = c); 154 | } 155 | }); 156 | 157 | return code; 158 | }; 159 | 160 | M.key = function (e) { 161 | var mods = 162 | (M.modifiers.ALT * e.altKey) | 163 | (M.modifiers.CTRL * e.ctrlKey) | 164 | (M.modifiers.META * e.metaKey) | 165 | (M.modifiers.SHIFT * e.shiftKey); 166 | 167 | var scan = e.keyCode, 168 | sname = M.keyNames[scan], 169 | mname = [], 170 | cname = []; 171 | 172 | Object.keys(M.modifiers).forEach(function (m) { 173 | if (mods & M.modifiers[m]) { 174 | mname.push(M.modNames[M.modifiers[m]]); 175 | } 176 | }); 177 | 178 | mname = mname.join(" "); 179 | 180 | var r = { 181 | modifiers: { code: 0, name: "" }, 182 | scan: { code: 0, name: "" }, 183 | }; 184 | 185 | if (mname) { 186 | r.modifiers = { code: mods, name: mname }; 187 | cname.push(mname); 188 | } 189 | 190 | if (sname) { 191 | r.scan = { code: scan, name: sname }; 192 | cname.push(sname); 193 | } 194 | 195 | r.code = mods | scan; 196 | r.name = cname.join(" "); 197 | 198 | return r; 199 | }; 200 | -------------------------------------------------------------------------------- /src/lib/matrix.js: -------------------------------------------------------------------------------- 1 | /// Matrix manipulations 2 | 3 | var M = (module.exports = {}); 4 | 5 | M.column = function (mat, ci) { 6 | return mat.map(function (row) { 7 | return row[ci]; 8 | }); 9 | }; 10 | 11 | M.transpose = function (mat) { 12 | if (!mat.length) return mat; 13 | return mat[0].map(function (_, ci) { 14 | return M.column(mat, ci); 15 | }); 16 | }; 17 | 18 | M.trim = function (mat, fn) { 19 | var fun = function (row) { 20 | return row.some(function (cell) { 21 | return fn(cell); 22 | }); 23 | }; 24 | 25 | mat = mat.filter(fun); 26 | mat = M.transpose(mat).filter(fun); 27 | return M.transpose(mat); 28 | }; 29 | 30 | M.each = function (mat, fn) { 31 | mat.forEach(function (row, ri) { 32 | row.forEach(function (cell, ci) { 33 | fn(row, cell, ri, ci); 34 | }); 35 | }); 36 | }; 37 | 38 | M.map = function (mat, fn) { 39 | return mat.map(function (row, ri) { 40 | return row.map(function (cell, ci) { 41 | return fn(row, cell, ri, ci); 42 | }); 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/lib/message.js: -------------------------------------------------------------------------------- 1 | /// Wrappers for browser.sendMessage 2 | 3 | var M = (module.exports = {}); 4 | 5 | var util = require("./util"); 6 | 7 | function convertMessage(msg) { 8 | if (typeof msg !== "object") { 9 | return { name: msg }; 10 | } 11 | return msg; 12 | } 13 | 14 | function convertSender(sender) { 15 | if (!sender) { 16 | return {}; 17 | } 18 | if (typeof sender !== "object") { 19 | return sender; 20 | } 21 | var k = Object.keys(sender); 22 | if (k.length === 1 && k[0] === "id") { 23 | return "background"; 24 | } 25 | if (sender.tab) { 26 | var p = Object.assign({}, sender); 27 | p.tabId = sender.tab.id; 28 | return p; 29 | } 30 | return sender; 31 | } 32 | 33 | function toBackground(msg) { 34 | msg.to = "background"; 35 | return util.callBrowserAsync("runtime.sendMessage", msg).then(function (res) { 36 | return { receiver: "background", data: res }; 37 | }); 38 | } 39 | 40 | function toFrame(msg, frame) { 41 | msg.to = frame; 42 | return util 43 | .callBrowserAsync("tabs.sendMessage", frame.tabId, msg, { 44 | frameId: frame.frameId, 45 | }) 46 | .then(function (res) { 47 | return { receiver: frame, data: res }; 48 | }); 49 | } 50 | 51 | function toFrameList(msg, frames) { 52 | return Promise.all( 53 | frames.map(function (frame) { 54 | return toFrame(msg, frame); 55 | }) 56 | ); 57 | } 58 | 59 | M.enumFrames = function (tabFilter) { 60 | function framesInTab(tab) { 61 | return util 62 | .callBrowserAsync("webNavigation.getAllFrames", { tabId: tab.id }) 63 | .then(function (frames) { 64 | if (!frames) { 65 | // Vivaldi, as of 1.8.770.56, doesn't support getAllFrames() properly 66 | // let's pretend there's only one top frame 67 | frames = [ 68 | { 69 | errorOccurred: false, 70 | frameId: 0, 71 | parentFrameId: -1, 72 | }, 73 | ]; 74 | } 75 | return frames.map(function (f) { 76 | f.tabId = tab.id; 77 | return f; 78 | }); 79 | }); 80 | } 81 | 82 | if (tabFilter === "active") { 83 | tabFilter = { active: true, currentWindow: true }; 84 | } 85 | 86 | return util 87 | .callBrowserAsync("tabs.query", tabFilter || {}) 88 | .then(function (tabs) { 89 | return Promise.all(tabs.map(framesInTab)); 90 | }) 91 | .then(function (res) { 92 | return util.flatten(res); 93 | }); 94 | }; 95 | 96 | M.background = function (msg) { 97 | console.log("MESSAGE: background", msg); 98 | return toBackground(convertMessage(msg)); 99 | }; 100 | 101 | M.frame = function (msg, frame) { 102 | console.log("MESSAGE: frame", msg, frame); 103 | return toFrame(convertMessage(msg), frame); 104 | }; 105 | 106 | M.allFrames = function (msg) { 107 | console.log("MESSAGE: allFrames", msg); 108 | msg = convertMessage(msg); 109 | return M.enumFrames("active").then(function (frames) { 110 | return toFrameList(msg, frames); 111 | }); 112 | }; 113 | 114 | M.topFrame = function (msg) { 115 | console.log("MESSAGE: topFrame", msg); 116 | msg = convertMessage(msg); 117 | return M.enumFrames("active").then(function (frames) { 118 | var top = frames.filter(function (f) { 119 | return f.frameId === 0; 120 | }); 121 | if (top.length) { 122 | return toFrame(msg, top[0]); 123 | } 124 | }); 125 | }; 126 | 127 | M.broadcast = function (msg) { 128 | console.log("MESSAGE: broadcast", msg); 129 | msg = convertMessage(msg); 130 | return M.enumFrames().then(function (frames) { 131 | return toFrameList(msg, frames); 132 | }); 133 | }; 134 | 135 | M.listen = function (listeners) { 136 | util.callBrowser("runtime.onMessage.addListener", function (msg, sender, fn) { 137 | if (listeners[msg.name]) { 138 | msg.sender = convertSender(sender); 139 | var res = listeners[msg.name](msg); 140 | return fn(res); 141 | } 142 | console.log("LOST", msg.name); 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /src/lib/number.js: -------------------------------------------------------------------------------- 1 | /// Tools to work with numbers 2 | 3 | var M = (module.exports = {}); 4 | 5 | function isDigit(s) { 6 | return s.match(/^\d+$/); 7 | } 8 | 9 | function parseInteger(s) { 10 | if (!isDigit(s)) { 11 | return null; 12 | } 13 | var n = Number(s); 14 | return isDigit(String(n)) ? n : null; 15 | } 16 | 17 | function parseFraction(s) { 18 | var n = parseInteger("1" + s); 19 | return n === null ? null : String(n).slice(1); 20 | } 21 | 22 | function parseGrouped(s, fmt) { 23 | if (!fmt.group || s.indexOf(fmt.group) < 0) { 24 | return parseInteger(s); 25 | } 26 | 27 | var g = "\\" + fmt.group; 28 | var re = new RegExp("^\\d{1,3}(" + g + "\\d{2,3})*$"); 29 | 30 | if (!s.match(re)) { 31 | return null; 32 | } 33 | 34 | return parseInteger(s.replace(/\D+/g, "")); 35 | } 36 | 37 | M.parse = function (s, fmt) { 38 | if (s[0] === "-") { 39 | var n = M.parse(s.slice(1), fmt); 40 | return n === null ? null : -n; 41 | } 42 | 43 | if (!fmt.decimal || s.indexOf(fmt.decimal) < 0) { 44 | return parseGrouped(s, fmt); 45 | } 46 | 47 | if (s === fmt.decimal) { 48 | return null; 49 | } 50 | 51 | var ds = s.split(fmt.decimal); 52 | 53 | if (ds.length === 1) { 54 | return parseGrouped(ds[0], fmt); 55 | } 56 | 57 | if (ds.length === 2) { 58 | var a = ds[0].length ? parseGrouped(ds[0], fmt) : 0; 59 | var b = ds[1].length ? parseFraction(ds[1]) : 0; 60 | 61 | if (a === null || b === null) { 62 | return null; 63 | } 64 | 65 | return Number(a + "." + b); 66 | } 67 | 68 | return null; 69 | }; 70 | 71 | M.extract = function (text, fmt) { 72 | if (!text) { 73 | return null; 74 | } 75 | 76 | text = String(text).replace(/^\s+|\s+$/g, ""); 77 | if (!text) { 78 | return null; 79 | } 80 | 81 | var g = fmt.group ? "\\" + fmt.group : ""; 82 | var d = fmt.decimal ? "\\" + fmt.decimal : ""; 83 | 84 | var re = new RegExp("-?[\\d" + g + d + "]*\\d", "g"); 85 | var m = text.match(re); 86 | 87 | if (!m || m.length !== 1) { 88 | return null; 89 | } 90 | 91 | var n = M.parse(m[0], fmt); 92 | if (n === null) { 93 | return null; 94 | } 95 | 96 | return n; 97 | }; 98 | 99 | M.defaultFormat = function () { 100 | var f = { group: ",", decimal: "." }; 101 | 102 | try { 103 | // Intl and formatToParts might not be available... 104 | 105 | var nf = new Intl.NumberFormat(); 106 | 107 | nf.formatToParts(123456.78).forEach(function (p) { 108 | if (String(p.type) === "group") f.group = p.value; 109 | if (String(p.type) === "decimal") f.decimal = p.value; 110 | }); 111 | return f; 112 | } catch (e) {} 113 | 114 | try { 115 | var s = (123456.78).toLocaleString().replace(/\d/g, ""), 116 | len = s.length; 117 | 118 | f.decimal = len > 0 ? s[len - 1] : "."; 119 | f.group = len > 1 ? s[len - 2] : ""; 120 | return f; 121 | } catch (e) {} 122 | 123 | return f; 124 | }; 125 | 126 | M.format = function (n, prec) { 127 | n = prec ? Number(n.toFixed(prec)) : Number(n); 128 | return (n || 0).toLocaleString(); 129 | }; 130 | -------------------------------------------------------------------------------- /src/lib/preferences.js: -------------------------------------------------------------------------------- 1 | /// Preferences, stored in browser.storage 2 | 3 | var M = (module.exports = {}); 4 | 5 | var keyboard = require("./keyboard"), 6 | number = require("./number"), 7 | util = require("./util"); 8 | 9 | var firstMod = keyboard.modifiers.ALT, 10 | secondMod = keyboard.mac ? keyboard.modifiers.META : keyboard.modifiers.CTRL; 11 | 12 | var defaults = { 13 | "modifier.cell": firstMod, 14 | "modifier.column": firstMod | secondMod, 15 | "modifier.row": 0, 16 | "modifier.table": 0, 17 | "modifier.extend": keyboard.modifiers.SHIFT, 18 | 19 | "capture.enabled": true, 20 | "capture.reset": false, 21 | 22 | "scroll.amount": 30, 23 | "scroll.acceleration": 5, 24 | 25 | "copy.format.enabled.richHTMLCSS": true, 26 | "copy.format.enabled.richHTML": true, 27 | "copy.format.enabled.textTabs": true, 28 | "copy.format.enabled.textTabsSwap": true, 29 | "copy.format.enabled.textCSV": true, 30 | "copy.format.enabled.textCSVSwap": true, 31 | "copy.format.enabled.textHTMLCSS": true, 32 | "copy.format.enabled.textHTML": true, 33 | "copy.format.enabled.textTextile": true, 34 | 35 | "copy.format.default.richHTMLCSS": true, 36 | 37 | "infobox.enabled": true, 38 | "infobox.position": "0", 39 | }; 40 | 41 | var captureModes = [ 42 | { 43 | id: "zzz", 44 | name: "Off", 45 | }, 46 | { 47 | id: "cell", 48 | name: "Cells", 49 | }, 50 | { 51 | id: "column", 52 | name: "Columns", 53 | }, 54 | { 55 | id: "row", 56 | name: "Rows", 57 | }, 58 | { 59 | id: "table", 60 | name: "Tables", 61 | }, 62 | ]; 63 | 64 | var copyFormats = [ 65 | { 66 | id: "richHTMLCSS", 67 | name: "As is", 68 | desc: "Copy the table as seen on the screen", 69 | }, 70 | { 71 | id: "richHTML", 72 | name: "Plain Table", 73 | desc: "Copy the table without formatting", 74 | }, 75 | { 76 | id: "textTabs", 77 | name: "Text", 78 | desc: "Copy as tab-delimited text", 79 | }, 80 | { 81 | id: "textTabsSwap", 82 | name: "Text+Swap", 83 | desc: "Copy as tab-delimited text, swap columns and rows", 84 | }, 85 | { 86 | id: "textCSV", 87 | name: "CSV", 88 | desc: "Copy as comma-separated text", 89 | }, 90 | { 91 | id: "textCSVSwap", 92 | name: "CSV+Swap", 93 | desc: "Copy as comma-separated text, swap columns and rows", 94 | }, 95 | { 96 | id: "textHTMLCSS", 97 | name: "HTML+CSS", 98 | desc: "Copy as HTML source, retain formatting", 99 | }, 100 | { 101 | id: "textHTML", 102 | name: "HTML", 103 | desc: "Copy as HTML source, without formatting", 104 | }, 105 | { 106 | id: "textTextile", 107 | name: "Textile", 108 | desc: "Copy as Textile (Text content)", 109 | }, 110 | ]; 111 | 112 | function sum(vs) { 113 | return vs.reduce(function (x, y) { 114 | return x + (Number(y) || 0); 115 | }, 0); 116 | } 117 | 118 | function getNumbers(values) { 119 | var vs = []; 120 | 121 | values.forEach(function (v) { 122 | if (v.isNumber) vs.push(v.number); 123 | }); 124 | 125 | return vs.length ? vs : null; 126 | } 127 | 128 | var infoFunctions = [ 129 | { 130 | name: "count", 131 | fn: function (values) { 132 | return values.length; 133 | }, 134 | }, 135 | { 136 | name: "sum", 137 | fn: function (values) { 138 | var vs = getNumbers(values); 139 | return vs ? number.format(sum(vs)) : null; 140 | }, 141 | }, 142 | { 143 | name: "average", 144 | fn: function (values) { 145 | var vs = getNumbers(values); 146 | return vs ? number.format(sum(vs) / vs.length, 2) : null; 147 | }, 148 | }, 149 | { 150 | name: "min", 151 | fn: function (values) { 152 | var vs = getNumbers(values); 153 | return vs ? number.format(Math.min.apply(Math, vs)) : null; 154 | }, 155 | }, 156 | { 157 | name: "max", 158 | fn: function (values) { 159 | var vs = getNumbers(values); 160 | return vs ? number.format(Math.max.apply(Math, vs)) : null; 161 | }, 162 | }, 163 | ]; 164 | 165 | var prefs = {}; 166 | 167 | function _constrain(min, val, max) { 168 | val = Number(val) || min; 169 | return Math.max(min, Math.min(val, max)); 170 | } 171 | 172 | M.load = function () { 173 | return util.callBrowserAsync("storage.local.get", null).then(function (obj) { 174 | obj = obj || {}; 175 | 176 | // from the previous version 177 | if ("modKey" in obj && String(obj.modKey) === "1") { 178 | console.log("FOUND ALTERNATE MODKEY SETTING"); 179 | obj["modifier.cell"] = secondMod; 180 | delete obj.modKey; 181 | } 182 | 183 | prefs = Object.assign({}, defaults, prefs, obj); 184 | 185 | prefs["scroll.amount"] = _constrain(1, prefs["scroll.amount"], 100); 186 | prefs["scroll.acceleration"] = _constrain( 187 | 0, 188 | prefs["scroll.acceleration"], 189 | 100 190 | ); 191 | 192 | if (!prefs["number.group"]) { 193 | prefs["number.group"] = number.defaultFormat().group; 194 | } 195 | 196 | if (!prefs["number.decimal"]) { 197 | prefs["number.decimal"] = number.defaultFormat().decimal; 198 | } 199 | 200 | console.log("PREFS LOAD", prefs); 201 | return prefs; 202 | }); 203 | }; 204 | 205 | M.save = function () { 206 | return util 207 | .callBrowserAsync("storage.local.clear") 208 | .then(function () { 209 | return util.callBrowserAsync("storage.local.set", prefs); 210 | }) 211 | .then(function () { 212 | console.log("PREFS SET", prefs); 213 | return prefs; 214 | }); 215 | }; 216 | 217 | M.setAll = function (obj) { 218 | prefs = Object.assign({}, prefs, obj); 219 | return M.save(); 220 | }; 221 | 222 | M.set = function (key, val) { 223 | prefs[key] = val; 224 | return M.save(); 225 | }; 226 | 227 | M.val = function (key) { 228 | return prefs[key]; 229 | }; 230 | 231 | M.int = function (key) { 232 | return Number(M.val(key)) || 0; 233 | }; 234 | 235 | M.copyFormats = function () { 236 | return copyFormats.map(function (f) { 237 | f.enabled = !!M.val("copy.format.enabled." + f.id); 238 | f.default = !!M.val("copy.format.default." + f.id); 239 | return f; 240 | }); 241 | }; 242 | 243 | M.numberFormat = function () { 244 | var g = M.val("number.group"); 245 | var d = M.val("number.decimal"); 246 | 247 | if (!g && !d) { 248 | return number.defaultFormat(); 249 | } 250 | 251 | return { 252 | group: g || "", 253 | decimal: d || "", 254 | }; 255 | }; 256 | 257 | M.infoFunctions = function () { 258 | return infoFunctions; 259 | }; 260 | 261 | M.captureModes = function () { 262 | return captureModes; 263 | }; 264 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | // Utility functions. 2 | 3 | var M = (module.exports = {}); 4 | 5 | M.numeric = function (a, b) { 6 | return a - b; 7 | }; 8 | 9 | M.toArray = function (coll) { 10 | return Array.prototype.slice.call(coll || [], 0); 11 | }; 12 | 13 | M.flatten = function (a) { 14 | while (a.some(Array.isArray)) a = Array.prototype.concat.apply([], a); 15 | return a; 16 | }; 17 | 18 | M.first = function (a, fn) { 19 | for (var i = 0; i < a.length; i++) { 20 | if (fn(a[i], i)) { 21 | return a[i]; 22 | } 23 | } 24 | return null; 25 | }; 26 | 27 | M.intersect = function (a, b) { 28 | return !(a[0] >= b[2] || a[2] <= b[0] || a[1] >= b[3] || a[3] <= b[1]); 29 | }; 30 | 31 | M.lstrip = function (s) { 32 | return s.replace(/^\s+/, ""); 33 | }; 34 | 35 | M.rstrip = function (s) { 36 | return s.replace(/\s+$/, ""); 37 | }; 38 | 39 | M.strip = function (s) { 40 | return s.replace(/^\s+|\s+$/g, ""); 41 | }; 42 | 43 | M.reduceWhitespace = function (html) { 44 | return html 45 | .replace(/\n\r/g, "\n") 46 | .replace(/\n[ ]+/g, "\n") 47 | .replace(/[ ]+\n/g, "\n") 48 | .replace(/\n+/g, "\n"); 49 | }; 50 | 51 | M.uid = function (len) { 52 | var s = ""; 53 | while (len--) { 54 | s += String.fromCharCode(97 + Math.floor(Math.random() * 26)); 55 | } 56 | return s; 57 | }; 58 | 59 | M.nobr = function (s) { 60 | return s.replace(/[\r\n]+/g, " "); 61 | }; 62 | 63 | M.format = function (s, obj) { 64 | return s.replace(/\${(\w+)}/g, function (_, $1) { 65 | return obj[$1]; 66 | }); 67 | }; 68 | 69 | var _times = {}; 70 | 71 | M.timeStart = function (name) { 72 | _times[name] = new Date(); 73 | return "TIME START: " + name; 74 | }; 75 | 76 | M.timeEnd = function (name) { 77 | if (_times[name]) { 78 | var t = new Date() - _times[name]; 79 | delete _times[name]; 80 | return "TIME END: " + name + " " + t; 81 | } 82 | }; 83 | 84 | function callBrowser(useAsync, fn, args) { 85 | var parts = fn.split("."), 86 | obj = browser, 87 | method = parts.pop(); 88 | 89 | parts.forEach(function (p) { 90 | obj = obj[p]; 91 | }); 92 | 93 | console.log("CALL_BROWSER", useAsync, fn); 94 | 95 | if (!useAsync) { 96 | try { 97 | return obj[method].apply(obj, args); 98 | } catch (err) { 99 | console.log("CALL_BROWSER_ERROR", fn, err.message); 100 | return null; 101 | } 102 | } 103 | 104 | return new Promise(function (resolve, reject) { 105 | try { 106 | obj[method].apply(obj, args).then( 107 | function (res) { 108 | resolve(res); 109 | }, 110 | function (err) { 111 | console.log("CALL_BROWSER_LAST_ERROR", fn, err); 112 | resolve(null); 113 | } 114 | ); 115 | } catch (err) { 116 | console.log("CALL_BROWSER_ERROR", fn, err.message); 117 | resolve(null); 118 | } 119 | }); 120 | } 121 | 122 | M.callBrowser = function (fn) { 123 | return callBrowser(false, fn, [].slice.call(arguments, 1)); 124 | }; 125 | 126 | M.callBrowserAsync = function (fn) { 127 | return callBrowser(true, fn, [].slice.call(arguments, 1)); 128 | }; 129 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "CopyTables", 4 | "version": "0.1.1", 5 | "homepage_url": "https://github.com/nirantak/copytables", 6 | "description": "Select table cells, rows and columns with your mouse or from a context menu. Copy as rich text, HTML, CSV and TSV", 7 | "author": "Nirantak Raghav", 8 | "content_scripts": [ 9 | { 10 | "js": ["content.js"], 11 | "css": ["content.css"], 12 | "matches": ["*://*/*", "file://*/*"], 13 | "all_frames": true 14 | } 15 | ], 16 | "background": { 17 | "scripts": ["background.js"], 18 | "persistent": false 19 | }, 20 | "permissions": [ 21 | "clipboardRead", 22 | "clipboardWrite", 23 | "contextMenus", 24 | "webNavigation", 25 | "storage" 26 | ], 27 | "icons": { 28 | "16": "ico16.png", 29 | "48": "ico48.png", 30 | "128": "ico128.png" 31 | }, 32 | "browser_action": { 33 | "default_icon": { 34 | "19": "ico19.png", 35 | "38": "ico38.png" 36 | }, 37 | "default_title": "CopyTables", 38 | "default_popup": "popup.html" 39 | }, 40 | "options_ui": { 41 | "page": "options.html", 42 | "open_in_tab": true 43 | }, 44 | "commands": { 45 | "capture_cell": { 46 | "description": "Capture cells" 47 | }, 48 | "capture_column": { 49 | "description": "Capture columns" 50 | }, 51 | "capture_row": { 52 | "description": "Capture rows" 53 | }, 54 | "capture_zzz": { 55 | "description": "Turn off the capture" 56 | }, 57 | "capture_table": { 58 | "description": "Capture tables" 59 | }, 60 | "find_previous": { 61 | "description": "Find previous table" 62 | }, 63 | "find_next": { 64 | "description": "Find next table" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | var dom = require("./lib/dom"), 2 | preferences = require("./lib/preferences"), 3 | keyboard = require("./lib/keyboard"), 4 | event = require("./lib/event"), 5 | message = require("./lib/message"), 6 | util = require("./lib/util"); 7 | 8 | var mouseButtonNames = [ 9 | "Left Button", 10 | "Middle/Wheel", 11 | "Right Button", 12 | "Button 3", 13 | "Button 4", 14 | ]; 15 | 16 | function modifiers(mode) { 17 | var tpl = [ 18 | '", 22 | ].join(""); 23 | 24 | var kmod = preferences.val("modifier." + mode); 25 | 26 | return keyboard.mouseModifiers 27 | .map(function (m) { 28 | return util.format(tpl, { 29 | code: m, 30 | name: keyboard.modHTMLNames[m], 31 | mode: mode, 32 | checked: kmod & m ? "checked" : "", 33 | }); 34 | }) 35 | .join(""); 36 | } 37 | 38 | function copyFormats() { 39 | var tpl = [ 40 | "", 41 | " ${name}${desc}", 42 | ' ", 45 | ' ", 48 | "", 49 | ].join(""); 50 | 51 | var s = preferences.copyFormats().map(function (f) { 52 | return util.format(tpl, f); 53 | }); 54 | 55 | return "" + s.join("") + "
"; 56 | } 57 | 58 | function load() { 59 | dom.findOne("#copy-formats").innerHTML = copyFormats(); 60 | 61 | dom.find("[data-modifiers]").forEach(function (el) { 62 | el.innerHTML = modifiers(dom.attr(el, "data-modifiers")); 63 | }); 64 | 65 | dom.find("[data-bool]").forEach(function (el) { 66 | el.checked = !!preferences.val(dom.attr(el, "data-bool")); 67 | }); 68 | 69 | dom.find("[data-select]").forEach(function (el) { 70 | el.checked = 71 | preferences.val(dom.attr(el, "data-select")) === 72 | dom.attr(el, "data-select-value"); 73 | }); 74 | 75 | dom.find("[data-text]").forEach(function (el) { 76 | var t = preferences.val(dom.attr(el, "data-text")); 77 | el.value = typeof t === "undefined" ? "" : t; 78 | }); 79 | } 80 | 81 | function save() { 82 | var prefs = {}; 83 | 84 | dom.find("[data-modifier]").forEach(function (el) { 85 | var code = Number(dom.attr(el, "data-modifier")), 86 | mode = dom.attr(el, "data-mode"); 87 | prefs["modifier." + mode] = 88 | (prefs["modifier." + mode] || 0) | (el.checked ? code : 0); 89 | }); 90 | 91 | dom.find("[data-bool]").forEach(function (el) { 92 | prefs[dom.attr(el, "data-bool")] = !!el.checked; 93 | }); 94 | 95 | dom.find("[data-select]").forEach(function (el) { 96 | if (el.checked) 97 | prefs[dom.attr(el, "data-select")] = dom.attr(el, "data-select-value"); 98 | }); 99 | 100 | dom.find("[data-text]").forEach(function (el) { 101 | prefs[dom.attr(el, "data-text")] = el.value; 102 | }); 103 | 104 | preferences.setAll(prefs).then(load); 105 | message.background("preferencesUpdated"); 106 | } 107 | 108 | window.onload = function () { 109 | preferences.load().then(load); 110 | 111 | event.listen(window, { 112 | input: save, 113 | change: save, 114 | click: function (e) { 115 | var cmd = dom.attr(e.target, "data-command"); 116 | if (cmd) { 117 | message.background({ name: "command", command: cmd }); 118 | event.reset(e); 119 | } 120 | }, 121 | }); 122 | }; 123 | -------------------------------------------------------------------------------- /src/options.pug: -------------------------------------------------------------------------------- 1 | mixin mousepad(name) 2 | label.mousepad 3 | input(data-text=name) 4 | span 5 | 6 | mixin modifier(code, mode) 7 | label.sticky 8 | input(type="checkbox" data-modifier=code data-mode=mode) 9 | span(data-modifier-name=code) 10 | 11 | //- see keyboard.modifiers 12 | mixin modifiers(mode) 13 | +modifier(1 << 10, mode) 14 | +modifier(1 << 11, mode) 15 | +modifier(1 << 12, mode) 16 | +modifier(1 << 13, mode) 17 | 18 | doctype html 19 | html(lang="en") 20 | head 21 | title CopyTables Options 22 | meta(charset="utf-8") 23 | meta(http-equiv="Content-type", content="text/html; charset=utf-8") 24 | meta(name="viewport", content="width=device-width, initial-scale=1") 25 | script(src="options.js") 26 | link(rel="stylesheet", type="text/css", href="options.css") 27 | 28 | body 29 | 30 | h1 CopyTables Options 31 | 32 | h2 Modifier Keys 33 | h3 34 | | Configure modifier keys for click-and-drag selection. 35 | | Note: some combinations might conflict with your system setup. 36 | 37 | .box 38 | table 39 | tr 40 | td 41 | b To select cells, hold 42 | td(data-modifiers="cell") 43 | tr 44 | td 45 | b To select columns, hold 46 | td(data-modifiers="column") 47 | tr 48 | td 49 | b To select rows, hold 50 | td(data-modifiers="row") 51 | tr 52 | td 53 | b To select tables, hold 54 | td(data-modifiers="table") 55 | tr 56 | td 57 | b To extend a selection, hold 58 | td(data-modifiers="extend") 59 | 60 | 61 | h2 Capture Mode 62 | h3 63 | | In the capture mode, you press a hot key first and then select with a single click. 64 | 65 | .box 66 | table 67 | tr 68 | td: b Enabled 69 | td 70 | label.cb 71 | input(type="checkbox" data-bool="capture.enabled") 72 | span Yes 73 | tr 74 | td 75 | b Reset 76 | i Automatically turn the capture mode off after selecting 77 | td 78 | label.cb 79 | input(type="checkbox" data-bool="capture.reset") 80 | span Yes 81 | tr 82 | td: b Keyboard Shortcuts 83 | td Configure by going to about:addons → Tools → Manage Extension Shortcuts 84 | 85 | 86 | h2 Copy Formats 87 | h3 Select copy formats you want to use. "Default" format is used when you select "Copy" from the Firefox menu. 88 | 89 | .box#copy-formats 90 | 91 | h2 Infobox 92 | h3 Infobox shows useful information about your selection (sum/average of numeric values etc). 93 | 94 | .box 95 | table 96 | tr 97 | td: b Enabled 98 | td 99 | label.cb 100 | input(type="checkbox" data-bool="infobox.enabled") 101 | span Yes 102 | tr 103 | td 104 | b Sticky 105 | i Keep infobox on screen until closed 106 | td 107 | label.cb 108 | input(type="checkbox" data-bool="infobox.sticky") 109 | span Yes 110 | tr 111 | td: b Position 112 | td 113 | table.infobox-position 114 | tr 115 | td 116 | div.position0: span 117 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="0") 118 | td 119 | div.position3: span 120 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="3") 121 | td 122 | div.position1: span 123 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="1") 124 | td 125 | div.position2: span 126 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="2") 127 | 128 | h2 Numbers 129 | h3 Configure how CopyTables parses numbers. 130 | 131 | .box 132 | table 133 | tr 134 | td 135 | b Decimal point 136 | td 137 | input(type="text" data-text="number.decimal") 138 | tr 139 | td 140 | b Group separator 141 | td 142 | input(type="text" data-text="number.group") 143 | 144 | 145 | 146 | h2 Interface 147 | h3 Miscellaneous interface options. 148 | 149 | .box 150 | table 151 | tr 152 | td 153 | b Scroll speed 154 | i Values are from 1 (very slow scrolling) to 100 (very fast). 155 | td 156 | input(type="number" data-text="scroll.amount") 157 | tr 158 | td 159 | b Scroll acceleration 160 | i Values are from 0 (no acceleration) to 100 (max acceleration). 161 | td 162 | input(type="number" data-text="scroll.acceleration") 163 | 164 | 165 | h2 Support 166 | 167 | .box 168 | 169 | h4 170 | Contact me if you have questions or want to report a problem. 171 | 172 | h4 173 | a(href="https://github.com/nirantak/copytables") Source Code 174 | |  |  175 | | © (2021 - present) Nirantak Raghav 176 | -------------------------------------------------------------------------------- /src/options.sass: -------------------------------------------------------------------------------- 1 | @import "colors" 2 | 3 | html 4 | margin: 0 5 | padding: 0 6 | 7 | body 8 | padding: 30px 30px 70px 30px 9 | 10 | body, td, th 11 | font-family: Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif 12 | font-size: 12px 13 | color: $body 14 | 15 | h1 16 | font-size: 24px 17 | background: url('ico48.png') no-repeat 18 | font-weight: 300 19 | padding: 10px 0 10px 68px 20 | margin: 0 21 | 22 | h2 23 | color: $hot 24 | margin: 36px 0 6px 70px 25 | font-size: 18px 26 | font-weight: 300 27 | 28 | h3 29 | margin: 0 0 0 70px 30 | font-size: 12px 31 | font-weight: 300 32 | max-width: 600px 33 | line-height: 140% 34 | 35 | h4 36 | margin: 0 0 12px 0 37 | font-size: 12px 38 | font-weight: 300 39 | max-width: 600px 40 | line-height: 140% 41 | 42 | .box 43 | padding: 12px 0 0 100px 44 | 45 | a 46 | color: $accent 47 | cursor: pointer 48 | 49 | p 50 | margin: 6px 0 51 | 52 | table 53 | border-collapse: collapse 54 | 55 | td 56 | margin: 0 57 | padding: 12px 58 | font-weight: 400 59 | text-align: left 60 | 61 | td b 62 | padding-right: 12px 63 | font-weight: 600 64 | white-space: nowrap 65 | 66 | td u 67 | margin: 0 6px 68 | font-weight: 800 69 | text-decoration: none 70 | background: $link 71 | color: white 72 | padding: 0 4px 73 | border-radius: 8px 74 | 75 | td i 76 | display: block 77 | font-size: 11px 78 | max-width: 220px 79 | padding: 0 12px 0 0 80 | 81 | td:first-child 82 | width: 200px 83 | 84 | tr 85 | border-bottom: 1px solid $lite 86 | 87 | tr tr 88 | border-bottom: none 89 | 90 | .sticky 91 | margin-right: 6px 92 | user-select: none 93 | font-size: 11px 94 | 95 | span 96 | background: white 97 | color: $link 98 | border: 1px solid $link 99 | border-radius: 8px 100 | padding: 4px 8px 101 | cursor: pointer 102 | white-space: nowrap 103 | 104 | &:hover span 105 | color: $accent 106 | border: 1px solid $accent 107 | 108 | input:checked + span 109 | color: white 110 | background: $accent 111 | border: 1px solid $accent 112 | 113 | input 114 | display: none 115 | 116 | .mousepad 117 | display: inline-block 118 | margin-left: 6px 119 | text-align: center 120 | width: 80px 121 | user-select: none 122 | font-size: 11px 123 | background: $accent 124 | border: 1px solid $accent 125 | color: white 126 | border-radius: 8px 127 | padding: 5px 5px 128 | transition: all 0.2s ease 129 | 130 | span 131 | cursor: pointer 132 | white-space: nowrap 133 | 134 | 135 | &:hover 136 | background-color: black 137 | 138 | input 139 | display: none 140 | 141 | 142 | .cb 143 | display: block 144 | span 145 | visibility: hidden 146 | padding-left: 4px 147 | 148 | input:checked + span 149 | color: $accent 150 | visibility: visible 151 | 152 | .infobox-position 153 | td 154 | text-align: center 155 | 156 | td:first-child 157 | width: auto 158 | 159 | div 160 | width: 50px 161 | height: 30px 162 | border: 1px solid $accent 163 | position: relative 164 | margin-bottom: 5px 165 | 166 | span 167 | 168 | width: 25px 169 | height: 8px 170 | background: $accent 171 | position: absolute 172 | 173 | &.position0 span 174 | left: 1px 175 | top: 1px 176 | 177 | &.position1 span 178 | right: 1px 179 | top: 1px 180 | 181 | &.position2 span 182 | right: 1px 183 | bottom: 1px 184 | 185 | &.position3 span 186 | left: 1px 187 | bottom: 1px 188 | 189 | #custom-code 190 | width: 400px 191 | height: 200px 192 | font-family: "Monaco", "UbuntuMono", monospace 193 | font-size: 11px 194 | border: 1px solid $border 195 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | var dom = require("./lib/dom"), 2 | preferences = require("./lib/preferences"), 3 | message = require("./lib/message"), 4 | event = require("./lib/event"), 5 | util = require("./lib/util"); 6 | 7 | function captureButtons() { 8 | var mode = preferences.val("_captureMode") || "zzz"; 9 | 10 | return preferences 11 | .captureModes() 12 | .map(function (m) { 13 | return util.format( 14 | '', 15 | { 16 | id: m.id, 17 | name: m.name, 18 | cls: m.id === mode ? "on" : "", 19 | } 20 | ); 21 | }) 22 | .join(""); 23 | } 24 | 25 | function copyButtons() { 26 | return preferences 27 | .copyFormats() 28 | .filter(function (f) { 29 | return f.enabled; 30 | }) 31 | .map(function (f) { 32 | return util.format( 33 | '', 34 | f 35 | ); 36 | }) 37 | .join(""); 38 | } 39 | 40 | function update() { 41 | dom.findOne("#copy-buttons").innerHTML = copyButtons(); 42 | 43 | if (preferences.val("capture.enabled")) { 44 | dom.findOne("#capture-row").style.display = ""; 45 | dom.findOne("#capture-buttons").innerHTML = captureButtons(); 46 | } else { 47 | dom.findOne("#capture-row").style.display = "none"; 48 | } 49 | } 50 | 51 | function init() { 52 | update(); 53 | 54 | event.listen(document, { 55 | click: function (e) { 56 | var cmd = dom.attr(e.target, "data-command"); 57 | if (cmd) { 58 | message.background({ name: "command", command: cmd }); 59 | } 60 | if (!dom.attr(e.target, "data-keep-open")) { 61 | window.close(); 62 | } 63 | }, 64 | }); 65 | } 66 | 67 | window.onload = function () { 68 | preferences.load().then(init); 69 | }; 70 | -------------------------------------------------------------------------------- /src/popup.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | script(src="popup.js") 6 | link(rel="stylesheet", type="text/css", href="popup.css") 7 | 8 | body 9 | table 10 | tr 11 | td 12 | b Copy: 13 | td 14 | span#copy-buttons 15 | 16 | tr 17 | td 18 | b Find: 19 | td 20 | button(data-command="find_previous" data-keep-open="1") Previous Table 21 | button(data-command="find_next" data-keep-open="1") Next Table 22 | 23 | tr#capture-row 24 | td 25 | b Capture: 26 | td 27 | span#capture-buttons 28 | 29 | div 30 | p 31 | a(data-command="open_options") Options 32 | -------------------------------------------------------------------------------- /src/popup.sass: -------------------------------------------------------------------------------- 1 | @import "colors" 2 | 3 | html 4 | margin: 0 5 | padding: 0 6 | 7 | body 8 | padding: 0 9 | min-width: 390px 10 | max-width: 600px 11 | 12 | body, td, th, button 13 | font-family: Arial, sans-serif 14 | font-size: 11px !important 15 | color: $body 16 | 17 | table 18 | width: 100% 19 | 20 | td 21 | margin: 0 22 | padding: 12px 23 | font-weight: 400 24 | text-align: left 25 | line-height: 260% 26 | border-bottom: 1px solid $border 27 | 28 | td b 29 | font-weight: 600 30 | white-space: nowrap 31 | 32 | a 33 | color: $link 34 | cursor: pointer 35 | border-bottom: 1px dotted $accent 36 | 37 | &:hover 38 | color: $accent 39 | 40 | button 41 | outline : none 42 | background: $button 43 | border: none 44 | border-radius: 6px 45 | padding: 4px 8px 46 | margin: 0 6px 47 | cursor: pointer 48 | transition: all 0.2s ease 49 | 50 | &.on, &:hover 51 | background: $accent 52 | color: white 53 | 54 | div 55 | padding: 0 0 12px 12px 56 | -------------------------------------------------------------------------------- /test/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "./test", 3 | "spec_files": ["./*_test.js"], 4 | "helpers": ["helpers/**/*.js"], 5 | "stopSpecOnExpectationFailure": false, 6 | "random": true 7 | } 8 | -------------------------------------------------------------------------------- /test/number_test.js: -------------------------------------------------------------------------------- 1 | var number = require("../src/lib/number"); 2 | 3 | var formats = { 4 | en: { group: ",", decimal: "." }, 5 | de: { group: ".", decimal: "," }, 6 | cz: { group: " ", decimal: "," }, 7 | }; 8 | 9 | describe("parse returns null", function () { 10 | it("with no input", () => expect(number.parse("", formats.en)).toBe(null)); 11 | 12 | it("with no digits", () => 13 | expect(number.parse("abc", formats.en)).toBe(null)); 14 | 15 | it("with invalid chars", () => 16 | expect(number.parse("12@34", formats.en)).toBe(null)); 17 | 18 | it("with just decimal point", () => 19 | expect(number.parse(".", formats.en)).toBe(null)); 20 | 21 | it("with two decimal points", () => 22 | expect(number.parse("123.123.456", formats.en)).toBe(null)); 23 | 24 | it("with a non-integer fraction", () => 25 | expect(number.parse("123.123,456", formats.en)).toBe(null)); 26 | 27 | it("with a group more than 3", () => 28 | expect(number.parse("123,1234", formats.en)).toBe(null)); 29 | 30 | it("with a group less than 2", () => 31 | expect(number.parse("123,1,234", formats.en)).toBe(null)); 32 | 33 | it("with an empty group", () => 34 | expect(number.parse("123,,234", formats.en)).toBe(null)); 35 | 36 | it("with overflow", () => 37 | expect(number.parse("234234234234234234234234234234234", formats.en)).toBe( 38 | null 39 | )); 40 | 41 | it("with decimal overflow", () => 42 | expect( 43 | number.parse("123.234234234234234234234234234234234", formats.en) 44 | ).toBe(null)); 45 | }); 46 | 47 | describe("extract returns null", function () { 48 | it("with no input", () => expect(number.extract("", formats.en)).toBe(null)); 49 | 50 | it("with no digits", () => 51 | expect(number.extract("abc", formats.en)).toBe(null)); 52 | 53 | it("with more than one number", () => 54 | expect(number.extract("abc 123 def 123.456", formats.en)).toBe(null)); 55 | 56 | it("with invalid number", () => 57 | expect(number.extract("abc 123.456.67", formats.en)).toBe(null)); 58 | }); 59 | 60 | describe("parse is ok", function () { 61 | it("with an integer", () => 62 | expect(number.parse("12345", formats.en)).toBe(12345)); 63 | it("with an negative integer", () => 64 | expect(number.parse("-12345", formats.en)).toBe(-12345)); 65 | it("with a decimal point", () => 66 | expect(number.parse("12345.678", formats.en)).toBe(12345.678)); 67 | it("with a void fraction", () => 68 | expect(number.parse("12345.", formats.en)).toBe(12345)); 69 | it("with a void fraction", () => 70 | expect(number.parse("12345.", formats.en)).toBe(12345)); 71 | it("with a void int part", () => 72 | expect(number.parse(".123", formats.en)).toBe(0.123)); 73 | it("with groups", () => 74 | expect(number.parse("1,23,456.789", formats.en)).toBe(123456.789)); 75 | it("with de groups", () => 76 | expect(number.parse("1.23.456,789", formats.de)).toBe(123456.789)); 77 | it("with cz groups", () => 78 | expect(number.parse("1 23 456,789", formats.cz)).toBe(123456.789)); 79 | 80 | it("with long decimal points", () => 81 | expect(number.parse("552.123", formats.en)).toBe(552.123)); 82 | it("with leading zero decimal points", () => 83 | expect(number.parse("552.005", formats.en)).toBe(552.005)); 84 | it("with zero decimal points", () => 85 | expect(number.parse("552.000", formats.en)).toBe(552)); 86 | }); 87 | 88 | describe("extract is ok", function () { 89 | it("with an integer", () => 90 | expect(number.extract("abc 12345 def", formats.en)).toBe(12345)); 91 | it("with a negative integer", () => 92 | expect(number.extract("abc -12345 def", formats.en)).toBe(-12345)); 93 | it("with en groups and decimals", () => 94 | expect(number.extract("abc -1,345,678.9 def", formats.en)).toBe( 95 | -1345678.9 96 | )); 97 | it("with de groups and decimals", () => 98 | expect(number.extract("abc -1.345.678,9 def", formats.de)).toBe( 99 | -1345678.9 100 | )); 101 | it("with cz groups and decimals", () => 102 | expect(number.extract("abc -1 345 678,9 def", formats.cz)).toBe( 103 | -1345678.9 104 | )); 105 | }); 106 | -------------------------------------------------------------------------------- /test/server/base.html: -------------------------------------------------------------------------------- 1 |

CopyTables Test Page

2 | 3 | 23 | 24 | 32 | 33 |
34 | 35 | 36 | {{{content}}} 37 | -------------------------------------------------------------------------------- /test/server/frame.html: -------------------------------------------------------------------------------- 1 | 16 | 24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /test/server/index.js: -------------------------------------------------------------------------------- 1 | let fs = require("fs"), 2 | express = require("express"), 3 | mustache = require("mustache"), 4 | glob = require("glob"), 5 | app = express(); 6 | 7 | Number.prototype.times = function (fn) { 8 | let a = []; 9 | for (let i = 0; i < Number(this); i++) a.push(fn(i)); 10 | return a; 11 | }; 12 | 13 | let file = (path) => fs.readFileSync(path, "utf8"); 14 | 15 | let renderHelpers = { 16 | textSource() { 17 | return ` 18 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 19 | Fusce et mauris eu arcu auctor imperdiet. 20 | Vivamus nec ex vitae libero ultrices vulputate eu non leo. 21 | In a mi a velit cursus mattis. 22 | Nam placerat enim quis ex euismod, quis dictum quam congue. 23 | Proin ut ante ut odio imperdiet semper ac non est. 24 | Morbi ac lectus dictum, vestibulum odio vitae, tempus risus. 25 | Fusce hendrerit felis sed nulla rhoncus fermentum. 26 | Duis dignissim sapien feugiat, cursus nulla non, vestibulum tellus. 27 | Ut commodo mauris quis dapibus venenatis. 28 | Duis iaculis mi quis massa dictum, sit amet placerat nisi ultrices. 29 | Sed mattis purus a est pharetra lacinia. 30 | Integer malesuada nisi in sodales viverra. 31 | Aliquam maximus risus ac ipsum sagittis aliquam. 32 | Duis consequat erat quis euismod fermentum. 33 | Suspendisse vitae augue id augue faucibus maximus. 34 | Vivamus nec erat rutrum, mollis ex nec, pretium mi. 35 | Vestibulum porta justo eu lorem eleifend laoreet. 36 | Cras maximus nisi et urna condimentum, a tincidunt dui fermentum. 37 | Maecenas at diam in ipsum convallis ultrices. 38 | Curabitur aliquam enim vitae neque rutrum, at rhoncus sem congue. 39 | Maecenas et ex ac lorem tempus accumsan et quis est. 40 | Nullam id ex porttitor, efficitur leo nec, maximus augue. 41 | Vivamus non sapien eu nisi commodo dignissim a at turpis. 42 | Vivamus a dolor eu odio tempor finibus id et massa. 43 | Donec finibus tellus sed dui gravida maximus. 44 | Donec malesuada ante vel sem dignissim viverra ac non nisi. 45 | Nunc euismod justo eget urna luctus dictum. 46 | Nulla sed erat tempor, lacinia massa ac, lacinia nisl. 47 | Praesent tempor tellus at velit egestas, et tincidunt lacus bibendum. 48 | `; 49 | }, 50 | 51 | text() { 52 | let text = this.textSource(); 53 | let a = Math.floor(Math.random() * text.length); 54 | let b = Math.floor(Math.random() * text.length); 55 | 56 | return "

" + text.substr(a, b) + "

"; 57 | }, 58 | 59 | numTable(rows, cols) { 60 | let s = ""; 61 | rows.times(function (r) { 62 | s += ""; 63 | cols.times(function (c) { 64 | s += `${r + 1}.${c + 1}`; 65 | }); 66 | s += ""; 67 | }); 68 | return `${s}
`; 69 | }, 70 | }; 71 | 72 | let renderTemplate = (tpl) => { 73 | let path; 74 | 75 | path = `${__dirname}/templates/${tpl}.html`; 76 | if (fs.existsSync(path)) { 77 | return mustache.render(file(path), { 78 | text: renderHelpers.text(), 79 | }); 80 | } 81 | 82 | path = `${__dirname}/templates/${tpl}.js`; 83 | if (fs.existsSync(path)) { 84 | return require(path).render(renderHelpers); 85 | } 86 | 87 | return `${tpl}=404`; 88 | }; 89 | 90 | let renderDoc = (content) => 91 | mustache.render(file(`${__dirname}/base.html`), { content: content }); 92 | 93 | // 94 | 95 | app.use(express.static(__dirname + "/public")); 96 | 97 | app.get("/only/:tpl", (req, res, next) => { 98 | let content = req.params.tpl.split(",").map(renderTemplate).join(""); 99 | res.send(renderDoc(content)); 100 | }); 101 | 102 | app.get("/base", (req, res, next) => { 103 | res.send(renderDoc("")); 104 | }); 105 | 106 | app.get("/frame", (req, res, next) => { 107 | res.send(file(`${__dirname}/frame.html`)); 108 | }); 109 | 110 | app.get("/raw/:tpl", (req, res, next) => { 111 | let content = req.params.tpl.split(",").map(renderTemplate).join(""); 112 | res.send(content); 113 | }); 114 | 115 | app.get("/all", (req, res, next) => { 116 | let content = "", 117 | all = 118 | "simple spans numbers forms hidden framea nested scroll frameb styled frameset dynamic"; 119 | 120 | all.split(" ").forEach((tpl) => { 121 | content += `

${tpl}

`; 122 | content += renderTemplate(tpl); 123 | content += renderHelpers.text(); 124 | }); 125 | 126 | res.send(renderDoc(content)); 127 | }); 128 | 129 | app.listen(9876); 130 | -------------------------------------------------------------------------------- /test/server/public/img/a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/test/server/public/img/a.png -------------------------------------------------------------------------------- /test/server/public/img/b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/test/server/public/img/b.png -------------------------------------------------------------------------------- /test/server/public/img/c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nirantak/copytables/dc564018033582e3a8a9e687bf417aa38a1d79ae/test/server/public/img/c.png -------------------------------------------------------------------------------- /test/server/templates/big.js: -------------------------------------------------------------------------------- 1 | module.exports.render = (h) => { 2 | function bgcolor(n) { 3 | n = ((n * 123456789) % 0xffffff).toString(16); 4 | while (n.length < 6) n = "0" + n; 5 | return `style="background-color:#${n}"`; 6 | } 7 | 8 | function chunk(n) { 9 | n = n % text.length; 10 | var a = text.substr(n + 0, n + 20); 11 | var b = text.substr(n + 20, n + 40); 12 | var c = text.substr(n + 40, n + 60); 13 | var d = text.substr(n + 60, n + 80); 14 | 15 | a = `${a}${b}${c}${d}`; 16 | return a + " " + a + " " + a; 17 | } 18 | 19 | function num(n, m) { 20 | return (n & 0xffff) * m; 21 | } 22 | 23 | let rc = 500, 24 | rows = [], 25 | text = h.textSource(); 26 | 27 | rc.times(function (i) { 28 | let cells = []; 29 | 30 | let k = Math.pow(7, i % 300) % 123456; 31 | 32 | cells.push(`${i}`); 33 | cells.push(`${k}`); 34 | cells.push( 35 | `${num(k, 123456)},${num(k, 123456)},${num( 36 | k, 37 | 123456 38 | )},${num(k, 123456)},${num(k, 123456)}` 39 | ); 40 | cells.push(`${chunk(k)}`); 41 | cells.push(`${chunk(k + 10)}`); 42 | cells.push(`${num(k, 9876)}`); 43 | 44 | rows.push(`${cells.join("")}`); 45 | }); 46 | 47 | return ` 48 | 58 |
59 | ${rows.join("")}
60 |
61 | `; 62 | }; 63 | -------------------------------------------------------------------------------- /test/server/templates/dynamic.html: -------------------------------------------------------------------------------- 1 | 47 | 48 | 58 | 59 |
60 |

61 | 62 |

63 |
64 | -------------------------------------------------------------------------------- /test/server/templates/forms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 15 | 19 | 23 | 31 | 32 |
4 | 5 | {{{text}}} 6 | 8 | 9 | {{{text}}} 10 | 12 | 13 | {{{text}}} 14 | 16 | 17 | {{{text}}} 18 | 20 | hello 21 | {{{text}}} 22 | 24 | 29 | {{{text}}} 30 |
33 | -------------------------------------------------------------------------------- /test/server/templates/framea.html: -------------------------------------------------------------------------------- 1 | 2 | {{{text}}} 3 | 4 | -------------------------------------------------------------------------------- /test/server/templates/frameb.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/server/templates/frameset.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/server/templates/frameset_content.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /test/server/templates/hidden.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
HIDDEN1239999999456111111
8 | 9 |
10 | 11 | HIDDEN TABLE: 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
HIDDEN1239999999456111111
20 | 21 | {{{text}}} NORMAL TABLE: 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
HIDDEN1239999999456111111
30 | -------------------------------------------------------------------------------- /test/server/templates/nested.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 38 | 39 | 40 | 41 | 42 |
one 5 | 6 | 7 | 8 | 15 | 16 |
two 9 | 10 | 11 | 12 | 13 |
three
14 |
17 |
four
one 25 | 26 | 27 | 28 | 35 | 36 |
two 29 | 30 | 31 | 32 | 33 |
three
34 |
37 |
four
43 | 44 | {{{text}}} 45 | -------------------------------------------------------------------------------- /test/server/templates/numbers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
1
1.07
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
#Dateprice
304/12/2016$0.11
1004/14/2016$0.22
504/19/2016$0.33
1004/19/2016$0.44
204/20/2016$0.55
204/20/2016$0.66
-56704/19/2016$0.77
56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
#price
3-0,3
30,341
3-1.245,576
3-1.245.678,576
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 |
#price
3-0,3
3-1 245,576
3-1 245 678,576
106 | 107 | {{{text}}} 108 | -------------------------------------------------------------------------------- /test/server/templates/paste.html: -------------------------------------------------------------------------------- 1 | 14 | 15 | 29 | 30 |
31 |
32 | 33 | 34 | {{{content}}} 35 | -------------------------------------------------------------------------------- /test/server/templates/script.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 20 | 23 | 24 |
4 | ONE: 5 | 8 | :END 9 | 11 | 12 | 17 | TWO: ??? :END 18 | 19 | 21 | three 22 |
25 | -------------------------------------------------------------------------------- /test/server/templates/scroll.js: -------------------------------------------------------------------------------- 1 | module.exports.render = (h) => ` 2 | 15 | 16 |
17 | ${h.numTable(20, 30)} 18 |
19 | 20 | ${h.text()} 21 | `; 22 | -------------------------------------------------------------------------------- /test/server/templates/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 |
helloworldlink relative
row2cell 2.2link relative 2
image a 16 | link absolute 20 |
image b
image c
32 | 35 | external img
39 | -------------------------------------------------------------------------------- /test/server/templates/spans.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
ServerOptionStatusDate
Before updateAfter update
alphaHDDokfailed2015/01/01
SSDok2015/01/02
betaHDDfailedok2015/01/03
SSDwait2015/01/04
no data2015/01/05
2015/01/06
61 | 62 | {{{text}}} 63 | -------------------------------------------------------------------------------- /test/server/templates/styled.html: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 53 | 54 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
default styleclass 1 styleclass 2 styleinline style
51 | class 2 + inline 52 | external background 59 | external background - inline 60 |
td animated
tr animatedtr animatedtr animatedtr animated
TD = NOT SELECTABLETD = NOT SELECTABLE
76 | 77 | 78 | 79 | 80 | 81 | 82 |
TABLE = NOT SELECTABLETABLE = NOT SELECTABLE
83 | 84 | {{{text}}} 85 | --------------------------------------------------------------------------------