Merge pull request #190 from crashbell/master

Add Lazy Load Option
This commit is contained in:
Buu Nguyen 2015-10-05 14:18:12 -07:00
commit 77282c0a52
16 changed files with 314 additions and 172 deletions

View File

@ -50,6 +50,9 @@ By default, Octotree only works on `github.com`. To support GitHub Enterprise on
## Changelog ## Changelog
### v1.7.2
* Fix bug long branches are not loaded correctly due to GitHub DOM change
### v1.7.1 ### v1.7.1
* Fix space between tree and GitHub contents due to GitHub DOM change * Fix space between tree and GitHub contents due to GitHub DOM change

BIN
dist/chrome.crx vendored

Binary file not shown.

BIN
dist/chrome.zip vendored

Binary file not shown.

BIN
dist/firefox.xpi vendored

Binary file not shown.

BIN
dist/opera.nex vendored

Binary file not shown.

View File

@ -7,12 +7,13 @@ const
'search', 'developer', 'account' 'search', 'developer', 'account'
] ]
, GH_RESERVED_REPO_NAMES = ['followers', 'following', 'repositories'] , GH_RESERVED_REPO_NAMES = ['followers', 'following', 'repositories']
, GH_BRANCH_SEL = '[aria-label="Switch branches or tags"]'
, GH_404_SEL = '#parallax_wrapper' , GH_404_SEL = '#parallax_wrapper'
, GH_PJAX_SEL = '#js-repo-pjax-container' , GH_PJAX_SEL = '#js-repo-pjax-container'
, GH_CONTAINERS = '.container' , GH_CONTAINERS = '.container'
function GitHub() { function GitHub() {
this._defaultBranch = {}
if (!window.MutationObserver) return if (!window.MutationObserver) return
// Fix #151 by detecting when page layout is updated. // Fix #151 by detecting when page layout is updated.
@ -36,24 +37,9 @@ function GitHub() {
} }
/** /**
* Selects a submodule. * Selects a file.
*/ * @param {String} path - the file path.
GitHub.prototype.selectSubmodule = function(path) { * @param {Number} tabSize - the tab size to use.
window.location.href = path
}
/**
* Downloads the file at the given
*/
GitHub.prototype.downloadFile = function(path, fileName) {
var link = document.createElement('a')
link.setAttribute('href', path.replace(/\/blob\//, '/raw/'))
link.setAttribute('download', fileName)
link.click()
}
/**
* Selects a path.
*/ */
GitHub.prototype.selectFile = function(path, tabSize) { GitHub.prototype.selectFile = function(path, tabSize) {
var container = $(GH_PJAX_SEL) var container = $(GH_PJAX_SEL)
@ -69,8 +55,30 @@ GitHub.prototype.selectFile = function(path, tabSize) {
else window.location.href = path + qs // falls back if no container (i.e. GitHub DOM has changed or is not yet available) else window.location.href = path + qs // falls back if no container (i.e. GitHub DOM has changed or is not yet available)
} }
/**
* Downloads a file
* @param {String} path - the file path.
* @param {String} fileName - the file name.
*/
GitHub.prototype.downloadFile = function(path, fileName) {
var link = document.createElement('a')
link.setAttribute('href', path.replace(/\/blob\//, '/raw/'))
link.setAttribute('download', fileName)
link.click()
}
/**
* Selects a submodule
* @param {String} path - the submodule path.
*/
GitHub.prototype.selectSubmodule = function(path) {
window.location.href = path
}
/** /**
* Updates page layout based on visibility status and width of the Octotree sidebar. * Updates page layout based on visibility status and width of the Octotree sidebar.
* @param {Boolean} sidebarVisible - current visibility of the sidebar.
* @param {Number} sidebarWidth - current width of the sidebar.
*/ */
GitHub.prototype.updateLayout = function(sidebarVisible, sidebarWidth) { GitHub.prototype.updateLayout = function(sidebarVisible, sidebarWidth) {
var $containers = $(GH_CONTAINERS) var $containers = $(GH_CONTAINERS)
@ -89,50 +97,83 @@ GitHub.prototype.updateLayout = function(sidebarVisible, sidebarWidth) {
} }
/** /**
* Returns the repository information if user is at a repository URL. Returns `null` otherwise. * Retrieves the repository info at the current location.
* @param {Boolean} showInNonCodePage - if false, should not return data in non-code pages.
* @param {Object} currentRepo - current repo being shown by Octotree.
* @param {String} token - the personal access token.
* @param {Function} cb - the callback function.
*/ */
GitHub.prototype.getRepoFromPath = function(showInNonCodePage, currentRepo) { GitHub.prototype.getRepoFromPath = function(showInNonCodePage, currentRepo, token, cb) {
// 404 page, skip // 404 page, skip
if ($(GH_404_SEL).length) return false if ($(GH_404_SEL).length) {
return cb()
}
// (username)/(reponame)[/(type)] // (username)/(reponame)[/(type)]
var match = window.location.pathname.match(/([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/) var match = window.location.pathname.match(/([^\/]+)\/([^\/]+)(?:\/([^\/]+))?/)
if (!match) return false if (!match) {
return cb()
}
var username = match[1]
var reponame = match[2]
// not a repository, skip // not a repository, skip
if (~GH_RESERVED_USER_NAMES.indexOf(match[1])) return false if (~GH_RESERVED_USER_NAMES.indexOf(username) ||
if (~GH_RESERVED_REPO_NAMES.indexOf(match[2])) return false ~GH_RESERVED_REPO_NAMES.indexOf(reponame)) {
return cb()
}
// skip non-code page unless showInNonCodePage is true // skip non-code page unless showInNonCodePage is true
if (!showInNonCodePage && match[3] && !~['tree', 'blob'].indexOf(match[3])) return false if (!showInNonCodePage && match[3] && !~['tree', 'blob'].indexOf(match[3])) {
return cb()
}
// get branch by inspecting page, quite fragile so provide multiple fallbacks // get branch by inspecting page, quite fragile so provide multiple fallbacks
var branch = var GH_BRANCH_SEL_1 = '[aria-label="Switch branches or tags"]'
$(GH_BRANCH_SEL).data('ref') || var GH_BRANCH_SEL_2 = '.repo-root a[data-branch]'
$(GH_BRANCH_SEL).children('.js-select-button').text() || var GH_BRANCH_SEL_3 = '.repository-sidebar a[aria-label="Code"]'
(currentRepo.username === match[1] && currentRepo.reponame === match[2] && currentRepo.branch) ||
'master'
return { var branch =
username : match[1], // Code page
reponame : match[2], $(GH_BRANCH_SEL_1).attr('title') || $(GH_BRANCH_SEL_2).data('branch') ||
branch : branch // Non-code page
($(GH_BRANCH_SEL_3).attr('href') || '').match(/([^\/]+)/g)[3] ||
// Assume same with previously
(currentRepo.username === username && currentRepo.reponame === reponame && currentRepo.branch) ||
// Default from cache
this._defaultBranch[username + '/' + reponame]
var repo = {username: username, reponame: reponame, branch: branch}
if (repo.branch) {
cb(null, repo)
}
else {
this._get(repo, null, token, function (err, data) {
if (err) return cb(err)
repo.branch = this._defaultBranch[username + '/' + reponame] = data.default_branch || 'master'
cb(null, repo)
}.bind(this))
} }
} }
/** /**
* Fetches data of a particular repository. * Retrieves the code tree of a repository.
* @param opts: { repo: repository, token (optional): user access token, apiUrl (optional): base API URL } * @param {Object} opts: { repo: repository, node(optional): selected node (null for resursively loading), token (optional): user access token, apiUrl (optional): base API URL }
* @param cb(err: error, tree: array (of arrays) of items) * @param {Function} cb(err: error, tree: array (of arrays) of items)
*/ */
GitHub.prototype.fetchData = function(opts, cb) { GitHub.prototype.getCodeTree = function(opts, cb) {
var self = this var self = this
, repo = opts.repo
, folders = { '': [] } , folders = { '': [] }
, repo = opts.repo
, token = opts.token
, encodedBranch = encodeURIComponent(decodeURIComponent(repo.branch)) , encodedBranch = encodeURIComponent(decodeURIComponent(repo.branch))
, $dummyDiv = $('<div/>') , $dummyDiv = $('<div/>')
getTree(encodedBranch + '?recursive=true', function(err, tree) { var treePath = (opts.node && (opts.node.sha || encodedBranch)) || (encodedBranch + '?recursive=1')
getTree(treePath, function(err, tree) {
if (err) return cb(err) if (err) return cb(err)
fetchSubmodules(function(err, submodules) { fetchSubmodules(function(err, submodules) {
@ -153,6 +194,10 @@ GitHub.prototype.fetchData = function(opts, cb) {
// we're done // we're done
if (item === undefined) return cb(null, folders['']) if (item === undefined) return cb(null, folders[''])
// includes parent path
if (opts.node && opts.node.path)
item.path = opts.node.path + '/' + item.path
path = item.path path = item.path
type = item.type type = item.type
index = path.lastIndexOf('/') index = path.lastIndexOf('/')
@ -162,10 +207,16 @@ GitHub.prototype.fetchData = function(opts, cb) {
item.text = name item.text = name
item.icon = type // use `type` as class name for tree node item.icon = type // use `type` as class name for tree node
if (opts.node) {
// no hierarchy in lazy loading
folders[''].push(item)
}
else
folders[path.substring(0, index)].push(item) folders[path.substring(0, index)].push(item)
if (type === 'tree') { if (type === 'tree') {
folders[item.path] = item.children = [] if (opts.node) item.children = true
else folders[item.path] = item.children = []
item.a_attr = { href: '#' } item.a_attr = { href: '#' }
} }
else if (type === 'blob') { else if (type === 'blob') {
@ -206,26 +257,29 @@ GitHub.prototype.fetchData = function(opts, cb) {
}) })
function getTree(tree, cb) { function getTree(tree, cb) {
get('/git/trees/' + tree, function(err, res) { self._get(repo, '/git/trees/' + tree, token, function(err, res) {
if (err) return cb(err) if (err) return cb(err)
cb(null, res.tree) cb(null, res.tree)
}) })
} }
function getBlob(sha, cb) { function getBlob(sha, cb) {
get('/git/blobs/' + sha, function(err, res) { self._get(repo, '/git/blobs/' + sha, token, function(err, res) {
if (err) return cb(err) if (err) return cb(err)
cb(null, atob(res.content.replace(/\n/g,''))) cb(null, atob(res.content.replace(/\n/g,'')))
}) })
} }
}
function get(path, cb) { GitHub.prototype._get = function(repo, path, token, cb) {
var token = opts.token var host = (location.host === 'github.com' ? 'api.github.com' : (location.host + '/api/v3'))
, host = (location.host === 'github.com' ? 'api.github.com' : (location.host + '/api/v3'))
, base = location.protocol + '//' + host + '/repos/' + repo.username + '/' + repo.reponame , base = location.protocol + '//' + host + '/repos/' + repo.username + '/' + repo.reponame
, cfg = { method: 'GET', url: base + path, cache: false } , cfg = { method: 'GET', url: base + (path || ''), cache: false }
if (token) {
cfg.headers = { Authorization: 'token ' + token }
}
if (token) cfg.headers = { Authorization: 'token ' + token }
$.ajax(cfg) $.ajax(cfg)
.done(function(data) { .done(function(data) {
cb(null, data) cb(null, data)
@ -280,5 +334,4 @@ GitHub.prototype.fetchData = function(opts, cb) {
needAuth : needAuth, needAuth : needAuth,
}) })
}) })
}
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "Octotree", "name": "Octotree",
"version": "1.7.1", "version": "2.0.0",
"manifest_version": 2, "manifest_version": 2,
"author": "Buu Nguyen", "author": "Buu Nguyen",
"description": "Display GitHub code in tree format", "description": "Display GitHub code in tree format",

View File

@ -7,6 +7,7 @@ const
TABSIZE : 'octotree.tabsize', TABSIZE : 'octotree.tabsize',
REMEMBER : 'octotree.remember', REMEMBER : 'octotree.remember',
LAZYLOAD : 'octotree.lazyload', LAZYLOAD : 'octotree.lazyload',
RECURSIVE : 'octotree.recursive',
HOTKEYS : 'octotree.hotkeys', HOTKEYS : 'octotree.hotkeys',
GHEURLS : 'octotree.gheurls', GHEURLS : 'octotree.gheurls',
WIDTH : 'octotree.sidebar_width', WIDTH : 'octotree.sidebar_width',
@ -21,6 +22,7 @@ const
TABSIZE : '', TABSIZE : '',
REMEMBER : false, REMEMBER : false,
LAZYLOAD : false, LAZYLOAD : false,
RECURSIVE: true,
// @ifdef SAFARI // @ifdef SAFARI
HOTKEYS : '⌘+b, ⌃+b', HOTKEYS : '⌘+b, ⌃+b',
// @endif // @endif
@ -43,4 +45,5 @@ const
OPTS_CHANGE : 'octotree:change', OPTS_CHANGE : 'octotree:change',
VIEW_READY : 'octotree:ready', VIEW_READY : 'octotree:ready',
VIEW_CLOSE : 'octotree:close', VIEW_CLOSE : 'octotree:close',
FETCH_ERROR : 'octotree:error'
} }

View File

@ -11,7 +11,7 @@
"icon": "data/icons/icon48.png", "icon": "data/icons/icon48.png",
"icon64": "data/icons/icon64.png", "icon64": "data/icons/icon64.png",
"license": "MIT", "license": "MIT",
"version": "1.7.1", "version": "2.0.0",
"permissions": { "permissions": {
"cross-domain-content": ["https://api.github.com", "https://github.com"] "cross-domain-content": ["https://api.github.com", "https://github.com"]
} }

View File

@ -50,6 +50,9 @@ $(document).ready(function() {
showView(hasError ? errorView.$view : treeView.$view) showView(hasError ? errorView.$view : treeView.$view)
}) })
.on(EVENT.OPTS_CHANGE, optionsChanged) .on(EVENT.OPTS_CHANGE, optionsChanged)
.on(EVENT.FETCH_ERROR, function(event, err) {
errorView.show(err)
})
}) })
$document $document
@ -81,6 +84,9 @@ $(document).ready(function() {
key.unbind(value[0]) key.unbind(value[0])
key(value[1], toggleSidebar) key(value[1], toggleSidebar)
break break
case STORE.RECURSIVE:
reload = true
break
} }
}) })
if (reload) tryLoadRepo(true) if (reload) tryLoadRepo(true)
@ -92,9 +98,12 @@ $(document).ready(function() {
, shown = store.get(STORE.SHOWN) , shown = store.get(STORE.SHOWN)
, lazyload = store.get(STORE.LAZYLOAD) , lazyload = store.get(STORE.LAZYLOAD)
, token = store.get(STORE.TOKEN) , token = store.get(STORE.TOKEN)
, repo = adapter.getRepoFromPath(showInNonCodePage, currRepo)
if (repo) { adapter.getRepoFromPath(showInNonCodePage, currRepo, token, function(err, repo) {
if (err) {
errorView.show(err)
}
else if (repo) {
$toggler.show() $toggler.show()
helpPopup.show() helpPopup.show()
@ -105,12 +114,9 @@ $(document).ready(function() {
if (repoChanged || reload === true) { if (repoChanged || reload === true) {
$document.trigger(EVENT.REQ_START) $document.trigger(EVENT.REQ_START)
currRepo = repo currRepo = repo
treeView.showHeader(repo)
adapter.fetchData({ repo: repo, token: token }, function(err, tree) { treeView.showHeader(repo)
if (err) errorView.show(err) treeView.show(repo, token)
else treeView.show(repo, tree)
})
} }
else treeView.syncSelection() else treeView.syncSelection()
} }
@ -119,6 +125,7 @@ $(document).ready(function() {
$toggler.hide() $toggler.hide()
toggleSidebar(false) toggleSidebar(false)
} }
})
} }
function showView(view) { function showView(view) {

View File

@ -202,6 +202,11 @@
label { label {
font-weight: normal !important; font-weight: normal !important;
} }
label.disabled {
color: gray;
}
input[type=text], textarea { input[type=text], textarea {
width: 100%; width: 100%;
} }

View File

@ -13,9 +13,9 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.7.1</string> <string>2.0.0</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.7.1</string> <string>2.0.0</string>
<key>Chrome</key> <key>Chrome</key>
<dict/> <dict/>
<key>Content</key> <key>Content</key>

View File

@ -53,7 +53,10 @@
<label><input type="checkbox" data-store="LAZYLOAD"> Only load tree when sidebar is open</label> <label><input type="checkbox" data-store="LAZYLOAD"> Only load tree when sidebar is open</label>
</div> </div>
<div> <div>
<label><input type="checkbox" data-store="COLLAPSE"> Collapse folders with single sub-folder</label> <label><input type="checkbox" data-store="RECURSIVE"> Load repository recursively</label>
</div>
<div>
<label><input type="checkbox" data-store="COLLAPSE" data-trigger-disable="RECURSIVE"> Collapse folders with single sub-folder</label>
</div> </div>
<div> <div>
<div> <div>

View File

@ -1,13 +1,24 @@
$(document).ready(function() { $(document).ready(function() {
// When navigating from non-code pages (i.e. Pulls, Issues) to code page // When navigating from non-code pages (i.e. Pulls, Issues) to code page
// GitHub doesn't reload the page but uses pjax. Need to detect and load Octotree. // GitHub doesn't reload the page but uses pjax. Need to detect and load Octotree.
var href, hash var firstLoad = true, href, hash
function detectLocationChange() { function detectLocationChange() {
if (location.href !== href || location.hash !== hash) { if (location.href !== href || location.hash !== hash) {
href = location.href href = location.href
hash = location.hash hash = location.hash
$(document).trigger(EVENT.LOC_CHANGE, href, hash)
// If this is the first time this is called, no need to notify change as
// Octotree does its own initialization after loading options.
if (firstLoad) {
firstLoad = false
} }
else {
setTimeout(function () {
$(document).trigger(EVENT.LOC_CHANGE, href, hash)
}, 200) // Waits a bit for pjax DOM change
}
}
setTimeout(detectLocationChange, 200) setTimeout(detectLocationChange, 200)
} }
detectLocationChange() detectLocationChange()

View File

@ -6,6 +6,23 @@ function OptionsView($dom, store) {
this.$view = $view this.$view = $view
$(document).ready(function() {
function triggerChange(checkbox) {
var store = $(checkbox).data('store')
, checkboxs = $view.find('[data-trigger-disable=' + store + ']')
checkboxs.prop('disabled', !checkbox.checked).closest('label').toggleClass('disabled', !checkbox.checked)
}
eachOption(
function($elm) {
// triggers to disable all checkboxs having data-trigger-disable
$elm.change(function(event) {
triggerChange(event.target)
})
}
)
})
// hide options view when sidebar is hidden // hide options view when sidebar is hidden
$(document).on(EVENT.TOGGLE, function(event, visible) { $(document).on(EVENT.TOGGLE, function(event, visible) {
if (!visible) toggle(false) if (!visible) toggle(false)
@ -23,7 +40,8 @@ function OptionsView($dom, store) {
else { else {
eachOption( eachOption(
function($elm, key, local, value, cb) { function($elm, key, local, value, cb) {
if ($elm.is(':checkbox')) $elm.prop('checked', value) // Original jQuery prop function doesn't trigger change event
if ($elm.is(':checkbox')) $elm.prop('checked', value).trigger("change")
else $elm.val(value) else $elm.val(value)
cb() cb()
}, },

View File

@ -74,15 +74,30 @@ TreeView.prototype.showHeader = function(repo) {
}) })
} }
TreeView.prototype.show = function(repo, treeData) { TreeView.prototype.show = function(repo, token) {
var self = this var self = this
, treeContainer = self.$view.find('.octotree_view_body') , treeContainer = self.$view.find('.octotree_view_body')
, tree = treeContainer.jstree(true) , tree = treeContainer.jstree(true)
, collapseTree = self.store.get(STORE.COLLAPSE) , collapseTree = self.store.get(STORE.COLLAPSE)
, recursiveLoad = self.store.get(STORE.RECURSIVE)
function fetchData(node, success) {
var selectedNode = node.original
if (node.id === '#') selectedNode = {path: ''}
self.adapter.getCodeTree({ repo: repo, token: token, node: recursiveLoad ? null : selectedNode}, function(err, treeData) {
if (err) $(self).trigger(EVENT.FETCH_ERROR, [err])
else success(treeData)
})
}
tree.settings.core.data = function (node, cb) {
fetchData(node, function(treeData) {
treeData = sort(treeData) treeData = sort(treeData)
if (collapseTree) treeData = collapse(treeData) if (collapseTree && recursiveLoad)
tree.settings.core.data = treeData treeData = collapse(treeData)
cb(treeData)
})
}
treeContainer.one('refresh.jstree', function() { treeContainer.one('refresh.jstree', function() {
self.syncSelection() self.syncSelection()
@ -97,7 +112,7 @@ TreeView.prototype.show = function(repo, treeData) {
return a.type === 'blob' ? 1 : -1 return a.type === 'blob' ? 1 : -1
}) })
folder.forEach(function(item) { folder.forEach(function(item) {
if (item.type === 'tree') sort(item.children) if (item.type === 'tree' && item.children !== true && item.children.length > 0) sort(item.children)
}) })
return folder return folder
} }
@ -119,17 +134,41 @@ TreeView.prototype.show = function(repo, treeData) {
TreeView.prototype.syncSelection = function() { TreeView.prototype.syncSelection = function() {
var tree = this.$view.find('.octotree_view_body').jstree(true) var tree = this.$view.find('.octotree_view_body').jstree(true)
, path = location.pathname , path = decodeURIComponent(location.pathname)
, recursiveLoad = this.store.get(STORE.RECURSIVE)
if (!tree) return if (!tree) return
tree.deselect_all()
// e.g. converts /buunguyen/octotree/type/branch/path to path // e.g. converts /buunguyen/octotree/type/branch/path to path
var match = path.match(/(?:[^\/]+\/){4}(.*)/) var match = path.match(/(?:[^\/]+\/){4}(.*)/)
, nodeId if (!match) return
if (match) {
nodeId = PREFIX + decodeURIComponent(match[1]) currentPath = match[1]
tree.select_node(nodeId)
tree.open_node(nodeId) // e.g. converts ["lib/controllers"] to ["lib", "lib/controllers"]
function createPaths(fullPath) {
var paths = fullPath.split("/")
, arrResult = [paths[0]]
paths.reduce(function(lastPath, curPath) {
var path = (lastPath + "/" + curPath)
arrResult.push(path)
return path
})
return arrResult
} }
function openPathAtIndex (paths, index) {
nodeId = PREFIX + paths[index]
if (tree.get_node(nodeId)) {
tree.deselect_all()
tree.select_node(nodeId)
tree.open_node(nodeId, function(node){
if (index < paths.length - 1) openPathAtIndex(paths, index + 1)
})
}
}
var paths = recursiveLoad ? [currentPath] : createPaths(currentPath)
openPathAtIndex(paths, 0)
} }