init
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
const body = $('body');
|
||||
|
||||
body.on('change', '[data-acl_picker] select', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const value = target.val();
|
||||
const item = target.closest('.permissions-item');
|
||||
const inputs = item.find('input[type="checkbox"], input[type="radio"]');
|
||||
const hidden = item.find('input[type="hidden"][name]');
|
||||
const wrapper = target.closest('[data-acl_picker_id]');
|
||||
const type = item.data('fieldType');
|
||||
|
||||
if (type === 'access') {
|
||||
inputs.each((index, input) => {
|
||||
input = $(input);
|
||||
const name = input.prop('name');
|
||||
input.prop('name', name.replace(/(.*)(\[[^\]]*\])/, `$1[${value}]`));
|
||||
});
|
||||
} else if (type === 'permissions') {
|
||||
const crudpContainer = item.find('[data-field-name]');
|
||||
inputs.each((index, input) => {
|
||||
input = $(input);
|
||||
const rand = Math.round(Math.random() * 500);
|
||||
const name = crudpContainer.data('fieldName');
|
||||
const id = input.prop('id').split('_').slice(0, -1).join('_') + `_${value}+${rand}`;
|
||||
// const key = input.data('crudpKey');
|
||||
hidden.prop('name', name.replace(/(.*)(\[[^\]]*\])/, `$1[${value}]`));
|
||||
input.prop('id', id);
|
||||
input.next('label').prop('for', id);
|
||||
});
|
||||
}
|
||||
|
||||
wrapper.find('.permissions-item .button.add-item')[!value ? 'addClass' : 'removeClass']('disabled').prop('disabled', !value ? 'disabled' : null);
|
||||
});
|
||||
|
||||
body.on('input', 'input[data-crudp-key]', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const container = target.closest('.crudp-container');
|
||||
const hidden = container.find('input[type="hidden"][name]');
|
||||
const key = target.data('crudpKey');
|
||||
const json = JSON.parse(hidden.val() || '{}');
|
||||
json[key] = target.val();
|
||||
hidden.val(JSON.stringify(json));
|
||||
});
|
||||
|
||||
body.on('click', '[data-acl_picker] .remove-item', (event) => {
|
||||
event.preventDefault();
|
||||
const target = $(event.currentTarget);
|
||||
const container = target.closest('.permissions-item');
|
||||
const wrapper = target.closest('[data-acl_picker_id]');
|
||||
container.remove();
|
||||
|
||||
const empty = wrapper.find('.permissions-item').length === 1;
|
||||
|
||||
// show the initial + button
|
||||
if (empty) {
|
||||
wrapper.find('.permissions-item.empty-list').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
body.on('click', '[data-acl_picker] .add-item', (event) => {
|
||||
event.preventDefault();
|
||||
const target = $(event.currentTarget);
|
||||
const item = target.closest('.permissions-item');
|
||||
const wrapper = target.closest('[data-acl_picker_id]');
|
||||
const ID = wrapper.data('acl_picker_id');
|
||||
const template = document.querySelector(`template[data-id="acl_picker-${ID}"]`);
|
||||
|
||||
const clone = $(template.content.firstElementChild).clone();
|
||||
clone.insertAfter(item);
|
||||
|
||||
// randomize ids
|
||||
clone.find('.switch-toggle input[type="radio"]').each((index, input) => {
|
||||
input = $(input);
|
||||
const id = input.prop('id');
|
||||
const label = input.next('label');
|
||||
const rand = (Date.now().toString(36) + Math.random().toString(36).substr(2, 5)).toLowerCase();
|
||||
|
||||
input.prop('id', `${id}${rand}`);
|
||||
label.prop('for', `${id}${rand}`);
|
||||
});
|
||||
|
||||
// hide the initial + button
|
||||
wrapper.find('.permissions-item.empty-list').addClass('hidden');
|
||||
|
||||
// disable all + buttons until one is selected
|
||||
wrapper.find('.permissions-item .button.add-item').addClass('disabled').prop('disabled', 'disabled');
|
||||
});
|
||||
205
user/plugins/admin/themes/grav/app/forms/fields/array.js
Normal file
205
user/plugins/admin/themes/grav/app/forms/fields/array.js
Normal file
@@ -0,0 +1,205 @@
|
||||
import $ from 'jquery';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
let body = $('body');
|
||||
|
||||
class Template {
|
||||
constructor(container) {
|
||||
this.container = $(container);
|
||||
|
||||
if (this.getName() === undefined) {
|
||||
this.container = this.container.closest('[data-grav-array-name]');
|
||||
}
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.container.data('grav-array-name') || '';
|
||||
}
|
||||
|
||||
getKeyPlaceholder() {
|
||||
return this.container.data('grav-array-keyname') || 'Key';
|
||||
}
|
||||
|
||||
getValuePlaceholder() {
|
||||
return this.container.data('grav-array-valuename') || 'Value';
|
||||
}
|
||||
|
||||
isValueOnly() {
|
||||
return this.container.find('[data-grav-array-mode="value_only"]:first').length || false;
|
||||
}
|
||||
|
||||
isTextArea() {
|
||||
return this.container.data('grav-array-textarea') || false;
|
||||
}
|
||||
|
||||
shouldBeDisabled() {
|
||||
// check for toggleables, if field is toggleable and it's not enabled, render disabled
|
||||
let toggle = this.container.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
|
||||
return toggle.length && toggle.is(':not(:checked)');
|
||||
}
|
||||
|
||||
getNewRow() {
|
||||
let tpl = '';const value = this.isTextArea()
|
||||
? `<textarea ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" name="" placeholder="${this.getValuePlaceholder()}"></textarea>`
|
||||
: `<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" name="" value="" placeholder="${this.getValuePlaceholder()}" />`;
|
||||
|
||||
if (this.isValueOnly()) {
|
||||
tpl += `
|
||||
<div class="form-row array-field-value_only" data-grav-array-type="row">
|
||||
<span data-grav-array-action="sort" class="fa fa-bars"></span>
|
||||
${value}
|
||||
`;
|
||||
} else {
|
||||
tpl += `
|
||||
<div class="form-row" data-grav-array-type="row">
|
||||
<span data-grav-array-action="sort" class="fa fa-bars"></span>
|
||||
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="key" type="text" value="" placeholder="${this.getKeyPlaceholder()}" />
|
||||
${value}
|
||||
`;
|
||||
}
|
||||
|
||||
tpl += `
|
||||
<span data-grav-array-action="rem" class="fa fa-minus"></span>
|
||||
<span data-grav-array-action="add" class="fa fa-plus"></span>
|
||||
</div>`;
|
||||
|
||||
return tpl;
|
||||
}
|
||||
}
|
||||
|
||||
export default class ArrayField {
|
||||
constructor() {
|
||||
body.on('input', '[data-grav-array-type="key"], [data-grav-array-type="value"]', (event) => this.actionInput(event));
|
||||
body.on('click touch', '[data-grav-array-action]:not([data-grav-array-action="sort"])', (event) => this.actionEvent(event));
|
||||
|
||||
this.arrays = $();
|
||||
|
||||
$('[data-grav-field="array"]').each((index, list) => this.addArray(list));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
}
|
||||
|
||||
addArray(list) {
|
||||
list = $(list);
|
||||
|
||||
list.find('[data-grav-array-type="container"]').each((index, container) => {
|
||||
container = $(container);
|
||||
if (container.data('array-sort') || container[0].hasAttribute('data-array-nosort')) { return; }
|
||||
|
||||
container.data('array-sort', new Sortable(container.get(0), {
|
||||
handle: '.fa-bars',
|
||||
animation: 150,
|
||||
onUpdate: () => {
|
||||
const item = container.find('[data-grav-array-type="row"]:first');
|
||||
this._setTemplate(item);
|
||||
const template = item.data('array-template');
|
||||
this.refreshNames(template);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
actionInput(event) {
|
||||
let element = $(event.target);
|
||||
let type = element.data('grav-array-type');
|
||||
|
||||
this._setTemplate(element);
|
||||
|
||||
let template = element.data('array-template');
|
||||
let keyElement = type === 'key' ? element : element.siblings('[data-grav-array-type="key"]:first');
|
||||
let valueElement = type === 'value' ? element : element.siblings('[data-grav-array-type="value"]:first');
|
||||
|
||||
let escaped_name = !template.isValueOnly() ? keyElement.val() : this.getIndexFor(element);
|
||||
escaped_name = escaped_name.toString().replace(/\[/g, '%5B').replace(/]/g, '%5D');
|
||||
let name = `${template.getName()}[${escaped_name}]`;
|
||||
|
||||
if (!template.isValueOnly() && (!keyElement.val() && !valueElement.val())) {
|
||||
valueElement.attr('name', '');
|
||||
} else {
|
||||
// valueElement.attr('name', !valueElement.val() ? template.getName() : name);
|
||||
valueElement.attr('name', name);
|
||||
}
|
||||
|
||||
this.refreshNames(template);
|
||||
}
|
||||
|
||||
actionEvent(event) {
|
||||
event && event.preventDefault();
|
||||
let element = $(event.target);
|
||||
let action = element.data('grav-array-action');
|
||||
let container = element.parents('[data-grav-array-type="container"]');
|
||||
|
||||
this._setTemplate(element);
|
||||
|
||||
this[`${action}Action`](element);
|
||||
|
||||
let siblings = container.find('> div');
|
||||
container[siblings.length > 1 ? 'removeClass' : 'addClass']('one-child');
|
||||
}
|
||||
|
||||
addAction(element) {
|
||||
let template = element.data('array-template');
|
||||
let row = element.closest('[data-grav-array-type="row"]');
|
||||
|
||||
row.after(template.getNewRow());
|
||||
}
|
||||
|
||||
remAction(element) {
|
||||
let template = element.data('array-template');
|
||||
let row = element.closest('[data-grav-array-type="row"]');
|
||||
let isLast = !row.siblings().length;
|
||||
|
||||
if (isLast) {
|
||||
let newRow = $(template.getNewRow());
|
||||
row.after(newRow);
|
||||
newRow.find('[data-grav-array-type="value"]:last').attr('name', template.getName());
|
||||
}
|
||||
|
||||
row.remove();
|
||||
this.refreshNames(template);
|
||||
}
|
||||
|
||||
refreshNames(template) {
|
||||
if (!template.isValueOnly()) { return; }
|
||||
|
||||
let row = template.container.find('> div > [data-grav-array-type="row"]');
|
||||
let inputs = row.find('[name]:not([name=""])');
|
||||
|
||||
inputs.each((index, input) => {
|
||||
input = $(input);
|
||||
const preserved_name = input.closest('[data-grav-array-name]');
|
||||
const name = `${preserved_name.attr('data-grav-array-name')}[${index}]`;
|
||||
input.attr('name', name);
|
||||
});
|
||||
|
||||
if (!inputs.length) {
|
||||
row.find('[data-grav-array-type="value"]').attr('name', template.getName());
|
||||
}
|
||||
}
|
||||
|
||||
getIndexFor(element) {
|
||||
let template = element.data('array-template');
|
||||
let row = element.closest('[data-grav-array-type="row"]');
|
||||
|
||||
return template.container.find(`${template.isValueOnly() ? '> div ' : ''} > [data-grav-array-type="row"]`).index(row);
|
||||
}
|
||||
|
||||
_setTemplate(element) {
|
||||
if (!element.data('array-template')) {
|
||||
element.data('array-template', new Template(element.closest('[data-grav-array-name]')));
|
||||
}
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let arrays = $(target).find('[data-grav-field="array"]');
|
||||
if (!arrays.length) { return; }
|
||||
|
||||
arrays.each((index, list) => {
|
||||
list = $(list);
|
||||
if (!~this.arrays.index(list)) {
|
||||
this.addArray(list);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new ArrayField();
|
||||
311
user/plugins/admin/themes/grav/app/forms/fields/collections.js
Normal file
311
user/plugins/admin/themes/grav/app/forms/fields/collections.js
Normal file
@@ -0,0 +1,311 @@
|
||||
import $ from 'jquery';
|
||||
import Sortable from 'sortablejs';
|
||||
import '../../utils/jquery-utils';
|
||||
|
||||
export default class CollectionsField {
|
||||
constructor() {
|
||||
this.lists = $();
|
||||
|
||||
const body = $('body');
|
||||
$('[data-type="collection"]').each((index, list) => this.addList(list));
|
||||
body.on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
body.on('click', (event) => {
|
||||
const target = $(event.target);
|
||||
if (!(target.is('[data-action="confirm"], [data-action="delete"]') || target.closest('[data-action="confirm"], [data-action="delete"]').length)) {
|
||||
CollectionsField.closeConfirmations();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
addList(list) {
|
||||
list = $(list);
|
||||
this.lists = this.lists.add(list);
|
||||
|
||||
list.on('click', '> .collection-actions [data-action="add"]', (event) => this.addItem(event));
|
||||
list.on('click', '> ul > li > .item-actions [data-action="confirm"]', (event) => this.confirmRemove(event));
|
||||
list.on('click', '> ul > li > .item-actions [data-action="delete"]', (event) => this.removeItem(event));
|
||||
list.on('click', '> ul > li > .item-actions [data-action="collapse"]', (event) => this.collapseItem(event));
|
||||
list.on('click', '> ul > li > .item-actions [data-action="expand"]', (event) => this.expandItem(event));
|
||||
list.on('click', '> .collection-actions [data-action-sort="date"]', (event) => this.sortItems(event));
|
||||
list.on('click', '> .collection-actions [data-action="collapse_all"]', (event) => this.collapseItems(event));
|
||||
list.on('click', '> .collection-actions [data-action="expand_all"]', (event) => this.expandItems(event));
|
||||
list.on('input change', '[data-key-observe]', (event) => this.observeKey(event));
|
||||
|
||||
list.find('[data-collection-holder]').each((index, container) => {
|
||||
container = $(container);
|
||||
if (container.data('collection-sort') || container[0].hasAttribute('data-collection-nosort')) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.data('collection-sort', new Sortable(container.get(0), {
|
||||
forceFallback: false,
|
||||
handle: '.collection-sort',
|
||||
animation: 150,
|
||||
onUpdate: () => this.reindex(container)
|
||||
}));
|
||||
});
|
||||
|
||||
this._updateActionsStateBasedOnMinMax(list);
|
||||
}
|
||||
|
||||
addItem(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let position = button.data('action-add') || 'bottom';
|
||||
let list = $(button.closest('[data-type="collection"]'));
|
||||
let template = $(list.find('> [data-collection-template="new"]').data('collection-template-html'));
|
||||
|
||||
this._updateActionsStateBasedOnMinMax(list);
|
||||
let items = list.closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
let maxItems = list.data('max');
|
||||
if (typeof maxItems !== 'undefined' && items.length >= maxItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
list.find('> [data-collection-holder]')[position === 'top'
|
||||
? 'prepend'
|
||||
: 'append'](template);
|
||||
this.reindex(list);
|
||||
|
||||
items = list.closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
let topAction = list.closest('[data-type="collection"]').find('[data-action-add="top"]');
|
||||
let sortAction = list.closest('[data-type="collection"]').find('[data-action="sort"]');
|
||||
|
||||
if (items.length) {
|
||||
if (topAction.length) {
|
||||
topAction.parent().removeClass('hidden');
|
||||
}
|
||||
if (sortAction.length && items.length > 1) {
|
||||
sortAction.removeClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// refresh toggleables in a list
|
||||
$('[data-grav-field="toggleable"] input[type="checkbox"]').trigger('change');
|
||||
}
|
||||
|
||||
static closeConfirmations() {
|
||||
$('.list-confirm-deletion[data-action="delete"]').addClass('hidden');
|
||||
}
|
||||
|
||||
confirmRemove(event) {
|
||||
|
||||
const button = $(event.currentTarget);
|
||||
const list = $(button.closest('.item-actions'));
|
||||
const action = list.find('.list-confirm-deletion[data-action="delete"]');
|
||||
const isHidden = action.hasClass('hidden');
|
||||
|
||||
CollectionsField.closeConfirmations();
|
||||
action[isHidden ? 'removeClass' : 'addClass']('hidden');
|
||||
}
|
||||
|
||||
removeItem(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let item = button.closest('[data-collection-item]');
|
||||
let list = $(button.closest('[data-type="collection"]'));
|
||||
|
||||
let items = list.closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
let minItems = list.data('min');
|
||||
|
||||
if (typeof minItems !== 'undefined' && items.length <= minItems) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.remove();
|
||||
this.reindex(list);
|
||||
|
||||
items = list.closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
let topAction = list.closest('[data-type="collection"]').find('[data-action-add="top"]');
|
||||
let sortAction = list.closest('[data-type="collection"]').find('[data-action="sort"]');
|
||||
|
||||
if (!items.length) {
|
||||
if (topAction.length) {
|
||||
topAction.parent().addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (sortAction.length && items.length <= 1) {
|
||||
sortAction.addClass('hidden');
|
||||
}
|
||||
this._updateActionsStateBasedOnMinMax(list);
|
||||
}
|
||||
|
||||
collapseItems(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let items = $(button.closest('[data-type="collection"]')).find('> ul > [data-collection-item] > .item-actions [data-action="collapse"]');
|
||||
|
||||
items.click();
|
||||
}
|
||||
|
||||
collapseItem(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let item = button.closest('[data-collection-item]');
|
||||
|
||||
button.attr('data-action', 'expand').removeClass('fa-chevron-circle-down').addClass('fa-chevron-circle-right');
|
||||
item.addClass('collection-collapsed');
|
||||
}
|
||||
|
||||
expandItems(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let items = $(button.closest('[data-type="collection"]')).find('> ul > [data-collection-item] > .item-actions [data-action="expand"]');
|
||||
|
||||
items.click();
|
||||
}
|
||||
|
||||
expandItem(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let item = button.closest('[data-collection-item]');
|
||||
|
||||
button.attr('data-action', 'collapse').removeClass('fa-chevron-circle-right').addClass('fa-chevron-circle-down');
|
||||
item.removeClass('collection-collapsed');
|
||||
}
|
||||
|
||||
sortItems(event) {
|
||||
let button = $(event.currentTarget);
|
||||
let sortby = button.data('action-sort');
|
||||
let sortby_dir = button.data('action-sort-dir') || 'asc';
|
||||
let list = $(button.closest('[data-type="collection"]'));
|
||||
let items = list.closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
|
||||
items.sort((a, b) => {
|
||||
let A = $(a).find('[name$="[' + sortby + ']"]');
|
||||
let B = $(b).find('[name$="[' + sortby + ']"]');
|
||||
let sort;
|
||||
|
||||
if (sortby_dir === 'asc') {
|
||||
sort = (A.val() < B.val())
|
||||
? -1
|
||||
: (A.val() > B.val())
|
||||
? 1
|
||||
: 0;
|
||||
} else {
|
||||
sort = (A.val() > B.val())
|
||||
? -1
|
||||
: (A.val() < B.val())
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
return sort;
|
||||
}).each((_, container) => {
|
||||
$(container).parent().append(container);
|
||||
});
|
||||
|
||||
this.reindex(list);
|
||||
}
|
||||
|
||||
observeKey(event) {
|
||||
let input = $(event.target);
|
||||
let value = input.val();
|
||||
let item = input.closest('[data-collection-key]');
|
||||
|
||||
item.data('collection-key-backup', item.data('collection-key')).data('collection-key', value);
|
||||
this.reindex(null, item);
|
||||
}
|
||||
|
||||
reindex(list, items) {
|
||||
items = items || $(list).closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
|
||||
items.each((index, item) => {
|
||||
item = $(item);
|
||||
|
||||
let observed = item.find('[data-key-observe]');
|
||||
let observedValue = observed.val();
|
||||
let hasCustomKey = observed.length;
|
||||
let currentKey = item.data('collection-key-backup');
|
||||
|
||||
item.attr('data-collection-key', hasCustomKey
|
||||
? observedValue
|
||||
: index);
|
||||
|
||||
['name', 'data-grav-field-name', 'for', 'id', 'data-grav-file-settings', 'data-file-post-add', 'data-file-post-remove', 'data-grav-array-name', 'data-grav-elements'].forEach((prop) => {
|
||||
item.find('[' + prop + '], [_' + prop + ']').each(function() {
|
||||
let element = $(this);
|
||||
let indexes = [];
|
||||
let array_index = null;
|
||||
let regexps = [
|
||||
new RegExp('\\[(\\d+|\\*|' + currentKey + ')\\]', 'g'),
|
||||
new RegExp('\\.(\\d+|\\*|' + currentKey + ')\\.', 'g')
|
||||
];
|
||||
|
||||
// special case to preserve array field index keys
|
||||
if (prop === 'name' && element.data('gravArrayType')) {
|
||||
const match_index = element.attr(prop).match(/\[[0-9]{1,}\]$/);
|
||||
const pattern = element[0].closest('[data-grav-array-name]').dataset.gravArrayName;
|
||||
if (match_index && pattern) {
|
||||
array_index = match_index[0];
|
||||
element.attr(prop, `${pattern}${match_index[0]}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCustomKey && !observedValue) {
|
||||
element.attr(`_${prop}`, element.attr(prop));
|
||||
element.attr(prop, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.attr(`_${prop}`)) {
|
||||
element.attr(prop, element.attr(`_${prop}`));
|
||||
element.attr(`_${prop}`, null);
|
||||
}
|
||||
|
||||
element.parents('[data-collection-key]').map((idx, parent) => indexes.push($(parent).attr('data-collection-key')));
|
||||
indexes.reverse();
|
||||
|
||||
let matchedKey = currentKey;
|
||||
let replaced = element.attr(prop).replace(regexps[0], (/* str, p1, offset */) => {
|
||||
let extras = '';
|
||||
if (array_index) {
|
||||
extras = array_index;
|
||||
console.log(indexes, extras);
|
||||
}
|
||||
|
||||
matchedKey = indexes.shift() || matchedKey;
|
||||
return `[${matchedKey}]${extras}`;
|
||||
});
|
||||
|
||||
replaced = replaced.replace(regexps[1], (/* str, p1, offset */) => {
|
||||
matchedKey = indexes.shift() || matchedKey;
|
||||
return `.${matchedKey}.`;
|
||||
});
|
||||
|
||||
element.attr(prop, replaced);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let collections = $(target).find('[data-type="collection"]');
|
||||
if (!collections.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
collections.each((index, collection) => {
|
||||
collection = $(collection);
|
||||
if (!~this.lists.index(collection)) {
|
||||
this.addList(collection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateActionsStateBasedOnMinMax(list) {
|
||||
let items = list.closest('[data-type="collection"]').find('> ul > [data-collection-item]');
|
||||
let minItems = list.data('min');
|
||||
let maxItems = list.data('max');
|
||||
|
||||
list.find('> .collection-actions [data-action="add"]').attr('disabled', false);
|
||||
list.find('> ul > li > .item-actions [data-action="delete"]').attr('disabled', false);
|
||||
|
||||
if (typeof minItems !== 'undefined' && items.length <= minItems) {
|
||||
list.find('> ul > li > .item-actions [data-action="delete"]').attr('disabled', true);
|
||||
}
|
||||
|
||||
if (typeof maxItems !== 'undefined' && items.length >= maxItems) {
|
||||
list.find('> .collection-actions [data-action="add"]').attr('disabled', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new CollectionsField();
|
||||
579
user/plugins/admin/themes/grav/app/forms/fields/colorpicker.js
Normal file
579
user/plugins/admin/themes/grav/app/forms/fields/colorpicker.js
Normal file
@@ -0,0 +1,579 @@
|
||||
import $ from 'jquery';
|
||||
import clamp from 'mout/math/clamp';
|
||||
import bind from 'mout/function/bind';
|
||||
import { rgbstr2hex, hsb2hex, hex2hsb, hex2rgb, parseHex } from '../../utils/colors';
|
||||
|
||||
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
|
||||
const body = $('body');
|
||||
|
||||
const MOUSEDOWN = 'mousedown touchstart MSPointerDown pointerdown';
|
||||
const MOUSEMOVE = 'mousemove touchmove MSPointerMove pointermove';
|
||||
const MOUSEUP = 'mouseup touchend MSPointerUp pointerup';
|
||||
const FOCUSIN = isFirefox ? 'focus' : 'focusin';
|
||||
|
||||
export default class ColorpickerField {
|
||||
constructor(selector) {
|
||||
this.selector = selector;
|
||||
this.field = $(this.selector);
|
||||
this.options = Object.assign({}, this.field.data('grav-colorpicker'));
|
||||
this.built = false;
|
||||
this.attach();
|
||||
|
||||
if (this.options.update) {
|
||||
this.field.on('change._grav_colorpicker', (event, field, hex, opacity) => {
|
||||
let backgroundColor = hex;
|
||||
let rgb = hex2rgb(hex);
|
||||
|
||||
if (opacity < 1) {
|
||||
backgroundColor = 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + opacity + ')';
|
||||
}
|
||||
|
||||
let target = field.closest(this.options.update);
|
||||
if (!target.length) {
|
||||
target = field.siblings(this.options.update);
|
||||
}
|
||||
if (!target.length) {
|
||||
target = field.parent('.g-colorpicker').find(this.options.update);
|
||||
}
|
||||
|
||||
target.css({ backgroundColor });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
attach() {
|
||||
body.on(FOCUSIN, this.selector, (event) => this.show(event, event.currentTarget));
|
||||
body.on(MOUSEDOWN, this.selector + ' .g-colorpicker, ' + this.selector + ' .g-colorpicker i', this.bound('iconClick'));
|
||||
body.on('keydown', this.selector, (event) => {
|
||||
switch (event.keyCode) {
|
||||
case 9: // tab
|
||||
this.hide();
|
||||
break;
|
||||
case 13: // enter
|
||||
case 27: // esc
|
||||
this.hide();
|
||||
event.currentTarget.blur();
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update on keyup
|
||||
body.on('keyup', this.selector, (event) => {
|
||||
this.updateFromInput(true, event.currentTarget);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Update on paste
|
||||
body.on('paste', this.selector, (event) => {
|
||||
setTimeout(() => this.updateFromInput(true, event.currentTarget), 1);
|
||||
});
|
||||
}
|
||||
|
||||
show(event, target) {
|
||||
target = $(target);
|
||||
|
||||
if (!this.built) {
|
||||
this.build();
|
||||
}
|
||||
|
||||
this.element = target;
|
||||
this.reposition();
|
||||
this.wrapper.addClass('cp-visible');
|
||||
this.updateFromInput();
|
||||
|
||||
this.wrapper.on(MOUSEDOWN, '.cp-grid, .cp-slider, .cp-opacity-slider', this.bound('bodyDown'));
|
||||
body.on(MOUSEMOVE, this.bound('bodyMove'));
|
||||
body.on(MOUSEDOWN, this.bound('bodyClick'));
|
||||
body.on(MOUSEUP, this.bound('targetReset'));
|
||||
$('#admin-main > .content-wrapper').on('scroll', this.bound('reposition'));
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.built) { return; }
|
||||
this.wrapper.removeClass('cp-visible');
|
||||
|
||||
this.wrapper.undelegate(MOUSEDOWN, '.cp-grid, .cp-slider, .cp-opacity-slider', this.bound('bodyDown'));
|
||||
body.off(MOUSEMOVE, this.bound('bodyMove'));
|
||||
body.off(MOUSEDOWN, this.bound('bodyClick'));
|
||||
body.off(MOUSEUP, this.bound('targetReset'));
|
||||
$('#admin-main > .content-wrapper').on('scroll', this.bound('reposition'));
|
||||
}
|
||||
|
||||
build() {
|
||||
this.wrapper = $('<div class="cp-wrapper cp-with-opacity cp-mode-hue" />');
|
||||
this.slider = $('<div class="cp-slider cp-sprite" />').appendTo(this.wrapper).append($('<div class="cp-picker" />'));
|
||||
this.opacitySlider = $('<div class="cp-opacity-slider cp-sprite" />').appendTo(this.wrapper).append($('<div class="cp-picker" />'));
|
||||
this.grid = $('<div class="cp-grid cp-sprite" />').appendTo(this.wrapper).append($('<div class="cp-grid-inner" />')).append($('<div class="cp-picker" />'));
|
||||
|
||||
$('<div />').appendTo(this.grid.find('.cp-picker'));
|
||||
|
||||
let tabs = $('<div class="cp-tabs" />').appendTo(this.wrapper);
|
||||
this.tabs = {
|
||||
hue: $('<div class="cp-tab-hue active" />').text('HUE').appendTo(tabs),
|
||||
brightness: $('<div class="cp-tab-brightness" />').text('BRI').appendTo(tabs),
|
||||
saturation: $('<div class="cp-tab-saturation" />').text('SAT').appendTo(tabs),
|
||||
wheel: $('<div class="cp-tab-wheel" />').text('WHEEL').appendTo(tabs),
|
||||
transparent: $('<div class="cp-tab-transp" />').text('TRANSPARENT').appendTo(tabs)
|
||||
};
|
||||
|
||||
tabs.on(MOUSEDOWN, '> div', (event) => {
|
||||
let element = $(event.currentTarget);
|
||||
if (element.is(this.tabs.transparent)) {
|
||||
let sliderHeight = this.opacitySlider.height();
|
||||
|
||||
this.opacity = 0;
|
||||
this.opacitySlider.find('.cp-picker').css({ 'top': clamp(sliderHeight - (sliderHeight * this.opacity), 0, sliderHeight) });
|
||||
this.move(this.opacitySlider, { manualOpacity: true });
|
||||
return;
|
||||
}
|
||||
|
||||
let active = tabs.find('.active');
|
||||
let mode = active.attr('class').replace(/\s|active|cp-tab-/g, '');
|
||||
let newMode = element.attr('class').replace(/\s|active|cp-tab-/g, '');
|
||||
|
||||
this.wrapper.removeClass('cp-mode-' + mode).addClass('cp-mode-' + newMode);
|
||||
active.removeClass('active');
|
||||
element.addClass('active');
|
||||
|
||||
this.mode = newMode;
|
||||
this.updateFromInput();
|
||||
});
|
||||
|
||||
this.wrapper.appendTo('.content-wrapper');
|
||||
|
||||
this.built = true;
|
||||
this.mode = 'hue';
|
||||
}
|
||||
|
||||
reposition() {
|
||||
let ct = $('.content-wrapper')[0];
|
||||
let offset = this.element[0].getBoundingClientRect();
|
||||
let ctOffset = ct.getBoundingClientRect();
|
||||
let delta = { x: 0, y: 0 };
|
||||
|
||||
if (this.options.offset) {
|
||||
delta.x = this.options.offset.x || 0;
|
||||
delta.y = this.options.offset.y || 0;
|
||||
}
|
||||
|
||||
this.wrapper.css({
|
||||
top: offset.top + offset.height + ct.scrollTop - ctOffset.top + delta.y,
|
||||
left: offset.left + ct.scrollLeft - ctOffset.left + delta.x
|
||||
});
|
||||
}
|
||||
|
||||
iconClick(event) {
|
||||
if (this.wrapper && this.wrapper.hasClass('cp-visible')) { return true; }
|
||||
|
||||
event && event.preventDefault();
|
||||
|
||||
let input = $(event.currentTarget).find('input');
|
||||
setTimeout(() => input.focus(), 50);
|
||||
}
|
||||
|
||||
bodyMove(event) {
|
||||
event && event.preventDefault();
|
||||
|
||||
if (this.target) { this.move(this.target, event); }
|
||||
}
|
||||
|
||||
bodyClick(event) {
|
||||
let target = $(event.target);
|
||||
|
||||
if (!target.closest('.cp-wrapper').length && !target.is(this.selector)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
bodyDown(event) {
|
||||
event && event.preventDefault();
|
||||
|
||||
this.target = $(event.currentTarget);
|
||||
this.move(this.target, event, true);
|
||||
}
|
||||
|
||||
targetReset(event) {
|
||||
event && event.preventDefault();
|
||||
|
||||
this.target = null;
|
||||
}
|
||||
|
||||
move(target, event) {
|
||||
let input = this.element;
|
||||
let picker = target.find('.cp-picker');
|
||||
let clientRect = target[0].getBoundingClientRect();
|
||||
let offsetX = clientRect.left + window.scrollX;
|
||||
let offsetY = clientRect.top + window.scrollY;
|
||||
let x = Math.round((event ? event.pageX : 0) - offsetX);
|
||||
let y = Math.round((event ? event.pageY : 0) - offsetY);
|
||||
let wx;
|
||||
let wy;
|
||||
let r;
|
||||
let phi;
|
||||
|
||||
// Touch support
|
||||
let touchEvents = event.changedTouches || (event.originalEvent && event.originalEvent.changedTouches);
|
||||
if (event && touchEvents) {
|
||||
x = (touchEvents ? touchEvents[0].pageX : 0) - offsetX;
|
||||
y = (touchEvents ? touchEvents[0].pageY : 0) - offsetY;
|
||||
}
|
||||
|
||||
if (event && event.manualOpacity) {
|
||||
y = clientRect.height;
|
||||
}
|
||||
|
||||
// Constrain picker to its container
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
if (x > clientRect.width) x = clientRect.width;
|
||||
if (y > clientRect.height) y = clientRect.height;
|
||||
|
||||
// Constrain color wheel values to the wheel
|
||||
if (target.parent('.cp-mode-wheel').length && picker.parent('.cp-grid').length) {
|
||||
wx = 75 - x;
|
||||
wy = 75 - y;
|
||||
r = Math.sqrt(wx * wx + wy * wy);
|
||||
phi = Math.atan2(wy, wx);
|
||||
|
||||
if (phi < 0) phi += Math.PI * 2;
|
||||
if (r > 75) {
|
||||
x = 75 - (75 * Math.cos(phi));
|
||||
y = 75 - (75 * Math.sin(phi));
|
||||
}
|
||||
|
||||
x = Math.round(x);
|
||||
y = Math.round(y);
|
||||
}
|
||||
|
||||
// Move the picker
|
||||
if (target.hasClass('cp-grid')) {
|
||||
picker.css({
|
||||
top: y,
|
||||
left: x
|
||||
});
|
||||
|
||||
this.updateFromPicker(input, target);
|
||||
} else {
|
||||
picker.css({
|
||||
top: y
|
||||
});
|
||||
this.updateFromPicker(input, target);
|
||||
}
|
||||
}
|
||||
|
||||
updateFromInput(dontFireEvent, element) {
|
||||
element = element ? $(element) : this.element;
|
||||
let value = element.val();
|
||||
let opacity = value.replace(/\s/g, '').match(/^rgba?\([0-9]{1,3},[0-9]{1,3},[0-9]{1,3},(.+)\)/);
|
||||
let hex;
|
||||
let hsb;
|
||||
|
||||
value = rgbstr2hex(value) || value;
|
||||
opacity = opacity ? clamp(opacity[1], 0, 1) : 1;
|
||||
|
||||
if (!(hex = parseHex(value))) { hex = '#ffffff'; }
|
||||
hsb = hex2hsb(hex);
|
||||
|
||||
if (this.built) {
|
||||
// opacity
|
||||
this.opacity = opacity;
|
||||
var sliderHeight = this.opacitySlider.height();
|
||||
this.opacitySlider.find('.cp-picker').css({ 'top': clamp(sliderHeight - (sliderHeight * this.opacity), 0, sliderHeight) });
|
||||
|
||||
// bg color
|
||||
let gridHeight = this.grid.height();
|
||||
let gridWidth = this.grid.width();
|
||||
let r;
|
||||
let phi;
|
||||
let x;
|
||||
let y;
|
||||
|
||||
sliderHeight = this.slider.height();
|
||||
|
||||
switch (this.mode) {
|
||||
case 'wheel':
|
||||
// Set grid position
|
||||
r = clamp(Math.ceil(hsb.s * 0.75), 0, gridHeight / 2);
|
||||
phi = hsb.h * Math.PI / 180;
|
||||
x = clamp(75 - Math.cos(phi) * r, 0, gridWidth);
|
||||
y = clamp(75 - Math.sin(phi) * r, 0, gridHeight);
|
||||
this.grid.css({ backgroundColor: 'transparent' }).find('.cp-picker').css({
|
||||
top: y,
|
||||
left: x
|
||||
});
|
||||
|
||||
// Set slider position
|
||||
y = 150 - (hsb.b / (100 / gridHeight));
|
||||
if (hex === '') y = 0;
|
||||
this.slider.find('.cp-picker').css({ top: y });
|
||||
|
||||
// Update panel color
|
||||
this.slider.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hsb.h,
|
||||
s: hsb.s,
|
||||
b: 100
|
||||
})
|
||||
});
|
||||
break;
|
||||
|
||||
case 'saturation':
|
||||
// Set grid position
|
||||
x = clamp((5 * hsb.h) / 12, 0, 150);
|
||||
y = clamp(gridHeight - Math.ceil(hsb.b / (100 / gridHeight)), 0, gridHeight);
|
||||
this.grid.find('.cp-picker').css({
|
||||
top: y,
|
||||
left: x
|
||||
});
|
||||
|
||||
// Set slider position
|
||||
y = clamp(sliderHeight - (hsb.s * (sliderHeight / 100)), 0, sliderHeight);
|
||||
this.slider.find('.cp-picker').css({ top: y });
|
||||
|
||||
// Update UI
|
||||
this.slider.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hsb.h,
|
||||
s: 100,
|
||||
b: hsb.b
|
||||
})
|
||||
});
|
||||
this.grid.find('.cp-grid-inner').css({ opacity: hsb.s / 100 });
|
||||
break;
|
||||
|
||||
case 'brightness':
|
||||
// Set grid position
|
||||
x = clamp((5 * hsb.h) / 12, 0, 150);
|
||||
y = clamp(gridHeight - Math.ceil(hsb.s / (100 / gridHeight)), 0, gridHeight);
|
||||
this.grid.find('.cp-picker').css({
|
||||
top: y,
|
||||
left: x
|
||||
});
|
||||
|
||||
// Set slider position
|
||||
y = clamp(sliderHeight - (hsb.b * (sliderHeight / 100)), 0, sliderHeight);
|
||||
this.slider.find('.cp-picker').css({ top: y });
|
||||
|
||||
// Update UI
|
||||
this.slider.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hsb.h,
|
||||
s: hsb.s,
|
||||
b: 100
|
||||
})
|
||||
});
|
||||
this.grid.find('.cp-grid-inner').css({ opacity: 1 - (hsb.b / 100) });
|
||||
break;
|
||||
case 'hue':
|
||||
default:
|
||||
// Set grid position
|
||||
x = clamp(Math.ceil(hsb.s / (100 / gridWidth)), 0, gridWidth);
|
||||
y = clamp(gridHeight - Math.ceil(hsb.b / (100 / gridHeight)), 0, gridHeight);
|
||||
this.grid.find('.cp-picker').css({
|
||||
top: y,
|
||||
left: x
|
||||
});
|
||||
|
||||
// Set slider position
|
||||
y = clamp(sliderHeight - (hsb.h / (360 / sliderHeight)), 0, sliderHeight);
|
||||
this.slider.find('.cp-picker').css({ top: y });
|
||||
|
||||
// Update panel color
|
||||
this.grid.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hsb.h,
|
||||
s: 100,
|
||||
b: 100
|
||||
})
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dontFireEvent) { element.val(this.getValue(hex)); }
|
||||
|
||||
(this.element || element).trigger('change._grav_colorpicker', [element, hex, opacity]);
|
||||
|
||||
}
|
||||
|
||||
updateFromPicker(input, target) {
|
||||
var getCoords = function(picker, container) {
|
||||
|
||||
var left, top;
|
||||
if (!picker.length || !container) return null;
|
||||
left = picker[0].getBoundingClientRect().left;
|
||||
top = picker[0].getBoundingClientRect().top;
|
||||
|
||||
return {
|
||||
x: left - container[0].getBoundingClientRect().left + (picker[0].offsetWidth / 2),
|
||||
y: top - container[0].getBoundingClientRect().top + (picker[0].offsetHeight / 2)
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
let hex;
|
||||
let hue;
|
||||
let saturation;
|
||||
let brightness;
|
||||
let x;
|
||||
let y;
|
||||
let r;
|
||||
let phi;
|
||||
|
||||
// Panel objects
|
||||
let grid = this.wrapper.find('.cp-grid');
|
||||
let slider = this.wrapper.find('.cp-slider');
|
||||
let opacitySlider = this.wrapper.find('.cp-opacity-slider');
|
||||
|
||||
// Picker objects
|
||||
let gridPicker = grid.find('.cp-picker');
|
||||
let sliderPicker = slider.find('.cp-picker');
|
||||
let opacityPicker = opacitySlider.find('.cp-picker');
|
||||
|
||||
// Picker positions
|
||||
let gridPos = getCoords(gridPicker, grid);
|
||||
let sliderPos = getCoords(sliderPicker, slider);
|
||||
let opacityPos = getCoords(opacityPicker, opacitySlider);
|
||||
|
||||
// Sizes
|
||||
let gridWidth = grid[0].getBoundingClientRect().width;
|
||||
let gridHeight = grid[0].getBoundingClientRect().height;
|
||||
let sliderHeight = slider[0].getBoundingClientRect().height;
|
||||
let opacitySliderHeight = opacitySlider[0].getBoundingClientRect().height;
|
||||
|
||||
let value = this.element.val();
|
||||
value = rgbstr2hex(value) || value;
|
||||
if (!(hex = parseHex(value))) { hex = '#ffffff'; }
|
||||
|
||||
// Handle colors
|
||||
if (target.hasClass('cp-grid') || target.hasClass('cp-slider')) {
|
||||
|
||||
// Determine HSB values
|
||||
switch (this.mode) {
|
||||
case 'wheel':
|
||||
// Calculate hue, saturation, and brightness
|
||||
x = (gridWidth / 2) - gridPos.x;
|
||||
y = (gridHeight / 2) - gridPos.y;
|
||||
r = Math.sqrt(x * x + y * y);
|
||||
phi = Math.atan2(y, x);
|
||||
if (phi < 0) phi += Math.PI * 2;
|
||||
if (r > 75) {
|
||||
r = 75;
|
||||
gridPos.x = 69 - (75 * Math.cos(phi));
|
||||
gridPos.y = 69 - (75 * Math.sin(phi));
|
||||
}
|
||||
saturation = clamp(r / 0.75, 0, 100);
|
||||
hue = clamp(phi * 180 / Math.PI, 0, 360);
|
||||
brightness = clamp(100 - Math.floor(sliderPos.y * (100 / sliderHeight)), 0, 100);
|
||||
hex = hsb2hex({
|
||||
h: hue,
|
||||
s: saturation,
|
||||
b: brightness
|
||||
});
|
||||
|
||||
// Update UI
|
||||
slider.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hue,
|
||||
s: saturation,
|
||||
b: 100
|
||||
})
|
||||
});
|
||||
break;
|
||||
|
||||
case 'saturation':
|
||||
// Calculate hue, saturation, and brightness
|
||||
hue = clamp(parseInt(gridPos.x * (360 / gridWidth), 10), 0, 360);
|
||||
saturation = clamp(100 - Math.floor(sliderPos.y * (100 / sliderHeight)), 0, 100);
|
||||
brightness = clamp(100 - Math.floor(gridPos.y * (100 / gridHeight)), 0, 100);
|
||||
hex = hsb2hex({
|
||||
h: hue,
|
||||
s: saturation,
|
||||
b: brightness
|
||||
});
|
||||
|
||||
// Update UI
|
||||
slider.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hue,
|
||||
s: 100,
|
||||
b: brightness
|
||||
})
|
||||
});
|
||||
grid.find('.cp-grid-inner').css({ opacity: saturation / 100 });
|
||||
break;
|
||||
|
||||
case 'brightness':
|
||||
// Calculate hue, saturation, and brightness
|
||||
hue = clamp(parseInt(gridPos.x * (360 / gridWidth), 10), 0, 360);
|
||||
saturation = clamp(100 - Math.floor(gridPos.y * (100 / gridHeight)), 0, 100);
|
||||
brightness = clamp(100 - Math.floor(sliderPos.y * (100 / sliderHeight)), 0, 100);
|
||||
hex = hsb2hex({
|
||||
h: hue,
|
||||
s: saturation,
|
||||
b: brightness
|
||||
});
|
||||
|
||||
// Update UI
|
||||
slider.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hue,
|
||||
s: saturation,
|
||||
b: 100
|
||||
})
|
||||
});
|
||||
grid.find('.cp-grid-inner').css({ opacity: 1 - (brightness / 100) });
|
||||
break;
|
||||
|
||||
default:
|
||||
// Calculate hue, saturation, and brightness
|
||||
hue = clamp(360 - parseInt(sliderPos.y * (360 / sliderHeight), 10), 0, 360);
|
||||
saturation = clamp(Math.floor(gridPos.x * (100 / gridWidth)), 0, 100);
|
||||
brightness = clamp(100 - Math.floor(gridPos.y * (100 / gridHeight)), 0, 100);
|
||||
hex = hsb2hex({
|
||||
h: hue,
|
||||
s: saturation,
|
||||
b: brightness
|
||||
});
|
||||
|
||||
// Update UI
|
||||
grid.css({
|
||||
backgroundColor: hsb2hex({
|
||||
h: hue,
|
||||
s: 100,
|
||||
b: 100
|
||||
})
|
||||
});
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Handle opacity
|
||||
if (target.hasClass('cp-opacity-slider')) {
|
||||
this.opacity = parseFloat(1 - (opacityPos.y / opacitySliderHeight)).toFixed(2);
|
||||
}
|
||||
|
||||
// Adjust case
|
||||
input.val(this.getValue(hex));
|
||||
|
||||
// Handle change event
|
||||
this.element.trigger('change._grav_colorpicker', [this.element, hex, this.opacity]);
|
||||
|
||||
}
|
||||
|
||||
getValue(hex) {
|
||||
if (this.opacity === 1) { return hex; }
|
||||
let rgb = hex2rgb(hex);
|
||||
|
||||
return 'rgba(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ', ' + this.opacity + ')';
|
||||
}
|
||||
|
||||
bound(name) {
|
||||
let bound = this._bound || (this._bound = {});
|
||||
return bound[name] || (bound[name] = bind(this[name], this));
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new ColorpickerField('[data-grav-colorpicker]');
|
||||
85
user/plugins/admin/themes/grav/app/forms/fields/cron.js
Normal file
85
user/plugins/admin/themes/grav/app/forms/fields/cron.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import $ from 'jquery';
|
||||
import '../../utils/cron-ui';
|
||||
import { translations } from 'grav-config';
|
||||
|
||||
export default class CronField {
|
||||
constructor() {
|
||||
this.items = $();
|
||||
|
||||
$('[data-grav-field="cron"]').each((index, cron) => this.addCron(cron));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
}
|
||||
|
||||
addCron(cron) {
|
||||
cron = $(cron);
|
||||
this.items = this.items.add(cron);
|
||||
|
||||
cron.find('.cron-selector').each((index, container) => {
|
||||
container = $(container);
|
||||
const input = container.closest('[data-grav-field]').find('input');
|
||||
|
||||
container.jqCron({
|
||||
numeric_zero_pad: true,
|
||||
enabled_minute: true,
|
||||
multiple_dom: true,
|
||||
multiple_month: true,
|
||||
multiple_mins: true,
|
||||
multiple_dow: true,
|
||||
multiple_time_hours: true,
|
||||
multiple_time_minutes: true,
|
||||
default_period: 'hour',
|
||||
default_value: input.val() || '* * * * *',
|
||||
no_reset_button: false,
|
||||
bind_to: input,
|
||||
bind_method: {
|
||||
set: function($element, value) {
|
||||
$element.val(value);
|
||||
}
|
||||
},
|
||||
texts: {
|
||||
en: {
|
||||
empty: translations.GRAV_CORE['CRON.EVERY'],
|
||||
empty_minutes: translations.GRAV_CORE['CRON.EVERY'],
|
||||
empty_time_hours: translations.GRAV_CORE['CRON.EVERY_HOUR'],
|
||||
empty_time_minutes: translations.GRAV_CORE['CRON.EVERY_MINUTE'],
|
||||
empty_day_of_week: translations.GRAV_CORE['CRON.EVERY_DAY_OF_WEEK'],
|
||||
empty_day_of_month: translations.GRAV_CORE['CRON.EVERY_DAY_OF_MONTH'],
|
||||
empty_month: translations.GRAV_CORE['CRON.EVERY_MONTH'],
|
||||
name_minute: translations.GRAV_CORE['NICETIME.MINUTE'],
|
||||
name_hour: translations.GRAV_CORE['NICETIME.HOUR'],
|
||||
name_day: translations.GRAV_CORE['NICETIME.DAY'],
|
||||
name_week: translations.GRAV_CORE['NICETIME.WEEK'],
|
||||
name_month: translations.GRAV_CORE['NICETIME.MONTH'],
|
||||
name_year: translations.GRAV_CORE['NICETIME.YEAR'],
|
||||
text_period: translations.GRAV_CORE['CRON.TEXT_PERIOD'],
|
||||
text_mins: translations.GRAV_CORE['CRON.TEXT_MINS'],
|
||||
text_time: translations.GRAV_CORE['CRON.TEXT_TIME'],
|
||||
text_dow: translations.GRAV_CORE['CRON.TEXT_DOW'],
|
||||
text_month: translations.GRAV_CORE['CRON.TEXT_MONTH'],
|
||||
text_dom: translations.GRAV_CORE['CRON.TEXT_DOM'],
|
||||
error1: translations.GRAV_CORE['CRON.ERROR1'],
|
||||
error2: translations.GRAV_CORE['CRON.ERROR2'],
|
||||
error3: translations.GRAV_CORE['CRON.ERROR3'],
|
||||
error4: translations.GRAV_CORE['CRON.ERROR4'],
|
||||
weekdays: translations.GRAV_CORE['DAYS_OF_THE_WEEK'],
|
||||
months: translations.GRAV_CORE['MONTHS_OF_THE_YEAR']
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let crons = $(target).find('[data-grav-field="cron"]');
|
||||
if (!crons.length) { return; }
|
||||
|
||||
crons.each((index, list) => {
|
||||
list = $(list);
|
||||
if (!~this.items.index(list)) {
|
||||
this.addCron(list);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new CronField();
|
||||
63
user/plugins/admin/themes/grav/app/forms/fields/datetime.js
Normal file
63
user/plugins/admin/themes/grav/app/forms/fields/datetime.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import $ from 'jquery';
|
||||
import { config } from 'grav-config';
|
||||
import '../../utils/bootstrap-datetimepicker';
|
||||
|
||||
export default class DateTimeField {
|
||||
|
||||
get defaults() {
|
||||
return {
|
||||
showTodayButton: true,
|
||||
showClear: true,
|
||||
locale: config.language || 'en',
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar-o',
|
||||
up: 'fa fa-chevron-up',
|
||||
down: 'fa fa-chevron-down',
|
||||
previous: 'fa fa-chevron-left',
|
||||
next: 'fa fa-chevron-right',
|
||||
today: 'fa fa-bullseye',
|
||||
clear: 'fa fa-trash-o',
|
||||
close: 'fa fa-remove'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(options) {
|
||||
this.items = $();
|
||||
this.options = Object.assign({}, this.defaults, options);
|
||||
|
||||
$('[data-grav-datetime]').each((index, field) => this.addItem(field));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
|
||||
}
|
||||
|
||||
addItem(list) {
|
||||
list = $(list);
|
||||
this.items = this.items.add(list);
|
||||
|
||||
if (list.data('DateTimePicker')) { return; }
|
||||
|
||||
let options = Object.assign({}, this.options, list.data('grav-datetime') || {});
|
||||
list.datetimepicker(options).on('dp.show dp.update', this._disableDecades);
|
||||
list.siblings('.field-icons').on('click', () => list.mousedown().focus());
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let fields = $(target).find('[data-grav-datetime]');
|
||||
if (!fields.length) { return; }
|
||||
|
||||
fields.each((index, field) => {
|
||||
field = $(field);
|
||||
if (!~this.items.index(field)) {
|
||||
this.addItem(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_disableDecades() {
|
||||
$('.datepicker-years .picker-switch').removeAttr('title').on('click', (e) => e.stopPropagation());
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new DateTimeField();
|
||||
237
user/plugins/admin/themes/grav/app/forms/fields/editor.js
Normal file
237
user/plugins/admin/themes/grav/app/forms/fields/editor.js
Normal file
@@ -0,0 +1,237 @@
|
||||
import $ from 'jquery';
|
||||
import Buttons, { strategies as buttonStrategies } from './editor/buttons';
|
||||
import codemirror from 'codemirror';
|
||||
import { watch } from 'watchjs';
|
||||
import jsyaml from 'js-yaml';
|
||||
|
||||
global.jsyaml = jsyaml;
|
||||
|
||||
// Modes
|
||||
import 'codemirror/mode/css/css';
|
||||
import 'codemirror/mode/gfm/gfm';
|
||||
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/mode/markdown/markdown';
|
||||
import 'codemirror/mode/php/php';
|
||||
import 'codemirror/mode/sass/sass';
|
||||
import 'codemirror/mode/twig/twig';
|
||||
import 'codemirror/mode/xml/xml';
|
||||
import 'codemirror/mode/yaml/yaml';
|
||||
|
||||
// Add-ons
|
||||
import 'codemirror/addon/edit/continuelist';
|
||||
import 'codemirror/addon/mode/overlay';
|
||||
import 'codemirror/addon/selection/active-line';
|
||||
import 'codemirror/addon/lint/lint';
|
||||
import 'codemirror/addon/lint/lint.css';
|
||||
import 'codemirror/addon/lint/css-lint';
|
||||
import 'codemirror/addon/lint/javascript-lint';
|
||||
import 'codemirror/addon/lint/json-lint';
|
||||
import 'codemirror/addon/lint/yaml-lint';
|
||||
|
||||
let IS_MOUSEDOWN = false;
|
||||
const ThemesMap = ['paper'];
|
||||
const Defaults = {
|
||||
codemirror: {
|
||||
mode: 'htmlmixed',
|
||||
theme: 'paper',
|
||||
lineWrapping: true,
|
||||
dragDrop: true,
|
||||
autoCloseTags: true,
|
||||
matchTags: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 4,
|
||||
indentWithTabs: false,
|
||||
tabSize: 4,
|
||||
hintOptions: { completionSingle: false },
|
||||
extraKeys: { 'Enter': 'newlineAndIndentContinueMarkdownList' }
|
||||
}
|
||||
};
|
||||
|
||||
export default class EditorField {
|
||||
constructor(options) {
|
||||
let body = $('body');
|
||||
this.editors = $();
|
||||
this.options = Object.assign({}, Defaults, options);
|
||||
this.buttons = Buttons;
|
||||
this.buttonStrategies = buttonStrategies;
|
||||
|
||||
watch(Buttons, (/* key, modifier, prev, next */) => {
|
||||
this.editors.each((index, editor) => $(editor).data('toolbar').renderButtons());
|
||||
});
|
||||
|
||||
$('[data-grav-editor]').each((index, editor) => this.addEditor(editor));
|
||||
|
||||
$(() => { body.trigger('grav-editor-ready'); });
|
||||
body.on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
|
||||
body.on('mouseup._grav', () => {
|
||||
if (!IS_MOUSEDOWN) { return true; }
|
||||
body.unbind('mousemove._grav');
|
||||
IS_MOUSEDOWN = false;
|
||||
});
|
||||
body.on('mousedown._grav', '.grav-editor-resizer', (event) => {
|
||||
event && event.preventDefault();
|
||||
IS_MOUSEDOWN = true;
|
||||
|
||||
let target = $(event.currentTarget);
|
||||
let container = target.siblings('.grav-editor-content');
|
||||
let editor = container.find('.CodeMirror');
|
||||
let codemirror = container.find('textarea').data('codemirror');
|
||||
|
||||
body.on('mousemove._grav', (event) => {
|
||||
editor.css('height', Math.max(100, event.pageY - container.offset().top));
|
||||
codemirror.refresh();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addButton(button, options) {
|
||||
if (options && (options.before || options.after)) {
|
||||
let index = this.buttons.navigation.findIndex((obj) => {
|
||||
let key = Object.keys(obj).shift();
|
||||
return obj[key].identifier === (options.before || options.after);
|
||||
});
|
||||
|
||||
if (!~index) {
|
||||
options = 'end';
|
||||
} else {
|
||||
this.buttons.navigation.splice(options.before ? index : index + 1, 0, button);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (options === 'start') { this.buttons.navigation.splice(0, 0, button); }
|
||||
if (!options || options === 'end') { this.buttons.navigation.push(button); }
|
||||
}
|
||||
|
||||
addEditor(textarea) {
|
||||
textarea = $(textarea);
|
||||
let options = Object.assign(
|
||||
{},
|
||||
this.options.codemirror,
|
||||
textarea.data('grav-editor').codemirror
|
||||
);
|
||||
let theme = options.theme || 'paper';
|
||||
|
||||
this.editors = this.editors.add(textarea);
|
||||
if (theme && !~ThemesMap.indexOf(theme)) {
|
||||
ThemesMap.push(theme);
|
||||
// let themeCSS = `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.12.0/theme/${theme}.min.css`;
|
||||
// $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', themeCSS));
|
||||
}
|
||||
|
||||
if (options.mode === 'yaml') {
|
||||
Object.assign(options.extraKeys, { Tab: function(cm) { cm.replaceSelection(' ', 'end'); }});
|
||||
}
|
||||
|
||||
let editor = codemirror.fromTextArea(textarea.get(0), options);
|
||||
textarea.data('codemirror', editor);
|
||||
textarea.data('toolbar', new Toolbar(textarea));
|
||||
textarea.addClass('code-mirrored');
|
||||
|
||||
if (options.toolbar === false) {
|
||||
textarea.data('toolbar').ui.navigation.addClass('grav-editor-hide-toolbar');
|
||||
}
|
||||
|
||||
editor.on('change', () => editor.save());
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let editors = $(target).find('[data-grav-editor]');
|
||||
if (!editors.length) { return; }
|
||||
|
||||
editors.each((index, editor) => {
|
||||
editor = $(editor);
|
||||
if (!~this.editors.index(editor)) {
|
||||
this.addEditor(editor);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Toolbar {
|
||||
static templates() {
|
||||
return {
|
||||
navigation: `
|
||||
<div class="grav-editor-toolbar">
|
||||
<div class="grav-editor-actions"></div>
|
||||
<div class="grav-editor-modes"></div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
}
|
||||
|
||||
constructor(editor) {
|
||||
this.editor = $(editor);
|
||||
this.codemirror = this.editor.data('codemirror');
|
||||
this.buttons = Buttons.navigation;
|
||||
this.ui = {
|
||||
navigation: $(Toolbar.templates().navigation)
|
||||
};
|
||||
|
||||
this.editor.parent('.grav-editor-content')
|
||||
.before(this.ui.navigation)
|
||||
.after(this.ui.states);
|
||||
|
||||
this.renderButtons();
|
||||
}
|
||||
|
||||
renderButtons() {
|
||||
let map = { 'actions': 'navigation', 'modes': 'states'};
|
||||
|
||||
['actions', 'modes'].forEach((type) => {
|
||||
this.ui.navigation.find(`.grav-editor-${type}`).empty().append('<ul />');
|
||||
Buttons[map[type]].forEach((button) => this.renderButton(button, type));
|
||||
});
|
||||
}
|
||||
|
||||
renderButton(button, type, location = null) {
|
||||
Object.keys(button).forEach((key) => {
|
||||
let obj = button[key];
|
||||
if (!obj.modes) { obj.modes = []; }
|
||||
if (!~this.codemirror.options.ignore.indexOf(key) && (!obj.modes.length || obj.modes.indexOf(this.codemirror.options.mode) > -1)) {
|
||||
let hint = obj.title ? `data-hint="${obj.title}"` : '';
|
||||
let element = $(`<li class="grav-editor-button-${key}"><a class="hint--top" ${hint}>${obj.label}</a></li>`);
|
||||
(location || this.ui.navigation.find(`.grav-editor-${type} ul:not(.dropdown-menu)`)).append(element);
|
||||
|
||||
if (obj.shortcut) {
|
||||
this.addShortcut(obj.identifier, obj.shortcut, element);
|
||||
}
|
||||
|
||||
obj.action && obj.action.call(obj.action, {
|
||||
codemirror: this.codemirror,
|
||||
button: element,
|
||||
textarea: this.editor,
|
||||
ui: this.ui
|
||||
});
|
||||
|
||||
if (obj.children) {
|
||||
let childrenContainer = $('<ul class="dropdown-menu" />');
|
||||
element.addClass('button-group').find('a').wrap('<div class="dropdown-toggle" data-toggle="dropdown"></div>');
|
||||
element.find('a').append(' <i class="fa fa-caret-down"></i>');
|
||||
element.append(childrenContainer);
|
||||
obj.children.forEach((child) => this.renderButton(child, type, childrenContainer));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addShortcut(identifier, shortcut, element) {
|
||||
let map = {};
|
||||
if (!Array.isArray(shortcut)) {
|
||||
shortcut = [shortcut];
|
||||
}
|
||||
|
||||
shortcut.forEach((key) => {
|
||||
map[key] = () => {
|
||||
element.trigger(`click.editor.${identifier}`, [this.codemirror]);
|
||||
};
|
||||
});
|
||||
|
||||
this.codemirror.addKeyMap(map);
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new EditorField();
|
||||
@@ -0,0 +1,416 @@
|
||||
import $ from 'jquery';
|
||||
import { config, translations } from 'grav-config';
|
||||
import request from '../../../utils/request';
|
||||
|
||||
let replacer = ({ name, replace, codemirror, button, mode = 'replaceSelections', runner }) => {
|
||||
button.on(`click.editor.${name}`, () => {
|
||||
strategies[mode]({ token: '$1', template: replace, codemirror, runner });
|
||||
});
|
||||
};
|
||||
|
||||
export let strategies = {
|
||||
replaceSelections({ template, token, codemirror, runner }) {
|
||||
let replacements = [];
|
||||
let ranges = [];
|
||||
let selections = codemirror.getSelections();
|
||||
let list = codemirror.listSelections();
|
||||
let accumulator = {};
|
||||
|
||||
selections.forEach((selection, index) => {
|
||||
let markup = template.replace(token, selection);
|
||||
|
||||
let cursor = markup.indexOf('$cur');
|
||||
let { line, ch } = list[index].anchor;
|
||||
|
||||
markup = markup.replace('$cur', '');
|
||||
markup = runner ? runner(selection, markup, list) : markup;
|
||||
replacements.push(markup);
|
||||
|
||||
if (!accumulator[line]) { accumulator[line] = 0; }
|
||||
|
||||
ch += accumulator[line] + (cursor === -1 ? markup.length : cursor);
|
||||
let range = { ch, line };
|
||||
|
||||
ranges.push({ anchor: range, head: range });
|
||||
accumulator[line] += markup.length - selection.length;
|
||||
});
|
||||
|
||||
codemirror.replaceSelections(replacements);
|
||||
codemirror.setSelections(ranges);
|
||||
codemirror.focus();
|
||||
},
|
||||
replaceLine({ template, token, codemirror, runner }) {
|
||||
let list = codemirror.listSelections();
|
||||
let range;
|
||||
|
||||
list.forEach((selection) => {
|
||||
let lines = {
|
||||
min: Math.min(selection.anchor.line, selection.head.line),
|
||||
max: Math.max(selection.anchor.line, selection.head.line)
|
||||
};
|
||||
|
||||
codemirror.eachLine(lines.min, lines.max + 1, (handler) => {
|
||||
let markup = template.replace(token, handler.text);
|
||||
let line = codemirror.getLineNumber(handler);
|
||||
markup = runner ? runner(handler, markup) : markup;
|
||||
codemirror.replaceRange(markup, { line, ch: 0 }, { line, ch: markup.length });
|
||||
range = { line, ch: markup.length };
|
||||
});
|
||||
});
|
||||
|
||||
codemirror.setSelection(range, range, 'end');
|
||||
codemirror.focus();
|
||||
},
|
||||
replaceRange() {}
|
||||
};
|
||||
|
||||
const flipDisabled = (codemirror, button, type) => {
|
||||
let hasHistory = codemirror.historySize()[type];
|
||||
let element = button.find('a');
|
||||
button[hasHistory ? 'removeClass' : 'addClass']('button-disabled');
|
||||
|
||||
if (!hasHistory) {
|
||||
element.attr('title-disabled', element.attr('title'));
|
||||
element.attr('data-hint-disabled', element.attr('data-hint'));
|
||||
element.removeAttr('title').removeAttr('data-hint');
|
||||
} else {
|
||||
element.attr('title', element.attr('title-disabled'));
|
||||
element.attr('data-hint', element.attr('data-hint-disabled'));
|
||||
element.removeAttr('title-disabled').removeAttr('data-hint-disabled');
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
navigation: [
|
||||
{
|
||||
undo: {
|
||||
identifier: 'undo',
|
||||
title: translations.PLUGIN_ADMIN.UNDO,
|
||||
label: '<i class="fa fa-fw fa-undo"></i>',
|
||||
modes: [],
|
||||
action({ codemirror, button, textarea}) {
|
||||
button.addClass('button-disabled');
|
||||
codemirror.on('change', () => flipDisabled(codemirror, button, 'undo'));
|
||||
button.on('click.editor.undo', () => {
|
||||
codemirror.undo();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
redo: {
|
||||
identifier: 'redo',
|
||||
title: translations.PLUGIN_ADMIN.REDO,
|
||||
label: '<i class="fa fa-fw fa-repeat"></i>',
|
||||
modes: [],
|
||||
action({ codemirror, button, textarea}) {
|
||||
button.addClass('button-disabled');
|
||||
codemirror.on('change', () => flipDisabled(codemirror, button, 'redo'));
|
||||
button.on('click.editor.redo', () => {
|
||||
codemirror.redo();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
identifier: 'headers',
|
||||
title: translations.PLUGIN_ADMIN.HEADERS,
|
||||
label: '<i class="fa fa-fw fa-header"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
children: [
|
||||
{
|
||||
h1: {
|
||||
identifier: 'h1',
|
||||
label: '<i class="fa fa-fw fa-header"></i>1',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'h1', replace: '# $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
h2: {
|
||||
identifier: 'h2',
|
||||
label: '<i class="fa fa-fw fa-header"></i>2',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'h2', replace: '## $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
h3: {
|
||||
identifier: 'h3',
|
||||
label: '<i class="fa fa-fw fa-header"></i>3',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'h3', replace: '### $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
h4: {
|
||||
identifier: 'h4',
|
||||
label: '<i class="fa fa-fw fa-header"></i>4',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'h4', replace: '#### $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
h5: {
|
||||
identifier: 'h5',
|
||||
label: '<i class="fa fa-fw fa-header"></i>5',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'h5', replace: '##### $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
h6: {
|
||||
identifier: 'h6',
|
||||
label: '<i class="fa fa-fw fa-header"></i>6',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'h6', replace: '###### $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
bold: {
|
||||
identifier: 'bold',
|
||||
title: translations.PLUGIN_ADMIN.BOLD,
|
||||
label: '<i class="fa fa-fw fa-bold"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
shortcut: ['Ctrl-B', 'Cmd-B'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'bold', replace: '**$1$cur**', codemirror, button });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
italic: {
|
||||
identifier: 'italic',
|
||||
title: translations.PLUGIN_ADMIN.ITALIC,
|
||||
label: '<i class="fa fa-fw fa-italic"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
shortcut: ['Ctrl-I', 'Cmd-I'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'italic', replace: '_$1$cur_', codemirror, button });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
strike: {
|
||||
identifier: 'strike',
|
||||
title: translations.PLUGIN_ADMIN.STRIKETHROUGH,
|
||||
label: '<i class="fa fa-fw fa-strikethrough"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'strike', replace: '~~$1$cur~~', codemirror, button });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
delimiter: {
|
||||
identifier: 'delimiter',
|
||||
title: translations.PLUGIN_ADMIN.SUMMARY_DELIMITER,
|
||||
label: '<i class="fa fa-fw fa-minus"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'delimiter', replace: `${config.site.delimiter}$1`, codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
link: {
|
||||
identifier: 'link',
|
||||
title: translations.PLUGIN_ADMIN.LINK,
|
||||
label: '<i class="fa fa-fw fa-link"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
shortcut: ['Ctrl-K', 'Cmd-K'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'link', replace: '[$1]($cur)', codemirror, button });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
image: {
|
||||
identifier: 'image',
|
||||
title: translations.PLUGIN_ADMIN.IMAGE,
|
||||
label: '<i class="fa fa-fw fa-picture-o"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'image', replace: '', codemirror, button });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
blockquote: {
|
||||
identifier: 'blockquote',
|
||||
title: translations.PLUGIN_ADMIN.BLOCKQUOTE,
|
||||
label: '<i class="fa fa-fw fa-quote-right"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'blockquote', replace: '> $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
listUl: {
|
||||
identifier: 'listUl',
|
||||
title: translations.PLUGIN_ADMIN.UNORDERED_LIST,
|
||||
label: '<i class="fa fa-fw fa-list-ul"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({ name: 'listUl', replace: '* $1', codemirror, button, mode: 'replaceLine' });
|
||||
}
|
||||
}
|
||||
}, {
|
||||
listOl: {
|
||||
identifier: 'listOl',
|
||||
title: translations.PLUGIN_ADMIN.ORDERED_LIST,
|
||||
label: '<i class="fa fa-fw fa-list-ol"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea }) {
|
||||
replacer({
|
||||
name: 'listOl',
|
||||
replace: '. $1',
|
||||
codemirror,
|
||||
button,
|
||||
mode: 'replaceLine',
|
||||
runner: function(line, markup) {
|
||||
let lineNo = codemirror.getLineNumber(line);
|
||||
let previousLine = codemirror.getLine(lineNo - 1) || '';
|
||||
let match = previousLine.match(/^(\d+)\./);
|
||||
let prefix = 1 + (match ? Number(match[1]) : 0);
|
||||
|
||||
return `${prefix}${markup}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
states: [{
|
||||
code: {
|
||||
identifier: 'editor',
|
||||
title: translations.PLUGIN_ADMIN.EDITOR,
|
||||
label: '<i class="fa fa-fw fa-code"></i>',
|
||||
action({ codemirror, button, textarea, ui }) {
|
||||
if (textarea.data('grav-editor-mode') === 'editor') {
|
||||
button.addClass('editor-active');
|
||||
}
|
||||
|
||||
button.on('click.states.editor', () => {
|
||||
button.siblings().removeClass('editor-active');
|
||||
button.addClass('editor-active');
|
||||
textarea.data('grav-editor-mode', 'editor');
|
||||
let previewContainer = textarea.data('grav-editor-preview-container');
|
||||
let content = textarea.parent('.grav-editor-content');
|
||||
|
||||
content.addClass('is-active');
|
||||
ui.navigation.find('.grav-editor-actions').css('visibility', 'visible');
|
||||
if (previewContainer) {
|
||||
previewContainer.removeClass('is-active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
preview: {
|
||||
identifier: 'preview',
|
||||
title: translations.PLUGIN_ADMIN.PREVIEW,
|
||||
label: '<i class="fa fa-fw fa-eye"></i>',
|
||||
modes: ['gfm', 'markdown'],
|
||||
action({ codemirror, button, textarea, ui }) {
|
||||
if (textarea.data('grav-editor-mode') === 'preview') {
|
||||
button.addClass('editor-active');
|
||||
}
|
||||
button.on('click.states.preview', () => {
|
||||
let previewContainer = textarea.data('grav-editor-preview-container');
|
||||
let content = textarea.parent('.grav-editor-content');
|
||||
button.siblings().removeClass('editor-active');
|
||||
button.addClass('editor-active');
|
||||
textarea.data('grav-editor-mode', 'preview');
|
||||
|
||||
if (!previewContainer) {
|
||||
previewContainer = $('<div class="grav-editor-preview" />');
|
||||
content.after(previewContainer);
|
||||
textarea.data('grav-editor-preview-container', previewContainer);
|
||||
}
|
||||
|
||||
previewContainer.css({ height: content.height() });
|
||||
previewContainer.addClass('is-active');
|
||||
content.removeClass('is-active');
|
||||
ui.navigation.find('.grav-editor-actions').css('visibility', 'hidden');
|
||||
|
||||
let url = `${textarea.data('grav-urlpreview')}/task${config.param_sep}processmarkdown`;
|
||||
let params = textarea.closest('form').serializeArray();
|
||||
let body = {};
|
||||
params.map((obj) => { body[obj.name] = obj.value; });
|
||||
request(url, {
|
||||
method: 'post',
|
||||
body
|
||||
}, (response) => previewContainer.html(response.preview));
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
fullscreen: {
|
||||
identifier: 'fullscreen',
|
||||
title: translations.PLUGIN_ADMIN.FULLSCREEN,
|
||||
label: '<i class="fa fa-fw fa-expand"></i>',
|
||||
action({ codemirror, button, textarea }) {
|
||||
button.on('click.editor.fullscreen', () => {
|
||||
let container = textarea.closest('.grav-editor');
|
||||
let wrapper = codemirror.getWrapperElement();
|
||||
let contentWrapper = $('.content-wrapper');
|
||||
|
||||
if (!container.hasClass('grav-editor-fullscreen')) {
|
||||
textarea.data('fullScreenRestore', {
|
||||
scrollTop: global.pageYOffset,
|
||||
scrollLeft: global.pageXOffset,
|
||||
width: wrapper.style.width,
|
||||
height: wrapper.style.height
|
||||
});
|
||||
|
||||
wrapper.style.width = '';
|
||||
wrapper.style.height = textarea.parent('.grav-editor-content').height() + 'px';
|
||||
global.document.documentElement.style.overflow = 'hidden';
|
||||
|
||||
let hints = container.find('.grav-editor-toolbar .hint--top');
|
||||
|
||||
if (hints) {
|
||||
hints.removeClass('hint--top').addClass('hint--bottom');
|
||||
$(hints[hints.length - 1]).addClass('hint--bottom-left');
|
||||
}
|
||||
if (contentWrapper) { contentWrapper.css('overflow', 'visible'); }
|
||||
} else {
|
||||
global.document.documentElement.style.overflow = '';
|
||||
let state = textarea.data('fullScreenRestore');
|
||||
|
||||
wrapper.style.width = state.width;
|
||||
wrapper.style.height = state.height;
|
||||
global.scrollTo(state.scrollLeft, state.scrollTop);
|
||||
|
||||
let hints = container.find('.grav-editor-toolbar .hint--bottom');
|
||||
|
||||
if (hints) {
|
||||
hints.removeClass('hint--bottom').addClass('hint--top');
|
||||
$(hints[hints.length - 1]).removeClass('hint--bottom-left');
|
||||
}
|
||||
if (contentWrapper) { contentWrapper.css('overflow', 'auto'); }
|
||||
}
|
||||
|
||||
container.toggleClass('grav-editor-fullscreen');
|
||||
|
||||
setTimeout(() => {
|
||||
codemirror.refresh();
|
||||
// this.preview.parent().css('height', this.code.height());
|
||||
$(global).trigger('resize');
|
||||
}, 5);
|
||||
});
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
12
user/plugins/admin/themes/grav/app/forms/fields/elements.js
Normal file
12
user/plugins/admin/themes/grav/app/forms/fields/elements.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
$(document).on('change', '[data-grav-elements] select', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const value = target.val();
|
||||
const id = target.closest('[data-grav-elements]').data('gravElements');
|
||||
|
||||
$(`[id^="${id}_"]`).css('display', 'none');
|
||||
$(`[id="${id}__${value}"]`).css('display', 'inherit');
|
||||
});
|
||||
|
||||
$('[data-grav-elements] select').trigger('change');
|
||||
144
user/plugins/admin/themes/grav/app/forms/fields/filepicker.js
Normal file
144
user/plugins/admin/themes/grav/app/forms/fields/filepicker.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import $ from 'jquery';
|
||||
import { config, uri_params } from 'grav-config';
|
||||
import request from '../../utils/request';
|
||||
|
||||
// const insertTextAt = (string, index, text) => [string.slice(0, index), text, string.slice(index)].join('');
|
||||
|
||||
export default class FilePickerField {
|
||||
|
||||
constructor(options) {
|
||||
this.items = $();
|
||||
this.options = Object.assign({}, this.defaults, options);
|
||||
|
||||
$('[data-grav-filepicker]').each((index, element) => this.addItem(element));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let fields = $(target).find('[data-grav-filepicker]');
|
||||
if (!fields.length) { return; }
|
||||
|
||||
fields.each((index, field) => {
|
||||
field = $(field);
|
||||
if (!~this.items.index(field)) {
|
||||
this.addItem(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addItem(element) {
|
||||
element = $(element);
|
||||
this.items = this.items.add(element);
|
||||
|
||||
let tag = element.prop('tagName').toLowerCase();
|
||||
let isInput = tag === 'input' || tag === 'select';
|
||||
|
||||
let field = (isInput ? element : element.find('input, select'));
|
||||
|
||||
let folder = '';
|
||||
let thumbs = {};
|
||||
|
||||
let onDemand = field.closest('[data-ondemand]').length > 0;
|
||||
|
||||
if (!field.length || field.get(0).selectize) { return; }
|
||||
|
||||
let getData = function getData(field, callback, mode = 'all') {
|
||||
let url = config.current_url + `.json/task${config.param_sep}getFilesInFolder`;
|
||||
let parent = field.closest('[data-grav-filepicker]');
|
||||
let name = parent.data('name');
|
||||
let value = parent.data('value');
|
||||
let params = JSON.stringify(uri_params || '{}');
|
||||
|
||||
request(url, {
|
||||
method: 'post',
|
||||
body: { name, params }
|
||||
}, (response) => {
|
||||
if (typeof response.files === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = [];
|
||||
thumbs = response.thumbs || {};
|
||||
|
||||
for (let i = 0; i < response.files.length; i++) {
|
||||
if (mode === 'selected' && response.files[i] !== value) { continue; }
|
||||
data.push({ 'name': response.files[i], 'status': 'available', thumb: thumbs[response.files[i]] || '' });
|
||||
}
|
||||
|
||||
for (let i = 0; i < response.pending.length; i++) {
|
||||
if (mode === 'selected' && response.pending[i] !== value) { continue; }
|
||||
data.push({ 'name': response.pending[i], 'status': 'pending', thumb: thumbs[response.pending[i]] || '' });
|
||||
}
|
||||
|
||||
folder = response.folder;
|
||||
callback(data, value);
|
||||
});
|
||||
};
|
||||
|
||||
let imagesPreview = field.closest('[data-preview-images]').length > 0;
|
||||
let selectedIsRendered = false;
|
||||
|
||||
let renderOption = function renderOption(item, escape) {
|
||||
let image = '';
|
||||
if (imagesPreview && folder && (!item.status || item.status === 'available') && item.name.match(/\.(jpg|jpeg|png|gif|webp)$/i)) {
|
||||
// const fallback2x = insertTextAt(`${config.base_url_relative}/../${folder}/${item.name}`, -4, '@2x');
|
||||
// const fallback3x = insertTextAt(`${config.base_url_relative}/../${folder}/${item.name}`, -4, '@3x');
|
||||
const source = thumbs[item.name] || `${config.base_url_relative}/../${folder}/${item.name}`;
|
||||
|
||||
// onerror="if(this.src==='${fallback2x}'){this.src='${fallback3x}';}else{this.src='${fallback2x}'}"
|
||||
image = `<img class="filepicker-field-image" src="${source}" />`;
|
||||
}
|
||||
|
||||
return `<div>
|
||||
<span class="title">
|
||||
${image} <span class="name filepicker-field-name">${escape(item.name)}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
};
|
||||
|
||||
field.selectize({
|
||||
plugins: ['required-fix'],
|
||||
valueField: 'name',
|
||||
labelField: 'name',
|
||||
searchField: 'name',
|
||||
optgroups: [
|
||||
{$order: 1, value: 'pending', label: 'Pending'},
|
||||
{$order: 2, value: 'available', label: 'Available'}
|
||||
],
|
||||
optgroupField: 'status',
|
||||
// lockOptgroupOrder: true,
|
||||
create: false,
|
||||
preload: false, // 'focus',
|
||||
render: {
|
||||
option: function(item, escape) {
|
||||
return renderOption(item, escape);
|
||||
},
|
||||
|
||||
item: function(item, escape) {
|
||||
return renderOption(item, escape);
|
||||
}
|
||||
},
|
||||
|
||||
onInitialize: function() {
|
||||
if (!onDemand) {
|
||||
this.load((callback) => getData(field, (data) => callback(data), 'selected'));
|
||||
}
|
||||
},
|
||||
|
||||
onLoad: function(/* data */) {
|
||||
if (!selectedIsRendered) {
|
||||
let name = this.getValue();
|
||||
this.updateOption(name, { name });
|
||||
|
||||
selectedIsRendered = true;
|
||||
}
|
||||
},
|
||||
|
||||
onFocus: function() {
|
||||
this.load((callback) => getData(field, (data) => callback(data)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new FilePickerField();
|
||||
402
user/plugins/admin/themes/grav/app/forms/fields/files.js
Normal file
402
user/plugins/admin/themes/grav/app/forms/fields/files.js
Normal file
@@ -0,0 +1,402 @@
|
||||
import $ from 'jquery';
|
||||
import Dropzone from 'dropzone';
|
||||
// import EXIF from 'exif-js';
|
||||
import request from '../../utils/request';
|
||||
import { config, translations } from 'grav-config';
|
||||
|
||||
// translations
|
||||
const Dictionary = {
|
||||
dictCancelUpload: translations.PLUGIN_ADMIN.DROPZONE_CANCEL_UPLOAD,
|
||||
dictCancelUploadConfirmation: translations.PLUGIN_ADMIN.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
|
||||
dictDefaultMessage: translations.PLUGIN_ADMIN.DROPZONE_DEFAULT_MESSAGE,
|
||||
dictFallbackMessage: translations.PLUGIN_ADMIN.DROPZONE_FALLBACK_MESSAGE,
|
||||
dictFallbackText: translations.PLUGIN_ADMIN.DROPZONE_FALLBACK_TEXT,
|
||||
dictFileTooBig: translations.PLUGIN_ADMIN.DROPZONE_FILE_TOO_BIG,
|
||||
dictInvalidFileType: translations.PLUGIN_ADMIN.DROPZONE_INVALID_FILE_TYPE,
|
||||
dictMaxFilesExceeded: translations.PLUGIN_ADMIN.DROPZONE_MAX_FILES_EXCEEDED,
|
||||
dictRemoveFile: translations.PLUGIN_ADMIN.DROPZONE_REMOVE_FILE,
|
||||
dictResponseError: translations.PLUGIN_ADMIN.DROPZONE_RESPONSE_ERROR
|
||||
};
|
||||
|
||||
Dropzone.autoDiscover = false;
|
||||
Dropzone.options.gravPageDropzone = {};
|
||||
Dropzone.confirm = (question, accepted, rejected) => {
|
||||
let doc = $(document);
|
||||
let modalSelector = '[data-remodal-id="delete-media"]';
|
||||
|
||||
let removeEvents = () => {
|
||||
doc.off('confirmation', modalSelector, accept);
|
||||
doc.off('cancellation', modalSelector, reject);
|
||||
|
||||
$(modalSelector).find('.remodal-confirm').removeClass('pointer-events-disabled');
|
||||
};
|
||||
|
||||
let accept = () => {
|
||||
accepted && accepted();
|
||||
removeEvents();
|
||||
};
|
||||
|
||||
let reject = () => {
|
||||
rejected && rejected();
|
||||
removeEvents();
|
||||
};
|
||||
|
||||
$.remodal.lookup[$(modalSelector).data('remodal')].open();
|
||||
doc.on('confirmation', modalSelector, accept);
|
||||
doc.on('cancellation', modalSelector, reject);
|
||||
};
|
||||
|
||||
const DropzoneMediaConfig = {
|
||||
timeout: 0,
|
||||
thumbnailWidth: 200,
|
||||
thumbnailHeight: 150,
|
||||
addRemoveLinks: false,
|
||||
dictDefaultMessage: translations.PLUGIN_ADMIN.DROP_FILES_HERE_TO_UPLOAD.replace(/</g, '<').replace(/>/g, '>'),
|
||||
dictRemoveFileConfirmation: '[placeholder]',
|
||||
previewTemplate: `
|
||||
<div class="dz-preview dz-file-preview dz-no-editor">
|
||||
<div class="dz-details">
|
||||
<div class="dz-filename"><span data-dz-name></span></div>
|
||||
<div class="dz-size" data-dz-size></div>
|
||||
<img data-dz-thumbnail />
|
||||
</div>
|
||||
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
|
||||
<div class="dz-success-mark"><span>✔</span></div>
|
||||
<div class="dz-error-mark"><span>✘</span></div>
|
||||
<div class="dz-error-message"><span data-dz-errormessage></span></div>
|
||||
<a class="dz-unset" title="${translations.PLUGIN_ADMIN.UNSET}" href="#" data-dz-unset>${translations.PLUGIN_ADMIN.UNSET}</a>
|
||||
<a class="dz-remove" title="${translations.PLUGIN_ADMIN.DELETE}" href="javascript:undefined;" data-dz-remove>${translations.PLUGIN_ADMIN.DELETE}</a>
|
||||
<a class="dz-metadata" title="${translations.PLUGIN_ADMIN.METADATA}" href="#" target="_blank" data-dz-metadata>${translations.PLUGIN_ADMIN.METADATA}</a>
|
||||
<a class="dz-view" title="${translations.PLUGIN_ADMIN.VIEW}" href="#" target="_blank" data-dz-view>${translations.PLUGIN_ADMIN.VIEW}</a>
|
||||
</div>`.trim()
|
||||
};
|
||||
|
||||
// global.EXIF = EXIF;
|
||||
|
||||
const ACCEPT_FUNC = function(file, done, settings) {
|
||||
const resolution = settings.resolution;
|
||||
if (!resolution) return done();
|
||||
|
||||
const reader = new FileReader();
|
||||
let error = '';
|
||||
const hasMin = (resolution.min && (resolution.min.width || resolution.min.height));
|
||||
const hasMax = (resolution.max && (resolution.max.width || resolution.max.height));
|
||||
if (hasMin || (!(settings.resizeWidth || settings.resizeHeight) && hasMax)) {
|
||||
reader.onload = function(event) {
|
||||
if (!/image\//.test(file.type)) {
|
||||
done();
|
||||
return;
|
||||
}
|
||||
|
||||
const image = new Image();
|
||||
image.src = event.target.result;
|
||||
image.onerror = function() {
|
||||
done(translations.PLUGIN_ADMIN.FILE_ERROR_UPLOAD);
|
||||
};
|
||||
image.onload = function() {
|
||||
if (resolution.min) {
|
||||
Object.keys(resolution.min).forEach((attr) => {
|
||||
if (resolution.min[attr] && this[attr] < resolution.min[attr]) {
|
||||
error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!(settings.resizeWidth || settings.resizeHeight)) {
|
||||
if (resolution.max) {
|
||||
Object.keys(resolution.max).forEach((attr) => {
|
||||
if (resolution.max[attr] && this[attr] > resolution.max[attr]) {
|
||||
error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(image.src); // release memory
|
||||
return error ? done(error) : done();
|
||||
};
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
return error ? done(error) : done();
|
||||
}
|
||||
};
|
||||
|
||||
export default class FilesField {
|
||||
constructor({ container = '.dropzone.files-upload', options = {} } = {}) {
|
||||
this.container = $(container);
|
||||
if (!this.container.length) { return; }
|
||||
|
||||
this.urls = {};
|
||||
this.customPost = this.container.data('filePostAdd') || {};
|
||||
this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
|
||||
klass: this,
|
||||
url: this.container.data('file-url-add') || config.current_url,
|
||||
acceptedFiles: this.container.data('media-types'),
|
||||
init: this.initDropzone
|
||||
}, this.container.data('dropzone-options'), options);
|
||||
|
||||
this.options = Object.assign({}, this.options, {
|
||||
accept: function(file, done) { ACCEPT_FUNC(file, done, this.options); }
|
||||
});
|
||||
|
||||
this.dropzone = new Dropzone(container, this.options);
|
||||
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
|
||||
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
|
||||
this.dropzone.on('addedfile', this.onDropzoneAddedFile.bind(this));
|
||||
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
|
||||
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
|
||||
this.dropzone.on('error', this.onDropzoneError.bind(this));
|
||||
|
||||
this.container.on('mouseenter', '[data-dz-view]', (e) => {
|
||||
const value = JSON.parse(this.container.find('[name][type="hidden"]').val() || '{}');
|
||||
const target = $(e.currentTarget);
|
||||
const file = target.parent('.dz-preview').find('.dz-filename');
|
||||
const filename = encodeURI(file.text());
|
||||
|
||||
const URL = Object.keys(value).filter((key) => value[key].name === filename).shift();
|
||||
target.attr('href', `${config.base_url_simple}/${URL}`);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
initDropzone() {
|
||||
let files = this.options.klass.container.find('[data-file]');
|
||||
let dropzone = this;
|
||||
if (!files.length) { return; }
|
||||
|
||||
files.each((index, file) => {
|
||||
file = $(file);
|
||||
let data = file.data('file');
|
||||
let mock = {
|
||||
name: data.name,
|
||||
size: data.size,
|
||||
type: data.type,
|
||||
status: Dropzone.ADDED,
|
||||
accepted: true,
|
||||
url: this.options.url,
|
||||
removeUrl: data.remove
|
||||
};
|
||||
|
||||
dropzone.files.push(mock);
|
||||
dropzone.options.addedfile.call(dropzone, mock);
|
||||
if (mock.type.match(/^image\//)) {
|
||||
dropzone.options.thumbnail.call(dropzone, mock, data.path);
|
||||
dropzone.createThumbnailFromUrl(mock, data.path);
|
||||
}
|
||||
|
||||
file.remove();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
getURI() {
|
||||
return this.container.data('mediaUri') || '';
|
||||
}
|
||||
|
||||
onDropzoneSending(file, xhr, formData) {
|
||||
if (Object.keys(this.customPost).length) {
|
||||
Object.keys(this.customPost).forEach((key) => {
|
||||
formData.append(key, this.customPost[key]);
|
||||
});
|
||||
} else {
|
||||
formData.append('name', this.options.dotNotation);
|
||||
formData.append('task', 'filesupload');
|
||||
formData.append('uri', this.getURI());
|
||||
}
|
||||
|
||||
formData.append('admin-nonce', config.admin_nonce);
|
||||
}
|
||||
|
||||
onDropzoneSuccess(file, response, xhr) {
|
||||
response = typeof response === 'string' ? JSON.parse(response) : response;
|
||||
if (this.options.reloadPage) {
|
||||
global.location.reload();
|
||||
}
|
||||
|
||||
// store params for removing file from session before it gets saved
|
||||
if (response.session) {
|
||||
file.sessionParams = response.session;
|
||||
file.removeUrl = this.options.url;
|
||||
|
||||
// Touch field value to force a mutation detection
|
||||
const input = this.container.find('[name][type="hidden"]');
|
||||
const value = input.val();
|
||||
input.val(value + ' ');
|
||||
}
|
||||
|
||||
return this.handleError({
|
||||
file,
|
||||
data: response,
|
||||
mode: 'removeFile',
|
||||
msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_UPLOAD} <strong>{{fileName}}</strong></p>
|
||||
<pre>${response.message}</pre>`
|
||||
});
|
||||
}
|
||||
|
||||
onDropzoneComplete(file) {
|
||||
if (!file.accepted && !file.rejected) {
|
||||
let data = {
|
||||
status: 'error',
|
||||
message: `${translations.PLUGIN_ADMIN.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
|
||||
};
|
||||
|
||||
return this.handleError({
|
||||
file,
|
||||
data,
|
||||
mode: 'removeFile',
|
||||
msg: `<p>${translations.PLUGIN_ADMIN.FILE_ERROR_ADD} <strong>{{fileName}}</strong></p>
|
||||
<pre>${data.message}</pre>`
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.reloadPage) {
|
||||
global.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
b64_to_utf8(str) {
|
||||
str = str.replace(/\s/g, '');
|
||||
return decodeURIComponent(escape(window.atob(str)));
|
||||
}
|
||||
|
||||
onDropzoneAddedFile(file, ...extra) {
|
||||
return this.dropzone.options.addedfile(file);
|
||||
}
|
||||
|
||||
onDropzoneRemovedFile(file, ...extra) {
|
||||
if (!file.accepted || file.rejected) { return; }
|
||||
let url = file.removeUrl || this.urls.delete || this.options.url;
|
||||
let path = (url || '').match(/path:(.*)\//);
|
||||
let body = { filename: file.name, uri: this.getURI() };
|
||||
|
||||
if (file.sessionParams) {
|
||||
body.task = 'filessessionremove';
|
||||
body.session = file.sessionParams;
|
||||
}
|
||||
|
||||
const customPost = this.container.data('filePostRemove') || {};
|
||||
if (Object.keys(customPost).length) {
|
||||
body = {};
|
||||
Object.keys(customPost).forEach((key) => {
|
||||
body[key] = customPost[key];
|
||||
});
|
||||
}
|
||||
|
||||
body['filename'] = file.name;
|
||||
body['admin-nonce'] = config.admin_nonce;
|
||||
|
||||
request(url, { method: 'post', body }, () => {
|
||||
if (!path) { return; }
|
||||
|
||||
path = this.b64_to_utf8(path[1]);
|
||||
let input = this.container.find('[name][type="hidden"]');
|
||||
let data = JSON.parse(input.val() || '{}');
|
||||
delete data[path];
|
||||
input.val(JSON.stringify(data));
|
||||
});
|
||||
}
|
||||
|
||||
onDropzoneError(file, response, xhr) {
|
||||
let message = xhr ? response.error.message : response;
|
||||
$(file.previewElement).find('[data-dz-errormessage]').html(message);
|
||||
|
||||
return this.handleError({
|
||||
file,
|
||||
data: { status: 'error' },
|
||||
msg: `<pre>${message}</pre>`
|
||||
});
|
||||
}
|
||||
|
||||
handleError(options) {
|
||||
let { file, data, mode, msg } = options;
|
||||
if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
|
||||
|
||||
switch (mode) {
|
||||
case 'addBack':
|
||||
if (file instanceof File) {
|
||||
this.dropzone.addFile.call(this.dropzone, file);
|
||||
} else {
|
||||
this.dropzone.files.push(file);
|
||||
this.dropzone.options.addedfile.call(this.dropzone, file);
|
||||
this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'removeFile':
|
||||
default:
|
||||
if (~this.dropzone.files.indexOf(file)) {
|
||||
file.rejected = true;
|
||||
this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
let modal = $('[data-remodal-id="generic"]');
|
||||
const cleanName = file.name.replace('<', '<').replace('>', '>');
|
||||
|
||||
modal.find('.error-content').html(msg.replace('{{fileName}}', cleanName));
|
||||
$.remodal.lookup[modal.data('remodal')].open();
|
||||
}
|
||||
}
|
||||
|
||||
export function UriToMarkdown(uri) {
|
||||
uri = uri.replace(/@3x|@2x|@1x/, '');
|
||||
uri = uri.replace(/\(/g, '%28');
|
||||
uri = uri.replace(/\)/g, '%29');
|
||||
|
||||
const title = uri.split('.').slice(0, -1).join('.');
|
||||
|
||||
return uri.match(/\.(jpe?g|png|gif|svg|webp|mp4|webm|ogv|mov)$/i) ? `` : `[${decodeURI(uri)}](${uri})`;
|
||||
}
|
||||
|
||||
let instances = [];
|
||||
let cache = $();
|
||||
const onAddedNodes = (event, target/* , record, instance */) => {
|
||||
let files = $(target).find('.dropzone.files-upload');
|
||||
if (!files.length) { return; }
|
||||
|
||||
files.each((index, file) => {
|
||||
file = $(file);
|
||||
if (!~cache.index(file)) {
|
||||
addNode(file);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addNode = (container) => {
|
||||
container = $(container);
|
||||
let input = container.find('input[type="file"]');
|
||||
let settings = container.data('grav-file-settings') || {};
|
||||
|
||||
if (settings.accept && ~settings.accept.indexOf('*')) {
|
||||
settings.accept = [''];
|
||||
}
|
||||
|
||||
let options = {
|
||||
url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
|
||||
paramName: settings.paramName || 'file',
|
||||
dotNotation: settings.name || 'file',
|
||||
acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
|
||||
maxFilesize: typeof settings.filesize !== 'undefined' ? settings.filesize : 256,
|
||||
maxFiles: settings.limit || null,
|
||||
resizeWidth: settings.resizeWidth || null,
|
||||
resizeHeight: settings.resizeHeight || null,
|
||||
resizeQuality: settings.resizeQuality || null,
|
||||
resolution: settings.resolution || null,
|
||||
accept: function(file, done) { ACCEPT_FUNC(file, done, settings); }
|
||||
};
|
||||
|
||||
cache = cache.add(container);
|
||||
container = container[0];
|
||||
instances.push(new FilesField({ container, options }));
|
||||
};
|
||||
|
||||
export let Instance = (() => {
|
||||
$('.dropzone.files-upload').each((i, container) => addNode(container));
|
||||
$('body').on('mutation._grav', onAddedNodes);
|
||||
|
||||
return instances;
|
||||
})();
|
||||
20
user/plugins/admin/themes/grav/app/forms/fields/folder.js
Normal file
20
user/plugins/admin/themes/grav/app/forms/fields/folder.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
const Regenerate = (field = '[name="data[folder]"]') => {
|
||||
const element = $(field);
|
||||
const title = $('[name="data[header][title]"]');
|
||||
const slug = $.slugify(title.val(), {custom: {"'": ''}});
|
||||
|
||||
element.addClass('highlight').val(slug);
|
||||
|
||||
setTimeout(() => element.removeClass('highlight'), 500);
|
||||
};
|
||||
|
||||
$(document).on('click', '[data-regenerate]', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const field = $(target.data('regenerate'));
|
||||
|
||||
Regenerate(field);
|
||||
});
|
||||
|
||||
export default Regenerate;
|
||||
298
user/plugins/admin/themes/grav/app/forms/fields/iconpicker.js
Normal file
298
user/plugins/admin/themes/grav/app/forms/fields/iconpicker.js
Normal file
@@ -0,0 +1,298 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
/* Icon Picker by QueryLoop
|
||||
* Author: @eliorivero
|
||||
* URL: http://queryloop.com/
|
||||
* License: GPLv2
|
||||
*/
|
||||
|
||||
var defaults = {
|
||||
'mode': 'dialog', // show overlay 'dialog' panel or slide down 'inline' panel
|
||||
'closeOnPick': true, // whether to close panel after picking or 'no'
|
||||
'save': 'class', // save icon 'class' or 'code'
|
||||
'size': '',
|
||||
'classes': {
|
||||
'launcher': '', // extra classes for launcher buttons
|
||||
'clear': 'remove-times', // extra classes for button that removes preview and clears field
|
||||
'highlight': '', // extra classes when highlighting an icon
|
||||
'close': '' // extra classes for close button
|
||||
},
|
||||
'iconSets': { // example data structure. Used to specify which launchers will be created
|
||||
'genericon': 'Genericon', // create a launcher to pick genericon icons
|
||||
'fa': 'FontAwesome' // create a launcher to pick fontawesome icons
|
||||
}
|
||||
};
|
||||
|
||||
class QL_Icon_Picker {
|
||||
|
||||
constructor(element, options) {
|
||||
this.iconSet = '';
|
||||
this.iconSetName = '';
|
||||
this.$field = '';
|
||||
this.element = element;
|
||||
this.settings = $.extend({}, defaults, options);
|
||||
this._defaults = defaults;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
var $brick = $(this.element);
|
||||
var pickerId = $brick.data('pickerid');
|
||||
var $preview = $('<div class="icon-preview icon-preview-' + pickerId + '" />');
|
||||
|
||||
this.$field = $brick.find('input');
|
||||
|
||||
// Add preview area
|
||||
this.makePreview($brick, pickerId, $preview);
|
||||
|
||||
// Make button to clear field and remove preview
|
||||
this.makeClear(pickerId, $preview);
|
||||
|
||||
// Make buttons that open the panel of icons
|
||||
this.makeLaunchers($brick, pickerId);
|
||||
|
||||
// Prepare display styles, inline and dialog
|
||||
this.makeDisplay($brick);
|
||||
}
|
||||
|
||||
makePreview($brick, pickerId, $preview) {
|
||||
var $icon = $('<i />');
|
||||
var iconValue = this.$field.val();
|
||||
|
||||
$preview.prependTo($brick);
|
||||
$icon.prependTo($preview);
|
||||
if (iconValue !== '') {
|
||||
$preview.addClass('icon-preview-on');
|
||||
$icon.addClass(iconValue);
|
||||
}
|
||||
}
|
||||
|
||||
makeClear(pickerId, $preview) {
|
||||
var base = this;
|
||||
var $clear = $('<a class="remove-icon ' + base.settings.classes.clear + '" />');
|
||||
|
||||
// Hide button to remove icon and preview and append it to preview area
|
||||
$clear.hide().prependTo($preview);
|
||||
// If there's a icon saved in the field, show remove icon button
|
||||
if (base.$field.val() !== '') {
|
||||
$clear.show();
|
||||
}
|
||||
|
||||
$preview.on('click', '.remove-icon', function(e) {
|
||||
e.preventDefault();
|
||||
base.$field.val('');
|
||||
$preview.removeClass('icon-preview-on').find('i').removeClass();
|
||||
$(this).hide();
|
||||
});
|
||||
}
|
||||
|
||||
makeDisplay($brick) {
|
||||
var base = this;
|
||||
var close = base.settings.classes.close;
|
||||
var $body = $('body');
|
||||
|
||||
var $close = $('<a href="#" class="icon-picker-close"/>');
|
||||
|
||||
if (base.settings.mode === 'inline') {
|
||||
$brick.find('.icon-set').append($close).removeClass('dialog').addClass('ip-inline ' + base.settings.size).parent().addClass('icon-set-wrap');
|
||||
} else if (base.settings.mode === 'dialog') {
|
||||
$('.icon-set').addClass('dialog ' + base.settings.size);
|
||||
if ($('.icon-picker-overlay').length <= 0) {
|
||||
$body.append('<div class="icon-picker-overlay"/>').append($close);
|
||||
}
|
||||
}
|
||||
$body
|
||||
.on('click', '.icon-picker-close, .icon-picker-overlay', function(e) {
|
||||
e.preventDefault();
|
||||
base.closePicker($brick, $(base.iconSet), base.settings.mode);
|
||||
})
|
||||
.on('mouseenter mouseleave', '.icon-picker-close', function(e) {
|
||||
if (e.type === 'mouseenter') {
|
||||
$(this).addClass(close);
|
||||
} else {
|
||||
$(this).removeClass(close);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
makeLaunchers($brick) {
|
||||
var base = this;
|
||||
var dataIconSets = $brick.data('iconsets');
|
||||
var iconSet;
|
||||
|
||||
if (typeof dataIconSets === 'undefined') {
|
||||
dataIconSets = base.settings.iconSets;
|
||||
}
|
||||
for (iconSet in dataIconSets) {
|
||||
if (dataIconSets.hasOwnProperty(iconSet)) {
|
||||
$brick.append('<a class="launch-icons button ' + base.settings.classes.launcher + '" data-icons="' + iconSet + '">' + dataIconSets[iconSet] + '</a>');
|
||||
}
|
||||
}
|
||||
|
||||
$brick.find('.launch-icons').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
var $self = $(this);
|
||||
var theseIcons = $self.data('icons');
|
||||
|
||||
base.iconSetName = theseIcons;
|
||||
base.iconSet = '.' + theseIcons + '-set';
|
||||
|
||||
// Initialize picker
|
||||
base.iconPick($brick);
|
||||
|
||||
// Show icon picker
|
||||
base.showPicker($brick, $(base.iconSet), base.settings.mode);
|
||||
});
|
||||
}
|
||||
|
||||
iconPick($brick) {
|
||||
var base = this;
|
||||
var highlight = 'icon-highlight ' + base.settings.classes.highlight;
|
||||
|
||||
$(base.iconSet).on('click', 'li', function(e) {
|
||||
e.preventDefault();
|
||||
var $icon = $(this);
|
||||
var icon = $icon.data(base.settings.save);
|
||||
|
||||
// Mark as selected
|
||||
$('.icon-selected').removeClass('icon-selected');
|
||||
$icon.addClass('icon-selected');
|
||||
if (base.$field.data('format') === 'short') {
|
||||
icon = icon.slice(6);
|
||||
}
|
||||
|
||||
// Save icon value to field
|
||||
base.$field.val(icon);
|
||||
|
||||
// Close icon picker
|
||||
if (base.settings.closeOnPick) {
|
||||
base.closePicker($brick, $icon.closest(base.iconSet), base.settings.mode);
|
||||
}
|
||||
|
||||
// Set preview
|
||||
base.setPreview($icon.data('class'));
|
||||
|
||||
// Broadcast event passing the selected icon.
|
||||
$('body').trigger('iconselected.queryloop', icon);
|
||||
});
|
||||
$(base.iconSet).on('mouseenter mouseleave', 'li', function(e) {
|
||||
if (e.type === 'mouseenter') {
|
||||
$(this).addClass(highlight);
|
||||
} else {
|
||||
$(this).removeClass(highlight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showPicker($brick, $icons, mode) {
|
||||
if (mode === 'inline') {
|
||||
$('.icon-set').removeClass('ip-inline-open');
|
||||
$brick.find($icons).toggleClass('ip-inline-open');
|
||||
} else if (mode === 'dialog') {
|
||||
$brick.find('.icon-picker-close').addClass('make-visible');
|
||||
$brick.find('.icon-picker-overlay').addClass('make-visible');
|
||||
$icons.addClass('dialog-open');
|
||||
}
|
||||
|
||||
$icons.find('.icon-selected').removeClass('icon-selected');
|
||||
var selectedIcon = this.$field.val().replace(' ', '.');
|
||||
if (selectedIcon !== '') {
|
||||
if (this.settings.save === 'class') {
|
||||
$icons.find('.' + selectedIcon).addClass('icon-selected');
|
||||
} else {
|
||||
$icons.find('[data-code="' + selectedIcon + '"]').addClass('icon-selected');
|
||||
}
|
||||
}
|
||||
// Broadcast event when the picker is shown passing the picker mode.
|
||||
$('body').trigger('iconpickershow.queryloop', mode);
|
||||
}
|
||||
|
||||
closePicker($brick, $icons, mode) {
|
||||
// Remove event so they don't fire from a different picker
|
||||
$(this.iconSet).off('click', 'li');
|
||||
|
||||
if (mode === 'inline') {
|
||||
$brick.find($icons).removeClass('ip-inline-open');
|
||||
} else if (mode === 'dialog') {
|
||||
$('.icon-picker-close, .icon-picker-overlay').removeClass('make-visible');
|
||||
}
|
||||
// Broadcast event when the picker is closed passing the picker mode.
|
||||
$('body').trigger('iconpickerclose.queryloop', mode);
|
||||
$('.icon-set').removeClass('dialog-open');
|
||||
}
|
||||
|
||||
setPreview(preview) {
|
||||
var $preview = $(this.element).find('.icon-preview');
|
||||
|
||||
$preview.addClass('icon-preview-on').find('i').removeClass()
|
||||
.addClass(this.iconSetName)
|
||||
.addClass(preview);
|
||||
$preview.find('a').show();
|
||||
}
|
||||
}
|
||||
|
||||
/* Grav */
|
||||
// extend $ with 3rd party QL Icon Picker
|
||||
$.fn.qlIconPicker = function(options) {
|
||||
this.each(function() {
|
||||
if (!$.data(this, 'plugin_qlIconPicker')) {
|
||||
$.data(this, 'plugin_qlIconPicker', new QL_Icon_Picker(this, options));
|
||||
}
|
||||
});
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
export default class IconpickerField {
|
||||
|
||||
constructor(options) {
|
||||
this.items = $();
|
||||
this.options = Object.assign({}, this.defaults, options);
|
||||
|
||||
$('[data-grav-iconpicker]').each((index, element) => this.addItem(element));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let fields = $(target).find('[data-grav-iconpicker]');
|
||||
if (!fields.length) { return; }
|
||||
|
||||
fields.each((index, field) => {
|
||||
field = $(field);
|
||||
if (!~this.items.index(field)) {
|
||||
this.addItem(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addItem(element) {
|
||||
element = $(element);
|
||||
this.items = this.items.add(element);
|
||||
element.find('.icon-picker').qlIconPicker({
|
||||
'save': 'class'
|
||||
});
|
||||
|
||||
// hack to remove extra icon sets that are just copies
|
||||
$('.icon-set:not(:first)').remove();
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new IconpickerField();
|
||||
|
||||
// Fix to close the dialog when clicking outside
|
||||
$(document).on('click', (event) => {
|
||||
const target = $(event.target);
|
||||
const match = '.icon-set.dialog-open, .launch-icons[data-icons]';
|
||||
if (!target.is(match) && !target.closest(match).length) {
|
||||
const dialogs = $('.icon-set.dialog-open');
|
||||
|
||||
// skip if there's no dialog open
|
||||
if (dialogs.length) {
|
||||
dialogs.each((index, dialog) => {
|
||||
const picker = $(dialog).siblings('.icon-picker');
|
||||
const data = picker.data('plugin_qlIconPicker');
|
||||
data.closePicker(picker, $(data.iconSet), data.settings.mode);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
if (document.querySelector('#pages-filters')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const wrapper = event.target.closest('.checkboxes.indeterminate');
|
||||
|
||||
if (wrapper) {
|
||||
event.preventDefault();
|
||||
const checkbox = wrapper.querySelector('input[type="checkbox"]:not([disabled])');
|
||||
const checkStatus = wrapper.dataset._checkStatus;
|
||||
wrapper.classList.remove('status-checked', 'status-unchecked', 'status-indeterminate');
|
||||
|
||||
switch (checkStatus) {
|
||||
// checked, going indeterminate
|
||||
case '1':
|
||||
wrapper.dataset._checkStatus = '2';
|
||||
checkbox.indeterminate = true;
|
||||
checkbox.checked = false;
|
||||
checkbox.value = 0;
|
||||
wrapper.classList.add('status-indeterminate');
|
||||
break;
|
||||
|
||||
// indeterminate, going unchecked
|
||||
case '2':
|
||||
wrapper.dataset._checkStatus = '0';
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = false;
|
||||
checkbox.value = '';
|
||||
wrapper.classList.add('status-unchecked');
|
||||
break;
|
||||
|
||||
// unchecked, going checked
|
||||
case '0':
|
||||
default:
|
||||
wrapper.dataset._checkStatus = '1';
|
||||
checkbox.indeterminate = false;
|
||||
checkbox.checked = true;
|
||||
checkbox.value = 1;
|
||||
wrapper.classList.add('status-checked');
|
||||
break;
|
||||
}
|
||||
|
||||
// const input = new CustomEvent('input', { detail: { target: checkbox }});
|
||||
// document.dispatchEvent(input);
|
||||
$(checkbox).trigger('input');
|
||||
}
|
||||
});
|
||||
|
||||
(document.querySelectorAll('input[type="checkbox"][indeterminate="true"]') || []).forEach((input) => { input.indeterminate = true; });
|
||||
76
user/plugins/admin/themes/grav/app/forms/fields/index.js
Normal file
76
user/plugins/admin/themes/grav/app/forms/fields/index.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import FilepickerField, { Instance as FilepickerFieldInstance } from './filepicker';
|
||||
import SelectizeField, { Instance as SelectizeFieldInstance } from './selectize';
|
||||
import ArrayField, { Instance as ArrayFieldInstance } from './array';
|
||||
import CollectionsField, { Instance as CollectionsFieldInstance } from './collections';
|
||||
import DateTimeField, { Instance as DateTimeFieldInstance } from './datetime';
|
||||
import EditorField, { Instance as EditorFieldInstance } from './editor';
|
||||
import ColorpickerField, { Instance as ColorpickerFieldInstance } from './colorpicker';
|
||||
import FilesField, { Instance as FilesFieldInstance } from './files';
|
||||
import FolderFieldInstance from './folder';
|
||||
import SelectUniqueField, { Instance as SelectUniqueInstance } from './selectunique';
|
||||
import IconpickerField, { Instance as IconpickerInstance } from './iconpicker';
|
||||
import CronField, { Instance as CronFieldInstance } from './cron';
|
||||
import ParentsField, { Instances as ParentsFieldInstance } from './parents';
|
||||
|
||||
import './acl-picker';
|
||||
import './permissions';
|
||||
import './indeterminate';
|
||||
import './mediapicker';
|
||||
import './multilevel';
|
||||
import './text';
|
||||
import './range';
|
||||
import './elements';
|
||||
|
||||
export default {
|
||||
FilepickerField: {
|
||||
FilepickerField,
|
||||
Instance: FilepickerFieldInstance
|
||||
},
|
||||
SelectizeField: {
|
||||
SelectizeField,
|
||||
Instance: SelectizeFieldInstance
|
||||
},
|
||||
ArrayField: {
|
||||
ArrayField,
|
||||
Instance: ArrayFieldInstance
|
||||
},
|
||||
CollectionsField: {
|
||||
CollectionsField,
|
||||
Instance: CollectionsFieldInstance
|
||||
},
|
||||
DateTimeField: {
|
||||
DateTimeField,
|
||||
Instance: DateTimeFieldInstance
|
||||
},
|
||||
EditorField: {
|
||||
EditorField,
|
||||
Instance: EditorFieldInstance
|
||||
},
|
||||
ColorpickerField: {
|
||||
ColorpickerField,
|
||||
Instance: ColorpickerFieldInstance
|
||||
},
|
||||
FilesField: {
|
||||
FilesField,
|
||||
Instance: FilesFieldInstance
|
||||
},
|
||||
FolderField: {
|
||||
Regenerate: FolderFieldInstance
|
||||
},
|
||||
SelectUniqueField: {
|
||||
SelectUniqueField,
|
||||
Instance: SelectUniqueInstance
|
||||
},
|
||||
IconpickerField: {
|
||||
IconpickerField,
|
||||
Instance: IconpickerInstance
|
||||
},
|
||||
CronField: {
|
||||
CronField,
|
||||
Instance: CronFieldInstance
|
||||
},
|
||||
ParentsField: {
|
||||
ParentsField,
|
||||
Instance: ParentsFieldInstance
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import $ from 'jquery';
|
||||
import { Instance as pagesTree } from '../../pages/tree';
|
||||
|
||||
$(function() {
|
||||
let modal = '';
|
||||
let body = $('body');
|
||||
|
||||
// Thumb Resizer
|
||||
$(document).on('input change', '.media-container .media-range', function(event) {
|
||||
const target = $(event.currentTarget);
|
||||
const container = target.closest('.remodal');
|
||||
let cards = container.find('.media-container div.card-item');
|
||||
let width = target.val() + 'px';
|
||||
cards.each(function() {
|
||||
$(this).css('width', width);
|
||||
});
|
||||
});
|
||||
|
||||
body.on('click', '[data-mediapicker-modal-trigger]', function(event) {
|
||||
const element = $(event.currentTarget);
|
||||
let modal_identifier = $(this).data('grav-mediapicker-unique-identifier');
|
||||
let modal_element = body.find(`[data-remodal-unique-identifier="${modal_identifier}"]`);
|
||||
modal = $.remodal.lookup[modal_element.data('remodal')];
|
||||
|
||||
if (!modal) {
|
||||
modal_element.remodal();
|
||||
modal = $.remodal.lookup[modal_element.data('remodal')];
|
||||
}
|
||||
|
||||
modal.open();
|
||||
modal.dataField = element.find('input');
|
||||
|
||||
// load all media
|
||||
modal_element.find('.js__files').trigger('fillView');
|
||||
|
||||
setTimeout(() => pagesTree.reload(), 100);
|
||||
});
|
||||
|
||||
/* handle media modal click actions */
|
||||
body.on('click', '[data-remodal-mediapicker] .media-container.in-modal .admin-media-details a', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
let val = $(event.target).parents('.js__media-element').data('file-url');
|
||||
let string = val.replace(/ /g, '%20');
|
||||
|
||||
modal.dataField.val(string);
|
||||
|
||||
modal.close();
|
||||
});
|
||||
});
|
||||
281
user/plugins/admin/themes/grav/app/forms/fields/multilevel.js
Normal file
281
user/plugins/admin/themes/grav/app/forms/fields/multilevel.js
Normal file
@@ -0,0 +1,281 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
$(function() {
|
||||
|
||||
const getField = function getField(level, name) {
|
||||
let levelMargin = level * 50;
|
||||
let top = (level === 0 ? 'top' : '');
|
||||
|
||||
let the_name = 'name="' + name + '"';
|
||||
if (level === 0) {
|
||||
// top
|
||||
the_name = 'data-attr-name="' + name + '"';
|
||||
}
|
||||
|
||||
const marginDir = window.getComputedStyle(document.body).direction === 'ltr' ? 'margin-left' : 'margin-right';
|
||||
|
||||
let field = `
|
||||
<div class="element-wrapper">
|
||||
<div class="form-row array-field-value_only js__multilevel-field ${top}"
|
||||
data-grav-array-type="row">
|
||||
<input
|
||||
type="text"
|
||||
${the_name}
|
||||
placeholder="Enter value"
|
||||
style="${marginDir}: ${levelMargin}px"
|
||||
value="" />
|
||||
|
||||
<span class="fa fa-minus js__remove-item"></span>
|
||||
<span class="fa fa-plus js__add-sibling hidden" data-level="${level}" ></span>
|
||||
<span class="fa fa-plus-circle js__add-children hidden" data-level="${level}"></span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return field;
|
||||
};
|
||||
|
||||
const hasChildInputs = function hasChildInputs($element) {
|
||||
if ($element.attr('name')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getTopItems = function getTopItems(element) {
|
||||
return $(element + ' .js__multilevel-field.top');
|
||||
};
|
||||
|
||||
const refreshControls = function refreshControls(unique_identifier) {
|
||||
let element = '[data-grav-multilevel-field]';
|
||||
if (unique_identifier) {
|
||||
element = '[data-grav-multilevel-field][data-id="' + unique_identifier + '"]';
|
||||
}
|
||||
|
||||
const hideButtons = function hideButtons() {
|
||||
$(element + ' .js__add-sibling').addClass('hidden');
|
||||
$(element + ' .js__add-children').addClass('hidden');
|
||||
};
|
||||
|
||||
const restoreAddSiblingButtons = function restoreAddSiblingButtons() {
|
||||
$(element + ' .children-wrapper').each(function() {
|
||||
let elements = $(this).children();
|
||||
elements.last().each(function() {
|
||||
let field = $(this);
|
||||
if (!$(this).hasClass('js__multilevel-field')) {
|
||||
field = $(this).find('.js__multilevel-field').first();
|
||||
}
|
||||
field.find('.js__add-sibling').removeClass('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
// add sibling to the last top element
|
||||
$(element + ' .js__multilevel-field.top').last().find('.js__add-sibling').removeClass('hidden');
|
||||
};
|
||||
|
||||
const restoreAddChildrenButtons = function restoreAddChildrenButtons() {
|
||||
$(element + ' .js__multilevel-field').each(function() {
|
||||
if ($(this).siblings('.children-wrapper').length === 0 || $(this).siblings('.children-wrapper').find('.js__multilevel-field').length === 0) {
|
||||
$(this).find('.js__add-children').removeClass('hidden');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const preventRemovingLastTopItem = function preventRemovingLastTopItem() {
|
||||
let top_items = getTopItems(element);
|
||||
if (top_items.length === 1) {
|
||||
top_items.first().find('.js__remove-item').addClass('hidden');
|
||||
}
|
||||
};
|
||||
|
||||
hideButtons();
|
||||
restoreAddSiblingButtons();
|
||||
restoreAddChildrenButtons();
|
||||
preventRemovingLastTopItem();
|
||||
};
|
||||
|
||||
const changeAllOccurrencesInTree = function($el, current_name, new_name) {
|
||||
$el.parents('[data-grav-multilevel-field]').find('input').each(function() {
|
||||
let $input = $(this);
|
||||
if ($input.attr('name')) {
|
||||
$input.attr('name', $input.attr('name').replace(current_name, new_name));
|
||||
}
|
||||
if ($input.attr('data-attr-name')) {
|
||||
$input.attr('data-attr-name', $input.attr('data-attr-name').replace(current_name, new_name));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
refreshControls();
|
||||
});
|
||||
|
||||
$(document).on('mouseleave', '[data-grav-multilevel-field]', function(event) {
|
||||
let top_items = getTopItems('[data-id="' + $(this).data('id') + '"]');
|
||||
let has_top_items_without_children = false;
|
||||
let element_content = '';
|
||||
top_items.each(function() {
|
||||
let item = $(this);
|
||||
if ($(item).siblings('.children-wrapper').find('input').length === 0) {
|
||||
has_top_items_without_children = true;
|
||||
element_content = item.find('input').val();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if (has_top_items_without_children) {
|
||||
if (element_content) {
|
||||
alert('Warning: if you save now, the element ' + element_content + ', without children, will be removed, because it\'s invalid YAML');
|
||||
} else {
|
||||
alert('Warning: if you save now, the top elements without children will be removed, because it\'s invalid YAML');
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '[data-grav-multilevel-field] .js__add-children', function(event) {
|
||||
let element = $(this);
|
||||
let unique_container_id = element.closest('.js__multilevel-field').data('id');
|
||||
let level = element.data('level') + 1;
|
||||
|
||||
const getParentOfElement = function getParentOfElement(element) {
|
||||
let parent = element.closest('.js__multilevel-field').parent().first();
|
||||
if (parent.find('.children-wrapper').length === 0) {
|
||||
$(parent).append('<div class="children-wrapper"></div>');
|
||||
}
|
||||
parent = parent.find('.children-wrapper').first();
|
||||
|
||||
return parent;
|
||||
};
|
||||
|
||||
const getNameFromParentInput = function getNameFromParentInput(parentInput, attr) {
|
||||
if (parentInput.hasClass('children-wrapper')) {
|
||||
parentInput = parentInput.siblings('.js__multilevel-field').first().find('input');
|
||||
}
|
||||
|
||||
return parentInput.attr(attr) + '[' + parentInput.val() + ']';
|
||||
};
|
||||
|
||||
const getInputFromChildrenWrapper = function getInputFromChildrenWrapper(parentChildrenWrapper) {
|
||||
return parentChildrenWrapper.siblings('.js__multilevel-field').first().find('input');
|
||||
};
|
||||
|
||||
let parentChildrenWrapper = getParentOfElement(element);
|
||||
let parentInput = getInputFromChildrenWrapper(parentChildrenWrapper);
|
||||
|
||||
let attr = 'name';
|
||||
if (parentInput.closest('.js__multilevel-field').hasClass('top')) {
|
||||
attr = 'data-attr-name';
|
||||
}
|
||||
|
||||
parentInput.attr(attr, parentInput.attr(attr).replace('[]', ''));
|
||||
|
||||
let name = getNameFromParentInput(parentInput, attr);
|
||||
let field = getField(level, name);
|
||||
|
||||
$(parentChildrenWrapper).append(field);
|
||||
refreshControls(unique_container_id);
|
||||
});
|
||||
|
||||
$(document).on('click', '[data-grav-multilevel-field] .js__add-sibling', function(event) {
|
||||
let element = $(this);
|
||||
let unique_container_id = element.closest('.js__multilevel-field').data('id');
|
||||
let level = element.data('level');
|
||||
element.closest('.children-wrapper').find('.js__add-sibling').addClass('hidden');
|
||||
|
||||
let sibling = null;
|
||||
let is_top = false;
|
||||
|
||||
if (element.closest('.js__multilevel-field').hasClass('top')) {
|
||||
is_top = true;
|
||||
}
|
||||
|
||||
if (is_top) {
|
||||
sibling = element.closest('.js__multilevel-field').first().find('input').last();
|
||||
} else {
|
||||
sibling = element.siblings('input').first();
|
||||
if (!sibling) {
|
||||
sibling = element.closest('.children-wrapper').first().find('input').last();
|
||||
}
|
||||
}
|
||||
|
||||
const getParentOfElement = function getParentOfElement(element) {
|
||||
let parent = element.closest('.js__multilevel-field').parent().first();
|
||||
if (!parent.hasClass('element-wrapper')) {
|
||||
if (parent.find('.element-wrapper').length === 0) {
|
||||
$(parent).append('<div class="element-wrapper"></div>');
|
||||
}
|
||||
|
||||
parent = parent.find('.element-wrapper').first();
|
||||
}
|
||||
|
||||
return parent;
|
||||
};
|
||||
|
||||
const getNameFromSibling = function getNameFromSibling(parent, sibling, is_top = false) {
|
||||
let name = sibling.attr('name');
|
||||
|
||||
if (hasChildInputs(sibling)) {
|
||||
let val = sibling.data('attr-name') + '[]';
|
||||
sibling.removeAttr('name');
|
||||
return val;
|
||||
}
|
||||
|
||||
let last_index = name.lastIndexOf('[');
|
||||
let almost_there = name.substr(last_index + 1);
|
||||
let last_tag = almost_there.substr(0, almost_there.length - 1);
|
||||
|
||||
if ($.isNumeric(last_tag)) {
|
||||
name = name.replace('[' + last_tag + ']', '[' + (parseInt(last_tag, 10) + 1) + ']');
|
||||
} else {
|
||||
if (is_top) {
|
||||
name = name.replace('[' + last_tag + ']', '');
|
||||
} else {
|
||||
name = name + '[1]';
|
||||
|
||||
// change sibling name attr if necessary
|
||||
if (sibling.attr('name').slice('-2') !== '[0]') {
|
||||
changeAllOccurrencesInTree(sibling, sibling.attr('name'), sibling.attr('name') + '[0]');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
let parent = getParentOfElement(element);
|
||||
let name = getNameFromSibling(parent, sibling, is_top);
|
||||
|
||||
let field = getField(level, name);
|
||||
$(field).insertAfter(parent);
|
||||
|
||||
refreshControls(unique_container_id);
|
||||
});
|
||||
|
||||
$(document).on('click', '[data-grav-multilevel-field] .js__remove-item', function(event) {
|
||||
$(this).parents('.element-wrapper').first().remove();
|
||||
let unique_container_id = $(this).closest('.js__multilevel-field').data('id');
|
||||
refreshControls(unique_container_id);
|
||||
});
|
||||
|
||||
// Store old value before editing a field
|
||||
$(document).on('focusin', '[data-grav-multilevel-field] input', function(event) {
|
||||
$(this).data('current-value', $(this).val());
|
||||
});
|
||||
|
||||
// Handle field edited event
|
||||
$(document).on('change', '[data-grav-multilevel-field] input', function(event) {
|
||||
let $el = $(this);
|
||||
let old_value = $el.data('current-value');
|
||||
let new_value = $el.val();
|
||||
|
||||
let full_name = $el.attr('name') || $el.attr('data-attr-name'); // first-level items have `data-attr-name` instead of `name`
|
||||
|
||||
let old_name_attr = full_name + '[' + old_value + ']';
|
||||
let new_name_attr = full_name + '[' + new_value + ']';
|
||||
|
||||
changeAllOccurrencesInTree($el, old_name_attr, new_name_attr);
|
||||
});
|
||||
|
||||
});
|
||||
270
user/plugins/admin/themes/grav/app/forms/fields/parents.js
Normal file
270
user/plugins/admin/themes/grav/app/forms/fields/parents.js
Normal file
@@ -0,0 +1,270 @@
|
||||
import $ from 'jquery';
|
||||
import Finder from '../../utils/finderjs';
|
||||
import { config as gravConfig } from 'grav-config';
|
||||
|
||||
let XHRUUID = 0;
|
||||
export const Instances = {};
|
||||
|
||||
export default class Parents {
|
||||
constructor(container, field, data) {
|
||||
this.container = $(container);
|
||||
this.fieldName = field.attr('name');
|
||||
this.field = $(`[name="${this.fieldName}"]`);
|
||||
this.data = data;
|
||||
this.parentLabel = $(`[data-parents-field-label="${this.fieldName}"]`);
|
||||
this.parentName = $(`[data-parents-field-name="${this.fieldName}"]`);
|
||||
|
||||
const dataLoad = this.dataLoad;
|
||||
|
||||
this.finder = new Finder(
|
||||
this.container,
|
||||
(parent, callback) => {
|
||||
return dataLoad.call(this, parent, callback);
|
||||
},
|
||||
{
|
||||
labelKey: 'name',
|
||||
defaultPath: this.field.val(),
|
||||
createItemContent: function(item) {
|
||||
return Parents.createItemContent(this.config, item);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
this.finder.$emitter.on('leaf-selected', (item) => {
|
||||
console.log('selected', item);
|
||||
this.finder.emit('create-column', () => this.createSimpleColumn(item));
|
||||
});
|
||||
|
||||
this.finder.$emitter.on('item-selected', (selected) => {
|
||||
console.log('selected', selected);
|
||||
// for future use only - create column-card creation for file with details like in macOS finder
|
||||
// this.finder.$emitter('create-column', () => this.createSimpleColumn(selected));
|
||||
}); */
|
||||
|
||||
this.finder.$emitter.on('column-created', () => {
|
||||
this.container[0].scrollLeft = this.container[0].scrollWidth - this.container[0].clientWidth;
|
||||
});
|
||||
}
|
||||
|
||||
static createItemContent(config, item) {
|
||||
const frag = document.createDocumentFragment();
|
||||
|
||||
const label = $(`<span title="${item[config.labelKey]}" />`);
|
||||
const infoContainer = $('<span class="info-container" />');
|
||||
const iconPrepend = $('<i />');
|
||||
const iconAppend = $('<i />');
|
||||
const badge = $('<span class="badge" />');
|
||||
const prependClasses = ['fa'];
|
||||
const appendClasses = ['fa'];
|
||||
|
||||
// prepend icon
|
||||
if (item.children || item.type === 'dir') {
|
||||
prependClasses.push('fa-folder');
|
||||
} else if (item.type === 'root') {
|
||||
prependClasses.push('fa-sitemap');
|
||||
} else if (item.type === 'file') {
|
||||
prependClasses.push('fa-file-o');
|
||||
}
|
||||
|
||||
iconPrepend.addClass(prependClasses.join(' '));
|
||||
|
||||
// text label
|
||||
label.text(item[config.labelKey]).prepend(iconPrepend);
|
||||
label.appendTo(frag);
|
||||
|
||||
// append icon
|
||||
if (item.children || item['has-children']) {
|
||||
appendClasses.push('fa-caret-right');
|
||||
badge.text(item.size || item.count || 0);
|
||||
badge.appendTo(infoContainer);
|
||||
}
|
||||
|
||||
iconAppend.addClass(appendClasses.join(' '));
|
||||
iconAppend.appendTo(infoContainer);
|
||||
infoContainer.appendTo(frag);
|
||||
|
||||
return frag;
|
||||
}
|
||||
|
||||
static createLoadingColumn() {
|
||||
return $(`
|
||||
<div class="fjs-col leaf-col" style="overflow: hidden;">
|
||||
<div class="leaf-row">
|
||||
<div class="grav-loading"><div class="grav-loader">Loading...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
static createErrorColumn(error) {
|
||||
return $(`
|
||||
<div class="fjs-col leaf-col" style="overflow: hidden;">
|
||||
<div class="leaf-row error">
|
||||
<i class="fa fa-fw fa-warning"></i>
|
||||
<span>${error}</span>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
createSimpleColumn(item) {}
|
||||
|
||||
dataLoad(parent, callback) {
|
||||
if (!parent) {
|
||||
return callback(this.data);
|
||||
}
|
||||
|
||||
if (parent.type !== 'dir' || !parent['has-children']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const UUID = ++XHRUUID;
|
||||
this.startLoader();
|
||||
|
||||
$.ajax({
|
||||
url: `${gravConfig.current_url}`,
|
||||
method: 'post',
|
||||
data: Object.assign({}, getExtraFormData(this.container), {
|
||||
route: b64_encode_unicode(parent.value),
|
||||
field: this.field.data('fieldName'),
|
||||
action: 'getLevelListing',
|
||||
'admin-nonce': gravConfig.admin_nonce
|
||||
}),
|
||||
success: (response) => {
|
||||
this.stopLoader();
|
||||
|
||||
if (response.status === 'error') {
|
||||
this.finder.$emitter.emit('create-column', Parents.createErrorColumn(response.message)[0]);
|
||||
return false;
|
||||
}
|
||||
// stale request
|
||||
if (UUID !== XHRUUID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return callback(response.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startLoader() {
|
||||
this.loadingIndicator = Parents.createLoadingColumn();
|
||||
this.finder.$emitter.emit('create-column', this.loadingIndicator[0]);
|
||||
|
||||
return this.loadingIndicator;
|
||||
}
|
||||
|
||||
stopLoader() {
|
||||
return this.loadingIndicator && this.loadingIndicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export const b64_encode_unicode = (str) => {
|
||||
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
||||
function toSolidBytes(match, p1) {
|
||||
return String.fromCharCode('0x' + p1);
|
||||
}));
|
||||
};
|
||||
|
||||
export const b64_decode_unicode = (str) => {
|
||||
return decodeURIComponent(atob(str).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
};
|
||||
|
||||
const getExtraFormData = (container) => {
|
||||
let form = container.closest('form');
|
||||
if (container.closest('[data-remodal-id]').length) {
|
||||
form = $('form#blueprints');
|
||||
}
|
||||
const data = {};
|
||||
const unique_id = form.find('[name="__unique_form_id__"]');
|
||||
|
||||
data['__form-name__'] = form.find('[name="__form-name__"]').val();
|
||||
data['form-nonce'] = form.find('[name="form-nonce"]').val();
|
||||
|
||||
if (unique_id.length) {
|
||||
data['__unique_form_id__'] = unique_id.val();
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
$(document).on('click', '[data-parents]', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const target = $(event.currentTarget);
|
||||
let field = target.closest('.parents-wrapper').find('input[name]');
|
||||
let fieldName = field.attr('name');
|
||||
|
||||
if (!field.length) {
|
||||
fieldName = target.data('parents');
|
||||
field = $(`[name="${target.data('parents')}"]`).first();
|
||||
}
|
||||
|
||||
const modal = $(`[data-remodal-id="${target.data('remodalTarget') || 'parents'}"]`);
|
||||
const loader = modal.find('.grav-loading');
|
||||
const content = modal.find('.parents-content');
|
||||
|
||||
loader.css('display', 'block');
|
||||
content.html('');
|
||||
$.ajax({
|
||||
url: `${gravConfig.current_url}`,
|
||||
method: 'post',
|
||||
data: Object.assign({}, getExtraFormData(target), {
|
||||
route: b64_encode_unicode(field.val()),
|
||||
field: field.data('fieldName'),
|
||||
action: 'getLevelListing',
|
||||
'admin-nonce': gravConfig.admin_nonce,
|
||||
initial: true
|
||||
}),
|
||||
success(response) {
|
||||
loader.css('display', 'none');
|
||||
|
||||
if (response.status === 'error') {
|
||||
content.html(response.message);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Instances[`${fieldName}-${modal.data('remodalId')}`]) {
|
||||
Instances[`${fieldName}-${modal.data('remodalId')}`] = new Parents(content, field, response.data);
|
||||
} else {
|
||||
Instances[`${fieldName}-${modal.data('remodalId')}`].finder.reload(response.data);
|
||||
}
|
||||
|
||||
modal.data('parents', Instances[`${fieldName}-${modal.data('remodalId')}`]);
|
||||
modal.data('parents-selectedField', field);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// apply finder selection to field
|
||||
$(document).on('click', '[data-remodal-id].parents-container [data-parents-select]', (event) => {
|
||||
const modal = $(event.currentTarget).closest('[data-remodal-id]');
|
||||
const parents = modal.data('parents');
|
||||
const selectedField = modal.data('parentsSelectedField');
|
||||
const finder = parents.finder;
|
||||
const field = parents.field;
|
||||
const parentLabel = parents.parentLabel;
|
||||
const parentName = parents.parentName;
|
||||
const selection = finder.findLastActive().item[0];
|
||||
const value = selection._item[finder.config.valueKey];
|
||||
const name = selection._item[finder.config.labelKey];
|
||||
|
||||
if (selectedField.closest('.remodal').length) {
|
||||
const index = field.index(selectedField);
|
||||
selectedField.val(value);
|
||||
$(parentLabel[index]).text(value);
|
||||
$(parentName[index]).text(name);
|
||||
} else {
|
||||
field.val(value);
|
||||
parentLabel.text(value);
|
||||
parentName.text(name);
|
||||
finder.config.defaultPath = value;
|
||||
|
||||
}
|
||||
const remodal = $.remodal.lookup[$(`[data-remodal-id="${modal.data('remodalId')}"]`).data('remodal')];
|
||||
remodal.close();
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
const body = $('body');
|
||||
const radioSelector = '.permission-container.parent-section input[type="radio"]';
|
||||
|
||||
const handleParent = (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const value = target.val();
|
||||
const container = target.closest('.parent-section');
|
||||
const fieldset = container.next('fieldset');
|
||||
const radios = fieldset.find(`input[type="radio"][value="${value}"]`);
|
||||
|
||||
if (container.data('isLocked') !== false) {
|
||||
container.data('isUpdating', true);
|
||||
radios.each((index, radio) => {
|
||||
const ID = radio.id;
|
||||
$(radio).siblings(`[for="${ID}"]`).trigger('click');
|
||||
});
|
||||
container.data('isUpdating', false);
|
||||
}
|
||||
};
|
||||
|
||||
const boundHandleParent = handleParent.bind(handleParent);
|
||||
|
||||
body.on('click', '.permission-container.parent-section label', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const container = target.closest('.parent-section');
|
||||
container.data('isLocked', true);
|
||||
});
|
||||
|
||||
body.on('input', radioSelector, boundHandleParent);
|
||||
|
||||
body.on('input', '.permissions-container input[type="radio"][data-parent-id]', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const parent = $(`[for="${target.data('parentId')}"]`);
|
||||
const container = target.closest('fieldset').prev('.permission-container.parent-section');
|
||||
|
||||
if (container.data('isUpdating') === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
body.off('input', radioSelector, boundHandleParent);
|
||||
container.data('isLocked', false);
|
||||
parent.trigger('click');
|
||||
body.on('input', radioSelector, boundHandleParent);
|
||||
});
|
||||
10
user/plugins/admin/themes/grav/app/forms/fields/range.js
Normal file
10
user/plugins/admin/themes/grav/app/forms/fields/range.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
$(document).on('input', '[type="range"].rangefield, [type="number"].rangefield', (event) => {
|
||||
const target = $(event.currentTarget);
|
||||
const type = target.attr('type').toLowerCase();
|
||||
const sibling = type === 'range' ? 'number' : 'range';
|
||||
const feedback = target.siblings(`[type="${sibling}"].rangefield`);
|
||||
|
||||
feedback.val(target.val());
|
||||
});
|
||||
66
user/plugins/admin/themes/grav/app/forms/fields/selectize.js
Normal file
66
user/plugins/admin/themes/grav/app/forms/fields/selectize.js
Normal file
@@ -0,0 +1,66 @@
|
||||
import $ from 'jquery';
|
||||
import 'selectize';
|
||||
import '../../utils/selectize-required-fix';
|
||||
import '../../utils/selectize-option-click';
|
||||
|
||||
const PagesRoute = {
|
||||
option: function(item, escape) {
|
||||
const label = escape(item.text).split(' ');
|
||||
const arrows = label.shift();
|
||||
const slug = label.shift();
|
||||
|
||||
return `<div class="selectize-route-option">
|
||||
<span class="text-grey">${arrows}</span>
|
||||
<span>
|
||||
<span class="text-update">${slug.replace('(', '/').replace(')', '')}</span>
|
||||
<span>${label.join(' ')}</span>
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
};
|
||||
|
||||
export default class SelectizeField {
|
||||
constructor(options = {}) {
|
||||
this.options = Object.assign({}, options);
|
||||
this.elements = [];
|
||||
|
||||
$('[data-grav-selectize]').each((index, element) => this.add(element));
|
||||
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
}
|
||||
|
||||
add(element) {
|
||||
element = $(element);
|
||||
|
||||
if (element.closest('template').length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let tag = element.prop('tagName').toLowerCase();
|
||||
let isInput = tag === 'input' || tag === 'select';
|
||||
|
||||
let data = (isInput ? element.closest('[data-grav-selectize]') : element).data('grav-selectize') || {};
|
||||
let field = (isInput ? element : element.find('input, select'));
|
||||
|
||||
if (field.attr('name') === 'data[route]') {
|
||||
data = $.extend({}, data, { render: PagesRoute });
|
||||
}
|
||||
|
||||
if (!field.length || field.get(0).selectize) { return; }
|
||||
const plugins = $.merge(data.plugins ? data.plugins : [], ['required-fix']);
|
||||
field.selectize($.extend({}, data, { plugins }));
|
||||
|
||||
this.elements.push(field.data('selectize'));
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target/* , record, instance */) {
|
||||
let fields = $(target).find('select.fancy, input.fancy, [data-grav-selectize]').filter((index, element) => {
|
||||
return !$(element).closest('template').length;
|
||||
});
|
||||
|
||||
if (!fields.length) { return; }
|
||||
|
||||
fields.each((index, field) => this.add(field));
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new SelectizeField();
|
||||
160
user/plugins/admin/themes/grav/app/forms/fields/selectunique.js
Normal file
160
user/plugins/admin/themes/grav/app/forms/fields/selectunique.js
Normal file
@@ -0,0 +1,160 @@
|
||||
import $ from 'jquery';
|
||||
import forIn from 'mout/object/forIn';
|
||||
// import { config } from 'grav-config';
|
||||
|
||||
const Data = {};
|
||||
export default class SelectUniqueField {
|
||||
|
||||
constructor(options) {
|
||||
const body = $('body');
|
||||
this.items = $();
|
||||
this.options = Object.assign({}, this.defaults, options);
|
||||
|
||||
$('[data-select-observe]').each((index, element) => this.addSelect(element)).last().trigger('change', { load: true });
|
||||
body.on('mutation._grav', this._onAddedNodes.bind(this));
|
||||
body.on('mutation_removed._grav', this._onRemovedNodes.bind(this));
|
||||
}
|
||||
|
||||
_onAddedNodes(event, target, record, instance) {
|
||||
let fields = $(target).find('[data-select-observe]');
|
||||
if (!fields.length) { return; }
|
||||
|
||||
fields.each((index, field) => {
|
||||
field = $(field);
|
||||
if (!~this.items.index(field)) {
|
||||
this.addSelect(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_onRemovedNodes(event, data/* , instance */) {
|
||||
const target = $(data.target);
|
||||
const holder = target.data('collectionHolder');
|
||||
if (!holder) { return false; }
|
||||
|
||||
const node = $(data.mutation.removedNodes);
|
||||
const value = node.find('[data-select-observe]').val();
|
||||
if (value) {
|
||||
Data[holder].state[value] = value;
|
||||
}
|
||||
|
||||
target.find('[data-select-observe]').each((index, field) => {
|
||||
field = $(field);
|
||||
|
||||
if (field.val() !== value) {
|
||||
this.updateOptions(field);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addSelect(element) {
|
||||
this.items = this.items.add(element);
|
||||
element = $(element);
|
||||
|
||||
const value = element.attr('value');
|
||||
const holder = element.closest('[data-collection-holder]').data('collectionHolder');
|
||||
const options = element.closest('[data-select-unique]').data('selectUnique');
|
||||
|
||||
if (!Data[holder]) {
|
||||
let data = {};
|
||||
if (Array.isArray(options)) {
|
||||
options.slice(0).map((item) => { data[item] = item; });
|
||||
} else {
|
||||
data = Object.assign({}, options);
|
||||
}
|
||||
|
||||
Data[holder] = { original: null, state: null };
|
||||
Data[holder].original = Object.assign({}, data);
|
||||
Data[holder].state = Object.assign({}, data);
|
||||
}
|
||||
|
||||
this.updateOptions(element);
|
||||
|
||||
element.data('originalValue', value);
|
||||
element.on('change', (event, extras) => {
|
||||
const target = $(event.currentTarget);
|
||||
if (target.data('dummyChange')) {
|
||||
target.data('dummyChange', false);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.refreshOptions(target, extras && extras.load ? null : element.data('originalValue'));
|
||||
element.data('originalValue', target.val());
|
||||
});
|
||||
}
|
||||
|
||||
updateOptions(element) {
|
||||
element = $(element);
|
||||
const value = element.attr('value');
|
||||
const holder = element.closest('[data-collection-holder]').data('collectionHolder');
|
||||
|
||||
forIn(Data[holder].state, (v, k) => {
|
||||
const selected = k === value ? 'selected="selected"' : '';
|
||||
|
||||
if (element.get(0).selectize) {
|
||||
const selectize = element.data('selectize');
|
||||
selectize.removeOption(k);
|
||||
selectize.addOption({ value: k, text: v });
|
||||
} else {
|
||||
element.append(`<option value="${k}" ${selected}>${v}</option>`);
|
||||
}
|
||||
|
||||
if (selected) {
|
||||
if (element.get(0).selectize) {
|
||||
const selectize = element.data('selectize');
|
||||
selectize.setValue(k);
|
||||
}
|
||||
delete Data[holder].state[value];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refreshOptions(element, originalValue) {
|
||||
const value = element.val();
|
||||
const holder = element.closest('[data-collection-holder]').data('collectionHolder');
|
||||
delete Data[holder].state[value];
|
||||
|
||||
if (originalValue && Data[holder].original[originalValue]) {
|
||||
Data[holder].state[originalValue] = Data[holder].original[originalValue];
|
||||
}
|
||||
|
||||
this.items.each((index, select) => {
|
||||
select = $(select);
|
||||
if (select[0] === element[0]) { return; }
|
||||
|
||||
const selectedValue = select.val();
|
||||
select.data('dummyChange', true);
|
||||
|
||||
if (select.get(0).selectize) {
|
||||
const selectize = select.data('selectize');
|
||||
|
||||
if (selectize) {
|
||||
selectize.clearOptions();
|
||||
|
||||
if (selectedValue) {
|
||||
selectize.addOption({
|
||||
value: selectedValue,
|
||||
text: Data[holder].original[selectedValue] || selectedValue
|
||||
});
|
||||
}
|
||||
|
||||
forIn(Data[holder].state, (v, k) => {
|
||||
selectize.addOption({ value: k, text: v });
|
||||
});
|
||||
|
||||
selectize.setValue(selectedValue, true);
|
||||
}
|
||||
} else {
|
||||
select.empty();
|
||||
forIn(Data[holder].state, (v, k) => {
|
||||
const selected = k === selectedValue ? 'selected="selected"' : '';
|
||||
select.append(`<option value="${k}" ${selected}>${v}</option>`);
|
||||
});
|
||||
}
|
||||
|
||||
select.data('dummyChange', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export let Instance = new SelectUniqueField();
|
||||
13
user/plugins/admin/themes/grav/app/forms/fields/text.js
Normal file
13
user/plugins/admin/themes/grav/app/forms/fields/text.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import $ from 'jquery';
|
||||
|
||||
$(document).ready(function() {
|
||||
$('.copy-to-clipboard').click(function(event) {
|
||||
var $tempElement = $('<input>');
|
||||
$('body').append($tempElement);
|
||||
$tempElement.val($(this).prev('input').val()).select();
|
||||
document.execCommand('Copy');
|
||||
$tempElement.remove();
|
||||
|
||||
$(this).attr('data-hint', 'Copied to clipboard!').addClass('hint--left');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user