Commit 762e0684 authored by Steel Wagstaff's avatar Steel Wagstaff

Merge remote-tracking branch 'hypothesis/master'

parents a3402ecc c8e523f1
......@@ -4,6 +4,16 @@ Entries in this change log follow the format suggested at http://keepachangelog.
# Change Log
## [1.33.0] - 2017-08-01
### Changed
- Add onLayoutChange documentation
([#503](https://github.com/hypothesis/client/pull/503)).
- Remove the alternate tab switcher design
([#513](https://github.com/hypothesis/client/pull/513)).
## [1.32.1] - 2017-07-25
### Changed
......
......@@ -41,6 +41,7 @@ node {
boolean isTag() {
try {
sh 'git fetch --tags'
sh 'git describe --exact-match --tags'
return true
} catch (Exception e) {
......
ADR 2: ePub Support
=====================================================
Context
-------
### Background
Before now, the client had not had the necessary pieces to support the standard ePub
book format. The primary obstacles that had to be traversed were 1) supporting iframe annotation, 2) document equivalency, and 3) scrolling to annotations.
Decision
--------
* Standard ePubs viewers (including Readium and ePubJS) use iframes as a way to embed the different book pages and render them inside of their viewer. Hypothesis, before now, did not support going into iframes and allowing annotation. We modified our client to be able to watch for same origin iframes (cross origin not supported yet) on the page. When an iframe is encountered, we (if we have access to it) inject the Hypothesis embed code inside. In addition to the embed, we mirror the embed configuration values that were set. The last part of this injection is adding a “subFrameIdentifier” field to the configuration that is a random string that does two things 1) identifies the sub frame so the top frame has a point of unique reference and 2) the existence of this field tells the injected client that it should only load the Guest and not the full sidebar UI. So to recap, the client watches for iframes then injects the client and configuration into new frames. The Guest only clients inside of the frames will now be able to make and load annotations for their respective iframe locations. The client uses cross frame library to communicate what data to load - so subframes have those requests bubble up to the top frame. The sidebar stores an array of frames that it loads data for and does another cross frame call when data is returned. With all of that, the client is now able to support annotating content inside of an iframe.
* For document equivalency, we support for documents to set two new dc-* meta tags to indicate 1) what book we are in and 2) what chapter are we in. Together, this allows cross domain document equivalency down to the chapter level of an ePub. The tags are: “dc.identifier” for the chapter and “dc.relation.isPartOf” for the unique book identifier.
* Since ePubs use iframes for their presentation, those frames have content in various layouts/locations and move the visible area of the frame as the user is navigating to the next page (similar to how image sprites are used to show a single icon from a large image that has many icons). There is no standard event in web standards or ePub that we can use to navigate to the proper section in the frame and have it properly align the frame to include the page contents in the same manner that it does when you manually navigate to the page. That is, we could use “scrollTo” functions but those functions will just bring the section into view but makes no attempt to properly snap to the correct vertical and horizontal spacing that make up a whole page. This meant that unless we fixed it, users who select an annotation attempting to navigate to it could end up in scenarios where the highlighted section is visible but you cut the book's visible page in half. To fix this, we introduced an implementation agnostic “scrollToRange” event that we attempt to use before falling back to the traditional scrollTo event. That is, if the site registers a listener for “scrollToRange” and preventsDefault() we assume that they have taken the range and applied the proper scrolling behavior needed to get the range into view correctly.
Status
------
Deployed
......@@ -102,7 +102,7 @@ loads.
window.hypothesisConfig = function () {
return {
services: [{
apiUrl: 'https://hypothes.is/api',
apiUrl: 'https://hypothes.is/api/',
authority: 'partner.org',
grantToken: '***',
icon: 'https://openclipart.org/download/272629/sihouette-animaux-10.svg'
......@@ -288,6 +288,31 @@ loads.
of that text when it is being viewed as well as the font-family of the
editor as the annotation is being written.
.. option:: onLayoutChange
``function``. This function will be a registered callback to be invoked when the sidebar
layout changes. Changes to the layout occur on load, when the sidebar is toggled to
show and hide, and when the user adjusts the sidebar manually.
This setting can only be set using :js:func:`window.hypothesisConfig`.
When a layout change happens the registered :option:`onLayoutChange` function will
receive a single ``Object`` as it's argument. This object details the layout parameters
after the change.
Layout object available fields:
.. option:: expanded
``Boolean``. If the sidebar is open, this value will be true.
.. option:: height
``Number``. The current visible height of the sidebar.
.. option:: width
``Number``. The current visible width of the sidebar.
Asset and Sidebar App Location
......
{
"name": "hypothesis",
"version": "1.32.1",
"version": "1.33.0",
"lockfileVersion": 1,
"dependencies": {
"@gulp-sourcemaps/map-sources": {
......
{
"name": "hypothesis",
"version": "1.32.1",
"version": "1.33.0",
"description": "Annotate with anyone, anywhere.",
"license": "BSD-2-Clause",
"homepage": "https://hypothes.is",
......
......@@ -177,7 +177,6 @@ module.exports = angular.module('h', [
.component('threadList', require('./components/thread-list'))
.component('timestamp', require('./components/timestamp'))
.component('topBar', require('./components/top-bar'))
.component('viewSwitcher', require('./components/view-switcher'))
.directive('formInput', require('./directive/form-input'))
.directive('formValidate', require('./directive/form-validate'))
......
......@@ -55,7 +55,6 @@ function SidebarContentController(
totalNotes: counts.notes,
totalAnnotations: counts.annotations,
totalOrphans: counts.orphans,
viewSwitcherEnabled: features.flagEnabled('view-switcher'),
waitingToAnchorAnnotations: counts.anchoring > 0,
});
});
......
'use strict';
var angular = require('angular');
var util = require('../../directive/test/util');
describe('viewSwitcher', function () {
before(function () {
angular.module('app', [])
.component('viewSwitcher', require('../view-switcher'));
});
beforeEach(function () {
var fakeAnnotationUI = {
getState: sinon.stub().returns({
frames: [
{
// The view switcher only shows after the first batch of
// annotations have been fetched.
isAnnotationFetchComplete: true,
},
],
}),
};
var fakeFeatures = {
flagEnabled: sinon.stub().returns(true),
};
angular.mock.module('app', {
annotationUI: fakeAnnotationUI,
features: fakeFeatures,
});
});
context('displays tabs, counts and selected tab', function () {
it('should display the tabs and counts of annotations and notes', function () {
var elem = util.createDirective(document, 'viewSwitcher', {
selectedTab: 'annotation',
totalAnnotations: '123',
totalNotes: '456',
});
var tabs = elem[0].querySelectorAll('button');
assert.include(tabs[0].textContent, 'Annotations');
assert.include(tabs[1].textContent, 'Notes');
assert.include(tabs[0].textContent, '123');
assert.include(tabs[1].textContent, '456');
});
it('should display annotations tab as selected', function () {
var elem = util.createDirective(document, 'viewSwitcher', {
selectedTab: 'annotation',
totalAnnotations: '123',
totalNotes: '456',
});
var tabs = elem[0].querySelectorAll('button');
assert.isTrue(tabs[0].classList.contains('is-selected'));
});
it('should display notes tab as selected', function () {
var elem = util.createDirective(document, 'viewSwitcher', {
selectedTab: 'note',
totalAnnotations: '123',
totalNotes: '456',
});
var tabs = elem[0].querySelectorAll('button');
assert.isTrue(tabs[1].classList.contains('is-selected'));
});
});
});
'use strict';
var uiConstants = require('../ui-constants');
module.exports = {
controllerAs: 'vm',
//@ngInject
controller: function ($element, annotationUI, features) {
this.TAB_ANNOTATIONS = uiConstants.TAB_ANNOTATIONS;
this.TAB_NOTES = uiConstants.TAB_NOTES;
this.TAB_ORPHANS = uiConstants.TAB_ORPHANS;
this.selectTab = function (type) {
annotationUI.clearSelectedAnnotations();
annotationUI.selectTab(type);
};
this.orphansTabFlagEnabled = function () {
return features.flagEnabled('orphans_tab');
};
this.showViewSwitcher = function() {
var frame = annotationUI.getState().frames[0];
if (frame && frame.isAnnotationFetchComplete) {
return true;
}
return false;
};
this.showAnnotationsUnavailableMessage = function () {
return this.selectedTab === this.TAB_ANNOTATIONS &&
this.totalAnnotations === 0 &&
!this.isWaitingToAnchorAnnotations;
};
this.showNotesUnavailableMessage = function () {
return this.selectedTab === this.TAB_NOTES &&
this.totalNotes === 0;
};
},
bindings: {
isLoading: '<',
isWaitingToAnchorAnnotations: '<',
selectedTab: '<',
totalAnnotations: '<',
totalNotes: '<',
totalOrphans: '<',
},
template: require('../templates/view-switcher.html'),
};
<selection-tabs
ng-if="!vm.viewSwitcherEnabled && !vm.search.query() && vm.selectedAnnotationCount() === 0"
ng-if="!vm.search.query() && vm.selectedAnnotationCount() === 0"
is-waiting-to-anchor-annotations="vm.waitingToAnchorAnnotations"
is-loading="vm.isLoading"
selected-tab="vm.selectedTab"
......@@ -7,15 +7,6 @@
total-notes="vm.totalNotes"
total-orphans="vm.totalOrphans">
</selection-tabs>
<view-switcher
ng-if="vm.viewSwitcherEnabled && !vm.search.query() && vm.selectedAnnotationCount() === 0"
is-waiting-to-anchor-annotations="vm.waitingToAnchorAnnotations"
is-loading="vm.isLoading"
selected-tab="vm.selectedTab"
total-annotations="vm.totalAnnotations"
total-notes="vm.totalNotes"
total-orphans="vm.totalOrphans">
</view-switcher>
<search-status-bar
ng-show="!vm.isLoading()"
......
<div class="view-switcher" ng-if="vm.showViewSwitcher()">
<button class="view-switcher__tab"
ng-class="{'is-selected': vm.selectedTab === vm.TAB_ANNOTATIONS}"
h-on-touch="vm.selectTab(vm.TAB_ANNOTATIONS)">
<span class="view-switcher__tab-label">
Annotations
</span>
<span class="view-switcher__tab-count"
ng-if="vm.totalAnnotations > 0 && !vm.isWaitingToAnchorAnnotations">
{{ vm.totalAnnotations }}
</span>
</button>
<button class="view-switcher__tab"
ng-class="{'is-selected': vm.selectedTab === vm.TAB_NOTES}"
h-on-touch="vm.selectTab(vm.TAB_NOTES)">
<span class="view-switcher__tab-label">
Page Notes
</span>
<span class="view-switcher__tab-count"
ng-if="vm.totalNotes > 0 && !vm.isWaitingToAnchorAnnotations">
{{ vm.totalNotes }}
</span>
</button>
<button class="view-switcher__tab view-switcher__tab--orphan"
ng-class="{'is-selected': vm.selectedTab === vm.TAB_ORPHANS}"
h-on-touch="vm.selectTab(vm.TAB_ORPHANS)"
ng-if="vm.totalOrphans > 0 && !vm.isWaitingToAnchorAnnotations">
<span class="view-switcher__tab-label">
Orphans
</span>
<span class="view-switcher__tab-count">
{{ vm.totalOrphans }}
</span>
</button>
</div>
<div ng-if="!vm.isLoading()" class="view-switcher__empty-message">
<div ng-if="vm.showNotesUnavailableMessage()" class="annotation-unavailable-message">
<p class="annotation-unavailable-message__label">
There are no page notes in this group.
<br />
Create one by clicking the
<i class="help-icon h-icon-note"></i>
button.
</p>
</div>
<div ng-if="vm.showAnnotationsUnavailableMessage()" class="annotation-unavailable-message">
<p class="annotation-unavailable-message__label">
There are no annotations in this group.
<br />
Create one by selecting some text and clicking the
<i class="help-icon h-icon-annotate"></i> button.
</p>
</div>
</div>
......@@ -39,7 +39,7 @@ describe('hostPageConfig', function () {
it('ignores non-whitelisted config params', function () {
var window_ = fakeWindow({
apiUrl: 'https://not-the-hypothesis/api',
apiUrl: 'https://not-the-hypothesis/api/',
});
assert.deepEqual(hostPageConfig(window_), {});
......
......@@ -29,7 +29,7 @@ describe('store', function () {
angular.mock.module('h', {
auth: fakeAuth,
settings: {apiUrl: 'http://example.com/api'},
settings: {apiUrl: 'http://example.com/api/'},
});
angular.mock.inject(function (_$q_) {
......@@ -50,7 +50,7 @@ describe('store', function () {
$httpBackend = _$httpBackend_;
store = _store_;
$httpBackend.expectGET('http://example.com/api').respond({
$httpBackend.expectGET('http://example.com/api/').respond({
// Return an API route directory.
// This should mirror the structure (but not the exact URLs) of
// https://hypothes.is/api/.
......
......@@ -31,7 +31,6 @@ $base-line-height: 20px;
@import './thread-list';
@import './tooltip';
@import './top-bar';
@import './view-switcher';
// Top-level styles
// ----------------
......
.view-switcher {
display: flex;
justify-content: center;
// These are the exact margins required to vertically align the top of the
// view switcher with the top of the Hide Highlights button to its left,
// and the top of the first annotation card below the view switcher with the
// top of the New Page Note button to its left.
margin-top: 1px;
margin-bottom: 6px;
}
.view-switcher__tab {
@include smallshadow
height: 30px; // Same height as used for Hide Highlights and New Page Note
// buttons to left of view switcher.
padding-left: 12px;
padding-right: 12px;
background: $white;
transition: background-color .1s linear;
// This fixes the tabs flashing gray for a moment each time you tap on one
// in iOS Safari.
-webkit-tap-highlight-color: rgba(0,0,0,0);
border: 1px solid $gray-lighter;
cursor: pointer;
user-select: none;
}
.view-switcher__tab {
border-right-width: 0;
}
.view-switcher__tab:last-child {
border-right-width: 1px;
}
.view-switcher__tab:first-child {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.view-switcher__tab:last-child {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.view-switcher__tab:focus {
outline: 0;
}
.view-switcher__tab::-moz-focus-inner {
border: 0;
}
.view-switcher__tab:hover,
.view-switcher__tab.is-selected {
background-color: #e6e6e6;
}
.view-switcher__empty-message {
position: relative;
top: 10px;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment