diff --git a/gulpfile.js b/gulpfile.js index 439618739..e60dffc70 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,6 +21,6 @@ elixir.extend('queryVersion', function(inputFiles) { elixir(function(mix) { mix.sass('styles.scss') .sass('print-styles.scss') - .browserify(['jquery-extensions.js', 'global.js'], 'public/js/common.js') + .browserify('global.js', 'public/js/common.js') .queryVersion(['css/styles.css', 'css/print-styles.css', 'js/common.js']); }); diff --git a/package.json b/package.json index acbb6f8fa..a1fb06b1c 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,17 @@ { "private": true, "devDependencies": { - "gulp": "^3.9.0", - "insert-css": "^0.2.0" + "gulp": "^3.9.0" }, "dependencies": { "angular": "^1.5.0-rc.0", + "angular-animate": "^1.5.0-rc.0", "angular-resource": "^1.5.0-rc.0", + "angular-sanitize": "^1.5.0-rc.0", "babel-runtime": "^5.8.29", "bootstrap-sass": "^3.0.0", "dropzone": "^4.0.1", "laravel-elixir": "^3.4.0", - "vue": "^1.0.13", - "vue-hot-reload-api": "^1.2.1", - "vue-resource": "^0.5.1", - "vueify": "^5.0.1", - "vueify-insert-css": "^1.0.0", "zeroclipboard": "^2.2.0" } } diff --git a/resources/assets/js/components/drop-zone.html b/resources/assets/js/components/drop-zone.html new file mode 100644 index 000000000..26e0ee2aa --- /dev/null +++ b/resources/assets/js/components/drop-zone.html @@ -0,0 +1,3 @@ +
+
Drop files or click here to upload
+
\ No newline at end of file diff --git a/resources/assets/js/components/image-manager.vue b/resources/assets/js/components/image-manager.vue deleted file mode 100644 index 757480606..000000000 --- a/resources/assets/js/components/image-manager.vue +++ /dev/null @@ -1,209 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/assets/js/components/image-picker.html b/resources/assets/js/components/image-picker.html new file mode 100644 index 000000000..1a07b9274 --- /dev/null +++ b/resources/assets/js/components/image-picker.html @@ -0,0 +1,15 @@ + +
+
+ Image Preview + Image Preview +
+ +
+ + + | + + + +
\ No newline at end of file diff --git a/resources/assets/js/components/image-picker.vue b/resources/assets/js/components/image-picker.vue deleted file mode 100644 index e3c564eb9..000000000 --- a/resources/assets/js/components/image-picker.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - - \ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js new file mode 100644 index 000000000..5d2d4bbca --- /dev/null +++ b/resources/assets/js/controllers.js @@ -0,0 +1,162 @@ +"use strict"; + +module.exports = function(ngApp) { + + ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout','imageManagerService', + function($scope, $attrs, $http, $timeout, imageManagerService) { + $scope.images = []; + $scope.imageType = $attrs.imageType; + $scope.selectedImage = false; + $scope.dependantPages = false; + $scope.showing = false; + $scope.hasMore = false; + $scope.imageUpdateSuccess = false; + $scope.imageDeleteSuccess = false; + var page = 0; + var previousClickTime = 0; + var dataLoaded = false; + var callback = false; + + $scope.getUploadUrl = function() { + return '/images/' + $scope.imageType + '/upload'; + }; + + $scope.uploadSuccess = function(file, data) { + $scope.$apply(() => { + $scope.images.unshift(data); + }); + }; + + function callbackAndHide(returnData) { + if (callback) callback(returnData); + $scope.showing = false; + } + + $scope.imageSelect = function (image) { + var dblClickTime = 300; + var currentTime = Date.now(); + var timeDiff = currentTime - previousClickTime; + + if (timeDiff < dblClickTime) { + // If double click + callbackAndHide(image); + } else { + // If single + $scope.selectedImage = image; + $scope.dependantPages = false; + } + previousClickTime = currentTime; + }; + + $scope.selectButtonClick = function() { + callbackAndHide($scope.selectedImage); + }; + + function show(doneCallback) { + callback = doneCallback; + $scope.showing = true; + // Get initial images if they have not yet been loaded in. + if (!dataLoaded) { + fetchData(); + dataLoaded = true; + } + } + + imageManagerService.show = show; + imageManagerService.showExternal = function(doneCallback) { + $scope.$apply(() => { + show(doneCallback); + }); + }; + window.ImageManager = imageManagerService; + + $scope.hide = function() { + $scope.showing = false; + }; + + function fetchData() { + var url = '/images/' + $scope.imageType + '/all/' + page; + $http.get(url).then((response) => { + $scope.images = $scope.images.concat(response.data.images); + $scope.hasMore = response.data.hasMore; + page++; + }); + } + + $scope.saveImageDetails = function(event) { + event.preventDefault(); + var url = '/images/update/' + $scope.selectedImage.id; + $http.put(url, this.selectedImage).then((response) => { + $scope.imageUpdateSuccess = true; + $timeout(() => { + $scope.imageUpdateSuccess = false; + }, 3000); + }, (response) => { + var errors = response.data; + var message = ''; + Object.keys(errors).forEach((key) => { + message += errors[key].join('\n'); + }); + $scope.imageUpdateFailure = message; + $timeout(() => { + $scope.imageUpdateFailure = false; + }, 5000); + }); + }; + + $scope.deleteImage = function(event) { + event.preventDefault(); + var force = $scope.dependantPages !== false; + var url = '/images/' + $scope.selectedImage.id; + if (force) url += '?force=true'; + $http.delete(url).then((response) => { + $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); + $scope.selectedImage = false; + $scope.imageDeleteSuccess = true; + $timeout(() => { + $scope.imageDeleteSuccess = false; + }, 3000); + }, (response) => { + // Pages failure + if (response.status === 400) { + $scope.dependantPages = response.data; + } + }); + }; + + }]); + + + ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', function($scope, $http, $attrs) { + $scope.searching = false; + $scope.searchTerm = ''; + $scope.searchResults = ''; + + $scope.searchBook = function (e) { + e.preventDefault(); + var term = $scope.searchTerm; + if (term.length == 0) return; + $scope.searching = true; + $scope.searchResults = ''; + var searchUrl = '/search/book/' + $attrs.bookId; + searchUrl += '?term=' + encodeURIComponent(term); + $http.get(searchUrl).then((response) => { + $scope.searchResults = response.data; + }); + }; + + $scope.checkSearchForm = function () { + if ($scope.searchTerm.length < 1) { + $scope.searching = false; + } + }; + + $scope.clearSearch = function() { + $scope.searching = false; + $scope.searchTerm = ''; + }; + + }]); + + +}; \ No newline at end of file diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index a5f45b589..4e936388b 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,11 +1,15 @@ +"use strict"; +var DropZone = require('dropzone'); var toggleSwitchTemplate = require('./components/toggle-switch.html'); +var imagePickerTemplate = require('./components/image-picker.html'); +var dropZoneTemplate = require('./components/drop-zone.html'); module.exports = function(ngApp) { /** * Toggle Switches - * Have basic on/off functionality. + * Has basic on/off functionality. * Use string values of 'true' & 'false' to dictate the current state. */ ngApp.directive('toggleSwitch', function() { @@ -29,4 +33,127 @@ module.exports = function(ngApp) { }); + /** + * Image Picker + * Is a simple front-end interface that connects to an ImageManager if present. + */ + ngApp.directive('imagePicker', ['$http', 'imageManagerService', function($http, imageManagerService) { + return { + restrict: 'E', + template: imagePickerTemplate, + scope: { + name: '@', + resizeHeight: '@', + resizeWidth: '@', + resizeCrop: '@', + showRemove: '=', + currentImage: '@', + currentId: '@', + defaultImage: '@', + imageClass: '@' + }, + link: function(scope, element, attrs) { + var usingIds = typeof scope.currentId !== 'undefined' || scope.currentId === 'false'; + scope.image = scope.currentImage; + scope.value = scope.currentImage || ''; + + function setImage(imageModel, imageUrl) { + scope.image = imageUrl; + scope.value = usingIds ? imageModel.id : imageUrl; + } + + scope.reset = function() { + setImage({id: 0}, scope.defaultImage); + }; + + scope.remove = function() { + scope.image = 'none'; + scope.value = 'none'; + }; + + scope.showImageManager = function() { + imageManagerService.show((image) => { + scope.updateImageFromModel(image); + }); + }; + + scope.updateImageFromModel = function(model) { + var isResized = scope.resizeWidth && scope.resizeHeight; + + if (!isResized) { + scope.$apply(() => { + setImage(model, model.url); + }); + return; + } + + var cropped = scope.resizeCrop ? 'true' : 'false'; + var requestString = '/images/thumb/' + model.id + '/' + scope.resizeWidth + '/' + scope.resizeHeight + '/' + cropped; + $http.get(requestString).then((response) => { + setImage(model, response.data.url); + }); + }; + + } + }; + }]); + + /** + * DropZone + * Used for uploading images + */ + ngApp.directive('dropZone', [function() { + return { + restrict: 'E', + template: dropZoneTemplate, + scope: { + uploadUrl: '@', + eventSuccess: '=', + eventError: '=' + }, + link: function(scope, element, attrs) { + var dropZone = new DropZone(element[0].querySelector('.dropzone-container'), { + url: scope.uploadUrl, + init: function() { + var dz = this; + dz.on('sending', function(file, xhr, data) { + var token = window.document.querySelector('meta[name=token]').getAttribute('content'); + data.append('_token', token); + }); + if (typeof scope.eventSuccess !== 'undefined') dz.on('success', scope.eventSuccess); + dz.on('success', function(file, data) { + $(file.previewElement).fadeOut(400, function () { + dz.removeFile(file); + }); + }); + if (typeof scope.eventError !== 'undefined') dz.on('error', scope.eventError); + dz.on('error', function (file, errorMessage, xhr) { + if (errorMessage.file) { + $(file.previewElement).find('[data-dz-errormessage]').text(errorMessage.file[0]); + } + }); + } + }); + } + }; + }]); + + + ngApp.directive('dropdown', [function() { + return { + restrict: 'A', + link: function(scope, element, attrs) { + var menu = element.find('ul'); + element.find('[dropdown-toggle]').on('click', function() { + menu.show().addClass('anim menuIn'); + element.mouseleave(function() { + menu.hide(); + menu.removeClass('anim menuIn'); + }); + }); + } + }; + }]); + + }; \ No newline at end of file diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index 1dfc2b6bb..e740654fd 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -6,9 +6,32 @@ window.ZeroClipboard.config({ // AngularJS - Create application and load components var angular = require('angular'); -var angularResource = require('angular-resource'); -var app = angular.module('bookStack', ['ngResource']); -var directives = require('./directives')(app); +var ngResource = require('angular-resource'); +var ngAnimate = require('angular-animate'); +var ngSanitize = require('angular-sanitize'); + +var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']); +var services = require('./services')(ngApp); +var directives = require('./directives')(ngApp); +var controllers = require('./controllers')(ngApp); + +//Global jQuery Config & Extensions + +// Smooth scrolling +jQuery.fn.smoothScrollTo = function() { + if(this.length === 0) return; + $('body').animate({ + scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin + }, 800); // Adjust to change animations speed (ms) + return this; +}; + +// Making contains text expression not worry about casing +$.expr[":"].contains = $.expr.createPseudo(function(arg) { + return function( elem ) { + return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; + }; +}); // Global jQuery Elements $(function () { @@ -18,9 +41,6 @@ $(function () { $(this).fadeOut(100); }); - // Dropdown toggles - $('[data-dropdown]').dropDown(); - // Chapter page list toggles $('.chapter-toggle').click(function(e) { e.preventDefault(); @@ -30,6 +50,11 @@ $(function () { }); + +function elemExists(selector) { + return document.querySelector(selector) !== null; +} + // TinyMCE editor if(elemExists('#html-editor')) { var tinyMceOptions = require('./pages/page-form'); diff --git a/resources/assets/js/jquery-extensions.js b/resources/assets/js/jquery-extensions.js deleted file mode 100644 index c41a721e4..000000000 --- a/resources/assets/js/jquery-extensions.js +++ /dev/null @@ -1,66 +0,0 @@ - -// Smooth scrolling -jQuery.fn.smoothScrollTo = function() { - if(this.length === 0) return; - $('body').animate({ - scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin - }, 800); // Adjust to change animations speed (ms) - return this; -}; - -// Making contains text expression not worry about casing -$.expr[":"].contains = $.expr.createPseudo(function(arg) { - return function( elem ) { - return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; - }; -}); - -// Show a success message after the element it's called upon. -jQuery.fn.showSuccess = function (message) { - var elem = $(this); - var success = $(''); - elem.after(success); - success.slideDown(400, function () { - setTimeout(function () { - success.slideUp(400, function () { - success.remove(); - }) - }, 2000); - }); -}; - -// Show a failure messages from laravel. Searches for the name of the inputs. -jQuery.fn.showFailure = function (messageMap) { - var elem = $(this); - $.each(messageMap, function (key, messages) { - var input = elem.find('[name="' + key + '"]').last(); - var fail = $(''); - input.after(fail); - fail.slideDown(400, function () { - setTimeout(function () { - fail.slideUp(400, function () { - fail.remove(); - }) - }, 2000); - }); - }); - -}; - -// Submit the form that the called upon element sits in. -jQuery.fn.submitForm = function() { - $(this).closest('form').submit(); -}; - -// Dropdown menu display -jQuery.fn.dropDown = function() { - var container = $(this), - menu = container.find('ul'); - container.find('[data-dropdown-toggle]').on('click', function() { - menu.show().addClass('anim menuIn'); - container.mouseleave(function() { - menu.hide(); - menu.removeClass('anim menuIn'); - }); - }); -}; \ No newline at end of file diff --git a/resources/assets/js/pages/book-show.js b/resources/assets/js/pages/book-show.js deleted file mode 100644 index 45159ad02..000000000 --- a/resources/assets/js/pages/book-show.js +++ /dev/null @@ -1,32 +0,0 @@ - -module.exports = { - el: '#book-dashboard', - data: { - searching: false, - searchTerm: '', - searchResults: '' - }, - methods: { - searchBook: function (e) { - e.preventDefault(); - var term = this.searchTerm; - if (term.length == 0) return; - this.searching = true; - this.searchResults = ''; - var searchUrl = this.$els.form.getAttribute('action'); - searchUrl += '?term=' + encodeURIComponent(term); - this.$http.get(searchUrl, function (data) { - this.$set('searchResults', data); - }); - }, - checkSearchForm: function (e) { - if (this.searchTerm.length < 1) { - this.searching = false; - } - }, - clearSearch: function(e) { - this.searching = false; - this.searchTerm = ''; - } - } -}; \ No newline at end of file diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 2bf722196..cadccb59c 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -98,7 +98,7 @@ module.exports = { icon: 'image', tooltip: 'Insert an image', onclick: function() { - ImageManager.show(function(image) { + window.ImageManager.showExternal(function(image) { var html = ''; html += ''+image.name+''; html += ''; @@ -106,6 +106,7 @@ module.exports = { }); } }); + // Paste image-uploads editor.on('paste', function(e) { if(e.clipboardData) { diff --git a/resources/assets/js/services.js b/resources/assets/js/services.js new file mode 100644 index 000000000..684a68450 --- /dev/null +++ b/resources/assets/js/services.js @@ -0,0 +1,12 @@ +"use strict"; + +module.exports = function(ngApp) { + + ngApp.factory('imageManagerService', function() { + return { + show: false, + showExternal: false + }; + }); + +}; \ No newline at end of file diff --git a/resources/assets/sass/_image-manager.scss b/resources/assets/sass/_image-manager.scss index 954e10ac0..3b3ce7d9c 100644 --- a/resources/assets/sass/_image-manager.scss +++ b/resources/assets/sass/_image-manager.scss @@ -1,7 +1,6 @@ .overlay { background-color: rgba(0, 0, 0, 0.2); position: fixed; - display: none; z-index: 95536; width: 100%; height: 100%; diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss index c048aa9fe..710bee7ee 100644 --- a/resources/assets/sass/styles.scss +++ b/resources/assets/sass/styles.scss @@ -18,6 +18,10 @@ [v-cloak], [v-show] {display: none;} +[ng\:cloak], [ng-cloak], .ng-cloak { + display: none !important; +} + // Jquery Sortable Styles .dragged { position: absolute; diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php index e8958f629..8a2e6bc98 100644 --- a/resources/views/base.blade.php +++ b/resources/views/base.blade.php @@ -52,8 +52,8 @@ @endif @if($signedIn) -