Improve option handling and GitLab styling

* Refactor storage support
* Options except urls are per-site
* Fix GitLab styling bug
This commit is contained in:
Buu Nguyen 2015-11-13 15:54:40 -08:00
parent f8161c02c7
commit 63e111e438
20 changed files with 205 additions and 245 deletions

View File

@ -50,8 +50,9 @@ By default, Octotree only works on `github.com` and `gitlab.com`. To support ent
### v2.0.0
* Support GitLab
* Modify Octotree options
* Support lazy-load individual folder (GitHub)
* Support lazy-load individual folder (GitHub only)
* Simplify Octotree options
* Support selecting different options for each host
### v1.7.2
* Fix bug long branches are not loaded correctly due to GitHub DOM change

View File

@ -1,29 +1,31 @@
var gulp = require('gulp')
, path = require('path')
, merge = require('event-stream').merge
, map = require('map-stream')
, spawn = require('child_process').spawn
, $ = require('gulp-load-plugins')()
'use strict'
const gulp = require('gulp')
const path = require('path')
const merge = require('event-stream').merge
const map = require('map-stream')
const spawn = require('child_process').spawn
const $ = require('gulp-load-plugins')()
// Tasks
gulp.task('clean', function () {
gulp.task('clean', () => {
return pipe('./tmp', [$.clean()])
})
gulp.task('build', function (cb) {
gulp.task('build', (cb) => {
$.runSequence('clean', 'styles', 'chrome', 'opera', 'safari', 'firefox', cb)
})
gulp.task('default', ['build'], function () {
gulp.task('default', ['build'], () => {
gulp.watch(['./libs/**/*', './src/**/*'], ['default'])
})
gulp.task('dist', ['build'], function (cb) {
gulp.task('dist', ['build'], (cb) => {
$.runSequence('firefox:xpi', 'chrome:zip', 'chrome:crx', 'opera:nex', cb)
})
gulp.task('test', ['build'], function (cb) {
var ps = spawn(
gulp.task('test', ['build'], (cb) => {
const ps = spawn(
'./node_modules/.bin/mocha',
['--harmony', '--reporter', 'spec', '--bail', '--recursive', '--timeout', '-1']
)
@ -32,62 +34,62 @@ gulp.task('test', ['build'], function (cb) {
ps.on('close', cb)
})
gulp.task('styles', function () {
gulp.task('styles', () => {
return pipe('./src/styles/octotree.less',
[$.less(), $.autoprefixer({cascade: true})],
'./tmp')
})
// Chrome
gulp.task('chrome:template', function () {
gulp.task('chrome:template', () => {
return buildTemplate({CHROME: true})
})
gulp.task('chrome:js', ['chrome:template'], function () {
return buildJs(['./src/config/chrome/storage.js'], {CHROME: true})
gulp.task('chrome:js', ['chrome:template'], () => {
return buildJs(['./src/config/chrome/overrides.js'], {CHROME: true})
})
gulp.task('chrome', ['chrome:js'], function () {
gulp.task('chrome', ['chrome:js'], () => {
return merge(
pipe('./icons/**/*', './tmp/chrome/icons'),
pipe(['./libs/**/*', './tmp/octotree.*', './src/config/chrome/**/*', '!./src/config/chrome/storage.js'], './tmp/chrome/')
)
})
gulp.task('chrome:zip', function () {
gulp.task('chrome:zip', () => {
return pipe('./tmp/chrome/**/*', [$.zip('chrome.zip')], './dist')
})
gulp.task('chrome:_crx', function (cb) {
gulp.task('chrome:_crx', (cb) => {
$.run('"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"' +
' --pack-extension=' + path.join(__dirname, './tmp/chrome') +
' --pack-extension-key=' + path.join(process.env.HOME, '.ssh/chrome.pem')
).exec(cb)
})
gulp.task('chrome:crx', ['chrome:_crx'], function () {
gulp.task('chrome:crx', ['chrome:_crx'], () => {
return pipe('./tmp/chrome.crx', './dist')
})
// Opera
gulp.task('opera', ['chrome'], function () {
gulp.task('opera', ['chrome'], () => {
return pipe('./tmp/chrome/**/*', './tmp/opera')
})
gulp.task('opera:nex', function () {
gulp.task('opera:nex', () => {
return pipe('./dist/chrome.crx', [$.rename('opera.nex')], './dist')
})
// Safari
gulp.task('safari:template', function () {
gulp.task('safari:template', () => {
return buildTemplate({SAFARI: true})
})
gulp.task('safari:js', ['safari:template'], function () {
return buildJs(['./src/config/safari/storage.js'], {SAFARI: true})
gulp.task('safari:js', ['safari:template'], () => {
return buildJs([], {SAFARI: true})
})
gulp.task('safari', ['safari:js'], function () {
gulp.task('safari', ['safari:js'], () => {
return merge(
pipe('./icons/**/*', './tmp/safari/octotree.safariextension/icons'),
pipe(['./libs/**/*', './tmp/octotree.js', './tmp/octotree.css',
@ -96,15 +98,15 @@ gulp.task('safari', ['safari:js'], function () {
})
// Firefox
gulp.task('firefox:template', function () {
gulp.task('firefox:template', () => {
return buildTemplate({FIREFOX: true})
})
gulp.task('firefox:js', ['firefox:template'], function () {
return buildJs(['./src/config/firefox/storage.js'], {FIREFOX: true})
gulp.task('firefox:js', ['firefox:template'], () => {
return buildJs([], {FIREFOX: true})
})
gulp.task('firefox', ['firefox:js'], function () {
gulp.task('firefox', ['firefox:js'], () => {
return merge(
pipe('./icons/**/*', './tmp/firefox/data/icons'),
pipe(['./libs/**/*', './tmp/octotree.js', './tmp/octotree.css'], './tmp/firefox/data'),
@ -113,7 +115,7 @@ gulp.task('firefox', ['firefox:js'], function () {
)
})
gulp.task('firefox:xpi', function (cb) {
gulp.task('firefox:xpi', (cb) => {
$.run('cd ./tmp/firefox && cfx xpi --output-file=../../dist/firefox.xpi').exec(cb)
})
@ -123,10 +125,12 @@ function pipe(src, transforms, dest) {
dest = transforms
transforms = null
}
var stream = gulp.src(src)
let stream = gulp.src(src)
transforms && transforms.forEach(function (transform) {
stream = stream.pipe(transform)
})
if (dest) stream = stream.pipe(gulp.dest(dest))
return stream
}
@ -135,20 +139,22 @@ function html2js(template) {
return map(escape)
function escape(file, cb) {
var path = $.util.replaceExtension(file.path, '.js')
, content = file.contents.toString()
, escaped = content.replace(/\\/g, "\\\\")
const path = $.util.replaceExtension(file.path, '.js')
const content = file.contents.toString()
const escaped = content
.replace(/\\/g, "\\\\")
.replace(/'/g, "\\'")
.replace(/\r?\n/g, "\\n' +\n '")
, body = template.replace('$$', escaped)
const body = template.replace('$$', escaped)
file.path = path
file.contents = new Buffer(body)
cb(null, file)
}
}
function buildJs(additions, ctx) {
var src = additions.concat([
function buildJs(overrides, ctx) {
const src = [
'./tmp/template.js',
'./src/constants.js',
'./src/adapters/adapter.js',
@ -161,8 +167,10 @@ function buildJs(additions, ctx) {
'./src/util.location.js',
'./src/util.module.js',
'./src/util.async.js',
'./src/octotree.js',
])
'./src/util.storage.js'
].concat(overrides)
.concat('./src/octotree.js')
return pipe(src, [
$.babel({presets: ['es2015']}),
$.concat('octotree.js'),

View File

@ -23,7 +23,7 @@
"crx": "^0.4.4",
"event-stream": "*",
"firefox-profile": "~0.3.6",
"gulp": "^3.8.6",
"gulp": "^3.9.0",
"gulp-autoprefixer": "0.0.8",
"gulp-babel": "^6.1.0",
"gulp-clean": "^0.3.1",

View File

@ -191,13 +191,31 @@ class Adapter {
}
/**
* Returns the CSS class to be added to the Octotree sidebar.
* Inits behaviors after the sidebar is added to the DOM.
* @api public
*/
init($sidebar) {
$sidebar
.resizable({ handles: 'e', minWidth: this.getMinWidth() })
.addClass(this.getCssClass())
}
/**
* Returns the CSS class to be added to the Octotree sidebar.
* @api protected
*/
getCssClass() {
throw new Error('Not implemented')
}
/**
* Returns the minimum width acceptable for the sidebar.
* @api protected
*/
getMinWidth() {
return 200
}
/**
* Returns whether the adapter is capable of loading the entire tree in
* a single request. This is usually determined by the underlying the API.
@ -247,7 +265,6 @@ class Adapter {
window.location.href = path
}
/**
* Selects a submodule.
* @api public

View File

@ -14,6 +14,7 @@ const GH_PJAX_SEL = '#js-repo-pjax-container'
const GH_CONTAINERS = '.container'
class GitHub extends Adapter {
// @override
getCssClass() {
return 'octotree_github_sidebar'

View File

@ -9,6 +9,7 @@ const GL_SHIFTED = 'h1.title'
const GL_PROJECT_ID = '#project_id'
class GitLab extends Adapter {
constructor(store) {
super()
@ -21,6 +22,11 @@ class GitLab extends Adapter {
store.set(STORE.TOKEN, token, true)
}
}
}
// @override
init($sidebar) {
super.init($sidebar)
// Triggers layout when the GL sidebar is toggled
$('.toggle-nav-collapse').click(() => {
@ -28,6 +34,22 @@ class GitLab extends Adapter {
$(document).trigger(EVENT.LAYOUT_CHANGE)
}, 10)
})
// GitLab disables our submit buttons, re-enable them
$('.octotree_view_body button[type="submit"]').click((event) => {
setTimeout(() => {
$(event.target).prop('disabled', false).removeClass('disabled')
}, 100)
})
// Reuses GitLab styles for inputs
$('.octotree_view_body input[type="text"], .octotree_view_body textarea')
.addClass('form-control')
// GitLab removes DOM, add back
$(document).on(EVENT.LOC_CHANGE, () => {
$sidebar.appendTo('body')
})
}
// @override
@ -35,6 +57,11 @@ class GitLab extends Adapter {
return 'octotree_gitlab_sidebar'
}
// @override
getMinWidth() {
return 230 // just enough to hide the GitLab sidebar
}
// @override
getCreateTokenUrl() {
return `${location.protocol}//${location.host}/profile/account`
@ -42,16 +69,16 @@ class GitLab extends Adapter {
// @override
updateLayout(sidebarVisible, sidebarWidth) {
const useNewDesign = $('.navbar-gitlab.header-collapsed, .navbar-gitlab.header-expanded').length > 0
const isNewDesign = $('.navbar-gitlab.header-collapsed, .navbar-gitlab.header-expanded').length > 0
const glSidebarExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
let glSidebarWidth = glSidebarExpanded ? 230 : 62
if (useNewDesign) {
if (isNewDesign) {
const glSidebarWidth = glSidebarExpanded ? 230 : 62
$(GL_SHIFTED).css('margin-left', sidebarVisible ? '' : 36)
$('.octotree_toggle').css('right', sidebarVisible ? '' : -(glSidebarWidth + 50))
}
else {
glSidebarWidth = glSidebarExpanded ? 230 : 52
const glSidebarWidth = glSidebarExpanded ? 230 : 52
$(GL_HEADER).css('z-index', 3)
$(GL_SIDEBAR).css('z-index', 1)
$(GL_SHIFTED).css('margin-left', sidebarVisible ? '' : 56)
@ -61,7 +88,7 @@ class GitLab extends Adapter {
})
}
$(GL_HEADER).css({'z-index': 3, 'padding-left': sidebarVisible ? sidebarWidth : ''})
$(GL_HEADER).css({'z-index': 3, 'margin-left': sidebarVisible ? sidebarWidth : ''})
$('.page-with-sidebar').css('padding-left', sidebarVisible ? sidebarWidth : '')
}
@ -138,25 +165,6 @@ class GitLab extends Adapter {
}, cb)
}
// @override
setSideBar(sidebar) {
this.sidebar = sidebar
// GL disables our submit buttons, re-enable them
const btns = $('.octotree_view_body button[type="submit"]')
btns.click((event) => {
setTimeout(() => {
$(event.target).prop('disabled', false).removeClass('disabled')
}, 30)
})
// Make inputs consistent with GL style
$('.octotree_view_body input[type="text"], .octotree_view_body textarea').addClass('form-control')
$(document).on(EVENT.LOC_CHANGE, () => {
this.sidebar.appendTo('body')
})
}
// @override
_getTree(path, opts, cb) {
this._get(`/tree?path=${path}&ref_name=${opts.encodedBranch}`, opts, cb)

View File

@ -3,18 +3,6 @@
display: none;
}
.page-sidebar-expanded {
.octotree_gitlab_sidebar {
left: 249px;
}
}
.page-sidebar-collapsed {
.octotree_gitlab_sidebar {
left: 79px;
}
}
.octotree_gitlab_sidebar {
a.octotree_toggle {
right: 5px;
@ -29,24 +17,6 @@
}
}
html:not(.octotree) {
.page-sidebar-expanded {
.octotree_gitlab_sidebar {
a.octotree_toggle {
right: -294px;
}
}
}
.page-sidebar-collapsed {
.octotree_gitlab_sidebar {
a.octotree_toggle {
right: -110px;
}
}
}
}
.octotree_gitlab_sidebar {
transition-duration: .3s;
padding-top: 58px;

View File

@ -0,0 +1,21 @@
(() => {
const oldSet = Storage.prototype.set
Storage.prototype.set = function (key, val, cb) {
this._cache = this._cache || {}
this._cache[key] = val
const shared = ~key.indexOf('.shared')
if (shared) chrome.storage.local.set({[key]: val}, cb || Function())
else oldSet.call(this, key, val, cb)
}
const oldGet = Storage.prototype.get
Storage.prototype.get = function (key, cb) {
this._cache = this._cache || {}
if (!cb) return this._cache[key]
const shared = ~key.indexOf('.shared')
if (shared) chrome.storage.local.get(key, (item) => cb(item[key]))
else oldGet.call(this, key, cb)
}
})()

View File

@ -1,48 +0,0 @@
function Storage() {
var cache = this.cache = {}
chrome.storage.onChanged.addListener(function(changes) {
for (var key in changes) cache[key] = changes[key].newValue
})
}
Storage.prototype.set = function(key, val, local, cb) {
if (typeof local === 'function') {
cb = local
local = false
}
cb = cb || Function()
this.cache[key] = val
if (local) {
localStorage.setItem(key, JSON.stringify(val))
cb()
}
else {
var item = {}
item[key] = val
chrome.storage.local.set(item, cb)
}
}
Storage.prototype.get = function(key, local, cb) {
if (typeof local === 'function') {
cb = local
local = false
}
if (!cb) return this.cache[key]
if (local) cb(parse(localStorage.getItem(key)))
else chrome.storage.local.get(key, function(item) {
cb(item[key])
})
function parse(val) {
try {
return JSON.parse(val)
} catch (e) {
return val
}
}
}

View File

@ -1,24 +0,0 @@
function Storage() {}
Storage.prototype.set = function(key, val, local, cb) {
if (typeof local === 'function') cb = local
localStorage.setItem(key, JSON.stringify(val))
if (cb) cb()
}
Storage.prototype.get = function(key, local, cb) {
if (typeof local === 'function') cb = local
var val = parse(localStorage.getItem(key))
if (cb) cb(val)
else return val
function parse(val) {
try {
return JSON.parse(val)
} catch (e) {
return val
}
}
}

View File

@ -1,24 +0,0 @@
function Storage() {}
Storage.prototype.set = function(key, val, local, cb) {
if (typeof local === 'function') cb = local
localStorage.setItem(key, JSON.stringify(val))
if (cb) cb()
}
Storage.prototype.get = function(key, local, cb) {
if (typeof local === 'function') cb = local
var val = parse(localStorage.getItem(key))
if (cb) cb(val)
else return val
function parse(val) {
try {
return JSON.parse(val)
} catch (e) {
return val
}
}
}

View File

@ -6,11 +6,11 @@ const STORE = {
NONCODE : 'octotree.noncode_shown',
HOTKEYS : 'octotree.hotkeys',
LOADALL : 'octotree.loadall',
GHEURLS : 'octotree.gheurls',
GLEURLS : 'octotree.gleurls',
WIDTH : 'octotree.sidebar_width',
POPUP : 'octotree.popup_shown',
SHOWN : 'octotree.sidebar_shown'
WIDTH : 'octotree.sidebar_width',
SHOWN : 'octotree.sidebar_shown',
GHEURLS : 'octotree.gheurls.shared',
GLEURLS : 'octotree.gleurls.shared'
}
const DEFAULTS = {
@ -26,7 +26,7 @@ const DEFAULTS = {
// @endif
GHEURLS : '',
GLEURLS : '',
WIDTH : 250,
WIDTH : 232,
POPUP : false,
SHOWN : false
}

View File

@ -5,10 +5,8 @@ $(document).ready(() => {
function setDefault(key, cb) {
const storeKey = STORE[key]
const local = storeKey === STORE.TOKEN
store.get(storeKey, local, (val) => {
store.set(storeKey, val == null ? DEFAULTS[key] : val, local, cb)
store.get(storeKey, (val) => {
store.set(storeKey, val == null ? DEFAULTS[key] : val, cb)
})
}
@ -36,13 +34,11 @@ $(document).ready(() => {
let hasError = false
$sidebar
.width(parseFloat(store.get(STORE.WIDTH)))
.resizable({ handles: 'e', minWidth: 230 }) // to hide GL sidebar
.width(parseInt(store.get(STORE.WIDTH)))
.resize(layoutChanged)
.addClass(adapter.getCssClass())
.appendTo($('body'))
adapter.setSideBar($sidebar)
adapter.init($sidebar)
layoutChanged()
$(window).resize((event) => { // handle zoom
@ -170,7 +166,7 @@ $(document).ready(() => {
}
function layoutChanged() {
const width = $sidebar.width()
const width = $sidebar.outerWidth()
adapter.updateLayout(isSidebarVisible(), width)
store.set(STORE.WIDTH, width)
}

View File

@ -221,9 +221,7 @@ a.octotree_toggle {
cursor: pointer;
display: none;
opacity: 0;
top: 40px;
left: 5px;
z-index: 93;
z-index: 999999999;
width: 260px;
text-align: left;
background-color: #fff;

View File

@ -1,3 +1,3 @@
@import "common";
@import "github";
@import "gitlab";
@import "base";
@import "../adapters/github";
@import "../adapters/gitlab";

View File

@ -32,6 +32,26 @@
<input type="text" data-store="TOKEN" data-perhost="true">
</div>
<div>
<div>
<label>Hotkeys</label>
<a href="https://github.com/madrobby/keymaster#defining-shortcuts" target="_blank" tabIndex="-1">supported keys</a>
</div>
<input type="text" data-store="HOTKEYS">
</div>
<div>
<label><input type="checkbox" data-store="REMEMBER"> Remember sidebar visibility</label>
</div>
<div>
<label><input type="checkbox" data-store="NONCODE"> Show in non-code pages</label>
</div>
<div class="octotree_github_only">
<label><input type="checkbox" data-store="LOADALL"> Load entire tree at once</label>
</div>
<!-- @ifdef CHROME -->
<div class="octotree_github_only">
<div>
@ -40,6 +60,7 @@
<textarea data-store="GHEURLS" placeholder="https://github.mysite1.com/ https://github.mysite2.com/">
</textarea>
</div>
<div class="octotree_gitlab_only">
<div>
<label>GitLab Enterprise URLs</label>
@ -49,24 +70,6 @@
</div>
<!-- @endif -->
<hr />
<div>
<label><input type="checkbox" data-store="REMEMBER"> Remember sidebar visibility</label>
</div>
<div>
<label><input type="checkbox" data-store="NONCODE"> Show in non-code pages</label>
</div>
<div class="octotree_github_only">
<label><input type="checkbox" data-store="LOADALL"> Load entire tree at once</label>
</div>
<div>
<div>
<label>Hotkeys</label>
<a href="https://github.com/madrobby/keymaster#defining-shortcuts" target="_blank" tabIndex="-1">supported keys</a>
</div>
<input type="text" data-store="HOTKEYS">
</div>
<div>
<button type="submit" class="btn">Save</button>
</div>

20
src/util.storage.js Normal file
View File

@ -0,0 +1,20 @@
class Storage {
set(key, val, cb) {
localStorage.setItem(key, JSON.stringify(val))
if (cb) cb()
}
get(key, cb) {
var val = parse(localStorage.getItem(key))
if (cb) cb(val)
else return val
function parse(val) {
try {
return JSON.parse(val)
} catch (e) {
return val
}
}
}
}

View File

@ -5,13 +5,27 @@ class HelpPopup {
}
show() {
const $view = this.$view
const store = this.store
const popupShown = store.get(STORE.POPUP)
const sidebarVisible = $('html').hasClass(PREFIX)
if (popupShown) return
if (popupShown || sidebarVisible) {
store.set(STORE.POPUP, true)
return
}
$view.css('display', 'block').appendTo($('body'))
const $view = this.$view
const $toggler = $('.octotree_toggle')
const offset = $toggler.offset()
const height = $toggler.outerHeight()
$view
.css({
display: 'block',
top: offset.top + height + 2,
left: offset.left
})
$view.appendTo($('body'))
$(document).one(EVENT.TOGGLE, hide)

View File

@ -28,7 +28,7 @@ class OptionsView {
_load() {
this._eachOption(
($elm, key, local, value, cb) => {
($elm, key, value, cb) => {
if ($elm.is(':checkbox')) $elm.prop('checked', value)
else $elm.val(value)
cb()
@ -70,11 +70,11 @@ class OptionsView {
_saveOptions() {
const changes = {}
this._eachOption(
($elm, key, local, value, cb) => {
($elm, key, value, cb) => {
const newValue = $elm.is(':checkbox') ? $elm.is(':checked') : $elm.val()
if (value === newValue) return cb()
changes[key] = [value, newValue]
this.store.set(key, newValue, local, cb)
this.store.set(key, newValue, cb)
},
() => {
this._toggle(false)
@ -90,10 +90,9 @@ class OptionsView {
(elm, cb) => {
const $elm = $(elm)
const key = STORE[$elm.data('store')]
const local = !!$elm.data('perhost')
this.store.get(key, local, (value) => {
processFn($elm, key, local, value, () => cb())
this.store.get(key, (value) => {
processFn($elm, key, value, () => cb())
})
},
completeFn