├── .gitignore
├── LICENSE
├── README.md
├── convert-logs.js
├── gulpfile.js
├── package-lock.json
├── package.json
├── read-log.js
├── reload-chrome.js
├── src
├── 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
├── start-dev.sh
└── 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
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | app
4 | npm-debug.log
5 | *.zip
6 | ._*
7 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014~present Georg Barikin
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 |
4 | 
5 |
6 | Chrome extension to select and copy table cells.
7 |
8 | [Install from Chrome Web Store](https://chrome.google.com/webstore/detail/copytables/ekdpkppgmlalfkphpibadldikjimijon)
9 |
10 | * Hold Alt and drag to select cells.
11 | * Hold Alt-Ctrl and drag to select columns.
12 | * Copy selection (or the whole table) as seen on the screen (for Word)
13 | * Copy as CSV or tab-delimited text (for Excel).
14 | * Copy as HTML (for your website).
15 |
16 | ### New in version 0.5
17 |
18 | * Configurable hotkeys for cell, column, row and table selection.
19 | * Capture mode: select with simple click and drag.
20 | * Full support of framed documents.
21 | * Popup information about selected cells.
22 | * Swap (transpose) copy.
23 |
24 | ### New in version 0.4
25 |
26 | * New popup menu.
27 | * Table search function.
28 | * Configurable hotkey (Ctrl/Alt).
29 |
30 | ### New in version 0.3
31 |
32 | * Styled HTML export.
33 |
34 | ### New in version 0.2
35 |
36 | * CSV export.
37 | * Better HTML export.
38 | * Better support for weird table backgrounds (thanks [apptaro](https://github.com/apptaro)).
39 | * Overall speed improvement.
40 |
--------------------------------------------------------------------------------
/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.call(x).replace('[object ', '').replace(']', '');
11 | } catch (e) {
12 | return '';
13 | }
14 | }
15 |
16 | function props(x, depth) {
17 | var r = Array.isArray(x) ? [] : {};
18 | try {
19 | Object.keys(x).forEach(function (k) {
20 | r[k] = inspect(x[k], depth + 1);
21 | });
22 | return r;
23 | } catch (e) {
24 | return 'error';
25 | }
26 | }
27 |
28 | function inspect(x, depth) {
29 | if (depth > 5)
30 | return '...';
31 | if (typeof x !== 'object' || x === null)
32 | return x;
33 | var t = type(x),
34 | p = props(x, depth);
35 | if (t === 'Object' || t === 'Array')
36 | 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 |
52 |
53 | function handleLogging(content, path, isDev) {
54 |
55 | var dbg = String(__dbg);
56 | var out = [];
57 |
58 | if (isDev) {
59 | out.push(dbg.replace(/\s+/g, ' '));
60 | }
61 |
62 | content.split('\n').forEach(function (line, lnum) {
63 | var m = line.match(/(.*?)console\.log\((.*)\)(.*)/);
64 |
65 | if (m) {
66 | if (isDev) {
67 | out.push([
68 | m[1],
69 | 'console.log(__dbg(',
70 | JSON.stringify(path),
71 | ',' + (lnum + 1),
72 | ',' + m[2] + '))',
73 | m[3]
74 | ].join(''));
75 | }
76 | } else {
77 | if (isDev && line.match(/function/)) {
78 | out.push(' // ' + path + ':' + (lnum + 1));
79 | }
80 | out.push(line);
81 | }
82 | });
83 |
84 | return out.join('\n');
85 | }
86 |
87 |
88 | module.exports = function (content) {
89 | return handleLogging(content, this.resourcePath, this.query == '?dev')
90 | }
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var
4 | clip = require('gulp-clip-empty-files'),
5 | cp = require('child_process'),
6 | del = require('del'),
7 | fs = require('fs'),
8 | gulp = require('gulp'),
9 | named = require('vinyl-named'),
10 | pug = require('gulp-pug'),
11 | path = require('path'),
12 | rename = require('gulp-rename'),
13 | sass = require('gulp-sass'),
14 | sassTypes = require('node-sass').types,
15 | webpack = require('webpack-stream'),
16 | uglify = require('gulp-uglify'),
17 | util = require('gulp-util')
18 | ;
19 |
20 | const DEST = './app';
21 | const TEST_URL = 'http://localhost:9876/all';
22 | const IS_DEV = process.env.NODE_ENV === 'dev';
23 |
24 | // based on https://coderwall.com/p/fhgu_q/inlining-images-with-gulp-sass
25 | function sassInlineImage(file) {
26 | var
27 | filePath = path.resolve(process.cwd(), file.getValue()),
28 | ext = filePath.split('.').pop(),
29 | data = fs.readFileSync(filePath),
30 | buffer = new Buffer(data),
31 | str = '"data:image/' + ext + ';base64,' + buffer.toString('base64') + '"';
32 | return sassTypes.String(str);
33 | }
34 |
35 | const webpackConfig = {
36 | devtool: null,
37 | module: {
38 | preLoaders: [
39 | {
40 | test: /\.js$/,
41 | loader: __dirname + '/convert-logs?' + (IS_DEV ? 'dev' : '')
42 | }
43 | ]
44 | }
45 | };
46 |
47 | gulp.task('js', function () {
48 | return gulp.src('./src/*.js')
49 | .pipe(named())
50 | .pipe(webpack(webpackConfig))
51 | .pipe(IS_DEV ? util.noop() : uglify())
52 | .pipe(gulp.dest(DEST))
53 | ;
54 | });
55 |
56 | gulp.task('sass', function () {
57 | return gulp.src('./src/*.sass')
58 | .pipe(sass({
59 | functions: {
60 | 'inline-image($file)': sassInlineImage
61 | }
62 | }))
63 | .pipe(clip())
64 | .pipe(gulp.dest(DEST))
65 | ;
66 | });
67 |
68 | gulp.task('pug', function () {
69 | return gulp.src('./src/*.pug')
70 | .pipe(pug())
71 | .pipe(gulp.dest(DEST))
72 | ;
73 | });
74 |
75 | gulp.task('copy', function () {
76 | return gulp.src('./src/*.{png,css,json,svg}')
77 | .pipe(gulp.dest(DEST))
78 | ;
79 | });
80 |
81 | gulp.task('clean', function () {
82 | return del.sync(DEST);
83 | });
84 |
85 | gulp.task('make', ['clean', 'pug', 'sass', 'copy', 'js']);
86 |
87 | gulp.task('reload', ['make'], function () {
88 | cp.execSync('osascript -l JavaScript ./reload-chrome.js');
89 | });
90 |
91 | gulp.task('watch', ['reload'], function () {
92 | return gulp.watch('./src/**/*', ['reload']);
93 | });
94 |
95 | gulp.task('deploy', ['make'], function () {
96 | var m = require('./src/manifest.json'),
97 | fn = 'copytables_' + m.version.replace(/\./g, '_') + '.zip';
98 | cp.execSync('rm -f ./' + fn + ' && zip -j ./' + fn + ' ./app/*');
99 | });
100 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "copytables",
3 | "version": "0.5.10",
4 | "description": "Chrome extension to select and copy table cells.",
5 | "scripts": {
6 | "chrome": "'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --enable-logging=stderr --v=1 2>&1 | node read-log.js",
7 | "dev": "NODE_ENV=dev gulp make",
8 | "watch": "NODE_ENV=dev gulp watch --url",
9 | "dev-server": "node ./test/server/index.js",
10 | "production": "NODE_ENV=production gulp make",
11 | "deploy": "NODE_ENV=production gulp deploy",
12 | "test": "jasmine --config=./test/jasmine.json"
13 |
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/gebrkn/copytables.git"
18 | },
19 | "author": "Georg Barikin",
20 | "license": "MIT",
21 | "bugs": {
22 | "url": "https://github.com/gebrkn/copytables/issues"
23 | },
24 | "homepage": "https://github.com/gebrkn/copytables#readme",
25 | "devDependencies": {
26 | "del": "^2.2.2",
27 | "express": "^4.15.2",
28 | "glob": "^7.1.1",
29 | "gulp": "^3.9.1",
30 | "gulp-clip-empty-files": "^0.1.2",
31 | "gulp-pug": "^3.3.0",
32 | "gulp-rename": "^1.2.2",
33 | "gulp-sass": "^3.1.0",
34 | "gulp-uglify": "^2.1.0",
35 | "gulp-util": "^3.0.8",
36 | "jasmine": "^3.3.1",
37 | "mustache": "^2.3.0",
38 | "vinyl-named": "^1.1.0",
39 | "webpack-stream": "^3.2.0"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/read-log.js:
--------------------------------------------------------------------------------
1 | var readline = require('readline');
2 |
3 | function proc(line) {
4 | var m = line.match(/@@CT<<(.+?)>>CT@@/);
5 | if (!m)
6 | return '';
7 | var data = JSON.parse(m[1]);
8 |
9 | var url = data.shift();
10 |
11 | if (url.match(/^chrome-extension/))
12 | url = '';
13 |
14 | var file = data.shift();
15 | var line = data.shift();
16 |
17 | var prefix = file.split('src/')[1].split('.')[0] + ":" + line + " ";
18 |
19 | var s = data.map(function(x) {
20 | return JSON.stringify(x)
21 | }).join(' ');
22 |
23 | if(url)
24 | s += ' ' + url;
25 |
26 | console.log(prefix + s);
27 |
28 | }
29 |
30 | readline.createInterface({
31 | input: process.stdin,
32 | output: process.stdout,
33 | terminal: false
34 | }).on('line', proc);
35 |
--------------------------------------------------------------------------------
/reload-chrome.js:
--------------------------------------------------------------------------------
1 | /// Reload extensions....
2 | /// run me as `osascript -l JavaScript reload-chrome.js`
3 | // requires Extension Reloader https://chrome.google.com/webstore/detail/extensions-reloader/fimgfedafeadlieiabdeeaodndnlbhid
4 |
5 | function run(argv) {
6 | var app = Application('Google Chrome'),
7 | extUrl = 'http://reload.extensions',
8 | ctxUrl = argv;
9 |
10 | function each(a, fn) {
11 | for (var i = 0; i < a.length; i++)
12 | fn(a[i], i);
13 | }
14 |
15 | function find(a, fn) {
16 | for (var i = 0; i < a.length; i++)
17 | if (fn(a[i], i))
18 | return a[i];
19 | return null;
20 | }
21 |
22 | function enumTabs() {
23 | var wts = [];
24 |
25 | each(app.windows, function (win) {
26 | each(win.tabs, function (tab, i) {
27 | wts.push({'window': win, 'tab': tab})
28 | });
29 | });
30 |
31 | return wts.filter(function (wt) {
32 | return !wt.window.title().match(/^Developer Tools/);
33 | });
34 | }
35 |
36 | function reload() {
37 | var wts = enumTabs();
38 |
39 | if (!wts.length) {
40 | console.log('no open tabs');
41 | var w = app.Window().make();
42 | w.tabs[0].url = extUrl;
43 | } else {
44 | wts[0].tab.url = extUrl;
45 | }
46 | }
47 |
48 | app.activate();
49 | reload();
50 | }
51 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | require('./background/index').main();
3 | })();
4 |
--------------------------------------------------------------------------------
/src/background/clipboard.js:
--------------------------------------------------------------------------------
1 | var M = module.exports = {};
2 |
3 | var
4 | dom = require('../lib/dom'),
5 | util = require('../lib/util')
6 | ;
7 |
8 |
9 | M.copyRich = function (text) {
10 | console.log(util.timeStart('copyRich'));
11 |
12 | var t = document.createElement('div');
13 | document.body.appendChild(t);
14 |
15 | t.contentEditable = true;
16 | t.innerHTML = text;
17 |
18 | dom.select(t);
19 | document.execCommand('copy');
20 | document.body.removeChild(t);
21 |
22 | console.log(util.timeEnd('copyRich'));
23 | };
24 |
25 | M.copyText = function (text) {
26 | console.log(util.timeStart('copyText'));
27 |
28 | var t = document.createElement('textarea');
29 | document.body.appendChild(t);
30 |
31 | t.value = text;
32 | t.focus();
33 | t.select();
34 |
35 | document.execCommand('copy');
36 | document.body.removeChild(t);
37 |
38 | console.log(util.timeEnd('copyText'));
39 | };
40 |
41 | M.content = function () {
42 | var cc = '',
43 | pasted = false;
44 |
45 | var pasteHandler = function (evt) {
46 | if (pasted)
47 | return;
48 | pasted = true;
49 | console.log('paste handler toggled');
50 | cc = evt.clipboardData.getData('text/html');
51 | evt.stopPropagation();
52 | evt.preventDefault();
53 | };
54 |
55 | document.addEventListener('paste', pasteHandler, true);
56 | console.log('exec paste');
57 | document.execCommand('paste');
58 | document.removeEventListener('paste', pasteHandler);
59 | return cc;
60 | };
--------------------------------------------------------------------------------
/src/background/commands.js:
--------------------------------------------------------------------------------
1 | var M = module.exports = {};
2 |
3 | var
4 | message = require('../lib/message'),
5 | preferences = require('../lib/preferences'),
6 | util = require('../lib/util'),
7 |
8 | menu = require('./menu'),
9 | copy = require('./copy'),
10 | helpers = require('./helpers')
11 | ;
12 |
13 | function findTableCommand(direction, sender) {
14 | console.log('findTableCommand', direction, sender);
15 |
16 | if (!sender) {
17 | return helpers.findTable(direction);
18 | }
19 |
20 | message.frame('tableIndexFromContextMenu', sender).then(function (res) {
21 | if (res.data !== null) {
22 | helpers.findTable(direction, {tabId: sender.tabId, frameId: sender.frameId, index: res.data});
23 | } else {
24 | helpers.findTable(direction);
25 | }
26 | })
27 | }
28 |
29 | function copyCommand(format, sender) {
30 | console.log(util.timeStart('copyCommand'));
31 |
32 | console.log('copyCommand', format, sender);
33 |
34 | var msg = {
35 | name: 'beginCopy',
36 | broadcast: !sender,
37 | options: copy.options(format)
38 | };
39 |
40 | var ok = true;
41 |
42 | function doCopy(r) {
43 | if (r.data) {
44 | ok = copy.exec(format, r.data);
45 | return true;
46 | }
47 | }
48 |
49 | if (sender) {
50 | message.frame(msg, sender).then(function(r) {
51 | doCopy(r);
52 | message.frame(ok ? 'endCopy' : 'endCopyFailed', sender);
53 | console.log(util.timeEnd('copyCommand'));
54 | });
55 | } else {
56 | message.allFrames(msg).then(function (res) {
57 | res.some(doCopy);
58 | message.allFrames(ok ? 'endCopy' : 'endCopyFailed');
59 | console.log(util.timeEnd('copyCommand'));
60 | });
61 | }
62 | }
63 |
64 | function captureCommand(mode) {
65 | if (mode === 'zzz') {
66 | mode = '';
67 | }
68 |
69 | var m = preferences.val('_captureMode');
70 | if (m === mode) {
71 | mode = '';
72 | }
73 |
74 | preferences.set('_captureMode', mode).then(function() {
75 | helpers.updateUI();
76 | message.allFrames('preferencesUpdated');
77 | });
78 | }
79 |
80 | function selectCommand(mode, sender) {
81 | if (sender) {
82 | message.frame({name: 'selectFromContextMenu', mode: mode}, sender);
83 | }
84 | }
85 |
86 | M.exec = function (cmd, sender) {
87 |
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.callChrome('runtime.openOptionsPage');
126 | case 'open_commands':
127 | return util.callChrome('tabs.create', {url: 'chrome://extensions/configureCommands'});
128 | }
129 | };
130 |
131 |
132 |
--------------------------------------------------------------------------------
/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 |
10 | function trimTextMatrix(mat) {
11 | mat = matrix.map(mat, function (row, cell) {
12 | return util.strip(util.nobr(cell));
13 | });
14 | return matrix.trim(mat, Boolean);
15 | }
16 |
17 | function asTabs(mat) {
18 | return trimTextMatrix(mat).map(function (row) {
19 | return row.join('\t')
20 | }).join('\n');
21 | }
22 |
23 | function asCSV(mat) {
24 | return trimTextMatrix(mat).map(function (row) {
25 | return row.map(function (cell) {
26 | if (cell.match(/^\w+$/) || cell.match(/^-?[0-9]+(\.[0-9]*)?$/))
27 | return cell;
28 | return '"' + cell.replace(/"/g, '""') + '"';
29 | }).join(',')
30 | }).join('\n');
31 | }
32 |
33 | function asTextile(mat, withHTML) {
34 | var rows = mat.map(function (row) {
35 |
36 | var cells = row
37 | .filter(function (node) {
38 | return node.td
39 | })
40 | .map(function (node) {
41 |
42 | var t = '', s = '';
43 |
44 | if (withHTML)
45 | t = dom.htmlContent(node.td);
46 | else
47 | t = dom.textContent(node.td);
48 |
49 | if (node.colSpan)
50 | s += '\\' + (node.colSpan + 1);
51 | if (node.rowSpan)
52 | s += '/' + (node.rowSpan + 1);
53 | if (s)
54 | s += '.';
55 |
56 | return '|' + s + ' ' + t.replace('|', '|');
57 |
58 | });
59 |
60 | return cells.join(' ') + ' |';
61 | });
62 |
63 | return rows.join('\n');
64 | }
65 |
66 |
67 | M.formats = {};
68 |
69 | M.formats.richHTMLCSS = {
70 | opts: {method: 'clipboard', withSelection: true, keepStyles: true, keepHidden: false},
71 | exec: function (t) {
72 | clipboard.copyRich(t.html())
73 | }
74 | };
75 |
76 | M.formats.richHTML = {
77 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
78 | exec: function (t) {
79 | clipboard.copyRich(t.html())
80 | }
81 | };
82 |
83 | M.formats.textTabs = {
84 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
85 | exec: function (t) {
86 | clipboard.copyText(asTabs(t.textMatrix()))
87 | }
88 | };
89 |
90 | M.formats.textTabsSwap = {
91 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
92 | exec: function (t) {
93 | clipboard.copyText(asTabs(matrix.transpose(t.textMatrix())))
94 | }
95 | };
96 |
97 | M.formats.textCSV = {
98 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
99 | exec: function (t) {
100 | clipboard.copyText(asCSV(t.textMatrix()))
101 | }
102 | };
103 |
104 | M.formats.textCSVSwap = {
105 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
106 | exec: function (t) {
107 | clipboard.copyText(asCSV(matrix.transpose(t.textMatrix())))
108 | }
109 | };
110 |
111 | M.formats.textHTML = {
112 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: true},
113 | exec: function (t) {
114 | clipboard.copyText(util.reduceWhitespace(t.html()))
115 | }
116 | };
117 |
118 | M.formats.textHTMLCSS = {
119 | opts: {method: 'clipboard', withSelection: true, keepStyles: true, keepHidden: true},
120 | exec: function (t) {
121 | clipboard.copyText(util.reduceWhitespace(t.html()))
122 | }
123 | };
124 |
125 | M.formats.textTextileHTML = {
126 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
127 | exec: function (t) {
128 | clipboard.copyText(asTextile(t.nodeMatrix(), true))
129 | }
130 | };
131 |
132 | M.formats.textTextile = {
133 | opts: {method: 'clipboard', withSelection: true, keepStyles: false, keepHidden: false},
134 | exec: function (t) {
135 | clipboard.copyText(asTextile(t.nodeMatrix(), false))
136 | }
137 | };
138 |
139 |
140 | //
141 |
142 | M.options = function (format) {
143 | return M.formats[format].opts;
144 | };
145 |
146 | M.exec = function (format, data) {
147 | var ok = false,
148 | t = new paste.table(),
149 | fmt = M.formats[format];
150 |
151 | if (t.init(data, fmt.opts)) {
152 | fmt.exec(t);
153 | ok = true;
154 | }
155 |
156 | t.destroy();
157 | return ok;
158 | };
159 |
--------------------------------------------------------------------------------
/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 | ;
8 |
9 | var badgeColor = '#1e88ff';
10 |
11 | M.updateUI = function () {
12 | preferences.load().then(function () {
13 | menu.create();
14 | M.updateBadge();
15 | });
16 | };
17 |
18 | M.setBadge = function (s) {
19 | util.callChrome('browserAction.setBadgeText', {text: s})
20 | util.callChrome('browserAction.setBadgeBackgroundColor', {color: badgeColor});
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((r.data || []).map(function (t) {
48 | t.frame = {
49 | tabId: r.receiver.tabId,
50 | frameId: r.receiver.frameId
51 | };
52 | return t;
53 | }));
54 | });
55 |
56 | return all.sort(function (a, b) {
57 | return a.frame.frameId - b.frame.frameId || a.index - b.index;
58 | });
59 | });
60 | };
61 |
62 | M.findTable = function (direction, start) {
63 |
64 |
65 | M.enumTables().then(function(allTables) {
66 |
67 | if (!allTables.length) {
68 | return;
69 | }
70 |
71 | var curr = -1;
72 |
73 | allTables.some(function (t, n) {
74 | if (start && t.frame.frameId == start.frameId && t.frame.tabId === start.tabId && t.index === start.index) {
75 | curr = n;
76 | return true;
77 | }
78 | if (!start && t.selected) {
79 | curr = n;
80 | return true;
81 | }
82 | });
83 |
84 | if (direction === +1) {
85 | if (curr === -1 || curr === allTables.length - 1)
86 | curr = 0;
87 | else
88 | curr += 1;
89 | } else {
90 | if (curr === -1 || curr === 0)
91 | curr = allTables.length - 1;
92 | else
93 | 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 |
9 | menu = require('./menu'),
10 | commands = require('./commands'),
11 | copy = require('./copy'),
12 | helpers = require('./helpers')
13 | ;
14 |
15 | var messageListeners = {
16 |
17 | dropAllSelections: function (msg) {
18 | message.allFrames('dropSelection');
19 | },
20 |
21 | dropOtherSelections: function (msg) {
22 | message.enumFrames('active').then(function (frames) {
23 | frames.forEach(function (frame) {
24 | if (frame.frameId !== msg.sender.frameId) {
25 | message.frame('dropSelection', frame);
26 | }
27 | });
28 | });
29 | },
30 |
31 | contextMenu: function (msg) {
32 | helpers.enumTables().then(function (ts) {
33 | menu.enable(['select_row', 'select_column', 'select_table'], msg.selectable);
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 |
55 | function init() {
56 | menu.create();
57 | message.listen(messageListeners);
58 |
59 | util.callChrome('contextMenus.onClicked.addListener', function (info, tab) {
60 | commands.exec(info.menuItemId, {tabId: tab.id, frameId: info.frameId});
61 | });
62 |
63 | util.callChrome('commands.onCommand.addListener', function (cmd) {
64 | commands.exec(cmd, null);
65 | });
66 |
67 | helpers.updateUI();
68 | }
69 |
70 | M.main = function () {
71 | preferences.load().then(init);
72 | };
73 |
--------------------------------------------------------------------------------
/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.callChrome('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.callChromeAsync('contextMenus.removeAll')
95 | .then(function () {
96 | createMenu(mainMenu);
97 | });
98 | };
99 |
100 | M.enable = function (ids, enabled) {
101 | ids.forEach(function (id) {
102 | util.callChrome('contextMenus.update', id, {enabled: enabled});
103 | if (id === 'copy') {
104 | var cf = preferences.copyFormats().filter(function (f) {
105 | return f.enabled;
106 | });
107 | cf.forEach(function (f) {
108 | util.callChrome('contextMenus.update', 'copy_' + f.id, {enabled: enabled});
109 | });
110 | }
111 | });
112 | };
113 |
--------------------------------------------------------------------------------
/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 |
11 | function toMatrix(tbl) {
12 | console.log(util.timeStart('toMatrix'));
13 |
14 | var tds = {},
15 | rows = {},
16 | cols = {};
17 |
18 | dom.cells(tbl).forEach(function (td) {
19 | var bounds = dom.bounds(td);
20 | var c = bounds.x, 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)
37 | return;
38 |
39 | var rs = parseInt(dom.attr(node.td, 'rowSpan')) || 1;
40 | var cs = parseInt(dom.attr(node.td, 'colSpan')) || 1;
41 |
42 | for (var i = 1; i < cs; i++) {
43 | if (row[ni + i] && !row[ni + i].td)
44 | row[ni + i].colRef = node;
45 | }
46 |
47 | for (var i = 1; i < rs; i++) {
48 | if (mat[ri + i] && mat[ri + i][ni] && !mat[ri + i][ni].td)
49 | mat[ri + i][ni].rowRef = node;
50 | }
51 | });
52 |
53 | console.log(util.timeEnd('toMatrix'));
54 |
55 | return mat;
56 | }
57 |
58 | function computeSpans(mat) {
59 | matrix.each(mat, function (_, node) {
60 | if (node.colRef)
61 | node.colRef.colSpan = (node.colRef.colSpan || 0) + 1;
62 | if (node.rowRef)
63 | node.rowRef.rowSpan = (node.rowRef.rowSpan || 0) + 1;
64 | });
65 | }
66 |
67 | function trim(tbl) {
68 | console.log(util.timeStart('trim.filter'));
69 |
70 | var mat = matrix.trim(toMatrix(tbl), function (node) {
71 | return cell.selected(node.td);
72 | });
73 |
74 | console.log(util.timeEnd('trim.filter'));
75 |
76 | console.log(util.timeStart('trim.remove'));
77 |
78 | var tds = [];
79 |
80 | matrix.each(mat, function (row, node) {
81 | if (node.td) {
82 | tds.push(node.td);
83 | }
84 | });
85 |
86 | var junk = [];
87 |
88 | dom.cells(tbl).forEach(function (td) {
89 | if (tds.indexOf(td) < 0)
90 | junk.push(td);
91 | else if (!cell.selected(td))
92 | td.innerHTML = '';
93 | });
94 |
95 | dom.remove(junk);
96 | junk = [];
97 |
98 | dom.find('tr', tbl).forEach(function (tr) {
99 | if (dom.find('td, th', tr).length === 0) {
100 | junk.push(tr);
101 | }
102 | });
103 |
104 | dom.remove(junk);
105 |
106 | console.log(util.timeEnd('trim.remove'));
107 |
108 | computeSpans(mat);
109 |
110 | matrix.each(mat, function (_, node) {
111 | if (node.td) {
112 | dom.attr(node.td, 'rowSpan', node.rowSpan ? (node.rowSpan + 1) : null);
113 | dom.attr(node.td, 'colSpan', node.colSpan ? (node.colSpan + 1) : null);
114 | }
115 | });
116 | }
117 |
118 | function fixRelativeLinks(where, fixCssUrls) {
119 |
120 | var aa = where.ownerDocument.createElement('A');
121 |
122 | function fixUrl(url) {
123 | // since we've set BASE url, this works
124 | aa.href = url;
125 | return aa.href;
126 | }
127 |
128 | function fixTags(tags, attrs) {
129 | dom.find(tags, where).forEach(function (t) {
130 | attrs.forEach(function (attr) {
131 | var v = dom.attr(t, attr);
132 | if (v) {
133 | dom.attr(t, attr, fixUrl(v));
134 | }
135 | });
136 | });
137 | }
138 |
139 |
140 | fixTags('A, AREA, LINK', ['href']);
141 | fixTags('IMG, INPUT, SCRIPT', ['src', 'longdesc', 'usemap']);
142 | fixTags('FORM', ['action']);
143 | fixTags('Q, BLOCKQUOTE, INS, DEL', ['cite']);
144 | fixTags('OBJECT', ['classid', 'codebase', 'data', 'usemap']);
145 |
146 | if (fixCssUrls) {
147 | dom.find('*', where).forEach(function (el) {
148 | var style = dom.attr(el, 'style');
149 |
150 | if (!style || style.toLowerCase().indexOf('url') < 0)
151 | return;
152 |
153 | var fixStyle = style.replace(/(\burl\s*\()([^()]+)/gi, function (_, pfx, url) {
154 | url = util.strip(url);
155 | if (url[0] === '"' || url[0] === '\'') {
156 | return pfx + url[0] + fixUrl(url.slice(1, -1)) + url[0];
157 | }
158 | return pfx + fixUrl(url);
159 | });
160 |
161 | if (fixStyle !== style)
162 | dom.attr(el, 'style', fixStyle);
163 | });
164 | }
165 | }
166 |
167 | function removeHiddenElements(node) {
168 | var hidden = [];
169 |
170 | dom.find('*', node).forEach(function (el) {
171 | if (!dom.visible(el)) {
172 | hidden.push(el);
173 | }
174 | });
175 |
176 | if (hidden.length) {
177 | console.log('removeHidden: ' + hidden.length);
178 | dom.remove(hidden);
179 | }
180 | }
181 | tcc = function (text) {
182 | console.log(util.timeStart('textCopy'));
183 |
184 | var t = document.createElement('textarea');
185 | document.body.appendChild(t);
186 |
187 | t.value = text;
188 | t.focus();
189 | t.select();
190 |
191 | document.execCommand('copy');
192 | document.body.removeChild(t);
193 |
194 | console.log(util.timeEnd('textCopy'));
195 | };
196 |
197 | M.table = function () {
198 | };
199 |
200 | M.table.prototype.init = function (data, options) {
201 | console.log(util.timeStart('paste.init'));
202 |
203 | this.frame = document.createElement('IFRAME');
204 | this.frame.setAttribute('sandbox', 'allow-same-origin');
205 | document.body.appendChild(this.frame);
206 |
207 | this.doc = this.frame.contentDocument;
208 | this.body = this.doc.body;
209 |
210 | var base = this.doc.createElement('BASE');
211 | dom.attr(base, 'href', data.url);
212 | this.body.appendChild(base);
213 |
214 | // some cells (e.g. containing an image) could become width=0
215 | // after paste, breaking toMatrix calculations
216 | var css = this.doc.createElement('STYLE');
217 | css.type = "text/css";
218 | css.innerHTML = 'td { min-width: 1px; }';
219 | this.body.appendChild(css);
220 |
221 | this.div = this.doc.createElement('DIV');
222 | this.div.contentEditable = true;
223 | this.body.appendChild(this.div);
224 |
225 | var ok = this.initTable(data, options);
226 |
227 | console.log('paste.init=' + ok + ' method=' + options.method);
228 | console.log(util.timeEnd('paste.init'));
229 |
230 | return ok;
231 | };
232 |
233 | M.table.prototype.initTable = function (data, options) {
234 |
235 |
236 | console.log(util.timeStart('paste.insertTable'));
237 |
238 | if (options.method === 'clipboard') {
239 | this.div.focus();
240 |
241 | // NB: just pasting the clipboard via `this.doc.execCommand('paste')`
242 | // is very slow for some reason. Instead, intercept paste
243 | // to obtain the clipboard and insert it via innerHTML which is waaay faster
244 |
245 | this.div.innerHTML = clipboard.content();
246 |
247 | // destroy the clipboard to avoid pasting of intermediate results
248 | // this doesn't really fix that because they can hit paste before
249 | // .content() finishes, but still...
250 | clipboard.copyText('');
251 | }
252 |
253 | if (options.method === 'transfer') {
254 | this.div.innerHTML = data.html;
255 | }
256 |
257 | console.log(util.timeEnd('paste.insertTable'));
258 |
259 | this.table = dom.findOne('table', this.div);
260 |
261 | if (!this.table || dom.tag(this.table) !== 'TABLE')
262 | return false;
263 |
264 | if (data.hasSelection) {
265 | console.log(util.timeStart('paste.trim'));
266 | trim(this.table);
267 | console.log(util.timeEnd('paste.trim'));
268 | }
269 |
270 | if (options.method === 'transfer' && options.keepStyles) {
271 | console.log(util.timeStart('paste.restoreStyles'));
272 |
273 | dom.findSelf('*', this.table).forEach(function (el) {
274 | var uid = dom.attr(el, 'data-copytables-uid');
275 | if (uid && el.style) {
276 | dom.removeAttr(el, 'style');
277 | el.style.cssText = css.compute(css.read(el), data.css[uid] || {});
278 | }
279 | dom.removeAttr(el, 'data-copytables-uid')
280 | });
281 |
282 | console.log(util.timeEnd('paste.restoreStyles'));
283 | }
284 |
285 | if (options.method === 'transfer' && !options.keepHidden) {
286 | console.log(util.timeStart('paste.removeHidden'));
287 | removeHiddenElements(this.div);
288 | console.log(util.timeEnd('paste.removeHidden'));
289 | }
290 |
291 | fixRelativeLinks(this.div, options.keepStyles);
292 | dom.cells(this.table).forEach(cell.reset);
293 |
294 | if (!options.keepStyles) {
295 | dom.findSelf('*', this.table).forEach(function (el) {
296 | dom.removeAttr(el, 'style');
297 | dom.removeAttr(el, 'class');
298 | });
299 | }
300 |
301 | return true;
302 | };
303 |
304 | M.table.prototype.html = function () {
305 | return this.table.outerHTML;
306 | };
307 |
308 | M.table.prototype.nodeMatrix = function () {
309 | var mat = toMatrix(this.table);
310 | computeSpans(mat);
311 | return mat;
312 | };
313 |
314 | M.table.prototype.textMatrix = function () {
315 | return matrix.map(toMatrix(this.table), function (_, node) {
316 | return dom.textContent(node.td);
317 | });
318 | };
319 |
320 |
321 | M.table.prototype.destroy = function () {
322 | if (this.frame)
323 | document.body.removeChild(this.frame);
324 | };
325 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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: Highlight $i
12 | transition: $no-trans
13 |
14 | [data-copytables-marked]:not([data-copytables-locked])
15 | background: Highlight $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 |
81 |
--------------------------------------------------------------------------------
/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 |
12 | infobox = require('./infobox'),
13 | table = require('./table'),
14 | scroller = require('./scroller')
15 | ;
16 |
17 | M.Capture = function () {
18 | this.anchorPoint = null;
19 | this.table = null;
20 | };
21 |
22 | M.Capture.prototype.markRect = function (evt) {
23 | var cx = evt.clientX,
24 | cy = evt.clientY;
25 |
26 |
27 | var p = this.scroller.adjustPoint(this.anchorPoint);
28 |
29 | var rect = [
30 | Math.min(cx, p.x),
31 | Math.min(cy, p.y),
32 | Math.max(cx, p.x),
33 | Math.max(cy, p.y)
34 | ];
35 |
36 | var big = 10e6;
37 |
38 | if (this.mode === 'column' || this.mode === 'table') {
39 | rect[1] = -big;
40 | rect[3] = +big;
41 | }
42 |
43 | if (this.mode === 'row' || this.mode === 'table') {
44 | rect[0] = -big;
45 | rect[2] = +big;
46 | }
47 |
48 | return rect;
49 | };
50 |
51 | M.Capture.prototype.setCaptured = function (rect) {
52 | dom.cells(this.table).forEach(function (td) {
53 | cell.unmark(td);
54 | if (util.intersect(dom.bounds(td).rect, rect)) {
55 | cell.mark(td);
56 | }
57 | });
58 | };
59 |
60 | M.Capture.prototype.setLocked = function (rect, canSelect) {
61 | dom.cells(this.table).forEach(function (td) {
62 | cell.unlock(td);
63 | if (util.intersect(dom.bounds(td).rect, rect)) {
64 | if (!canSelect) {
65 | cell.unselect(td);
66 | cell.lock(td);
67 | }
68 | }
69 | });
70 | };
71 |
72 | M.Capture.prototype.selection = function () {
73 | var self = this,
74 | tds = cell.findSelected(self.table);
75 |
76 | if (!self.selectedCells) {
77 | return [true, self.selectedCells = tds];
78 | }
79 |
80 | if (tds.length !== self.selectedCells.length) {
81 | return [true, self.selectedCells = tds];
82 | }
83 |
84 | var eq = true;
85 |
86 | tds.forEach(function (td, i) {
87 | eq = eq && td === self.selectedCells[i];
88 | });
89 |
90 | if (!eq) {
91 | return [true, self.selectedCells = tds];
92 | }
93 |
94 | return [false, self.selectedCells];
95 | };
96 |
97 |
98 | M.Capture.prototype.start = function (evt, mode, extend) {
99 |
100 | var t = table.locate(evt.target);
101 |
102 | this.table = t.table;
103 | this.scroller = new scroller.Scroller(t.td);
104 | this.mode = mode;
105 | this.extend = extend;
106 |
107 | if (!this.anchorPoint)
108 | extend = false;
109 |
110 | if (!extend) {
111 | this.anchorPoint = {
112 | x: dom.bounds(t.td).x + 1,
113 | y: dom.bounds(t.td).y + 1
114 | };
115 | this.setLocked(this.markRect(evt), !cell.selected(t.td));
116 | }
117 |
118 | var self = this;
119 |
120 | var tracker = function (type, evt) {
121 | if(type === 'move')
122 | self.scroller.reset();
123 | self.scroller.scroll(evt);
124 | self.setCaptured(self.markRect(evt));
125 | infobox.update(self.table);
126 |
127 | if (type === 'up') {
128 | self.onDone(self.table);
129 | }
130 | };
131 |
132 | event.trackMouse(evt, tracker);
133 | };
134 |
135 | M.Capture.prototype.stop = function () {
136 | dom.find('td, th').forEach(function (td) {
137 | cell.unmark(td);
138 | cell.unlock(td);
139 | });
140 | };
--------------------------------------------------------------------------------
/src/content/index.js:
--------------------------------------------------------------------------------
1 | /// Content script main
2 |
3 | var M = module.exports = {};
4 |
5 | var
6 | preferences = require('../lib/preferences'),
7 | keyboard = require('../lib/keyboard'),
8 | event = require('../lib/event'),
9 | message = require('../lib/message'),
10 | dom = require('../lib/dom'),
11 | util = require('../lib/util'),
12 |
13 | capture = require('./capture'),
14 | infobox = require('./infobox'),
15 | selection = require('./selection'),
16 | table = require('./table'),
17 | loader = require('./loader')
18 | ;
19 |
20 | var mouseButton = 0,
21 | currentCapture = null;
22 |
23 | function parseEvent(evt) {
24 |
25 | var key = keyboard.key(evt),
26 | emod = preferences.int('modifier.extend'),
27 | mods = key.modifiers.code,
28 | kmods = mods & ~emod;
29 |
30 | if (preferences.val('capture.enabled') && preferences.val('_captureMode') && !kmods) {
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 |
37 | var cap = util.first(preferences.captureModes(), function (m) {
38 | return kmods === preferences.int('modifier.' + m.id);
39 | });
40 |
41 | if (cap) {
42 | console.log('got modifier', cap.id, mods & emod);
43 | return [cap.id, mods & emod];
44 | }
45 | }
46 | }
47 |
48 | function destroyCapture() {
49 | if (currentCapture) {
50 | currentCapture.stop();
51 | currentCapture = null;
52 | }
53 | }
54 |
55 | function captureDone(tbl) {
56 | table.selectCaptured(tbl);
57 | if (preferences.val('capture.reset')) {
58 | preferences.set('_captureMode', '').then(function () {
59 | message.background('preferencesUpdated');
60 | });
61 | }
62 | }
63 |
64 | function startCapture(evt, mode, extend) {
65 | var t = table.locate(evt.target);
66 |
67 | if (!t) {
68 | destroyCapture();
69 | return false;
70 | }
71 |
72 | if (currentCapture && currentCapture.table !== t.table) {
73 | destroyCapture();
74 | extend = false;
75 | }
76 |
77 | if (currentCapture) {
78 | currentCapture.stop();
79 | }
80 |
81 | currentCapture = currentCapture || new capture.Capture();
82 |
83 | currentCapture.onDone = captureDone;
84 |
85 | selection.start(evt.target);
86 | currentCapture.start(evt, mode, extend);
87 | }
88 |
89 | var copyLock = false,
90 | copyWaitTimeout = 300,
91 | copyWaitTimer = 0;
92 |
93 | function beginCopy(msg) {
94 | var tbl = selection.table(),
95 | hasSelection = true;
96 |
97 | if (!tbl) {
98 | if (msg.broadcast)
99 | return null;
100 | var el = event.lastTarget(),
101 | t = table.locate(el);
102 | if (!t)
103 | return null;
104 | tbl = t.table;
105 | hasSelection = false;
106 | }
107 |
108 | copyLock = true;
109 | var data = table.copy(tbl, msg.options, hasSelection);
110 |
111 | copyWaitTimer = setTimeout(function () {
112 | dom.attr(document.body, 'data-copytables-wait', 1);
113 | }, copyWaitTimeout);
114 |
115 | return data;
116 | }
117 |
118 | function endCopy() {
119 | copyLock = false;
120 | clearTimeout(copyWaitTimer);
121 | dom.removeAttr(document.body, 'data-copytables-wait');
122 | }
123 |
124 | var eventListeners = {
125 | mousedownCapture: function (evt) {
126 | event.register(evt);
127 |
128 | if (evt.button !== mouseButton) {
129 | return;
130 | }
131 |
132 | var p = parseEvent(evt);
133 | console.log('parseEvent=', p);
134 |
135 | if (!p || !selection.selectable(evt.target)) {
136 | message.background('dropAllSelections');
137 | return;
138 | }
139 |
140 | window.focus();
141 | startCapture(evt, p[0], p[1]);
142 | },
143 |
144 | copy: function (evt) {
145 | if (selection.active() && !copyLock) {
146 | console.log('COPY MINE');
147 | message.background('genericCopy');
148 | event.reset(evt);
149 | } else if (copyLock) {
150 | console.log('COPY LOCKED');
151 | event.reset(evt);
152 | } else {
153 | console.log('COPY PASS');
154 | }
155 | },
156 |
157 | contextmenu: function (evt) {
158 | event.register(evt);
159 |
160 | message.background({
161 | name: 'contextMenu',
162 | selectable: selection.selectable(evt.target),
163 | selected: selection.active()
164 | });
165 | }
166 | };
167 |
168 | var messageListeners = {
169 | dropSelection: function () {
170 | if (selection.table()) {
171 | selection.drop();
172 | }
173 | // hide infobox once a selection is dropped
174 | // this means users won't be able to click and copy from the infobox
175 | infobox.remove();
176 | },
177 |
178 | preferencesUpdated: preferences.load,
179 |
180 | enumTables: function (msg) {
181 | return table.enum(selection.table());
182 | },
183 |
184 | selectTableByIndex: function (msg) {
185 | var tbl = table.byIndex(msg.index);
186 | if (tbl) {
187 | selection.select(tbl, 'table');
188 | tbl.scrollIntoView(true);
189 | infobox.update(selection.table());
190 | }
191 | },
192 |
193 | selectFromContextMenu: function (msg) {
194 | var el = event.lastTarget(),
195 | t = table.locate(el);
196 |
197 | if (t) {
198 | selection.toggle(t.td, msg.mode);
199 | } else if (msg.mode === 'table') {
200 | selection.toggle(dom.closest(el, 'table'), 'table');
201 | }
202 |
203 | infobox.update(selection.table());
204 | },
205 |
206 | tableIndexFromContextMenu: function () {
207 | var el = event.lastTarget(),
208 | tbl = dom.closest(el, 'table');
209 | return tbl ? table.indexOf(tbl) : null;
210 | },
211 |
212 | beginCopy: beginCopy,
213 | endCopy: endCopy,
214 |
215 | endCopyFailed: function () {
216 | if (copyLock) {
217 | // inform the user that copy/paste failed
218 | console.error('Sorry, Copytables was unable to copy this table.');
219 | }
220 | endCopy();
221 | }
222 |
223 | };
224 |
225 | function init() {
226 | event.listen(document, eventListeners);
227 | message.listen(messageListeners);
228 | }
229 |
230 | M.main = function () {
231 | loader.load().then(function () {
232 | if (!document.body) {
233 | console.log('no body', document.URL);
234 | return;
235 | }
236 | preferences.load().then(init);
237 | });
238 | };
239 |
--------------------------------------------------------------------------------
/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 |
12 | function getValue(td, fmt) {
13 | var val = {text: '', number: 0, isNumber: false};
14 |
15 | dom.textContentItems(td).some(function (t) {
16 | var n = number.extract(t, fmt);
17 | if (n !== null) {
18 | return val = {text: t, number: n, isNumber: true};
19 | }
20 | });
21 |
22 | return val;
23 | }
24 |
25 | function data(tbl) {
26 | if (!tbl) {
27 | return null;
28 | }
29 |
30 | var cells = cell.findSelected(tbl);
31 |
32 | if (!cells || !cells.length) {
33 | return null;
34 | }
35 |
36 | var fmt = preferences.numberFormat();
37 | var values = [];
38 |
39 | cells.forEach(function (td) {
40 | values.push(getValue(td, fmt));
41 | });
42 |
43 | return preferences.infoFunctions().map(function (f) {
44 | return {
45 | title: f.name + ':',
46 | message: f.fn(values)
47 | }
48 | });
49 | };
50 |
51 | var
52 | boxId = '__copytables_infobox__',
53 | pendingContent = null,
54 | timer = 0,
55 | freq = 500;
56 |
57 | function getBox() {
58 | return dom.findOne('#' + boxId);
59 | }
60 |
61 | function setTimer() {
62 | if (!timer)
63 | timer = setInterval(draw, freq);
64 | }
65 |
66 | function clearTimer() {
67 | clearInterval(timer);
68 | timer = 0;
69 | }
70 |
71 | function html(items, sticky) {
72 | var h = [];
73 |
74 | items.forEach(function (item) {
75 | if (item.message !== null)
76 | h.push(' ' + item.title + '' + item.message + ' ');
77 | });
78 |
79 | h = h.join('');
80 |
81 | if (sticky) {
82 | h += '× ';
83 | } else {
84 | h += ' ';
85 | }
86 |
87 | return h;
88 | }
89 |
90 | function init() {
91 | var box = dom.create('div', {
92 | id: boxId,
93 | 'data-position': preferences.val('infobox.position') || '0'
94 | });
95 | document.body.appendChild(box);
96 |
97 | box.addEventListener('click', function (e) {
98 | if (dom.tag(e.target) === 'SPAN')
99 | hide();
100 | });
101 |
102 | return box;
103 | }
104 |
105 | function draw() {
106 | if (!pendingContent) {
107 | //console.log('no pendingContent');
108 | clearTimer();
109 | return;
110 | }
111 |
112 | if (pendingContent === 'hide') {
113 | //console.log('removed');
114 | dom.remove([getBox()]);
115 | clearTimer();
116 | return;
117 | }
118 |
119 | var box = getBox() || init();
120 |
121 | dom.removeClass(box, 'hidden');
122 | box.innerHTML = pendingContent;
123 |
124 | pendingContent = null;
125 | //console.log('drawn');
126 | }
127 |
128 | function show(items) {
129 | var p = html(items, preferences.val('infobox.sticky'));
130 |
131 | if (p === pendingContent) {
132 | //console.log('same content');
133 | return;
134 | }
135 |
136 | if (pendingContent) {
137 | //console.log('queued');
138 | }
139 |
140 | pendingContent = p;
141 | setTimer();
142 | }
143 |
144 | function hide() {
145 | //console.log('about to remove...');
146 | pendingContent = 'hide';
147 | dom.addClass(getBox(), 'hidden');
148 | setTimer();
149 | }
150 |
151 | M.update = function (tbl) {
152 | if (preferences.val('infobox.enabled')) {
153 | var d = data(tbl);
154 | if (d && d.length)
155 | show(d);
156 | }
157 | };
158 |
159 | M.remove = function () {
160 | if (!preferences.val('infobox.sticky')) {
161 | hide();
162 | }
163 | };
164 |
--------------------------------------------------------------------------------
/src/content/loader.js:
--------------------------------------------------------------------------------
1 | var M = module.exports = {};
2 |
3 | M.load = function () {
4 |
5 | return new Promise(function(resolve) {
6 |
7 | function ready() {
8 | return document && (document.readyState === 'interactive' || document.readyState === 'complete');
9 | }
10 |
11 | function onload(e) {
12 | if (ready()) {
13 | console.log('loaded', document.readyState, document.URL);
14 | document.removeEventListener('readystatechange', onload);
15 | resolve();
16 | }
17 | }
18 |
19 | if (ready()) {
20 | console.log('ready', document.readyState, document.URL);
21 | return resolve();
22 | }
23 |
24 | console.log('not loaded', document.readyState, document.URL);
25 | document.addEventListener('readystatechange', onload);
26 | });
27 |
28 | };
29 |
--------------------------------------------------------------------------------
/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 (!css.overflowX.match(/scroll|auto/) && !css.overflowY.match(/scroll|auto/))
10 | return false;
11 | return el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
12 | }
13 |
14 | function closestScrollable(el) {
15 | while (el && el !== document.body && el !== document.documentElement) {
16 | if (isScrollable(el))
17 | return el;
18 | el = el.parentNode;
19 | }
20 | return null;
21 | }
22 |
23 | function position(base) {
24 | return base
25 | ? {x: base.scrollLeft, y: base.scrollTop}
26 | : {x: window.scrollX, y: window.scrollY};
27 |
28 | };
29 |
30 | M.Scroller = function(el) {
31 | this.base = closestScrollable(el.parentNode),
32 | this.anchor = position(this.base);
33 | this.reset();
34 | };
35 |
36 | M.Scroller.prototype.reset = function() {
37 | this.amount = preferences.int('scroll.amount');
38 | this.acceleration = preferences.int('scroll.acceleration');
39 | };
40 |
41 | M.Scroller.prototype.adjustPoint = function (pt) {
42 | var p = position(this.base);
43 | return {
44 | x: pt.x + this.anchor.x - p.x,
45 | y: pt.y + this.anchor.y - p.y
46 | }
47 | };
48 |
49 | M.Scroller.prototype.scroll = function (e) {
50 |
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 |
61 | var b = dom.bounds(this.base);
62 | var p = adjust(
63 | this.amount,
64 | this.base.scrollLeft,
65 | this.base.scrollTop,
66 | this.base.clientWidth,
67 | this.base.clientHeight,
68 | e.clientX - b.x,
69 | e.clientY - b.y
70 | );
71 |
72 | this.base.scrollLeft = p.x;
73 | this.base.scrollTop = p.y;
74 |
75 | } else {
76 |
77 | var p = adjust(
78 | this.amount,
79 | window.scrollX,
80 | window.scrollY,
81 | window.innerWidth,
82 | window.innerHeight,
83 | e.clientX,
84 | e.clientY
85 | );
86 |
87 | if (p.x != window.scrollX || p.y != window.scrollY) {
88 | window.scrollTo(p.x, p.y);
89 | }
90 | }
91 |
92 | this.amount = Math.max(1, Math.min(100, this.amount + this.amount * (this.acceleration / 100)));
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 |
12 | function cellsToSelect(el, mode) {
13 | if (mode === 'table') {
14 | var tbl = dom.closest(el, 'table');
15 | return tbl ? dom.cells(tbl) : [];
16 | }
17 |
18 | var t = table.locate(el);
19 |
20 | if (!t) {
21 | return [];
22 | }
23 |
24 | if (mode === 'cell') {
25 | return [t.td];
26 | }
27 |
28 | var sel = dom.bounds(t.td);
29 |
30 | return dom.cells(t.table).filter(function (td) {
31 | var b = dom.bounds(td);
32 |
33 | switch (mode) {
34 | case 'column':
35 | return sel.x === b.x;
36 | case 'row':
37 | return sel.y === b.y;
38 | }
39 | });
40 | }
41 |
42 | var excludeElements = 'a, input, button, textarea, select, img';
43 |
44 | M.selectable = function (el) {
45 | return !!(el && dom.closest(el, 'table') && !dom.closest(el, excludeElements));
46 | };
47 |
48 | M.selected = function (el) {
49 | var t = table.locate(el);
50 | return t && cell.selected(t.td);
51 | };
52 |
53 | M.drop = function () {
54 | dom.find('td, th').forEach(cell.reset);
55 | };
56 |
57 | M.active = function () {
58 | return !!cell.find('selected').length;
59 | };
60 |
61 | M.table = function () {
62 | var cs = cell.find('selected');
63 | return cs.length ? dom.closest(cs[0], 'table') : null;
64 | };
65 |
66 | M.start = function (el) {
67 | var t = table.locate(el);
68 |
69 | if (!t) {
70 | return false;
71 | }
72 |
73 | window.getSelection().removeAllRanges();
74 | message.background('dropOtherSelections')
75 |
76 | if (M.table() !== t.table) {
77 | M.drop();
78 | }
79 |
80 | return true;
81 | };
82 |
83 | M.select = function (el, mode) {
84 | if (dom.is(el, 'table'))
85 | el = dom.cells(el)[0];
86 | if (el && M.start(el)) {
87 | var tds = cellsToSelect(el, mode);
88 | tds.forEach(cell.select);
89 | }
90 | };
91 |
92 | M.toggle = function (el, mode) {
93 | if (dom.is(el, 'table'))
94 | el = dom.cells(el)[0];
95 | if (el && M.start(el)) {
96 | var tds = cellsToSelect(el, mode),
97 | fn = tds.every(cell.selected) ? cell.reset : cell.select;
98 | tds.forEach(fn);
99 | }
100 | };
101 |
--------------------------------------------------------------------------------
/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 |
10 | function listTables() {
11 | var all = [];
12 |
13 | dom.find('table').forEach(function (tbl, n) {
14 | if (!dom.cells(tbl).length || !dom.visible(tbl))
15 | return;
16 | all.push({
17 | index: n,
18 | table: tbl
19 | });
20 | });
21 |
22 | return all;
23 | }
24 |
25 | M.locate = function (el) {
26 | var td = dom.closest(el, 'td, th'),
27 | tbl = dom.closest(td, 'table');
28 |
29 | return (td && tbl) ? {td: td, table: tbl} : null;
30 | };
31 |
32 | M.indexOf = function (tbl) {
33 | var res = -1;
34 |
35 | listTables().forEach(function (r) {
36 | if (tbl === r.table)
37 | res = r.index;
38 | });
39 |
40 | return res;
41 | };
42 |
43 | M.byIndex = function (index) {
44 | var res = null;
45 |
46 | listTables().forEach(function (r) {
47 | if (index === r.index)
48 | res = r.table;
49 | });
50 |
51 | console.log('byIndex', res);
52 | return res;
53 | };
54 |
55 | M.enum = function (selectedTable) {
56 | return listTables().map(function (r) {
57 | r.selected = r.table === selectedTable;
58 | delete r.table;
59 | return r;
60 | });
61 | };
62 |
63 | M.copy = function (tbl, options, hasSelection) {
64 | console.log(util.timeStart('table.copy'));
65 |
66 | var data = {
67 | hasSelection: hasSelection,
68 | url: document.location ? document.location.href : ''
69 | };
70 |
71 | if (hasSelection) {
72 | // lock selected cells to remove highlighting with no animation
73 | dom.cells(tbl).forEach(function (td) {
74 | if (cell.selected(td)) {
75 | cell.lock(td)
76 | }
77 | });
78 | }
79 |
80 | if (options.method === 'transfer') {
81 | data.css = {};
82 |
83 | if (options.keepStyles) {
84 | dom.findSelf('*', tbl).forEach(function (el, uid) {
85 | dom.attr(el, 'data-copytables-uid', uid);
86 | data.css[uid] = css.read(el);
87 | });
88 | }
89 |
90 | data.html = tbl.outerHTML;
91 |
92 | if (options.keepStyles) {
93 | dom.findSelf('*', tbl).forEach(function (el) {
94 | dom.removeAttr(el, 'data-copytables-uid');
95 | });
96 | }
97 | }
98 |
99 | if (options.method === 'clipboard') {
100 | // work around "unselectable" tables
101 |
102 | var style = document.createElement('STYLE');
103 | style.type = 'text/css';
104 | style.innerHTML = '* { user-select: auto !important; -webkit-user-select: auto !important }';
105 | document.body.appendChild(style);
106 |
107 | dom.select(tbl);
108 |
109 | // wrap copy in a capturing handler to work around copy-hijackers
110 |
111 | var copyHandler = function (evt) {
112 | console.log('COPY IN TABLE');
113 | evt.stopPropagation();
114 | };
115 |
116 | document.addEventListener('copy', copyHandler, true);
117 | document.execCommand('copy');
118 | document.removeEventListener('copy', copyHandler, true);
119 |
120 | dom.deselect();
121 | document.body.removeChild(style);
122 | }
123 |
124 | if (hasSelection) {
125 | dom.cells(tbl).forEach(function (td) {
126 | if (cell.selected(td)) {
127 | cell.unlock(td)
128 | }
129 | });
130 | }
131 |
132 | console.log('table.copy method=' + options.method);
133 | console.log(util.timeEnd('table.copy'));
134 |
135 | return data;
136 | };
137 |
138 | M.selectCaptured = function (tbl) {
139 | dom.cells(tbl).forEach(function (td) {
140 | if (cell.locked(td)) {
141 | cell.reset(td);
142 | } else if (cell.marked(td)) {
143 | cell.unmark(td);
144 | cell.select(td);
145 | }
146 | });
147 | };
148 |
--------------------------------------------------------------------------------
/src/ico128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/src/ico128.png
--------------------------------------------------------------------------------
/src/ico16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/src/ico16.png
--------------------------------------------------------------------------------
/src/ico19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/src/ico19.png
--------------------------------------------------------------------------------
/src/ico32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/src/ico32.png
--------------------------------------------------------------------------------
/src/ico38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/src/ico38.png
--------------------------------------------------------------------------------
/src/ico48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/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.split(',').map(function (s) {
59 | return '[' + prefix + s.trim() + ']'
60 | }).join(',');
61 | return dom.find(sel, where);
62 | };
63 |
64 | M.findSelected = function (where) {
65 | var sel = '[{}selected]:not([{}locked]), [{}marked]:not([{}locked])'.replace(/{}/g, prefix);
66 | return dom.find(sel, where);
67 | };
68 |
69 | M.reset = function (td) {
70 | td.removeAttribute(prefix + 'selected');
71 | td.removeAttribute(prefix + 'marked');
72 | td.removeAttribute(prefix + 'locked');
73 | };
74 |
--------------------------------------------------------------------------------
/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 |
27 | if (cs.getPropertyValue('display') === 'none') {
28 | res['display'] = 'none';
29 | }
30 |
31 | return res;
32 | };
33 |
34 | M.compute = function (defaults, custom) {
35 | var rules = [];
36 |
37 | Object.keys(custom).forEach(function (k) {
38 | if (custom[k] !== defaults[k]) {
39 | rules.push(k + ':' + custom[k]);
40 | }
41 | });
42 |
43 | return rules.join('; ');
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)
31 | return '';
32 | return String(el.tagName).toUpperCase();
33 | };
34 |
35 | M.indexOf = function (el, sel, where) {
36 | var idx = -1;
37 | M.find(sel, where).forEach(function (e, n) {
38 | if (e === el) {
39 | idx = n;
40 | }
41 | });
42 | return idx;
43 | };
44 |
45 | M.nth = function (n, sel, where) {
46 | return M.find(sel, where).item(n)
47 | };
48 |
49 | M.attr = function (el, name, value) {
50 | if (!el || !el.getAttribute) {
51 | return null;
52 | }
53 | if (arguments.length === 2) {
54 | return el.getAttribute(name);
55 | }
56 | if (value === null) {
57 | return el.removeAttribute(name);
58 | }
59 | return el.setAttribute(name, value);
60 | };
61 |
62 | M.removeAttr = function (el, name) {
63 | if (el && el.removeAttribute) {
64 | el.removeAttribute(name);
65 | }
66 | };
67 |
68 | M.findSelf = function (sel, where) {
69 | return [where || document].concat(toArray(M.find(sel, where)));
70 | };
71 |
72 | M.rows = function (tbl) {
73 | if (tbl && tbl.rows) {
74 | return toArray(tbl.rows);
75 | }
76 | return [];
77 | };
78 |
79 | M.cells = function (tbl) {
80 | var ls = [];
81 |
82 | M.rows(tbl).forEach(function (tr) {
83 | ls = ls.concat(toArray(tr.cells));
84 | });
85 |
86 | return ls;
87 | };
88 |
89 | M.remove = function (els) {
90 | els.forEach(function (el) {
91 | if (el && el.parentNode)
92 | el.parentNode.removeChild(el);
93 | });
94 | };
95 |
96 | M.closest = function (el, sel) {
97 | while (el && el.matches) {
98 | if (el.matches(sel))
99 | return el;
100 | el = el.parentNode;
101 | }
102 | return null;
103 | };
104 |
105 | M.contains = function (parent, el) {
106 | while (el) {
107 | if (el === parent)
108 | return true;
109 | el = el.parentNode;
110 | }
111 | return false;
112 | };
113 |
114 | M.bounds = function (el) {
115 | var r = el.getBoundingClientRect();
116 | return {
117 | x: r.left,
118 | y: r.top,
119 | right: r.right,
120 | bottom: r.bottom,
121 | rect: [r.left, r.top, r.right, r.bottom]
122 | };
123 | };
124 |
125 | M.offset = function (el) {
126 | var r = {x: 0, y: 0};
127 | while (el) {
128 | r.x += el.offsetLeft;
129 | r.y += el.offsetTop;
130 | el = el.offsetParent;
131 | }
132 | return r;
133 | };
134 |
135 | M.addClass = function (el, cls) {
136 | return el && el.classList && el.classList.add(cls);
137 | };
138 |
139 | M.removeClass = function (el, cls) {
140 | return el && el.classList && el.classList.remove(cls);
141 | };
142 |
143 | M.hasClass = function (el, cls) {
144 | return el && el.classList && el.classList.contains(cls);
145 | };
146 |
147 | function _strip(s) {
148 | return String(s || '').replace(/^\s+|\s+$/g, '');
149 | }
150 |
151 | M.textContentItems = function (node) {
152 | var c = [];
153 |
154 | function walk(n) {
155 | if (!n)
156 | return;
157 |
158 | if (n.nodeType === 3) {
159 | var t = _strip(n.textContent);
160 | if (t.length)
161 | c.push(t);
162 | return;
163 | }
164 |
165 | if (!M.visible(n)) {
166 | return;
167 | }
168 |
169 | (n.childNodes || []).forEach(walk);
170 | }
171 |
172 | walk(node);
173 | return c;
174 | };
175 |
176 | M.textContent = function (node) {
177 | return _strip(M.textContentItems(node).join(' '));
178 | };
179 |
180 | M.htmlContent = function (node) {
181 | if (!node)
182 | return '';
183 | return _strip(node.innerHTML);
184 | };
185 |
186 |
187 | M.deselect = function () {
188 | var selection = window.getSelection();
189 | selection.removeAllRanges();
190 | };
191 |
192 | M.select = function (el) {
193 | var range = document.createRange(),
194 | selection = window.getSelection();
195 |
196 | range.selectNodeContents(el);
197 | selection.removeAllRanges();
198 | selection.addRange(range);
199 | };
200 |
201 | M.create = function (tag, atts) {
202 | var e = document.createElement(tag);
203 | if (atts) {
204 | Object.keys(atts).forEach(function (a) {
205 | e.setAttribute(a, atts[a]);
206 | });
207 | }
208 | return e;
209 | };
210 |
--------------------------------------------------------------------------------
/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 |
47 | function watch() {
48 | fn('tick', tracker.lastEvent);
49 | tracker.timer = setTimeout(watch, tracker.freq);
50 | }
51 |
52 | function reset() {
53 | clearInterval(tracker.timer);
54 | tracker.active = false;
55 | M.unlisten(document, listeners);
56 | }
57 |
58 | var listeners = {
59 |
60 | mousemove: function (evt) {
61 | M.reset(evt);
62 | tracker.lastEvent = evt;
63 |
64 | if(evt.buttons === 1) {
65 | clearTimeout(tracker.timer);
66 | fn('move', tracker.lastEvent);
67 | watch();
68 | } else {
69 | reset();
70 | fn('up', evt);
71 | }
72 | },
73 |
74 | mouseup: function (evt) {
75 | M.reset(evt);
76 | tracker.lastEvent = evt;
77 | reset();
78 | fn('up', evt);
79 | }
80 | };
81 |
82 | if (tracker.active) {
83 | console.log('mouse tracker already active');
84 | reset();
85 | }
86 |
87 | tracker.active = true;
88 | console.log('mouse tracker started');
89 |
90 | M.listen(document, listeners);
91 | listeners.mousemove(evt);
92 | };
93 |
--------------------------------------------------------------------------------
/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 = [M.modifiers.SHIFT, M.modifiers.ALT, M.modifiers.CTRL, M.modifiers.META];
23 |
24 |
25 | M.modNames = {};
26 |
27 | M.modNames[M.modifiers.CTRL] = 'Ctrl';
28 | M.modNames[M.modifiers.ALT] = M.mac ? 'Opt' : 'Alt';
29 | M.modNames[M.modifiers.META] = M.mac ? 'Cmd' : (M.win ? 'Win' : 'Meta');
30 | M.modNames[M.modifiers.SHIFT] = 'Shift';
31 |
32 | M.modHTMLNames = {};
33 |
34 | M.modHTMLNames[M.modifiers.CTRL] = M.mac ? '⌃ control' : M.modNames[M.modifiers.CTRL];
35 | M.modHTMLNames[M.modifiers.ALT] = M.mac ? '⌥ option' : M.modNames[M.modifiers.ALT];
36 | M.modHTMLNames[M.modifiers.META] = M.mac ? '⌘ command' : M.modNames[M.modifiers.META];
37 | M.modHTMLNames[M.modifiers.SHIFT] = M.mac ? '⇧ shift' : M.modNames[M.modifiers.SHIFT];
38 |
39 |
40 | M.keyNames = {
41 | 8: "Backspace",
42 | 9: "Tab",
43 | 13: "Enter",
44 | 19: "Break",
45 | 20: "Caps",
46 | 27: "Esc",
47 | 32: "Space",
48 | 33: "PgUp",
49 | 34: "PgDn",
50 | 35: "End",
51 | 36: "Home",
52 | 37: "Left",
53 | 38: "Up",
54 | 39: "Right",
55 | 40: "Down",
56 | 45: "Ins",
57 | 46: "Del",
58 | 48: "0",
59 | 49: "1",
60 | 50: "2",
61 | 51: "3",
62 | 52: "4",
63 | 53: "5",
64 | 54: "6",
65 | 55: "7",
66 | 56: "8",
67 | 57: "9",
68 | 65: "A",
69 | 66: "B",
70 | 67: "C",
71 | 68: "D",
72 | 69: "E",
73 | 70: "F",
74 | 71: "G",
75 | 72: "H",
76 | 73: "I",
77 | 74: "J",
78 | 75: "K",
79 | 76: "L",
80 | 77: "M",
81 | 78: "N",
82 | 79: "O",
83 | 80: "P",
84 | 81: "Q",
85 | 82: "R",
86 | 83: "S",
87 | 84: "T",
88 | 85: "U",
89 | 86: "V",
90 | 87: "W",
91 | 88: "X",
92 | 89: "Y",
93 | 90: "Z",
94 | 93: "Select",
95 | 96: "Num0",
96 | 97: "Num1",
97 | 98: "Num2",
98 | 99: "Num3",
99 | 100: "Num4",
100 | 101: "Num5",
101 | 102: "Num6",
102 | 103: "Num7",
103 | 104: "Num8",
104 | 105: "Num9",
105 | 106: "Num*",
106 | 107: "Num+",
107 | 109: "Num-",
108 | 110: "Num.",
109 | 111: "Num/",
110 | 112: "F1",
111 | 113: "F2",
112 | 114: "F3",
113 | 115: "F4",
114 | 116: "F5",
115 | 117: "F6",
116 | 118: "F7",
117 | 119: "F8",
118 | 120: "F9",
119 | 121: "F10",
120 | 122: "F11",
121 | 123: "F12",
122 | 144: "NumLock",
123 | 145: "ScrollLock",
124 | 186: ";",
125 | 187: "=",
126 | 188: ",",
127 | 189: "-",
128 | 190: ".",
129 | 191: "/",
130 | 192: "`",
131 | 219: "(",
132 | 220: "\\",
133 | 221: ")",
134 | 222: "'",
135 | };
136 |
137 | M.keyCode = function (name) {
138 | var code;
139 |
140 | Object.keys(M.keyNames).some(function (c) {
141 | if (M.keyNames[c] === name) {
142 | return code = c;
143 | }
144 | });
145 |
146 | return code;
147 | };
148 |
149 | M.key = function (e) {
150 | var mods =
151 | (M.modifiers.ALT * e.altKey) |
152 | (M.modifiers.CTRL * e.ctrlKey) |
153 | (M.modifiers.META * e.metaKey) |
154 | (M.modifiers.SHIFT * e.shiftKey);
155 |
156 | var scan = e.keyCode,
157 | sname = M.keyNames[scan],
158 | mname = [],
159 | cname = [];
160 |
161 | Object.keys(M.modifiers).forEach(function (m) {
162 | if ((mods & M.modifiers[m])) {
163 | mname.push(M.modNames[M.modifiers[m]]);
164 | }
165 | });
166 |
167 | mname = mname.join(' ');
168 |
169 | var r = {
170 | modifiers: {code: 0, name: ''},
171 | scan: {code: 0, name: ''}
172 | };
173 |
174 | if (mname) {
175 | r.modifiers = {code: mods, name: mname};
176 | cname.push(mname);
177 | }
178 |
179 | if (sname) {
180 | r.scan = {code: scan, name: sname};
181 | cname.push(sname);
182 | }
183 |
184 | r.code = mods | scan;
185 | r.name = cname.join(' ');
186 |
187 | return r;
188 | };
189 |
--------------------------------------------------------------------------------
/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)
13 | return mat;
14 | return mat[0].map(function (_, ci) {
15 | return M.column(mat, ci);
16 | });
17 | };
18 |
19 | M.trim = function (mat, fn) {
20 |
21 | var fun = function (row) {
22 | return row.some(function (cell) {
23 | return fn(cell);
24 | });
25 | };
26 |
27 | mat = mat.filter(fun);
28 | mat = M.transpose(mat).filter(fun);
29 | return M.transpose(mat);
30 | };
31 |
32 | M.each = function (mat, fn) {
33 | mat.forEach(function (row, ri) {
34 | row.forEach(function (cell, ci) {
35 | fn(row, cell, ri, ci);
36 | });
37 | });
38 | };
39 |
40 | M.map = function (mat, fn) {
41 | return mat.map(function (row, ri) {
42 | return row.map(function (cell, ci) {
43 | return fn(row, cell, ri, ci);
44 | });
45 | });
46 | };
47 |
--------------------------------------------------------------------------------
/src/lib/message.js:
--------------------------------------------------------------------------------
1 | /// Wrappers for chrome...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.callChromeAsync('runtime.sendMessage', msg)
36 | .then(function (res) {
37 | return {receiver: 'background', data: res};
38 | });
39 | }
40 |
41 | function toFrame(msg, frame) {
42 | msg.to = frame;
43 | return util.callChromeAsync('tabs.sendMessage', frame.tabId, msg, {frameId: frame.frameId})
44 | .then(function (res) {
45 | return {receiver: frame, data: res};
46 | });
47 | }
48 |
49 | function toFrameList(msg, frames) {
50 | return Promise.all(frames.map(function (frame) {
51 | return toFrame(msg, frame);
52 | }));
53 | }
54 |
55 | M.enumFrames = function (tabFilter) {
56 |
57 | function framesInTab(tab) {
58 | return util.callChromeAsync('webNavigation.getAllFrames', {tabId: tab.id})
59 | .then(function (frames) {
60 | if (!frames) {
61 | // Vivaldi, as of 1.8.770.56, doesn't support getAllFrames() properly
62 | // let's pretend there's only one top frame
63 | frames = [{
64 | errorOccurred: false,
65 | frameId: 0,
66 | parentFrameId: -1
67 | }];
68 | }
69 | return frames.map(function (f) {
70 | f.tabId = tab.id;
71 | return f;
72 | })
73 | });
74 | }
75 |
76 | if (tabFilter === 'active') {
77 | tabFilter = {active: true, currentWindow: true};
78 | }
79 |
80 | return util.callChromeAsync('tabs.query', tabFilter || {})
81 | .then(function (tabs) {
82 | return Promise.all(tabs.map(framesInTab));
83 | }).then(function (res) {
84 | return util.flatten(res);
85 | });
86 | }
87 |
88 | M.background = function (msg) {
89 | console.log('MESSAGE: background', msg);
90 | return toBackground(convertMessage(msg));
91 | };
92 |
93 | M.frame = function (msg, frame) {
94 | console.log('MESSAGE: frame', msg, frame);
95 | return toFrame(convertMessage(msg), frame);
96 | };
97 |
98 | M.allFrames = function (msg) {
99 | console.log('MESSAGE: allFrames', msg);
100 | msg = convertMessage(msg);
101 | return M.enumFrames('active').then(function (frames) {
102 | return toFrameList(msg, frames);
103 | });
104 | };
105 |
106 | M.topFrame = function (msg) {
107 | console.log('MESSAGE: topFrame', msg);
108 | msg = convertMessage(msg);
109 | return M.enumFrames('active').then(function (frames) {
110 | var top = frames.filter(function (f) {
111 | return f.frameId === 0;
112 | });
113 | if (top.length) {
114 | return toFrame(msg, top[0]);
115 | }
116 | });
117 | };
118 |
119 | M.broadcast = function (msg) {
120 | console.log('MESSAGE: broadcast', msg);
121 | msg = convertMessage(msg);
122 | return M.enumFrames().then(function (frames) {
123 | return toFrameList(msg, frames);
124 | });
125 | };
126 |
127 | M.listen = function (listeners) {
128 | util.callChrome('runtime.onMessage.addListener', function (msg, sender, fn) {
129 | if (listeners[msg.name]) {
130 | msg.sender = convertSender(sender);
131 | var res = listeners[msg.name](msg);
132 | return fn(res);
133 | }
134 | console.log('LOST', msg.name);
135 | });
136 | };
137 |
--------------------------------------------------------------------------------
/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 |
39 | if (s[0] === '-') {
40 | var n = M.parse(s.slice(1), fmt);
41 | return n === null ? null : -n;
42 | }
43 |
44 | if (!fmt.decimal || s.indexOf(fmt.decimal) < 0) {
45 | return parseGrouped(s, fmt);
46 | }
47 |
48 | if (s === fmt.decimal) {
49 | return null;
50 | }
51 |
52 | var ds = s.split(fmt.decimal);
53 |
54 | if (ds.length === 1) {
55 | return parseGrouped(ds[0], fmt);
56 | }
57 |
58 | if (ds.length === 2) {
59 | var a = ds[0].length ? parseGrouped(ds[0], fmt) : 0;
60 | var b = ds[1].length ? parseFraction(ds[1]) : 0;
61 |
62 | if (a === null || b === null) {
63 | return null;
64 | }
65 |
66 | return Number(a + '.' + b);
67 | }
68 |
69 | return null;
70 | };
71 |
72 | M.extract = function (text, fmt) {
73 | if (!text) {
74 | return null;
75 | }
76 |
77 | text = String(text).replace(/^\s+|\s+$/g, '');
78 | if (!text) {
79 | return null;
80 | }
81 |
82 | var g = fmt.group ? '\\' + fmt.group : '';
83 | var d = fmt.decimal ? '\\' + fmt.decimal : '';
84 |
85 | var re = new RegExp('-?[\\d' + g + d + ']*\\d', 'g');
86 | var m = text.match(re);
87 |
88 | if (!m || m.length !== 1) {
89 | return null;
90 | }
91 |
92 | var n = M.parse(m[0], fmt);
93 | if (n === null) {
94 | return null;
95 | }
96 |
97 | return n;
98 | };
99 |
100 | M.defaultFormat = function () {
101 | var f = {group: ',', decimal: '.'};
102 |
103 | try {
104 | // Intl and formatToParts might not be available...
105 |
106 | var nf = new Intl.NumberFormat();
107 |
108 | nf.formatToParts(123456.78).forEach(function (p) {
109 | if (String(p.type) === 'group')
110 | f.group = p.value;
111 | if (String(p.type) === 'decimal')
112 | f.decimal = p.value;
113 | })
114 | return f;
115 | } catch (e) {
116 | }
117 |
118 | try {
119 | var s = (123456.78).toLocaleString().replace(/\d/g, ''),
120 | len = s.length;
121 |
122 | f.decimal = len > 0 ? s[len - 1] : '.';
123 | f.group = len > 1 ? s[len - 2] : '';
124 | return f;
125 |
126 | } catch (e) {
127 | }
128 |
129 | return f;
130 | };
131 |
132 | M.format = function (n, prec) {
133 | n = prec ? Number(n.toFixed(prec)) : Number(n);
134 | return (n || 0).toLocaleString();
135 | };
136 |
--------------------------------------------------------------------------------
/src/lib/preferences.js:
--------------------------------------------------------------------------------
1 | // Preferences, stored in chrome.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.richHTML': true,
26 | 'copy.format.enabled.richHTMLCSS': true,
27 | 'copy.format.enabled.textCSV': true,
28 | 'copy.format.enabled.textCSVSwap': true,
29 | 'copy.format.enabled.textHTML': true,
30 | 'copy.format.enabled.textHTMLCSS': true,
31 | 'copy.format.enabled.textTabs': true,
32 | 'copy.format.enabled.textTabsSwap': 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 | "id": "textTextileHTML",
112 | "name": "Textile+HTML",
113 | "desc": "Copy as Textile (HTML content)"
114 | },
115 | ];
116 |
117 | function sum(vs) {
118 | return vs.reduce(function (x, y) {
119 | return x + (Number(y) || 0)
120 | }, 0);
121 | }
122 |
123 | function getNumbers(values) {
124 | var vs = [];
125 |
126 | values.forEach(function (v) {
127 | if (v.isNumber)
128 | vs.push(v.number);
129 | });
130 |
131 | return vs.length ? vs : null;
132 | }
133 |
134 | var infoFunctions = [
135 | {
136 | name: 'count',
137 | fn: function (values) {
138 | return values.length;
139 | }
140 | },
141 | {
142 | name: 'sum',
143 | fn: function (values) {
144 | var vs = getNumbers(values);
145 | return vs ? number.format(sum(vs)) : null;
146 | }
147 | },
148 | {
149 | name: 'average',
150 | fn: function (values) {
151 | var vs = getNumbers(values);
152 | return vs ? number.format(sum(vs) / vs.length, 2) : null;
153 | }
154 | },
155 | {
156 | name: 'min',
157 | fn: function (values) {
158 | var vs = getNumbers(values);
159 | return vs ? number.format(Math.min.apply(Math, vs)) : null;
160 | }
161 | },
162 | {
163 | name: 'max',
164 | fn: function (values) {
165 | var vs = getNumbers(values);
166 | return vs ? number.format(Math.max.apply(Math, vs)) : null;
167 | }
168 | }
169 | ];
170 |
171 | var prefs = {};
172 |
173 | function _constrain(min, val, max) {
174 | val = Number(val) || min;
175 | return Math.max(min, Math.min(val, max));
176 | }
177 |
178 | M.load = function () {
179 | return util.callChromeAsync('storage.local.get', null)
180 | .then(function (obj) {
181 | obj = obj || {};
182 |
183 | // from the previous version
184 | if ('modKey' in obj && String(obj.modKey) === '1') {
185 | console.log('FOUND ALTERNATE MODKEY SETTING');
186 | obj['modifier.cell'] = secondMod;
187 | delete obj.modKey;
188 | }
189 |
190 | prefs = Object.assign({}, defaults, prefs, obj);
191 |
192 | prefs['scroll.amount'] = _constrain(1, prefs['scroll.amount'], 100);
193 | prefs['scroll.acceleration'] = _constrain(0, prefs['scroll.acceleration'], 100);
194 |
195 | if (!prefs['number.group']) {
196 | prefs['number.group'] = number.defaultFormat().group;
197 | }
198 |
199 | if (!prefs['number.decimal']) {
200 | prefs['number.decimal'] = number.defaultFormat().decimal;
201 | }
202 |
203 | console.log('PREFS LOAD', prefs);
204 | return prefs;
205 | });
206 | };
207 |
208 | M.save = function () {
209 | return util.callChromeAsync('storage.local.clear')
210 | .then(function () {
211 | return util.callChromeAsync('storage.local.set', prefs);
212 | }).then(function () {
213 | console.log('PREFS SET', prefs);
214 | return prefs;
215 | });
216 | };
217 |
218 | M.setAll = function (obj) {
219 | prefs = Object.assign({}, prefs, obj);
220 | return M.save();
221 | };
222 |
223 | M.set = function (key, val) {
224 | prefs[key] = val;
225 | return M.save();
226 | };
227 |
228 | M.val = function (key) {
229 | return prefs[key];
230 | };
231 |
232 | M.int = function (key) {
233 | return Number(M.val(key)) || 0;
234 | };
235 |
236 | M.copyFormats = function () {
237 | return copyFormats.map(function (f) {
238 | f.enabled = !!M.val('copy.format.enabled.' + f.id);
239 | f.default = !!M.val('copy.format.default.' + f.id);
240 | return f;
241 | });
242 | };
243 |
244 | M.numberFormat = function () {
245 | var g = M.val('number.group');
246 | var d = M.val('number.decimal');
247 |
248 | if (!g && !d) {
249 | return number.defaultFormat();
250 | }
251 |
252 | return {
253 | group: g || '',
254 | decimal: d || '',
255 | };
256 | }
257 |
258 | M.infoFunctions = function () {
259 | return infoFunctions;
260 | };
261 |
262 | M.captureModes = function () {
263 | return captureModes;
264 | };
265 |
--------------------------------------------------------------------------------
/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))
15 | a = Array.prototype.concat.apply([], a);
16 | return a;
17 | };
18 |
19 | M.first = function (a, fn) {
20 | for (var i = 0; i < a.length; i++) {
21 | if (fn(a[i], i)) {
22 | return a[i];
23 | }
24 | }
25 | return null;
26 | };
27 |
28 | M.intersect = function (a, b) {
29 | return !(a[0] >= b[2] || a[2] <= b[0] || a[1] >= b[3] || a[3] <= b[1])
30 | };
31 |
32 | M.lstrip = function (s) {
33 | return s.replace(/^\s+/, '')
34 | };
35 |
36 | M.rstrip = function (s) {
37 | return s.replace(/\s+$/, '')
38 | };
39 |
40 | M.strip = function (s) {
41 | return s.replace(/^\s+|\s+$/g, '');
42 | };
43 |
44 | M.reduceWhitespace = function (html) {
45 | return html.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 callChrome(useAsync, fn, args) {
85 | var parts = fn.split('.'),
86 | obj = chrome,
87 | method = parts.pop();
88 |
89 | parts.forEach(function (p) {
90 | obj = obj[p];
91 | });
92 |
93 | console.log('CALL_CHROME', useAsync, fn);
94 |
95 | if (!useAsync) {
96 | try {
97 | return obj[method].apply(obj, args);
98 | } catch (err) {
99 | console.log('CALL_CHROME_ERROR', fn, err.message);
100 | return null;
101 | }
102 | }
103 | return new Promise(function (resolve, reject) {
104 | function callback(res) {
105 | var err = chrome.runtime.lastError;
106 | if (err) {
107 | console.log('CALL_CHROME_LAST_ERROR', fn, err);
108 | resolve(null);
109 | } else {
110 | resolve(res);
111 | }
112 | }
113 |
114 | try {
115 | obj[method].apply(obj, args.concat(callback));
116 | } catch (err) {
117 | console.log('CALL_CHROME_ERROR', fn, err.message);
118 | resolve(null);
119 | }
120 | });
121 | }
122 |
123 | M.callChrome = function (fn) {
124 | return callChrome(false, fn, [].slice.call(arguments, 1));
125 | };
126 |
127 | M.callChromeAsync = function (fn) {
128 | return callChrome(true, fn, [].slice.call(arguments, 1));
129 | };
130 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "description": "Select table cells, rows and columns with your mouse or from a context menu. Copy as rich text, HTML, tab-delimited and CSV.",
4 | "content_scripts": [
5 | {
6 | "js": [
7 | "content.js"
8 | ],
9 | "css": [
10 | "content.css"
11 | ],
12 | "matches": [
13 | "*://*/*",
14 | "file://*/*"
15 | ],
16 | "all_frames": true
17 | }
18 | ],
19 | "background": {
20 | "scripts": [
21 | "background.js"
22 | ],
23 | "persistent": false
24 | },
25 | "permissions": [
26 | "clipboardRead",
27 | "clipboardWrite",
28 | "contextMenus",
29 | "webNavigation",
30 | "storage"
31 | ],
32 | "icons": {
33 | "16": "ico16.png",
34 | "48": "ico48.png",
35 | "128": "ico128.png"
36 | },
37 | "browser_action": {
38 | "default_icon": {
39 | "19": "ico19.png",
40 | "38": "ico38.png"
41 | },
42 | "default_title": "Copytables",
43 | "default_popup": "popup.html"
44 | },
45 | "options_page": "options.html",
46 | "commands": {
47 | "find_previous": {
48 | "description": "Find previous table"
49 | },
50 | "find_next": {
51 | "description": "Find next table"
52 | },
53 | "capture_cell": {
54 | "description": "Capture cells"
55 | },
56 | "capture_column": {
57 | "description": "Capture columns"
58 | },
59 | "capture_row": {
60 | "description": "Capture rows"
61 | },
62 | "capture_zzz": {
63 | "description": "Turn off the capture"
64 | },
65 | "capture_table": {
66 | "description": "Capture tables"
67 | }
68 |
69 |
70 | },
71 | "name": "Copytables",
72 | "version": "0.5.10",
73 | "homepage_url": "https://merribithouse.net/copytables"
74 | }
75 |
--------------------------------------------------------------------------------
/src/options.js:
--------------------------------------------------------------------------------
1 | var
2 | dom = require('./lib/dom'),
3 | preferences = require('./lib/preferences'),
4 | keyboard = require('./lib/keyboard'),
5 | event = require('./lib/event'),
6 | message = require('./lib/message'),
7 | util = require('./lib/util')
8 | ;
9 |
10 | var mouseButtonNames = ['Left Button', 'Middle/Wheel', 'Right Button', 'Button 3', 'Button 4'];
11 |
12 | function modifiers(mode) {
13 |
14 | var tpl = [
15 | '',
16 | ' ',
17 | ' ${name} ',
18 | ' '
19 | ].join('');
20 |
21 | var kmod = preferences.val('modifier.' + mode);
22 |
23 | return keyboard.mouseModifiers.map(function (m) {
24 | return util.format(tpl, {
25 | code: m,
26 | name: keyboard.modHTMLNames[m],
27 | mode: mode,
28 | checked: (kmod & m) ? 'checked' : ''
29 | });
30 | }).join('');
31 | }
32 |
33 | function copyFormats() {
34 |
35 | var tpl = [
36 | '
',
37 | ' ${name} ${desc} ',
38 | ' ',
39 | ' Enabled ' +
40 | ' ',
41 | ' ',
42 | ' Default ',
43 | ' ',
44 | ' '
45 | ].join('');
46 |
47 |
48 | var s = preferences.copyFormats().map(function (f) {
49 | return util.format(tpl, f);
50 | });
51 |
52 | return '';
53 | }
54 |
55 |
56 | function load() {
57 | dom.findOne('#copy-formats').innerHTML = copyFormats();
58 |
59 | dom.find('[data-modifiers]').forEach(function (el) {
60 | el.innerHTML = modifiers(dom.attr(el, 'data-modifiers'));
61 | });
62 |
63 | dom.find('[data-bool]').forEach(function (el) {
64 | el.checked = !!preferences.val(dom.attr(el, 'data-bool'));
65 | });
66 |
67 | dom.find('[data-select]').forEach(function (el) {
68 | el.checked = preferences.val(dom.attr(el, 'data-select')) === dom.attr(el, 'data-select-value');
69 | });
70 |
71 | dom.find('[data-text]').forEach(function (el) {
72 | var t = preferences.val(dom.attr(el, 'data-text'));
73 | el.value = typeof t === 'undefined' ? '' : t;
74 | });
75 | }
76 |
77 | function save() {
78 | var prefs = {};
79 |
80 | dom.find('[data-modifier]').forEach(function (el) {
81 | var code = Number(dom.attr(el, 'data-modifier')),
82 | mode = dom.attr(el, 'data-mode');
83 | prefs['modifier.' + mode] = (prefs['modifier.' + mode] || 0) | (el.checked ? code : 0);
84 | });
85 |
86 | dom.find('[data-bool]').forEach(function (el) {
87 | prefs[dom.attr(el, 'data-bool')] = !!el.checked;
88 | });
89 |
90 | dom.find('[data-select]').forEach(function (el) {
91 | if (el.checked)
92 | prefs[dom.attr(el, 'data-select')] = dom.attr(el, 'data-select-value');
93 | });
94 |
95 | dom.find('[data-text]').forEach(function (el) {
96 | prefs[dom.attr(el, 'data-text')] = el.value;
97 | });
98 |
99 | preferences.setAll(prefs).then(load);
100 | message.background('preferencesUpdated');
101 | }
102 |
103 |
104 | window.onload = function () {
105 | preferences.load().then(load);
106 |
107 | event.listen(window, {
108 | input: save,
109 | change: save,
110 | click: function (e) {
111 | var cmd = dom.attr(e.target, 'data-command');
112 | if (cmd) {
113 | message.background({name: 'command', command: cmd});
114 | event.reset(e);
115 | }
116 | }
117 | });
118 | };
119 |
120 |
--------------------------------------------------------------------------------
/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
20 | head
21 | title Copytables Options
22 | script(src="options.js")
23 | link(rel="stylesheet" type="text/css" href="options.css")
24 |
25 | body
26 |
27 | h1 Copytables options
28 |
29 | h2 Modifier keys
30 | h3
31 | | Configure modifier keys for click-and-drag selection.
32 | | Note: some combinations might conflict with your system setup.
33 |
34 | .box
35 | table
36 | tr
37 | td
38 | b To select cells, hold
39 | td(data-modifiers="cell")
40 | tr
41 | td
42 | b To select columns, hold
43 | td(data-modifiers="column")
44 | tr
45 | td
46 | b To select rows, hold
47 | td(data-modifiers="row")
48 | tr
49 | td
50 | b To select tables, hold
51 | td(data-modifiers="table")
52 | tr
53 | td
54 | b To extend a selection, hold
55 | td(data-modifiers="extend")
56 |
57 |
58 | h2 Capture mode
59 | h3
60 | | In the capture mode, you press a hot key first and then select with a single click.
61 |
62 | .box
63 | table
64 | tr
65 | td: b Enabled
66 | td
67 | label.cb
68 | input(type="checkbox" data-bool="capture.enabled")
69 | span Yes
70 | tr
71 | td
72 | b Reset
73 | i Automatically turn the capture mode off after selecting
74 | td
75 | label.cb
76 | input(type="checkbox" data-bool="capture.reset")
77 | span Yes
78 | tr
79 | td: b Keyboard shortcuts
80 | td
81 | a(href="#" data-command="open_commands") Configure
82 |
83 |
84 | h2 Copy formats
85 | h3 Select copy formats you want to use. "Default" format is used when you select "Copy" from the Chrome menu.
86 |
87 | .box#copy-formats
88 |
89 | h2 Infobox
90 | h3 Infobox shows useful information about your selection (sum/average of numeric values etc).
91 |
92 | .box
93 | table
94 | tr
95 | td: b Enabled
96 | td
97 | label.cb
98 | input(type="checkbox" data-bool="infobox.enabled")
99 | span Yes
100 | tr
101 | td
102 | b Sticky
103 | i Keep infobox on screen until closed
104 | td
105 | label.cb
106 | input(type="checkbox" data-bool="infobox.sticky")
107 | span Yes
108 | tr
109 | td: b Position
110 | td
111 | table.infobox-position
112 | tr
113 | td
114 | div.position0: span
115 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="0")
116 | td
117 | div.position3: span
118 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="3")
119 | td
120 | div.position1: span
121 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="1")
122 | td
123 | div.position2: span
124 | input(type="radio" name="infobox.position" data-select="infobox.position" data-select-value="2")
125 |
126 | h2 Numbers
127 | h3 Configure how Copytables parses numbers.
128 |
129 | .box
130 | table
131 | tr
132 | td
133 | b Decimal point
134 | td
135 | input(type="text" data-text="number.decimal")
136 | tr
137 | td
138 | b Group separator
139 | td
140 | input(type="text" data-text="number.group")
141 |
142 |
143 |
144 |
145 | h2 Interface
146 | h3 Miscellaneous interface options.
147 |
148 | .box
149 | table
150 | tr
151 | td
152 | b Scroll speed
153 | i Values are from 1 (very slow scrolling) to 100 (very fast).
154 | td
155 | input(type="number" data-text="scroll.amount")
156 | tr
157 | td
158 | b Scroll acceleration
159 | i Values are from 0 (no acceleration) to 100 (max acceleration).
160 | td
161 | input(type="number" data-text="scroll.acceleration")
162 |
163 |
164 | h2 Support
165 |
166 | .box
167 |
168 | h4
169 | Contact me if you have questions or want to report a problem.
170 |
171 | h4
172 | a(href="https://merribithouse.net/copytables") About
173 | | |
174 | a(href="https://github.com/gebrkn/copytables") Source code
175 | | |
176 | | © 2014~present Georg Barikin, georg@merribithouse.net
177 |
178 |
--------------------------------------------------------------------------------
/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 |
143 | .cb
144 | display: block
145 | span
146 | visibility: hidden
147 | padding-left: 4px
148 |
149 | input:checked + span
150 | color: $accent
151 | visibility: visible
152 |
153 | .infobox-position
154 | td
155 | text-align: center
156 |
157 | td:first-child
158 | width: auto
159 |
160 | div
161 | width: 50px
162 | height: 30px
163 | border: 1px solid $accent
164 | position: relative
165 | margin-bottom: 5px
166 |
167 | span
168 |
169 | width: 25px
170 | height: 8px
171 | background: $accent
172 | position: absolute
173 |
174 | &.position0 span
175 | left: 1px
176 | top: 1px
177 |
178 | &.position1 span
179 | right: 1px
180 | top: 1px
181 |
182 | &.position2 span
183 | right: 1px
184 | bottom: 1px
185 |
186 | &.position3 span
187 | left: 1px
188 | bottom: 1px
189 |
190 | #custom-code
191 | width: 400px
192 | height: 200px
193 | font-family: "Monaco", "UbuntuMono", monospace
194 | font-size: 11px
195 | border: 1px solid $border
196 |
--------------------------------------------------------------------------------
/src/popup.js:
--------------------------------------------------------------------------------
1 | var
2 | dom = require('./lib/dom'),
3 | preferences = require('./lib/preferences'),
4 | message = require('./lib/message'),
5 | event = require('./lib/event'),
6 | util = require('./lib/util')
7 | ;
8 |
9 | function captureButtons() {
10 | var mode = preferences.val('_captureMode') || 'zzz';
11 |
12 | return preferences.captureModes().map(function (m) {
13 | return util.format(
14 | '${name} ',
15 | {
16 | id: m.id,
17 | name: m.name,
18 | cls: (m.id === mode) ? 'on' : ''
19 | });
20 | }).join('');
21 | }
22 |
23 | function copyButtons() {
24 | return preferences.copyFormats().filter(function (f) {
25 | return f.enabled;
26 | }).map(function (f) {
27 | return util.format(
28 | '${name} ',
29 | f);
30 | }).join('');
31 | }
32 |
33 | function update() {
34 |
35 | dom.findOne('#copy-buttons').innerHTML = copyButtons();
36 |
37 | if (preferences.val('capture.enabled')) {
38 | dom.findOne('#capture-row').style.display = '';
39 | dom.findOne('#capture-buttons').innerHTML = captureButtons();
40 | } else {
41 | dom.findOne('#capture-row').style.display = 'none';
42 | }
43 | }
44 |
45 | function init() {
46 | update();
47 |
48 | event.listen(document, {
49 | 'click': function (e) {
50 | var cmd = dom.attr(e.target, 'data-command');
51 | if (cmd) {
52 | message.background({name: 'command', command: cmd});
53 | }
54 | if (!dom.attr(e.target, 'data-keep-open')) {
55 | window.close();
56 | }
57 | }
58 | });
59 | }
60 |
61 | window.onload = function () {
62 | preferences.load().then(init);
63 | };
64 |
--------------------------------------------------------------------------------
/src/popup.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | script(src="popup.js")
5 | link(rel="stylesheet" type="text/css" href="popup.css")
6 |
7 | body
8 | table
9 | tr
10 | td
11 | b Copy:
12 | td
13 | span#copy-buttons
14 |
15 | tr
16 | td
17 | b Find:
18 | td
19 | button(data-command="find_previous" data-keep-open="1") Previous Table
20 | button(data-command="find_next" data-keep-open="1") Next Table
21 |
22 | tr#capture-row
23 | td
24 | b Capture:
25 | td
26 | span#capture-buttons
27 |
28 | div
29 | p
30 | a(data-command="open_options") Options
31 | p
32 | a(data-command="open_commands") Keyboard shortcuts
33 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/start-dev.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | for CMD in 'dev-server' 'chrome' 'watch'
4 | do
5 | sleep 2s
6 | osascript -e "tell application \"Terminal\" to do script \"cd ${PWD};npm run ${CMD}\""
7 | done
8 |
--------------------------------------------------------------------------------
/test/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "./test",
3 | "spec_files": [
4 | "./*_test.js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.js"
8 | ],
9 | "stopSpecOnExpectationFailure": false,
10 | "random": true
11 | }
12 |
--------------------------------------------------------------------------------
/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 |
10 | describe('parse returns null', function () {
11 | it('with no input', () =>
12 | expect(number.parse('', formats.en)).toBe(null));
13 |
14 | it('with no digits', () =>
15 | expect(number.parse('abc', formats.en)).toBe(null));
16 |
17 | it('with invalid chars', () =>
18 | expect(number.parse('12@34', formats.en)).toBe(null));
19 |
20 | it('with just decimal point', () =>
21 | expect(number.parse('.', formats.en)).toBe(null));
22 |
23 | it('with two decimal points', () =>
24 | expect(number.parse('123.123.456', formats.en)).toBe(null));
25 |
26 | it('with a non-integer fraction', () =>
27 | expect(number.parse('123.123,456', formats.en)).toBe(null));
28 |
29 | it('with a group more than 3', () =>
30 | expect(number.parse('123,1234', formats.en)).toBe(null));
31 |
32 | it('with a group less than 2', () =>
33 | expect(number.parse('123,1,234', formats.en)).toBe(null));
34 |
35 | it('with an empty group', () =>
36 | expect(number.parse('123,,234', formats.en)).toBe(null));
37 |
38 | it('with overflow', () =>
39 | expect(number.parse('234234234234234234234234234234234', formats.en)).toBe(null));
40 |
41 | it('with decimal overflow', () =>
42 | expect(number.parse('123.234234234234234234234234234234234', formats.en)).toBe(null));
43 | });
44 |
45 | describe('extract returns null', function () {
46 | it('with no input', () =>
47 | expect(number.extract('', formats.en)).toBe(null));
48 |
49 | it('with no digits', () =>
50 | expect(number.extract('abc', formats.en)).toBe(null));
51 |
52 | it('with more than one number', () =>
53 | expect(number.extract('abc 123 def 123.456', formats.en)).toBe(null));
54 |
55 | it('with invalid number', () =>
56 | expect(number.extract('abc 123.456.67', formats.en)).toBe(null));
57 |
58 | });
59 |
60 |
61 | describe('parse is ok', function () {
62 | it('with an integer', () =>
63 | expect(number.parse('12345', formats.en)).toBe(12345));
64 | it('with an negative integer', () =>
65 | expect(number.parse('-12345', formats.en)).toBe(-12345));
66 | it('with a decimal point', () =>
67 | expect(number.parse('12345.678', formats.en)).toBe(12345.678));
68 | it('with a void fraction', () =>
69 | expect(number.parse('12345.', formats.en)).toBe(12345));
70 | it('with a void fraction', () =>
71 | expect(number.parse('12345.', formats.en)).toBe(12345));
72 | it('with a void int part', () =>
73 | expect(number.parse('.123', formats.en)).toBe(0.123));
74 | it('with groups', () =>
75 | expect(number.parse('1,23,456.789', formats.en)).toBe(123456.789));
76 | it('with de groups', () =>
77 | expect(number.parse('1.23.456,789', formats.de)).toBe(123456.789));
78 | it('with cz groups', () =>
79 | expect(number.parse('1 23 456,789', formats.cz)).toBe(123456.789));
80 |
81 | it('with long decimal points', () =>
82 | expect(number.parse('552.123', formats.en)).toBe(552.123));
83 | it('with leading zero decimal points', () =>
84 | expect(number.parse('552.005', formats.en)).toBe(552.005));
85 | it('with zero decimal points', () =>
86 | expect(number.parse('552.000', formats.en)).toBe(552));
87 | });
88 |
89 |
90 | describe('extract is ok', function () {
91 | it('with an integer', () =>
92 | expect(number.extract('abc 12345 def', formats.en)).toBe(12345));
93 | it('with a negative integer', () =>
94 | expect(number.extract('abc -12345 def', formats.en)).toBe(-12345));
95 | it('with en groups and decimals', () =>
96 | expect(number.extract('abc -1,345,678.9 def', formats.en)).toBe(-1345678.9));
97 | it('with de groups and decimals', () =>
98 | expect(number.extract('abc -1.345.678,9 def', formats.de)).toBe(-1345678.9));
99 | it('with cz groups and decimals', () =>
100 | expect(number.extract('abc -1 345 678,9 def', formats.cz)).toBe(-1345678.9));
101 | });
102 |
103 |
104 |
--------------------------------------------------------------------------------
/test/server/base.html:
--------------------------------------------------------------------------------
1 | copytables test page
2 |
3 |
23 |
24 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{{content}}}
38 |
--------------------------------------------------------------------------------
/test/server/frame.html:
--------------------------------------------------------------------------------
1 |
16 |
23 |
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++)
10 | a.push(fn(i));
11 | return a;
12 | };
13 |
14 | let file = path => fs.readFileSync(path, 'utf8');
15 |
16 | let renderHelpers = {
17 |
18 | textSource() {
19 | return `
20 | All Gaul is divided into three parts, one of which the Belgae inhabit, the Aquitani
21 | another, those who in their own language are called Celts, in our Gauls, the third. All
22 | these differ from each other in language, customs and laws. The river Garonne separates
23 | the Gauls from the Aquitani; the Marne and the Seine separate them from the Belgae. Of all
24 | these, the Belgae are the bravest, because they are furthest from the civilization and
25 | refinement of [our] Province, and merchants least frequently resort to them, and import
26 | those things which tend to effeminate the mind; and they are the nearest to the Germans,
27 | who dwell beyond the Rhine, with whom they are continually waging war; for which reason
28 | the Helvetii also surpass the rest of the Gauls in valor, as they contend with the Germans
29 | in almost daily battles, when they either repel them from their own territories, or
30 | themselves wage war on their frontiers. One part of these, which it has been said that the
31 | Gauls occupy, takes its beginning at the river Rhone; it is bounded by the river Garonne,
32 | the ocean, and the territories of the Belgae; it borders, too, on the side of the Sequani
33 | and the Helvetii, upon the river Rhine, and stretches toward the north. The Belgae rises
34 | from the extreme frontier of Gaul, extend to the lower part of the river Rhine; and look
35 | toward the north and the rising sun. Aquitania extends from the river Garonne to the
36 | Pyrenaean mountains and to that part of the ocean which is near Spain: it looks between
37 | the setting of the sun, and the north star.
38 | `;
39 | },
40 |
41 | text() {
42 | let text = this.textSource();
43 | let a = Math.floor(Math.random() * text.length);
44 | let b = Math.floor(Math.random() * text.length);
45 |
46 | return '' + text.substr(a, b) + '
';
47 | },
48 |
49 | numTable(rows, cols) {
50 | let s = '';
51 | rows.times(function (r) {
52 | s += '';
53 | cols.times(function (c) {
54 | s += `${r + 1}.${c + 1} `;
55 | });
56 | s += ' ';
57 | });
58 | return ``;
59 | }
60 | };
61 |
62 |
63 | let renderTemplate = tpl => {
64 | let path;
65 |
66 | path = `${__dirname}/templates/${tpl}.html`;
67 | if (fs.existsSync(path)) {
68 | return mustache.render(file(path), {
69 | text: renderHelpers.text(),
70 | });
71 | }
72 |
73 | path = `${__dirname}/templates/${tpl}.js`;
74 | if (fs.existsSync(path)) {
75 | return require(path).render(renderHelpers);
76 | }
77 |
78 | return `${tpl}=404`;
79 |
80 | };
81 |
82 | let renderDoc = content => mustache.render(
83 | file(`${__dirname}/base.html`),
84 | {content: content}
85 | );
86 |
87 |
88 | //
89 |
90 | app.use(express.static(__dirname + '/public'));
91 |
92 | app.get('/only/:tpl', (req, res, next) => {
93 | let content = req.params.tpl.split(',').map(renderTemplate).join('');
94 | res.send(renderDoc(content));
95 | });
96 |
97 | app.get('/base', (req, res, next) => {
98 | res.send(renderDoc(''));
99 | });
100 |
101 | app.get('/frame', (req, res, next) => {
102 | res.send(file(`${__dirname}/frame.html`));
103 | });
104 |
105 | app.get('/raw/:tpl', (req, res, next) => {
106 | let content = req.params.tpl.split(',').map(renderTemplate).join('');
107 | res.send(content);
108 | });
109 |
110 |
111 | app.get('/all', (req, res, next) => {
112 | let content = '',
113 | all = 'simple spans numbers forms hidden framea nested scroll frameb styled frameset dynamic';
114 |
115 | all.split(' ').forEach(tpl => {
116 | content += `${tpl} `;
117 | content += renderTemplate(tpl);
118 | content += renderHelpers.text();
119 | });
120 |
121 | res.send(renderDoc(content));
122 | });
123 |
124 | app.listen(9876);
125 |
--------------------------------------------------------------------------------
/test/server/public/img/a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/test/server/public/img/a.png
--------------------------------------------------------------------------------
/test/server/public/img/b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/test/server/public/img/b.png
--------------------------------------------------------------------------------
/test/server/public/img/c.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gebrkn/copytables/2eeda5d09dd8ffbf2d00fa5535c71644fc17ecdc/test/server/public/img/c.png
--------------------------------------------------------------------------------
/test/server/templates/big.js:
--------------------------------------------------------------------------------
1 |
2 | module.exports.render = h => {
3 |
4 | function bgcolor(n) {
5 | n = ((n * 123456789) % 0xffffff).toString(16);
6 | while(n.length < 6) n = '0' + n;
7 | return `style="background-color:#${n}"`;
8 | }
9 |
10 | function chunk(n) {
11 | n = n % text.length;
12 | var a = text.substr(n + 0, n + 20);
13 | var b = text.substr(n + 20, n + 40);
14 | var c = text.substr(n + 40, n + 60);
15 | var d = text.substr(n + 60, n + 80);
16 |
17 | a = `${a} ${b} ${c} ${d} `;
18 | return a + ' ' + a + ' ' + a;
19 | }
20 |
21 | function num(n, m) {
22 | return (n & 0xffff) * m;
23 | }
24 |
25 | let rc = 500,
26 | rows = [],
27 | text = h.textSource();
28 |
29 | rc.times(function (i) {
30 | let cells = [];
31 |
32 | let k = Math.pow(7, (i % 300)) % 123456;
33 |
34 | cells.push(`${i} `);
35 | cells.push(`${k} `);
36 | cells.push(`${num(k, 123456)},${num(k, 123456)},${num(k, 123456)},${num(k, 123456)},${num(k, 123456)} `);
37 | cells.push(`${chunk(k)} `);
38 | cells.push(`${chunk(k + 10)} `);
39 | cells.push(`${num(k, 9876)} `);
40 |
41 | rows.push(`${cells.join('')} `);
42 | });
43 |
44 |
45 | return `
46 |
56 |
59 | `
60 | };
--------------------------------------------------------------------------------
/test/server/templates/dynamic.html:
--------------------------------------------------------------------------------
1 |
49 |
50 |
63 |
64 |
65 |
66 | load
67 |
68 |
--------------------------------------------------------------------------------
/test/server/templates/forms.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test/server/templates/hidden.html:
--------------------------------------------------------------------------------
1 |
2 | HIDDEN 123
3 | 9999999 456
4 | 111111
5 |
6 |
7 |
8 |
9 | HIDDEN TABLE:
10 |
11 |
12 | HIDDEN 123
13 | 9999999 456
14 | 111111
15 |
16 |
17 | {{{text}}}
18 |
19 | NORMAL TABLE:
20 |
21 |
22 |
23 | HIDDEN 123
24 | 9999999 456
25 | 111111
26 |
27 |
--------------------------------------------------------------------------------
/test/server/templates/nested.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | one
4 |
5 |
6 |
7 | two
8 |
9 |
10 |
11 | three
12 |
13 |
14 |
15 |
16 |
17 |
18 | four
19 |
20 |
21 | one
22 |
23 |
24 |
25 | two
26 |
27 |
28 |
29 | three
30 |
31 |
32 |
33 |
34 |
35 |
36 | four
37 |
38 |
39 |
40 | {{{text}}}
41 |
--------------------------------------------------------------------------------
/test/server/templates/numbers.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 | #
11 | Date
12 | price
13 |
14 |
15 |
16 |
17 | 3
18 | 04/12/2016
19 | $0.11
20 |
21 |
22 | 10
23 | 04/14/2016
24 | $0.22
25 |
26 |
27 | 5
28 | 04/19/2016
29 | $0.33
30 |
31 |
32 | 10
33 | 04/19/2016
34 | $0.44
35 |
36 |
37 | 2
38 | 04/20/2016
39 | $0.55
40 |
41 |
42 | 2
43 | 04/20/2016
44 | $0.66
45 |
46 |
47 | -567
48 | 04/19/2016
49 | $0.77
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | #
58 | price
59 |
60 |
61 |
62 |
63 | 3
64 | -0,3
65 |
66 |
67 | 3
68 | 0,341
69 |
70 |
71 | 3
72 | -1.245,576
73 |
74 |
75 | 3
76 | -1.245.678,576
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | #
85 | price
86 |
87 |
88 |
89 |
90 | 3
91 | -0,3
92 |
93 |
94 | 3
95 | -1 245,576
96 |
97 |
98 | 3
99 | -1 245 678,576
100 |
101 |
102 |
103 |
104 |
105 | {{{text}}}
106 |
--------------------------------------------------------------------------------
/test/server/templates/paste.html:
--------------------------------------------------------------------------------
1 |
13 |
14 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {{{content}}}
33 |
--------------------------------------------------------------------------------
/test/server/templates/script.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ONE: :END
5 |
6 |
7 |
8 |
13 | TWO: ??? :END
14 |
15 |
16 |
17 | three
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/server/templates/scroll.js:
--------------------------------------------------------------------------------
1 | module.exports.render = h => `
2 |
16 |
17 |
18 | ${h.numTable(20, 30)}
19 |
20 |
21 | ${h.text()}
22 | `;
23 |
--------------------------------------------------------------------------------
/test/server/templates/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | hello
4 | world
5 | link relative
6 |
7 |
8 | row2
9 | cell 2.2
10 | link relative 2
11 |
12 |
13 |
14 | image a
15 | link absolute
16 |
17 |
18 |
19 | image b
20 |
21 |
22 |
23 | image c
24 |
25 |
26 |
27 | external img
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/server/templates/spans.html:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 | Server
22 | Option
23 | Status
24 | Date
25 |
26 |
27 | Before update
28 | After update
29 |
30 |
31 | alpha
32 | HDD
33 | ok
34 | failed
35 | 2015/01/01
36 |
37 |
38 | SSD
39 | ok
40 | 2015/01/02
41 |
42 |
43 | beta
44 | HDD
45 | failed
46 | ok
47 | 2015/01/03
48 |
49 |
50 | SSD
51 | wait
52 | 2015/01/04
53 |
54 |
55 | no data
56 | 2015/01/05
57 |
58 |
59 | 2015/01/06
60 |
61 |
62 |
63 | {{{text}}}
64 |
--------------------------------------------------------------------------------
/test/server/templates/styled.html:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
47 |
48 | default style
49 | class 1 style
50 | class 2 style
51 | inline style
52 |
53 |
54 | class 2 + inline
55 | external background
56 | external background - inline
57 |
58 |
59 | td animated
60 |
61 |
62 | tr animated
63 | tr animated
64 | tr animated
65 | tr animated
66 |
67 |
68 | TD = NOT SELECTABLE
69 | TD = NOT SELECTABLE
70 |
71 |
72 |
73 |
74 |
75 | TABLE = NOT SELECTABLE
76 | TABLE = NOT SELECTABLE
77 |
78 |
79 |
80 | {{{text}}}
81 |
--------------------------------------------------------------------------------