Support turbolink navigation for GitLab adapter, update GL tree item hovered color, refactor code

This commit is contained in:
Buu Nguyen 2015-11-17 10:45:30 -08:00
parent cbe8a47e5e
commit 28916ba301
8 changed files with 464 additions and 43 deletions

423
libs/turbolinks.js Normal file
View File

@ -0,0 +1,423 @@
// Generated by CoffeeScript 1.6.3
(function() {
var CSRFToken, anchoredLink, browserCompatibleDocumentParser, browserIsntBuggy, browserSupportsPushState, cacheCurrentPage, cacheSize, changePage, constrainPageCacheTo, createDocument, crossOriginLink, currentState, executeScriptTags, extractLink, extractTitleAndBody, fetchHistory, fetchReplacement, handleClick, ignoreClick, initializeTurbolinks, installClickHandlerLast, loadedAssets, noTurbolink, nonHtmlLink, nonStandardClick, pageCache, pageChangePrevented, pagesCached, processResponse, recallScrollPosition, referer, reflectNewUrl, reflectRedirectedUrl, rememberCurrentState, rememberCurrentUrl, removeHash, removeNoscriptTags, requestMethod, requestMethodIsSafe, resetScrollPosition, targetLink, triggerEvent, visit, xhr, _ref,
__hasProp = {}.hasOwnProperty,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
cacheSize = 10;
currentState = null;
referer = null;
loadedAssets = null;
pageCache = {};
createDocument = null;
requestMethod = ((_ref = document.cookie.match(/request_method=(\w+)/)) != null ? _ref[1].toUpperCase() : void 0) || '';
xhr = null;
fetchReplacement = function(url) {
var safeUrl;
triggerEvent('page:fetch');
safeUrl = removeHash(url);
if (xhr != null) {
xhr.abort();
}
xhr = new XMLHttpRequest;
xhr.open('GET', safeUrl, true);
xhr.setRequestHeader('Accept', 'text/html, application/xhtml+xml, application/xml');
xhr.setRequestHeader('X-XHR-Referer', referer);
xhr.onload = function() {
var doc;
triggerEvent('page:receive');
if (doc = processResponse()) {
reflectNewUrl(url);
changePage.apply(null, extractTitleAndBody(doc));
reflectRedirectedUrl();
if (document.location.hash) {
document.location.href = document.location.href;
} else {
resetScrollPosition();
}
return triggerEvent('page:load');
} else {
return document.location.href = url;
}
};
xhr.onloadend = function() {
return xhr = null;
};
xhr.onabort = function() {
return rememberCurrentUrl();
};
xhr.onerror = function() {
return document.location.href = url;
};
return xhr.send();
};
fetchHistory = function(position) {
var page;
cacheCurrentPage();
page = pageCache[position];
if (xhr != null) {
xhr.abort();
}
changePage(page.title, page.body);
recallScrollPosition(page);
return triggerEvent('page:restore');
};
cacheCurrentPage = function() {
pageCache[currentState.position] = {
url: document.location.href,
body: document.body,
title: document.title,
positionY: window.pageYOffset,
positionX: window.pageXOffset
};
return constrainPageCacheTo(cacheSize);
};
pagesCached = function(size) {
if (size == null) {
size = cacheSize;
}
if (/^[\d]+$/.test(size)) {
return cacheSize = parseInt(size);
}
};
constrainPageCacheTo = function(limit) {
var key, value;
for (key in pageCache) {
if (!__hasProp.call(pageCache, key)) continue;
value = pageCache[key];
if (key <= currentState.position - limit) {
pageCache[key] = null;
}
}
};
changePage = function(title, body, csrfToken, runScripts) {
document.title = title;
document.documentElement.replaceChild(body, document.body);
if (csrfToken != null) {
CSRFToken.update(csrfToken);
}
removeNoscriptTags();
if (runScripts) {
executeScriptTags();
}
currentState = window.history.state;
return triggerEvent('page:change');
};
executeScriptTags = function() {
var attr, copy, nextSibling, parentNode, script, scripts, _i, _j, _len, _len1, _ref1, _ref2;
scripts = Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])'));
for (_i = 0, _len = scripts.length; _i < _len; _i++) {
script = scripts[_i];
if (!((_ref1 = script.type) === '' || _ref1 === 'text/javascript')) {
continue;
}
copy = document.createElement('script');
_ref2 = script.attributes;
for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) {
attr = _ref2[_j];
copy.setAttribute(attr.name, attr.value);
}
copy.appendChild(document.createTextNode(script.innerHTML));
parentNode = script.parentNode, nextSibling = script.nextSibling;
parentNode.removeChild(script);
parentNode.insertBefore(copy, nextSibling);
}
};
removeNoscriptTags = function() {
var noscript, noscriptTags, _i, _len;
noscriptTags = Array.prototype.slice.call(document.body.getElementsByTagName('noscript'));
for (_i = 0, _len = noscriptTags.length; _i < _len; _i++) {
noscript = noscriptTags[_i];
noscript.parentNode.removeChild(noscript);
}
};
reflectNewUrl = function(url) {
if (url !== referer) {
return window.history.pushState({
turbolinks: true,
position: currentState.position + 1
}, '', url);
}
};
reflectRedirectedUrl = function() {
var location, preservedHash;
if (location = xhr.getResponseHeader('X-XHR-Redirected-To')) {
preservedHash = removeHash(location) === location ? document.location.hash : '';
return window.history.replaceState(currentState, '', location + preservedHash);
}
};
rememberCurrentUrl = function() {
return window.history.replaceState({
turbolinks: true,
position: Date.now()
}, '', document.location.href);
};
rememberCurrentState = function() {
return currentState = window.history.state;
};
recallScrollPosition = function(page) {
return window.scrollTo(page.positionX, page.positionY);
};
resetScrollPosition = function() {
return window.scrollTo(0, 0);
};
removeHash = function(url) {
var link;
link = url;
if (url.href == null) {
link = document.createElement('A');
link.href = url;
}
return link.href.replace(link.hash, '');
};
triggerEvent = function(name) {
var event;
event = document.createEvent('Events');
event.initEvent(name, true, true);
return document.dispatchEvent(event);
};
pageChangePrevented = function() {
return !triggerEvent('page:before-change');
};
processResponse = function() {
var assetsChanged, clientOrServerError, doc, extractTrackAssets, intersection, validContent;
clientOrServerError = function() {
var _ref1;
return (400 <= (_ref1 = xhr.status) && _ref1 < 600);
};
validContent = function() {
return xhr.getResponseHeader('Content-Type').match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/);
};
extractTrackAssets = function(doc) {
var node, _i, _len, _ref1, _results;
_ref1 = doc.head.childNodes;
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
node = _ref1[_i];
if ((typeof node.getAttribute === "function" ? node.getAttribute('data-turbolinks-track') : void 0) != null) {
_results.push(node.src || node.href);
}
}
return _results;
};
assetsChanged = function(doc) {
var fetchedAssets;
loadedAssets || (loadedAssets = extractTrackAssets(document));
fetchedAssets = extractTrackAssets(doc);
return fetchedAssets.length !== loadedAssets.length || intersection(fetchedAssets, loadedAssets).length !== loadedAssets.length;
};
intersection = function(a, b) {
var value, _i, _len, _ref1, _results;
if (a.length > b.length) {
_ref1 = [b, a], a = _ref1[0], b = _ref1[1];
}
_results = [];
for (_i = 0, _len = a.length; _i < _len; _i++) {
value = a[_i];
if (__indexOf.call(b, value) >= 0) {
_results.push(value);
}
}
return _results;
};
if (!clientOrServerError() && validContent()) {
doc = createDocument(xhr.responseText);
if (doc && !assetsChanged(doc)) {
return doc;
}
}
};
extractTitleAndBody = function(doc) {
var title;
title = doc.querySelector('title');
return [title != null ? title.textContent : void 0, doc.body, CSRFToken.get(doc).token, 'runScripts'];
};
CSRFToken = {
get: function(doc) {
var tag;
if (doc == null) {
doc = document;
}
return {
node: tag = doc.querySelector('meta[name="csrf-token"]'),
token: tag != null ? typeof tag.getAttribute === "function" ? tag.getAttribute('content') : void 0 : void 0
};
},
update: function(latest) {
var current;
current = this.get();
if ((current.token != null) && (latest != null) && current.token !== latest) {
return current.node.setAttribute('content', latest);
}
}
};
browserCompatibleDocumentParser = function() {
var createDocumentUsingDOM, createDocumentUsingParser, createDocumentUsingWrite, e, testDoc, _ref1;
createDocumentUsingParser = function(html) {
return (new DOMParser).parseFromString(html, 'text/html');
};
createDocumentUsingDOM = function(html) {
var doc;
doc = document.implementation.createHTMLDocument('');
doc.documentElement.innerHTML = html;
return doc;
};
createDocumentUsingWrite = function(html) {
var doc;
doc = document.implementation.createHTMLDocument('');
doc.open('replace');
doc.write(html);
doc.close();
return doc;
};
try {
if (window.DOMParser) {
testDoc = createDocumentUsingParser('<html><body><p>test');
return createDocumentUsingParser;
}
} catch (_error) {
e = _error;
testDoc = createDocumentUsingDOM('<html><body><p>test');
return createDocumentUsingDOM;
} finally {
if ((testDoc != null ? (_ref1 = testDoc.body) != null ? _ref1.childNodes.length : void 0 : void 0) !== 1) {
return createDocumentUsingWrite;
}
}
};
installClickHandlerLast = function(event) {
if (!event.defaultPrevented) {
document.removeEventListener('click', handleClick, false);
return document.addEventListener('click', handleClick, false);
}
};
handleClick = function(event) {
var link;
if (!event.defaultPrevented) {
link = extractLink(event);
if (link.nodeName === 'A' && !ignoreClick(event, link)) {
if (!pageChangePrevented()) {
visit(link.href);
}
return event.preventDefault();
}
}
};
extractLink = function(event) {
var link;
link = event.target;
while (!(!link.parentNode || link.nodeName === 'A')) {
link = link.parentNode;
}
return link;
};
crossOriginLink = function(link) {
return location.protocol !== link.protocol || location.host !== link.host;
};
anchoredLink = function(link) {
return ((link.hash && removeHash(link)) === removeHash(location)) || (link.href === location.href + '#');
};
nonHtmlLink = function(link) {
var url;
url = removeHash(link);
return url.match(/\.[a-z]+(\?.*)?$/g) && !url.match(/\.html?(\?.*)?$/g);
};
noTurbolink = function(link) {
var ignore;
while (!(ignore || link === document)) {
ignore = link.getAttribute('data-no-turbolink') != null;
link = link.parentNode;
}
return ignore;
};
targetLink = function(link) {
return link.target.length !== 0;
};
nonStandardClick = function(event) {
return event.which > 1 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
};
ignoreClick = function(event, link) {
return crossOriginLink(link) || anchoredLink(link) || nonHtmlLink(link) || noTurbolink(link) || targetLink(link) || nonStandardClick(event);
};
initializeTurbolinks = function() {
rememberCurrentUrl();
rememberCurrentState();
createDocument = browserCompatibleDocumentParser();
document.addEventListener('click', installClickHandlerLast, true);
return window.addEventListener('popstate', function(event) {
var state;
state = event.state;
if (state != null ? state.turbolinks : void 0) {
if (pageCache[state.position]) {
return fetchHistory(state.position);
} else {
return visit(event.target.location.href);
}
}
}, false);
};
browserSupportsPushState = window.history && window.history.pushState && window.history.replaceState && window.history.state !== void 0;
browserIsntBuggy = !navigator.userAgent.match(/CriOS\//);
requestMethodIsSafe = requestMethod === 'GET' || requestMethod === '';
if (browserSupportsPushState && browserIsntBuggy && requestMethodIsSafe) {
visit = function(url) {
referer = document.location.href;
cacheCurrentPage();
return fetchReplacement(url);
};
initializeTurbolinks();
} else {
visit = function(url) {
return document.location.href = url;
};
}
this.Turbolinks = {
visit: visit,
pagesCached: pagesCached
};
}).call(this);

View File

@ -245,7 +245,7 @@ class Adapter {
* Updates the layout based on sidebar visibility and width.
* @api public
*/
updateLayout(sidebarVisible, sidebarWidth) {
updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
throw new Error('Not implemented')
}
@ -284,14 +284,6 @@ class Adapter {
link.click()
}
/**
* Keep the sidebar for further purpose: adjust, re-append...
* @api public
*/
setSideBar(sidebar) {
// dummy method
}
/**
* Gets tree at path.
* @param {Object} opts - {token, repo}

View File

@ -40,7 +40,7 @@ class GitHub extends Adapter {
}
// @override
updateLayout(sidebarVisible, sidebarWidth) {
updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
const SPACING = 10
const $containers = $(GH_CONTAINERS)

View File

@ -5,7 +5,7 @@ const GL_RESERVED_USER_NAMES = [
const GL_RESERVED_REPO_NAMES = []
const GL_HEADER = '.navbar-gitlab'
const GL_SIDEBAR = '.sidebar-wrapper'
const GL_SHIFTED = 'h1.title'
const GL_TITLE = 'h1.title'
const GL_PROJECT_ID = '#project_id'
class GitLab extends Adapter {
@ -46,18 +46,15 @@ class GitLab extends Adapter {
$('.octotree_view_body input[type="text"], .octotree_view_body textarea')
.addClass('form-control')
/**
* GitLab uses Turbolinks to handle page load
* https://github.com/rails/turbolinks
*/
// GitLab uses Turbolinks to handle page load
$(document)
.on('page:update', () => {
.on('page:fetch', () => $(document).trigger(EVENT.REQ_START))
.on('page:load', () => {
// GitLab removes DOM, add back
$sidebar.appendTo('body')
$(document).trigger(EVENT.LOC_CHANGE)
$(document).trigger(EVENT.REQ_END)
})
.on('page:fetch', () => $(document).trigger(EVENT.REQ_START))
}
// @override
@ -76,29 +73,30 @@ class GitLab extends Adapter {
}
// @override
updateLayout(sidebarVisible, sidebarWidth) {
updateLayout(togglerVisible, sidebarVisible, sidebarWidth) {
const isNewDesign = $('.navbar-gitlab.header-collapsed, .navbar-gitlab.header-expanded').length > 0
const glSidebarExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
const toggleVisible = $('.octotree_toggle').is(":visible")
if (isNewDesign) {
const glSidebarWidth = glSidebarExpanded ? 230 : 62
$(GL_SHIFTED).css('margin-left', sidebarVisible ? '' : 36)
$(GL_TITLE).css('margin-left', sidebarVisible ? '' : 36)
$('.octotree_toggle').css('right', sidebarVisible ? '' : -(glSidebarWidth + 50))
}
else {
const glSidebarWidth = glSidebarExpanded ? 230 : 52
$(GL_HEADER).css('z-index', 3)
$(GL_SIDEBAR).css('z-index', 1)
$(GL_SHIFTED).css('margin-left', sidebarVisible ? '' : 56)
$(GL_TITLE).css('margin-left', sidebarVisible ? '' : 56)
$('.octotree_toggle').css({
'right': sidebarVisible ? '' : -102,
'top': sidebarVisible ? '' : 8
right: sidebarVisible ? '' : -102,
top: sidebarVisible ? '' : 8
})
}
// reset if toggle is not visible
if (!toggleVisible) $(GL_SHIFTED).css('margin-left', '')
// Reset title margin if toggler is not visible
if (!togglerVisible) {
$(GL_TITLE).css('margin-left', '')
}
$(GL_HEADER).css({'z-index': 3, 'margin-left': sidebarVisible ? sidebarWidth : ''})
$('.page-with-sidebar').css('padding-left', sidebarVisible ? sidebarWidth : '')
@ -139,7 +137,7 @@ class GitLab extends Adapter {
return cb()
}
// get branch by inspecting page, quite fragile so provide multiple fallbacks
// Get branch by inspecting page, quite fragile so provide multiple fallbacks
const GL_BRANCH_SEL_1 = '#repository_ref'
const GL_BRANCH_SEL_2 = '.select2-container.project-refs-select.select2 .select2-chosen'
const GL_BRANCH_SEL_3 = '.nav.nav-sidebar .shortcuts-tree'
@ -168,6 +166,11 @@ class GitLab extends Adapter {
}
}
// @override
selectFile(path) {
Turbolinks.visit(path)
}
// @override
loadCodeTree(opts, cb) {
opts.path = opts.node.path

View File

@ -89,7 +89,7 @@
}
.jstree-default {
.jstree-wholerow-hovered {
background: #fffaf1;
background: #f8eec7;
}
.jstree-wholerow-clicked {
background: #e7e9ed;

View File

@ -18,6 +18,7 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
'jquery.js',
'jquery-ui.js',
'jquery.pjax.js',
'turbolinks.js',
'jstree.js',
'keymaster.js',
'octotree.js'

View File

@ -32,12 +32,7 @@ $(document).ready(() => {
let currRepo = false
let hasError = false
$sidebar
.width(parseInt(store.get(STORE.WIDTH)))
.resize(layoutChanged)
.appendTo($('body'))
$(window).resize((event) => { // handle zoom
$(window).resize((event) => {
if (event.target === window) layoutChanged()
})
@ -45,7 +40,8 @@ $(document).ready(() => {
key.filter = () => $toggler.is(':visible')
key(store.get(STORE.HOTKEYS), toggleSidebarAndSave)
;[treeView, errorView, optsView].forEach((view) => {
const views = [treeView, errorView, optsView]
for (const view of views) {
$(view)
.on(EVENT.VIEW_READY, function (event) {
if (this !== optsView) {
@ -56,21 +52,22 @@ $(document).ready(() => {
.on(EVENT.VIEW_CLOSE, () => showView(hasError ? errorView.$view : treeView.$view))
.on(EVENT.OPTS_CHANGE, optionsChanged)
.on(EVENT.FETCH_ERROR, (event, err) => showError(err))
})
}
$document
.on(EVENT.REQ_START, () => $toggler.addClass('octotree_loading'))
.on(EVENT.REQ_END, () => $toggler.removeClass('octotree_loading'))
.on(EVENT.LAYOUT_CHANGE, layoutChanged)
.on(EVENT.TOGGLE, layoutChanged)
.on(EVENT.LOC_CHANGE, () => {
tryLoadRepo()
layoutChanged()
})
.on(EVENT.LOC_CHANGE, () => tryLoadRepo())
$sidebar
.width(parseInt(store.get(STORE.WIDTH)))
.resize(layoutChanged)
.appendTo($('body'))
adapter.init($sidebar)
tryLoadRepo()
layoutChanged()
return tryLoadRepo()
function optionsChanged(event, changes) {
let reload = false
@ -129,6 +126,7 @@ $(document).ready(() => {
$toggler.hide()
toggleSidebar(false)
}
layoutChanged()
})
}
@ -164,12 +162,16 @@ $(document).ready(() => {
function layoutChanged() {
const width = $sidebar.outerWidth()
adapter.updateLayout(isSidebarVisible(), width)
adapter.updateLayout(isTogglerVisible(), isSidebarVisible(), width)
store.set(STORE.WIDTH, width)
}
function isSidebarVisible() {
return $html.hasClass(PREFIX)
}
function isTogglerVisible() {
return $toggler.is(':visible')
}
}
})

View File

@ -111,7 +111,7 @@ class TreeView {
// refocus after complete so that keyboard navigation works, fix #158
const refocusAfterCompletion = () => {
$(document).one('pjax:success', () => {
$(document).one('pjax:success page:load', () => {
this.$jstree.get_container().focus()
})
}