mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
[DSC-516] Change truncable components in order to show more/less button
This commit is contained in:
@@ -7,13 +7,11 @@
|
|||||||
class="lead"
|
class="lead"
|
||||||
[innerHTML]="firstMetadataValue('organization.legalName')"></span>
|
[innerHTML]="firstMetadataValue('organization.legalName')"></span>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3">
|
|
||||||
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
<span *ngIf="dso.allMetadata(['dc.description']).length > 0"
|
||||||
class="item-list-org-unit-description">
|
class="item-list-org-unit-description">
|
||||||
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span
|
||||||
[innerHTML]="firstMetadataValue('dc.description')"></span>
|
[innerHTML]="firstMetadataValue('dc.description')"></span>
|
||||||
</ds-truncatable-part>
|
</ds-truncatable-part>
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable-part>
|
|
||||||
</span>
|
</span>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -1,5 +1,11 @@
|
|||||||
<div class="clamp-{{background}}-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
|
<div class="clamp-{{background}}-{{lines}} min-{{minLines}} {{type}} {{fixedHeight ? 'fixedHeight' : ''}}">
|
||||||
<div class="content dont-break-out">
|
<div class="content dont-break-out" id="dontBreakContent">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
<label for="dontBreakContent" role="button" id="expandButton">
|
||||||
|
<a dsDragClick (actualClick)="toggle()">
|
||||||
|
<i class="fas fa-angle-down"></i> {{ 'item.truncatable-part.show-more' | translate }}</a>
|
||||||
|
</label>
|
||||||
|
<a id="collapseButton" dsDragClick (actualClick)="toggle()" *ngIf="expand && expandable">
|
||||||
|
<i class="fas fa-angle-up"></i> {{ 'item.truncatable-part.show-less' | translate }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,10 @@
|
|||||||
|
#dontBreakContent:not(.truncated) ~ label{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #207698 !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@@ -4,10 +4,16 @@ import { TruncatablePartComponent } from './truncatable-part.component';
|
|||||||
import { TruncatableService } from '../truncatable.service';
|
import { TruncatableService } from '../truncatable.service';
|
||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getMockTranslateService } from '../../mocks/translate.service.mock';
|
||||||
|
import { TranslateLoaderMock } from '../../mocks/translate-loader.mock';
|
||||||
|
import { mockTruncatableService } from '../../mocks/mock-trucatable.service';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
describe('TruncatablePartComponent', () => {
|
describe('TruncatablePartComponent', () => {
|
||||||
let comp: TruncatablePartComponent;
|
let comp: TruncatablePartComponent;
|
||||||
let fixture: ComponentFixture<TruncatablePartComponent>;
|
let fixture: ComponentFixture<TruncatablePartComponent>;
|
||||||
|
let translateService: TranslateService;
|
||||||
const id1 = '123';
|
const id1 = '123';
|
||||||
const id2 = '456';
|
const id2 = '456';
|
||||||
|
|
||||||
@@ -22,8 +28,16 @@ describe('TruncatablePartComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
translateService = getMockTranslateService();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [NoopAnimationsModule],
|
imports: [NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
declarations: [TruncatablePartComponent],
|
declarations: [TruncatablePartComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||||
@@ -52,6 +66,11 @@ describe('TruncatablePartComponent', () => {
|
|||||||
it('lines should equal minlines', () => {
|
it('lines should equal minlines', () => {
|
||||||
expect((comp as any).lines).toEqual(comp.minLines.toString());
|
expect((comp as any).lines).toEqual(comp.minLines.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('collapseButton should be hidden', () => {
|
||||||
|
const a = fixture.debugElement.query(By.css('#collapseButton'));
|
||||||
|
expect(a).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When the item is expanded', () => {
|
describe('When the item is expanded', () => {
|
||||||
@@ -72,5 +91,62 @@ describe('TruncatablePartComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect((comp as any).lines).toEqual('none');
|
expect((comp as any).lines).toEqual('none');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('collapseButton should be shown', () => {
|
||||||
|
(comp as any).setLines();
|
||||||
|
(comp as any).expandable = true;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const a = fixture.debugElement.query(By.css('#collapseButton'));
|
||||||
|
expect(a).not.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('TruncatablePartComponent', () => {
|
||||||
|
let comp: TruncatablePartComponent;
|
||||||
|
let fixture: ComponentFixture<TruncatablePartComponent>;
|
||||||
|
let translateService: TranslateService;
|
||||||
|
const identifier = '1234567890';
|
||||||
|
let truncatableService;
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
translateService = getMockTranslateService();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
NoopAnimationsModule,
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
declarations: [TruncatablePartComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(TruncatablePartComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(TruncatablePartComponent);
|
||||||
|
comp = fixture.componentInstance; // TruncatablePartComponent test instance
|
||||||
|
comp.id = identifier;
|
||||||
|
fixture.detectChanges();
|
||||||
|
truncatableService = (comp as any).service;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When toggle is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(truncatableService, 'toggle');
|
||||||
|
comp.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggle on the TruncatableService', () => {
|
||||||
|
expect(truncatableService.toggle).toHaveBeenCalledWith(identifier);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
import { Component, Inject, Input, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
|
||||||
import { TruncatableService } from '../truncatable.service';
|
import { TruncatableService } from '../truncatable.service';
|
||||||
import { hasValue } from '../../empty.util';
|
import { hasValue } from '../../empty.util';
|
||||||
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
|
import { NativeWindowRef, NativeWindowService } from 'src/app/core/services/window.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-truncatable-part',
|
selector: 'ds-truncatable-part',
|
||||||
@@ -49,8 +51,34 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
|||||||
* Subscription to unsubscribe from
|
* Subscription to unsubscribe from
|
||||||
*/
|
*/
|
||||||
private sub;
|
private sub;
|
||||||
|
/**
|
||||||
|
* store variable used for local to expand collapse
|
||||||
|
*/
|
||||||
|
expand = false;
|
||||||
|
/**
|
||||||
|
* variable to check if expandable
|
||||||
|
*/
|
||||||
|
expandable = false;
|
||||||
|
/**
|
||||||
|
* variable to check if it is a browser
|
||||||
|
*/
|
||||||
|
isBrowser: boolean;
|
||||||
|
/**
|
||||||
|
* variable which save get observer
|
||||||
|
*/
|
||||||
|
observer;
|
||||||
|
/**
|
||||||
|
* variable to save content to be observed
|
||||||
|
*/
|
||||||
|
observedContent;
|
||||||
|
|
||||||
public constructor(private service: TruncatableService) {
|
public constructor(
|
||||||
|
private service: TruncatableService,
|
||||||
|
@Inject(DOCUMENT) private document: any,
|
||||||
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
|
@Inject(PLATFORM_ID) platformId: object
|
||||||
|
) {
|
||||||
|
this.isBrowser = isPlatformBrowser(platformId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,12 +95,70 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
|||||||
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
|
this.sub = this.service.isCollapsed(this.id).subscribe((collapsed: boolean) => {
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
this.lines = this.minLines.toString();
|
this.lines = this.minLines.toString();
|
||||||
|
this.expand = false;
|
||||||
} else {
|
} else {
|
||||||
this.lines = this.maxLines < 0 ? 'none' : this.maxLines.toString();
|
this.lines = this.maxLines < 0 ? 'none' : this.maxLines.toString();
|
||||||
|
this.expand = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngAfterContentChecked() {
|
||||||
|
if (this.isBrowser) {
|
||||||
|
if (this.observer && this.observedContent) {
|
||||||
|
this.toUnobserve();
|
||||||
|
}
|
||||||
|
this.toObserve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to get data to be observed
|
||||||
|
*/
|
||||||
|
toObserve() {
|
||||||
|
this.observedContent = this.document.querySelectorAll('#dontBreakContent');
|
||||||
|
this.observer = new (this._window.nativeWindow as any).ResizeObserver(entries => {
|
||||||
|
// tslint:disable-next-line:prefer-const
|
||||||
|
for (let entry of entries) {
|
||||||
|
if (!entry.target.classList.contains('notruncatable')) {
|
||||||
|
if (entry.target.scrollHeight > entry.contentRect.height) {
|
||||||
|
if (entry.target.children.length > 0) {
|
||||||
|
if (entry.target.children[0].offsetHeight > entry.contentRect.height) {
|
||||||
|
entry.target.classList.add('truncated');
|
||||||
|
} else {
|
||||||
|
entry.target.classList.remove('truncated');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.target.classList.add('truncated');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entry.target.classList.remove('truncated');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.observedContent.forEach(p => {
|
||||||
|
this.observer.observe(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to remove data which is observed
|
||||||
|
*/
|
||||||
|
toUnobserve() {
|
||||||
|
this.observedContent.forEach(p => {
|
||||||
|
this.observer.unobserve(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expands the truncatable when it's collapsed, collapses it when it's expanded
|
||||||
|
*/
|
||||||
|
public toggle() {
|
||||||
|
this.service.toggle(this.id);
|
||||||
|
this.expandable = !this.expandable;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from the subscription
|
* Unsubscribe from the subscription
|
||||||
*/
|
*/
|
||||||
@@ -80,5 +166,8 @@ export class TruncatablePartComponent implements OnInit, OnDestroy {
|
|||||||
if (hasValue(this.sub)) {
|
if (hasValue(this.sub)) {
|
||||||
this.sub.unsubscribe();
|
this.sub.unsubscribe();
|
||||||
}
|
}
|
||||||
|
if (this.isBrowser) {
|
||||||
|
this.toUnobserve();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
<div dsDragClick (actualClick)="toggle()" (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse()">
|
<div (mouseenter)="hoverExpand()" (mouseleave)="hoverCollapse()">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -70,15 +70,4 @@ describe('TruncatableComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When toggle is called', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(truncatableService, 'toggle');
|
|
||||||
comp.toggle();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call toggle on the TruncatableService', () => {
|
|
||||||
expect(truncatableService.toggle).toHaveBeenCalledWith(identifier);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { AfterViewChecked, Component, ElementRef, Input, OnInit } from '@angular/core';
|
||||||
Component, Input
|
|
||||||
} from '@angular/core';
|
|
||||||
import { TruncatableService } from './truncatable.service';
|
import { TruncatableService } from './truncatable.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -13,7 +11,7 @@ import { TruncatableService } from './truncatable.service';
|
|||||||
/**
|
/**
|
||||||
* Component that represents a section with one or more truncatable parts that all listen to this state
|
* Component that represents a section with one or more truncatable parts that all listen to this state
|
||||||
*/
|
*/
|
||||||
export class TruncatableComponent {
|
export class TruncatableComponent implements OnInit, AfterViewChecked {
|
||||||
/**
|
/**
|
||||||
* Is true when all truncatable parts in this truncatable should be expanded on loading
|
* Is true when all truncatable parts in this truncatable should be expanded on loading
|
||||||
*/
|
*/
|
||||||
@@ -29,7 +27,7 @@ export class TruncatableComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() onHover = false;
|
@Input() onHover = false;
|
||||||
|
|
||||||
public constructor(private service: TruncatableService) {
|
public constructor(private service: TruncatableService, private el: ElementRef,) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,11 +59,14 @@ export class TruncatableComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
ngAfterViewChecked() {
|
||||||
* Expands the truncatable when it's collapsed, collapses it when it's expanded
|
const truncatedElements = this.el.nativeElement.querySelectorAll('.truncated');
|
||||||
*/
|
if (truncatedElements?.length > 1) {
|
||||||
public toggle() {
|
for (let i = 0; i < (truncatedElements.length - 1); i++) {
|
||||||
this.service.toggle(this.id);
|
truncatedElements[i].classList.remove('truncated');
|
||||||
|
truncatedElements[i].classList.add('notruncatable');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2056,6 +2056,10 @@
|
|||||||
|
|
||||||
"item.search.title": "Item Search",
|
"item.search.title": "Item Search",
|
||||||
|
|
||||||
|
"item.truncatable-part.show-more": "Show more",
|
||||||
|
|
||||||
|
"item.truncatable-part.show-less": "Collapse",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"item.page.abstract": "Abstract",
|
"item.page.abstract": "Abstract",
|
||||||
|
Reference in New Issue
Block a user