Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
481aa74c
Unverified
Commit
481aa74c
authored
Dec 17, 2019
by
Kyle Keating
Committed by
GitHub
Dec 17, 2019
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Create TagEditor component (#1558)
Add new preact tag editor component
parent
418f2d85
Changes
8
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
8 changed files
with
546 additions
and
80 deletions
+546
-80
tag-editor.js
src/sidebar/components/tag-editor.js
+203
-24
tag-list.js
src/sidebar/components/tag-list.js
+2
-2
tag-editor-test.js
src/sidebar/components/test/tag-editor-test.js
+283
-36
index.js
src/sidebar/index.js
+4
-1
annotation.html
src/sidebar/templates/annotation.html
+8
-4
tag-editor.html
src/sidebar/templates/tag-editor.html
+0
-13
tag-editor.scss
src/styles/sidebar/components/tag-editor.scss
+45
-0
sidebar.scss
src/styles/sidebar/sidebar.scss
+1
-0
No files found.
src/sidebar/components/tag-editor.js
View file @
481aa74c
'use strict'
;
'use strict'
;
// @ngInject
const
{
createElement
}
=
require
(
'preact'
);
function
TagEditorController
(
tags
)
{
const
propTypes
=
require
(
'prop-types'
);
this
.
onTagsChanged
=
function
()
{
const
{
useMemo
,
useRef
,
useState
}
=
require
(
'preact/hooks'
);
tags
.
store
(
this
.
tagList
);
const
newTags
=
this
.
tagList
.
map
(
function
(
item
)
{
const
{
withServices
}
=
require
(
'../util/service-context'
);
return
item
.
text
;
const
SvgIcon
=
require
(
'./svg-icon'
);
// Global counter used to create a unique id for each instance of a TagEditor
let
datalistIdCounter
=
0
;
/**
* Component to edit annotation's tags.
*/
function
TagEditor
({
onEditTags
,
tags
:
tagsService
,
tagList
})
{
const
inputEl
=
useRef
(
null
);
const
[
showSuggestions
,
setShowSuggestions
]
=
useState
(
false
);
const
[
datalistId
]
=
useState
(()
=>
{
++
datalistIdCounter
;
return
`tag-editor-datalist-
${
datalistIdCounter
}
`
;
});
// List of suggestions returned from the tagsService
const
suggestions
=
useMemo
(()
=>
{
// Remove any repeated suggestions that are already tags.
const
removeDuplicates
=
(
suggestions
,
tags
)
=>
{
const
suggestionsSet
=
[];
suggestions
.
forEach
(
suggestion
=>
{
if
(
tags
.
indexOf
(
suggestion
)
<
0
)
{
suggestionsSet
.
push
(
suggestion
);
}
});
});
this
.
onEditTags
({
tags
:
newTags
}
);
return
suggestionsSet
.
sort
(
);
};
};
// Call filter with an empty string to return all suggestions
return
removeDuplicates
(
tagsService
.
filter
(
''
),
tagList
);
},
[
tagsService
,
tagList
]);
this
.
autocomplete
=
function
(
query
)
{
/**
return
Promise
.
resolve
(
tags
.
filter
(
query
));
* Handle changes to this annotation's tags
*/
const
updateTags
=
tagList
=>
{
// update suggested tags list via service
tagsService
.
store
(
tagList
.
map
(
tag
=>
({
text
:
tag
})));
onEditTags
({
tags
:
tagList
});
};
};
this
.
$onChanges
=
function
(
changes
)
{
/**
if
(
changes
.
tags
)
{
* Remove a tag from this annotation.
this
.
tagList
=
changes
.
tags
.
currentValue
.
map
(
function
(
tag
)
{
*
return
{
text
:
tag
};
* @param {string} tag
});
*/
const
removeTag
=
tag
=>
{
const
newTagList
=
[...
tagList
];
// make a copy
const
index
=
newTagList
.
indexOf
(
tag
);
newTagList
.
splice
(
index
,
1
);
updateTags
(
newTagList
);
};
/**
* Adds a tag to the annotation equal to the value of the input field
* and then clears out the suggestions list and the input field.
*/
const
addTag
=
()
=>
{
const
value
=
inputEl
.
current
.
value
.
trim
();
if
(
value
.
length
===
0
)
{
// don't add an empty tag
return
;
}
if
(
tagList
.
indexOf
(
value
)
>=
0
)
{
// don't add duplicate tag
return
;
}
}
updateTags
([...
tagList
,
value
]);
setShowSuggestions
(
false
);
inputEl
.
current
.
value
=
''
;
inputEl
.
current
.
focus
();
};
};
/**
* If the user pressed enter or comma while focused on
* the <input>, then add a new tag.
*/
const
handleKeyPress
=
e
=>
{
if
(
e
.
key
===
'Enter'
||
e
.
key
===
','
)
{
addTag
();
// preventDefault stops the delimiter from being
// added to the input field
e
.
preventDefault
();
}
};
const
handleOnInput
=
e
=>
{
if
(
e
.
inputType
===
'insertText'
)
{
// Show the suggestions if the user types something into the field
setShowSuggestions
(
true
);
}
else
if
(
e
.
inputType
===
undefined
||
e
.
inputType
===
'insertReplacementText'
)
{
// nb. Chrome / Safari reports undefined and Firefox reports 'insertReplacementText'
// for the inputTyp value when clicking on an element in the datalist.
//
// There are two ways to arrive here, either click an item with the mouse
// or use keyboard navigation and press 'Enter'.
//
// If the input value typed already exactly matches the option selected
// then this event won't fire and a user would have to press 'Enter' a second
// time to trigger the handleKeyPress callback above to add the tag.
// Bug: https://github.com/hypothesis/client/issues/1604
addTag
();
}
else
if
(
inputEl
.
current
.
value
.
length
===
0
)
{
// If the user deleted input, hide suggestions. This has
// no effect in Safari and the list will stay open.
setShowSuggestions
(
false
);
}
};
const
handleKeyUp
=
e
=>
{
// Safari on macOS and iOS have an issue where pressing "Enter" in an
// input when its value exactly matches a suggestion in the associated <datalist>
// does not generate a "keypress" event. Therefore we catch the subsequent
// "keyup" event instead.
if
(
e
.
key
===
'Enter'
)
{
// nb. `addTag` will do nothing if the "keypress" event was already handled.
addTag
();
}
};
const
suggestionsList
=
()
=>
{
return
(
<
datalist
id
=
{
datalistId
}
className
=
"tag-editor__suggestions"
aria
-
label
=
"Annotation suggestions"
>
{
showSuggestions
&&
suggestions
.
map
(
suggestion
=>
(
<
option
key
=
{
suggestion
}
value
=
{
suggestion
}
/
>
))}
<
/datalist
>
);
};
return
(
<
section
className
=
"tag-editor"
>
<
ul
className
=
"tag-editor__tag-list"
aria
-
label
=
"Suggested tags for annotation"
>
{
tagList
.
map
(
tag
=>
{
return
(
<
li
key
=
{
`
${
tag
}
`
}
className
=
"tag-editor__tag-item"
aria
-
label
=
{
`Tag:
${
tag
}
`
}
>
<
span
className
=
"tag-editor__edit"
>
{
tag
}
<
/span
>
<
button
onClick
=
{()
=>
{
removeTag
(
tag
);
}}
title
=
{
`Remove Tag:
${
tag
}
`
}
className
=
"tag-editor__delete"
>
<
SvgIcon
name
=
"cancel"
/>
<
/button
>
<
/li
>
);
})}
<
/ul
>
<
input
list
=
{
datalistId
}
onInput
=
{
handleOnInput
}
onKeyPress
=
{
handleKeyPress
}
onKeyUp
=
{
handleKeyUp
}
ref
=
{
inputEl
}
placeholder
=
"Add tags..."
className
=
"tag-editor__input"
type
=
"text"
/>
{
suggestionsList
()}
<
/section
>
);
}
}
module
.
exports
=
{
/**
controller
:
TagEditorController
,
* @typedef Tag
controllerAs
:
'vm'
,
* @param tag {string} - The tag text
bindings
:
{
*/
tags
:
'<'
,
onEditTags
:
'&'
,
TagEditor
.
propTypes
=
{
},
/**
template
:
require
(
'../templates/tag-editor.html'
),
* Callback that saves the tag list.
*
* @param {Array<Tag>} - Array of tags to save
*/
onEditTags
:
propTypes
.
func
.
isRequired
,
/* The list of editable tags as strings. */
tagList
:
propTypes
.
array
.
isRequired
,
/** Services */
tags
:
propTypes
.
object
.
isRequired
,
serviceUrl
:
propTypes
.
func
.
isRequired
,
};
};
TagEditor
.
injectedProps
=
[
'serviceUrl'
,
'tags'
];
module
.
exports
=
withServices
(
TagEditor
);
src/sidebar/components/tag-list.js
View file @
481aa74c
...
@@ -60,10 +60,10 @@ function TagList({ annotation, serviceUrl, settings, tags }) {
...
@@ -60,10 +60,10 @@ function TagList({ annotation, serviceUrl, settings, tags }) {
TagList
.
propTypes
=
{
TagList
.
propTypes
=
{
/* Annotation that owns the tags. */
/* Annotation that owns the tags. */
annotation
:
propTypes
.
object
,
annotation
:
propTypes
.
object
.
isRequired
,
/* List of tags as strings. */
/* List of tags as strings. */
tags
:
propTypes
.
array
,
tags
:
propTypes
.
array
.
isRequired
,
/** Services */
/** Services */
serviceUrl
:
propTypes
.
func
,
serviceUrl
:
propTypes
.
func
,
...
...
src/sidebar/components/test/tag-editor-test.js
View file @
481aa74c
This diff is collapsed.
Click to expand it.
src/sidebar/index.js
View file @
481aa74c
...
@@ -194,7 +194,10 @@ function startAngularApp(config) {
...
@@ -194,7 +194,10 @@ function startAngularApp(config) {
)
)
.
component
(
'streamContent'
,
require
(
'./components/stream-content'
))
.
component
(
'streamContent'
,
require
(
'./components/stream-content'
))
.
component
(
'svgIcon'
,
wrapReactComponent
(
require
(
'./components/svg-icon'
)))
.
component
(
'svgIcon'
,
wrapReactComponent
(
require
(
'./components/svg-icon'
)))
.
component
(
'tagEditor'
,
require
(
'./components/tag-editor'
))
.
component
(
'tagEditor'
,
wrapReactComponent
(
require
(
'./components/tag-editor'
))
)
.
component
(
'tagList'
,
wrapReactComponent
(
require
(
'./components/tag-list'
)))
.
component
(
'tagList'
,
wrapReactComponent
(
require
(
'./components/tag-list'
)))
.
component
(
'threadList'
,
require
(
'./components/thread-list'
))
.
component
(
'threadList'
,
require
(
'./components/thread-list'
))
.
component
(
'topBar'
,
wrapReactComponent
(
require
(
'./components/top-bar'
)))
.
component
(
'topBar'
,
wrapReactComponent
(
require
(
'./components/top-bar'
)))
...
...
src/sidebar/templates/annotation.html
View file @
481aa74c
...
@@ -37,12 +37,16 @@
...
@@ -37,12 +37,16 @@
h-branding=
"accentColor"
>
mire
</a>
h-branding=
"accentColor"
>
mire
</a>
</div>
</div>
<!-- Tags -->
<!-- Tags -->
<div
class=
"annotation-body"
ng-if=
"vm.editing()"
>
<tag-editor
<tag-editor
tags=
"vm.state().tags"
ng-if=
"vm.editing()"
on-edit-tags=
"vm.setTags(tags)"
></tag-editor>
annotation=
"vm.annotation"
</div>
on-edit-tags=
"vm.setTags(tags)"
tag-list=
"vm.state().tags"
/>
</tag-editor>
<tag-list
<tag-list
ng-if=
"!vm.editing()"
annotation=
"vm.annotation"
annotation=
"vm.annotation"
tags=
"vm.state().tags"
tags=
"vm.state().tags"
></tag-list>
></tag-list>
...
...
src/sidebar/templates/tag-editor.html
deleted
100644 → 0
View file @
418f2d85
<tags-input
ng-model=
"vm.tagList"
name=
"tags"
class=
"tags"
placeholder=
"Add tags…"
min-length=
"1"
replace-spaces-with-dashes=
"false"
enable-editing-last-tag=
"true"
on-tag-added=
"vm.onTagsChanged()"
on-tag-removed=
"vm.onTagsChanged()"
>
<auto-complete
source=
"vm.autocomplete($query)"
min-length=
"1"
max-results-to-show=
"10"
></auto-complete>
</tags-input>
src/styles/sidebar/components/tag-editor.scss
0 → 100644
View file @
481aa74c
@use
"../../mixins/forms"
;
@use
"../../mixins/buttons"
;
@use
"../../variables"
as
var
;
.tag-editor
{
&
__input
{
@include
forms
.
form-input
;
width
:
100%
;
&
:focus
{
@include
forms
.
form-input-focus
;
}
}
&
__tag-list
{
display
:
flex
;
flex-wrap
:
wrap
;
}
&
__tag-item
{
display
:
flex
;
margin-right
:
0
.5em
;
margin-bottom
:
0
.5em
;
}
&
__edit
{
color
:
var
.
$grey-6
;
background
:
var
.
$grey-1
;
border
:
1px
solid
var
.
$grey-3
;
border-radius
:
2px
0
0
2px
;
border-right-width
:
0
;
padding
:
0
0
.5em
;
}
&
__delete
{
@include
buttons
.
button-base
;
background-color
:
var
.
$grey-1
;
padding
:
0
0
.5em
;
border
:
1px
solid
var
.
$grey-3
;
border-radius
:
0
2px
2px
0
;
&
:hover
{
background-color
:
var
.
$grey-2
;
}
}
}
src/styles/sidebar/sidebar.scss
View file @
481aa74c
...
@@ -60,6 +60,7 @@
...
@@ -60,6 +60,7 @@
@use
'./components/sidebar-panel'
;
@use
'./components/sidebar-panel'
;
@use
'./components/svg-icon'
;
@use
'./components/svg-icon'
;
@use
'./components/spinner'
;
@use
'./components/spinner'
;
@use
'./components/tag-editor'
;
@use
'./components/tag-list'
;
@use
'./components/tag-list'
;
@use
'./components/tags-input'
;
@use
'./components/tags-input'
;
@use
'./components/thread-list'
;
@use
'./components/thread-list'
;
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment