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
855d3695
Unverified
Commit
855d3695
authored
Nov 20, 2019
by
Robert Knight
Committed by
GitHub
Nov 20, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1475 from hypothesis/convert-excerpt
Convert excerpt to Preact (2/n)
parents
804c0177
533fbaf0
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
541 additions
and
638 deletions
+541
-638
annotation-body.js
src/sidebar/components/annotation-body.js
+102
-0
annotation-quote.js
src/sidebar/components/annotation-quote.js
+54
-0
annotation.js
src/sidebar/components/annotation.js
+0
-4
excerpt.js
src/sidebar/components/excerpt.js
+177
-165
annotation-body-test.js
src/sidebar/components/test/annotation-body-test.js
+33
-0
annotation-quote-test.js
src/sidebar/components/test/annotation-quote-test.js
+29
-0
annotation-test.js
src/sidebar/components/test/annotation-test.js
+21
-30
excerpt-test.js
src/sidebar/components/test/excerpt-test.js
+102
-176
index.js
src/sidebar/index.js
+8
-10
annotation.html
src/sidebar/templates/annotation.html
+14
-37
excerpt.html
src/sidebar/templates/excerpt.html
+0
-21
excerpt-overflow-monitor.js
src/sidebar/util/excerpt-overflow-monitor.js
+0
-109
excerpt-overflow-monitor-test.js
src/sidebar/util/test/excerpt-overflow-monitor-test.js
+0
-86
excerpt.scss
src/styles/sidebar/components/excerpt.scss
+1
-0
No files found.
src/sidebar/components/annotation-body.js
0 → 100644
View file @
855d3695
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
Excerpt
=
require
(
'./excerpt'
);
const
MarkdownEditor
=
require
(
'./markdown-editor'
);
const
MarkdownView
=
require
(
'./markdown-view'
);
/**
* Display the rendered content of an annotation.
*/
function
AnnotationBody
({
collapse
,
isEditing
,
isHiddenByModerator
,
hasContent
,
onCollapsibleChanged
,
onEditText
,
onToggleCollapsed
,
text
,
})
{
return
(
<
section
className
=
"annotation-body"
>
{
!
isEditing
&&
(
<
Excerpt
collapse
=
{
collapse
}
collapsedHeight
=
{
400
}
inlineControls
=
{
false
}
onCollapsibleChanged
=
{
onCollapsibleChanged
}
onToggleCollapsed
=
{
collapsed
=>
onToggleCollapsed
({
collapsed
})}
overflowThreshold
=
{
20
}
>
<
MarkdownView
markdown
=
{
text
}
textClass
=
{{
'annotation-body is-hidden'
:
isHiddenByModerator
,
'has-content'
:
hasContent
,
}}
/
>
<
/Excerpt
>
)}
{
isEditing
&&
<
MarkdownEditor
text
=
{
text
}
onEditText
=
{
onEditText
}
/>
}
<
/section
>
);
}
AnnotationBody
.
propTypes
=
{
/**
* Whether to limit the height of the annotation body.
*
* If this is true and the intrinsic height exceeds a fixed threshold, the
* body is truncated. See `onCollapsibleChanged` and `onToggleCollapsed`.
*/
collapse
:
propTypes
.
bool
,
/**
* Whether to show moderated content, if `isHiddenByModerator` is true or
* a placeholder otherwise.
*
* This will be `true` if the current user is a moderator of the annotation's
* group. For non-moderators the content is not exposed via the API.
*/
hasContent
:
propTypes
.
bool
,
/**
* Whether to display the body in edit mode (if true) or view mode.
*/
isEditing
:
propTypes
.
bool
,
/**
* `true` if the contents of this annotation body have been redacted by
* a moderator.
*/
isHiddenByModerator
:
propTypes
.
bool
,
/**
* Callback invoked when the height of the rendered annotation body increases
* above or falls below the threshold at which the `collapse` prop will affect
* it.
*/
onCollapsibleChanged
:
propTypes
.
func
,
/**
* Callback invoked when the user edits the content of the annotation body.
*/
onEditText
:
propTypes
.
func
,
/**
* Callback invoked when the user clicks a shaded area at the bottom of a
* truncated body to indicate that they want to see the rest of the content.
*/
onToggleCollapsed
:
propTypes
.
func
,
/**
* The markdown annotation body, which is either rendered as HTML (if `isEditing`
* is false) or displayed in a text area otherwise.
*/
text
:
propTypes
.
string
,
};
module
.
exports
=
AnnotationBody
;
src/sidebar/components/annotation-quote.js
0 → 100644
View file @
855d3695
'use strict'
;
const
classnames
=
require
(
'classnames'
);
const
{
createElement
}
=
require
(
'preact'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
{
applyTheme
}
=
require
(
'../util/theme'
);
const
Excerpt
=
require
(
'./excerpt'
);
/**
* Display the selected text from the document associated with an annotation.
*/
function
AnnotationQuote
({
isOrphan
,
quote
,
settings
=
{}
})
{
return
(
<
section
className
=
{
classnames
(
'annotation-quote-list'
,
isOrphan
&&
'is-orphan'
)}
>
<
Excerpt
collapsedHeight
=
{
35
}
inlineControls
=
{
true
}
overflowThreshold
=
{
20
}
>
<
blockquote
className
=
"annotation-quote"
style
=
{
applyTheme
([
'selectionFontFamily'
],
settings
)}
>
{
quote
}
<
/blockquote
>
<
/Excerpt
>
<
/section
>
);
}
AnnotationQuote
.
propTypes
=
{
/**
* If `true`, display an indicator that the annotated text was not found in
* the current version of the document.
*/
isOrphan
:
propTypes
.
bool
,
/**
* The text that the annotation refers to. This is rendered as plain text
* (ie. HTML tags are rendered literally).
*/
quote
:
propTypes
.
string
,
// Used for theming.
settings
:
propTypes
.
object
,
};
AnnotationQuote
.
injectedProps
=
[
'settings'
];
module
.
exports
=
withServices
(
AnnotationQuote
);
src/sidebar/components/annotation.js
View file @
855d3695
...
...
@@ -562,10 +562,6 @@ function AnnotationController(
return
;
}
self
.
canCollapseBody
=
canCollapse
;
// This event handler is called from outside the digest cycle, so
// explicitly trigger a digest.
$scope
.
$digest
();
};
this
.
setText
=
function
(
text
)
{
...
...
src/sidebar/components/excerpt.js
View file @
855d3695
'use strict'
;
// @ngInject
function
ExcerptController
(
$element
,
$scope
,
ExcerptOverflowMonitor
)
{
const
self
=
this
;
const
classnames
=
require
(
'classnames'
);
const
propTypes
=
require
(
'prop-types'
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
useCallback
,
useLayoutEffect
,
useRef
,
useState
,
}
=
require
(
'preact/hooks'
);
const
{
applyTheme
}
=
require
(
'../util/theme'
);
const
{
withServices
}
=
require
(
'../util/service-context'
);
const
observeElementSize
=
require
(
'../util/observe-element-size'
);
if
(
this
.
collapse
===
undefined
)
{
this
.
collapse
=
true
;
}
/**
* An optional toggle link at the bottom of an excerpt which controls whether
* it is expanded or collapsed.
*/
function
InlineControls
({
isCollapsed
,
setCollapsed
,
linkStyle
=
{}
})
{
const
toggleTitle
=
isCollapsed
?
'Show the full excerpt'
:
'Show the first few lines only'
;
const
toggleLabel
=
isCollapsed
?
'More'
:
'Less'
;
return
(
<
div
className
=
"excerpt__inline-controls"
>
<
span
className
=
"excerpt__toggle-link"
>
<
a
href
=
"#"
onClick
=
{()
=>
setCollapsed
(
!
isCollapsed
)}
title
=
{
toggleTitle
}
style
=
{
linkStyle
}
>
{
toggleLabel
}
<
/a
>
<
/span
>
<
/div
>
);
}
if
(
this
.
animate
===
undefined
)
{
this
.
animate
=
true
;
}
InlineControls
.
propTypes
=
{
isCollapsed
:
propTypes
.
bool
,
setCollapsed
:
propTypes
.
func
,
linkStyle
:
propTypes
.
object
,
};
this
.
isExpandable
=
function
()
{
return
this
.
overflowing
&&
this
.
collapse
;
};
this
.
isCollapsible
=
function
()
{
return
this
.
overflowing
&&
!
this
.
collapse
;
};
this
.
toggle
=
function
(
event
)
{
// When the user clicks a link explicitly to toggle the collapsed state,
// the event is not propagated.
event
.
stopPropagation
();
this
.
collapse
=
!
this
.
collapse
;
};
this
.
expand
=
function
()
{
// When the user expands the excerpt 'implicitly' by clicking at the bottom
// of the collapsed excerpt, the event is allowed to propagate. For
// annotation cards, this causes clicking on a quote to scroll the view to
// the selected annotation.
this
.
collapse
=
false
;
};
this
.
showInlineControls
=
function
()
{
return
this
.
overflowing
&&
this
.
inlineControls
;
};
this
.
bottomShadowStyles
=
function
()
{
return
{
excerpt__shadow
:
true
,
'excerpt__shadow--transparent'
:
this
.
inlineControls
,
'is-hidden'
:
!
this
.
isExpandable
(),
};
};
// Test if the element or any of its parents have been hidden by
// an 'ng-show' directive
function
isElementHidden
()
{
let
el
=
$element
[
0
];
while
(
el
)
{
if
(
el
.
classList
.
contains
(
'ng-hide'
))
{
return
true
;
}
el
=
el
.
parentElement
;
}
return
false
;
}
const
noop
=
()
=>
{};
const
overflowMonitor
=
new
ExcerptOverflowMonitor
(
{
getState
:
function
()
{
return
{
animate
:
self
.
animate
,
collapsedHeight
:
self
.
collapsedHeight
,
collapse
:
self
.
collapse
,
overflowHysteresis
:
self
.
overflowHysteresis
,
};
},
contentHeight
:
function
()
{
const
contentElem
=
$element
[
0
].
querySelector
(
'.excerpt'
);
if
(
!
contentElem
)
{
return
null
;
}
return
contentElem
.
scrollHeight
;
},
onOverflowChanged
:
function
(
overflowing
)
{
self
.
overflowing
=
overflowing
;
if
(
self
.
onCollapsibleChanged
)
{
self
.
onCollapsibleChanged
({
collapsible
:
overflowing
});
}
// Even though this change happens outside the framework, we use
// $digest() rather than $apply() here to avoid a large number of full
// digest cycles if many excerpts update their overflow state at the
// same time. The onCollapsibleChanged() handler, if any, is
// responsible for triggering any necessary digests in parent scopes.
$scope
.
$digest
();
},
},
window
.
requestAnimationFrame
/**
* A container which truncates its content when they exceed a specified height.
*
* The collapsed state of the container can be handled either via internal
* controls (if `inlineControls` is `true`) or by the caller using the
* `collapse` prop.
*/
function
Excerpt
({
children
,
collapse
=
false
,
collapsedHeight
,
inlineControls
=
true
,
onCollapsibleChanged
=
noop
,
onToggleCollapsed
=
noop
,
overflowThreshold
=
0
,
settings
=
{},
})
{
const
[
collapsedByInlineControls
,
setCollapsedByInlineControls
]
=
useState
(
true
);
this
.
contentStyle
=
overflowMonitor
.
contentStyle
;
// Container for the excerpt's content.
const
contentElement
=
useRef
(
null
);
// Measured height of `contentElement` in pixels.
const
[
contentHeight
,
setContentHeight
]
=
useState
(
0
);
// Update the measured height of the content after the initial render and
// when the size of the content element changes.
const
updateContentHeight
=
useCallback
(()
=>
{
const
newContentHeight
=
contentElement
.
current
.
clientHeight
;
setContentHeight
(
newContentHeight
);
// prettier-ignore
const
isCollapsible
=
newContentHeight
>
(
collapsedHeight
+
overflowThreshold
);
onCollapsibleChanged
({
collapsible
:
isCollapsible
});
},
[
collapsedHeight
,
onCollapsibleChanged
,
overflowThreshold
]);
// Listen for document events which might affect whether the excerpt
// is overflowing, even if its content has not changed.
$element
[
0
].
addEventListener
(
'load'
,
overflowMonitor
.
check
,
false
/* capture */
useLayoutEffect
(()
=>
{
const
cleanup
=
observeElementSize
(
contentElement
.
current
,
updateContentHeight
);
window
.
addEventListener
(
'resize'
,
overflowMonitor
.
check
);
$scope
.
$on
(
'$destroy'
,
function
()
{
window
.
removeEventListener
(
'resize'
,
overflowMonitor
.
check
);
});
// Watch for changes to the visibility of the excerpt.
// Unfortunately there is no DOM API for this, so we rely on a digest
// being triggered after the visibility changes.
$scope
.
$watch
(
isElementHidden
,
function
(
hidden
)
{
if
(
!
hidden
)
{
overflowMonitor
.
check
();
updateContentHeight
();
return
cleanup
;
},
[
updateContentHeight
]);
// Render the (possibly truncated) content and controls for
// expanding/collapsing the content.
// prettier-ignore
const
isOverflowing
=
contentHeight
>
(
collapsedHeight
+
overflowThreshold
);
const
isCollapsed
=
inlineControls
?
collapsedByInlineControls
:
collapse
;
const
isExpandable
=
isOverflowing
&&
isCollapsed
;
const
contentStyle
=
{};
if
(
contentHeight
!==
0
)
{
contentStyle
[
'max-height'
]
=
isExpandable
?
collapsedHeight
:
contentHeight
;
}
});
// Watch input properties which may affect the overflow state
$scope
.
$watch
(
'vm.contentData'
,
overflowMonitor
.
check
);
// Trigger an initial calculation of the overflow state.
//
// This is performed asynchronously so that the content of the <excerpt>
// has settled - ie. all Angular directives have been fully applied and
// the DOM has stopped changing. This may take several $digest cycles.
overflowMonitor
.
check
();
const
setCollapsed
=
collapsed
=>
inlineControls
?
setCollapsedByInlineControls
(
collapsed
)
:
onToggleCollapsed
(
collapsed
);
return
(
<
div
className
=
"excerpt"
style
=
{
contentStyle
}
>
<
div
test
-
name
=
"excerpt-content"
ref
=
{
contentElement
}
>
{
children
}
<
/div
>
<
div
onClick
=
{()
=>
setCollapsed
(
false
)}
className
=
{
classnames
({
excerpt__shadow
:
true
,
'excerpt__shadow--transparent'
:
inlineControls
,
'is-hidden'
:
!
isExpandable
,
})}
title
=
"Show the full excerpt"
/>
{
isOverflowing
&&
inlineControls
&&
(
<
InlineControls
isCollapsed
=
{
collapsedByInlineControls
}
setCollapsed
=
{
setCollapsed
}
linkStyle
=
{
applyTheme
([
'selectionFontFamily'
],
settings
)}
/
>
)}
<
/div
>
);
}
/**
* @description This component truncates the height of its contents to a
* specified number of lines and provides controls for expanding
* and collapsing the resulting truncated element.
Excerpt
.
propTypes
=
{
children
:
propTypes
.
object
,
/**
* If `true`, the excerpt provides internal controls to expand and collapse
* the content. If `false`, the caller sets the collapsed state via the
* `collapse` prop.
*
* When using inline controls, the excerpt is initially collapsed.
*/
module
.
exports
=
{
controller
:
ExcerptController
,
controllerAs
:
'vm'
,
bindings
:
{
/** Whether or not expansion should be animated. Defaults to true. */
animate
:
'<?'
,
inlineControls
:
propTypes
.
bool
,
/**
* The data which is used to generate the excerpt's content.
* When this changes, the excerpt will recompute whether the content
* is overflowing.
* If the content should be truncated if its height exceeds
* `collapsedHeight + overflowThreshold`.
*
* This prop is only used if `inlineControls` is false.
*/
contentData
:
'<'
,
collapse
:
propTypes
.
bool
,
/**
* Specifies whether controls to expand and collapse
* the excerpt should be shown inside the <excerpt> component.
* If false, external controls can expand/collapse the excerpt by
* setting the 'collapse' property.
* Maximum height of the container, in pixels, when it is collapsed.
*/
inlineControls
:
'<'
,
/** Sets whether or not the excerpt is collapsed. */
collapse
:
'=?'
,
collapsedHeight
:
propTypes
.
number
,
/**
* Called when the collapsibility of the excerpt (that is, whether or
* not the content height exceeds the collapsed height), changes.
*
* Note: This function is *not* called from inside a digest cycle,
* the caller is responsible for triggering any necessary digests.
* An additional margin of pixels by which the content height can exceed
* `collapsedHeight` before it becomes collapsible.
*/
onCollapsibleChanged
:
'&?'
,
/** The height of this container in pixels when collapsed.
overflowThreshold
:
propTypes
.
number
,
/**
* Called when the content height exceeds or falls below `collapsedHeight + overflowThreshold`.
*/
collapsedHeight
:
'<'
,
onCollapsibleChanged
:
propTypes
.
func
,
/**
* The number of pixels by which the height of the excerpt's content
* must extend beyond the collapsed height in order for truncation to
* be activated. This prevents the 'More' link from being shown to expand
* the excerpt if it has only been truncated by a very small amount, such
* that expanding the excerpt would reveal no extra lines of text.
* When `inlineControls` is `false`, this function is called when the user
* requests to expand the content by clicking a zone at the bottom of the
* container.
*/
overflowHysteresis
:
'<?'
,
},
transclude
:
true
,
template
:
require
(
'../templates/excerpt.html'
)
,
onToggleCollapsed
:
propTypes
.
func
,
// Used for theming.
settings
:
propTypes
.
object
,
};
Excerpt
.
injectedProps
=
[
'settings'
];
module
.
exports
=
withServices
(
Excerpt
);
src/sidebar/components/test/annotation-body-test.js
0 → 100644
View file @
855d3695
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
AnnotationBody
=
require
(
'../annotation-body'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
describe
(
'AnnotationBody'
,
()
=>
{
function
createBody
(
props
=
{})
{
return
mount
(
<
AnnotationBody
text
=
"test comment"
{...
props
}
/>
)
;
}
beforeEach
(()
=>
{
AnnotationBody
.
$imports
.
$mock
(
mockImportedComponents
());
});
afterEach
(()
=>
{
AnnotationBody
.
$imports
.
$restore
();
});
it
(
'displays the body if `isEditing` is false'
,
()
=>
{
const
wrapper
=
createBody
({
isEditing
:
false
});
assert
.
isFalse
(
wrapper
.
exists
(
'MarkdownEditor'
));
assert
.
isTrue
(
wrapper
.
exists
(
'MarkdownView'
));
});
it
(
'displays an editor if `isEditing` is true'
,
()
=>
{
const
wrapper
=
createBody
({
isEditing
:
true
});
assert
.
isTrue
(
wrapper
.
exists
(
'MarkdownEditor'
));
assert
.
isFalse
(
wrapper
.
exists
(
'MarkdownView'
));
});
});
src/sidebar/components/test/annotation-quote-test.js
0 → 100644
View file @
855d3695
'use strict'
;
const
{
createElement
}
=
require
(
'preact'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
AnnotationQuote
=
require
(
'../annotation-quote'
);
const
mockImportedComponents
=
require
(
'./mock-imported-components'
);
describe
(
'AnnotationQuote'
,
()
=>
{
function
createQuote
(
props
)
{
return
mount
(
<
AnnotationQuote
quote
=
"test quote"
settings
=
{{}}
{...
props
}
/
>
);
}
beforeEach
(()
=>
{
AnnotationQuote
.
$imports
.
$mock
(
mockImportedComponents
());
});
afterEach
(()
=>
{
AnnotationQuote
.
$imports
.
$restore
();
});
it
(
'renders the quote'
,
()
=>
{
const
wrapper
=
createQuote
();
const
quote
=
wrapper
.
find
(
'blockquote'
);
assert
.
equal
(
quote
.
text
(),
'test quote'
);
});
});
src/sidebar/components/test/annotation-test.js
View file @
855d3695
...
...
@@ -156,16 +156,22 @@ describe('annotation', function() {
onClick
:
'&'
,
},
})
.
component
(
'
markdownEditor
'
,
{
.
component
(
'
annotationBody
'
,
{
bindings
:
{
text
:
'<'
,
collapse
:
'<'
,
hasContent
:
'<'
,
isEditing
:
'<'
,
isHiddenByModerator
:
'<'
,
onCollapsibleChanged
:
'&'
,
onEditText
:
'&'
,
onToggleCollapsed
:
'&'
,
text
:
'<'
,
},
})
.
component
(
'
markdownView
'
,
{
.
component
(
'
annotationQuote
'
,
{
bindings
:
{
markdow
n
:
'<'
,
textClass
:
'<'
,
isOrpha
n
:
'<'
,
quote
:
'<'
,
},
});
});
...
...
@@ -1256,18 +1262,6 @@ describe('annotation', function() {
});
});
it
(
'renders quotes as plain text'
,
function
()
{
const
ann
=
fixtures
.
defaultAnnotation
();
ann
.
target
[
0
].
selector
=
[
{
type
:
'TextQuoteSelector'
,
exact
:
'<<-&->>'
,
},
];
const
el
=
createDirective
(
ann
).
element
;
assert
.
equal
(
el
[
0
].
querySelector
(
'blockquote'
).
textContent
,
'<<-&->>'
);
});
[
{
context
:
'for moderators'
,
...
...
@@ -1275,10 +1269,8 @@ describe('annotation', function() {
// Content still present.
text
:
'Some offensive content'
,
}),
textClass
:
{
'annotation-body is-hidden'
:
true
,
'has-content'
:
true
,
},
isHiddenByModerator
:
true
,
hasContent
:
true
,
},
{
context
:
'for non-moderators'
,
...
...
@@ -1287,18 +1279,17 @@ describe('annotation', function() {
tags
:
[],
text
:
''
,
}),
textClass
:
{
'annotation-body is-hidden'
:
true
,
'has-content'
:
false
,
isHiddenByModerator
:
true
,
hasContent
:
false
,
},
},
].
forEach
(
testCase
=>
{
it
(
`renders hidden annotations with a custom text class (
${
testCase
.
context
}
)`
,
()
=>
{
const
el
=
createDirective
(
testCase
.
ann
).
element
;
].
forEach
(({
ann
,
context
,
isHiddenByModerator
,
hasContent
})
=>
{
it
(
`passes moderation status to annotation body (
${
context
}
)`
,
()
=>
{
const
el
=
createDirective
(
ann
).
element
;
assert
.
match
(
el
.
find
(
'
markdown-view'
).
controller
(
'markdownView
'
),
el
.
find
(
'
annotation-body'
).
controller
(
'annotationBody
'
),
sinon
.
match
({
textClass
:
testCase
.
textClass
,
isHiddenByModerator
,
hasContent
,
})
);
});
...
...
src/sidebar/components/test/excerpt-test.js
View file @
855d3695
'use strict'
;
const
angular
=
require
(
'angular'
);
const
util
=
require
(
'../../directive/test/util'
);
const
excerpt
=
require
(
'../excerpt'
);
describe
(
'excerpt'
,
function
()
{
// ExcerptOverflowMonitor fake instance created by the current test
let
fakeOverflowMonitor
;
const
SHORT_DIV
=
'<div id="foo" style="height:5px;"></div>'
;
const
TALL_DIV
=
'<div id="foo" style="height:200px;">foo bar</div>'
;
function
excerptComponent
(
attrs
,
content
)
{
const
defaultAttrs
=
{
contentData
:
'the content'
,
collapsedHeight
:
40
,
inlineControls
:
false
,
};
attrs
=
Object
.
assign
(
defaultAttrs
,
attrs
);
return
util
.
createDirective
(
document
,
'excerpt'
,
attrs
,
{},
content
);
const
{
createElement
}
=
require
(
'preact'
);
const
{
act
}
=
require
(
'preact/test-utils'
);
const
{
mount
}
=
require
(
'enzyme'
);
const
Excerpt
=
require
(
'../excerpt'
);
describe
(
'Excerpt'
,
()
=>
{
const
SHORT_DIV
=
<
div
id
=
"foo"
style
=
"height: 5px;"
/>
;
const
TALL_DIV
=
(
<
div
id
=
"foo"
style
=
"height: 200px;"
>
foo
bar
<
/div
>
);
const
DEFAULT_CONTENT
=
<
span
className
=
"the-content"
>
default
content
<
/span>
;
let
container
;
let
fakeObserveElementSize
;
function
createExcerpt
(
props
=
{},
content
=
DEFAULT_CONTENT
)
{
return
mount
(
<
Excerpt
collapse
=
{
true
}
collapsedHeight
=
{
40
}
inlineControls
=
{
false
}
settings
=
{{}}
{...
props
}
>
{
content
}
<
/Excerpt>
,
{
attachTo
:
container
}
);
}
before
(
function
()
{
angular
.
module
(
'app'
,
[]).
component
(
'excerpt'
,
excerpt
);
beforeEach
(()
=>
{
fakeObserveElementSize
=
sinon
.
stub
();
container
=
document
.
createElement
(
'div'
);
document
.
body
.
appendChild
(
container
);
Excerpt
.
$imports
.
$mock
({
'../util/observe-element-size'
:
fakeObserveElementSize
,
});
});
beforeEach
(
function
()
{
function
FakeOverflowMonitor
(
ctrl
)
{
fakeOverflowMonitor
=
this
;
// eslint-disable-line consistent-this
afterEach
(()
=>
{
container
.
remove
();
});
this
.
ctrl
=
ctrl
;
this
.
check
=
sinon
.
stub
();
this
.
contentStyle
=
sinon
.
stub
().
returns
({});
function
getExcerptHeight
(
wrapper
)
{
return
wrapper
.
find
(
'.excerpt'
).
prop
(
'style'
)[
'max-height'
];
}
angular
.
mock
.
module
(
'app'
);
angular
.
mock
.
module
(
function
(
$provide
)
{
$provide
.
value
(
'ExcerptOverflowMonitor'
,
FakeOverflowMonitor
);
}
);
it
(
'renders content in container'
,
()
=>
{
const
wrapper
=
createExcerpt
();
const
contentEl
=
wrapper
.
find
(
'[test-name="excerpt-content"]'
);
assert
.
include
(
contentEl
.
html
(),
'default content'
);
});
context
(
'when created'
,
function
()
{
it
(
'schedules an overflow state recalculation'
,
function
()
{
excerptComponent
({},
'<span id="foo"></span>'
);
assert
.
called
(
fakeOverflowMonitor
.
check
);
it
(
'truncates content if it exceeds `collapsedHeight` + `overflowThreshold`'
,
()
=>
{
const
wrapper
=
createExcerpt
({},
TALL_DIV
);
assert
.
equal
(
getExcerptHeight
(
wrapper
),
40
);
});
it
(
'passes input properties to overflow state recalc'
,
function
()
{
const
attrs
=
{
animate
:
false
,
collapsedHeight
:
40
,
inlineControls
:
false
,
overflowHysteresis
:
20
,
};
excerptComponent
(
attrs
,
'<span></span>'
);
assert
.
deepEqual
(
fakeOverflowMonitor
.
ctrl
.
getState
(),
{
animate
:
attrs
.
animate
,
collapsedHeight
:
attrs
.
collapsedHeight
,
collapse
:
true
,
overflowHysteresis
:
attrs
.
overflowHysteresis
,
});
it
(
'does not truncate content if it does not exceed `collapsedHeight` + `overflowThreshold`'
,
()
=>
{
const
wrapper
=
createExcerpt
({},
SHORT_DIV
);
assert
.
equal
(
getExcerptHeight
(
wrapper
),
5
);
});
it
(
'reports the content height to ExcerptOverflowMonitor'
,
function
()
{
excerptComponent
({},
TALL_DIV
);
assert
.
deepEqual
(
fakeOverflowMonitor
.
ctrl
.
contentHeight
(),
200
);
});
});
it
(
'updates the collapsed state when the content height changes'
,
()
=>
{
const
wrapper
=
createExcerpt
({},
SHORT_DIV
);
assert
.
called
(
fakeObserveElementSize
);
context
(
'input changes'
,
function
()
{
it
(
'schedules an overflow state check when inputs change'
,
function
()
{
const
element
=
excerptComponent
({},
'<span></span>'
);
fakeOverflowMonitor
.
check
.
reset
();
element
.
scope
.
contentData
=
'new-content'
;
element
.
scope
.
$digest
();
assert
.
calledOnce
(
fakeOverflowMonitor
.
check
);
const
contentElem
=
fakeObserveElementSize
.
getCall
(
0
).
args
[
0
];
const
sizeChangedCallback
=
fakeObserveElementSize
.
getCall
(
0
).
args
[
1
];
act
(()
=>
{
contentElem
.
style
.
height
=
'400px'
;
sizeChangedCallback
();
});
wrapper
.
update
();
it
(
'does not schedule a state check if inputs are unchanged'
,
function
()
{
const
element
=
excerptComponent
({},
'<span></span>'
);
fakeOverflowMonitor
.
check
.
reset
();
element
.
scope
.
$digest
();
assert
.
notCalled
(
fakeOverflowMonitor
.
check
);
});
});
assert
.
equal
(
getExcerptHeight
(
wrapper
),
40
);
context
(
'document events'
,
function
()
{
it
(
'schedules an overflow check when media loads'
,
function
()
{
const
element
=
excerptComponent
(
{},
'<img src="https://example.com/foo.jpg">'
);
fakeOverflowMonitor
.
check
.
reset
();
util
.
sendEvent
(
element
[
0
],
'load'
);
assert
.
called
(
fakeOverflowMonitor
.
check
);
act
(()
=>
{
contentElem
.
style
.
height
=
'10px'
;
sizeChangedCallback
();
});
wrapper
.
update
();
it
(
'schedules an overflow check when the window is resized'
,
function
()
{
const
element
=
excerptComponent
({},
'<span></span>'
);
fakeOverflowMonitor
.
check
.
reset
();
util
.
sendEvent
(
element
[
0
].
ownerDocument
.
defaultView
,
'resize'
);
assert
.
called
(
fakeOverflowMonitor
.
check
);
});
assert
.
equal
(
getExcerptHeight
(
wrapper
),
10
);
});
context
(
'visibility changes'
,
function
()
{
it
(
'schedules an overflow check when shown'
,
function
()
{
const
element
=
excerptComponent
({},
'<span></span>'
);
fakeOverflowMonitor
.
check
.
reset
();
it
(
'calls `onCollapsibleChanged` when collapsibility changes'
,
()
=>
{
const
onCollapsibleChanged
=
sinon
.
stub
();
createExcerpt
({
onCollapsibleChanged
},
SHORT_DIV
);
// ng-hide is the class used by the ngShow and ngHide directives
// to show or hide elements. For now, this is the only way of hiding
// or showing excerpts that we need to support.
element
[
0
].
classList
.
add
(
'ng-hide'
);
element
.
scope
.
$digest
();
assert
.
notCalled
(
fakeOverflowMonitor
.
check
);
element
[
0
].
classList
.
remove
(
'ng-hide'
);
element
.
scope
.
$digest
();
assert
.
called
(
fakeOverflowMonitor
.
check
);
});
const
contentElem
=
fakeObserveElementSize
.
getCall
(
0
).
args
[
0
];
const
sizeChangedCallback
=
fakeObserveElementSize
.
getCall
(
0
).
args
[
1
];
act
(()
=>
{
contentElem
.
style
.
height
=
'400px'
;
sizeChangedCallback
();
});
context
(
'excerpt content style'
,
function
()
{
it
(
'sets the content style using ExcerptOverflowMonitor#contentStyle()'
,
function
()
{
const
element
=
excerptComponent
({},
'<span></span>'
);
fakeOverflowMonitor
.
contentStyle
.
returns
({
'max-height'
:
'52px'
});
element
.
scope
.
$digest
();
const
content
=
element
[
0
].
querySelector
(
'.excerpt'
);
assert
.
equal
(
content
.
style
.
cssText
.
trim
(),
'max-height: 52px;'
);
});
assert
.
calledWith
(
onCollapsibleChanged
,
{
collapsible
:
true
});
});
function
isHidden
(
el
)
{
return
!
el
.
offsetParent
||
el
.
classList
.
contains
(
'ng-hide'
);
}
it
(
'calls `onToggleCollapsed` when user clicks in bottom area to expand excerpt'
,
()
=>
{
const
onToggleCollapsed
=
sinon
.
stub
();
const
wrapper
=
createExcerpt
({
onToggleCollapsed
},
TALL_DIV
);
const
control
=
wrapper
.
find
(
'.excerpt__shadow'
);
assert
.
equal
(
getExcerptHeight
(
wrapper
),
40
);
control
.
simulate
(
'click'
);
assert
.
called
(
onToggleCollapsed
);
});
function
findVisible
(
el
,
selector
)
{
const
elements
=
el
.
querySelectorAll
(
selector
);
for
(
let
i
=
0
;
i
<
elements
.
length
;
i
++
)
{
if
(
!
isHidden
(
elements
[
i
]))
{
return
elements
[
i
];
}
}
return
undefined
;
}
context
(
'when inline controls are enabled'
,
()
=>
{
it
(
'displays inline controls if collapsed'
,
()
=>
{
const
wrapper
=
createExcerpt
({
inlineControls
:
true
},
TALL_DIV
);
assert
.
isTrue
(
wrapper
.
exists
(
'.excerpt__inline-controls'
));
});
describe
(
'inline controls'
,
function
()
{
function
findInlineControl
(
el
)
{
return
findVisible
(
el
,
'.excerpt__toggle-link'
);
}
it
(
'does not display inline controls if not collapsed'
,
()
=>
{
const
wrapper
=
createExcerpt
({
inlineControls
:
true
},
SHORT_DIV
);
assert
.
isFalse
(
wrapper
.
exists
(
'.excerpt__inline-controls'
)
);
}
);
it
(
'displays inline controls if collapsed'
,
function
()
{
const
element
=
excerptComponent
({
inlineControls
:
true
},
TALL_DIV
);
fakeOverflowMonitor
.
ctrl
.
onOverflowChanged
(
true
);
const
expandLink
=
findInlineControl
(
element
[
0
]);
assert
.
ok
(
expandLink
);
assert
.
equal
(
expandLink
.
querySelector
(
'a'
).
textContent
,
'More'
);
});
it
(
'does not display inline controls if not collapsed'
,
function
()
{
const
element
=
excerptComponent
({
inlineControls
:
true
},
SHORT_DIV
);
const
expandLink
=
findInlineControl
(
element
[
0
]);
assert
.
notOk
(
expandLink
);
});
it
(
'toggles the expanded state when clicked'
,
function
()
{
const
element
=
excerptComponent
({
inlineControls
:
true
},
TALL_DIV
);
fakeOverflowMonitor
.
ctrl
.
onOverflowChanged
(
true
);
const
expandLink
=
findInlineControl
(
element
[
0
]);
angular
.
element
(
expandLink
.
querySelector
(
'a'
)).
click
();
element
.
scope
.
$digest
();
const
collapseLink
=
findInlineControl
(
element
[
0
]);
assert
.
equal
(
collapseLink
.
querySelector
(
'a'
).
textContent
,
'Less'
);
});
});
describe
(
'bottom area'
,
function
()
{
it
(
'expands the excerpt when clicking at the bottom if collapsed'
,
function
()
{
const
element
=
excerptComponent
({
inlineControls
:
true
},
TALL_DIV
);
element
.
scope
.
$digest
();
assert
.
isTrue
(
element
.
ctrl
.
collapse
);
const
bottomArea
=
element
[
0
].
querySelector
(
'.excerpt__shadow'
);
angular
.
element
(
bottomArea
).
click
();
assert
.
isFalse
(
element
.
ctrl
.
collapse
);
});
});
describe
(
'#onCollapsibleChanged'
,
function
()
{
it
(
'is called when overflow state changes'
,
function
()
{
const
callback
=
sinon
.
stub
();
excerptComponent
(
{
onCollapsibleChanged
:
{
args
:
[
'collapsible'
],
callback
:
callback
,
},
},
'<span></span>'
);
fakeOverflowMonitor
.
ctrl
.
onOverflowChanged
(
true
);
assert
.
calledWith
(
callback
,
true
);
fakeOverflowMonitor
.
ctrl
.
onOverflowChanged
(
false
);
assert
.
calledWith
(
callback
,
false
);
it
(
'toggles the expanded state when clicked'
,
()
=>
{
const
wrapper
=
createExcerpt
({
inlineControls
:
true
},
TALL_DIV
);
const
control
=
wrapper
.
find
(
'.excerpt__inline-controls a'
);
assert
.
equal
(
getExcerptHeight
(
wrapper
),
40
);
control
.
simulate
(
'click'
);
assert
.
equal
(
getExcerptHeight
(
wrapper
),
200
);
});
});
});
src/sidebar/index.js
View file @
855d3695
...
...
@@ -130,6 +130,10 @@ function startAngularApp(config) {
// UI components
.
component
(
'annotation'
,
require
(
'./components/annotation'
))
.
component
(
'annotationBody'
,
wrapReactComponent
(
require
(
'./components/annotation-body'
))
)
.
component
(
'annotationHeader'
,
wrapReactComponent
(
require
(
'./components/annotation-header'
))
...
...
@@ -142,6 +146,10 @@ function startAngularApp(config) {
'annotationPublishControl'
,
wrapReactComponent
(
require
(
'./components/annotation-publish-control'
))
)
.
component
(
'annotationQuote'
,
wrapReactComponent
(
require
(
'./components/annotation-quote'
))
)
.
component
(
'annotationShareDialog'
,
require
(
'./components/annotation-share-dialog'
)
...
...
@@ -151,7 +159,6 @@ function startAngularApp(config) {
'annotationViewerContent'
,
require
(
'./components/annotation-viewer-content'
)
)
.
component
(
'excerpt'
,
require
(
'./components/excerpt'
))
.
component
(
'helpPanel'
,
wrapReactComponent
(
require
(
'./components/help-panel'
))
...
...
@@ -160,14 +167,6 @@ function startAngularApp(config) {
'loggedOutMessage'
,
wrapReactComponent
(
require
(
'./components/logged-out-message'
))
)
.
component
(
'markdownEditor'
,
wrapReactComponent
(
require
(
'./components/markdown-editor'
))
)
.
component
(
'markdownView'
,
wrapReactComponent
(
require
(
'./components/markdown-view'
))
)
.
component
(
'moderationBanner'
,
wrapReactComponent
(
require
(
'./components/moderation-banner'
))
...
...
@@ -232,7 +231,6 @@ function startAngularApp(config) {
// Utilities
.
value
(
'Discovery'
,
require
(
'../shared/discovery'
))
.
value
(
'ExcerptOverflowMonitor'
,
require
(
'./util/excerpt-overflow-monitor'
))
.
value
(
'OAuthClient'
,
require
(
'./util/oauth-client'
))
.
value
(
'VirtualThreadList'
,
require
(
'./virtual-thread-list'
))
.
value
(
'isSidebar'
,
isSidebar
)
...
...
src/sidebar/templates/annotation.html
View file @
855d3695
...
...
@@ -13,45 +13,22 @@
show-document-info=
"vm.showDocumentInfo"
>
</annotation-header>
<
!-- Excerpts -->
<section
class=
"annotation-quote-list
"
ng-class=
"{'is-orphan' : vm.isOrphan()}
"
<
annotation-quote
quote=
"vm.quote()
"
is-orphan=
"vm.isOrphan()
"
ng-if=
"vm.quote()"
>
<excerpt
collapsed-height=
"35"
inline-controls=
"true"
overflow-hysteresis=
"20"
content-data=
"selector.exact"
>
<blockquote
class=
"annotation-quote"
h-branding=
"selectionFontFamily"
ng-bind=
"vm.quote()"
></blockquote>
</excerpt>
</section>
</annotation-quote>
<!-- / Excerpts -->
<!-- Body -->
<section
name=
"text"
class=
"annotation-body"
>
<excerpt
inline-controls=
"false"
on-collapsible-changed=
"vm.setBodyCollapsible(collapsible)"
<annotation-body
collapse=
"vm.collapseBody"
collapsed-height=
"400"
overflow-hysteresis=
"20"
content-data=
"vm.state().text"
ng-if=
"!vm.editing()"
>
<markdown-view
markdown=
"vm.state().text"
text-class=
"{'annotation-body is-hidden':vm.isHiddenByModerator(),
'has-content':vm.hasContent()}"
>
</markdown-view>
</excerpt>
<markdown-editor
text=
"vm.state().text"
has-content=
"vm.hasContent()"
is-editing=
"vm.editing()"
is-hidden-by-moderator=
"vm.isHiddenByModerator()"
on-collapsible-changed=
"vm.setBodyCollapsible(collapsible)"
on-edit-text=
"vm.setText(text)"
ng-if=
"vm.editing()"
>
</markdown-editor>
</section>
<!-- / Body -->
on-toggle-collapsed=
"vm.collapseBody = collapsed"
text=
"vm.state().text"
>
</annotation-body>
<!-- Tags -->
<div
class=
"annotation-body form-field"
ng-if=
"vm.editing()"
>
...
...
src/sidebar/templates/excerpt.html
deleted
100644 → 0
View file @
804c0177
<div
class=
"excerpt__container"
>
<div
class=
"excerpt"
ng-style=
"vm.contentStyle()"
>
<div
ng-transclude
></div>
<div
ng-click=
"vm.expand()"
ng-class=
"vm.bottomShadowStyles()"
title=
"Show the full excerpt"
></div>
<div
class=
"excerpt__inline-controls"
ng-show=
"vm.showInlineControls()"
>
<span
class=
"excerpt__toggle-link"
ng-show=
"vm.isExpandable()"
>
…
<a
ng-click=
"vm.toggle($event)"
title=
"Show the full excerpt"
h-branding=
"accentColor, selectionFontFamily"
>
More
</a>
</span>
<span
class=
"excerpt__toggle-link"
ng-show=
"vm.isCollapsible()"
>
<a
ng-click=
"vm.toggle($event)"
title=
"Show the first few lines only"
h-branding=
"accentColor, selectionFontFamily"
>
Less
</a>
</span>
</div>
</div>
</div>
src/sidebar/util/excerpt-overflow-monitor.js
deleted
100644 → 0
View file @
804c0177
'use strict'
;
function
toPx
(
val
)
{
return
val
.
toString
()
+
'px'
;
}
/**
* Interface used by ExcerptOverflowMonitor to retrieve the state of the
* <excerpt> and report when the state changes.
*
* interface Excerpt {
* getState(): State;
* contentHeight(): number | undefined;
* onOverflowChanged(): void;
* }
*/
/**
* A helper for the <excerpt> component which handles determinination of the
* overflow state and content styling given the current state of the component
* and the height of its contents.
*
* When the state of the excerpt or its content changes, the component should
* call check() to schedule an async update of the overflow state.
*
* @param {Excerpt} excerpt - Interface used to query the current state of the
* excerpt and notify it when the overflow state changes.
* @param {(callback) => number} requestAnimationFrame -
* Function called to schedule an async recalculation of the overflow
* state.
*/
function
ExcerptOverflowMonitor
(
excerpt
,
requestAnimationFrame
)
{
let
pendingUpdate
=
false
;
// Last-calculated overflow state
let
prevOverflowing
;
function
update
()
{
const
state
=
excerpt
.
getState
();
if
(
!
pendingUpdate
)
{
return
;
}
pendingUpdate
=
false
;
const
hysteresisPx
=
state
.
overflowHysteresis
||
0
;
const
overflowing
=
excerpt
.
contentHeight
()
>
state
.
collapsedHeight
+
hysteresisPx
;
if
(
overflowing
===
prevOverflowing
)
{
return
;
}
prevOverflowing
=
overflowing
;
excerpt
.
onOverflowChanged
(
overflowing
);
}
/**
* Schedule a deferred check of whether the content is collapsed.
*/
function
check
()
{
if
(
pendingUpdate
)
{
return
;
}
pendingUpdate
=
true
;
requestAnimationFrame
(
update
);
}
/**
* Returns an object mapping CSS properties to values that should be applied
* to an excerpt's content element in order to truncate it based on the
* current overflow state.
*/
function
contentStyle
()
{
const
state
=
excerpt
.
getState
();
let
maxHeight
=
''
;
if
(
prevOverflowing
)
{
if
(
state
.
collapse
)
{
maxHeight
=
toPx
(
state
.
collapsedHeight
);
}
else
if
(
state
.
animate
)
{
// Animating the height change requires that the final
// height be specified exactly, rather than relying on
// auto height
maxHeight
=
toPx
(
excerpt
.
contentHeight
());
}
}
else
if
(
typeof
prevOverflowing
===
'undefined'
&&
state
.
collapse
)
{
// If the excerpt is collapsed but the overflowing state has not yet
// been computed then the exact max height is unknown, but it will be
// in the range [state.collapsedHeight, state.collapsedHeight +
// state.overflowHysteresis]
//
// Here we guess that the final content height is most likely to be
// either less than `collapsedHeight` or more than `collapsedHeight` +
// `overflowHysteresis`, in which case it will be truncated to
// `collapsedHeight`.
maxHeight
=
toPx
(
state
.
collapsedHeight
);
}
return
{
'max-height'
:
maxHeight
,
};
}
this
.
contentStyle
=
contentStyle
;
this
.
check
=
check
;
}
module
.
exports
=
ExcerptOverflowMonitor
;
src/sidebar/util/test/excerpt-overflow-monitor-test.js
deleted
100644 → 0
View file @
804c0177
'use strict'
;
const
ExcerptOverflowMonitor
=
require
(
'../excerpt-overflow-monitor'
);
describe
(
'ExcerptOverflowMonitor'
,
function
()
{
let
contentHeight
;
let
ctrl
;
let
monitor
;
let
state
;
beforeEach
(
function
()
{
contentHeight
=
0
;
state
=
{
animate
:
true
,
collapsedHeight
:
100
,
collapse
:
true
,
overflowHysteresis
:
20
,
};
ctrl
=
{
getState
:
function
()
{
return
state
;
},
contentHeight
:
function
()
{
return
contentHeight
;
},
onOverflowChanged
:
sinon
.
stub
(),
};
monitor
=
new
ExcerptOverflowMonitor
(
ctrl
,
function
(
callback
)
{
callback
();
});
});
describe
(
'overflow state'
,
function
()
{
it
(
'overflows if height > collaped height + hysteresis'
,
function
()
{
contentHeight
=
200
;
monitor
.
check
();
assert
.
calledWith
(
ctrl
.
onOverflowChanged
,
true
);
});
it
(
'does not overflow if height < collapsed height'
,
function
()
{
contentHeight
=
80
;
monitor
.
check
();
assert
.
calledWith
(
ctrl
.
onOverflowChanged
,
false
);
});
it
(
'does not overflow if height is in [collapsed height, collapsed height + hysteresis]'
,
function
()
{
contentHeight
=
110
;
monitor
.
check
();
assert
.
calledWith
(
ctrl
.
onOverflowChanged
,
false
);
});
});
context
(
'#contentStyle'
,
function
()
{
it
(
'sets max-height if collapsed and overflowing'
,
function
()
{
contentHeight
=
200
;
monitor
.
check
();
assert
.
deepEqual
(
monitor
.
contentStyle
(),
{
'max-height'
:
'100px'
});
});
it
(
'sets max height to empty if not overflowing'
,
function
()
{
contentHeight
=
80
;
monitor
.
check
();
assert
.
deepEqual
(
monitor
.
contentStyle
(),
{
'max-height'
:
''
});
});
it
(
'sets max-height if overflow state is unknown'
,
function
()
{
// Before the initial overflow check, the state is unknown
assert
.
deepEqual
(
monitor
.
contentStyle
(),
{
'max-height'
:
'100px'
});
});
});
context
(
'#check'
,
function
()
{
it
(
'calls onOverflowChanged() if state changed'
,
function
()
{
contentHeight
=
200
;
monitor
.
check
();
ctrl
.
onOverflowChanged
=
sinon
.
stub
();
contentHeight
=
250
;
monitor
.
check
();
assert
.
notCalled
(
ctrl
.
onOverflowChanged
);
});
});
});
src/styles/sidebar/components/excerpt.scss
View file @
855d3695
...
...
@@ -5,6 +5,7 @@
.excerpt
{
transition
:
max-height
$expand-duration
ease-in
;
overflow
:
hidden
;
position
:
relative
;
}
// a container which wraps the <excerpt> and contains the excerpt
...
...
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