Ghost-Admin/app/components/gh-navitem-url-input.js

171 lines
5.0 KiB
JavaScript

import TextField from '@ember/component/text-field';
import classic from 'ember-classic-decorator';
import validator from 'validator';
import {classNames} from '@ember-decorators/component';
import {computed} from '@ember/object';
import {run} from '@ember/runloop';
// URI is attached to the window global as part of the
// google-caja html-css-sanitizer-bundle
const {URI} = window;
let joinUrlParts = function (url, path) {
if (path[0] !== '/' && url.slice(-1) !== '/') {
path = `/${path}`;
} else if (path[0] === '/' && url.slice(-1) === '/') {
path = path.slice(1);
}
return url + path;
};
let isRelative = function (url) {
// "protocol://", "//example.com", "scheme:", "#anchor", & invalid paths
// should all be treated as absolute
return !url.match(/\s/) && !validator.isURL(url) && !url.match(/^(\/\/|#|[a-zA-Z0-9-]+:)/);
};
@classic
@classNames('gh-input')
export default class GhNavitemUrlInput extends TextField {
// Allowed actions
update() {}
clearErrors() {}
@computed('baseUrl', 'value')
get isBaseUrl() {
return this.baseUrl === this.value;
}
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
// value coming is likely to be relative but we always want to show
// absolute urls in the input fields
this.set('value', this._makeAbsoluteUrl(this.url));
}
focusIn(event) {
this.set('hasFocus', true);
if (this.isBaseUrl) {
// position the cursor at the end of the input
run.next(function (el) {
let {length} = el.value;
el.setSelectionRange(length, length);
}, event.target);
}
}
keyDown(event) {
// delete the "placeholder" value all at once
if (this.isBaseUrl && (event.keyCode === 8 || event.keyCode === 46)) {
this.set('value', '');
event.preventDefault();
}
// CMD-S
if (event.keyCode === 83 && event.metaKey) {
this.notifyUrlChanged();
}
}
keyPress(event) {
this.clearErrors();
// enter key
if (event.keyCode === 13) {
this.notifyUrlChanged();
}
return true;
}
focusOut() {
this.set('hasFocus', false);
this.notifyUrlChanged();
}
notifyUrlChanged() {
let url = this.value.trim();
let urlURI = URI.parse(url);
let baseUrl = this.baseUrl;
let baseURI = URI.parse(baseUrl);
function getHost(uri) {
let host = uri.getDomain();
if (uri.getPort()) {
host = `${host}:${uri.getPort()}`;
}
return host;
}
let urlHost = getHost(urlURI);
let baseHost = getHost(baseURI);
// ensure value property is trimmed
this.set('value', url);
// if we have an email address, add the mailto:
if (validator.isEmail(url)) {
url = this.update(`mailto:${url}`);
this.set('value', url);
return;
}
// get our baseUrl relativity checks in order
let isAnchorLink = url.match(/^#/);
let isRelativeToBasePath = urlURI.getPath() && urlURI.getPath().indexOf(baseURI.getPath()) === 0;
// if our path is only missing a trailing / mark it as relative
if (`${urlURI.getPath()}/` === baseURI.getPath()) {
isRelativeToBasePath = true;
}
let isOnSameHost = urlHost === baseHost || (!urlHost && isRelativeToBasePath);
// if relative to baseUrl, remove the base url before sending to action
if (!isAnchorLink && isOnSameHost && isRelativeToBasePath) {
url = url.replace(/^[a-zA-Z0-9-]+:/, '');
url = url.replace(/^\/\//, '');
url = url.replace(baseHost, '');
url = url.replace(baseURI.getPath(), '');
// handle case where url path is same as baseUrl path but missing trailing slash
if (urlURI.getPath().slice(-1) !== '/') {
url = url.replace(baseURI.getPath().slice(0, -1), '');
}
if (url !== '' || !this.isNew) {
if (!url.match(/^\//)) {
url = `/${url}`;
}
if (!url.match(/\/$/) && !url.match(/[.#?]/)) {
url = `${url}/`;
}
}
}
// we update with the relative URL but then transform it back to absolute
// for the input value. This avoids problems where the underlying relative
// value hasn't changed even though the input value has
if (url.match(/^(\/\/|#|[a-zA-Z0-9-]+:)/) || validator.isURL(url) || validator.isURL(`${baseHost}${url}`)) {
url = this.update(url);
this.set('value', this._makeAbsoluteUrl(url));
}
}
_makeAbsoluteUrl(url) {
if (isRelative(url)) {
url = joinUrlParts(this.baseUrl, url);
}
return url;
}
}