Migrated entity selector out of angular
This commit is contained in:
parent
cfc05c1b22
commit
baea92b206
10 changed files with 198 additions and 161 deletions
47
resources/assets/js/components/entity-selector-popup.js
Normal file
47
resources/assets/js/components/entity-selector-popup.js
Normal file
|
@ -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;
|
113
resources/assets/js/components/entity-selector.js
Normal file
113
resources/assets/js/components/entity-selector.js
Normal file
|
@ -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;
|
|
@ -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 = {};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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',
|
||||
|
|
20
resources/assets/js/dom-polyfills.js
Normal file
20
resources/assets/js/dom-polyfills.js
Normal file
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div id="entity-selector-wrap">
|
||||
<div overlay entity-link-selector>
|
||||
<div overlay entity-selector-popup>
|
||||
<div class="popup-body small flex-child">
|
||||
<div class="popup-header primary-background">
|
||||
<div class="popup-title">{{ trans('entities.entity_select') }}</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<div class="form-group">
|
||||
<div entity-selector class="entity-selector {{$selectorSize or ''}}" entity-types="{{ $entityTypes or 'book,chapter,page' }}">
|
||||
<input type="hidden" entity-selector-input name="{{$name}}" value="">
|
||||
<input type="text" placeholder="{{ trans('common.search') }}" ng-model="search" ng-model-options="{debounce: 200}" ng-change="searchEntities()">
|
||||
<div class="text-center loading" ng-show="loading">@include('partials.loading-icon')</div>
|
||||
<div ng-show="!loading" ng-bind-html="entityResults"></div>
|
||||
<input type="text" placeholder="{{ trans('common.search') }}" entity-selector-search>
|
||||
<div class="text-center loading" entity-selector-loading>@include('partials.loading-icon')</div>
|
||||
<div entity-selector-results></div>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue