Skip to content

Instantly share code, notes, and snippets.

@jonghwanhyeon
Last active December 15, 2015 03:39
Show Gist options
  • Save jonghwanhyeon/5195598 to your computer and use it in GitHub Desktop.
Save jonghwanhyeon/5195598 to your computer and use it in GitHub Desktop.
Drag to open link in new tab
chrome.extension.onConnect.addListener(function (port) {
port.onMessage.addListener(openLink);
});
var openLink = (function () {
var openers = {
tab: function (information ) {
chrome.tabs.create({
url: information.url,
active: false
});
},
window: function (information) {
chrome.windows.create({
url: information.url,
focused: true
});
}
};
return function (information) {
var opener = openers[information.opensIn];
if (!opener) return ;
opener(information);
};
})();
{
"manifest_version": 2,
"version": "1.0",
"manifest_version": 2,
"name": "Drag to open link in new tab",
"description": "Drag to open link in new tab",
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts": [
{
"matches": ["*://*/*"],
"js": ["observer.js"]
}
],
"permissions": [
"tabs"
]
}
/* jQuery-like DOM manipulator */
var $ = (function () {
var toString = Object.prototype.toString;
var slice = Array.prototype.slice;
var $ = function (object) {
var usesNewOperator = (this instanceof $);
if (!usesNewOperator) return new $(object);
if ($.isString(object)) {
return $.query(object);
}
if (object instanceof $) {
object = object.context;
} else if ($.isNodeList(object)) {
object = slice.call(object, 0);
} else if (!$.isArray(object)) {
object = [object];
}
this.context = object;
};
$.prototype.each = function (callback) {
for (var i = 0, length = this.context.length; i < length; i += 1) {
var stopsIteration = (callback.call(this.context[i], i, this.context[i]) === false);
if (stopsIteration) break;
}
return this;
};
$.prototype.html = function (html) {
this.each(function () {
this.innerHTML = html;
});
return this;
};
$.prototype.style = function (object) {
if ($.isString(object)) {
var key = object;
return window.getComputedStyle(this.context[0])[key];
}
var properties = object;
this.each(function () {
var element = this;
Object.keys(properties).forEach(function (key) {
element.style[key] = properties[key];
});
});
return this;
};
$.prototype.append = function (object) {
var target = this.context[0];
$(object).each(function () {
target.appendChild(this);
});
return this;
};
$.prototype.appendTo = function (target) {
$(target).append(this);
return this;
};
$.prototype.remove = function () {
this.each(function () {
if (this.parentNode) {
this.parentNode.removeChild(this);
}
});
return this;
};
$.prototype.size = function () {
return {
width: this.context[0].offsetWidth,
height: this.context[0].offsetHeight
};
};
$.prototype.offset = function () {
var offset = {
x: 0,
y: 0
};
var element = this.context[0];
while (element) {
offset.x += element.offsetLeft;
offset.y += element.offsetTop;
element = offset.offsetParent;
}
return offset;
}
$.prototype.on = function (type, tagName, callback) {
if (arguments.length == 2) {
callback = selector;
tagName = undefined;
} else {
tagName = tagName.toLowerCase();
}
var needsTagNameMatch = (tagName != undefined);
this.each(function () {
this.addEventListener(type, function (event) {
if (needsTagNameMatch) {
var tagNameIsMatched = (event.target.tagName.toLowerCase() === tagName);
if (!tagNameIsMatched) return ;
}
callback.call(event.target, event);
}, false);
});
return this;
};
$.prototype.show = function () {
this.each(function () {
$(this).style({
display: 'block'
});
});
return this;
};
$.prototype.hide = function () {
this.each(function () {
$(this).style({
display: 'none'
});
});
return this;
};
$.prototype.toggle = function () {
this.each(function () {
var isShown = (this.style('display') === 'block');
if (isShown) {
$(this).hide();
} else {
$(this).show();
}
});
return this;
};
$.query = function (selector) {
return $(document.querySelectorAll(selector));
};
$.isString = function (object) {
return ((typeof object) === 'string');
};
(function () {
var types = ['Array', 'NodeList'];
var typesToString = types.map(function (type) {
return '[object ' + type + ']';
});
types.forEach(function (type, index) {
$['is' + type] = function (object) {
return (toString.call(object) == typesToString[index]);
};
});
})();
return $;
})();
/* utilities */
var defer = function (task) {
window.setTimeout(task, 0);
};
/* constants */
var ACCEPTABLE_DISTANCE = 140;
var ACCEPTANCE_COLOR = '#00FF00';
var NOT_ACCEPTANCE_COLOR = '#FF0000';
var MESSAGE_FONT_SIZE = 32;
var MESSAGE_FOR_DEFAULT = 'Drag it';
var MESSAGE_FOR_NEW_TAB = 'Open link<br />in new tab';
var MESSAGE_FOR_NEW_WINDOW = 'Open link<br />in new window';
var MESSAGE_FOR_CANCEL = 'Cancel';
var INDICATOR_WIDTH = ACCEPTABLE_DISTANCE * 2;
var INDICATOR_HEIGHT = ACCEPTABLE_DISTANCE * 2;
/* utilities - point, distance */
var getPoint = function (event, type) {
if (type === undefined) type = 'page';
return {
x: event[type + 'X'],
y: event[type + 'Y']
};
};
var pointIsOnTheRightSideOf = function (currentPoint, basisPoint) {
return ((currentPoint.x - basisPoint.x) >= 0);
};
var calculateDistance = function (from, to) {
var differnce = {
x: from.x - to.x,
y: from.y - to.y
};
return Math.sqrt((differnce.x * differnce.x) + (differnce.y * differnce.y));
};
/* view factory */
var $view = {
create: (function () {
var creators = {
indicator: function (settings) {
var $indicator = $(document.createElement('div')).style({
position: 'absolute',
left: (settings.point.x - (settings.width / 2)) + 'px',
top: (settings.point.y - (settings.height / 2)) + 'px',
width: settings.width + 'px',
height: settings.height + 'px',
borderWidth: '5px',
borderStyle: 'solid',
borderColor: settings.borderColor,
borderRadius: '50%',
backgroundColor: 'white',
opacity: '0.4',
zIndex: '9999',
display: 'none'
});
$indicator.$message = $view.create('message', {
width: settings.width,
height: settings.height,
fontSize: settings.fontSize,
message: settings.message
}).appendTo($indicator);
return $indicator;
},
message: function (settings) {
var $message = $(document.createElement('span')).style({
width: settings.width + 'px',
height: settings.height + 'px',
fontFamily: 'Tahoma',
fontSize: settings.fontSize + 'px',
lineHeight: '1.3',
textAlign: 'center',
verticalAlign: 'middle',
display: 'table-cell'
});
$message.html(settings.message);
return $message;
}
};
return function (type, settings) {
var creator = creators[type];
if (!creator) return null;
return creator(settings);
};
})()
};
/* dragging checker */
var draggingChecker = {
check: function (settings) {
var distance = calculateDistance(settings.initialPoint, settings.currentPoint);
var isAcceptableDistance = (distance <= settings.acceptableDistance);
var opensInNewTab = pointIsOnTheRightSideOf(settings.currentPoint, settings.initialPoint);
return {
isAcceptableDistance: isAcceptableDistance,
opensInNewTab: opensInNewTab
};
}
};
/* drag handling routine */
var $indicator = null;
var initialPoint = { x: null, y: null };
$(document).on('dragstart', 'a', function (event) {
initialPoint = getPoint(event);
$indicator = $view.create('indicator', {
point: initialPoint,
width: INDICATOR_WIDTH,
height: INDICATOR_HEIGHT,
borderColor: ACCEPTANCE_COLOR,
fontSize: MESSAGE_FONT_SIZE,
message: MESSAGE_FOR_DEFAULT
}).appendTo('body');
// deferring to show $indicator, because if not, it will cause blocking drag events
defer(function () {
$indicator.show();
});
}).on('drag', 'a', function (event) {
// drag event is up to end, Chrome fires drag event with zero client point.
var shouldBeIgnored = ((event.clientX === 0) && (event.clientY === 0));
if (shouldBeIgnored) return ;
var information = draggingChecker.check({
initialPoint: initialPoint,
currentPoint: getPoint(event),
acceptableDistance: ACCEPTABLE_DISTANCE
});
var color, mesage;
if (information.isAcceptableDistance) {
color = ACCEPTANCE_COLOR;
message = (information.opensInNewTab ? MESSAGE_FOR_NEW_TAB : MESSAGE_FOR_NEW_WINDOW);
} else {
color = NOT_ACCEPTANCE_COLOR;
message = MESSAGE_FOR_CANCEL;
}
$indicator.style({
borderColor: color
}).$message.html(message);
}).on('dragend', 'a', function (event) {
var information = draggingChecker.check({
initialPoint: initialPoint,
currentPoint: getPoint(event),
acceptableDistance: ACCEPTABLE_DISTANCE
});
if (information.isAcceptableDistance) {
var information = {
opensIn: (information.opensInNewTab ? 'tab' : 'window'),
url: event.target.href
};
chrome.extension.connect().postMessage(information);
}
$indicator.remove();
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment