diff --git a/resources/assets/js/components/entity-selector-popup.js b/resources/assets/js/components/entity-selector-popup.js new file mode 100644 index 000000000..64c0c62e9 --- /dev/null +++ b/resources/assets/js/components/entity-selector-popup.js @@ -0,0 +1,47 @@ + +class EntitySelectorPopup { + + constructor(elem) { + this.elem = elem; + window.EntitySelectorPopup = this; + + this.callback = null; + this.selection = null; + + this.selectButton = elem.querySelector('.entity-link-selector-confirm'); + this.selectButton.addEventListener('click', this.onSelectButtonClick.bind(this)); + + window.$events.listen('entity-select-change', this.onSelectionChange.bind(this)); + window.$events.listen('entity-select-confirm', this.onSelectionConfirm.bind(this)); + } + + show(callback) { + this.callback = callback; + this.elem.components.overlay.show(); + } + + hide() { + this.elem.components.overlay.hide(); + } + + onSelectButtonClick() { + this.hide(); + if (this.selection !== null && this.callback) this.callback(this.selection); + } + + onSelectionConfirm(entity) { + this.hide(); + if (this.callback && entity) this.callback(entity); + } + + onSelectionChange(entity) { + this.selection = entity; + if (entity === null) { + this.selectButton.setAttribute('disabled', 'true'); + } else { + this.selectButton.removeAttribute('disabled'); + } + } +} + +module.exports = EntitySelectorPopup; \ No newline at end of file diff --git a/resources/assets/js/components/entity-selector.js b/resources/assets/js/components/entity-selector.js new file mode 100644 index 000000000..57b2499cc --- /dev/null +++ b/resources/assets/js/components/entity-selector.js @@ -0,0 +1,113 @@ + +class EntitySelector { + + constructor(elem) { + this.elem = elem; + this.search = ''; + this.lastClick = 0; + + let entityTypes = elem.hasAttribute('entity-types') ? elem.getAttribute('entity-types') : 'page,book,chapter'; + this.searchUrl = window.baseUrl(`/ajax/search/entities?types=${encodeURIComponent(entityTypes)}`); + + this.input = elem.querySelector('[entity-selector-input]'); + this.searchInput = elem.querySelector('[entity-selector-search]'); + this.loading = elem.querySelector('[entity-selector-loading]'); + this.resultsContainer = elem.querySelector('[entity-selector-results]'); + + this.elem.addEventListener('click', this.onClick.bind(this)); + + let lastSearch = 0; + this.searchInput.addEventListener('input', event => { + lastSearch = Date.now(); + this.showLoading(); + setTimeout(() => { + if (Date.now() - lastSearch < 199) return; + this.searchEntities(this.searchInput.value); + }, 200); + }); + this.searchInput.addEventListener('keydown', event => { + if (event.keyCode === 13) event.preventDefault(); + }); + + this.showLoading(); + this.initialLoad(); + } + + showLoading() { + this.loading.style.display = 'block'; + this.resultsContainer.style.display = 'none'; + } + + hideLoading() { + this.loading.style.display = 'none'; + this.resultsContainer.style.display = 'block'; + } + + initialLoad() { + window.$http.get(this.searchUrl).then(resp => { + this.resultsContainer.innerHTML = resp.data; + this.hideLoading(); + }) + } + + searchEntities(searchTerm) { + this.input.value = ''; + let url = this.searchUrl + `&term=${encodeURIComponent(searchTerm)}`; + window.$http.get(url).then(resp => { + this.resultsContainer.innerHTML = resp.data; + this.hideLoading(); + }); + } + + isDoubleClick() { + let now = Date.now(); + let answer = now - this.lastClick < 300; + this.lastClick = now; + return answer; + } + + onClick(event) { + let t = event.target; + + if (t.matches('.entity-list a')) { + event.preventDefault(); + event.stopPropagation(); + let item = t.closest('[data-entity-type]'); + this.selectItem(item); + } else if (t.matches('[data-entity-type]')) { + this.selectItem(t) + } + + } + + selectItem(item) { + let isDblClick = this.isDoubleClick(); + let type = item.getAttribute('data-entity-type'); + let id = item.getAttribute('data-entity-id'); + let isSelected = item.classList.contains('selected') || isDblClick; + + this.unselectAll(); + this.input.value = isSelected ? `${type}:${id}` : ''; + + if (!isSelected) window.$events.emit('entity-select-change', null); + if (!isDblClick && !isSelected) return; + + let link = item.querySelector('.entity-list-item-link').getAttribute('href'); + let name = item.querySelector('.entity-list-item-name').textContent; + let data = {id: Number(id), name: name, link: link}; + + if (isDblClick) window.$events.emit('entity-select-confirm', data); + if (isSelected) window.$events.emit('entity-select-change', data); + } + + unselectAll() { + let selected = this.elem.querySelectorAll('.selected'); + for (let i = 0, len = selected.length; i < len; i++) { + selected[i].classList.remove('selected'); + selected[i].classList.remove('primary-background'); + } + } + +} + +module.exports = EntitySelector; \ No newline at end of file diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js index 43466a0d9..a324ab0c9 100644 --- a/resources/assets/js/components/index.js +++ b/resources/assets/js/components/index.js @@ -6,6 +6,8 @@ let componentMapping = { 'notification': require('./notification'), 'chapter-toggle': require('./chapter-toggle'), 'expand-toggle': require('./expand-toggle'), + 'entity-selector-popup': require('./entity-selector-popup'), + 'entity-selector': require('./entity-selector'), }; window.components = {}; diff --git a/resources/assets/js/components/notification.js b/resources/assets/js/components/notification.js index 1a9819702..daef5bd6f 100644 --- a/resources/assets/js/components/notification.js +++ b/resources/assets/js/components/notification.js @@ -6,7 +6,7 @@ class Notification { this.type = elem.getAttribute('notification'); this.textElem = elem.querySelector('span'); this.autohide = this.elem.hasAttribute('data-autohide'); - window.Events.listen(this.type, text => { + window.$events.listen(this.type, text => { this.show(text); }); elem.addEventListener('click', this.hide.bind(this)); diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index fc92121ff..8813eb881 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -252,7 +252,7 @@ module.exports = function (ngApp, events) { // Show the popup link selector and insert a link when finished function showLinkSelector() { let cursorPos = cm.getCursor('from'); - window.showEntityLinkSelector(entity => { + window.EntitySelectorPopup.show(entity => { let selectedText = cm.getSelection() || entity.name; let newText = `[${selectedText}](${entity.link})`; cm.focus(); @@ -387,154 +387,6 @@ module.exports = function (ngApp, events) { } }]); - ngApp.directive('entityLinkSelector', [function($http) { - return { - restrict: 'A', - link: function(scope, element, attrs) { - - const selectButton = element.find('.entity-link-selector-confirm'); - let callback = false; - let entitySelection = null; - - // Handle entity selection change, Stores the selected entity locally - function entitySelectionChange(entity) { - entitySelection = entity; - if (entity === null) { - selectButton.attr('disabled', 'true'); - } else { - selectButton.removeAttr('disabled'); - } - } - events.listen('entity-select-change', entitySelectionChange); - - // Handle selection confirm button click - selectButton.click(event => { - hide(); - if (entitySelection !== null) callback(entitySelection); - }); - - // Show selector interface - function show() { - element.fadeIn(240); - } - - // Hide selector interface - function hide() { - element.fadeOut(240); - } - scope.hide = hide; - - // Listen to confirmation of entity selections (doubleclick) - events.listen('entity-select-confirm', entity => { - hide(); - callback(entity); - }); - - // Show entity selector, Accessible globally, and store the callback - window.showEntityLinkSelector = function(passedCallback) { - show(); - callback = passedCallback; - }; - - } - }; - }]); - - - ngApp.directive('entitySelector', ['$http', '$sce', function ($http, $sce) { - return { - restrict: 'A', - scope: true, - link: function (scope, element, attrs) { - scope.loading = true; - scope.entityResults = false; - scope.search = ''; - - // Add input for forms - const input = element.find('[entity-selector-input]').first(); - - // Detect double click events - let lastClick = 0; - function isDoubleClick() { - let now = Date.now(); - let answer = now - lastClick < 300; - lastClick = now; - return answer; - } - - // Listen to entity item clicks - element.on('click', '.entity-list a', function(event) { - event.preventDefault(); - event.stopPropagation(); - let item = $(this).closest('[data-entity-type]'); - itemSelect(item, isDoubleClick()); - }); - element.on('click', '[data-entity-type]', function(event) { - itemSelect($(this), isDoubleClick()); - }); - - // Select entity action - function itemSelect(item, doubleClick) { - let entityType = item.attr('data-entity-type'); - let entityId = item.attr('data-entity-id'); - let isSelected = !item.hasClass('selected') || doubleClick; - element.find('.selected').removeClass('selected').removeClass('primary-background'); - if (isSelected) item.addClass('selected').addClass('primary-background'); - let newVal = isSelected ? `${entityType}:${entityId}` : ''; - input.val(newVal); - - if (!isSelected) { - events.emit('entity-select-change', null); - } - - if (!doubleClick && !isSelected) return; - - let link = item.find('.entity-list-item-link').attr('href'); - let name = item.find('.entity-list-item-name').text(); - - if (doubleClick) { - events.emit('entity-select-confirm', { - id: Number(entityId), - name: name, - link: link - }); - } - - if (isSelected) { - events.emit('entity-select-change', { - id: Number(entityId), - name: name, - link: link - }); - } - } - - // Get search url with correct types - function getSearchUrl() { - let types = (attrs.entityTypes) ? encodeURIComponent(attrs.entityTypes) : encodeURIComponent('page,book,chapter'); - return window.baseUrl(`/ajax/search/entities?types=${types}`); - } - - // Get initial contents - $http.get(getSearchUrl()).then(resp => { - scope.entityResults = $sce.trustAsHtml(resp.data); - scope.loading = false; - }); - - // Search when typing - scope.searchEntities = function() { - scope.loading = true; - input.val(''); - let url = getSearchUrl() + '&term=' + encodeURIComponent(scope.search); - $http.get(url).then(resp => { - scope.entityResults = $sce.trustAsHtml(resp.data); - scope.loading = false; - }); - }; - } - }; - }]); - ngApp.directive('commentReply', [function () { return { restrict: 'E', diff --git a/resources/assets/js/dom-polyfills.js b/resources/assets/js/dom-polyfills.js new file mode 100644 index 000000000..fcd89b766 --- /dev/null +++ b/resources/assets/js/dom-polyfills.js @@ -0,0 +1,20 @@ +/** + * Polyfills for DOM API's + */ + +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} + +if (!Element.prototype.closest) { + Element.prototype.closest = function (s) { + var el = this; + var ancestor = this; + if (!document.documentElement.contains(el)) return null; + do { + if (ancestor.matches(s)) return ancestor; + ancestor = ancestor.parentElement; + } while (ancestor !== null); + return null; + }; +} \ No newline at end of file diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js index ee7cf3cc1..85f9f77a6 100644 --- a/resources/assets/js/global.js +++ b/resources/assets/js/global.js @@ -1,5 +1,6 @@ "use strict"; require("babel-polyfill"); +require('./dom-polyfills'); // Url retrieval function window.baseUrl = function(path) { @@ -13,9 +14,11 @@ window.baseUrl = function(path) { class EventManager { constructor() { this.listeners = {}; + this.stack = []; } emit(eventName, eventData) { + this.stack.push({name: eventName, data: eventData}); if (typeof this.listeners[eventName] === 'undefined') return this; let eventsToStart = this.listeners[eventName]; for (let i = 0; i < eventsToStart.length; i++) { @@ -32,7 +35,7 @@ class EventManager { } } -window.Events = new EventManager(); +window.$events = new EventManager(); const Vue = require("vue"); const axios = require("axios"); @@ -47,13 +50,13 @@ axiosInstance.interceptors.request.use(resp => { return resp; }, err => { if (typeof err.response === "undefined" || typeof err.response.data === "undefined") return Promise.reject(err); - if (typeof err.response.data.error !== "undefined") window.Events.emit('error', err.response.data.error); - if (typeof err.response.data.message !== "undefined") window.Events.emit('error', err.response.data.message); + if (typeof err.response.data.error !== "undefined") window.$events.emit('error', err.response.data.error); + if (typeof err.response.data.message !== "undefined") window.$events.emit('error', err.response.data.message); }); window.$http = axiosInstance; Vue.prototype.$http = axiosInstance; -Vue.prototype.$events = window.Events; +Vue.prototype.$events = window.$events; // AngularJS - Create application and load components @@ -78,8 +81,8 @@ require("./components"); // Load in angular specific items const Directives = require('./directives'); const Controllers = require('./controllers'); -Directives(ngApp, window.Events); -Controllers(ngApp, window.Events); +Directives(ngApp, window.$events); +Controllers(ngApp, window.$events); //Global jQuery Config & Extensions diff --git a/resources/assets/js/pages/page-form.js b/resources/assets/js/pages/page-form.js index 08e4c0c34..497dc0212 100644 --- a/resources/assets/js/pages/page-form.js +++ b/resources/assets/js/pages/page-form.js @@ -274,7 +274,7 @@ module.exports = function() { file_browser_callback: function (field_name, url, type, win) { if (type === 'file') { - window.showEntityLinkSelector(function(entity) { + window.EntitySelectorPopup.show(function(entity) { let originalField = win.document.getElementById(field_name); originalField.value = entity.link; $(originalField).closest('.mce-form').find('input').eq(2).val(entity.name); diff --git a/resources/views/components/entity-selector-popup.blade.php b/resources/views/components/entity-selector-popup.blade.php index 39d25bfa6..ecd03c80f 100644 --- a/resources/views/components/entity-selector-popup.blade.php +++ b/resources/views/components/entity-selector-popup.blade.php @@ -1,5 +1,5 @@
-
+