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
Hide 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'
;
// @ngInject
function
TagEditorController
(
tags
)
{
this
.
onTagsChanged
=
function
()
{
tags
.
store
(
this
.
tagList
);
const
newTags
=
this
.
tagList
.
map
(
function
(
item
)
{
return
item
.
text
;
});
this
.
onEditTags
({
tags
:
newTags
});
const
{
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
useMemo
,
useRef
,
useState
}
=
require
(
'preact/hooks'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
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
);
}
});
return
suggestionsSet
.
sort
();
};
// Call filter with an empty string to return all suggestions
return
removeDuplicates
(
tagsService
.
filter
(
''
),
tagList
);
},
[
tagsService
,
tagList
]);
/**
* 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
.
autocomplete
=
function
(
query
)
{
return
Promise
.
resolve
(
tags
.
filter
(
query
));
/**
* Remove a tag from this annotation.
*
* @param {string} tag
*/
const
removeTag
=
tag
=>
{
const
newTagList
=
[...
tagList
];
// make a copy
const
index
=
newTagList
.
indexOf
(
tag
);
newTagList
.
splice
(
index
,
1
);
updateTags
(
newTagList
);
};
this
.
$onChanges
=
function
(
changes
)
{
if
(
changes
.
tags
)
{
this
.
tagList
=
changes
.
tags
.
currentValue
.
map
(
function
(
tag
)
{
return
{
text
:
tag
};
});
/**
* 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
,
controllerAs
:
'vm'
,
bindings
:
{
tags
:
'<'
,
onEditTags
:
'&'
,
},
template
:
require
(
'../templates/tag-editor.html'
),
/**
* @typedef Tag
* @param tag {string} - The tag text
*/
TagEditor
.
propTypes
=
{
/**
* 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 }) {
TagList
.
propTypes
=
{
/* Annotation that owns the tags. */
annotation
:
propTypes
.
object
,
annotation
:
propTypes
.
object
.
isRequired
,
/* List of tags as strings. */
tags
:
propTypes
.
array
,
tags
:
propTypes
.
array
.
isRequired
,
/** Services */
serviceUrl
:
propTypes
.
func
,
...
...
src/sidebar/components/test/tag-editor-test.js
View file @
481aa74c
'use strict'
;
const
angular
=
require
(
'angular'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
util
=
require
(
'../../directive/test/util'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
const
TagEditor
=
require
(
'../tag-editor'
);
describe
(
'tagEditor'
,
function
()
{
let
fakeTags
;
describe
(
'TagEditor'
,
function
()
{
let
fakeTags
=
[
'tag1'
,
'tag2'
];
let
fakeTagsService
;
let
fakeServiceUrl
;
let
fakeOnEditTags
;
before
(
function
()
{
angular
.
module
(
'app'
,
[]).
component
(
'tagEditor'
,
require
(
'../tag-editor'
));
});
function
createComponent
(
props
)
{
return
mount
(
<
TagEditor
// props
onEditTags
=
{
fakeOnEditTags
}
tagList
=
{
fakeTags
}
// service props
serviceUrl
=
{
fakeServiceUrl
}
tags
=
{
fakeTagsService
}
{...
props
}
/
>
);
}
// Simulates a selection event from datalist
function
selectOption
(
wrapper
)
{
wrapper
.
find
(
'input'
).
simulate
(
'input'
,
{
inputType
:
undefined
});
}
// Simulates a selection event from datalist. This is the
// only event that fires in in Safari. In Chrome, both the `keyup`
// and `input` events fire, but only the first one adds the tag.
function
selectOptionViaKeyUp
(
wrapper
)
{
wrapper
.
find
(
'input'
).
simulate
(
'keyup'
,
{
key
:
'Enter'
});
}
// Simulates a typing input event
function
typeInput
(
wrapper
)
{
wrapper
.
find
(
'input'
).
simulate
(
'input'
,
{
inputType
:
'insertText'
});
}
beforeEach
(
function
()
{
fakeTags
=
{
filter
:
sinon
.
stub
(),
fakeOnEditTags
=
sinon
.
stub
();
fakeServiceUrl
=
sinon
.
stub
().
returns
(
'http://serviceurl.com'
);
fakeTagsService
=
{
filter
:
sinon
.
stub
().
returns
([
'tag4'
,
'tag3'
]),
store
:
sinon
.
stub
(),
};
angular
.
mock
.
module
(
'app'
,
{
tags
:
fakeTags
,
TagEditor
.
$imports
.
$mock
(
mockImportedComponents
());
});
afterEach
(()
=>
{
TagEditor
.
$imports
.
$restore
();
});
it
(
'adds appropriate tag values to the elements'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'li'
).
forEach
((
tag
,
i
)
=>
{
assert
.
isTrue
(
tag
.
hasClass
(
'tag-editor__tag-item'
));
assert
.
equal
(
tag
.
text
(),
fakeTags
[
i
]);
assert
.
equal
(
tag
.
prop
(
'aria-label'
),
`Tag:
${
fakeTags
[
i
]}
`
);
});
});
it
(
'converts tags to the form expected by ng-tags-input'
,
function
()
{
const
element
=
util
.
createDirective
(
document
,
'tag-editor'
,
{
tags
:
[
'foo'
,
'bar'
],
it
(
"creates a `list` prop on the input that matches the datalist's `id`"
,
()
=>
{
const
wrapper
=
createComponent
();
assert
.
equal
(
wrapper
.
find
(
'input'
).
prop
(
'list'
),
wrapper
.
find
(
'datalist'
).
prop
(
'id'
)
);
});
it
(
'creates multiple TagEditors with unique datalist `id`s'
,
()
=>
{
const
wrapper1
=
createComponent
();
const
wrapper2
=
createComponent
();
assert
.
notEqual
(
wrapper1
.
find
(
'datalist'
).
prop
(
'id'
),
wrapper2
.
find
(
'datalist'
).
prop
(
'id'
)
);
});
it
(
'generates a ordered datalist containing the array values returned from fakeTagsService.filter '
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'non-empty'
;
wrapper
.
find
(
'input'
).
simulate
(
'input'
,
{
inputType
:
'insertText'
});
// fakeTagsService.filter returns ['tag4', 'tag3'], but
// datalist shall be ordered as ['tag3', 'tag4']
assert
.
equal
(
wrapper
.
find
(
'datalist option'
)
.
at
(
0
)
.
prop
(
'value'
),
'tag3'
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
)
.
at
(
1
)
.
prop
(
'value'
),
'tag4'
);
});
[
{
text
:
' in Chrome and Safari'
,
eventPayload
:
{
inputType
:
undefined
},
},
{
text
:
' in Firefox'
,
eventPayload
:
{
inputType
:
'insertReplacementText'
},
},
].
forEach
(
test
=>
{
it
(
`clears the suggestions when selecting a tag from datalist
${
test
.
text
}
`
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
typeInput
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
2
);
wrapper
.
find
(
'input'
).
simulate
(
'input'
,
test
.
eventPayload
);
// simulates a selection
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
0
);
assert
.
isTrue
(
fakeTagsService
.
store
.
calledWith
([
{
text
:
'tag1'
},
{
text
:
'tag2'
},
{
text
:
'tag3'
},
])
);
});
assert
.
deepEqual
(
element
.
ctrl
.
tagList
,
[{
text
:
'foo'
},
{
text
:
'bar'
}]);
});
describe
(
'when tags are changed'
,
function
()
{
let
element
;
let
onEditTags
;
it
(
'clears the suggestions when deleting input'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
typeInput
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
2
);
wrapper
.
find
(
'input'
).
instance
().
value
=
''
;
wrapper
.
find
(
'input'
)
.
simulate
(
'input'
,
{
inputType
:
'deleteContentBackward'
});
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
0
);
});
beforeEach
(
function
()
{
onEditTags
=
sinon
.
stub
();
element
=
util
.
createDirective
(
document
,
'tag-editor'
,
{
onEditTags
:
{
args
:
[
'tags'
],
callback
:
onEditTags
},
tags
:
[
'foo'
],
});
element
.
ctrl
.
onTagsChanged
();
it
(
'does not clear the suggestions when deleting only part of the input'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
typeInput
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
2
);
wrapper
.
find
(
'input'
).
instance
().
value
=
't'
;
// non-empty input remains
wrapper
.
find
(
'input'
)
.
simulate
(
'input'
,
{
inputType
:
'deleteContentBackward'
});
assert
.
notEqual
(
wrapper
.
find
(
'datalist option'
).
length
,
0
);
});
it
(
'does not render duplicate suggestions'
,
()
=>
{
// `tag3` supplied in the `tagList` will be a duplicate value relative
// with the fakeTagsService.filter result above.
const
wrapper
=
createComponent
({
editMode
:
true
,
tagList
:
[
'tag1'
,
'tag2'
,
'tag3'
],
});
wrapper
.
find
(
'input'
).
instance
().
value
=
'non-empty'
;
typeInput
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
1
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
)
.
at
(
0
)
.
prop
(
'value'
),
'tag4'
);
});
it
(
'calls onEditTags handler'
,
function
()
{
assert
.
calledWith
(
onEditTags
,
sinon
.
match
([
'foo'
]));
context
(
'when adding tags'
,
()
=>
{
/**
* Helper function to assert that a tag was correctly added
*/
const
assertAddTagsSuccess
=
(
wrapper
,
tagList
)
=>
{
// saves the suggested tags to the service
assert
.
isTrue
(
fakeTagsService
.
store
.
calledWith
(
tagList
.
map
(
tag
=>
({
text
:
tag
})))
);
// called the onEditTags callback prop
assert
.
isTrue
(
fakeOnEditTags
.
calledWith
({
tags
:
tagList
}));
// clears out the suggestions
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
0
);
// assert the input value is cleared out
assert
.
equal
(
wrapper
.
find
(
'input'
).
instance
().
value
,
''
);
// note: focus not tested
};
/**
* Helper function to assert that a tag was correctly not added
*/
const
assertAddTagsFail
=
()
=>
{
assert
.
isTrue
(
fakeTagsService
.
store
.
notCalled
);
assert
.
isTrue
(
fakeOnEditTags
.
notCalled
);
};
it
(
'adds a tag from the input field'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
selectOption
(
wrapper
);
assertAddTagsSuccess
(
wrapper
,
[
'tag1'
,
'tag2'
,
'tag3'
]);
});
it
(
'saves tags to the store'
,
function
()
{
assert
.
calledWith
(
fakeTags
.
store
,
sinon
.
match
([{
text
:
'foo'
}]));
it
(
'adds a tag from the input field via keyup event'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
selectOptionViaKeyUp
(
wrapper
);
assertAddTagsSuccess
(
wrapper
,
[
'tag1'
,
'tag2'
,
'tag3'
]);
});
});
describe
(
'#autocomplete'
,
function
()
{
it
(
'suggests tags using the `tags` service'
,
function
()
{
const
element
=
util
.
createDirective
(
document
,
'tag-editor'
,
{
tags
:
[],
it
(
'populate the datalist, then adds a tag from the input field'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
typeInput
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
2
);
selectOption
(
wrapper
);
assertAddTagsSuccess
(
wrapper
,
[
'tag1'
,
'tag2'
,
'tag3'
]);
});
it
(
'clears out the <option> elements after adding a tag'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'non-empty'
;
typeInput
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
2
);
selectOption
(
wrapper
);
assert
.
equal
(
wrapper
.
find
(
'datalist option'
).
length
,
0
);
});
it
(
'should not add a tag if the input is empty'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
''
;
selectOption
(
wrapper
);
assertAddTagsFail
();
});
it
(
'should not add a tag if the input is blank space'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
' '
;
selectOption
(
wrapper
);
assertAddTagsFail
();
});
it
(
'should not add a tag if its a duplicate of one already in the list'
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag1'
;
selectOption
(
wrapper
);
assertAddTagsFail
();
});
[
{
key
:
'Enter'
,
text
:
'adds a tag via keypress `Enter`'
,
run
:
wrapper
=>
{
assertAddTagsSuccess
(
wrapper
,
[
'tag1'
,
'tag2'
,
'tag3'
]);
},
},
{
key
:
','
,
text
:
'adds a tag via keypress `,`'
,
run
:
wrapper
=>
{
assertAddTagsSuccess
(
wrapper
,
[
'tag1'
,
'tag2'
,
'tag3'
]);
},
},
{
key
:
'e'
,
text
:
'does not add a tag when key is not `,` or `Enter`'
,
run
:
()
=>
{
assertAddTagsFail
();
},
},
].
forEach
(
test
=>
{
it
(
test
.
text
,
()
=>
{
const
wrapper
=
createComponent
();
wrapper
.
find
(
'input'
).
instance
().
value
=
'tag3'
;
wrapper
.
find
(
'input'
).
simulate
(
'keypress'
,
{
key
:
test
.
key
});
test
.
run
(
wrapper
);
});
element
.
ctrl
.
autocomplete
(
'query'
);
assert
.
calledWith
(
fakeTags
.
filter
,
'query'
);
});
});
context
(
'when removing tags'
,
()
=>
{
it
(
'removes `tag1` when clicking its delete button'
,
()
=>
{
const
wrapper
=
createComponent
();
// note: initial tagList is ['tag1', 'tag2']
assert
.
equal
(
wrapper
.
find
(
'.tag-editor__edit'
).
length
,
2
);
wrapper
.
find
(
'button'
)
.
at
(
0
)
// delete 'tag1'
.
simulate
(
'click'
);
// saves the suggested tags to the service (only 'tag2' should be passed)
assert
.
isTrue
(
fakeTagsService
.
store
.
calledWith
([{
text
:
'tag2'
}]));
// called the onEditTags callback prop (only 'tag2' should be passed)
assert
.
isTrue
(
fakeOnEditTags
.
calledWith
({
tags
:
[
'tag2'
]
}));
});
});
});
src/sidebar/index.js
View file @
481aa74c
...
...
@@ -194,7 +194,10 @@ function startAngularApp(config) {
)
.
component
(
'streamContent'
,
require
(
'./components/stream-content'
))
.
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
(
'threadList'
,
require
(
'./components/thread-list'
))
.
component
(
'topBar'
,
wrapReactComponent
(
require
(
'./components/top-bar'
)))
...
...
src/sidebar/templates/annotation.html
View file @
481aa74c
...
...
@@ -37,12 +37,16 @@
h-branding=
"accentColor"
>
mire
</a>
</div>
<!-- Tags -->
<div
class=
"annotation-body"
ng-if=
"vm.editing()"
>
<tag-editor
tags=
"vm.state().tags"
on-edit-tags=
"vm.setTags(tags)"
></tag-editor>
</div>
<tag-editor
ng-if=
"vm.editing()"
annotation=
"vm.annotation"
on-edit-tags=
"vm.setTags(tags)"
tag-list=
"vm.state().tags"
/>
</tag-editor>
<tag-list
ng-if=
"!vm.editing()"
annotation=
"vm.annotation"
tags=
"vm.state().tags"
></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 @@
@use
'./components/sidebar-panel'
;
@use
'./components/svg-icon'
;
@use
'./components/spinner'
;
@use
'./components/tag-editor'
;
@use
'./components/tag-list'
;
@use
'./components/tags-input'
;
@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