Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Link Popovers added #380

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion .travis/before_script.sh
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ fi
if [ $TRAVIS_BRANCH = "master" ] || [ $TRAVIS_BRANCH = "staging" ]; then
echo "Using config.js for branch $TRAVIS_BRANCH"

openssl aes-256-cbc -K $encrypted_f03f2d3a9637_key -iv $encrypted_f03f2d3a9637_iv -in .travis/secrets.tar.enc -out .travis/secrets.tar -d
openssl aes-256-cbc -K $encrypted_249e297d6459_key -iv $encrypted_249e297d6459_iv -in .travis/secrets.tar.enc -out .travis/secrets.tar -d

tar xvf .travis/secrets.tar --directory .travis

Binary file modified .travis/secrets.tar.enc
Binary file not shown.
88 changes: 68 additions & 20 deletions gulpfile.js
Original file line number Diff line number Diff line change
@@ -220,7 +220,16 @@ var gulp = require('gulp'),
spawn = require('child_process').spawn,
gutil = require('gulp-util');


/**
* Logs the error occured in the pipe without killing the gulp process
* emits an end event to the corresponding stream
* @function endErrorProcess
* @param {Error} err
*/
function endErrorProcess(err){
console.log(err);
this.emit('end');
}
/*================================================
= Report Errors to Console =
================================================*/
@@ -244,14 +253,16 @@ gulp.task('clean', function () {
path.join(config.dest, 'l10n'),
path.join(config.dest, 'app.manifest')
], { read: false })
.pipe(rimraf());
.pipe(rimraf())
.on('error', endErrorProcess);
});

gulp.task('clean:manifest', function () {
return gulp.src([
path.join(config.dest, 'app.manifest')
], { read: false })
.pipe(rimraf());
.pipe(rimraf())
.on('error', endErrorProcess);
});


@@ -279,7 +290,8 @@ gulp.task('connect', function() {

gulp.task('livereload', function () {
gulp.src(path.join(config.dest, '*.html'))
.pipe(connect.reload());
.pipe(connect.reload())
.on('error', endErrorProcess);
});


@@ -295,10 +307,12 @@ gulp.task('images', function () {
progressive: true,
svgoPlugins: [{removeViewBox: false}],
use: [pngcrush()]
}));
}))
.on('error', endErrorProcess);
}

return stream.pipe(gulp.dest(path.join(config.dest, 'images')));
return stream.pipe(gulp.dest(path.join(config.dest, 'images')))
.on('error', endErrorProcess);
});


@@ -308,7 +322,8 @@ gulp.task('images', function () {

gulp.task('fonts', function() {
return gulp.src(config.vendor.fonts)
.pipe(gulp.dest(path.join(config.dest, 'fonts')));
.pipe(gulp.dest(path.join(config.dest, 'fonts')))
.on('error', endErrorProcess);
});

/*==================================
@@ -317,7 +332,8 @@ gulp.task('fonts', function() {

gulp.task('l10n', function() {
return gulp.src('src/l10n/**/*')
.pipe(gulp.dest(path.join(config.dest, 'l10n')));
.pipe(gulp.dest(path.join(config.dest, 'l10n')))
.on('error', endErrorProcess);
});


@@ -358,7 +374,9 @@ function buildHtml (env) {

return gulp.src(['src/html/**/*.html'])
.pipe(replace('<!-- inject:js -->', inject.join('\n ')))
.pipe(gulp.dest(config.dest));
.on('error', endErrorProcess)
.pipe(gulp.dest(config.dest))
.on('error', endErrorProcess);
}

gulp.task('html', function() {
@@ -377,10 +395,12 @@ gulp.task('html:production', function() {
gulp.task('sass', function () {
gulp.src('./src/sass/app.sass')
.pipe(sourcemaps.init())
.on('error', endErrorProcess)
.pipe(sass({
includePaths: [ path.resolve(__dirname, 'src/sass'), path.resolve(__dirname, 'bower_components'), path.resolve(__dirname, 'bower_components/bootstrap-sass/assets/stylesheets') ]
}).on('error', sass.logError))
.pipe(postcss([ autoprefixer({ browsers: ['last 2 versions', 'Android >= 4'] }) ]))
.on('error', endErrorProcess)
/* Currently not working with sourcemaps
.pipe(mobilizer('app.css', {
'app.css': {
@@ -394,11 +414,15 @@ gulp.task('sass', function () {
}))
*/
.pipe(gulpif(config.cssmin, cssmin()))
.on('error', endErrorProcess)
.pipe(rename({suffix: '.min'}))
.on('error', endErrorProcess)
.pipe(sourcemaps.write('.', {
sourceMappingURLPrefix: '/css/'
}))
.pipe(gulp.dest(path.join(config.dest, 'css')));
.on('error', endErrorProcess)
.pipe(gulp.dest(path.join(config.dest, 'css')))
.on('error', endErrorProcess);
});

/*====================================================================
@@ -408,7 +432,9 @@ gulp.task('sass', function () {
gulp.task('jshint', function() {
return gulp.src('./src/js/**/*.js')
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
.on('error', endErrorProcess)
.pipe(jshint.reporter('jshint-stylish'))
.on('error', endErrorProcess);
});


@@ -422,44 +448,61 @@ gulp.task('js:app', function() {
return streamqueue({ objectMode: true },
// Vendor: angular, mobile-angular-ui, etc.
gulp.src(config.vendor.js)
.pipe(sourcemaps.init()),
.pipe(sourcemaps.init())
.on('error', endErrorProcess),
// app.js is configured
gulp.src('./src/js/app.js')
.pipe(sourcemaps.init())
.on('error', endErrorProcess)
.pipe(replace('value(\'config\', {}). // inject:app:config',
'value(\'config\', ' + JSON.stringify(config.app) + ').'))
.on('error', endErrorProcess)
.pipe(babel({
presets: ['es2015']
})),
}))
.on('error', endErrorProcess),
// rest of app logic
gulp.src(['./src/js/**/*.js', '!./src/js/app.js', '!./src/js/widgets.js'])
.pipe(sourcemaps.init())
.on('error', endErrorProcess)
.pipe(babel({
presets: ['es2015'],
plugins: ['transform-object-assign']
}))
.pipe(ngFilesort()),
.on('error', endErrorProcess)
.pipe(ngFilesort())
.on('error', endErrorProcess),
// app templates
gulp.src(['src/templates/**/*.html']).pipe(templateCache({ module: 'Teem' }))
.pipe(sourcemaps.init())
.on('error', endErrorProcess)
.pipe(babel({
presets: ['es2015']
}))
.on('error', endErrorProcess)
)
.pipe(concat('app.js'))
.on('error', endErrorProcess)
.pipe(ngAnnotate())
.on('error', endErrorProcess)
.pipe(gulpif(config.uglify, uglify()))
.on('error', endErrorProcess)
.pipe(rename({suffix: '.min'}))
.on('error', endErrorProcess)
.pipe(sourcemaps.write('.', {
sourceMappingURLPrefix: '/js/'
}))
.pipe(gulp.dest(path.join(config.dest, 'js')));
.on('error', endErrorProcess)
.pipe(gulp.dest(path.join(config.dest, 'js')))
.on('error', endErrorProcess);
});

gulp.task('js:widgets', function() {
return gulp.src('./src/js/widgets.js')
.pipe(uglify())
.pipe(gulp.dest(path.join(config.dest, 'js')));
.on('error', endErrorProcess)
.pipe(gulp.dest(path.join(config.dest, 'js')))
.on('error', endErrorProcess);
});


@@ -481,7 +524,8 @@ gulp.task('cordova:sync:clean', function() {

return gulp.src([dest],
{ read: false })
.pipe(rimraf());
.pipe(rimraf())
.on('error', endErrorProcess);


});
@@ -493,7 +537,8 @@ gulp.task('cordova:sync:copy', function() {


return gulp.src([ source + '{cordova.js,cordova_plugins.js,plugins/**/*}'])
.pipe(gulp.dest(dest));
.pipe(gulp.dest(dest))
.on('error', endErrorProcess);
});

gulp.task('cordova:sync', function(cb) {
@@ -503,7 +548,8 @@ gulp.task('cordova:sync', function(cb) {

gulp.task('cordova', function() {
return gulp.src('src/vendor/cordova/**/*')
.pipe(gulp.dest(path.join(config.dest, 'js/cordova')));
.pipe(gulp.dest(path.join(config.dest, 'js/cordova')))
.on('error', endErrorProcess);
});


@@ -530,7 +576,9 @@ function buildManifest (env) {
exclude: 'app.manifest',
hash: true
}))
.pipe(gulp.dest(config.dest));
.on('error', endErrorProcess)
.pipe(gulp.dest(config.dest))
.on('error', endErrorProcess);
}

gulp.task('manifest', function(){
174 changes: 141 additions & 33 deletions src/js/directives/pad.js
Original file line number Diff line number Diff line change
@@ -7,23 +7,23 @@
* # Chat Ctrl
* Show Pad for a given project
*/

let timer;
angular.module('Teem')
.directive('pad', function() {
.directive('pad', function () {
return {
scope: true,
link: function($scope, elem, attrs) {
link: function ($scope, elem, attrs) {
$scope.editingDefault = attrs.editingDefault;
},
controller: [
'SessionSvc', '$rootScope', '$scope', '$route', '$location',
'$timeout', 'SharedState', 'needWidget', '$element',
function(SessionSvc, $rootScope, $scope, $route, $location,
$timeout, SharedState, needWidget, $element) {
'$timeout', 'SharedState', 'needWidget', '$element', 'linkPreview',
function (SessionSvc, $rootScope, $scope, $route, $location,
$timeout, SharedState, needWidget, $element, linkPreview) {

var buttons = ['text_fields', 'format_bold', 'format_italic', 'format_strikethrough',
'format_align_left', 'format_align_center', 'format_align_right',
'format_list_bulleted', 'format_list_numbered'];
'format_align_left', 'format_align_center', 'format_align_right',
'format_list_bulleted', 'format_list_numbered'];

var annotationMap = {
'text_fields': 'paragraph/header=h3',
@@ -39,6 +39,106 @@ angular.module('Teem')

var annotations = {};

function openLinkPopover(event, range) {
timer = $timeout(() => {
event.stopPropagation();
let div = document.createElement('div');
let btn = event.target;
//cannot inject the spinner HTML directly here
let inHTML = `
<style>
.pos-r{
position relative;
margin: 150px 0;
}
</style>
<div class="pos-r">
<div class="spinner-container">
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</div>
`;
linkPreview.getMetaData(btn.href)
.then((meta) => {
if (!meta) {
div.style.display = 'none';
return;
}
let urlImage = meta.image,
urlAuthor = meta.author,
urlTitle = meta.title,
urlDescription = meta.description;
let innerEle = document.createElement('div');
if (urlImage && urlDescription) {
innerEle.innerHTML = document.getElementById('urlImage-and-urlDescription').innerHTML;
innerEle.querySelector('#popoverLinkTitle').innerHTML = urlTitle;
innerEle.querySelector('#popoverLinkImage').src = urlImage;
innerEle.querySelector('#popoverLinkDescription').innerHTML = urlDescription;
innerEle.querySelector('#popoverLinkUrl').innerHTML = btn.href;
inHTML = innerEle.innerHTML;
}
else if (urlDescription && !urlImage) {
div.style.height = '110px';
innerEle.innerHTML = document.getElementById('description-and-no-image').innerHTML;
innerEle.querySelector('#popoverLinkTitle').innerHTML = urlTitle;
innerEle.querySelector('#popoverLinkDescription').innerHTML = urlDescription;
innerEle.querySelector('#popoverLinkUrl').innerHTML = btn.href;
inHTML = innerEle.innerHTML;
}
else {
if (!urlTitle) {
div.style.height = '110px';
innerEle.innerHTML = document.getElementById('no-title-in-url').innerHTML;
innerEle.querySelector('#popoverLinkDescription').innerHTML = urlDescription;
innerEle.querySelector('#popoverLinkUrl').innerHTML = btn.href;
inHTML = innerEle.innerHTML;
}
else {
div.style.height = '110px';
innerEle.innerHTML = document.getElementById('no-title-in-url').innerHTML;
innerEle.querySelector('#popoverLinkTitle').innerHTML = urlTitle;
innerEle.querySelector('#popoverLinkDescription').innerHTML = urlDescription;
innerEle.querySelector('#popoverLinkUrl').innerHTML = btn.href;
inHTML = innerEle.innerHTML;
}
}
div.innerHTML = inHTML;
})
.catch((err) => {
console.log(err);
});
let clientRect = range.node.nextSibling ?
range.node.nextSibling.getBoundingClientRect() :
range.node.parentElement.getBoundingClientRect();
div.innerHTML = inHTML;
div.style.width = '345px';
div.style.height = '300px';
div.style.position = 'absolute';
div.style.border = '1px solid #F0F0F0';
div.style.top = clientRect.top + 35 + 'px';
div.style.left = clientRect.left + 'px';
div.style.zIndex = 3;
div.style.backgroundColor = '#F2F2F2';
div.style.paddingTop = '5px';
div.id = 'popover-container';
document.body.appendChild(div);
}, 700);
}

function closeLinkPopover(delay) {
if (timer) {
$timeout.cancel(timer);
timer = null;
$timeout(() => {
if (document.getElementById('popover-container')) {
document.body.removeChild(document.getElementById('popover-container'));
}
}, delay);
}
}

function imgWidget(parentElement, before, state) {
state = state || before;

@@ -48,13 +148,13 @@ angular.module('Teem')

// cannot use spinner template directly here
parentElement.innerHTML = `
<div class="pos-r">
<div class="spinner-container">
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</div>`;
<div class="pos-r">
<div class="spinner-container">
<svg class="spinner" width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</div>`;

$scope.project.attachments[state].file.getUrl().then(url => {
parentElement.innerHTML = `<img src="${url}">`;
@@ -71,25 +171,33 @@ angular.module('Teem')

$scope.padAnnotations = {
'paragraph/header': {
onAdd: function() {
onAdd: function () {
$scope.pad.outline = this.editor.getAnnotationSet('paragraph/header');
$timeout();
},
onChange: function() {
onChange: function () {
$scope.pad.outline = this.editor.getAnnotationSet('paragraph/header');
$timeout();
},
onRemove: function() {
onRemove: function () {
$scope.pad.outline = this.editor.getAnnotationSet('paragraph/header');
$timeout();
}
},
'link': {
onEvent: function(range, event) {
onEvent: function (range, event) {
if (event.type === 'click') {
event.stopPropagation();
closeLinkPopover(0);
$scope.linkModal.open(range);
}
else if (event.type === 'mouseover') {
openLinkPopover(event, range);
console.log(range);
}
else if (event.type === 'mouseout') {
closeLinkPopover(500);
}
}
}
};
@@ -108,18 +216,18 @@ angular.module('Teem')
$timeout();
}

$scope.padCreate = function(editor) {
$scope.padCreate = function (editor) {

$scope.linkModal = {
add: function(event) {
add: function (event) {
event.stopPropagation();
let range = editor.getSelection();
if (range.text) {
editor.setAnnotation('link', '');
}
$scope.linkModal.open(range);
},
open: function(range) {
open: function (range) {
let annotation = editor.getAnnotationInRange(range, 'link');

$scope.linkModal.range = range;
@@ -135,17 +243,17 @@ angular.module('Teem')
$scope.linkModal.link = annotation ? annotation.value : '';
$scope.linkModal.show = true;

let emptyInput = !range.text ? 'text': 'link';
let emptyInput = !range.text ? 'text' : 'link';
let autofocus = document.querySelector('#link-modal [ng-model="linkModal.' + emptyInput + '"]');
$timeout(() => autofocus && autofocus.focus());
},
change: function() {
change: function () {
let range = editor.setText($scope.linkModal.range, $scope.linkModal.text);
editor.setAnnotationInRange(range, 'link', $scope.linkModal.link);
$scope.linkModal.show = false;
$scope.linkModal.edit = false;
},
clear: function() {
clear: function () {
editor.clearAnnotationInRange($scope.linkModal.range, 'link');
$scope.linkModal.show = false;
$scope.linkModal.edit = false;
@@ -154,13 +262,13 @@ angular.module('Teem')

disableAllButtons();

editor.onSelectionChanged(function(range) {
editor.onSelectionChanged(function (range) {
annotations = range.annotations;
updateAllButtons();
});
};

$scope.padReady = function(editor) {
$scope.padReady = function (editor) {
// FIXME
// SwellRT editor is created with .wave-editor-off
// Should use .wave-editor-on when SwellRT editor callback is available
@@ -172,7 +280,7 @@ angular.module('Teem')

$scope.pad.outline = editor.getAnnotationSet('paragraph/header');

$scope.annotate = function(btn) {
$scope.annotate = function (btn) {
let [key, val] = annotationMap[btn].split('=');
let currentVal = annotations[key];
if (currentVal === val) {
@@ -184,12 +292,12 @@ angular.module('Teem')
editorElement.focus();
};

$scope.clearFormat = function() {
$scope.clearFormat = function () {
editor.clearAnnotation('style');
editorElement.focus();
};

$scope.widget = function(type) {
$scope.widget = function (type) {
if (type === 'need') {
needWidget.add(editor, $scope);
}
@@ -235,9 +343,9 @@ angular.module('Teem')

};

$scope.$watchCollection(function() {
$scope.$watchCollection(function () {
return SessionSvc.status;
}, function(current) {
}, function (current) {
$scope.pad.saving = !current.sync;
});

@@ -248,7 +356,7 @@ angular.module('Teem')
});
};

}],
}],
templateUrl: 'pad.html'
};
});
37 changes: 37 additions & 0 deletions src/js/services/pad/linkPreview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
(function() {
'use strict';


/**
* @module Teem
* @method linkPreview
* @param {String} url
* Returns the parsed meta data of the given link
*/

let linkPreviewFactory = angular.module('Teem');


function linkPreview($http) {
const LINK_PREVIEW_SERVER_URL = 'http://localhost:9090/fetch';
function getMetaData(url){
//TODO: implement a check for the URL to be correct
if(!url){
return;
}
return $http.post(LINK_PREVIEW_SERVER_URL,{url})
.then((res) => {
return res.data;
})
.catch((err) => {
console.log(err);
});
}

return {
getMetaData
};
}
linkPreview.$inject = ['$http'];
linkPreviewFactory.factory('linkPreview', linkPreview);
})();
65 changes: 64 additions & 1 deletion src/sass/pad.sass
Original file line number Diff line number Diff line change
@@ -77,7 +77,7 @@

.project-desktop
.project-pad-editing
margin-top: -61px
margin-top: -61px
.swellrt-placeholder:empty
color: $placeholder-color
font-family: "Lato", sans-serif, "Material Icons"
@@ -183,3 +183,66 @@
font-weight: bold
color: $pad-empty-tip-modal-description
margin-top: 20px

$black: #000
$seashell: #f1f1f1

#popover
width: 330px
height: 270px
margin: 0 5px
border-radius: 6px

div
&.popover-link-image
width: 330px
height: 190px
margin: 5px auto

&.popover-link-description
width: 320px
height: auto
max-height: 50px
margin: 0 auto
overflow: hidden
word-wrap: break-word


.popover-link-title
word-wrap: break-word
text-overflow: ellpsis
overflow: hidden
white-space: nowrap
font-weight: 600
margin-left: 5px

.popover-link-address
color: $black
margin-left: 5px
overflow: hidden
word-wrap: break-word
text-overflow: ellipsis
white-space: nowrap

#popover-container
&:after
content: ""
position: absolute
bottom: -25px
left: 175px
border-style: solid
visibility: hidden
width: 0
z-index: 1

&:before
content: ""
position: absolute
top: -11px
left: -1px
border-style: solid
border-width: 0 10px 10px
border-color: $seashell transparent
display: block
width: 0
z-index: 0
44 changes: 41 additions & 3 deletions src/templates/pad.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div class="pad-wrapper flex-container" ng-class="project.isParticipant() && (isDesktop() || pad.editing) ? 'project-pad-editing' : ''">
<div class="pad no-pointer flex-container" ng-click="editOn()" ui-outer-click="editOff()" ui-outer-click-if="pad.editing && !isDesktop() && !pad.selectingFile"
ngf-drop="widget('img', $file)" ngf-fix-orientation="true">
ngf-drop="widget('img', $file)" ngf-fix-orientation="true">
<div ng-if="project.isParticipant() && (isDesktop() || pad.editing)" class="pad-toolbar" sticky>
<div class="btn-toolbar" role="toolbar" aria-label="Toolbar with button groups">
<button type="button" ng-click="linkModal.add($event)" class="btn btn-raised btn-link btn-xs" uib-tooltip="{{'pad.tooltip.link' | translate}}" tooltip-append-to-body="true">
@@ -42,8 +42,8 @@
<div id="link-modal" class="well bs-component" ng-class="{hidden: !linkModal.show}" ui-outer-click="linkModal.show = false; linkModal.edit = false">
<div ng-if="linkModal.annotation.value && !linkModal.edit">
<a href="{{linkModal.link}}" target="_blank">{{linkModal.link}}</a>
- <a ng-click="linkModal.edit = true; $event.stopPropagation()" translate>pad.link.change</a>
| <a ng-click="linkModal.clear()" translate>pad.link.delete</a>
- <a ng-click="linkModal.edit = true; $event.stopPropagation()" translate>pad.link.change</a>
| <a ng-click="linkModal.clear()" translate>pad.link.delete</a>
</div>
<form class="form-horizontal" ng-submit="linkModal.change()" ng-if="!linkModal.annotation.value || linkModal.edit">
<fieldset>
@@ -66,3 +66,41 @@
</div>

</div>

<div style="display: none;">
<!--If urlImage as well as urlDescription are there then we can have this as our template-->
<div id="urlImage-and-urlDescription">
<div id="popover">
<div class="popover-link-title" id="popoverLinkTitle"></div>
<div class="popover-link-image">
<img src="" alt="" height="180" width="330" id="popoverLinkImage">
</div>
<div class="popover-link-description" id="popoverLinkDescription"></div>
<div class="popover-link-address" id="popoverLinkUrl"></div>
</div>
</div>

<!--If only description is there-->
<div id="description-and-no-image">
<div id="popover">
<div class="popover-link-title"></div>
<div class="popover-link-description"></div>
<div class="popover-link-address"></div>
</div>
</div>
<!--If title of the URL is not there-->
<div id="no-title-in-url">
<div id="popover" align="center">
<div class="popover-link-description">A description of this content is not available accross the page!</div>
<div class="popover-link-address">${btn.href}</div>
</div>
</div>
<!--If not adequate meta data is there-->
<div id="not-adequate-metadata">
<div id="popover" align="center">
<div class="popover-link-title"></div>
<div class="popover-link-description">A description of this content is not available accross the page!</div>
<div class="popover-link-address"></div>
</div>
</div>
</div>
5 changes: 5 additions & 0 deletions swellrt/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -15,6 +15,11 @@ services:
mongo:
image: mongo:latest
restart: always
teem-link-preview:
image: krshubham/teem-link-preview:latest
restart: always
ports:
- "0.0.0.0:9090:9090"