aboutsummaryrefslogtreecommitdiff
path: root/src/content
diff options
context:
space:
mode:
authorShin'ya Ueoka <ueokande@i-beam.org>2018-05-01 15:51:40 +0900
committerShin'ya Ueoka <ueokande@i-beam.org>2018-05-01 15:51:40 +0900
commit9da2f5fd786ff7ef64eca0a86efb1c0b9a164244 (patch)
treeb888349817d0016dc1499932671bbe6cd95efdaa /src/content
parentf9889b7f2b634bfe5a540633d8952f4a6f900658 (diff)
parent89d6afecfd257ff4fc62748f771abf37ef3a2852 (diff)
Merge remote-tracking branch 'origin/master' into patch-1
Diffstat (limited to 'src/content')
-rw-r--r--src/content/actions/find.js59
-rw-r--r--src/content/actions/follow-controller.js3
-rw-r--r--src/content/actions/operation.js32
-rw-r--r--src/content/actions/setting.js9
-rw-r--r--src/content/components/common/follow.js28
-rw-r--r--src/content/components/common/input.js6
-rw-r--r--src/content/components/common/keymapper.js3
-rw-r--r--src/content/components/top-content/find.js13
-rw-r--r--src/content/components/top-content/follow-controller.js23
-rw-r--r--src/content/console-frames.js9
-rw-r--r--src/content/focuses.js13
-rw-r--r--src/content/navigates.js62
-rw-r--r--src/content/reducers/find.js2
-rw-r--r--src/content/reducers/follow-controller.js2
-rw-r--r--src/content/scrolls.js177
-rw-r--r--src/content/urls.js26
16 files changed, 327 insertions, 140 deletions
diff --git a/src/content/actions/find.js b/src/content/actions/find.js
index 80d6210..c7345cc 100644
--- a/src/content/actions/find.js
+++ b/src/content/actions/find.js
@@ -5,6 +5,7 @@
// NOTE: window.find is not standard API
// https://developer.mozilla.org/en-US/docs/Web/API/Window/find
+import messages from 'shared/messages';
import actions from 'content/actions';
import * as consoleFrames from '../console-frames';
@@ -14,6 +15,13 @@ const postPatternNotFound = (pattern) => {
'Pattern not found: ' + pattern);
};
+const postPatternFound = (pattern) => {
+ return consoleFrames.postInfo(
+ window.document,
+ 'Pattern found: ' + pattern,
+ );
+};
+
const find = (string, backwards) => {
let caseSensitive = false;
let wrapScan = true;
@@ -24,32 +32,49 @@ const find = (string, backwards) => {
return window.find(string, caseSensitive, backwards, wrapScan);
};
-const findNext = (keyword, reset, backwards) => {
+const findNext = (currentKeyword, reset, backwards) => {
if (reset) {
window.getSelection().removeAllRanges();
}
- let found = find(keyword, backwards);
- if (!found) {
- window.getSelection().removeAllRanges();
- found = find(keyword, backwards);
+ let promise = Promise.resolve(currentKeyword);
+ if (currentKeyword) {
+ browser.runtime.sendMessage({
+ type: messages.FIND_SET_KEYWORD,
+ keyword: currentKeyword,
+ });
+ } else {
+ promise = browser.runtime.sendMessage({
+ type: messages.FIND_GET_KEYWORD,
+ });
}
- if (!found) {
- postPatternNotFound(keyword);
- }
- return {
- type: actions.FIND_SET_KEYWORD,
- keyword,
- found,
- };
+
+ return promise.then((keyword) => {
+ let found = find(keyword, backwards);
+ if (!found) {
+ window.getSelection().removeAllRanges();
+ found = find(keyword, backwards);
+ }
+ if (found) {
+ postPatternFound(keyword);
+ } else {
+ postPatternNotFound(keyword);
+ }
+
+ return {
+ type: actions.FIND_SET_KEYWORD,
+ keyword,
+ found,
+ };
+ });
};
-const next = (keyword, reset) => {
- return findNext(keyword, reset, false);
+const next = (currentKeyword, reset) => {
+ return findNext(currentKeyword, reset, false);
};
-const prev = (keyword, reset) => {
- return findNext(keyword, reset, true);
+const prev = (currentKeyword, reset) => {
+ return findNext(currentKeyword, reset, true);
};
export { next, prev };
diff --git a/src/content/actions/follow-controller.js b/src/content/actions/follow-controller.js
index 3fd4dce..006b248 100644
--- a/src/content/actions/follow-controller.js
+++ b/src/content/actions/follow-controller.js
@@ -1,9 +1,10 @@
import actions from 'content/actions';
-const enable = (newTab) => {
+const enable = (newTab, background) => {
return {
type: actions.FOLLOW_CONTROLLER_ENABLE,
newTab,
+ background,
};
};
diff --git a/src/content/actions/operation.js b/src/content/actions/operation.js
index 767f14b..9171766 100644
--- a/src/content/actions/operation.js
+++ b/src/content/actions/operation.js
@@ -2,12 +2,16 @@ import operations from 'shared/operations';
import messages from 'shared/messages';
import * as scrolls from 'content/scrolls';
import * as navigates from 'content/navigates';
+import * as focuses from 'content/focuses';
import * as urls from 'content/urls';
import * as consoleFrames from 'content/console-frames';
import * as addonActions from './addon';
+import * as properties from 'shared/settings/properties';
// eslint-disable-next-line complexity
-const exec = (operation) => {
+const exec = (operation, repeat, settings) => {
+ let smoothscroll = settings.properties.smoothscroll ||
+ properties.defaults.smoothscroll;
switch (operation.type) {
case operations.ADDON_ENABLE:
return addonActions.enable();
@@ -24,23 +28,24 @@ const exec = (operation) => {
type: messages.FIND_PREV,
}), '*');
case operations.SCROLL_VERTICALLY:
- return scrolls.scrollVertically(window, operation.count);
+ return scrolls.scrollVertically(operation.count, smoothscroll, repeat);
case operations.SCROLL_HORIZONALLY:
- return scrolls.scrollHorizonally(window, operation.count);
+ return scrolls.scrollHorizonally(operation.count, smoothscroll, repeat);
case operations.SCROLL_PAGES:
- return scrolls.scrollPages(window, operation.count);
+ return scrolls.scrollPages(operation.count, smoothscroll, repeat);
case operations.SCROLL_TOP:
- return scrolls.scrollTop(window);
+ return scrolls.scrollTop(smoothscroll, repeat);
case operations.SCROLL_BOTTOM:
- return scrolls.scrollBottom(window);
+ return scrolls.scrollBottom(smoothscroll, repeat);
case operations.SCROLL_HOME:
- return scrolls.scrollHome(window);
+ return scrolls.scrollHome(smoothscroll, repeat);
case operations.SCROLL_END:
- return scrolls.scrollEnd(window);
+ return scrolls.scrollEnd(smoothscroll, repeat);
case operations.FOLLOW_START:
return window.top.postMessage(JSON.stringify({
type: messages.FOLLOW_START,
- newTab: operation.newTab
+ newTab: operation.newTab,
+ background: operation.background,
}), '*');
case operations.NAVIGATE_HISTORY_PREV:
return navigates.historyPrev(window);
@@ -54,12 +59,13 @@ const exec = (operation) => {
return navigates.parent(window);
case operations.NAVIGATE_ROOT:
return navigates.root(window);
+ case operations.FOCUS_INPUT:
+ return focuses.focusInput();
case operations.URLS_YANK:
urls.yank(window);
- return consoleFrames.postMessage(window.document, {
- type: messages.CONSOLE_SHOW_INFO,
- text: 'Current url yanked',
- });
+ return consoleFrames.postInfo(window.document, 'Current url yanked');
+ case operations.URLS_PASTE:
+ return urls.paste(window, operation.newTab ? operation.newTab : false);
default:
browser.runtime.sendMessage({
type: messages.BACKGROUND_OPERATION,
diff --git a/src/content/actions/setting.js b/src/content/actions/setting.js
index 0238c71..4c1e385 100644
--- a/src/content/actions/setting.js
+++ b/src/content/actions/setting.js
@@ -1,10 +1,17 @@
import actions from 'content/actions';
import * as keyUtils from 'shared/utils/keys';
+import operations from 'shared/operations';
+
+const reservedKeymaps = {
+ '<Esc>': { type: operations.CANCEL },
+ '<C-[>': { type: operations.CANCEL },
+};
const set = (value) => {
let entries = [];
if (value.keymaps) {
- entries = Object.entries(value.keymaps).map((entry) => {
+ let keymaps = Object.assign({}, value.keymaps, reservedKeymaps);
+ entries = Object.entries(keymaps).map((entry) => {
return [
keyUtils.fromMapKeys(entry[0]),
entry[1],
diff --git a/src/content/components/common/follow.js b/src/content/components/common/follow.js
index 7a35105..2a55ea3 100644
--- a/src/content/components/common/follow.js
+++ b/src/content/components/common/follow.js
@@ -4,7 +4,8 @@ import * as dom from 'shared/utils/dom';
const TARGET_SELECTOR = [
'a', 'button', 'input', 'textarea', 'area',
- '[contenteditable=true]', '[contenteditable=""]', '[tabindex]'
+ '[contenteditable=true]', '[contenteditable=""]', '[tabindex]',
+ '[role="button"]'
].join(',');
@@ -29,11 +30,27 @@ const inViewport = (win, element, viewSize, framePosition) => {
return true;
};
+const isAriaHiddenOrAriaDisabled = (win, element) => {
+ if (!element || win.document.documentElement === element) {
+ return false;
+ }
+ for (let attr of ['aria-hidden', 'aria-disabled']) {
+ if (element.hasAttribute(attr)) {
+ let hidden = element.getAttribute(attr).toLowerCase();
+ if (hidden === '' || hidden === 'true') {
+ return true;
+ }
+ }
+ }
+ return isAriaHiddenOrAriaDisabled(win, element.parentNode);
+};
+
export default class Follow {
constructor(win, store) {
this.win = win;
this.store = store;
this.newTab = false;
+ this.background = false;
this.hints = {};
this.targets = [];
@@ -47,6 +64,7 @@ export default class Follow {
this.win.parent.postMessage(JSON.stringify({
type: messages.FOLLOW_KEY_PRESS,
key: key.key,
+ ctrlKey: key.ctrlKey,
}), '*');
return true;
}
@@ -68,6 +86,7 @@ export default class Follow {
type: messages.OPEN_URL,
url: element.href,
newTab: true,
+ background: this.background,
});
}
@@ -79,12 +98,13 @@ export default class Follow {
}), '*');
}
- createHints(keysArray, newTab) {
+ createHints(keysArray, newTab, background) {
if (keysArray.length !== this.targets.length) {
throw new Error('illegal hint count');
}
this.newTab = newTab;
+ this.background = background;
this.hints = {};
for (let i = 0; i < keysArray.length; ++i) {
let keys = keysArray[i];
@@ -150,7 +170,8 @@ export default class Follow {
case messages.FOLLOW_REQUEST_COUNT_TARGETS:
return this.countHints(sender, message.viewSize, message.framePosition);
case messages.FOLLOW_CREATE_HINTS:
- return this.createHints(message.keysArray, message.newTab);
+ return this.createHints(
+ message.keysArray, message.newTab, message.background);
case messages.FOLLOW_SHOW_HINTS:
return this.showHints(message.keys);
case messages.FOLLOW_ACTIVATE:
@@ -170,6 +191,7 @@ export default class Follow {
style.visibility !== 'hidden' &&
element.type !== 'hidden' &&
element.offsetHeight > 0 &&
+ !isAriaHiddenOrAriaDisabled(win, element) &&
inViewport(win, element, viewSize, framePosition);
});
return filtered;
diff --git a/src/content/components/common/input.js b/src/content/components/common/input.js
index 22b0a91..eefaf10 100644
--- a/src/content/components/common/input.js
+++ b/src/content/components/common/input.js
@@ -1,6 +1,10 @@
import * as dom from 'shared/utils/dom';
import * as keys from 'shared/utils/keys';
+const cancelKey = (e) => {
+ return e.key === 'Escape' || e.key === '[' && e.ctrlKey;
+};
+
export default class InputComponent {
constructor(target) {
this.pressed = {};
@@ -37,7 +41,7 @@ export default class InputComponent {
capture(e) {
if (this.fromInput(e)) {
- if (e.key === 'Escape' && e.target.blur) {
+ if (cancelKey(e) && e.target.blur) {
e.target.blur();
}
return;
diff --git a/src/content/components/common/keymapper.js b/src/content/components/common/keymapper.js
index fb8fabe..d9d1b2f 100644
--- a/src/content/components/common/keymapper.js
+++ b/src/content/components/common/keymapper.js
@@ -47,7 +47,8 @@ export default class KeymapperComponent {
return true;
}
let operation = keymaps.get(matched[0]);
- this.store.dispatch(operationActions.exec(operation));
+ this.store.dispatch(operationActions.exec(
+ operation, key.repeat, state.setting));
this.store.dispatch(inputActions.clearKeys());
return true;
}
diff --git a/src/content/components/top-content/find.js b/src/content/components/top-content/find.js
index bccf040..4d46d79 100644
--- a/src/content/components/top-content/find.js
+++ b/src/content/components/top-content/find.js
@@ -1,6 +1,5 @@
import * as findActions from 'content/actions/find';
import messages from 'shared/messages';
-import * as consoleFrames from '../../console-frames';
export default class FindComponent {
constructor(win, store) {
@@ -32,23 +31,11 @@ export default class FindComponent {
next() {
let state = this.store.getState().find;
-
- if (!state.found) {
- return consoleFrames.postError(
- window.document,
- 'Pattern not found: ' + state.keyword);
- }
return this.store.dispatch(findActions.next(state.keyword, false));
}
prev() {
let state = this.store.getState().find;
-
- if (!state.found) {
- return consoleFrames.postError(
- window.document,
- 'Pattern not found: ' + state.keyword);
- }
return this.store.dispatch(findActions.prev(state.keyword, false));
}
}
diff --git a/src/content/components/top-content/follow-controller.js b/src/content/components/top-content/follow-controller.js
index d373177..7f36604 100644
--- a/src/content/components/top-content/follow-controller.js
+++ b/src/content/components/top-content/follow-controller.js
@@ -1,8 +1,7 @@
import * as followControllerActions from 'content/actions/follow-controller';
import messages from 'shared/messages';
import HintKeyProducer from 'content/hint-key-producer';
-
-const DEFAULT_HINT_CHARSET = 'abcdefghijklmnopqrstuvwxyz';
+import * as properties from 'shared/settings/properties';
const broadcastMessage = (win, message) => {
let json = JSON.stringify(message);
@@ -29,11 +28,11 @@ export default class FollowController {
switch (message.type) {
case messages.FOLLOW_START:
return this.store.dispatch(
- followControllerActions.enable(message.newTab));
+ followControllerActions.enable(message.newTab, message.background));
case messages.FOLLOW_RESPONSE_COUNT_TARGETS:
return this.create(message.count, sender);
case messages.FOLLOW_KEY_PRESS:
- return this.keyPress(message.key);
+ return this.keyPress(message.key, message.ctrlKey);
}
}
@@ -70,7 +69,11 @@ export default class FollowController {
});
}
- keyPress(key) {
+ keyPress(key, ctrlKey) {
+ if (key === '[' && ctrlKey) {
+ this.store.dispatch(followControllerActions.disable());
+ return true;
+ }
switch (key) {
case 'Enter':
this.activate();
@@ -84,7 +87,7 @@ export default class FollowController {
this.store.dispatch(followControllerActions.backspace());
break;
default:
- if (DEFAULT_HINT_CHARSET.includes(key)) {
+ if (this.hintchars().includes(key)) {
this.store.dispatch(followControllerActions.keyPress(key));
}
break;
@@ -93,7 +96,7 @@ export default class FollowController {
}
count() {
- this.producer = new HintKeyProducer(DEFAULT_HINT_CHARSET);
+ this.producer = new HintKeyProducer(this.hintchars());
let doc = this.win.document;
let viewWidth = this.win.innerWidth || doc.documentElement.clientWidth;
let viewHeight = this.win.innerHeight || doc.documentElement.clientHeight;
@@ -126,6 +129,7 @@ export default class FollowController {
type: messages.FOLLOW_CREATE_HINTS,
keysArray: produced,
newTab: this.state.newTab,
+ background: this.state.background,
}), '*');
}
@@ -135,4 +139,9 @@ export default class FollowController {
type: messages.FOLLOW_REMOVE_HINTS,
});
}
+
+ hintchars() {
+ return this.store.getState().setting.properties.hintchars ||
+ properties.defaults.hintchars;
+ }
}
diff --git a/src/content/console-frames.js b/src/content/console-frames.js
index 515ae09..0c0ec02 100644
--- a/src/content/console-frames.js
+++ b/src/content/console-frames.js
@@ -28,4 +28,11 @@ const postError = (doc, message) => {
});
};
-export { initialize, blur, postMessage, postError };
+const postInfo = (doc, message) => {
+ return postMessage(doc, {
+ type: messages.CONSOLE_SHOW_INFO,
+ text: message,
+ });
+};
+
+export { initialize, blur, postError, postInfo };
diff --git a/src/content/focuses.js b/src/content/focuses.js
new file mode 100644
index 0000000..a6f6cc8
--- /dev/null
+++ b/src/content/focuses.js
@@ -0,0 +1,13 @@
+import * as doms from 'shared/utils/dom';
+
+const focusInput = () => {
+ let inputTypes = ['email', 'number', 'search', 'tel', 'text', 'url'];
+ let inputSelector = inputTypes.map(type => `input[type=${type}]`).join(',');
+ let targets = window.document.querySelectorAll(inputSelector + ',textarea');
+ let target = Array.from(targets).find(doms.isVisible);
+ if (target) {
+ target.focus();
+ }
+};
+
+export { focusInput };
diff --git a/src/content/navigates.js b/src/content/navigates.js
index 64e5fc0..c9baa30 100644
--- a/src/content/navigates.js
+++ b/src/content/navigates.js
@@ -1,17 +1,18 @@
-const PREV_LINK_PATTERNS = [
- /\bprev\b/i, /\bprevious\b/i, /\bback\b/i,
- /</, /\u2039/, /\u2190/, /\xab/, /\u226a/, /<</
-];
-const NEXT_LINK_PATTERNS = [
- /\bnext\b/i,
- />/, /\u203a/, /\u2192/, /\xbb/, /\u226b/, />>/
-];
-
-const findLinkByPatterns = (win, patterns) => {
- let links = win.document.getElementsByTagName('a');
- return Array.prototype.find.call(links, (link) => {
- return patterns.some(ptn => ptn.test(link.textContent));
- });
+const REL_PATTERN = {
+ prev: /^(?:prev(?:ious)?|older)\b|\u2039|\u2190|\xab|\u226a|<</i,
+ next: /^(?:next|newer)\b|\u203a|\u2192|\xbb|\u226b|>>/i,
+};
+
+// Return the last element in the document matching the supplied selector
+// and the optional filter, or null if there are no matches.
+const selectLast = (win, selector, filter) => {
+ let nodes = win.document.querySelectorAll(selector);
+
+ if (filter) {
+ nodes = Array.from(nodes).filter(filter);
+ }
+
+ return nodes.length ? nodes[nodes.length - 1] : null;
};
const historyPrev = (win) => {
@@ -22,30 +23,37 @@ const historyNext = (win) => {
win.history.forward();
};
-const linkPrev = (win) => {
- let link = win.document.querySelector('a[rel=prev]');
+// Code common to linkPrev and linkNext which navigates to the specified page.
+const linkRel = (win, rel) => {
+ let link = selectLast(win, `link[rel~=${rel}][href]`);
+
if (link) {
- return link.click();
+ win.location = link.href;
+ return;
}
- link = findLinkByPatterns(win, PREV_LINK_PATTERNS);
+
+ const pattern = REL_PATTERN[rel];
+
+ link = selectLast(win, `a[rel~=${rel}][href]`) ||
+ // `innerText` is much slower than `textContent`, but produces much better
+ // (i.e. less unexpected) results
+ selectLast(win, 'a[href]', lnk => pattern.test(lnk.innerText));
+
if (link) {
link.click();
}
};
+const linkPrev = (win) => {
+ linkRel(win, 'prev');
+};
+
const linkNext = (win) => {
- let link = win.document.querySelector('a[rel=next]');
- if (link) {
- return link.click();
- }
- link = findLinkByPatterns(win, NEXT_LINK_PATTERNS);
- if (link) {
- link.click();
- }
+ linkRel(win, 'next');
};
const parent = (win) => {
- let loc = win.location;
+ const loc = win.location;
if (loc.hash !== '') {
loc.hash = '';
return;
diff --git a/src/content/reducers/find.js b/src/content/reducers/find.js
index eb43c37..8d63ee5 100644
--- a/src/content/reducers/find.js
+++ b/src/content/reducers/find.js
@@ -1,7 +1,7 @@
import actions from 'content/actions';
const defaultState = {
- keyword: '',
+ keyword: null,
found: false,
};
diff --git a/src/content/reducers/follow-controller.js b/src/content/reducers/follow-controller.js
index 2afb232..78fd848 100644
--- a/src/content/reducers/follow-controller.js
+++ b/src/content/reducers/follow-controller.js
@@ -3,6 +3,7 @@ import actions from 'content/actions';
const defaultState = {
enabled: false,
newTab: false,
+ background: false,
keys: '',
};
@@ -12,6 +13,7 @@ export default function reducer(state = defaultState, action = {}) {
return Object.assign({}, state, {
enabled: true,
newTab: action.newTab,
+ background: action.background,
keys: '',
});
case actions.FOLLOW_CONTROLLER_DISABLE:
diff --git a/src/content/scrolls.js b/src/content/scrolls.js
index ef38273..0d1f7c8 100644
--- a/src/content/scrolls.js
+++ b/src/content/scrolls.js
@@ -1,27 +1,14 @@
+import * as doms from 'shared/utils/dom';
+
const SCROLL_DELTA_X = 48;
const SCROLL_DELTA_Y = 48;
+const SMOOTH_SCROLL_DURATION = 150;
-const isVisible = (win, element) => {
- let rect = element.getBoundingClientRect();
- if (rect.width === 0 || rect.height === 0) {
- return false;
- }
- if (rect.right < 0 && rect.bottom < 0) {
- return false;
- }
- if (win.innerWidth < rect.left && win.innerHeight < rect.top) {
- return false;
- }
-
- let { display, visibility } = win.getComputedStyle(element);
- if (display === 'none' || visibility === 'hidden') {
- return false;
- }
- return true;
-};
+// dirty way to store scrolling state on globally
+let scrolling = [false];
-const isScrollableStyle = (win, element) => {
- let { overflowX, overflowY } = win.getComputedStyle(element);
+const isScrollableStyle = (element) => {
+ let { overflowX, overflowY } = window.getComputedStyle(element);
return !(overflowX !== 'scroll' && overflowX !== 'auto' &&
overflowY !== 'scroll' && overflowY !== 'auto');
};
@@ -35,15 +22,14 @@ const isOverflowed = (element) => {
// this method is called by each scrolling, and the returned value of this
// method is not cached. That does not cause performance issue because in the
// most pages, the window is root element i,e, documentElement.
-const findScrollable = (win, element) => {
- if (isScrollableStyle(win, element) && isOverflowed(element)) {
+const findScrollable = (element) => {
+ if (isScrollableStyle(element) && isOverflowed(element)) {
return element;
}
- let children = Array.prototype
- .filter.call(element.children, e => isVisible(win, e));
+ let children = Array.from(element.children).filter(doms.isVisible);
for (let child of children) {
- let scrollable = findScrollable(win, child);
+ let scrollable = findScrollable(child);
if (scrollable) {
return scrollable;
}
@@ -51,68 +37,153 @@ const findScrollable = (win, element) => {
return null;
};
-const scrollTarget = (win) => {
- if (isOverflowed(win.document.documentElement)) {
- return win.document.documentElement;
+const scrollTarget = () => {
+ if (isOverflowed(window.document.documentElement)) {
+ return window.document.documentElement;
}
- if (isOverflowed(win.document.body)) {
- return win.document.body;
+ if (isOverflowed(window.document.body)) {
+ return window.document.body;
}
- let target = findScrollable(win, win.document.documentElement);
+ let target = findScrollable(window.document.documentElement);
if (target) {
return target;
}
- return win.document.documentElement;
+ return window.document.documentElement;
};
-const scrollVertically = (win, count) => {
- let target = scrollTarget(win);
+class SmoothScroller {
+ constructor(element, repeat) {
+ this.element = element;
+ this.repeat = repeat;
+ this.scrolling = scrolling;
+ if (repeat) {
+ this.easing = SmoothScroller.linearEasing;
+ } else {
+ this.easing = SmoothScroller.inOutQuadEasing;
+ }
+ }
+
+ scroll(x, y) {
+ if (this.scrolling[0]) {
+ return;
+ }
+ scrolling[0] = true;
+
+ this.startX = this.element.scrollLeft;
+ this.startY = this.element.scrollTop;
+
+ this.targetX = x;
+ this.targetY = y;
+ this.distanceX = x - this.startX;
+ this.distanceY = y - this.startY;
+ this.timeStart = 0;
+
+ window.requestAnimationFrame(this.loop.bind(this));
+ }
+
+ loop(time) {
+ if (!this.timeStart) {
+ this.timeStart = time;
+ }
+
+ let elapsed = time - this.timeStart;
+ let v = this.easing(elapsed / SMOOTH_SCROLL_DURATION);
+ let nextX = this.startX + this.distanceX * v;
+ let nextY = this.startY + this.distanceY * v;
+
+ window.scrollTo(nextX, nextY);
+
+ if (elapsed < SMOOTH_SCROLL_DURATION) {
+ window.requestAnimationFrame(this.loop.bind(this));
+ } else {
+ scrolling[0] = false;
+ this.element.scrollTo(this.targetX, this.targetY);
+ }
+ }
+
+ static inOutQuadEasing(t) {
+ if (t < 1) {
+ return t * t;
+ }
+ return -(t - 1) * (t - 1) + 1;
+ }
+
+ static linearEasing(t) {
+ return t;
+ }
+}
+
+class RoughtScroller {
+ constructor(element) {
+ this.element = element;
+ }
+
+ scroll(x, y) {
+ this.element.scrollTo(x, y);
+ }
+}
+
+const scroller = (element, smooth, repeat) => {
+ if (smooth) {
+ return new SmoothScroller(element, repeat);
+ }
+ return new RoughtScroller(element);
+};
+
+const scrollVertically = (count, smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = target.scrollTop + SCROLL_DELTA_Y * count;
- target.scrollTo(x, y);
+ if (repeat && smooth) {
+ y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
+ }
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollHorizonally = (win, count) => {
- let target = scrollTarget(win);
+const scrollHorizonally = (count, smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft + SCROLL_DELTA_X * count;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ if (repeat && smooth) {
+ y = target.scrollTop + SCROLL_DELTA_Y * count * 4;
+ }
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollPages = (win, count) => {
- let target = scrollTarget(win);
+const scrollPages = (count, smooth, repeat) => {
+ let target = scrollTarget();
let height = target.clientHeight;
let x = target.scrollLeft;
let y = target.scrollTop + height * count;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollTop = (win) => {
- let target = scrollTarget(win);
+const scrollTop = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = 0;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollBottom = (win) => {
- let target = scrollTarget(win);
+const scrollBottom = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollLeft;
let y = target.scrollHeight;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollHome = (win) => {
- let target = scrollTarget(win);
+const scrollHome = (smooth, repeat) => {
+ let target = scrollTarget();
let x = 0;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
-const scrollEnd = (win) => {
- let target = scrollTarget(win);
+const scrollEnd = (smooth, repeat) => {
+ let target = scrollTarget();
let x = target.scrollWidth;
let y = target.scrollTop;
- target.scrollTo(x, y);
+ scroller(target, smooth, repeat).scroll(x, y);
};
export {
diff --git a/src/content/urls.js b/src/content/urls.js
index 8f8a1ac..9b7b284 100644
--- a/src/content/urls.js
+++ b/src/content/urls.js
@@ -1,3 +1,5 @@
+import messages from 'shared/messages';
+
const yank = (win) => {
let input = win.document.createElement('input');
win.document.body.append(input);
@@ -12,4 +14,26 @@ const yank = (win) => {
input.remove();
};
-export { yank };
+const paste = (win, newTab) => {
+ let textarea = win.document.createElement('textarea');
+ win.document.body.append(textarea);
+
+ textarea.style.position = 'fixed';
+ textarea.style.top = '-100px';
+ textarea.contentEditable = 'true';
+ textarea.focus();
+
+ if (win.document.execCommand('paste')) {
+ if (/^(https?|ftp):\/\//.test(textarea.textContent)) {
+ browser.runtime.sendMessage({
+ type: messages.OPEN_URL,
+ url: textarea.textContent,
+ newTab: newTab ? newTab : false,
+ });
+ }
+ }
+
+ textarea.remove();
+};
+
+export { yank, paste };