diff --git a/libs/turbolinks.js b/libs/turbolinks.js new file mode 100644 index 0000000..7723d62 --- /dev/null +++ b/libs/turbolinks.js @@ -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('

test'); + return createDocumentUsingParser; + } + } catch (_error) { + e = _error; + testDoc = createDocumentUsingDOM('

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); diff --git a/src/adapters/adapter.js b/src/adapters/adapter.js index dac06cb..ce51960 100644 --- a/src/adapters/adapter.js +++ b/src/adapters/adapter.js @@ -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} diff --git a/src/adapters/github.js b/src/adapters/github.js index 042635c..cb5196e 100644 --- a/src/adapters/github.js +++ b/src/adapters/github.js @@ -40,7 +40,7 @@ class GitHub extends Adapter { } // @override - updateLayout(sidebarVisible, sidebarWidth) { + updateLayout(togglerVisible, sidebarVisible, sidebarWidth) { const SPACING = 10 const $containers = $(GH_CONTAINERS) diff --git a/src/adapters/gitlab.js b/src/adapters/gitlab.js index 63d9aaf..3c96fd2 100644 --- a/src/adapters/gitlab.js +++ b/src/adapters/gitlab.js @@ -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 diff --git a/src/adapters/gitlab.less b/src/adapters/gitlab.less index 5df7c33..ecd291e 100644 --- a/src/adapters/gitlab.less +++ b/src/adapters/gitlab.less @@ -89,7 +89,7 @@ } .jstree-default { .jstree-wholerow-hovered { - background: #fffaf1; + background: #f8eec7; } .jstree-wholerow-clicked { background: #e7e9ed; diff --git a/src/config/chrome/background.js b/src/config/chrome/background.js index ef0b5e2..3008799 100644 --- a/src/config/chrome/background.js +++ b/src/config/chrome/background.js @@ -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' diff --git a/src/octotree.js b/src/octotree.js index dd0d9e4..3ccc128 100755 --- a/src/octotree.js +++ b/src/octotree.js @@ -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') + } } }) diff --git a/src/view.tree.js b/src/view.tree.js index a3417e3..d73e401 100644 --- a/src/view.tree.js +++ b/src/view.tree.js @@ -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() }) }