diff --git a/.travis/before_script.sh b/.travis/before_script.sh
index 1d62dad7..a7d85c78 100755
--- a/.travis/before_script.sh
+++ b/.travis/before_script.sh
@@ -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
diff --git a/.travis/secrets.tar.enc b/.travis/secrets.tar.enc
index e24ba777..a192d49a 100644
Binary files a/.travis/secrets.tar.enc and b/.travis/secrets.tar.enc differ
diff --git a/gulpfile.js b/gulpfile.js
index fdd99d4f..229d989e 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -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.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(){
diff --git a/src/js/directives/pad.js b/src/js/directives/pad.js
index 8133d0f1..60b97a38 100644
--- a/src/js/directives/pad.js
+++ b/src/js/directives/pad.js
@@ -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 = `
+
+
+ `;
+ 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 = `
- `;
+ `;
$scope.project.attachments[state].file.getUrl().then(url => {
parentElement.innerHTML = ``;
@@ -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,10 +216,10 @@ 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) {
@@ -119,7 +227,7 @@ angular.module('Teem')
}
$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'
};
});
diff --git a/src/js/services/pad/linkPreview.js b/src/js/services/pad/linkPreview.js
new file mode 100644
index 00000000..1d7a0284
--- /dev/null
+++ b/src/js/services/pad/linkPreview.js
@@ -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);
+})();
\ No newline at end of file
diff --git a/src/sass/pad.sass b/src/sass/pad.sass
index 8d1817e2..c0dc6f39 100644
--- a/src/sass/pad.sass
+++ b/src/sass/pad.sass
@@ -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
diff --git a/src/templates/pad.html b/src/templates/pad.html
index db88fc30..42c50b35 100644
--- a/src/templates/pad.html
+++ b/src/templates/pad.html
@@ -1,6 +1,6 @@
+ ngf-drop="widget('img', $file)" ngf-fix-orientation="true">