Skip to content

Commit

Permalink
v5.2.0
Browse files Browse the repository at this point in the history
- refactor(ShareButtonsService): Initialize meta tags from document head 'og' meta tags.
- refactor(ShareButtonsService): Add `url` as a meta tag property to the global config.
- refactor(ShareDirective): Initialize meta tags inputs from the global config.
- refactor(Props): Add url as meta tag property.
- refactor(Operators): Removes Pinterest operator since description and image meta tags are being set from the service.
- refactor(Operators): Removes None operator since url became part of the meta tags.
- refactor(Operators): Changes `EmailOperator` to `urlInMessageOperator`
- refactor(Utils): Move `getOS()` and `getValidUrl` to utils file and remove encodeUriComponent.
- refactor(Utils): Do not encode the url in `getValidUrl` function since the url becomes a meta tag.
- fix(WhatsApp): Adds share URL to message body, closes #239
  • Loading branch information
MurhafSousli committed Mar 27, 2018
1 parent 5aecd67 commit 83a0ff9
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 212 deletions.
161 changes: 60 additions & 101 deletions lib/core/src/share-button.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
Renderer2,
ChangeDetectorRef,
PLATFORM_ID,
Inject
Inject,
OnChanges,
SimpleChanges
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http';
Expand All @@ -22,6 +24,7 @@ import { of } from 'rxjs/observable/of';

import { ShareButtons } from './share.service';
import { IShareButton, ShareButtonRef } from './share.models';
import { getOS, getValidUrl } from './utils';

/** Google analytics ref */
declare const ga: Function;
Expand All @@ -30,77 +33,37 @@ declare const window: any;
@Directive({
selector: '[shareButton]'
})
export class ShareButtonDirective {
export class ShareButtonDirective implements OnChanges {

/** A ref for window object that works on SSR */
window: any;
window: Window;

/** A ref for navigator object that works on SSR */
navigator: Navigator;

/** Button properties */
prop: IShareButton;

/** The validated share URL */
url: string;

/** Button class - used to remove previous class when the button type is changed */
/** A ref to button class - used to remove previous class when the button type is changed */
buttonClass: string;

/** Meta tags inputs - initialized from the global options */
@Input() sbTitle = this.shareService.title;
@Input() sbDescription = this.shareService.description;
@Input() sbImage = this.shareService.image;
@Input() sbTags = this.shareService.tags;

/** Create share button */
@Input('shareButton')
set setButton(buttonName: string) {

/** Create a new button of type <buttonName> */
const button = {...this.shareService.prop[buttonName]};

if (button) {

// Set share button
this.prop = button;
/** Share button type */
@Input() shareButton: string;

// Remove previous button class
this.renderer.removeClass(this.el.nativeElement, `sb-${this.buttonClass}`);

// Add new button class
this.renderer.addClass(this.el.nativeElement, `sb-${button.type}`);

// Set button css color variable
this.el.nativeElement.style.setProperty(`--${this.prop.type}-color`, this.prop.color);

// Keep a copy of the class for future replacement
this.buttonClass = button.type;

this.getShareCount();
} else {
throw new Error(`[ShareButtons]: The share button '${buttonName}' does not exist!`);
}
}

/** Set share URL */
@Input()
set sbUrl(newUrl: string) {

/** Check if new URL is equal the current URL */
if (newUrl !== this.url) {
this.url = this.getValidURL(newUrl);
this.getShareCount();
}
}
/** Meta tags inputs - initialized from the global options */
@Input() sbUrl: string;
@Input() sbTitle: string;
@Input() sbDescription: string;
@Input() sbImage: string;
@Input() sbTags: string;

/** Share count event */
/** Stream that emits when share count is fetched */
@Output() sbCount = new EventEmitter<number>();

/** Share dialog opened event */
/** Stream that emits when share dialog is opened */
@Output() sbOpened = new EventEmitter<string>();

/** Share dialog closed event */
/** Stream that emits when share dialog is closed */
@Output() sbClosed = new EventEmitter<string>();

constructor(private shareService: ShareButtons,
Expand All @@ -115,45 +78,44 @@ export class ShareButtonDirective {
}
}

/**
* Share link on element click
*/
/** Share link on element click */
@HostListener('click')
onClick() {
/** Set user did not set the url using [sbUrl], use window URL */
if (!this.url) {
this.url = encodeURIComponent(this.window.location.href);
}

const ref: ShareButtonRef = {
url: this.url,
cd: this.cd,
renderer: this.renderer,
window: this.window,
prop: this.prop,
el: this.el.nativeElement,
os: this.getOS(),
os: getOS(this.window, this.navigator),
metaTags: {
title: this.sbTitle,
description: this.sbDescription,
image: this.sbImage,
url: this.sbUrl,
title: this.sbTitle || this.shareService.title,
description: this.sbDescription || this.shareService.description,
image: this.sbImage || this.shareService.image,
tags: this.sbTags,
via: this.shareService.twitterAccount,
}
};

/** Share the link */
// Share the link
of(ref).pipe(
...this.prop.share.operators,
tap((sharerURL: string) => this.share(sharerURL)),
take(1)
).subscribe();
}

getShareCount() {
// if count output has observers, emit the share count */
if (this.url && this.sbCount.observers.length && this.prop.count) {
this.count(this.url).subscribe((count: number) => this.sbCount.emit(count));
ngOnChanges(changes: SimpleChanges) {

if (changes['shareButton'].firstChange || changes['shareButton'].previousValue !== this.shareButton) {
this.createShareButton(this.shareButton);
}

if (changes['sbUrl'] && (changes['sbUrl'].firstChange || changes['url'].previousValue !== this.sbUrl)) {
this.sbUrl = getValidUrl(this.sbUrl || this.shareService.url, this.window.location.href);
this.getShareCount(this.sbUrl);
}
}

Expand All @@ -166,7 +128,7 @@ export class ShareButtonDirective {

// GA Tracking
if (this.shareService.gaTracking && typeof ga !== 'undefined') {
ga('send', 'social', this.prop.type, 'click', this.url);
ga('send', 'social', this.prop.type, 'click', this.sbUrl);
}

// Emit when share dialog is opened
Expand Down Expand Up @@ -208,41 +170,38 @@ export class ShareButtonDirective {
}
}

/**
* Get a valid URL for sharing
* @param url - URL to validate
* @returns Sharable URL
*/
private getValidURL(url: string) {

if (url) {
const r = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/;
private createShareButton(buttonsName: string) {
const button = {...this.shareService.prop[buttonsName]};

if (r.test(url)) {
return encodeURIComponent(url);
}
console.warn(`[ShareButtons]: Sharing link '${url}' is invalid!`);
}
// fallback to page current URL
return encodeURIComponent(this.window.location.href);
}
if (button) {

/**
* Detect operating system.
* returns 'ios', 'android', or 'desktop'.
*/
private getOS() {
const userAgent = this.navigator.userAgent || this.navigator.vendor || this.window.opera;
// Set share button properties
this.prop = button;

if (/android/i.test(userAgent)) {
return 'android';
// Remove previous button class
this.renderer.removeClass(this.el.nativeElement, `sb-${this.buttonClass}`);

// Add new button class
this.renderer.addClass(this.el.nativeElement, `sb-${button.type}`);

// Set button css color variable
this.el.nativeElement.style.setProperty(`--${this.prop.type}-color`, this.prop.color);

// Keep a copy of the class for future replacement
this.buttonClass = button.type;

this.getShareCount(this.sbUrl);
} else {
throw new Error(`[ShareButtons]: The share button '${buttonsName}' does not exist!`);
}
}

// iOS detection from: http://stackoverflow.com/a/9039885/177710
if (/iPad|iPhone|iPod/.test(userAgent) && !this.window.MSStream) {
return 'ios';
private getShareCount(url: string) {
// if url is valid and button supports share count
if (url && this.prop.count && this.sbCount.observers.length) {
this.count(url).subscribe((count: number) => this.sbCount.emit(count));
}
return 'desktop';
}

}
5 changes: 3 additions & 2 deletions lib/core/src/share.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export interface ShareButtonsOptions {
include?: string[];
exclude?: string[];
size?: number;
url?: string;
title?: string;
description?: string;
image?: string;
Expand Down Expand Up @@ -77,7 +78,7 @@ export interface IShareButton {

/**
* Share button directive ref interface
* It has all the needed objects for the share operators
* This ref to be used in the share operators
*/
export interface ShareButtonRef {
prop?: IShareButton;
Expand All @@ -87,8 +88,8 @@ export interface ShareButtonRef {
el?: HTMLElement;
os?: string;
temp?: any;
url?: string;
metaTags: {
url?: string;
title?: string;
description?: string;
image?: string;
Expand Down
77 changes: 24 additions & 53 deletions lib/core/src/share.operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,30 @@ import { ShareButtonRef } from './share.models';
import { copyToClipboard, mergeDeep } from './utils';

/**
* None operator - just return the sharer URL
*/
export const noneOperator = map((ref: ShareButtonRef) => (ref.prop.share[ref.os]) ? ref.prop.share[ref.os] + ref.url : null);

/**
* Meta tags operator - Serialize meta tags in the sharer URL
* Meta tags operator - Serialize meta tags into the sharer URL
*/
export const metaTagsOperator = map((ref: ShareButtonRef) => {

// object contains supported meta tags
const metaTags = ref.prop.share.metaTags;

// object contains meta tags values */
const metaTagsValues = ref.metaTags;

// Social network sharer URL */
const SharerURL = ref.prop.share[ref.os];
if (SharerURL) {

// User share link
let link = ref.url;
// object contains supported meta tags
const metaTags = ref.prop.share.metaTags;

// Set each meta tag with user value
if (metaTags) {
Object.keys(metaTags).map((key) => {
if (metaTagsValues[key]) {
link += `&${metaTags[key]}=${encodeURIComponent(metaTagsValues[key])}`;
}
});
// object contains meta tags values */
const metaTagsValues = ref.metaTags;

let link = '';
// Set each meta tag with user value
if (metaTags) {
link = Object.entries(metaTags).map(([key, metaTag]) =>
metaTagsValues[key] ? `${metaTag}=${encodeURIComponent(metaTagsValues[key])}` : ''
).join('&');
}
return SharerURL + link;
}
return SharerURL + link;
return;
});

/**
Expand All @@ -43,33 +37,7 @@ export const metaTagsOperator = map((ref: ShareButtonRef) => {
export const printOperator = map((ref: ShareButtonRef) => ref.window.print());

/**
* Pinterest operator - Since Pinterest requires the description and image meta tags,
* this function checks if the meta tags are presented, if not it falls back to page meta tags
* This should placed after the metaTagsOperator
*/
export const pinterestOperator = map((url: string) => {
if (!url.includes('&description')) {
/** If user didn't add description, get it from the OG meta tag */
const ogDescription: Element = document.querySelector(`meta[property="og:description"]`);
if (ogDescription) {
url += '&description=' + ogDescription.getAttribute('content');
} else {
console.warn(`[ShareButtons]: You didn't set the description text for Pinterest button`);
}
}
if (!url.includes('&media')) {
const ogImage: Element = document.querySelector(`meta[property="og:image"]`);
if (ogImage) {
url += '&media=' + ogImage.getAttribute('content');
} else {
console.warn(`[ShareButtons]: You didn't set the image URL for Pinterest button`);
}
}
return url;
});

/**
* Copy button operator - to copy link to clipboard
* Copy link to clipboard, used for copy button
*/
export const copyOperators = [
map((ref: ShareButtonRef) => {
Expand All @@ -78,7 +46,7 @@ export const copyOperators = [
ref.renderer.setStyle(ref.el, 'pointer-events', 'none');

ref.temp = {text: ref.prop.text, icon: ref.prop.icon};
const link = decodeURIComponent(ref.url);
const link = decodeURIComponent(ref.metaTags.url);

copyToClipboard(link, ref.os === 'ios')
.then(() => {
Expand All @@ -105,12 +73,15 @@ export const copyOperators = [
})
];

export const emailOperator = map((ref: ShareButtonRef) => {
/**
* Add the share URL to message body, used for WhatsApp and Email buttons
*/
export const urlInMessageOperator = map((ref: ShareButtonRef) => {
const description = ref.metaTags.description;
const url = decodeURIComponent(ref.url);
const url = ref.metaTags.url;
const newRef: ShareButtonRef = {
metaTags: {
description: description ? `${description}\r\n\r\n${url}` : url
description: description ? `${description}\r\n${url}` : url
}
};
return mergeDeep(ref, newRef);
Expand Down
Loading

0 comments on commit 83a0ff9

Please sign in to comment.