Migrated `<GhUnsplashPhoto>` to glimmer component

refs https://github.com/TryGhost/Ghost/issues/14101

- extracted the ratio zoom handling to a `{{ratio-zoom}}` modifier to clean up the component and avoid needing lifecycle hooks that don't exist in Glimmer components
- disabled `no-nested-interactive` linting in the template - not ideal but we'd need a much bigger design refactor to eliminate the nested links
This commit is contained in:
Kevin Ansfield 2022-02-08 10:13:18 +00:00
parent ec45a9d0aa
commit f46111bb77
4 changed files with 129 additions and 140 deletions

View File

@ -1769,3 +1769,11 @@ remove|ember-template-lint|no-invalid-interactive|3|45|3|45|a6f8ead2f0f3f3bdd702
remove|ember-template-lint|no-invalid-interactive|80|132|80|132|b6e7f26b5f7ac647dd8659b6a67fa0d0de27142f|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash.hbs
remove|ember-template-lint|no-passed-in-event-handlers|27|24|27|24|b5cce16b65649e02f9ce2a14986a6f83e46cbd58|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash.hbs
remove|ember-template-lint|no-passed-in-event-handlers|28|24|28|24|fb4c7afaa35fdebcf2f71540bdb1903fd70bb9d7|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash.hbs
remove|ember-template-lint|no-action|1|46|1|46|7deb52aad96dd83886b4877b4042e1d7e173f96e|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|no-action|18|63|18|63|be51c27cc7a2bc76b71c185656c46f6d0496d6ad|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|no-nested-interactive|6|16|6|16|9b80f9fdc2a6a96bd2a8489ab62540b8d7d8e3b0|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|no-nested-interactive|7|16|7|16|e0cc49f31d676d97f88b72277592aef5c790ad15|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|no-nested-interactive|11|20|11|20|23958701600d242ed32febf00abfb86671b44ad3|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|no-nested-interactive|14|20|14|20|fbc1273324d6ef3210b3c903dda0e18daaf35bcd|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|no-nested-interactive|18|16|18|16|7c6cd3a62707e321896096224b805580adfe62a1|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs
remove|ember-template-lint|require-valid-alt-text|12|24|12|24|67be77b9e376bf354f1ea2b8eca8e987dd4bd9cf|1643760000000|1646352000000|1648940400000|app/components/gh-unsplash-photo.hbs

View File

@ -1,21 +1,22 @@
<a class="gh-unsplash-photo" href="#" onclick={{action "zoom"}} data-unsplash-zoomed-photo={{if this.zoomed this.photo.id}} data-test-unsplash-photo={{this.photo.id}} style={{this.style}}>
{{!-- template-lint-disable no-nested-interactive --}}
<a class="gh-unsplash-photo" href="#" {{on "click" this.zoom}} {{ratio-zoom zoomed=@zoomed ratio=@photo.ratio}} style={{this.style}} data-test-unsplash-photo={{@photo.id}}>
<div class="gh-unsplash-photo-container" style={{this.containerStyle}} data-test-unsplash-photo-container>
<img src={{this.imageUrl}} alt={{this.photo.description}} width={{this.width}} height={{this.height}} data-test-unsplash-photo-image />
<img src={{this.imageUrl}} alt={{@photo.description}} width={{this.width}} height={{this.height}} data-test-unsplash-photo-image />
<div class="gh-unsplash-photo-overlay">
<div class="gh-unsplash-photo-header">
<a class="gh-unsplash-button-likes gh-unsplash-button" href="{{this.photo.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank" rel="noopener noreferrer">{{svg-jar "unsplash-heart"}}{{this.photo.likes}}</a>
<a class="gh-unsplash-button-download gh-unsplash-button" href="{{this.photo.links.download}}/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit&force=true">{{svg-jar "download"}}</a>
<a class="gh-unsplash-button-likes gh-unsplash-button" href="{{@photo.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank" rel="noopener noreferrer">{{svg-jar "unsplash-heart"}}{{@photo.likes}}</a>
<a class="gh-unsplash-button-download gh-unsplash-button" href="{{@photo.links.download}}/?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit&force=true">{{svg-jar "download"}}</a>
</div>
<div class="gh-unsplash-photo-footer">
<div class="gh-unsplash-photo-author">
<a class="gh-unsplash-photo-author-img" href="{{this.photo.user.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank" rel="noopener noreferrer">
<img src="{{this.photo.user.profile_image.medium}}" />
<a class="gh-unsplash-photo-author-img" href="{{@photo.user.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank" rel="noopener noreferrer">
<img src="{{@photo.user.profile_image.medium}}" alt={{@photo.alt_description}} />
</a>
<a class="gh-unsplash-photo-author-name" href="{{this.photo.user.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank" rel="noopener noreferrer">
{{this.photo.user.name}}
<a class="gh-unsplash-photo-author-name" href="{{@photo.user.links.html}}?utm_source=ghost&utm_medium=referral&utm_campaign=api-credit" target="_blank" rel="noopener noreferrer">
{{@photo.user.name}}
</a>
</div>
<a class="gh-unsplash-button" href="#" onclick={{action "select"}}>Insert image</a>
<a class="gh-unsplash-button" href="#" {{on "click" this.select}}>Insert image</a>
</div>
</div>
</div>

View File

@ -1,32 +1,23 @@
import $ from 'jquery';
import Component from '@ember/component';
import {computed} from '@ember/object';
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {htmlSafe} from '@ember/template';
import {run} from '@ember/runloop';
import {tracked} from '@glimmer/tracking';
export default Component.extend({
export default class GhUnsplashPhoto extends Component {
@tracked height = 0;
@tracked width = 1200;
height: 0,
photo: null,
tagName: '',
width: 1200,
zoomed: false,
// closure actions
select() {},
zoom() {},
style: computed('zoomed', function () {
return htmlSafe(this.zoomed ? 'width: auto; margin: 0;' : '');
}),
get style() {
return htmlSafe(this.args.zoomed ? 'width: auto; margin: 0;' : '');
}
// avoid "binding style attributes" warnings
containerStyle: computed('photo.color', 'zoomed', function () {
let styles = [];
let ratio = this.get('photo.ratio');
let zoomed = this.zoomed;
get containerStyle() {
const styles = [];
const ratio = this.args.photo.ratio;
const zoomed = this.args.zoomed;
styles.push(`background-color: ${this.get('photo.color')}`);
styles.push(`background-color: ${this.args.photo.color}`);
if (zoomed) {
styles.push(`cursor: zoom-out`);
@ -35,118 +26,39 @@ export default Component.extend({
}
return htmlSafe(styles.join('; '));
}),
}
imageUrl: computed('photo.urls.regular', function () {
let url = this.get('photo.urls.regular');
get imageUrl() {
let url = this.args.photo.urls.regular;
url = url.replace('&w=1080', '&w=1200');
return url;
}),
didReceiveAttrs() {
this._super(...arguments);
this.set('height', this.width * this.photo.ratio);
if (this.zoomed && !this._zoomed) {
this._setZoomedSize();
}
this._zoomed = this.zoomed;
if (this.zoomed && !this._resizeHandler) {
this._setupResizeHandler();
} else if (!this.zoomed && this._resizeHandler) {
this._teardownResizeHandler();
}
},
didInsertElement() {
this._super(...arguments);
this._hasRendered = true;
if (this.zoomed) {
this._setZoomedSize();
}
},
willDestroyElement() {
this._super(...arguments);
this._teardownResizeHandler();
},
actions: {
select(event) {
event.preventDefault();
event.stopPropagation();
this.select(this.photo);
},
zoom(event) {
let $target = $(event.target);
// only zoom when it wasn't one of the child links clicked
if (!$target.is('a') && $target.closest('a').hasClass('gh-unsplash-photo')) {
event.preventDefault();
this.zoom(this.photo);
}
// don't propagate otherwise we can trigger the closeZoom action on the overlay
event.stopPropagation();
}
},
_setZoomedSize() {
if (!this._hasRendered) {
return false;
}
let a = document.querySelector(`[data-unsplash-zoomed-photo="${this.photo.id}"]`);
a.style.width = '100%';
a.style.height = '100%';
let offsets = a.getBoundingClientRect();
let ratio = this.photo.ratio;
let maxHeight = {
width: offsets.height / ratio,
height: offsets.height
};
let maxWidth = {
width: offsets.width,
height: offsets.width * ratio
};
let usableSize = null;
if (ratio <= 1) {
usableSize = maxWidth.height > offsets.height ? maxHeight : maxWidth;
} else {
usableSize = maxHeight.width > offsets.width ? maxWidth : maxHeight;
}
a.style.width = `${usableSize.width}px`;
a.style.height = `${usableSize.height}px`;
},
_setupResizeHandler() {
if (this._resizeHandler) {
return;
}
this._resizeHandler = run.bind(this, this._handleResize);
window.addEventListener('resize', this._resizeHandler);
},
_teardownResizeHandler() {
window.removeEventListener('resize', this._resizeHandler);
this._resizeHandler = null;
},
_handleResize() {
this._throttleResize = run.throttle(this, this._setZoomedSize, 100);
}
});
constructor() {
super(...arguments);
this.height = this.width * this.args.photo.ratio;
}
@action
select(event) {
event.preventDefault();
event.stopPropagation();
this.args.select(this.args.photo);
}
@action
zoom(event) {
const {target} = event;
// only zoom when it wasn't one of the child links clicked
if (!target.matches('a') && target.closest('a').classList.contains('gh-unsplash-photo')) {
event.preventDefault();
this.args.zoom(this.args.photo);
}
// don't propagate otherwise we can trigger the closeZoom action on the overlay
event.stopPropagation();
}
}

View File

@ -0,0 +1,68 @@
import Modifier from 'ember-modifier';
import {bind, throttle} from '@ember/runloop';
export default class RatioZoom extends Modifier {
resizeHandler = null;
didReceiveArguments() {
const {zoomed} = this.args.named;
if (zoomed) {
this.setZoomedSize();
}
}
willDestroy() {
this.removeResizeEventListener();
}
setZoomedSize() {
const {ratio} = this.args.named;
this.element.style.width = '100%';
this.element.style.height = '100%';
const offsets = this.element.getBoundingClientRect();
let maxHeight = {
width: offsets.height / ratio,
height: offsets.height
};
let maxWidth = {
width: offsets.width,
height: offsets.width * ratio
};
let usableSize = null;
if (ratio <= 1) {
usableSize = maxWidth.height > offsets.height ? maxHeight : maxWidth;
} else {
usableSize = maxHeight.width > offsets.width ? maxWidth : maxHeight;
}
this.element.style.width = `${usableSize.width}px`;
this.element.style.height = `${usableSize.height}px`;
this.addResizeEventListener();
}
handleResize() {
throttle(this, this.setZoomedSize, 100);
}
addResizeEventListener() {
if (!this.resizeHandler) {
this.resizeHandler = bind(this, this.handleResize);
window.addEventListener('resize', this.resizeHandler);
}
}
removeResizeEventListener() {
if (this.resizeHandler) {
window.removeEventListener('resize', this.resizeHandler);
this.resizeHandler = null;
}
}
}