Commit c302c9c3 authored by Kristof Csillag's avatar Kristof Csillag

Merge pull request #543 from hypothesis/514-clickable-tabs-2

Add V1 of tagging support

Closes #514 
parents 2ed9fe7c d23add88
......@@ -1028,3 +1028,63 @@ pre {outline: 1px solid #ccc; padding: 5px; margin: 5px; }
margin: -1em 0;
}
}
// Tags for the annotations
.tags {
@include pie-clearfix;
@include reset-box-model;
@include reset-font;
background: none;
clear: both;
margin-top: .2em;
margin-bottom: .5em;
.tagit-close {
cursor: pointer;
position: absolute;
right: .1em;
top: 50%;
margin-top: -8px;
line-height: 17px;
}
.text-icon { display: none; }
input { float: left; }
li {
display: block;
float: left;
margin: .1em;
padding: .1em 1.2em .1em .3em;
position: relative;
& + li { margin-left: .1em; }
}
&[readonly] {
input, .tagit-close { display: none; }
li { padding-right: .3em; }
}
}
// Only show one line of tags in bucket view
.summary .tags {
height: 2.0em;
overflow: hidden;
}
// Specific rules for tags on the displayer page
.gray-tags .tags {
// Make tags white (on gray bg)
li { background: white; }
// Don't show the stub of the editor
li.tagit-new { display: none; }
}
h3.tagstream, h3.userstream {
letter-spacing: normal;
word-spacing: normal;
margin-top: 0;
}
......@@ -2,6 +2,7 @@ imports = [
'bootstrap'
'h.controllers'
'h.directives'
'h.app_directives'
'h.displayer'
'h.filters'
'h.services'
......
annotation = ['$filter', 'annotator', ($filter, annotator) ->
link: (scope, elem, attrs, controller) ->
return unless controller?
# Bind shift+enter to save
elem.bind
keydown: (e) ->
if e.keyCode == 13 && e.shiftKey
e.preventDefault()
scope.save()
# Watch for changes
scope.$watch 'model.$modelValue.id', (id) ->
scope.thread = annotator.threading.idTable[id]
# Publish the controller
scope.model = controller
controller: 'AnnotationController'
priority: 100 # Must run before ngModel
require: '?ngModel'
restrict: 'C'
scope: {}
templateUrl: 'annotation.html'
]
angular.module('h.app_directives', ['ngSanitize'])
.directive('annotation', annotation)
......@@ -229,6 +229,7 @@ class Annotation
annotator.deleteAnnotation $scope.model.$modelValue
else
$scope.model.$modelValue.text = $scope.origText
$scope.model.$modelValue.tags = $scope.origTags
$scope.action = 'create'
$scope.save = ->
......@@ -270,6 +271,7 @@ class Annotation
$scope.action = 'edit'
$scope.editing = true
$scope.origText = $scope.model.$modelValue.text
$scope.origTags = $scope.model.$modelValue.tags
$scope.delete = ->
annotation = $scope.thread.message
......@@ -292,7 +294,9 @@ class Annotation
$scope.action = 'delete'
$scope.editing = true
$scope.origText = $scope.model.$modelValue.text
$scope.origTags = $scope.model.$modelValue.tags
$scope.model.$modelValue.text = ''
$scope.model.$modelValue.tags = ''
$scope.authorize = (action) ->
if $scope.model.$modelValue? and annotator.plugins?.Permissions?
......
annotation = ['$filter', 'annotator', ($filter, annotator) ->
link: (scope, elem, attrs, controller) ->
return unless controller?
# Bind shift+enter to save
elem.bind
keydown: (e) ->
if e.keyCode == 13 && e.shiftKey
e.preventDefault()
scope.save()
# Watch for changes
scope.$watch 'model.$modelValue.id', (id) ->
scope.thread = annotator.threading.idTable[id]
# Publish the controller
scope.model = controller
controller: 'AnnotationController'
priority: 100 # Must run before ngModel
require: '?ngModel'
restrict: 'C'
scope: {}
templateUrl: 'annotation.html'
]
authentication = ->
base =
username: null
......@@ -288,8 +262,51 @@ repeatAnim = ->
.css({ 'margin-left': itemElm.width() })
.animate({ 'margin-left': '0px' }, 1500)
# Directive to edit/display a tag list.
tags = ['$window', ($window) ->
link: (scope, elem, attr, ctrl) ->
return unless ctrl?
elem.tagit
caseSensitive: false
placeholderText: attr.placeholder
keepPlaceholder: true
preprocessTag: (val) ->
val.toLowerCase().replace /[^a-z0-9\-\_\s]/g, ''
afterTagAdded: (evt, ui) ->
ctrl.$setViewValue elem.tagit 'assignedTags'
afterTagRemoved: (evt, ui) ->
ctrl.$setViewValue elem.tagit 'assignedTags'
autocomplete:
source: []
onTagClicked: (evt, ui) ->
tag = ui.tagLabel
$window.open "/t/" + tag
ctrl.$formatters.push (tags=[]) ->
assigned = elem.tagit 'assignedTags'
for t in assigned when t not in tags
elem.tagit 'removeTagByLabel', t
for t in tags when t not in assigned
elem.tagit 'createTag', t
attr.$observe 'readonly', (readonly) ->
tagInput = elem.find('input').last()
assigned = elem.tagit 'assignedTags'
if readonly
tagInput.attr('disabled', true)
tagInput.removeAttr('placeholder')
elem.hide() unless assigned.length
else
tagInput.removeAttr('disabled')
tagInput.attr('placeholder', attr['placeholder'])
elem.show()
require: '?ngModel'
restrict: 'C'
]
angular.module('h.directives', ['ngSanitize'])
.directive('annotation', annotation)
.directive('authentication', authentication)
.directive('markdown', markdown)
.directive('privacy', privacy)
......@@ -297,6 +314,7 @@ angular.module('h.directives', ['ngSanitize'])
.directive('resettable', resettable)
.directive('slowValidate', slowValidate)
.directive('tabReveal', tabReveal)
.directive('tags', tags)
.directive('thread', thread)
.directive('userPicker', userPicker)
.directive('ngBlur', ngBlur)
......
......@@ -127,5 +127,5 @@ class Displayer
document.init_annotation = null
$scope.open()
angular.module('h.displayer',['h.streamfilter','h.filters','bootstrap'])
angular.module('h.displayer',['h.streamfilter','h.filters','h.directives','bootstrap'])
.controller('DisplayerCtrl', Displayer)
get_quote = (annotation) ->
if not 'target' in annotation then return ''
quote = '(Reply annotation)'
for target in annotation['target']
for selector in target['selector']
if selector['type'] is 'TextQuoteSelector'
quote = selector['exact'] + ' '
quote
class TagStream
path: window.location.protocol + '//' + window.location.hostname + ':' +
window.location.port + '/__streamer__'
this.$inject = ['$location','$scope','$timeout','streamfilter']
constructor: ($location, $scope, $timeout, streamfilter) ->
$scope.annotations = []
$scope.tag = $location.absUrl().split('/').pop()
$scope.tagLabel = decodeURIComponent $scope.tag
$scope.filter =
streamfilter
.setPastDataHits(100)
.setMatchPolicyIncludeAny()
.setClausesParse('tags:#' + $scope.tag)
.getFilter()
$scope.manage_new_data = (data, action) =>
for annotation in data
annotation.action = action
annotation.quote = get_quote annotation
annotation._anim = 'fade'
$scope.annotations.splice 0,0,annotation
$scope.open = =>
$scope.sock = new SockJS(@path)
$scope.sock.onopen = =>
$scope.sock.send JSON.stringify $scope.filter
$scope.sock.onclose = =>
$timeout $scope.open, 5000
$scope.sock.onmessage = (msg) =>
console.log 'Got something'
console.log msg
data = msg.data[0]
action = msg.data[1]
unless data instanceof Array then data = [data]
$scope.$apply =>
$scope.manage_new_data data, action
$scope.open()
angular.module('h.tagstream',['h.streamfilter', 'h.filters','h.directives','bootstrap'])
.controller('TagStreamCtrl', TagStream)
This diff is collapsed.
This diff is collapsed.
/*!
* jQuery UI Core 1.10.3
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/category/ui-core/
*/
(function( $, undefined ) {
var uuid = 0,
runiqueId = /^ui-id-\d+$/;
// $.ui might exist from components with no dependencies, e.g., $.ui.position
$.ui = $.ui || {};
$.extend( $.ui, {
version: "1.10.3",
keyCode: {
BACKSPACE: 8,
COMMA: 188,
DELETE: 46,
DOWN: 40,
END: 35,
ENTER: 13,
ESCAPE: 27,
HOME: 36,
LEFT: 37,
NUMPAD_ADD: 107,
NUMPAD_DECIMAL: 110,
NUMPAD_DIVIDE: 111,
NUMPAD_ENTER: 108,
NUMPAD_MULTIPLY: 106,
NUMPAD_SUBTRACT: 109,
PAGE_DOWN: 34,
PAGE_UP: 33,
PERIOD: 190,
RIGHT: 39,
SPACE: 32,
TAB: 9,
UP: 38
}
});
// plugins
$.fn.extend({
focus: (function( orig ) {
return function( delay, fn ) {
return typeof delay === "number" ?
this.each(function() {
var elem = this;
setTimeout(function() {
$( elem ).focus();
if ( fn ) {
fn.call( elem );
}
}, delay );
}) :
orig.apply( this, arguments );
};
})( $.fn.focus ),
scrollParent: function() {
var scrollParent;
if (($.ui.ie && (/(static|relative)/).test(this.css("position"))) || (/absolute/).test(this.css("position"))) {
scrollParent = this.parents().filter(function() {
return (/(relative|absolute|fixed)/).test($.css(this,"position")) && (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x"));
}).eq(0);
} else {
scrollParent = this.parents().filter(function() {
return (/(auto|scroll)/).test($.css(this,"overflow")+$.css(this,"overflow-y")+$.css(this,"overflow-x"));
}).eq(0);
}
return (/fixed/).test(this.css("position")) || !scrollParent.length ? $(document) : scrollParent;
},
zIndex: function( zIndex ) {
if ( zIndex !== undefined ) {
return this.css( "zIndex", zIndex );
}
if ( this.length ) {
var elem = $( this[ 0 ] ), position, value;
while ( elem.length && elem[ 0 ] !== document ) {
// Ignore z-index if position is set to a value where z-index is ignored by the browser
// This makes behavior of this function consistent across browsers
// WebKit always returns auto if the element is positioned
position = elem.css( "position" );
if ( position === "absolute" || position === "relative" || position === "fixed" ) {
// IE returns 0 when zIndex is not specified
// other browsers return a string
// we ignore the case of nested elements with an explicit value of 0
// <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
value = parseInt( elem.css( "zIndex" ), 10 );
if ( !isNaN( value ) && value !== 0 ) {
return value;
}
}
elem = elem.parent();
}
}
return 0;
},
uniqueId: function() {
return this.each(function() {
if ( !this.id ) {
this.id = "ui-id-" + (++uuid);
}
});
},
removeUniqueId: function() {
return this.each(function() {
if ( runiqueId.test( this.id ) ) {
$( this ).removeAttr( "id" );
}
});
}
});
// selectors
function focusable( element, isTabIndexNotNaN ) {
var map, mapName, img,
nodeName = element.nodeName.toLowerCase();
if ( "area" === nodeName ) {
map = element.parentNode;
mapName = map.name;
if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
return false;
}
img = $( "img[usemap=#" + mapName + "]" )[0];
return !!img && visible( img );
}
return ( /input|select|textarea|button|object/.test( nodeName ) ?
!element.disabled :
"a" === nodeName ?
element.href || isTabIndexNotNaN :
isTabIndexNotNaN) &&
// the element and all of its ancestors must be visible
visible( element );
}
function visible( element ) {
return $.expr.filters.visible( element ) &&
!$( element ).parents().addBack().filter(function() {
return $.css( this, "visibility" ) === "hidden";
}).length;
}
$.extend( $.expr[ ":" ], {
data: $.expr.createPseudo ?
$.expr.createPseudo(function( dataName ) {
return function( elem ) {
return !!$.data( elem, dataName );
};
}) :
// support: jQuery <1.8
function( elem, i, match ) {
return !!$.data( elem, match[ 3 ] );
},
focusable: function( element ) {
return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) );
},
tabbable: function( element ) {
var tabIndex = $.attr( element, "tabindex" ),
isTabIndexNaN = isNaN( tabIndex );
return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN );
}
});
// support: jQuery <1.8
if ( !$( "<a>" ).outerWidth( 1 ).jquery ) {
$.each( [ "Width", "Height" ], function( i, name ) {
var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
type = name.toLowerCase(),
orig = {
innerWidth: $.fn.innerWidth,
innerHeight: $.fn.innerHeight,
outerWidth: $.fn.outerWidth,
outerHeight: $.fn.outerHeight
};
function reduce( elem, size, border, margin ) {
$.each( side, function() {
size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
if ( border ) {
size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
}
if ( margin ) {
size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
}
});
return size;
}
$.fn[ "inner" + name ] = function( size ) {
if ( size === undefined ) {
return orig[ "inner" + name ].call( this );
}
return this.each(function() {
$( this ).css( type, reduce( this, size ) + "px" );
});
};
$.fn[ "outer" + name] = function( size, margin ) {
if ( typeof size !== "number" ) {
return orig[ "outer" + name ].call( this, size );
}
return this.each(function() {
$( this).css( type, reduce( this, size, true, margin ) + "px" );
});
};
});
}
// support: jQuery <1.8
if ( !$.fn.addBack ) {
$.fn.addBack = function( selector ) {
return this.add( selector == null ?
this.prevObject : this.prevObject.filter( selector )
);
};
}
// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)
if ( $( "<a>" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) {
$.fn.removeData = (function( removeData ) {
return function( key ) {
if ( arguments.length ) {
return removeData.call( this, $.camelCase( key ) );
} else {
return removeData.call( this );
}
};
})( $.fn.removeData );
}
// deprecated
$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() );
$.support.selectstart = "onselectstart" in document.createElement( "div" );
$.fn.extend({
disableSelection: function() {
return this.bind( ( $.support.selectstart ? "selectstart" : "mousedown" ) +
".ui-disableSelection", function( event ) {
event.preventDefault();
});
},
enableSelection: function() {
return this.unbind( ".ui-disableSelection" );
}
});
$.extend( $.ui, {
// $.ui.plugin is deprecated. Use $.widget() extensions instead.
plugin: {
add: function( module, option, set ) {
var i,
proto = $.ui[ module ].prototype;
for ( i in set ) {
proto.plugins[ i ] = proto.plugins[ i ] || [];
proto.plugins[ i ].push( [ option, set[ i ] ] );
}
},
call: function( instance, name, args ) {
var i,
set = instance.plugins[ name ];
if ( !set || !instance.element[ 0 ].parentNode || instance.element[ 0 ].parentNode.nodeType === 11 ) {
return;
}
for ( i = 0; i < set.length; i++ ) {
if ( instance.options[ set[ i ][ 0 ] ] ) {
set[ i ][ 1 ].apply( instance.element, args );
}
}
}
},
// only used by resizable
hasScroll: function( el, a ) {
//If overflow is hidden, the element might have extra content, but the user wants to hide it
if ( $( el ).css( "overflow" ) === "hidden") {
return false;
}
var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
has = false;
if ( el[ scroll ] > 0 ) {
return true;
}
// TODO: determine which cases actually cause this to happen
// if the element doesn't have the scroll set, see if it's possible to
// set the scroll
el[ scroll ] = 1;
has = ( el[ scroll ] > 0 );
el[ scroll ] = 0;
return has;
}
});
})( jQuery );
/*!
* jQuery UI Effects Blind 1.10.3
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/blind-effect/
*
* Depends:
* jquery.ui.effect.js
*/
(function( $, undefined ) {
var rvertical = /up|down|vertical/,
rpositivemotion = /up|left|vertical|horizontal/;
$.effects.effect.blind = function( o, done ) {
// Create element
var el = $( this ),
props = [ "position", "top", "bottom", "left", "right", "height", "width" ],
mode = $.effects.setMode( el, o.mode || "hide" ),
direction = o.direction || "up",
vertical = rvertical.test( direction ),
ref = vertical ? "height" : "width",
ref2 = vertical ? "top" : "left",
motion = rpositivemotion.test( direction ),
animation = {},
show = mode === "show",
wrapper, distance, margin;
// if already wrapped, the wrapper's properties are my property. #6245
if ( el.parent().is( ".ui-effects-wrapper" ) ) {
$.effects.save( el.parent(), props );
} else {
$.effects.save( el, props );
}
el.show();
wrapper = $.effects.createWrapper( el ).css({
overflow: "hidden"
});
distance = wrapper[ ref ]();
margin = parseFloat( wrapper.css( ref2 ) ) || 0;
animation[ ref ] = show ? distance : 0;
if ( !motion ) {
el
.css( vertical ? "bottom" : "right", 0 )
.css( vertical ? "top" : "left", "auto" )
.css({ position: "absolute" });
animation[ ref2 ] = show ? margin : distance + margin;
}
// start at 0 if we are showing
if ( show ) {
wrapper.css( ref, 0 );
if ( ! motion ) {
wrapper.css( ref2, margin + distance );
}
}
// Animate
wrapper.animate( animation, {
duration: o.duration,
easing: o.easing,
queue: false,
complete: function() {
if ( mode === "hide" ) {
el.hide();
}
$.effects.restore( el, props );
$.effects.removeWrapper( el );
done();
}
});
};
})(jQuery);
/*!
* jQuery UI Effects Highlight 1.10.3
* http://jqueryui.com
*
* Copyright 2013 jQuery Foundation and other contributors
* Released under the MIT license.
* http://jquery.org/license
*
* http://api.jqueryui.com/highlight-effect/
*
* Depends:
* jquery.ui.effect.js
*/
(function( $, undefined ) {
$.effects.effect.highlight = function( o, done ) {
var elem = $( this ),
props = [ "backgroundImage", "backgroundColor", "opacity" ],
mode = $.effects.setMode( elem, o.mode || "show" ),
animation = {
backgroundColor: elem.css( "backgroundColor" )
};
if (mode === "hide") {
animation.opacity = 0;
}
$.effects.save( elem, props );
elem
.show()
.css({
backgroundImage: "none",
backgroundColor: o.color || "#ffff99"
})
.animate( animation, {
queue: false,
duration: o.duration,
easing: o.easing,
complete: function() {
if ( mode === "hide" ) {
elem.hide();
}
$.effects.restore( elem, props );
done();
}
});
};
})(jQuery);
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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