This commit is contained in:
Loïc Guibert
2022-09-30 20:02:02 +01:00
commit 66dafc36c3
2561 changed files with 454489 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import $ from 'jquery';
import { translations } from 'grav-config';
import request from '../utils/request';
import { Instances as Charts } from './chart';
$('[data-backup][data-ajax*="backup/"]').on('click', function() {
let element = $(this);
let url = element.data('ajax');
const inDropdown = element.closest('.dropdown-menu');
(inDropdown.length ? inDropdown : element)
.closest('.button-group').find('> button:first')
.attr('disabled', 'disabled')
.find('> .fa').removeClass('fa-life-ring').addClass('fa-spin fa-refresh');
request(url, (/* response */) => {
if (Charts && Charts.backups) {
Charts.backups.updateData({ series: [0, 100] });
Charts.backups.element.find('.numeric').html(`0 <em>${translations.PLUGIN_ADMIN.DAYS.toLowerCase()}</em>`);
}
(inDropdown.length ? inDropdown : element)
.closest('.button-group').find('> button:first')
.removeAttr('disabled')
.find('> .fa').removeClass('fa-spin fa-refresh').addClass('fa-life-ring');
});
});
$('[data-backup][data-ajax*="backupDelete"]').on('click', function() {
let element = $(this);
let url = element.data('ajax');
const tr = element.closest('tr');
tr.addClass('deleting');
request(url, (response) => {
if (response.status === 'success') {
tr.remove();
} else {
tr.removeClass('deleting');
}
});
});

View File

@@ -0,0 +1,49 @@
import $ from 'jquery';
import { config } from 'grav-config';
import request from '../utils/request';
const getUrl = (type = '') => {
if (type) {
type = `cleartype:${type}/`;
}
return `${config.base_url_relative}/cache.json/task${config.param_sep}clearCache/${type}admin-nonce${config.param_sep}${config.admin_nonce}`;
};
export default class Cache {
constructor() {
this.element = $('[data-clear-cache]');
$('body').on('click', '[data-clear-cache]', (event) => this.clear(event, event.target));
}
clear(event, element) {
let type = '';
if (event && event.preventDefault) { event.preventDefault(); }
if (typeof event === 'string') { type = event; }
element = element ? $(element) : $(`[data-clear-cache-type="${type}"]`);
type = type || $(element).data('clear-cache-type') || '';
let url = element.data('clearCache') || getUrl(type);
this.disable();
request(url, () => this.enable());
}
enable() {
this.element
.removeAttr('disabled')
.find('> .fa').removeClass('fa-refresh fa-spin fa-retweet').addClass('fa-retweet');
}
disable() {
this.element
.attr('disabled', 'disabled')
.find('> .fa').removeClass('fa-retweet').addClass('fa-refresh fa-spin');
}
}
let Instance = new Cache();
export { Instance };

View File

@@ -0,0 +1,138 @@
import $ from 'jquery';
import chartist from 'chartist';
import { translations } from 'grav-config';
import { Instance as gpm } from '../utils/gpm';
import { Instance as updates } from '../updates';
// let isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
export const defaults = {
data: {
series: [100, 0]
},
options: {
Pie: {
donut: true,
donutWidth: 10,
startAngle: 0,
total: 100,
showLabel: false,
height: 150,
// chartPadding: !isFirefox ? 10 : 25 // workaround for older versions of firefox
chartPadding: 5
},
Bar: {
height: 164,
chartPadding: 20, // workaround for older versions of firefox
axisX: {
showGrid: false,
labelOffset: {
x: 0,
y: 0
}
},
axisY: {
offset: 15,
showLabel: true,
showGrid: true,
labelOffset: {
x: 5,
y: 5
},
scaleMinSpace: 25
}
}
}
};
export default class Chart {
constructor(element, options = {}, data = {}) {
this.element = $(element) || [];
if (!this.element[0]) { return; }
let type = (this.element.data('chart-type') || 'pie').toLowerCase();
this.type = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase();
options = Object.assign({}, defaults.options[this.type], options);
data = Object.assign({}, defaults.data, data);
Object.assign(this, {
options,
data
});
this.chart = chartist[this.type](this.element.find('.ct-chart').empty()[0], this.data, this.options);
this.chart.on('created', () => {
this.element.find('.hidden').removeClass('hidden');
// FIX: workaround for chartist issue not allowing HTML in labels anymore
// https://github.com/gionkunz/chartist-js/issues/937
this.element.find('.ct-label').each((index, label) => {
label = $(label);
const text = label.html().replace('&lt;', '<').replace('&gt;', '>');
label.html(text);
});
});
}
updateData(data) {
Object.assign(this.data, data);
this.chart.update(this.data);
}
};
export class UpdatesChart extends Chart {
constructor(element, options = {}, data = {}) {
super(element, options, data);
this.chart.on('draw', (data) => this.draw(data));
gpm.on('fetched', (response) => {
if (!response.payload) { return; }
let payload = response.payload.grav;
let missing = (response.payload.resources.total + (payload.isUpdatable ? 1 : 0)) * 100 / (response.payload.installed + (payload.isUpdatable ? 1 : 0));
let updated = 100 - missing;
this.updateData({ series: [updated, missing] });
if (response.payload.resources.total) {
updates.maintenance('show');
}
});
}
draw(data) {
if (data.index) { return; }
let notice = translations.PLUGIN_ADMIN[data.value === 100 ? 'FULLY_UPDATED' : 'UPDATES_AVAILABLE'];
this.element.find('.numeric span').text(`${Math.round(data.value)}%`);
this.element.find('.js__updates-available-description').html(notice);
this.element.find('.hidden').removeClass('hidden');
}
updateData(data) {
super.updateData(data);
// missing updates
if (this.data.series[0] < 100) {
this.element.closest('#updates').find('[data-update-packages]').fadeIn();
}
}
}
let charts = {};
$('[data-chart-name]').each(function() {
let element = $(this);
let name = element.data('chart-name') || '';
let options = element.data('chart-options') || {};
let data = element.data('chart-data') || {};
if (name === 'updates') {
charts[name] = new UpdatesChart(element, options, data);
} else {
charts[name] = new Chart(element, options, data);
}
});
export let Instances = charts;

View File

@@ -0,0 +1,12 @@
import Chart, { UpdatesChart, Instances } from './chart';
import { Instance as Cache } from './cache';
import './backup';
export default {
Chart: {
Chart,
UpdatesChart,
Instances
},
Cache
};

View File

@@ -0,0 +1 @@
// See ../updates/update.js

View File

@@ -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');
});

View 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();

View 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();

View 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]');

View 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();

View 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();

View 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();

View File

@@ -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: '![$1]($cur)', 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);
});
}
}
}]
};

View 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');

View 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();

View 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(/&lt;/g, '<').replace(/&gt;/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('<', '&lt;').replace('>', '&gt;');
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) ? `![${title}](${uri} "${title}")` : `[${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;
})();

View 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;

View 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);
});
}
}
});

View File

@@ -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; });

View 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
}
};

View File

@@ -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();
});
});

View 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);
});
});

View 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();
});

View File

@@ -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);
});

View 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());
});

View 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();

View 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();

View 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');
});
});

View File

@@ -0,0 +1,139 @@
import $ from 'jquery';
/* Dependencies for checking if changes happened since load on a form
import toastr from '../utils/toastr';
import { translations } from 'grav-config';
import { Instance as FormState } from './state';
*/
export default class Form {
constructor(form) {
this.form = $(form);
if (!this.form.length || this.form.prop('tagName').toLowerCase() !== 'form') { return; }
/* Option for not saving while nothing in a form has changed
this.form.on('submit', (event) => {
if (FormState.equals()) {
event.preventDefault();
toastr.info(translations.PLUGIN_ADMIN.NOTHING_TO_SAVE);
}
}); */
this._attachShortcuts();
this._attachToggleables();
this._attachDisabledFields();
this._submitUncheckedFields();
this.observer = new MutationObserver(this.addedNodes);
this.form.each((index, form) => this.observer.observe(form, { subtree: true, childList: true }));
}
_attachShortcuts() {
// CTRL + S / CMD + S - shortcut for [Save] when available
let saveTask = $('#titlebar [name="task"][value="save"][form="blueprints"]');
if (saveTask.length) {
$(global).on('keydown', function(event) {
const key = String.fromCharCode(event.which).toLowerCase();
if (!event.shiftKey && ((event.ctrlKey && !event.altKey) || event.metaKey) && key === 's') {
event.preventDefault();
saveTask.click();
}
});
}
}
_attachToggleables() {
let query = '[data-grav-field="toggleable"] input[type="checkbox"]';
this.form.on('change', query, (event) => {
let toggle = $(event.target);
let enabled = toggle.is(':checked');
let parent = toggle.closest('.form-field');
let label = parent.find('label.toggleable');
let fields = parent.find('.form-data');
let inputs = fields.find('input, select, textarea, button');
label.add(fields).css('opacity', enabled ? '' : 0.7);
inputs.map((index, input) => {
let isSelectize = input.selectize;
input = $(input);
if (isSelectize) {
isSelectize[enabled ? 'enable' : 'disable']();
} else {
input.prop('disabled', !enabled);
}
});
});
this.form.find(query).trigger('change');
}
_attachDisabledFields() {
let prefix = '.form-field-toggleable .form-data';
let query = [];
['input', 'select', 'label[for]', 'textarea', '.selectize-control'].forEach((item) => {
query.push(`${prefix} ${item}`);
});
this.form.on('mousedown', query.join(', '), (event) => {
let input = $(event.target);
let isFor = input.prop('for');
let isSelectize = (input.hasClass('selectize-control') || input.parents('.selectize-control')).length;
if (isFor) { input = $(`[id="${isFor}"]`); }
if (isSelectize) { input = input.closest('.selectize-control').siblings('select[name]'); }
if (!input.prop('disabled')) { return true; }
let toggle = input.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
toggle.trigger('click');
});
}
_submitUncheckedFields() {
let submitted = false;
this.form.each((index, form) => {
form = $(form);
form.on('submit', () => {
// workaround for MS Edge, submitting multiple forms at the same time
if (submitted) { return false; }
let formId = form.attr('id');
let unchecked = form.find('input[type="checkbox"]:not(:checked):not(:disabled)');
let submit = form.find('[type="submit"]').add(`[form="${formId}"][type="submit"]`);
if (!unchecked.length) { return true; }
submit.addClass('pointer-events-disabled');
unchecked.each((index, element) => {
element = $(element);
let name = element.prop('name');
let fake = $(`<input type="hidden" name="${name}" value="0" />`);
form.append(fake);
});
submitted = true;
return true;
});
});
}
addedNodes(mutations) {
mutations.forEach((mutation) => {
if (mutation.type !== 'childList') { return; }
if (mutation.addedNodes) {
$('body').trigger('mutation._grav', mutation.target, mutation, this);
}
if (mutation.removedNodes) {
$('body').trigger('mutation_removed._grav', { target: mutation.target, mutation }, this);
}
});
}
}
export let Instance = new Form('form#blueprints');

View File

@@ -0,0 +1,16 @@
import FormState, { Instance as FormStateInstance } from './state';
import Form, { Instance as FormInstance } from './form';
import Fields from './fields';
export default {
Form: {
Form,
Instance: FormInstance
},
Fields,
FormState: {
FormState,
Instance: FormStateInstance
}
};

View File

@@ -0,0 +1,150 @@
import $ from 'jquery';
import Immutable from 'immutable';
import immutablediff from 'immutablediff';
import '../utils/jquery-utils';
let FormLoadState = {};
const DOMBehaviors = {
attach() {
this.preventUnload();
this.preventClickAway();
},
preventUnload() {
let selector = '[name="task"][value^="save"], [data-delete-action], [data-flex-safe-action]';
if ($._data(window, 'events') && ($._data(window, 'events').beforeunload || []).filter((event) => event.namespace === '_grav').length) {
return;
}
// Allow some elements to leave the page without native confirmation
$(selector).on('click._grav', function(event) {
$(global).off('beforeunload');
});
// Catch browser uri change / refresh attempt and stop it if the form state is dirty
$(global).on('beforeunload._grav', () => {
if (Instance.equals() === false) {
return 'You have made changes on this page that you have not yet confirmed. If you navigate away from this page you will lose your unsaved changes.';
}
});
},
preventClickAway() {
let selector = 'a[href]:not([href^="#"]):not([target="_blank"]):not([href^="javascript:"])';
if ($._data($(selector).get(0), 'events') && ($._data($(selector).get(0), 'events').click || []).filter((event) => event.namespace === '_grav')) {
return;
}
// Prevent clicking away if the form state is dirty
// instead, display a confirmation before continuing
$(selector).on('click._grav', function(event) {
let isClean = Instance.equals();
if (isClean === null || isClean) { return true; }
event.preventDefault();
let destination = $(this).attr('href');
let modal = $('[data-remodal-id="changes"]');
let lookup = $.remodal.lookup[modal.data('remodal')];
let buttons = $('a.button', modal);
let handler = function(event) {
event.preventDefault();
let action = $(this).data('leave-action');
buttons.off('click', handler);
lookup.close();
if (action === 'continue') {
$(global).off('beforeunload');
global.location.href = destination;
}
};
buttons.on('click', handler);
lookup.open();
});
}
};
export default class FormState {
constructor(options = {
ignore: [],
form_id: 'blueprints'
}) {
this.options = options;
this.refresh();
if (!this.form || !this.fields.length) { return; }
FormLoadState = this.collect();
this.loadState = FormLoadState;
DOMBehaviors.attach();
}
refresh() {
this.form = $(`form#${this.options.form_id}`).filter(':noparents(.remodal)');
this.fields = $(`form#${this.options.form_id} *, [form="${this.options.form_id}"]`).filter(':input:not(.no-form)').filter(':noparents(.remodal)');
return this;
}
collect() {
if (!this.form || !this.fields.length) { return; }
let values = {};
this.refresh().fields.each((index, field) => {
field = $(field);
let name = field.prop('name');
let type = field.prop('type');
let tag = field.prop('tagName').toLowerCase();
let value;
if (name.startsWith('toggleable_') || name === 'data[lang]' || name === 'data[redirect]') {
return;
}
switch (type) {
case 'checkbox':
value = field.is(':checked');
break;
case 'radio':
if (!field.is(':checked')) { return; }
value = field.val();
break;
default:
value = field.val();
}
if (tag === 'select' && value === null) {
value = '';
}
if (Array.isArray(value)) {
value = value.join('|');
}
if (name && !~this.options.ignore.indexOf(name)) {
values[name] = value;
}
});
return Immutable.OrderedMap(values);
}
diff() {
return immutablediff(FormLoadState, this.collect());
}
// When the form doesn't exist or there are no fields, `equals` returns `null`
// for this reason, _NEVER_ check with !Instance.equals(), use Instance.equals() === false
equals() {
if (!this.form || !this.fields.length) { return null; }
return Immutable.is(FormLoadState, this.collect());
}
};
export let Instance = new FormState();
export { DOMBehaviors };

View File

@@ -0,0 +1,73 @@
// polyfills
import '@babel/polyfill';
import $ from 'jquery';
import './utils/remodal';
import 'simplebar/dist/simplebar.min.js';
import { UriToMarkdown } from './forms/fields/files.js';
import GPM, { Instance as gpm } from './utils/gpm';
import KeepAlive from './utils/keepalive';
import Updates, { Instance as updates, Notifications, Feed } from './updates';
import Dashboard from './dashboard';
import Pages from './pages';
import Forms from './forms';
import Cookies from './utils/cookies';
import './plugins';
import './themes';
import MediaFilter, { Instance as MediaFilterInstance} from './media';
import toastr from './utils/toastr';
import request from './utils/request';
import './utils/2fa';
import './tools';
import './whitelabel';
// bootstrap jQuery extensions
import './utils/bootstrap-transition';
import './utils/bootstrap-collapse';
import './utils/bootstrap-dropdown';
// tabs memory
import './utils/tabs-memory';
// changelog
import './utils/changelog';
// Main Sidebar
import Sidebar, { Instance as sidebar } from './utils/sidebar';
// starts the keep alive, auto runs every X seconds
KeepAlive.start();
// global event to catch sidebar_state changes
$(global).on('sidebar_state._grav', () => {
Object.keys(Dashboard.Chart.Instances).forEach((chart) => {
setTimeout(() => Dashboard.Chart.Instances[chart].chart.update(), 10);
});
});
export default {
GPM: {
GPM,
Instance: gpm
},
KeepAlive,
Dashboard,
Pages,
Forms,
Updates: {
Updates,
Notifications,
Feed,
Instance: updates
},
Sidebar: {
Sidebar,
Instance: sidebar
},
MediaFilter: {
MediaFilter,
Instance: MediaFilterInstance
},
Scrollbar: { Scrollbar: { deprecated: true }, Instance: { deprecated: true } },
Utils: { request, toastr, Cookies, UriToMarkdown }
};

View File

@@ -0,0 +1,226 @@
import $ from 'jquery';
import { config, uri_params } from 'grav-config';
import request from '../utils/request';
export default class Filter {
constructor() {
this.URI = `${config.base_url_relative}/media-manager/`;
}
filter(name, value) {
let filtered = [];
let keys = Object.keys(uri_params);
if (!~keys.indexOf(name)) { keys.push(name); }
keys.forEach((key) => {
let filter = Filter.cleanValue(key === name ? value : uri_params[key]);
if (filter !== '*') {
filtered.push(`${key}${config.param_sep}${filter}`);
}
});
global.location = this.URI + filtered.join('/');
}
static cleanValue(value) {
return encodeURIComponent(value.replace('/', '\\'));
}
}
export let Instance = new Filter();
var isLoading = false;
var filters = {};
var global_index = 1;
var files_ended = false;
const MEDIA_PAGINATION_INTERVAL = 20;
/* handle changing file type / date filter */
$('body').on('change', '.thumbs-list-container select.filter', (event) => {
let target = $(event.currentTarget);
let filterName = target.data('name');
let filterValue = target.val();
if (filterValue) {
filters[filterName] = filterValue;
} else {
delete filters[filterName];
}
filterFiles();
});
/* initialize media uploader */
if ($('.thumbs-list-container .dropzone')[0]) {
$('.thumbs-list-container .dropzone')[0].dropzone.on('queuecomplete', function() {
let body = {};
if (filters.page) { body.page = filters.page; }
if (filters.date) { body.date = filters.date; }
if (filters.type) { body.type = filters.type; }
$('.dropzone')[0].dropzone.files.forEach(function(file) { file.previewElement.remove(); });
$('.dropzone').first().removeClass('dz-started');
request(`${config.base_url_relative}/media-manager.json/task${config.param_sep}clearMediaCache`, { method: 'post', body }, () => {
filterFiles();
});
});
}
/* handle loading media */
var loadMedia = function loadMedia(filters, callback) {
var url = `${config.base_url_relative}/media.json/tmpl${config.param_sep}media-list-content/index${config.param_sep}${global_index}`;
if (filters.page) {
url += `/page${config.param_sep}${(filters.page).split('/').join('%5C')}`;
}
if (filters.type && filters.type !== '*') {
url += `/type${config.param_sep}${filters.type}`;
}
if (filters.date && filters.date !== '*') {
url += `/date${config.param_sep}${filters.date}`;
}
if (!isLoading) {
isLoading = true;
$('.spinning-wheel').show();
$.get(url, function(content) {
$('.js__files').append(content);
$('.spinning-wheel').hide();
$('.media-container .media-range').trigger('change');
isLoading = false;
global_index++;
callback(content);
});
}
};
var cleanFilesList = function cleanFilesList() {
$('.js__files .card-item').remove();
};
var resetActiveStateInSidebar = function resetActiveStateInSidebar() {
$('.pages-list-container .row').removeClass('active'); // clear active state in sidebar
};
var showEmptyState = function showEmptyState() {
$('.thumbs-list-container').append('<p class="card-item empty-space">No media found</p>');
};
var filterFiles = function filterFiles() {
cleanFilesList();
global_index = 0;
files_ended = false;
$('.empty-space').remove();
loadMedia(filters, function(content) {
if (!content.trim().length) {
showEmptyState();
} else {
if (!filters.page && (!filters.date || filters.date === '*') && (!filters.type || filters.type === '*')) {
$('.js__files').trigger('fillView');
}
}
});
};
/* handle changing page */
$('body').on('click', '.pages-list-container .js__page-link', (event) => {
var page = $(event.target).data('page');
filters['page'] = page;
$('.media-list-title .page-indicator').html(page); // set indication
$('.js__reset-pages-filter').removeClass('hidden'); // activate reset pages icon
resetActiveStateInSidebar();
$(event.target).parents('.row').addClass('active'); // set active state in sidebar
$('.js__file-uploader').removeClass('hidden');
// customize processing URL, as the page changes dynamically
if ($('.dropzone')[0]) {
$('.dropzone')[0].dropzone.on('processing', function(file) {
this.options.url = `${config.base_url_relative}/media-manager${page}.json/task${config.param_sep}addmedia`;
});
}
$('.js__button-clear-media-cache').addClass('hidden');
filterFiles();
disableInfiniteScrolling(); // only infinite scroll on main list, not inside single pages
});
/* handle clearing page filter */
$('body').on('click', '.js__reset-pages-filter', (event) => {
$('.media-list-title .page-indicator').html('All Pages'); // set indication
cleanFilesList();
resetActiveStateInSidebar();
$('.js__reset-pages-filter').addClass('hidden'); // remove reset pages icon
$('.js__file-uploader').addClass('hidden');
$('.js__button-clear-media-cache').removeClass('hidden');
delete filters['page'];
filterFiles();
});
/* handle infinite loading */
var enableInfiniteScrolling = function enableInfiniteScrolling() {
$('.spinning-wheel').hide();
var view = $('.mediapicker-scroll').last();
if (!view.length) { return; }
$(view).on('scroll', function() {
if (($(this).scrollTop() + $(this).innerHeight() + 100) >= $(this)[0].scrollHeight) {
fillView();
}
});
};
var loadNextBatch = function loadNextBatch(callback) {
if (files_ended) {
return;
}
loadMedia({}, function(content) {
if (!$(content).length || ((content.split('card-item').length - 1) < MEDIA_PAGINATION_INTERVAL)) {
files_ended = true;
} else {
if (callback) {
callback();
}
}
$('.media-container .media-range').trigger('input');
});
};
var fillView = function fillView() {
if (!$('.js__files').find('.card-item').last().offset()) {
setTimeout(function() {
// retry later
fillView();
}, 300);
return;
}
if ($('.js__files').find('.card-item').last().offset().top - 1 <= $('.media-container').height()) {
loadNextBatch(function() {
fillView();
});
}
};
/* disable infinite loading */
var disableInfiniteScrolling = function disableInfiniteScrolling() {
$('.spinning-wheel').hide();
$('.content-wrapper').unbind('scroll');
};
$('.js__files').on('fillView', function(event) {
// the first batch got the max number of media files, try loading more
if (($('.js__files')[0].innerHTML.split('card-item').length - 1) === MEDIA_PAGINATION_INTERVAL) {
fillView();
enableInfiniteScrolling();
}
});

View File

@@ -0,0 +1,148 @@
import $ from 'jquery';
import { config, translations } from 'grav-config';
import request from '../utils/request';
import debounce from 'debounce';
import { Instance as pagesTree } from './tree';
import 'selectize';
import '../utils/selectize-required-fix.js';
import '../utils/storage';
/* @formatter:off */
/* eslint-disable */
const options = [
{ flag: translations.PLUGIN_ADMIN.MODULE, key: 'Module', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.VISIBLE, key: 'Visible', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.ROUTABLE, key: 'Routable', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.PUBLISHED, key: 'Published', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_MODULE, key: 'NonModule', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_VISIBLE, key: 'NonVisible', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_ROUTABLE, key: 'NonRoutable', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_PUBLISHED, key: 'NonPublished', cat: 'mode' }
];
/* @formatter:on */
/* eslint-enable */
export default class PagesFilter {
constructor(filters, search) {
this.filters = $(filters);
this.search = $(search);
this.options = options;
this.tree = pagesTree;
let storage = JSON.parse(localStorage.getItem('grav:admin:pages:filter') || '{}');
if (!this.filters.length || !this.search.length) { return; }
this.labels = this.filters.data('filter-labels');
this.search.on('input', debounce(() => this.filter(), 250));
this.filters.on('change', () => this.filter());
// restore state
if (storage.flags || storage.query) {
this.setValues(storage);
this.filter();
}
this._initSelectize();
}
filter(value) {
let data = { flags: '', query: '' };
if (typeof value === 'object') {
Object.assign(data, value);
}
if (typeof value === 'string') {
data.query = value;
}
if (typeof value === 'undefined') {
data.flags = this.filters.val();
data.query = this.search.val();
}
if (!Object.keys(data).filter((key) => data[key] !== '').length) {
this.resetValues();
return;
}
data.flags = data.flags.replace(/(\s{1,})?,(\s{1,})?/g, ',');
this.setValues({ flags: data.flags, query: data.query }, 'silent');
request(`${config.base_url_relative}/pages-filter.json/task${config.param_sep}filterPages`, {
method: 'post',
body: data
}, (response) => {
this.refreshDOM(response);
});
}
refreshDOM(response) {
let items = $('[data-nav-id]');
if (!response) {
items.removeClass('search-match').show();
this.tree.restore();
return;
}
items.removeClass('search-match').hide();
response.results.forEach((page) => {
let match = items.filter(`[data-nav-id="${page}"]`).addClass('search-match').show();
match.parents('[data-nav-id]').addClass('search-match').show();
this.tree.expand(page, 'no-store');
});
}
setValues({ flags = '', query = ''}, silent) {
let flagsArray = flags.replace(/(\s{1,})?,(\s{1,})?/g, ',').split(',');
if (this.filters.val() !== flags) {
let selectize = this.filters.data('selectize');
this.filters[selectize ? 'setValue' : 'val'](flagsArray, silent);
}
if (this.search.val() !== query) { this.search.val(query); }
localStorage.setItem('grav:admin:pages:filter', JSON.stringify({ flags, query }));
}
resetValues() {
this.setValues('', 'silent');
this.refreshDOM();
}
_initSelectize() {
let extras = {
type: this.filters.data('filter-types') || {},
access: this.filters.data('filter-access-levels') || {}
};
Object.keys(extras).forEach((cat) => {
Object.keys(extras[cat]).forEach((key) => {
this.options.push({
cat,
key,
flag: extras[cat][key]
});
});
});
this.filters.selectize({
maxItems: null,
valueField: 'key',
labelField: 'flag',
searchField: ['flag', 'key'],
options: this.options,
optgroups: this.labels,
optgroupField: 'cat',
optgroupLabelField: 'name',
optgroupValueField: 'id',
optgroupOrder: this.labels.map((item) => item.id),
plugins: ['optgroup_columns', 'required-fix']
});
}
}
let Instance = new PagesFilter('input[name="page-filter"]', 'input[name="page-search"]');
export { Instance };

View File

@@ -0,0 +1,53 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import PageFilters, { Instance as PageFiltersInstance } from './filter';
import Page from './page';
const pad = (n, s) => (`000${n}`).substr(-s);
// Pages Ordering
let Ordering = null;
let orderingElement = $('#ordering');
if (orderingElement.length) {
Ordering = new Sortable(orderingElement.get(0), {
filter: '.ignore',
onUpdate: function() {
/* Old single page index behavior
let item = $(event.item);
let index = orderingElement.children().index(item) + 1;
$('[data-order]').val(index);
*/
let indexes = [];
const children = orderingElement.children();
const padZero = (children.length + '').split('').length;
children.each((index, item) => {
item = $(item);
indexes.push(item.data('id'));
item.find('.page-order').text(`${pad(index + 1, padZero)}.`);
});
$('[data-order]').val(indexes.join(','));
}
});
$(document).on('input', '[name="data[folder]"]', (event) => {
const target = $(event.currentTarget);
const activeOrder = $('[data-id][data-active-id]');
activeOrder.data('id', target.val());
Ordering.options.onUpdate();
});
}
export default {
Ordering,
Page,
PageFilters: {
PageFilters,
Instance: PageFiltersInstance
}
};

View File

@@ -0,0 +1,69 @@
import $ from 'jquery';
import '../../utils/jquery-utils';
import request from '../../utils/request';
import { config } from 'grav-config';
let custom = false;
let folder = $('[data-remodal-id="modal"] input[name="data[folder]"], [data-remodal-id="module"] input[name="data[folder]"], [data-remodal-id="modal-page-copy"] input[name="data[folder]"]');
let title = $('[data-remodal-id="modal"] input[name="data[title]"], [data-remodal-id="module"] input[name="data[title]"], [data-remodal-id="modal-page-copy"] input[name="data[title]"]');
let getFields = (type, target) => {
target = $(target);
let query = `[data-remodal-id="${target.closest('[data-remodal-id]').data('remodal-id')}"]`;
return {
title: type === 'title' ? $(target) : $(`${query} input[name="data[title]"]`),
folder: type === 'folder' ? $(target) : $(`${query} input[name="data[folder]"]`)
};
};
title.on('input focus blur', (event) => {
if (custom) { return true; }
let elements = getFields('title', event.currentTarget);
let slug = $.slugify(elements.title.val(), {custom: { "'": '', '': '', '': '' }});
elements.folder.val(slug);
});
folder.on('input', (event) => {
let elements = getFields('folder', event.currentTarget);
let input = elements.folder.get(0);
let value = elements.folder.val();
let selection = {
start: input.selectionStart,
end: input.selectionEnd
};
value = value.toLowerCase().replace(/\s/g, '-').replace(/[^a-z0-9_\-]/g, '');
elements.folder.val(value);
custom = !!value;
// restore cursor position
input.setSelectionRange(selection.start, selection.end);
});
folder.on('focus blur', (event) => {
getFields('title').title.trigger('input');
});
$(document).on('change', '[name="data[route]"]', (event) => {
const rawroute = $(event.currentTarget).val();
const pageTemplate = $('[name="data[name]"]');
const URI = `${config.base_url_relative}/ajax.json/task${config.param_sep}getChildTypes`;
if (pageTemplate.length === 0) {
return;
}
request(URI, {
method: 'post',
body: { rawroute }
}, (response) => {
const type = response.child_type;
if (type !== '' && type !== 'default') {
pageTemplate.val(type);
pageTemplate.data('selectize').setValue(type);
}
});
});

View File

@@ -0,0 +1,15 @@
import $ from 'jquery';
$(document).on('click', '[data-remodal-target="delete"]', function() {
let confirm = $('[data-remodal-id="delete"] [data-delete-action]');
let link = $(this).data('delete-url');
confirm.data('delete-action', link);
});
$(document).on('click', '[data-delete-action]', function() {
let remodal = $.remodal.lookup[$('[data-remodal-id="delete"]').data('remodal')];
global.location.href = $(this).data('delete-action');
remodal.close();
});

View File

@@ -0,0 +1,5 @@
import $ from 'jquery';
$('.disable-after-click').on('click', function() {
$(this).addClass('pointer-events-disabled');
});

View File

@@ -0,0 +1,47 @@
import $ from 'jquery';
import './add';
import './move';
import './delete';
import './unset';
import './disable-buttons';
import PageMedia, { Instance as PageMediaInstances } from './media';
import './multilang';
const switcher = $('input[type="radio"][name="mode-switch"]');
if (switcher) {
let link = switcher.closest(':checked').data('leave-url');
let fakeLink = $(`<a href="${link}" />`);
switcher.parent().append(fakeLink);
switcher.siblings('label').on('mousedown touchdown', (event) => {
event.preventDefault();
// let remodal = $.remodal.lookup[$('[data-remodal-id="changes"]').data('remodal')];
let confirm = $('[data-remodal-id="changes"] [data-leave-action="continue"]');
confirm.one('click', () => {
$(global).on('beforeunload._grav');
fakeLink.off('click._grav');
$(event.target).trigger('click');
});
fakeLink.trigger('click._grav');
});
switcher.on('change', (event) => {
let radio = $(event.target);
link = radio.data('leave-url');
setTimeout(() => fakeLink.attr('href', link).get(0).click(), 5);
});
}
export default {
Media: {
PageMedia,
PageMediaInstances
}
};

View File

@@ -0,0 +1,293 @@
import $ from 'jquery';
import Cookies from '../../utils/cookies.js';
import request from '../../utils/request';
import FilesField, { UriToMarkdown } from '../../forms/fields/files';
import { config, translations } from 'grav-config';
import { Instance as Editor } from '../../forms/fields/editor';
import Sortable from 'sortablejs';
const previewTemplate = `
<div class="dz-preview dz-file-preview">
<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-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>
<a class="dz-insert" title="${translations.PLUGIN_ADMIN.INSERT}" href="javascript:undefined;" data-dz-insert>${translations.PLUGIN_ADMIN.INSERT}</a>
</div>`.trim();
export default class PageMedia extends FilesField {
constructor({ container = '#grav-dropzone', options = {} } = {}) {
options = Object.assign(options, { previewTemplate });
super({ container, options });
if (!this.container.length) { return; }
this.urls = {
fetch: `${this.container.data('media-url')}/task${config.param_sep}listmedia`,
add: `${this.container.data('media-url')}/task${config.param_sep}addmedia`,
delete: `${this.container.data('media-url')}/task${config.param_sep}delmedia`
};
this.dropzone.options.url = this.urls.add;
if (typeof this.options.fetchMedia === 'undefined' || this.options.fetchMedia) {
this.fetchMedia();
}
if (typeof this.options.attachDragDrop === 'undefined' || this.options.attachDragDrop) {
this.attachDragDrop();
}
const field = $(`[name="${this.container.data('dropzone-field')}"]`);
if (field.length) {
this.sortable = new Sortable(this.container.get(0), {
animation: 150,
// forceFallback: true,
setData: (dataTransfer, target) => {
target = $(target);
let uri = encodeURI(target.find('.dz-filename').text());
let shortcode = UriToMarkdown(uri);
this.dropzone.disable();
target.addClass('hide-backface');
dataTransfer.effectAllowed = 'copy';
dataTransfer.setData('text', shortcode);
},
onSort: () => {
let names = [];
this.container.find('[data-dz-name]').each((index, file) => {
file = $(file);
const name = file.text().trim();
names.push(name);
});
field.val(names.join(','));
}
});
}
}
fetchMedia() {
const order = this.container.closest('.form-field').find('[name="data[header][media_order]"]').val();
const body = { uri: this.getURI(), order };
let url = this.urls.fetch;
request(url, { method: 'post', body }, (response) => {
let results = response.results;
Object.keys(results).forEach((name) => {
let data = results[name];
let mock = { name, size: data.size, accepted: true, extras: data };
this.dropzone.files.push(mock);
this.dropzone.options.addedfile.call(this.dropzone, mock);
this.dropzone.options.thumbnail.call(this.dropzone, mock, data.url);
});
this.updateThumbsSize();
this.container.find('.dz-preview').prop('draggable', 'true');
});
}
onDropzoneSending(file, xhr, formData) {
/*
// Cannot call super because Safari and IE API don't implement `delete`
super.onDropzoneSending(file, xhr, formData);
formData.delete('task');
*/
formData.append('name', this.options.dotNotation || file.name);
formData.append('admin-nonce', config.admin_nonce);
formData.append('uri', this.getURI());
}
onDropzoneComplete(file) {
super.onDropzoneComplete(file);
if (this.sortable) {
this.sortable.options.onSort();
}
// accepted
this.updateThumbsSize();
this.updateMediaCount();
$('.dz-preview').prop('draggable', 'true');
}
onDropzoneAddedFile(file, ...extra) {
super.onDropzoneAddedFile(file, extra);
this.updateThumbsSize();
}
onDropzoneRemovedFile(file, ...extra) {
super.onDropzoneRemovedFile(file, ...extra);
this.updateMediaCount();
if (this.sortable) {
this.sortable.options.onSort();
}
}
updateThumbsSize() {
const status = JSON.parse(Cookies.get('grav-admin-pagemedia') || '{}');
if (status.width) {
const input = this.container.closest('.pagemedia-field').find('.media-resizer');
updateMediaSizes(input[0], status.width, false);
}
}
updateMediaCount() {
const element = this.container.closest('.pagemedia-field').find('[data-pagemedia-count]');
element.text(`(${this.dropzone.files.length})`);
}
attachDragDrop() {
this.container.delegate('[data-dz-insert]', 'click', (e) => {
let target = $(e.currentTarget).parent('.dz-preview').find('.dz-filename');
let editor = Editor.editors.filter((index, editor) => $(editor).attr('name') === 'data[content]');
if (editor.length) {
editor = editor.data('codemirror');
editor.focus();
let filename = encodeURI(target.text());
let shortcode = UriToMarkdown(filename);
editor.doc.replaceSelection(shortcode);
}
});
this.container.delegate('[data-dz-view]', 'mouseenter', (e) => {
let target = $(e.currentTarget);
let file = target.parent('.dz-preview').find('.dz-filename');
let filename = encodeURI(file.text());
let URL = target.closest('[data-media-path]').data('media-path');
let original = this.dropzone.files.filter((file) => encodeURI(file.name) === filename).shift();
original = original && ((original.extras && original.extras.original) || encodeURI(original.name));
target.attr('href', `${URL}/${original}`);
});
this.container.delegate('[data-dz-metadata]', 'click', (e) => {
e.preventDefault();
const target = $(e.currentTarget);
const file = target.parent('.dz-preview').find('.dz-filename');
const filename = encodeURI(file.text());
const cleanName = file.text().replace('<', '&lt;').replace('>', '&gt;');
let fileObj = this.dropzone.files.filter((file) => file.name === global.decodeURI(filename)).shift() || {};
if (!fileObj.extras) {
fileObj.extras = { metadata: [] };
}
if (Array.isArray(fileObj.extras.metadata) && !fileObj.extras.metadata.length) {
fileObj.extras.metadata = { '': `${cleanName}.meta.yaml doesn't exist` };
}
fileObj = fileObj.extras;
const modal_element = $('body').find('[data-remodal-id="metadata"]');
const modal = $.remodal.lookup[modal_element.data('remodal')];
modal_element.find('h1 strong').html(cleanName);
if (fileObj.url) {
modal_element.find('.meta-preview').html(`<img src="${fileObj.url}" />`);
}
const container = modal_element.find('.meta-content').html('<ul />').find('ul');
Object.keys(fileObj.metadata).forEach((meta) => {
const cleanMeta = fileObj.metadata[meta].replace('<', '&lt;').replace('>', '&gt;');
container.append(`<li><strong>${meta ? meta + ':' : ''}</strong> ${cleanMeta}</li>`);
});
modal.open();
});
this.container.delegate('.dz-preview', 'dragstart', (e) => {
let target = $(e.currentTarget);
let uri = encodeURI(target.find('.dz-filename').text());
let shortcode = UriToMarkdown(uri);
this.dropzone.disable();
target.addClass('hide-backface');
e.originalEvent.dataTransfer.effectAllowed = 'copy';
e.originalEvent.dataTransfer.setData('text', shortcode);
});
this.container.delegate('.dz-preview', 'dragend', (e) => {
let target = $(e.currentTarget);
this.dropzone.enable();
target.removeClass('hide-backface');
});
}
}
export const updateMediaSizes = (input, width, store = true) => {
const storageLocation = input.dataset.storageLocation || 'grav-admin-pagemedia';
const status = JSON.parse(Cookies.get(storageLocation) || '{}');
const height = 150 * width / 200;
const media = $(input).closest('.pagemedia-field').find('.dz-details, [data-dz-thumbnail]');
media.css({ width, height });
if (store) {
const data = Object.assign({}, status, { width });
Cookies.set(storageLocation, JSON.stringify(data), { expires: Infinity });
}
};
export const updateMediaCollapseStatus = (element, store = true) => {
const storageLocation = element.dataset.storageLocation || 'grav-admin-pagemedia';
const status = JSON.parse(Cookies.get(storageLocation) || '{}');
element = $(element);
const icon = element.find('i.fa');
const container = element.closest('.pagemedia-field');
const panel = container.find('.form-data');
const slider = container.find('.media-resizer').parent();
const isCollapsed = !icon.hasClass('fa-chevron-down');
const collapsed = !isCollapsed;
icon.removeClass('fa-chevron-down fa-chevron-right').addClass(isCollapsed ? 'fa-chevron-down' : 'fa-chevron-right');
slider[isCollapsed ? 'removeClass' : 'addClass']('hidden');
panel[isCollapsed ? 'slideDown' : 'slideUp']();
if (store) {
const data = Object.assign({}, status, { collapsed });
Cookies.set(storageLocation, JSON.stringify(data), { expires: Infinity });
}
};
$(document).on('input', '.media-resizer', (event) => {
const target = $(event.currentTarget);
const width = target.val();
updateMediaSizes(event.currentTarget, width);
});
$(document).on('click', '.media-collapser', (event) => {
updateMediaCollapseStatus(event.currentTarget);
});
$(document).ready(() => {
$('.media-resizer').each((index, input) => {
const storageLocation = input.dataset.storageLocation || 'grav-admin-pagemedia';
const status = JSON.parse(Cookies.get(storageLocation) || '{}');
if (status.width) {
updateMediaSizes(input, status.width, false);
}
});
});
export let Instance = new PageMedia();

View File

@@ -0,0 +1,63 @@
import $ from 'jquery';
$(document).on('click', '[data-page-move] button[name="task"][value="save"]', (event) => {
/* let route = $('form#blueprints:first select[name="data[route]"]');
let moveTo = $('[data-page-move] select').val();
if (route.length && route.val() !== moveTo) {
let selectize = route.data('selectize');
route.val(moveTo);
if (selectize) selectize.setValue(moveTo);
}*/
const modal = $(event.currentTarget).closest('[data-remodal-id]');
const parents = modal.data('parents') || {};
const finder = parents.finder;
if (!parents || !finder) { return true; }
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];
field.val(value);
parentLabel.text(value);
parentName.text(name);
finder.config.defaultPath = value;
$('<div />').css({
backgroundColor: 'rgba(255, 255, 255, 0.1)',
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 15000
}).appendTo($('body'));
});
/*
$(document).on('click', '[data-remodal-id="parents"] [data-parents-select]', (event) => {
const modal = $(event.currentTarget).closest('[data-remodal-id]');
const parents = modal.data('parents');
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];
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();
});
*/

View File

@@ -0,0 +1,22 @@
import $ from 'jquery';
$('[name="task"][value="saveas"], [name="task"][value="switchlanguage"]').on('mousedown touchstart', (event) => {
let fields = ['lang', 'redirect'];
let element = $(event.currentTarget);
let form = $(`#${element.attr('form')}`);
if (!form.length) { return; }
fields.forEach((field) => {
let value = element.attr(field);
if (!value) { return; }
let input = form.find(`[name="data[${field}]"]`);
if (!input.length) {
input = $(`<input type="hidden" name="data[${field}]" value="" />`);
form.append(input);
}
input.val(value);
});
return true;
});

View File

@@ -0,0 +1,18 @@
import $ from 'jquery';
$(document).on('click', '.dz-unset', function() {
const file_upload = $(this).closest('.files-upload');
$(this).closest('.dz-image-preview').remove();
const unset_image = $(this).closest('.dz-image-preview').find('[data-dz-name]').text().trim();
const images = JSON.parse(file_upload.find('input[data-grav-field="hidden"]').val()) || {};
let image_array = {};
$.each(images, function(ind, obj) {
if (!ind.endsWith(unset_image)) {
image_array[ind] = obj;
}
});
file_upload.find('input[data-grav-field="hidden"]').val(JSON.stringify(image_array));
});

View File

@@ -0,0 +1,125 @@
import $ from 'jquery';
import '../utils/storage';
const sessionKey = 'grav:admin:pages';
if (!sessionStorage.getItem(sessionKey)) {
sessionStorage.setItem(sessionKey, '{}');
}
export default class PagesTree {
constructor(query, elements = undefined) {
this.query = query;
this.elements = $(elements !== undefined ? elements : this.query);
this.session = JSON.parse(sessionStorage.getItem(sessionKey) || '{}');
if (!this.elements.length) { return; }
this.restore();
this.elements.find('.page-icon').on('click', (event) => this.toggle(event.target));
this.elements.data('tree_init', 1);
$('[data-page-toggleall]').on('click', (event) => {
let element = $(event.target).closest('[data-page-toggleall]');
let action = element.data('page-toggleall');
this[action]();
});
}
reload() {
const elements = $(this.query).filter((index, element) => !$(element).data('tree_init'));
if (!elements.length) { return; }
this.constructor(this.query, elements);
}
toggle(elements, dontStore = false) {
if (typeof elements === 'string') {
elements = $(`[data-nav-id="${elements}"]`).find('[data-toggle="children"]');
}
elements = $(elements || this.elements);
elements.each((index, element) => {
element = $(element);
let state = this.getState(element.closest('[data-toggle="children"]'));
this[state.isOpen ? 'collapse' : 'expand'](state.id, dontStore);
});
}
collapse(elements, dontStore = false) {
if (typeof elements === 'string') {
elements = $(`[data-nav-id="${elements}"]`).find('[data-toggle="children"]');
}
elements = $(elements || this.elements);
elements.each((index, element) => {
element = $(element);
let state = this.getState(element);
if (state.isOpen) {
state.children.hide();
state.icon.removeClass('children-open').addClass('children-closed');
if (!dontStore) { delete this.session[state.id]; }
}
});
if (!dontStore) { this.save(); }
}
expand(elements, dontStore = false) {
if (typeof elements === 'string') {
let element = $(`[data-nav-id="${elements}"]`);
let parents = element.parents('[data-nav-id]');
// loop back through parents, we don't want to expand an hidden child
if (parents.length) {
parents = parents.find('[data-toggle="children"]:first');
parents = parents.add(element.find('[data-toggle="children"]:first'));
return this.expand(parents, dontStore);
}
elements = element.find('[data-toggle="children"]:first');
}
elements = $(elements || this.elements);
elements.each((index, element) => {
element = $(element);
let state = this.getState(element);
if (!state.isOpen) {
state.children.show();
state.icon.removeClass('children-closed').addClass('children-open');
if (!dontStore) { this.session[state.id] = 1; }
}
});
if (!dontStore) { this.save(); }
}
restore() {
this.collapse(null, true);
Object.keys(this.session).forEach((key) => {
this.expand(key, 'no-store');
});
}
save() {
return sessionStorage.setItem(sessionKey, JSON.stringify(this.session));
}
getState(element) {
element = $(element);
return {
id: element.closest('[data-nav-id]').data('nav-id'),
children: element.closest('li.page-item').find('ul:first'),
icon: element.find('.page-icon'),
get isOpen() { return this.icon.hasClass('children-open'); }
};
}
}
let Instance = new PagesTree('[data-toggle="children"]');
export { Instance };

View File

@@ -0,0 +1,92 @@
import $ from 'jquery';
import packages from '../utils/packages';
import camelCase from 'mout/string/camelCase';
import debounce from 'debounce';
import contains from 'mout/string/contains';
// Plugins sliders details
$('.gpm-name, .gpm-actions').on('click', function(e) {
let element = $(this);
let target = $(e.target);
let tag = target.prop('tagName').toLowerCase();
if (tag === 'a' || element.parent('a').length || target.parent('a').length) { return true; }
let wrapper = element.siblings('.gpm-details').find('.table-wrapper');
wrapper.slideToggle({
duration: 350,
complete: () => {
let visible = wrapper.is(':visible');
wrapper
.closest('tr')
.find('.gpm-details-expand i')
.removeClass('fa-chevron-' + (visible ? 'down' : 'up'))
.addClass('fa-chevron-' + (visible ? 'up' : 'down'));
}
});
});
// Removing plugin
$(document).on('click', '[data-plugin-action="remove-package"]', (event) => {
packages.handleRemovingPackage('plugin', event);
});
// Reinstall plugin
$(document).on('click', '[data-plugin-action="reinstall-package"]', (event) => {
packages.handleReinstallPackage('plugin', event);
});
$(document).on('click', '[data-plugin-action="remove-dependency-package"]', (event) => {
packages.handleRemovingDependency('plugin', event);
});
// Trigger the add new plugin / update plugin modal
$(document).on('click', '[data-plugin-action="start-package-installation"]', (event) => {
packages.handleGettingPackageDependencies('plugin', event, 'install');
});
// Trigger the update all plugins modal
$(document).on('click', '[data-plugin-action="start-packages-update"]', (event) => {
packages.handleGettingPackageDependencies('plugin', event);
});
// Install a plugin dependencies and the plugin
$(document).on('click', '[data-plugin-action="install-dependencies-and-package"]', (event) => {
packages.handleInstallingDependenciesAndPackage('plugin', event);
});
// Install a plugin
$(document).on('click', '[data-plugin-action="install-package"]', (event) => {
packages.handleInstallingPackage('plugin', event);
});
// Sort plugins/themes dropdown
$(document).on('change', '.sort-actions select', (event) => {
let direction = $('.sort-actions .sort-icon .fa').hasClass('fa-sort-amount-desc') ? 'desc' : 'asc';
let sorting = $(event.currentTarget).val();
packages.Sort[camelCase(`by-${sorting}`)](direction);
});
// Sort plugins/themes icon
$(document).on('click', '.sort-icon', (event) => {
let icon = $(event.currentTarget).find('.fa');
let current = icon.hasClass('fa-sort-amount-asc') ? 'asc' : 'desc';
let opposite = current === 'asc' ? 'desc' : 'asc';
icon.removeClass(`fa-sort-amount-${current}`).addClass(`fa-sort-amount-${opposite}`);
$('.sort-actions select').trigger('change');
});
// Filter plugin/theme
$(document).on('input', '[data-gpm-filter]', debounce((event) => {
let value = $($(event.currentTarget)).val();
let items = $('[data-gpm-plugin], [data-gpm-theme]');
items.hide().filter((index, item) => {
item = $(item);
return contains(item.data('gpm-plugin'), value) || contains(item.data('gpm-theme'), value) || contains(item.data('gpm-name').toLowerCase(), value.toLowerCase());
}).show();
}, 250));

View File

@@ -0,0 +1,46 @@
import $ from 'jquery';
import packages from '../utils/packages';
// Themes Switcher Warning
$(document).on('mousedown', '[data-remodal-target="theme-switch-warn"]', (event) => {
let name = $(event.target).closest('[data-gpm-theme]').find('.gpm-name a:first').text();
let remodal = $('.remodal.theme-switcher');
remodal.find('strong').text(name);
remodal.find('.button.continue').attr('href', $(event.target).attr('href'));
});
// Removing theme
$(document).on('click', '[data-theme-action="remove-package"]', (event) => {
packages.handleRemovingPackage('theme', event);
});
// Reinstall theme
$(document).on('click', '[data-theme-action="reinstall-package"]', (event) => {
packages.handleReinstallPackage('theme', event);
});
$(document).on('click', '[data-theme-action="remove-dependency-package"]', (event) => {
packages.handleRemovingDependency('theme', event);
});
// Opened the add new theme / update theme modal
$(document).on('click', '[data-theme-action="start-package-installation"]', (event) => {
packages.handleGettingPackageDependencies('theme', event, 'install');
});
// Trigger the update all themes modal
$(document).on('click', '[data-theme-action="start-packages-update"]', (event) => {
packages.handleGettingPackageDependencies('theme', event);
});
// Install a theme dependencies and the theme
$(document).on('click', '[data-theme-action="install-dependencies-and-package"]', (event) => {
packages.handleInstallingDependenciesAndPackage('theme', event);
});
// Install a theme
$(document).on('click', '[data-theme-action="install-package"]', (event) => {
packages.handleInstallingPackage('theme', event);
});

View File

@@ -0,0 +1 @@
import './logs';

View File

@@ -0,0 +1,14 @@
import $ from 'jquery';
import { setParam } from 'mout/queryString';
const prepareQuery = (key, value) => {
return setParam(global.location.href, key, value);
};
$(document).on('change', '.logs-content .block-select select[name]', (event) => {
const target = $(event.currentTarget);
const name = target.attr('name');
const value = target.val();
global.location.href = prepareQuery(name, value);
});

View File

@@ -0,0 +1,24 @@
import $ from 'jquery';
import request from '../utils/request';
const switcher = $('input[type="radio"][name="channel-switch"]');
if (switcher) {
switcher.on('change', (event) => {
let radio = $(event.target);
let url = `${radio.parent('[data-url]').data('url')}`;
request(url, {
method: 'post',
body: {
task: 'gpmRelease',
release: radio.val()
}
},
(response) => {
if (response.reload) {
global.location.reload();
}
});
});
}

View File

@@ -0,0 +1,26 @@
import $ from 'jquery';
import { Instance as gpm } from '../utils/gpm';
import { translations } from 'grav-config';
import toastr from '../utils/toastr';
// Check for updates trigger
$('[data-gpm-checkupdates]').on('click', function() {
let element = $(this);
element.find('i').addClass('fa-spin');
gpm.fetch((response) => {
element.find('i').removeClass('fa-spin');
let payload = response.payload;
if (!payload) { return; }
if (!payload.grav.isUpdatable && !payload.resources.total) {
toastr.success(translations.PLUGIN_ADMIN.EVERYTHING_UP_TO_DATE);
} else {
var grav = payload.grav.isUpdatable ? 'Grav v' + payload.grav.available : '';
var resources = payload.resources.total ? payload.resources.total + ' ' + translations.PLUGIN_ADMIN.UPDATES_ARE_AVAILABLE : '';
if (!resources) { grav += ' ' + translations.PLUGIN_ADMIN.IS_AVAILABLE_FOR_UPDATE; }
toastr.info(grav + (grav && resources ? ' ' + translations.PLUGIN_ADMIN.AND + ' ' : '') + resources);
}
}, true);
});

View File

@@ -0,0 +1,68 @@
import $ from 'jquery';
import { config } from 'grav-config';
import request from '../utils/request';
const URI = `${config.base_url_relative}/ajax.json/task${config.param_sep}getNewsFeed`;
class Feed {
constructor() {
this.data = null;
}
fetch(refresh = false, callback = function() {}) {
request(URI, {
method: 'post',
body: { refresh }
}, (response) => {
this.data = response;
callback(response);
});
}
refresh(refresh = false) {
const feed = $('#news-feed .widget-content');
if (!feed.length) { return; }
let loader = feed.find('.widget-loader');
loader.find('div').remove();
loader.find('.fa-warning').removeClass('fa-warning').addClass('fa-refresh fa-spin');
loader.show();
feed.find('> ul').hide();
if (!this.data || this.data.error || refresh) {
this.fetch(refresh, this.updateContent.bind(this));
} else {
this.updateContent();
}
}
updateContent() {
const feed = $('#news-feed .widget-content');
if (!feed.length) { return; }
let loader = feed.find('.widget-loader').hide();
let content = feed.find('> ul').empty().show();
if (this.data.error || this.data.status === 'error') {
loader.show().find('div').remove();
loader.find('.fa-refresh').removeClass('fa-refresh fa-spin').addClass('fa-warning');
loader.append(`<div>${this.data.error ? this.data.error.message : this.data.message || 'Unable to download news feed'}</div>`);
return;
}
if (this.data && this.data.feed_data) {
content.append(this.data.feed_data);
}
}
}
let feed = new Feed();
$(document).ready(() => feed.refresh());
$(document).on('click', '[data-refresh="feed"]', (event) => {
event.preventDefault();
feed.refresh(true);
});
export default feed;

View File

@@ -0,0 +1,196 @@
import $ from 'jquery';
import unique from 'mout/array/unique';
import { config, translations } from 'grav-config';
import { Instance as gpm } from '../utils/gpm';
import Notifications from './notifications';
import Feed from './feed';
import './check';
import './update';
import './channel-switcher';
export default class Updates {
constructor(payload = {}) {
this.setPayload(payload);
this.task = `task${config.param_sep}`;
this.updateURL = '';
}
setPayload(payload = {}) {
this.payload = payload;
return this;
}
fetch(force = false) {
gpm.fetch((response) => this.setPayload(response), force);
return this;
}
maintenance(mode = 'hide') {
let element = $('#updates [data-update-packages]');
element[mode === 'show' ? 'fadeIn' : 'fadeOut']();
if (mode === 'hide') {
$('.badges.with-updates').removeClass('with-updates').find('.badge.updates').remove();
}
return this;
}
grav() {
let payload = this.payload.grav;
if (payload && payload.isUpdatable) {
let task = this.task;
let bar = '';
if (!payload.isSymlink) {
this.updateURL = `${config.base_url_relative}/update.json/${task}updategrav/admin-nonce${config.param_sep}${config.admin_nonce}`;
bar += `<button data-remodal-target="update-grav" class="button button-small secondary pointer-events-none" id="grav-update-button">${translations.PLUGIN_ADMIN.UPDATE_GRAV_NOW} <span class="cnt-down">(5s)</span></button>`;
} else {
bar += `<span class="hint--left" style="float: right;" data-hint="${translations.PLUGIN_ADMIN.GRAV_SYMBOLICALLY_LINKED}"><i class="fa fa-fw fa-link"></i></span>`;
}
bar += `
Grav <b>v${payload.available}</b> ${translations.PLUGIN_ADMIN.IS_NOW_AVAILABLE}! <span class="less">(${translations.PLUGIN_ADMIN.CURRENT} v${payload.version})</span>
`;
let element = $('[data-gpm-grav]').removeClass('hidden');
if (element.is(':empty')) {
element.hide();
}
element
.addClass('grav')
.html(`${bar}`)
.slideDown(150, function() {
var c = 5;
var x = setInterval(function() {
c -= 1;
element.find('.pointer-events-none .cnt-down').text('(' + c + 's)');
}, 1000);
setTimeout(function() {
clearInterval(x);
element.find('.pointer-events-none .cnt-down').remove();
element.find('.pointer-events-none').removeClass('pointer-events-none');
}, 5000);
})
.parent('#messages').addClass('default-box-shadow');
}
return this;
}
resources() {
if (!this.payload || !this.payload.resources || !this.payload.resources.total) {
return this.maintenance('hide');
}
let is_current_package_latest = true;
let map = ['plugins', 'themes'];
let singles = ['plugin', 'theme'];
let { plugins, themes } = this.payload.resources;
if (!this.payload.resources.total) { return this; }
[plugins, themes].forEach(function(resources, index) {
if (!resources || Array.isArray(resources)) { return; }
let length = Object.keys(resources).length;
let type = map[index];
// sidebar
$(`#admin-menu a[href$="/${map[index]}"]`)
.find('.badges')
.addClass('with-updates')
.find('.badge.updates').text(length);
var type_translation = '';
// update all
if (type === 'plugins') {
type_translation = translations.PLUGIN_ADMIN.PLUGINS;
} else {
type_translation = translations.PLUGIN_ADMIN.THEMES;
}
let updateAll = $(`.grav-update.${type}`);
updateAll.css('display', 'block').html(`
<p>
<a href="#" class="button button-small secondary" data-remodal-target="update-packages" data-packages-slugs="${Object.keys(resources).join()}" data-${singles[index]}-action="start-packages-update">${translations.PLUGIN_ADMIN.UPDATE} ${translations.PLUGIN_ADMIN.ALL} ${type_translation}</a>
<i class="fa fa-bullhorn"></i>
${length} ${translations.PLUGIN_ADMIN.OF_YOUR} ${type_translation.toLowerCase()} ${translations.PLUGIN_ADMIN.HAVE_AN_UPDATE_AVAILABLE}
</p>
`);
let existing_slugs = $('[data-update-packages]').attr('data-packages-slugs') || '';
if (existing_slugs) {
existing_slugs = existing_slugs.split(',');
} else {
existing_slugs = [];
}
let slugs = unique(existing_slugs.concat(Object.keys(resources))).join();
$('[data-update-packages]').attr('data-packages-slugs', `${slugs}`);
Object.keys(resources).forEach(function(item) {
// listing page
let container = $(`[data-gpm-${singles[index]}="${item}"]`);
let element = container.find('.gpm-name');
let url = element.find('a');
let content_wrapper = container.parents('.content-wrapper');
if (type === 'plugins' && !element.find('.badge.update').length) {
element.append(`<a class="plugin-update-button" href="${url.attr('href')}"><span class="badge update">${translations.PLUGIN_ADMIN.UPDATE_AVAILABLE}!</span></a>`);
content_wrapper.addClass('has-updates');
} else if (type === 'themes') {
element.append(`<div class="gpm-ribbon"><a href="${url.attr('href')}">${translations.PLUGIN_ADMIN.UPDATE.toUpperCase()}</a></div>`);
content_wrapper.addClass('has-updates');
}
// details page
if (container.length) {
let details = $(`.grav-update.${singles[index]}`);
if (details.length) {
let releaseType = resources[item].type === 'testing' ? '<span class="gpm-testing">test release</span>' : '';
details.html(`
<p>
<a href="#" class="button button-small secondary" data-remodal-target="update-packages" data-packages-slugs="${item}" data-${singles[index]}-action="start-package-installation">${translations.PLUGIN_ADMIN.UPDATE} ${singles[index].charAt(0).toUpperCase() + singles[index].substr(1).toLowerCase()}</a>
<i class="fa fa-bullhorn"></i>
<strong>v${resources[item].available}</strong> ${releaseType} ${translations.PLUGIN_ADMIN.OF_THIS} ${singles[index]} ${translations.PLUGIN_ADMIN.IS_NOW_AVAILABLE}!
</p>
`).css('display', 'block');
is_current_package_latest = false;
}
}
});
$('[data-update-packages]').removeClass('hidden');
});
$('.content-wrapper').addClass('updates-checked');
if (!is_current_package_latest) {
$('.warning-reinstall-not-latest-release').removeClass('hidden');
}
}
}
let Instance = new Updates();
export { Instance, Notifications, Feed };
// automatically refresh UI for updates (graph, sidebar, plugin/themes pages) after every fetch
gpm.on('fetched', (response, raw) => {
Instance.setPayload(response.payload || {});
Instance.grav().resources();
});
if (config.enable_auto_updates_check === '1') {
gpm.fetch();
}

View File

@@ -0,0 +1,169 @@
import $ from 'jquery';
import { config } from 'grav-config';
import request from '../utils/request';
const canFetchNotifications = () => config.notifications.enabled;
const notificationsFilters = () => config.notifications.filters;
class Notifications {
static addShowAllInFeed() {
$('#notifications ul').append('<li class="show-all" data-notification-action="show-all-notifications">Show all</li>');
}
static showNotificationInFeed(notification) {
let notifications = $('#notifications').removeClass('hidden');
let loader = notifications.find('.widget-loader').hide();
let content = notifications.find('.widget-content > ul').show();
loader.find('div').remove();
loader.find('.fa-warning').removeClass('fa-warning').addClass('fa-refresh fa-spin');
content
.append(notification)
.find('li:nth-child(n+11)').addClass('hidden'); // hide all items > 10
if (content.find('li.hidden').length) {
Notifications.addShowAllInFeed();
}
}
static showNotificationInTop(notification) {
const container = $('.top-notifications-container');
const dummy = $('<div />').html(notification);
container.removeClass('hidden').append(dummy.children());
dummy.children().slideDown(150);
}
static showNotificationInDashboard(notification) {
const container = $('.dashboard-notifications-container');
const dummy = $('<div />').html(notification);
container.removeClass('hidden').append(dummy.children());
dummy.children().slideDown(150);
}
static showNotificationInPlugins(notification) {
const container = $('.plugins-notifications-container');
const dummy = $('<div />').html(notification);
container.removeClass('hidden').append(dummy.children());
dummy.children().slideDown(150);
}
static showNotificationInThemes(notification) {
const container = $('.themes-notifications-container');
const dummy = $('<div />').html(notification);
container.removeClass('hidden').append(dummy.children());
dummy.children().slideDown(150);
}
static processLocation(location, notification) {
switch (location) {
case 'feed':
Notifications.showNotificationInFeed(notification);
break;
case 'top':
if (!notification.read) {
Notifications.showNotificationInTop(notification);
}
break;
case 'dashboard':
if (!notification.read) {
Notifications.showNotificationInDashboard(notification);
}
break;
case 'plugins':
if (!notification.read) {
Notifications.showNotificationInPlugins(notification);
}
break;
case 'themes':
if (!notification.read) {
Notifications.showNotificationInThemes(notification);
}
break;
}
}
// Grav.default.Notifications.fetch()
fetch({ filter = notificationsFilters(), refresh = false } = {}) {
if (!canFetchNotifications()) {
return false;
}
let feed = $('#notifications');
let loader = feed.find('.widget-loader');
let content = feed.find('.widget-content > ul');
loader.find('div').remove();
loader.find('.fa-warning').removeClass('fa-warning').addClass('fa-refresh fa-spin');
loader.show();
content.hide();
let processNotifications = (response) => {
let notifications = response.notifications;
$('#notifications').find('.widget-content > ul').empty();
if (notifications) {
Object.keys(notifications).forEach((location) => Notifications.processLocation(location, notifications[location]));
}
};
request(`${config.base_url_relative}/task${config.param_sep}getNotifications`, {
method: 'post',
body: { refresh, filter }
}, (response) => {
processNotifications(response);
}).catch(() => {
let widget = $('#notifications .widget-content');
widget
.find('.widget-loader')
.find('div').remove();
widget
.find('.widget-loader')
.append('<div>Failed to retrieve notifications</div>')
.find('.fa-spin')
.removeClass('fa-spin fa-refresh').addClass('fa-warning');
});
}
}
let notifications = new Notifications();
export default notifications;
if (canFetchNotifications()) {
notifications.fetch();
/* Hide a notification and store it hidden */
// <a href="#" data-notification-action="hide-notification" data-notification-id="${notification.id}" class="close hide-notification"><i class="fa fa-close"></i></a>
$(document).on('click', '[data-notification-action="hide-notification"]', (event) => {
let notification_id = $(event.target).parents('.hide-notification').data('notification-id');
let url = `${config.base_url_relative}/notifications.json/task${config.param_sep}hideNotification/notification_id${config.param_sep}${notification_id}`;
request(url, { method: 'post' }, () => {});
$(event.target).parents('.single-notification').hide();
});
$(document).on('click', '[data-notification-action="hide-notification"]', (event) => {
const target = $(event.currentTarget);
const notification = target.parent();
notification.slideUp(() => notification.remove());
});
$(document).on('click', '[data-notification-action="show-all-notifications"]', (event) => {
$('#notifications .show-all').hide();
$('#notifications .hidden').removeClass('hidden');
});
$(document).on('click', '[data-refresh="notifications"]', (event) => {
event.preventDefault();
notifications.fetch({ filter: ['feed'], refresh: true });
});
}

View File

@@ -0,0 +1,22 @@
import $ from 'jquery';
import { translations } from 'grav-config';
import formatBytes from '../utils/formatbytes';
import request from '../utils/request';
import { Instance as Update } from './index';
// Dashboard update and Grav update
$(document).on('click.remodal', '[data-remodal-id="update-grav"] [data-remodal-action="confirm"]', () => {
const element = $('#grav-update-button');
element.html(`${translations.PLUGIN_ADMIN.UPDATING_PLEASE_WAIT} ${formatBytes(Update.payload.grav.assets['grav-update'].size)}..`);
element.attr('disabled', 'disabled').find('> .fa').removeClass('fa-cloud-download').addClass('fa-refresh fa-spin');
request(Update.updateURL, (response) => {
if (response.type === 'updategrav') {
$('[data-gpm-grav]').remove();
$('#footer .grav-version').html(response.version);
}
element.removeAttr('disabled').find('> .fa').removeClass('fa-refresh fa-spin').addClass('fa-cloud-download');
});
});

View File

@@ -0,0 +1,32 @@
import $ from 'jquery';
import { config } from 'grav-config';
import request from '../utils/request';
const body = $('body');
// Dashboard update and Grav update
body.on('click', '[data-2fa-regenerate]', function(event) {
event.preventDefault();
let element = $(this);
let url = `${config.base_url_relative}/ajax.json/task${config.param_sep}regenerate2FASecret`;
element.attr('disabled', 'disabled').find('> .fa').addClass('fa-spin');
request(url, { method: 'post' }, (response) => {
$('[data-2fa-image]').attr('src', response.image);
$('[data-2fa-secret]').text(response.secret);
$('[data-2fa-value]').val(response.secret.replace(' ', ''));
element.removeAttr('disabled').find('> .fa').removeClass('fa-spin');
});
});
const toggleSecret = () => {
const toggle = $('#toggle_twofa_enabled1');
const secret = $('.twofa-secret');
secret[toggle.is(':checked') ? 'addClass' : 'removeClass']('show');
};
body.on('click', '.twofa-toggle input', toggleSecret);
toggleSecret();

View File

@@ -0,0 +1,210 @@
import jQuery from 'jquery';
/* ========================================================================
* Bootstrap: collapse.js v3.4.0
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
/* jshint latedef: false */
+(function($) {
'use strict';
// COLLAPSE PUBLIC CLASS DEFINITION
// ================================
var Collapse = function(element, options) {
this.$element = $(element);
this.options = $.extend({}, Collapse.DEFAULTS, options);
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
'[data-toggle="collapse"][data-target="#' + element.id + '"]');
this.transitioning = null;
if (this.options.parent) {
this.$parent = this.getParent();
} else {
this.addAriaAndCollapsedClass(this.$element, this.$trigger);
}
if (this.options.toggle) this.toggle();
};
Collapse.VERSION = '3.4.0';
Collapse.TRANSITION_DURATION = 350;
Collapse.DEFAULTS = {
toggle: true
};
Collapse.prototype.dimension = function() {
var hasWidth = this.$element.hasClass('width');
return hasWidth ? 'width' : 'height';
};
Collapse.prototype.show = function() {
if (this.transitioning || this.$element.hasClass('in')) return;
var activesData;
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing');
if (actives && actives.length) {
activesData = actives.data('bs.collapse');
if (activesData && activesData.transitioning) return;
}
var startEvent = $.Event('show.bs.collapse');
this.$element.trigger(startEvent);
if (startEvent.isDefaultPrevented()) return;
if (actives && actives.length) {
Plugin.call(actives, 'hide');
activesData || actives.data('bs.collapse', null);
}
var dimension = this.dimension();
this.$element
.removeClass('collapse')
.addClass('collapsing')[dimension](0)
.attr('aria-expanded', true);
this.$trigger
.removeClass('collapsed')
.attr('aria-expanded', true);
this.transitioning = 1;
var complete = function() {
this.$element
.removeClass('collapsing')
.addClass('collapse in')[dimension]('');
this.transitioning = 0;
this.$element
.trigger('shown.bs.collapse');
};
if (!$.support.transition) return complete.call(this);
var scrollSize = $.camelCase(['scroll', dimension].join('-'));
this.$element
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]);
};
Collapse.prototype.hide = function() {
if (this.transitioning || !this.$element.hasClass('in')) return;
var startEvent = $.Event('hide.bs.collapse');
this.$element.trigger(startEvent);
if (startEvent.isDefaultPrevented()) return;
var dimension = this.dimension();
this.$element[dimension](this.$element[dimension]())[0].offsetHeight;
this.$element
.addClass('collapsing')
.removeClass('collapse in')
.attr('aria-expanded', false);
this.$trigger
.addClass('collapsed')
.attr('aria-expanded', false);
this.transitioning = 1;
var complete = function() {
this.transitioning = 0;
this.$element
.removeClass('collapsing')
.addClass('collapse')
.trigger('hidden.bs.collapse');
};
if (!$.support.transition) return complete.call(this);
this.$element[dimension](0)
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION);
};
Collapse.prototype.toggle = function() {
this[this.$element.hasClass('in') ? 'hide' : 'show']();
};
Collapse.prototype.getParent = function() {
return $(this.options.parent)
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
.each($.proxy(function(i, element) {
var $element = $(element);
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element);
}, this))
.end();
};
Collapse.prototype.addAriaAndCollapsedClass = function($element, $trigger) {
var isOpen = $element.hasClass('in');
$element.attr('aria-expanded', isOpen);
$trigger
.toggleClass('collapsed', !isOpen)
.attr('aria-expanded', isOpen);
};
function getTargetFromTrigger($trigger) {
var href;
var target = $trigger.attr('data-target') ||
(href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, ''); // strip for ie7
return $(target);
}
// COLLAPSE PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function() {
var $this = $(this);
var data = $this.data('bs.collapse');
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option === 'object' && option);
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false;
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)));
if (typeof option === 'string') data[option]();
});
}
var old = $.fn.collapse;
$.fn.collapse = Plugin;
$.fn.collapse.Constructor = Collapse;
// COLLAPSE NO CONFLICT
// ====================
$.fn.collapse.noConflict = function() {
$.fn.collapse = old;
return this;
};
// COLLAPSE DATA-API
// =================
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function(e) {
var $this = $(this);
if (!$this.attr('data-target')) e.preventDefault();
var $target = getTargetFromTrigger($this);
var data = $target.data('bs.collapse');
var option = data ? 'toggle' : $this.data();
Plugin.call($target, option);
});
}(jQuery));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
import jQuery from 'jquery';
/* ========================================================================
* Bootstrap: dropdown.js v3.4.1
* https://getbootstrap.com/docs/3.4/javascript/#dropdowns
* ========================================================================
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/v3-dev/LICENSE)
* ======================================================================== */
+(function($) {
'use strict';
// DROPDOWN CLASS DEFINITION
// =========================
const backdrop = '.dropdown-backdrop';
const toggle = '[data-toggle="dropdown"]';
const Dropdown = function(element) {
$(element).on('click.bs.dropdown', this.toggle);
};
Dropdown.VERSION = '3.4.1';
function getParent($this) {
let selector = $this.attr('data-target');
if (!selector) {
selector = $this.attr('href');
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, ''); // strip for ie7
}
const $parent = selector !== '#' ? $(document).find(selector) : null;
return $parent && $parent.length ? $parent : $this.parent();
}
function clearMenus(e) {
if (e && e.which === 3) { return; }
$(backdrop).remove();
$(toggle).each(function() {
const $this = $(this);
const $parent = getParent($this);
const relatedTarget = { relatedTarget: this };
if (!$parent.hasClass('open')) { return; }
if (e && e.type === 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) { return; }
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget));
if (e.isDefaultPrevented()) { return; }
$this.attr('aria-expanded', 'false');
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget));
});
}
Dropdown.prototype.toggle = function(e) {
const $this = $(this);
if ($this.is('.disabled, :disabled')) { return; }
const $parent = getParent($this);
const isActive = $parent.hasClass('open');
clearMenus();
if (!isActive) {
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
// if mobile we use a backdrop because click events don't delegate
$(document.createElement('div'))
.addClass('dropdown-backdrop')
.insertAfter($(this))
.on('click', clearMenus);
}
const relatedTarget = { relatedTarget: this };
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget));
if (e.isDefaultPrevented()) { return; }
$this
.trigger('focus')
.attr('aria-expanded', 'true');
$parent
.toggleClass('open')
.trigger($.Event('shown.bs.dropdown', relatedTarget));
}
return false;
};
Dropdown.prototype.keydown = function(e) {
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return;
const $this = $(this);
e.preventDefault();
e.stopPropagation();
if ($this.is('.disabled, :disabled')) {
return;
}
const $parent = getParent($this);
const isActive = $parent.hasClass('open');
if (!isActive && e.which !== 27 || isActive && e.which === 27) {
if (e.which === 27) {
$parent.find(toggle).trigger('focus');
}
return $this.trigger('click');
}
const desc = ' li:not(.disabled):visible a';
const $items = $parent.find('.dropdown-menu' + desc);
if (!$items.length) {
return;
}
let index = $items.index(e.target);
if (e.which === 38 && index > 0) { index--; } // up
if (e.which === 40 && index < $items.length - 1) { index++; } // down
if (!~index) { index = 0; }
$items.eq(index).trigger('focus');
};
// DROPDOWN PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function() {
const $this = $(this);
let data = $this.data('bs.dropdown');
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)));
if (typeof option === 'string') data[option].call($this);
});
}
const old = $.fn.dropdown;
$.fn.dropdown = Plugin;
$.fn.dropdown.Constructor = Dropdown;
// DROPDOWN NO CONFLICT
// ====================
$.fn.dropdown.noConflict = function() {
$.fn.dropdown = old;
return this;
};
// APPLY TO STANDARD DROPDOWN ELEMENTS
// ===================================
$(document)
.on('click.bs.dropdown.data-api', clearMenus)
.on('click.bs.dropdown.data-api', '.dropdown form', function(e) { e.stopPropagation(); })
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown);
}(jQuery));

View File

@@ -0,0 +1,52 @@
import jQuery from 'jquery';
+(function($) {
'use strict';
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
// ============================================================
function transitionEnd() {
var el = document.createElement('bootstrap');
var transEndEventNames = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};
for (var name in transEndEventNames) {
if (el.style[name] !== undefined) {
return { end: transEndEventNames[name] };
}
}
return false; // explicit for ie8 ( ._.)
}
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function(duration) {
var called = false;
var $el = this;
$(this).one('bsTransitionEnd', function() { called = true; });
var callback = function() { if (!called) $($el).trigger($.support.transition.end); };
setTimeout(callback, duration);
return this;
};
$(function() {
$.support.transition = transitionEnd();
if (!$.support.transition) return;
$.event.special.bsTransitionEnd = {
bindType: $.support.transition.end,
delegateType: $.support.transition.end,
handle: function(e) {
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments);
}
};
});
}(jQuery));

View File

@@ -0,0 +1,25 @@
/* eslint-disable */
import $ from 'jquery';
let TRIGGER = null;
$(document).on('click', '[data-remodal-changelog]', (event) => {
TRIGGER = event.currentTarget;
});
$(document).on('opened', '[data-remodal-id="changelog"]', () => {
const instance = $.remodal.lookup[$('[data-remodal-id=changelog]').data('remodal')];
instance.$modal.html('<div class="changelog-overflow center" style="padding:5rem 0;text-align:center;"><i class="fa fa-spinner fa-spin fa-3x fa-fw"></i></div>');
if (!TRIGGER) { return true; }
const url = $(TRIGGER).data('remodalChangelog');
$.ajax({url: url}).done(function(data) {
instance.$modal.html(data);
});
});
$(document).on('closed', '[data-remodal-id="changelog"]', () => {
const instance = $.remodal.lookup[$('[data-remodal-id=changelog]').data('remodal')];
instance.$modal.html('');
});

View File

@@ -0,0 +1,152 @@
// Parses a string and returns a valid hex string when possible
// parseHex('#fff') => '#ffffff'
export const parseHex = (string) => {
string = string.replace(/[^A-F0-9]/ig, '');
if (string.length !== 3 && string.length !== 6) return '';
if (string.length === 3) {
string = string[0] + string[0] + string[1] + string[1] + string[2] + string[2];
}
return '#' + string.toLowerCase();
};
// Converts an HSB object to an RGB object
// hsb2rgb({h: 0, s: 0, b: 100}) => {r: 255, g: 255, b: 255}
export const hsb2rgb = (hsb) => {
let rgb = {};
let h = Math.round(hsb.h);
let s = Math.round(hsb.s * 255 / 100);
let v = Math.round(hsb.b * 255 / 100);
if (s === 0) {
rgb.r = rgb.g = rgb.b = v;
} else {
var t1 = v;
var t2 = (255 - s) * v / 255;
var t3 = (t1 - t2) * (h % 60) / 60;
if (h === 360) h = 0;
if (h < 60) {
rgb.r = t1;
rgb.b = t2;
rgb.g = t2 + t3;
} else if (h < 120) {
rgb.g = t1;
rgb.b = t2;
rgb.r = t1 - t3;
} else if (h < 180) {
rgb.g = t1;
rgb.r = t2;
rgb.b = t2 + t3;
} else if (h < 240) {
rgb.b = t1;
rgb.r = t2;
rgb.g = t1 - t3;
} else if (h < 300) {
rgb.b = t1;
rgb.g = t2;
rgb.r = t2 + t3;
} else if (h < 360) {
rgb.r = t1;
rgb.g = t2;
rgb.b = t1 - t3;
} else {
rgb.r = 0;
rgb.g = 0;
rgb.b = 0;
}
}
return {
r: Math.round(rgb.r),
g: Math.round(rgb.g),
b: Math.round(rgb.b)
};
};
// Converts an RGB object to a HEX string
// rgb2hex({r: 255, g: 255, b: 255}) => #ffffff
export const rgb2hex = (rgb) => {
var hex = [
rgb.r.toString(16),
rgb.g.toString(16),
rgb.b.toString(16)
];
hex.forEach((val, nr) => {
if (val.length === 1) hex[nr] = '0' + val;
});
return '#' + hex.join('');
};
// Converts and RGB(a) string to a HEX string
// rgbstr2hex('rgba(255, 255, 255, 0.5)') => #ffffff
export const rgbstr2hex = (rgb) => {
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
return (rgb && rgb.length === 4) ? '#' +
('0' + parseInt(rgb[1], 10).toString(16)).slice(-2) +
('0' + parseInt(rgb[2], 10).toString(16)).slice(-2) +
('0' + parseInt(rgb[3], 10).toString(16)).slice(-2) : '';
};
// Converts an HSB object to a HEX string
// hsb2hex({h: 0, s: 0, b: 100}) => #ffffff
export const hsb2hex = (hsb) => {
return rgb2hex(hsb2rgb(hsb));
};
// Converts a HEX string to an HSB object
// hex2hsb('#ffffff') => {h: 0, s: 0, b: 100}
export const hex2hsb = (hex) => {
let hsb = rgb2hsb(hex2rgb(hex));
if (hsb.s === 0) hsb.h = 360;
return hsb;
};
// Converts an RGB object to an HSB object
// rgb2hsb({r: 255, g: 255, b: 255}) => {h: 0, s: 0, b: 100}
export const rgb2hsb = (rgb) => {
let hsb = {
h: 0,
s: 0,
b: 0
};
let min = Math.min(rgb.r, rgb.g, rgb.b);
let max = Math.max(rgb.r, rgb.g, rgb.b);
let delta = max - min;
hsb.b = max;
hsb.s = max !== 0 ? 255 * delta / max : 0;
if (hsb.s !== 0) {
if (rgb.r === max) {
hsb.h = (rgb.g - rgb.b) / delta;
} else if (rgb.g === max) {
hsb.h = 2 + (rgb.b - rgb.r) / delta;
} else {
hsb.h = 4 + (rgb.r - rgb.g) / delta;
}
} else {
hsb.h = -1;
}
hsb.h *= 60;
if (hsb.h < 0) {
hsb.h += 360;
}
hsb.s *= 100 / 255;
hsb.b *= 100 / 255;
return hsb;
};
// Converts a HEX string to an RGB object
// hex2rgb('#ffffff') => {r: 255, g: 255, b: 255}
export const hex2rgb = (hex) => {
hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16);
return {
/* jshint ignore:start */
r: hex >> 16,
g: (hex & 0x00FF00) >> 8,
b: (hex & 0x0000FF)
/* jshint ignore:end */
};
};

View File

@@ -0,0 +1,164 @@
/*
* Cookies.js - 1.2.3-grav
* https://github.com/ScottHamper/Cookies
*
* With SameSite support by Grav
*
* This is free and unencumbered software released into the public domain.
*/
const factory = function(window) {
if (typeof window.document !== 'object') {
throw new Error('Cookies.js requires a `window` with a `document` object');
}
const Cookies = (key, value, options) => {
return arguments.length === 1
? Cookies.get(key)
: Cookies.set(key, value, options);
};
// Allows for setter injection in unit tests
Cookies._document = window.document;
// Used to ensure cookie keys do not collide with
// built-in `Object` properties
Cookies._cacheKeyPrefix = 'cookey.'; // Hurr hurr, :)
Cookies._maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC');
Cookies.defaults = {
path: '/',
secure: false,
sameSite: 'Lax'
};
Cookies.get = (key) => {
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
Cookies._renewCache();
}
const value = Cookies._cache[Cookies._cacheKeyPrefix + key];
return value === undefined ? undefined : decodeURIComponent(value);
};
Cookies.set = (key, value, options) => {
options = Cookies._getExtendedOptions(options);
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
Cookies._document.cookie = Cookies._generateCookieString(key, value, options);
return Cookies;
};
Cookies.expire = (key, options) => {
return Cookies.set(key, undefined, options);
};
Cookies._getExtendedOptions = (options) => {
return {
path: options && options.path || Cookies.defaults.path,
domain: options && options.domain || Cookies.defaults.domain,
expires: options && options.expires || Cookies.defaults.expires,
secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure,
sameSite: options && options.sameSite || Cookies.defaults.sameSite
};
};
Cookies._isValidDate = (date) => {
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
};
Cookies._getExpiresDate = (expires, now) => {
now = now || new Date();
if (typeof expires === 'number') {
expires = expires === Infinity
? Cookies._maxExpireDate
: new Date(now.getTime() + expires * 1000);
} else if (typeof expires === 'string') {
expires = new Date(expires);
}
if (expires && !Cookies._isValidDate(expires)) {
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
}
return expires;
};
Cookies._generateCookieString = (key, value, options) => {
key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent);
key = key.replace(/\(/g, '%28').replace(/\)/g, '%29');
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
options = options || {};
let cookieString = key + '=' + value;
cookieString += options.path ? ';path=' + options.path : '';
cookieString += options.domain ? ';domain=' + options.domain : '';
cookieString += options.expires ? ';expires=' + options.expires.toUTCString() : '';
cookieString += options.secure ? ';secure' : '';
cookieString += options.sameSite ? ';SameSite=' + options.sameSite : '';
return cookieString;
};
Cookies._getCacheFromString = (documentCookie) => {
let cookieCache = {};
const cookiesArray = documentCookie ? documentCookie.split('; ') : [];
for (let i = 0; i < cookiesArray.length; i++) {
const cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) {
cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value;
}
}
return cookieCache;
};
Cookies._getKeyValuePairFromCookieString = (cookieString) => {
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
let separatorIndex = cookieString.indexOf('=');
// IE omits the "=" when the cookie value is an empty string
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
const key = cookieString.substr(0, separatorIndex);
let decodedKey;
try {
decodedKey = decodeURIComponent(key);
} catch (e) {
if (console && typeof console.error === 'function') {
console.error('Could not decode cookie with key "' + key + '"', e);
}
}
return {
key: decodedKey,
value: cookieString.substr(separatorIndex + 1) // Defer decoding value until accessed
};
};
Cookies._renewCache = () => {
Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie);
Cookies._cachedDocumentCookie = Cookies._document.cookie;
};
Cookies._areEnabled = () => {
const testKey = 'cookies.js';
const areEnabled = Cookies.set(testKey, 1).get(testKey) === '1';
Cookies.expire(testKey);
return areEnabled;
};
Cookies.enabled = Cookies._areEnabled();
return Cookies;
};
global.Cookies = (global && typeof global.document === 'object') ? factory(global) : factory;
export default global.Cookies;

View File

@@ -0,0 +1,864 @@
/* eslint-disable */
import $ from 'jquery';
/*
* This file is part of the Arnapou jqCron package.
*
* (c) Arnaud Buathier <arnaud@arnapou.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Default settings
*/
var jqCronDefaultSettings = {
texts: {},
monthdays: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31],
hours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
hour_labels: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23"],
minutes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
lang: 'en',
enabled_minute: false,
enabled_hour: true,
enabled_day: true,
enabled_week: true,
enabled_month: true,
enabled_year: true,
multiple_dom: false,
multiple_month: false,
multiple_mins: false,
multiple_dow: false,
multiple_time_hours: false,
multiple_time_minutes: false,
numeric_zero_pad: false,
default_period: 'day',
default_value: '',
no_reset_button: true,
disabled: false,
bind_to: null,
bind_method: {
set: function($element, value) {
$element.is(':input') ? $element.val(value) : $element.data('jqCronValue', value);
},
get: function($element) {
return $element.is(':input') ? $element.val() : $element.data('jqCronValue');
}
}
};
/**
* Custom extend of json for jqCron settings.
* We don't use jQuery.extend because simple extend does not fit our needs, and deep extend has a bad
* feature for us : it replaces keys of "Arrays" instead of replacing the full array.
*/
(function($){
var extend = function(dst, src) {
for(var i in src) {
if($.isPlainObject(src[i])) {
dst[i] = extend(dst[i] && $.isPlainObject(dst[i]) ? dst[i] : {}, src[i]);
}
else if($.isArray(src[i])) {
dst[i] = src[i].slice(0);
}
else if(src[i] !== undefined) {
dst[i] = src[i];
}
}
return dst;
};
this.jqCronMergeSettings = function(obj) {
return extend(extend({}, jqCronDefaultSettings), obj || {});
};
}).call(window, $);
/**
* Shortcut to get the instance of jqCron instance from one jquery object
*/
(function($){
$.fn.jqCronGetInstance = function() {
return this.data('jqCron');
};
}).call(window, $);
/**
* Main plugin
*/
(function($){
$.fn.jqCron = function(settings) {
var saved_settings = settings;
return this.each(function() {
var cron, saved;
var $this = $(this);
var settings = jqCronMergeSettings(saved_settings); // clone settings
var translations = settings.texts[settings.lang];
if (typeof(translations) !== 'object' || $.isEmptyObject(translations)) {
console && console.error(
'Missing translations for language "' + settings.lang + '". ' +
'Please include jqCron.' + settings.lang + '.js or manually provide ' +
'the necessary translations when calling $.fn.jqCron().'
);
return;
}
if(!settings.jquery_container) {
if($this.is(':container')) {
settings.jquery_element = $this.uniqueId('jqCron');
}
else if($this.is(':autoclose')) {
// delete already generated dom if exists
if($this.next('.jqCron').length == 1) {
$this.next('.jqCron').remove();
}
// generate new
settings.jquery_element = $('<span class="jqCron"></span>').uniqueId('jqCron').insertAfter($this);
}
else {
console && console.error(settings.texts[settings.lang].error1.replace('%s', this.tagName));
return;
}
}
// autoset bind_to if it is an input
if($this.is(':input')) {
settings.bind_to = settings.bind_to || $this;
}
// init cron object
if(settings.bind_to){
if(settings.bind_to.is(':input')) {
// auto bind from input to object if an input, textarea ...
settings.bind_to.blur(function(){
var value = settings.bind_method.get(settings.bind_to);
$this.jqCronGetInstance().setCron(value);
});
}
saved = settings.bind_method.get(settings.bind_to);
cron = new jqCron(settings);
cron.setCron(saved);
}
else {
cron = new jqCron(settings);
}
$(this).data('jqCron', cron);
});
};
}).call(window, $);
/**
* jqCron class
*/
(function($){
var jqCronInstances = [];
function jqCron(settings) {
var _initialized = false;
var _self = this;
var _$elt = this;
var _$obj = $('<span class="jqCron-container"></span>');
var _$blocks = $('<span class="jqCron-blocks"></span>');
var _$blockPERIOD = $('<span class="jqCron-period"></span>');
var _$blockDOM = $('<span class="jqCron-dom"></span>');
var _$blockMONTH = $('<span class="jqCron-month"></span>');
var _$blockMINS = $('<span class="jqCron-mins"></span>');
var _$blockDOW = $('<span class="jqCron-dow"></span>');
var _$blockTIME = $('<span class="jqCron-time"></span>');
var _$cross = $('<span class="jqCron-cross">&#10008;</span>');
var _selectors = [];
var _selectorPeriod, _selectorMins, _selectorTimeH, _selectorTimeM, _selectorDow, _selectorDom, _selectorMonth;
// instanciate a new selector
function newSelector($block, multiple, type){
var selector = new jqCronSelector(_self, $block, multiple, type);
selector.$.bind('selector:open', function(){
// we close all opened selectors of all other jqCron
for(var n = jqCronInstances.length; n--; ){
if(jqCronInstances[n] != _self) {
jqCronInstances[n].closeSelectors();
}
else {
// we close all other opened selectors of this jqCron
for(var o = _selectors.length; o--; ){
if(_selectors[o] != selector) {
_selectors[o].close();
}
}
}
}
});
selector.$.bind('selector:change', function(){
var boundChanged = false;
// don't propagate if not initialized
if(!_initialized) return;
// bind data between two minute selectors (only if they have the same multiple settings)
if(settings.multiple_mins == settings.multiple_time_minutes) {
if(selector == _selectorMins) {
boundChanged = _selectorTimeM.setValue(_selectorMins.getValue());
}
else if(selector == _selectorTimeM) {
boundChanged = _selectorMins.setValue(_selectorTimeM.getValue());
}
}
// we propagate the change event to the main object
boundChanged || _$obj.trigger('cron:change', _self.getCron());
});
_selectors.push(selector);
return selector;
}
// disable the selector
this.disable = function(){
_$obj.addClass('disable');
settings.disable = true;
_self.closeSelectors();
};
// return if the selector is disabled
this.isDisabled = function() {
return settings.disable == true;
};
// enable the selector
this.enable = function(){
_$obj.removeClass('disable');
settings.disable = false;
};
// get cron value
this.getCron = function(){
var period = _selectorPeriod.getValue();
var items = ['*', '*', '*', '*', '*'];
if(period == 'hour') {
items[0] = _selectorMins.getCronValue();
}
if(period == 'day' || period == 'week' || period == 'month' || period == 'year') {
items[0] = _selectorTimeM.getCronValue();
items[1] = _selectorTimeH.getCronValue();
}
if(period == 'month' || period == 'year') {
items[2] = _selectorDom.getCronValue();
}
if(period == 'year') {
items[3] = _selectorMonth.getCronValue();
}
if(period == 'week') {
items[4] = _selectorDow.getCronValue();
}
return items.join(' ');
};
// set cron (string like * * * * *)
this.setCron = function(str) {
if(!str) return;
try {
str = str.replace(/\s+/g, ' ').replace(/^ +/, '').replace(/ +$/, ''); // sanitize
var mask = str.replace(/[^\* ]/g, '-').replace(/-+/g, '-').replace(/ +/g, '');
var items = str.split(' ');
if (items.length != 5) _self.error(_self.getText('error2'));
if(mask == '*****') { // 1 possibility
_selectorPeriod.setValue('minute');
}
else if(mask == '-****') { // 1 possibility
_selectorPeriod.setValue('hour');
_selectorMins.setCronValue(items[0]);
_selectorTimeM.setCronValue(items[0]);
}
else if(mask.substring(2, mask.length) == '***') { // 4 possibilities
_selectorPeriod.setValue('day');
_selectorMins.setCronValue(items[0]);
_selectorTimeM.setCronValue(items[0]);
_selectorTimeH.setCronValue(items[1]);
}
else if(mask.substring(2, mask.length) == '-**') { // 4 possibilities
_selectorPeriod.setValue('month');
_selectorMins.setCronValue(items[0]);
_selectorTimeM.setCronValue(items[0]);
_selectorTimeH.setCronValue(items[1]);
_selectorDom.setCronValue(items[2]);
}
else if(mask.substring(2, mask.length) == '**-') { // 4 possibilities
_selectorPeriod.setValue('week');
_selectorMins.setCronValue(items[0]);
_selectorTimeM.setCronValue(items[0]);
_selectorTimeH.setCronValue(items[1]);
_selectorDow.setCronValue(items[4]);
}
else if (mask.substring(3, mask.length) == '-*') { // 8 possibilities
_selectorPeriod.setValue('year');
_selectorMins.setCronValue(items[0]);
_selectorTimeM.setCronValue(items[0]);
_selectorTimeH.setCronValue(items[1]);
_selectorDom.setCronValue(items[2]);
_selectorMonth.setCronValue(items[3]);
}
else {
_self.error(_self.getText('error4'));
}
_self.clearError();
} catch(e) {}
};
// close all child selectors
this.closeSelectors = function(){
for(var n = _selectors.length; n--; ){
_selectors[n].close();
}
};
// get the main element id
this.getId = function(){
return _$elt.attr('id');
}
// get the translated text
this.getText = function(key) {
var text = settings.texts[settings.lang][key] || null;
if(typeof(text) == "string" && text.match('<b')){
text = text.replace(/(<b *\/>)/gi, '</span><b /><span class="jqCron-text">');
text = '<span class="jqCron-text">' + text + '</span>';
}
return text;
};
// get the human readable text
this.getHumanText = function() {
var texts=[];
_$obj
.find('> span > span:visible')
.find('.jqCron-text, .jqCron-selector > span')
.each(function() {
var text = $(this).text().replace(/\s+$/g, '').replace(/^\s+/g, '');
text && texts.push(text);
});
return texts.join(' ').replace(/\s:\s/g, ':');
}
// get settings
this.getSettings = function(){
return settings;
};
// display an error
this.error = function(msg) {
console && console.error('[jqCron Error] ' + msg);
_$obj.addClass('jqCron-error').attr('title', msg);
throw msg;
};
// clear error
this.clearError = function(){
_$obj.attr('title', '').removeClass('jqCron-error');
};
// clear
this.clear = function() {
_selectorDom.setValue([]);
_selectorDow.setValue([]);
_selectorMins.setValue([]);
_selectorMonth.setValue([]);
_selectorTimeH.setValue([]);
_selectorTimeM.setValue([]);
_self.triggerChange();
};
// init (called in constructor)
this.init = function(){
var n,i,labelsList,list;
if(_initialized) return;
settings = jqCronMergeSettings(settings);
settings.jquery_element || _self.error(_self.getText('error3'));
_$elt = settings.jquery_element;
_$elt.append(_$obj);
_$obj.data('id', settings.id);
_$obj.data('jqCron', _self);
_$obj.append(_$blocks);
settings.no_reset_button || _$obj.append(_$cross);
(!settings.disable) || _$obj.addClass('disable');
_$blocks.append(_$blockPERIOD);
if ( /^(ko)$/i.test(settings.lang) )
{
_$blocks.append(_$blockMONTH, _$blockDOM);
}
else
{
_$blocks.append(_$blockDOM, _$blockMONTH);
}
_$blocks.append(_$blockMINS);
_$blocks.append(_$blockDOW);
_$blocks.append(_$blockTIME);
// various binding
_$cross.click(function(){
_self.isDisabled() || _self.clear();
});
// binding from cron to target
_$obj.bind('cron:change', function(evt, value){
if(!settings.bind_to) return;
settings.bind_method.set && settings.bind_method.set(settings.bind_to, value);
_self.clearError();
});
// PERIOD
_$blockPERIOD.append(_self.getText('text_period'));
_selectorPeriod = newSelector(_$blockPERIOD, false, 'period');
settings.enabled_minute && _selectorPeriod.add('minute', _self.getText('name_minute'));
settings.enabled_hour && _selectorPeriod.add('hour', _self.getText('name_hour'));
settings.enabled_day && _selectorPeriod.add('day', _self.getText('name_day'));
settings.enabled_week && _selectorPeriod.add('week', _self.getText('name_week'));
settings.enabled_month && _selectorPeriod.add('month', _self.getText('name_month'));
settings.enabled_year && _selectorPeriod.add('year', _self.getText('name_year'));
_selectorPeriod.$.bind('selector:change', function(e, value){
_$blockDOM.hide();
_$blockMONTH.hide();
_$blockMINS.hide();
_$blockDOW.hide();
_$blockTIME.hide();
if(value == 'hour') {
_$blockMINS.show();
}
else if(value == 'day') {
_$blockTIME.show();
}
else if(value == 'week') {
_$blockDOW.show();
_$blockTIME.show();
}
else if(value == 'month') {
_$blockDOM.show();
_$blockTIME.show();
}
else if(value == 'year') {
_$blockDOM.show();
_$blockMONTH.show();
_$blockTIME.show();
}
});
_selectorPeriod.setValue(settings.default_period);
// MINS (minutes)
_$blockMINS.append(_self.getText('text_mins'));
_selectorMins = newSelector(_$blockMINS, settings.multiple_mins, 'minutes');
for(i=0, list=settings.minutes; i<list.length; i++){
_selectorMins.add(list[i], list[i]);
}
// TIME (hour:min)
_$blockTIME.append(_self.getText('text_time'));
_selectorTimeH = newSelector(_$blockTIME, settings.multiple_time_hours, 'time_hours');
for(i=0, list=settings.hours, labelsList=settings.hour_labels; i<list.length; i++){
_selectorTimeH.add(list[i], labelsList[i]);
}
_selectorTimeM = newSelector(_$blockTIME, settings.multiple_time_minutes, 'time_minutes');
for(i=0, list=settings.minutes; i<list.length; i++){
_selectorTimeM.add(list[i], list[i]);
}
// DOW (day of week)
_$blockDOW.append(_self.getText('text_dow'));
_selectorDow = newSelector(_$blockDOW, settings.multiple_dow, 'day_of_week');
for(i=0, list=_self.getText('weekdays'); i<list.length; i++){
_selectorDow.add(i+1, list[i]);
}
// DOM (day of month)
_$blockDOM.append(_self.getText('text_dom'));
_selectorDom = newSelector(_$blockDOM, settings.multiple_dom, 'day_of_month');
for(i=0, list=settings.monthdays; i<list.length; i++){
_selectorDom.add(list[i], list[i]);
}
// MONTH (day of week)
_$blockMONTH.append(_self.getText('text_month'));
_selectorMonth = newSelector(_$blockMONTH, settings.multiple_month, 'month');
for(i=0, list=_self.getText('months'); i<list.length; i++){
_selectorMonth.add(i+1, list[i]);
}
// close all selectors when we click in body
$('body').click(function(){
var i, n = _selectors.length;
for(i = 0; i < n; i++){
_selectors[i].close();
}
});
_initialized = true;
// default value
if(settings.default_value) {
_self.setCron(settings.default_value);
}
};
// trigger a change event
this.triggerChange = function(){
_$obj.trigger('cron:change', _self.getCron());
};
// store instance in array
jqCronInstances.push(this);
// expose main jquery object
this.$ = _$obj;
// init
try {
this.init();
_self.triggerChange();
} catch(e){}
}
this.jqCron = jqCron;
}).call(window, $);
/**
* jqCronSelector class
*/
(function($){
function jqCronSelector(_cron, _$block, _multiple, _type){
var _self = this;
var _$list = $('<ul class="jqCron-selector-list"></ul>');
var _$title = $('<span class="jqCron-selector-title"></span>');
var _$selector = $('<span class="jqCron-selector"></span>');
var _values = {};
var _value = [];
var _hasNumericTexts = true;
var _numeric_zero_pad = _cron.getSettings().numeric_zero_pad;
// return an array without doublon
function array_unique(l){
var i=0,n=l.length,k={},a=[];
while(i<n) {
k[l[i]] || (k[l[i]] = 1 && a.push(l[i]));
i++;
}
return a;
}
// get the value (an array if multiple, else a single value)
this.getValue = function(){
return _multiple ? _value : _value[0];
};
// get a correct string for cron
this.getCronValue = function(){
if(_value.length == 0) return '*';
var cron = [_value[0]], i, s = _value[0], c = _value[0], n = _value.length;
for(i=1; i<n; i++) {
if(_value[i] == c+1) {
c = _value[i];
cron[cron.length-1] = s+'-'+c;
}
else {
s = c = _value[i];
cron.push(c);
}
}
return cron.join(',');
};
// set the cron value
this.setCronValue = function(str) {
var values = [], m ,i, n;
if(str !== '*') {
while(str != '') {
// test "*/n" expression
m = str.match(/^\*\/([0-9]+),?/);
if(m && m.length == 2) {
for(i=0; i<=59; i+=(m[1]|0)) {
values.push(i);
}
str = str.replace(m[0], '');
continue;
}
// test "a-b/n" expression
m = str.match(/^([0-9]+)-([0-9]+)\/([0-9]+),?/);
if(m && m.length == 4) {
for(i=(m[1]|0); i<=(m[2]|0); i+=(m[3]|0)) {
values.push(i);
}
str = str.replace(m[0], '');
continue;
}
// test "a-b" expression
m = str.match(/^([0-9]+)-([0-9]+),?/);
if(m && m.length == 3) {
for(i=(m[1]|0); i<=(m[2]|0); i++) {
values.push(i);
}
str = str.replace(m[0], '');
continue;
}
// test "c" expression
m = str.match(/^([0-9]+),?/);
if(m && m.length == 2) {
values.push(m[1]|0);
str = str.replace(m[0], '');
continue;
}
// something goes wrong in the expression
return ;
}
}
_self.setValue(values);
};
// close the selector
this.close = function(){
_$selector.trigger('selector:close');
};
// open the selector
this.open = function(){
_$selector.trigger('selector:open');
};
// whether the selector is open
this.isOpened = function() {
return _$list.is(':visible');
};
// add a selected value to the list
this.addValue = function(key) {
var values = _multiple ? _value.slice(0) : []; // clone array
values.push(key);
_self.setValue(values);
};
// remove a selected value from the list
this.removeValue = function(key) {
if(_multiple) {
var i, newValue = [];
for(i=0; i<_value.length; i++){
if(key != [_value[i]]) {
newValue.push(_value[i]);
}
}
_self.setValue(newValue);
}
else {
_self.clear();
}
};
// set the selected value(s) of the list
this.setValue = function(keys){
var i, newKeys = [], saved = _value.join(' ');
if(!$.isArray(keys)) keys = [keys];
_$list.find('li').removeClass('selected');
keys = array_unique(keys);
keys.sort(function(a, b){
var ta = typeof(a);
var tb = typeof(b);
if(ta==tb && ta=="number") return a-b;
else return String(a) == String(b) ? 0 : (String(a) < String(b) ? -1 : 1);
});
if(_multiple) {
for(i=0; i<keys.length; i++){
if(keys[i] in _values) {
_values[keys[i]].addClass('selected');
newKeys.push(keys[i]);
}
}
}
else {
if(keys[0] in _values) {
_values[keys[0]].addClass('selected');
newKeys.push(keys[0]);
}
}
// remove unallowed values
_value = newKeys;
if(saved != _value.join(' ')) {
_$selector.trigger('selector:change', _multiple ? keys : keys[0]);
return true;
}
return false;
};
// get the title text
this.getTitleText = function(){
var getValueText = function(key) {
return (key in _values) ? _values[key].text() : key;
};
if(_value.length == 0) {
return _cron.getText('empty_' + _type) || _cron.getText('empty');
}
var cron = [getValueText(_value[0])], i, s = _value[0], c = _value[0], n = _value.length;
for(i=1; i<n; i++) {
if(_value[i] == c+1) {
c = _value[i];
cron[cron.length-1] = getValueText(s)+'-'+getValueText(c);
}
else {
s = c = _value[i];
cron.push(getValueText(c));
}
}
return cron.join(',');
};
// clear list
this.clear = function() {
_values = {};
_self.setValue([]);
_$list.empty();
};
// add a (key, value) pair
this.add = function(key, value) {
if(!(value+'').match(/^[0-9]+$/)) _hasNumericTexts = false;
if(_numeric_zero_pad && _hasNumericTexts && value < 10) {
value = '0'+value;
}
var $item = $('<li>' + value + '</li>');
_$list.append($item);
_values[key] = $item;
$item.click(function(){
if(_multiple && $(this).hasClass('selected')) {
_self.removeValue(key);
}
else {
_self.addValue(key);
if(!_multiple) _self.close();
}
});
};
// expose main jquery object
this.$ = _$selector;
// constructor
_$block.find('b:eq(0)').after(_$selector).remove();
_$selector
.addClass('jqCron-selector-' + _$block.find('.jqCron-selector').length)
.append(_$title)
.append(_$list)
.bind('selector:open', function(){
if(_hasNumericTexts) {
var nbcols = 1, n = _$list.find('li').length;
if(n > 5 && n <= 16) nbcols = 2;
else if(n > 16 && n <= 23) nbcols = 3;
else if(n > 23 && n <= 40) nbcols = 4;
else if(n > 40) nbcols = 5;
_$list.addClass('cols'+nbcols);
}
_$list.show();
})
.bind('selector:close', function(){
_$list.hide();
})
.bind('selector:change', function(){
_$title.html(_self.getTitleText());
})
.click(function(e){
e.stopPropagation();
})
.trigger('selector:change')
;
$.fn.disableSelection && _$selector.disableSelection(); // only work with jQuery UI
_$title.click(function(e){
(_self.isOpened() || _cron.isDisabled()) ? _self.close() : _self.open();
});
_self.close();
_self.clear();
}
this.jqCronSelector = jqCronSelector;
}).call(window, $);
/**
* Generate unique id for each element.
* Skip elements which have already an id.
*/
(function($){
var jqUID = 0;
var jqGetUID = function(prefix){
var id;
while(1) {
jqUID++;
id = ((prefix || 'JQUID')+'') + jqUID;
if(!document.getElementById(id)) return id;
}
};
$.fn.uniqueId = function(prefix) {
return this.each(function(){
if($(this).attr('id')) return;
var id = jqGetUID(prefix);
$(this).attr('id', id);
});
};
}).call(window, $);
/**
* Extends jQuery selectors with new block selector
*/
(function($){
$.extend($.expr[':'], {
container: function(a) {
return (a.tagName+'').toLowerCase() in {
a:1,
abbr:1,
acronym:1,
address:1,
b:1,
big:1,
blockquote:1,
button:1,
cite:1,
code:1,
dd: 1,
del:1,
dfn:1,
div:1,
dt:1,
em:1,
fieldset:1,
form:1,
h1:1,
h2:1,
h3:1,
h4:1,
h5:1,
h6: 1,
i:1,
ins:1,
kbd:1,
label:1,
li:1,
p:1,
pre:1,
q:1,
samp:1,
small:1,
span:1,
strong:1,
sub: 1,
sup:1,
td:1,
tt:1
};
},
autoclose: function(a) {
return (a.tagName+'').toLowerCase() in {
area:1,
base:1,
basefont:1,
br:1,
col:1,
frame:1,
hr:1,
img:1,
input:1,
link:1,
meta:1,
param:1
};
}
});
}).call(window, $);

View File

@@ -0,0 +1,325 @@
/**
* (c) Trilby Media, LLC
* Author Djamil Legato
*
* Based on Mark Matyas's Finderjs
* MIT License
*/
import $ from 'jquery';
import EventEmitter from 'eventemitter3';
export const DEFAULTS = {
labelKey: 'name',
valueKey: 'value', // new
childKey: 'children',
iconKey: 'icon', // new
itemKey: 'item-key', // new
pathBar: true,
className: {
container: 'fjs-container',
pathBar: 'fjs-path-bar',
col: 'fjs-col',
list: 'fjs-list',
item: 'fjs-item',
active: 'fjs-active',
children: 'fjs-has-children',
url: 'fjs-url',
itemPrepend: 'fjs-item-prepend',
itemContent: 'fjs-item-content',
itemAppend: 'fjs-item-append'
}
};
class Finder {
constructor(container, data, options) {
this.$emitter = new EventEmitter();
this.container = $(container);
this.data = data;
this.config = $.extend({}, DEFAULTS, options);
// dom events
this.container.on('click', this.clickEvent.bind(this));
this.container.on('keydown', this.keydownEvent.bind(this));
// internal events
this.$emitter.on('item-selected', this.itemSelected.bind(this));
this.$emitter.on('create-column', this.addColumn.bind(this));
this.$emitter.on('navigate', this.navigate.bind(this));
this.$emitter.on('go-to', this.goTo.bind(this, this.data));
this.container.addClass(this.config.className.container).attr('tabindex', 0);
this.createColumn(this.data);
if (this.config.pathBar) {
this.pathBar = this.createPathBar();
this.pathBar.on('click', '[data-breadcrumb-node]', (event) => {
event.preventDefault();
const location = $(event.currentTarget).data('breadcrumbNode');
this.goTo(this.data, location);
});
}
// '' is <Root>
if (this.config.defaultPath || this.config.defaultPath === '') {
this.goTo(this.data, this.config.defaultPath);
}
}
reload(data = this.data) {
this.createColumn(data);
// '' is <Root>
if (this.config.defaultPath || this.config.defaultPath === '') {
this.goTo(data, this.config.defaultPath);
}
}
createColumn(data, parent) {
const callback = (data) => this.createColumn(data, parent);
if (typeof data === 'function') {
data.call(this, parent, callback);
} else if (Array.isArray(data) || typeof data === 'object') {
if (typeof data === 'object') {
data = Array.from(data);
}
const list = this.createList(data);
const div = $('<div />');
div.append(list).addClass(this.config.className.col);
this.$emitter.emit('create-column', div);
return div;
} else {
throw new Error('Unknown data type');
}
}
createPathBar() {
this.container.siblings(`.${this.config.className.pathBar}`).remove();
const pathBar = $(`<div class="${this.config.className.pathBar}" />`);
pathBar.insertAfter(this.container);
return pathBar;
}
clickEvent(event) {
event.stopPropagation();
event.preventDefault();
const target = $(event.target);
const column = target.closest(`.${this.config.className.col}`);
const item = target.closest(`.${this.config.className.item}`);
if (item.length) {
this.$emitter.emit('item-selected', { column, item });
}
}
keydownEvent(event) {
const codes = { 37: 'left', 38: 'up', 39: 'right', 40: 'down' };
if (event.keyCode in codes) {
event.stopPropagation();
event.preventDefault();
this.$emitter.emit('navigate', {
direction: codes[event.keyCode]
});
}
}
itemSelected(value) {
const element = value.item;
if (!element.length) { return false; }
const item = element[0]._item;
const column = value.column;
const data = item[this.config.childKey] || this.data;
const active = $(column).find(`.${this.config.className.active}`);
if (active.length) {
active.removeClass(this.config.className.active);
}
element.addClass(this.config.className.active);
column.nextAll().remove(); // ?!?!?
this.container[0].focus();
window.scrollTo(window.pageXOffset, window.pageYOffset);
this.updatePathBar();
let newColumn;
if (data) {
newColumn = this.createColumn(data, item);
this.$emitter.emit('interior-selected', item);
} else {
this.$emitter.emit('leaf-selected', item);
}
return newColumn;
}
addColumn(column) {
this.container.append(column);
this.$emitter.emit('column-created', column);
}
navigate(value) {
const active = this.findLastActive();
const direction = value.direction;
let column;
let item;
let target;
if (active) {
item = active.item;
column = active.column;
if (direction === 'up' && item.prev().length) {
target = item.prev();
} else if (direction === 'down' && item.next().length) {
target = item.next();
} else if (direction === 'right' && column.next().length) {
column = column.next();
target = column.find(`.${this.config.className.item}`).first();
} else if (direction === 'left' && column.prev().length) {
column = column.prev();
target = column.find(`.${this.config.className.active}`).first() || column.find(`.${this.config.className.item}`);
}
} else {
column = this.container.find(`.${this.config.className.col}`).first();
target = column.find(`.${this.config.className.item}`).first();
}
if (target) {
this.$emitter.emit('item-selected', {
column,
item: target
});
}
}
goTo(data, path) {
path = Array.isArray(path) ? path : path.split('/').map(bit => bit.trim()).filter(Boolean);
if (path.length) {
this.container.children().remove();
}
if (typeof data === 'function') {
data.call(this, null, (data) => this.selectPath(path, data));
} else {
this.selectPath(path, data);
}
}
selectPath(path, data, column) {
column = column || (path.length ? this.createColumn(data) : this.container.find(`> .${this.config.className.col}`));
const current = path[0] || '';
const children = data.find((item) => item[this.config.itemKey] === current);
const newColumn = this.itemSelected({
column,
item: column.find(`[data-fjs-item="${current}"]`).first()
});
path.shift();
if (path.length && children) {
this.selectPath(path, children[this.config.childKey], newColumn);
}
}
findLastActive() {
const active = this.container.find(`.${this.config.className.active}`);
if (!active.length) {
return null;
}
const item = active.last();
const column = item.closest(`.${this.config.className.col}`);
return { item, column };
}
createList(data) {
const list = $('<ul />');
const items = data.map((item) => this.createItem(item));
const fragments = items.reduce((fragment, current) => {
fragment.appendChild(current[0] || current);
return fragment;
}, document.createDocumentFragment());
list.append(fragments).addClass(this.config.className.list);
return list;
}
createItem(item) {
const listItem = $('<li />');
const listItemClasses = [this.config.className.item];
const link = $('<a />');
const createItemContent = this.config.createItemContent || this.createItemContent;
const fragment = createItemContent.call(this, item);
link.append(fragment)
.attr('href', '')
.attr('tabindex', -1);
if (item.url) {
link.attr('href', item.url);
listItemClasses.push(item.className);
}
if (item[this.config.childKey]) {
listItemClasses.push(this.config.className[this.config.childKey]);
}
listItemClasses.push(`fjs-item-${item.type}`);
listItem.addClass(listItemClasses.join(' '));
listItem.append(link)
.attr('data-fjs-item', item[this.config.itemKey]);
listItem[0]._item = item;
return listItem;
}
updatePathBar() {
if (!this.config.pathBar) { return false; }
const activeItems = this.container.find(`.${this.config.className.active}`);
let itemKeys = '';
this.pathBar.children().empty();
activeItems.each((index, activeItem) => {
const item = activeItem._item;
const isLast = (index + 1) === activeItems.length;
itemKeys += `/${item[this.config.itemKey]}`;
this.pathBar.append(`
<span class="breadcrumb-node breadcrumb-node-${item.type}" ${item.type === 'dir' ? `data-breadcrumb-node="${itemKeys}"` : ''}>
<i class="fa fa-fw ${this.getIcon(item.type)}"></i>
<span class="breadcrumb-node-name">${$('<div />').html(item[this.config.labelKey]).html()}</span>
${!isLast ? '<i class="fa fa-fw fa-chevron-right"></i>' : ''}
</span>
`);
});
}
getIcon(type) {
switch (type) {
case 'root':
return 'fa-sitemap';
case 'file':
return 'fa-file-o';
case 'dir':
default:
return 'fa-folder';
}
}
}
export default Finder;

View File

@@ -0,0 +1,11 @@
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
export default function formatBytes(bytes, decimals) {
if (bytes === 0) return '0 Byte';
let k = 1000;
let value = Math.floor(Math.log(bytes) / Math.log(k));
let decimal = decimals + 1 || 3;
return (bytes / Math.pow(k, value)).toPrecision(decimal) + ' ' + sizes[value];
}

View File

@@ -0,0 +1,57 @@
import { parseJSON, parseStatus, userFeedbackError } from './response';
import { config } from 'grav-config';
import EventEmitter from 'events';
export default class GPM extends EventEmitter {
constructor(action = 'getUpdates') {
super();
this.payload = {};
this.raw = {};
this.action = action;
}
setPayload(payload = {}) {
this.payload = payload;
this.emit('payload', payload);
return this;
}
setAction(action = 'getUpdates') {
this.action = action;
this.emit('action', action);
return this;
}
fetch(callback = () => true, flush = false) {
let data = new FormData();
data.append('admin-nonce', config.admin_nonce);
if (flush) {
data.append('flush', true);
}
this.emit('fetching', this);
fetch(`${config.base_url_relative}/update.json/task${config.param_sep}getUpdates`, {
credentials: 'same-origin',
method: 'post',
body: data
}).then((response) => { this.raw = response; return response; })
.then(parseStatus)
.then(parseJSON)
.then((response) => this.response(response))
.then((response) => callback(response, this.raw))
.then((response) => this.emit('fetched', this.payload, this.raw, this))
.catch(userFeedbackError);
}
response(response) {
this.payload = response;
return response;
}
}
export let Instance = new GPM();

View File

@@ -0,0 +1,50 @@
import $ from 'jquery';
import getSlug from 'speakingurl';
// jQuery no parents filter
$.expr[':']['noparents'] = $.expr.createPseudo((text) => (element) => $(element).parents(text).length < 1);
// Slugify
// CommonJS and ES6 version of https://github.com/madflow/jquery-slugify
$.fn.slugify = (source, options) => {
return this.each((element) => {
let target = $(element);
let source = $(source);
target.on('keyup change', () => {
target.data('locked', target.val() !== '' && target.val() !== undefined);
});
source.on('keyup change', () => {
if (target.data('locked') === true) { return true; }
let isInput = target.is('input') || target.is('textarea');
target[isInput ? 'val' : 'text']($.slugify(source.val(), options));
});
});
};
// Static method.
$.slugify = (sourceString, options) => {
options = $.extend({}, $.slugify.options, options);
options.lang = options.lang || $('html').prop('lang');
if (typeof options.preSlug === 'function') {
sourceString = options.preSlug(sourceString);
}
sourceString = options.slugFunc(sourceString, options);
if (typeof options.postSlug === 'function') {
sourceString = options.postSlug(sourceString);
}
return sourceString;
};
// Default plugin options
$.slugify.options = {
preSlug: null,
postSlug: null,
slugFunc: (input, opts) => getSlug(input, opts)
};

View File

@@ -0,0 +1,34 @@
import { config } from 'grav-config';
import { userFeedbackError } from './response';
const MAX_SAFE_DELAY = 2147483647;
class KeepAlive {
constructor() {
this.active = false;
}
start() {
let timeout = config.admin_timeout / 1.5 * 1000;
this.timer = setInterval(() => this.fetch(), Math.min(timeout, MAX_SAFE_DELAY));
this.active = true;
}
stop() {
clearInterval(this.timer);
this.active = false;
}
fetch() {
let data = new FormData();
data.append('admin-nonce', config.admin_nonce);
fetch(`${config.base_url_relative}/task${config.param_sep}keepAlive`, {
credentials: 'same-origin',
method: 'post',
body: data
}).catch(userFeedbackError);
}
}
export default new KeepAlive();

View File

@@ -0,0 +1,21 @@
import $ from 'jquery';
import isOnline from '../utils/offline';
const offlineElement = $('#offline-status');
$(window).on('offline', () => {
offlineElement.slideDown();
});
$(window).on('online', () => {
offlineElement.slideUp();
});
$(document).ready(() => {
if (!isOnline) {
offlineElement.slideDown();
}
});
// assume online if can't check
export default typeof global.navigator.onLine !== 'undefined' ? global.navigator.onLine : true;

View File

@@ -0,0 +1,498 @@
import $ from 'jquery';
import { config, translations } from 'grav-config';
import request from '../utils/request';
import { Instance as gpm } from '../utils/gpm';
class Sorter {
getElements(elements, container) {
this.elements = elements || document.querySelectorAll('[data-gpm-plugin], [data-gpm-theme]');
this.container = container || document.querySelector('.gpm-plugins > table > tbody, .gpm-themes > .themes.card-row');
return this.elements;
}
static sort(A, B, direction = 'asc') {
if (A > B) { return (direction === 'asc') ? 1 : -1; }
if (A < B) { return (direction === 'asc') ? -1 : 1; }
return 0;
}
byCommon(direction = 'asc', data = '') {
const elements = this.getElements();
this.removeGumroad();
Array.from(elements).sort((a, b) => {
let A = a.dataset[data].toString().toLowerCase();
let B = b.dataset[data].toString().toLowerCase();
return Sorter.sort(A, B, direction);
}).forEach((element) => {
this.container.appendChild(element);
});
this.addGumroad();
return this.container;
}
byName(direction = 'asc', data = 'gpmName') {
return this.byCommon(direction, data);
}
byAuthor(direction = 'asc', data = 'gpmAuthor') {
return this.byCommon(direction, data);
}
byOfficial(direction = 'asc', data = 'gpmOfficial') {
return this.byCommon(direction, data);
}
byPremium(direction = 'asc', data = 'gpmPremium') {
return this.byCommon(direction, data);
}
byReleaseDate(direction = 'asc', data = 'gpmReleaseDate') {
const elements = this.getElements();
this.removeGumroad();
Array.from(elements).sort((a, b) => {
let A = new Date(a.dataset[data]).getTime();
let B = new Date(b.dataset[data]).getTime();
return Sorter.sort(A, B, direction === 'asc' ? 'desc' : 'asc');
}).forEach((element) => {
this.container.appendChild(element);
});
this.addGumroad();
return this.container;
}
byUpdatable(direction = 'asc', data = 'gpmUpdatable') {
return this.byCommon(direction, data);
}
byEnabled(direction = 'asc', data = 'gpmEnabled') {
return this.byCommon(direction, data);
}
byTesting(direction = 'asc', data = 'gpmTesting') {
return this.byCommon(direction, data);
}
addGumroad() {
if (window.GumroadOverlay) {
window.GumroadOverlay.startNodeAdditionObserver();
}
}
removeGumroad() {
if (window.GumroadOverlay) {
window.GumroadOverlay.nodeAdditionObserver.disconnect();
}
}
}
class Packages {
constructor() {
this.Sort = new Sorter();
}
static getBackToList(type) {
global.location.href = `${config.base_url_relative}/${type}s`;
}
static addDependencyToList(type, dependency, slug = '') {
if (['admin', 'form', 'login', 'email', 'grav'].indexOf(dependency) !== -1) { return; }
let container = $('.package-dependencies-container');
let text = `${dependency} <a href="#" class="button" data-dependency-slug="${dependency}" data-${type}-action="remove-dependency-package">Remove</a>`;
if (slug) {
text += ` (was needed by ${slug})`;
}
container.append(`<li>${text}</li>`);
}
addDependenciesToList(dependencies, slug = '') {
dependencies.forEach((dependency) => {
Packages.addDependencyToList('plugin', dependency.name || dependency, slug);
});
}
static getTaskUrl(type, task) {
let url = `${config.base_url_relative}`;
url += `/${type}s.json`;
url += `/task${config.param_sep}${task}`;
return url;
}
static getRemovePackageUrl(type) {
return `${Packages.getTaskUrl(type, 'removePackage')}`;
}
static getReinstallPackageUrl(type) {
return `${Packages.getTaskUrl(type, 'reinstallPackage')}`;
}
static getGetPackagesDependenciesUrl(type) {
return `${Packages.getTaskUrl(type, 'getPackagesDependencies')}`;
}
static getInstallDependenciesOfPackagesUrl(type) {
return `${Packages.getTaskUrl(type, 'installDependenciesOfPackages')}`;
}
static getInstallPackageUrl(type) {
return `${Packages.getTaskUrl(type, 'installPackage')}`;
}
removePackage(type, slug) {
let url = Packages.getRemovePackageUrl(type);
request(url, {
method: 'post',
body: {
package: slug
}
}, (response) => {
if (response.status === 'success') {
$('.remove-package-confirm').addClass('hidden');
if (response.dependencies && response.dependencies.length > 0) {
this.addDependenciesToList(response.dependencies);
$('.remove-package-dependencies').removeClass('hidden');
} else {
$('.remove-package-done').removeClass('hidden');
}
// The package was removed. When the modal closes, move to the packages list
$(document).on('closing', '[data-remodal-id="remove-package"]', () => {
Packages.getBackToList(type);
});
} else {
$('.remove-package-confirm').addClass('hidden');
$('.remove-package-error').removeClass('hidden');
}
});
}
reinstallPackage(type, slug, package_name, current_version) {
$('.button-bar button').addClass('hidden');
$('.button-bar .spinning-wheel').removeClass('hidden');
let url = Packages.getReinstallPackageUrl(type);
request(url, {
method: 'post',
body: {
slug: slug,
type: type,
package_name: package_name,
current_version: current_version
}
}, (response) => {
if (response.status === 'success') {
$('.reinstall-package-confirm').addClass('hidden');
$('.reinstall-package-done').removeClass('hidden');
} else {
$('.reinstall-package-confirm').addClass('hidden');
$('.reinstall-package-error').removeClass('hidden');
}
window.location.reload();
});
}
removeDependency(type, slug, button) {
let url = Packages.getRemovePackageUrl(type);
request(url, {
method: 'post',
body: {
package: slug
}
}, (response) => {
if (response.status === 'success') {
button.removeClass('button');
button.replaceWith($('<span>Removed successfully</span>'));
if (response.dependencies && response.dependencies.length > 0) {
this.addDependenciesToList(response.dependencies, slug);
}
}
});
}
static addNeededDependencyToList(action, slug) {
$('.install-dependencies-package-container .type-' + action).removeClass('hidden');
let list = $('.install-dependencies-package-container .type-' + action + ' ul');
if (action !== 'install') {
let current_version = '';
let available_version = '';
let name = '';
let resources = gpm.payload.payload.resources;
if (resources.plugins[slug]) {
available_version = resources.plugins[slug].available;
current_version = resources.plugins[slug].version;
name = resources.plugins[slug].name;
} else if (resources.themes[slug]) {
available_version = resources.themes[slug].available;
current_version = resources.themes[slug].version;
name = resources.themes[slug].name;
}
list.append(`<li>${name ? name : slug}, ${translations.PLUGIN_ADMIN.FROM} v<strong>${current_version}</strong> ${translations.PLUGIN_ADMIN.TO} v<strong>${available_version}</strong></li>`);
} else {
list.append(`<li>${name ? name : slug}</li>`);
}
}
getPackagesDependencies(type, slugs, finishedLoadingCallback) {
let url = Packages.getGetPackagesDependenciesUrl(type);
request(url, {
method: 'post',
body: {
packages: slugs
}
}, (response) => {
finishedLoadingCallback();
if (response.status === 'success') {
if (response.dependencies) {
let hasDependencies = false;
for (var dependency in response.dependencies) {
if (response.dependencies.hasOwnProperty(dependency)) {
if (dependency === 'grav') {
continue;
}
hasDependencies = true;
let dependencyName = dependency;
let action = response.dependencies[dependency];
Packages.addNeededDependencyToList(action, dependencyName);
}
}
if (hasDependencies) {
$('[data-packages-modal] .install-dependencies-package-container').removeClass('hidden');
} else {
$('[data-packages-modal] .install-package-container').removeClass('hidden');
}
} else {
$('[data-packages-modal] .install-package-container').removeClass('hidden');
}
} else {
$('[data-packages-modal] .install-package-error').removeClass('hidden');
}
});
}
installDependenciesOfPackages(type, slugs, callbackSuccess, callbackError) {
let url = Packages.getInstallDependenciesOfPackagesUrl(type);
request(url, {
method: 'post',
body: {
packages: slugs
}
}, callbackSuccess);
}
installPackages(type, slugs, callbackSuccess) {
let url = Packages.getInstallPackageUrl(type);
global.Promise.all(slugs.map((slug) => {
return new global.Promise((resolve, reject) => {
request(url, {
method: 'post',
body: {
package: slug,
type: type
}
}, (response) => {
resolve(response);
});
});
})).then(callbackSuccess);
}
static getSlugsFromEvent(event) {
let slugs = '';
if ($(event.target).is('[data-packages-slugs]')) {
slugs = $(event.target).attr('data-packages-slugs');
} else {
slugs = $(event.target).parent('[data-packages-slugs]').attr('data-packages-slugs');
}
if (typeof slugs === 'undefined') {
return null;
}
slugs = slugs.split(',');
return typeof slugs === 'string' ? [slugs] : slugs;
}
handleGettingPackageDependencies(type, event, action = 'update') {
let slugs = Packages.getSlugsFromEvent(event);
if (!slugs) {
alert('No slug set');
return;
}
// Cleanup
$('.packages-names-list').html('');
$('.install-dependencies-package-container li').remove();
slugs.forEach((slug) => {
if (action === 'update') {
let current_version = '';
let available_version = '';
let name = '';
let resources = gpm.payload.payload.resources;
if (resources.plugins[slug]) {
available_version = resources.plugins[slug].available;
current_version = resources.plugins[slug].version;
name = resources.plugins[slug].name;
} else if (resources.themes[slug]) {
available_version = resources.themes[slug].available;
current_version = resources.themes[slug].version;
name = resources.themes[slug].name;
}
$('.packages-names-list').append(`<li>${name ? name : slug}, ${translations.PLUGIN_ADMIN.FROM} v<strong>${current_version}</strong> ${translations.PLUGIN_ADMIN.TO} v<strong>${available_version}</strong></li>`);
} else {
$('.packages-names-list').append(`<li>${name ? name : slug}</li>`);
}
});
event.preventDefault();
event.stopPropagation();
// fix mismatching types when sharing install modal between plugins/themes
const query = '[data-packages-modal] [data-theme-action], [data-packages-modal] [data-plugin-action]';
const data = $(query).data('themeAction') || $(query).data('pluginAction');
$(query).removeAttr('data-theme-action').removeAttr('data-plugin-action').attr(`data-${type}-action`, data);
// Restore original state
$('[data-packages-modal] .loading').removeClass('hidden');
$('[data-packages-modal] .install-dependencies-package-container').addClass('hidden');
$('[data-packages-modal] .install-package-container').addClass('hidden');
$('[data-packages-modal] .installing-dependencies').addClass('hidden');
$('[data-packages-modal] .installing-package').addClass('hidden');
$('[data-packages-modal] .installation-complete').addClass('hidden');
$('[data-packages-modal] .install-package-error').addClass('hidden');
this.getPackagesDependencies(type, slugs, () => {
let slugs_string = slugs.join();
$(`[data-packages-modal] [data-${type}-action="install-dependencies-and-package"]`).attr('data-packages-slugs', slugs_string);
$(`[data-packages-modal] [data-${type}-action="install-package"]`).attr('data-packages-slugs', slugs_string);
$('[data-packages-modal] .loading').addClass('hidden');
});
}
handleInstallingDependenciesAndPackage(type, event) {
let slugs = Packages.getSlugsFromEvent(event);
event.preventDefault();
event.stopPropagation();
$('[data-packages-modal] .install-dependencies-package-container').addClass('hidden');
$('[data-packages-modal] .install-package-container').addClass('hidden');
$('[data-packages-modal] .installing-dependencies').removeClass('hidden');
this.installDependenciesOfPackages(type, slugs, (response) => {
$('[data-packages-modal] .installing-dependencies').addClass('hidden');
$('[data-packages-modal] .installing-package').removeClass('hidden');
this.installPackages(type, slugs, () => {
$('[data-packages-modal] .installing-package').addClass('hidden');
$('[data-packages-modal] .installation-complete').removeClass('hidden');
if (response.status === 'error') {
let remodal = $.remodal.lookup[$('[data-packages-modal]').data('remodal')];
remodal.close();
return;
}
setTimeout(() => {
if (slugs.length === 1) {
global.location.href = `${config.base_url_relative}/${type}s/${slugs[0]}`;
} else {
global.location.href = `${config.base_url_relative}/${type}s`;
}
}, 1000);
});
});
}
handleInstallingPackage(type, event) {
let slugs = Packages.getSlugsFromEvent(event);
event.preventDefault();
event.stopPropagation();
$('[data-packages-modal] .install-package-container').addClass('hidden');
$('[data-packages-modal] .installing-package').removeClass('hidden');
this.installPackages(type, slugs, (response) => {
$('[data-packages-modal] .installing-package').addClass('hidden');
$('[data-packages-modal] .installation-complete').removeClass('hidden');
const errors = Array.from(response).filter((r) => r.status === 'error');
if (errors && errors.length) {
let remodal = $.remodal.lookup[$('[data-packages-modal].remodal-is-opened').data('remodal')];
remodal.close();
return;
}
if (slugs.length === 1) {
global.location.href = `${config.base_url_relative}/${type}s/${slugs[0]}`;
} else {
global.location.href = `${config.base_url_relative}/${type}s`;
}
});
}
handleRemovingPackage(type, event) {
let slug = $(event.target).attr('data-packages-slugs');
event.preventDefault();
event.stopPropagation();
this.removePackage(type, slug);
}
handleReinstallPackage(type, event) {
let target = $(event.target);
let slug = target.attr('data-package-slug');
let package_name = target.attr('data-package-name');
let current_version = target.attr('data-package-current-version');
event.preventDefault();
event.stopPropagation();
this.reinstallPackage(type, slug, package_name, current_version);
}
handleRemovingDependency(type, event) {
let slug = $(event.target).attr('data-dependency-slug');
let button = $(event.target);
event.preventDefault();
event.stopPropagation();
this.removeDependency(type, slug, button);
}
}
export default new Packages();

View File

@@ -0,0 +1,820 @@
/* Remodal from https://github.com/vodkabears/Remodal
* With Stackable option from https://github.com/antstorm/Remodal patch
*/
import $ from 'jquery';
!(function(root, factory) {
return factory(root, $);
})(this, function(global, $) {
'use strict';
/**
* Name of the plugin
* @private
* @const
* @type {String}
*/
var PLUGIN_NAME = 'remodal';
/**
* Namespace for CSS and events
* @private
* @const
* @type {String}
*/
var NAMESPACE = window.REMODAL_GLOBALS && window.REMODAL_GLOBALS.NAMESPACE || PLUGIN_NAME;
/**
* Animationstart event with vendor prefixes
* @private
* @const
* @type {String}
*/
var ANIMATIONSTART_EVENTS = $.map(
['animationstart', 'webkitAnimationStart', 'MSAnimationStart', 'oAnimationStart'],
function(eventName) {
return eventName + '.' + NAMESPACE;
}
).join(' ');
/**
* Animationend event with vendor prefixes
* @private
* @const
* @type {String}
*/
var ANIMATIONEND_EVENTS = $.map(
['animationend', 'webkitAnimationEnd', 'MSAnimationEnd', 'oAnimationEnd'],
function(eventName) {
return eventName + '.' + NAMESPACE;
}
).join(' ');
/**
* Default settings
* @private
* @const
* @type {Object}
*/
var DEFAULTS = $.extend({
hashTracking: true,
closeOnConfirm: true,
closeOnCancel: true,
closeOnEscape: true,
closeOnOutsideClick: true,
modifier: '',
stack: false,
appendTo: null
}, window.REMODAL_GLOBALS && window.REMODAL_GLOBALS.DEFAULTS);
/**
* States of the Remodal
* @private
* @const
* @enum {String}
*/
var STATES = {
CLOSING: 'closing',
CLOSED: 'closed',
OPENING: 'opening',
OPENED: 'opened'
};
/**
* Reasons of the state change.
* @private
* @const
* @enum {String}
*/
var STATE_CHANGE_REASONS = {
CONFIRMATION: 'confirmation',
CANCELLATION: 'cancellation'
};
/**
* Is animation supported?
* @private
* @const
* @type {Boolean}
*/
var IS_ANIMATION = (function() {
var style = document.createElement('div').style;
return style.animationName !== undefined ||
style.WebkitAnimationName !== undefined ||
style.MozAnimationName !== undefined ||
style.msAnimationName !== undefined ||
style.OAnimationName !== undefined;
})();
/**
* Is iOS?
* @private
* @const
* @type {Boolean}
*/
var IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform);
/**
* Current modal
* @private
* @type {Remodal}
*/
var openModals = [];
/**
* Scrollbar position
* @private
* @type {Number}
*/
var scrollTop;
/**
* Returns an animation duration
* @private
* @param {jQuery} $elem
* @returns {Number}
*/
function getAnimationDuration($elem) {
if (
IS_ANIMATION &&
$elem.css('animation-name') === 'none' &&
$elem.css('-webkit-animation-name') === 'none' &&
$elem.css('-moz-animation-name') === 'none' &&
$elem.css('-o-animation-name') === 'none' &&
$elem.css('-ms-animation-name') === 'none'
) {
return 0;
}
var duration = $elem.css('animation-duration') ||
$elem.css('-webkit-animation-duration') ||
$elem.css('-moz-animation-duration') ||
$elem.css('-o-animation-duration') ||
$elem.css('-ms-animation-duration') ||
'0s';
var delay = $elem.css('animation-delay') ||
$elem.css('-webkit-animation-delay') ||
$elem.css('-moz-animation-delay') ||
$elem.css('-o-animation-delay') ||
$elem.css('-ms-animation-delay') ||
'0s';
var iterationCount = $elem.css('animation-iteration-count') ||
$elem.css('-webkit-animation-iteration-count') ||
$elem.css('-moz-animation-iteration-count') ||
$elem.css('-o-animation-iteration-count') ||
$elem.css('-ms-animation-iteration-count') ||
'1';
var max;
var len;
var num;
var i;
duration = duration.split(', ');
delay = delay.split(', ');
iterationCount = iterationCount.split(', ');
// The 'duration' size is the same as the 'delay' size
for (i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; i < len; i++) {
num = parseFloat(duration[i]) * parseInt(iterationCount[i], 10) + parseFloat(delay[i]);
if (num > max) {
max = num;
}
}
return max;
}
/**
* Returns a scrollbar width
* @private
* @returns {Number}
*/
function getScrollbarWidth() {
if ($(document).height() <= $(window).height()) {
return 0;
}
var outer = document.createElement('div');
var inner = document.createElement('div');
var widthNoScroll;
var widthWithScroll;
outer.style.visibility = 'hidden';
outer.style.width = '100px';
document.body.appendChild(outer);
widthNoScroll = outer.offsetWidth;
// Force scrollbars
outer.style.overflow = 'scroll';
// Add inner div
inner.style.width = '100%';
outer.appendChild(inner);
widthWithScroll = inner.offsetWidth;
// Remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
/**
* Locks the screen
* @private
*/
function lockScreen() {
if (IS_IOS) {
return;
}
var $html = $('html');
var lockedClass = namespacify('is-locked');
var paddingRight;
var $body;
if (!$html.hasClass(lockedClass)) {
$body = $(document.body);
// Zepto does not support '-=', '+=' in the `css` method
paddingRight = parseInt($body.css('padding-right'), 10) + getScrollbarWidth();
$body.css('padding-right', paddingRight + 'px');
$html.addClass(lockedClass);
}
}
/**
* Unlocks the screen
* @private
*/
function unlockScreen() {
if (IS_IOS) {
return;
}
var $html = $('html');
var lockedClass = namespacify('is-locked');
var paddingRight;
var $body;
if ($html.hasClass(lockedClass)) {
$body = $(document.body);
// Zepto does not support '-=', '+=' in the `css` method
paddingRight = parseInt($body.css('padding-right'), 10) - getScrollbarWidth();
$body.css('padding-right', paddingRight + 'px');
$html.removeClass(lockedClass);
}
}
/**
* Sets a state for an instance
* @private
* @param {Remodal} instance
* @param {STATES} state
* @param {Boolean} isSilent If true, Remodal does not trigger events
* @param {String} Reason of a state change.
*/
function setState(instance, state, isSilent, reason) {
var newState = namespacify('is', state);
var allStates = [namespacify('is', STATES.CLOSING),
namespacify('is', STATES.OPENING),
namespacify('is', STATES.CLOSED),
namespacify('is', STATES.OPENED)].join(' ');
instance.$bg
.removeClass(allStates)
.addClass(newState);
instance.$overlay
.removeClass(allStates)
.addClass(newState);
instance.$wrapper
.removeClass(allStates)
.addClass(newState);
instance.$modal
.removeClass(allStates)
.addClass(newState);
instance.state = state;
!isSilent && instance.$modal.trigger({
type: state,
reason: reason
}, [{ reason: reason }]);
}
/**
* Synchronizes with the animation
* @param {Function} doBeforeAnimation
* @param {Function} doAfterAnimation
* @param {Remodal} instance
*/
function syncWithAnimation(doBeforeAnimation, doAfterAnimation, instance) {
var runningAnimationsCount = 0;
var handleAnimationStart = function(e) {
if (e.target !== this) {
return;
}
runningAnimationsCount++;
};
var handleAnimationEnd = function(e) {
if (e.target !== this) {
return;
}
if (--runningAnimationsCount === 0) {
// Remove event listeners
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
});
doAfterAnimation();
}
};
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName]
.on(ANIMATIONSTART_EVENTS, handleAnimationStart)
.on(ANIMATIONEND_EVENTS, handleAnimationEnd);
});
doBeforeAnimation();
// If the animation is not supported by a browser or its duration is 0
if (
getAnimationDuration(instance.$bg) === 0 &&
getAnimationDuration(instance.$overlay) === 0 &&
getAnimationDuration(instance.$wrapper) === 0 &&
getAnimationDuration(instance.$modal) === 0
) {
// Remove event listeners
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
});
doAfterAnimation();
}
}
/**
* Closes immediately
* @private
* @param {Remodal} instance
*/
function halt(instance) {
if (instance.state === STATES.CLOSED) {
return;
}
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
});
removeModal(instance);
instance.$bg.removeClass(instance.settings.modifier);
instance.$overlay.removeClass(instance.settings.modifier).hide();
instance.$wrapper.hide();
if (openModals.length === 0) {
unlockScreen();
}
setState(instance, STATES.CLOSED, true);
}
/**
* Parses a string with options
* @private
* @param str
* @returns {Object}
*/
function parseOptions(str) {
var obj = {};
var arr;
var len;
var val;
var i;
// Remove spaces before and after delimiters
str = str.replace(/\s*:\s*/g, ':').replace(/\s*,\s*/g, ',');
// Parse a string
arr = str.split(',');
for (i = 0, len = arr.length; i < len; i++) {
arr[i] = arr[i].split(':');
val = arr[i][1];
// Convert a string value if it is like a boolean
if (typeof val === 'string' || val instanceof String) {
val = val === 'true' || (val === 'false' ? false : val);
}
// Convert a string value if it is like a number
if (typeof val === 'string' || val instanceof String) {
val = !isNaN(val) ? +val : val;
}
obj[arr[i][0]] = val;
}
return obj;
}
/**
* Generates a string separated by dashes and prefixed with NAMESPACE
* @private
* @param {...String}
* @returns {String}
*/
function namespacify() {
var result = NAMESPACE;
for (var i = 0; i < arguments.length; ++i) {
result += '-' + arguments[i];
}
return result;
}
/**
* Handles the hashchange event
* @private
* @listens hashchange
*/
function handleHashChangeEvent() {
var id = location.hash.replace('#', '');
var instance;
var $elem;
var current = currentModal();
if (!id) {
// Check if we have currently opened modal and animation was completed
if (current && current.state === STATES.OPENED && current.settings.hashTracking) {
current.close();
}
} else {
if (!current || current.id !== id) {
// Catch syntax error if your hash is bad
try {
$elem = $(
'[data-' + PLUGIN_NAME + '-id="' + id + '"]'
);
} catch (err) {
}
if ($elem && $elem.length) {
instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)];
if (instance && instance.settings.hashTracking) {
instance.open();
}
}
}
}
}
function currentModal() {
return openModals[openModals.length - 1];
}
function removeModal(remodal) {
var index = openModals.indexOf(remodal);
if (index >= 0) {
openModals.slice(index, 1);
}
}
/**
* Remodal constructor
* @constructor
* @param {jQuery} $modal
* @param {Object} options
*/
function Remodal($modal, options) {
var $body = $(document.body);
var $appendTo = $body;
var remodal = this;
remodal.id = $modal.attr('data-' + PLUGIN_NAME + '-id');
remodal.settings = $.extend({}, DEFAULTS, options);
remodal.index = $[PLUGIN_NAME].lookup.push(remodal) - 1;
remodal.state = STATES.CLOSED;
// remodal.$overlay = $('.' + namespacify('overlay'));
if (remodal.settings.appendTo !== null && remodal.settings.appendTo.length) {
$appendTo = $(remodal.settings.appendTo);
}
if (!remodal.$overlay) {
remodal.$overlay = $('<div>').addClass(namespacify('overlay') + ' ' + namespacify('is', STATES.CLOSED)).hide();
$appendTo.append(remodal.$overlay);
}
remodal.$bg = $('.' + namespacify('bg')).addClass(namespacify('is', STATES.CLOSED));
remodal.$modal = $modal
.addClass(
NAMESPACE + ' ' +
namespacify('is-initialized') + ' ' +
remodal.settings.modifier + ' ' +
namespacify('is', STATES.CLOSED))
.attr('tabindex', '-1');
remodal.$wrapper = $('<div>')
.addClass(
namespacify('wrapper') + ' ' +
remodal.settings.modifier + ' ' +
namespacify('is', STATES.CLOSED))
.hide()
.append(remodal.$modal);
$appendTo.append(remodal.$wrapper);
// Add the event listener for the close button
remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="close"]', function(e) {
e.preventDefault();
remodal.close();
});
// Add the event listener for the cancel button
remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="cancel"]', function(e) {
e.preventDefault();
remodal.$modal.trigger(STATE_CHANGE_REASONS.CANCELLATION);
if (remodal.settings.closeOnCancel) {
remodal.close(STATE_CHANGE_REASONS.CANCELLATION);
}
});
// Add the event listener for the confirm button
remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="confirm"]', function(e) {
e.preventDefault();
remodal.$modal.trigger(STATE_CHANGE_REASONS.CONFIRMATION);
if (remodal.settings.closeOnConfirm) {
remodal.close(STATE_CHANGE_REASONS.CONFIRMATION);
}
});
// Add the event listener for the overlay
remodal.$wrapper.on('click.' + NAMESPACE, function(e) {
var $target = $(e.target);
var isWrapper = $target.hasClass(namespacify('wrapper'));
var isWithin = $target.closest('.' + namespacify('is', STATES.OPENED)).length;
if (!isWrapper && isWithin) {
return;
}
if (remodal.settings.closeOnOutsideClick) {
remodal.close();
}
});
}
/**
* Opens a modal window
* @public
*/
Remodal.prototype.open = function() {
var remodal = this;
var current;
var modalCount;
// Check if the animation was completed
if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING) {
return;
}
// id = remodal.$modal.attr('data-' + PLUGIN_NAME + '-id');
if (remodal.id && remodal.settings.hashTracking) {
scrollTop = $(window).scrollTop();
location.hash = remodal.id;
}
if (!remodal.settings.stack) {
current = currentModal();
if (current && current !== remodal) {
halt(current);
}
}
modalCount = openModals.push(remodal);
remodal.$overlay.css('z-index', function(_, value) { return parseInt(value, 10) + modalCount; });
remodal.$wrapper.css('z-index', function(_, value) { return parseInt(value, 10) + modalCount; });
lockScreen();
remodal.$bg.addClass(remodal.settings.modifier);
remodal.$overlay.addClass(remodal.settings.modifier).show();
remodal.$wrapper.show().scrollTop(0);
remodal.$modal.focus();
syncWithAnimation(
function() {
setState(remodal, STATES.OPENING);
},
function() {
setState(remodal, STATES.OPENED);
},
remodal);
};
/**
* Closes a modal window
* @public
* @param {String} reason
*/
Remodal.prototype.close = function(reason) {
var remodal = this;
var current;
// Check if the animation was completed
if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING || remodal.state === STATES.CLOSED) {
return;
}
removeModal(remodal);
if (
remodal.settings.hashTracking &&
remodal.id === location.hash.substr(1)
) {
current = currentModal();
if (current) {
location.hash = current.id;
} else {
location.hash = '';
$(window).scrollTop(scrollTop);
}
}
syncWithAnimation(
function() {
setState(remodal, STATES.CLOSING, false, reason);
},
function() {
remodal.$bg.removeClass(remodal.settings.modifier);
remodal.$overlay.removeClass(remodal.settings.modifier).hide();
remodal.$wrapper.hide();
if (openModals.length === 0) {
unlockScreen();
}
setState(remodal, STATES.CLOSED, false, reason);
},
remodal);
};
/**
* Returns a current state of a modal
* @public
* @returns {STATES}
*/
Remodal.prototype.getState = function() {
return this.state;
};
/**
* Destroys a modal
* @public
*/
Remodal.prototype.destroy = function() {
var lookup = $[PLUGIN_NAME].lookup;
var instanceCount;
halt(this);
this.$wrapper.remove();
delete lookup[this.index];
instanceCount = $.grep(lookup, function(instance) {
return !!instance;
}).length;
if (instanceCount === 0) {
this.$overlay.remove();
this.$bg.removeClass(
namespacify('is', STATES.CLOSING) + ' ' +
namespacify('is', STATES.OPENING) + ' ' +
namespacify('is', STATES.CLOSED) + ' ' +
namespacify('is', STATES.OPENED));
}
};
/**
* Special plugin object for instances
* @public
* @type {Object}
*/
$[PLUGIN_NAME] = {
lookup: []
};
/**
* Plugin constructor
* @constructor
* @param {Object} options
* @returns {JQuery}
*/
$.fn[PLUGIN_NAME] = function(opts) {
var instance;
var $elem;
this.each(function(index, elem) {
$elem = $(elem);
if ($elem.data(PLUGIN_NAME) == null) {
instance = new Remodal($elem, opts);
$elem.data(PLUGIN_NAME, instance.index);
if (
instance.settings.hashTracking &&
instance.id === location.hash.substr(1)
) {
instance.open();
}
} else {
instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)];
}
});
return instance;
};
$(document).ready(function() {
// data-remodal-target opens a modal window with the special Id
$(document).on('click', '[data-' + PLUGIN_NAME + '-target]', function(e) {
e.preventDefault();
var elem = e.currentTarget;
var id = elem.getAttribute('data-' + PLUGIN_NAME + '-target');
var $target = $('[data-' + PLUGIN_NAME + '-id="' + id + '"]');
$[PLUGIN_NAME].lookup[$target.data(PLUGIN_NAME)].open();
});
// Auto initialization of modal windows
// They should have the 'remodal' class attribute
// Also you can write the `data-remodal-options` attribute to pass params into the modal
$(document).find('.' + NAMESPACE).each(function(i, container) {
var $container = $(container);
var options = $container.data(PLUGIN_NAME + '-options');
if (!options) {
options = {};
} else if (typeof options === 'string' || options instanceof String) {
options = parseOptions(options);
}
$container[PLUGIN_NAME](options);
});
// Handles the keydown event
$(document).on('keydown.' + NAMESPACE, function(e) {
var current = currentModal();
if (current && current.settings.closeOnEscape && current.state === STATES.OPENED && e.keyCode === 27) {
current.close();
}
});
// Handles the hashchange event
$(window).on('hashchange.' + NAMESPACE, handleHashChangeEvent);
});
});

View File

@@ -0,0 +1,38 @@
import { parseStatus, parseJSON, userFeedback, userFeedbackError } from './response';
import { config } from 'grav-config';
let raw;
let request = function(url, options = {}, callback = () => true) {
if (typeof options === 'function') {
callback = options;
options = {};
}
if (options.method && options.method === 'post') {
let data = new FormData();
options.body = Object.assign({ 'admin-nonce': config.admin_nonce }, options.body || {});
Object.keys(options.body).map((key) => data.append(key, options.body[key]));
options.body = data;
}
options = Object.assign({
credentials: 'same-origin',
headers: {
'Accept': 'application/json'
}
}, options);
return fetch(url, options)
.then((response) => {
raw = response;
return response;
})
.then(parseStatus)
.then(parseJSON)
.then(userFeedback)
.then((response) => callback(response, raw))
.catch(userFeedbackError);
};
export default request;

View File

@@ -0,0 +1,101 @@
import $ from 'jquery';
import toastr from './toastr';
import isOnline from './offline';
import { config } from 'grav-config';
import trim from 'mout/string/trim';
let UNLOADING = false;
let error = function(response) {
let error = new Error(response.statusText || response || '');
error.response = response;
return error;
};
export function parseStatus(response) {
return response;
/* Whoops can handle JSON responses so we don't need this for now.
if (response.status >= 200 && response.status < 300) {
return response;
} else {
throw error(response);
}
*/
}
export function parseJSON(response) {
return response.text().then((text) => {
let parsed = text;
try {
parsed = JSON.parse(text);
} catch (error) {
let content = document.createElement('div');
content.innerHTML = text;
let the_error = new Error();
the_error.stack = trim(content.innerText);
throw the_error;
}
return parsed;
});
}
export function userFeedback(response) {
if (UNLOADING) { return true; }
let status = response.status || (response.error ? 'error' : '');
let message = response.message || (response.error ? response.error.message : null);
let settings = response.toastr || null;
let backup;
switch (status) {
case 'unauthenticated':
document.location.href = config.base_url_relative;
throw error('Logged out');
case 'unauthorized':
status = 'error';
message = message || 'Unauthorized.';
break;
case 'error':
status = 'error';
message = message || 'Unknown error.';
break;
case 'success':
status = 'success';
message = message || '';
break;
default:
status = 'error';
message = message || 'Invalid AJAX response.';
break;
}
if (settings) {
backup = Object.assign({}, toastr.options);
Object.keys(settings).forEach((key) => { toastr.options[key] = settings[key]; });
}
if (message && (isOnline || (!isOnline && status !== 'error'))) {
toastr[status === 'success' ? 'success' : 'error'](message);
}
if (settings) {
toastr.options = backup;
}
return response;
}
export function userFeedbackError(error) {
if (UNLOADING) { return true; }
let stack = error.stack ? `<pre><code>${error.stack}</code></pre>` : '';
toastr.error(`Fetch Failed: <br /> ${error.message} ${stack}`);
console.error(`${error.message} at ${error.stack}`);
}
$(global).on('beforeunload._ajax', () => {
UNLOADING = true;
});

View File

@@ -0,0 +1,42 @@
import $ from 'jquery';
import Selectize from 'selectize';
Selectize.define('option_click', function(options) {
const self = this;
const setup = self.setup;
this.setup = function() {
setup.apply(self, arguments);
let clicking = false;
// Detect click on a .clickable
self.$dropdown_content.on('mousedown click', function(e) {
const target = $(e.target);
if (target.hasClass('clickable') || target.closest('.clickable').length) {
if (e.type === 'mousedown') {
clicking = true;
self.isFocused = false; // awful hack to defuse the document mousedown listener
} else {
self.isFocused = true;
setTimeout(function() {
clicking = false; // wait until blur has been preempted
});
}
} else { // cleanup in case user right-clicked or dragged off the element
clicking = false;
self.isFocused = true;
}
});
// Intercept default handlers
self.$dropdown.off('mousedown click', '[data-selectable]').on('mousedown click', '[data-selectable]', function() {
if (!clicking) {
return self.onOptionSelect.apply(self, arguments);
}
});
self.$control_input.off('blur').on('blur', function() {
if (!clicking) {
return self.onBlur.apply(self, arguments);
}
});
};
});

View File

@@ -0,0 +1,28 @@
/**
* This is a plugin to override the `.refreshValidityState` method of
* the Selectize library (https://selectize.github.io/selectize.js/).
* The library is not maintained anymore (as of 2017-09-13) and contains
* a bug which causes Microsoft Edge to not work with selectized [required]
* form fields. This plugin should be removed if
* https://github.com/selectize/selectize.js/pull/1320 is ever merged
* and a new version of Selectize gets released.
*/
import Selectize from 'selectize';
Selectize.define('required-fix', function(options) {
this.refreshValidityState = () => {
if (!this.isRequired) return false;
let invalid = !this.items.length;
this.isInvalid = invalid;
if (invalid) {
this.$control_input.attr('required', '');
this.$input.removeAttr('required');
} else {
this.$control_input.removeAttr('required');
this.$input.attr('required');
}
};
});

View File

@@ -0,0 +1,180 @@
import $ from 'jquery';
import Cookies from '../utils/cookies';
const MOBILE_BREAKPOINT = 48 - 0.062;
const DESKTOP_BREAKPOINT = 75 + 0.063;
const EVENTS = 'touchstart._grav click._grav';
const TARGETS = '[data-sidebar-mobile-toggle], #overlay';
const MOBILE_QUERY = `(max-width: ${MOBILE_BREAKPOINT}em)`;
const DESKTOP_QUERY = `(min-width: ${DESKTOP_BREAKPOINT}em)`;
let map = new global.Map();
export default class Sidebar {
constructor() {
this.timeout = null;
this.isOpen = false;
this.body = $('body');
this.matchMedia = global.matchMedia(MOBILE_QUERY);
this.enable();
}
enable() {
const sidebar = $('#admin-sidebar');
this.matchMedia.addListener(this._getBound('checkMatch'));
this.checkMatch(this.matchMedia);
this.body.on(EVENTS, '[data-sidebar-toggle]', this._getBound('toggleSidebarState'));
if (sidebar.data('quickopen')) {
sidebar.hover(this._getBound('quickOpenIn'), this._getBound('quickOpenOut'));
}
}
disable() {
const sidebar = $('#admin-sidebar');
this.close();
this.matchMedia.removeListener(this._getBound('checkMatch'));
this.body.off(EVENTS, '[data-sidebar-toggle]', this._getBound('toggleSidebarState'));
if (sidebar.data('quickopen')) {
sidebar.off('mouseenter mouseleave');
}
}
attach() {
this.body.on(EVENTS, TARGETS, this._getBound('toggle'));
}
detach() {
this.body.off(EVENTS, TARGETS, this._getBound('toggle'));
}
quickOpenIn(/* event */) {
let isDesktop = global.matchMedia(DESKTOP_QUERY).matches;
let delay = $('#admin-sidebar').data('quickopen-delay') || 500;
if (this.body.hasClass('sidebar-mobile-open')) { return; }
let shouldQuickOpen = isDesktop ? this.body.hasClass('sidebar-closed') : !this.body.hasClass('sidebar-open');
if (!shouldQuickOpen && !this.body.hasClass('sidebar-quickopen')) { return this.quickOpenOut(); }
this.timeout = setTimeout(() => {
this.body.addClass('sidebar-open sidebar-quickopen');
$(global).trigger('sidebar_state._grav', isDesktop);
}, delay);
}
quickOpenOut(/* event */) {
clearTimeout(this.timeout);
if (this.body.hasClass('sidebar-quickopen')) {
this.body.removeClass('sidebar-open sidebar-quickopen');
}
return true;
}
open(event, quick = false) {
if (event) { event.preventDefault(); }
let overlay = $('#overlay');
let sidebar = $('#admin-sidebar');
this.body.addClass('sidebar-mobile-open');
overlay.css('display', 'block');
if (!quick) {
sidebar.css('display', 'block').animate({
opacity: 1
}, 200, () => {
this.isOpen = true;
});
} else {
sidebar.css({ display: 'block', opacity: 1 });
this.isOpen = true;
}
}
close(event, quick = false) {
if (event) { event.preventDefault(); }
let overlay = $('#overlay');
let sidebar = $('#admin-sidebar');
this.body.removeClass('sidebar-mobile-open');
overlay.css('display', 'none');
if (!quick) {
sidebar.animate({
opacity: 0
}, 200, () => {
sidebar.css('display', 'none');
this.isOpen = false;
});
} else {
sidebar.css({ opacity: 0, display: 'none' });
this.isOpen = false;
}
}
toggle(event) {
if (event) { event.preventDefault(); }
return this[this.isOpen ? 'close' : 'open'](event);
}
toggleSidebarState(event) {
if (event) { event.preventDefault(); }
clearTimeout(this.timeout);
let isDesktop = global.matchMedia(DESKTOP_QUERY).matches;
let cookie = null;
if (isDesktop) {
this.body.removeClass('sidebar-open');
}
if (!isDesktop) {
this.body.removeClass('sidebar-closed');
this.body.removeClass('sidebar-mobile-open');
}
this.body.toggleClass(`sidebar-${isDesktop ? 'closed' : 'open'}`);
$(global).trigger('sidebar_state._grav', isDesktop);
if (isDesktop) {
cookie = !this.body.hasClass('sidebar-closed');
} else {
cookie = this.body.hasClass('sidebar-open');
}
Cookies.set('grav-admin-sidebar', cookie, { expires: Infinity });
}
checkMatch(data) {
let sidebar = $('#admin-sidebar');
let overlay = $('#overlay');
this.isOpen = false;
overlay.css('display', 'none');
sidebar.css({
display: data.matches ? 'none' : 'inherit',
opacity: data.matches ? 0 : 1
});
if (data.matches) {
this.body.removeClass('sidebar-open sidebar-closed');
}
this[data.matches ? 'attach' : 'detach']();
}
_resetMap() {
return map.clear();
}
_getBound(fn) {
if (map.has(fn)) {
return map.get(fn);
}
return map.set(fn, this[fn].bind(this)).get(fn);
}
}
export let Instance = new Sidebar();

View File

@@ -0,0 +1,41 @@
// localStorage
(function() {
function isSupported() {
var item = 'localStoragePollyfill';
try {
localStorage.setItem(item, item);
localStorage.removeItem(item);
sessionStorage.setItem(item, item);
sessionStorage.removeItem(item);
return true;
} catch (e) {
return false;
}
}
if (!isSupported()) {
try {
Storage.prototype._data = {};
Storage.prototype.setItem = function(id, val) {
this._data[id] = String(val);
return this._data[id];
};
Storage.prototype.getItem = function(id) {
return this._data.hasOwnProperty(id) ? this._data[id] : undefined;
};
Storage.prototype.removeItem = function(id) {
return delete this._data[id];
};
Storage.prototype.clear = function() {
this._data = {};
return this._data;
};
} catch (e) {
console.error('localStorage pollyfill error: ', e);
}
}
}());

View File

@@ -0,0 +1,29 @@
import $ from 'jquery';
import Cookies from '../utils/cookies';
import { Instance as Editors } from '../forms/fields/editor';
let Data = JSON.parse(Cookies.get('grav-tabs-state') || '{}');
$('body').on('touchstart click', '[data-tabid]', (event) => {
event && event.stopPropagation();
let target = $(event.currentTarget);
Data[target.data('tabkey')] = target.data('scope');
Cookies.set('grav-tabs-state', JSON.stringify(Data), { expires: Infinity });
const panel = $(`[id="${target.data('tabid')}"]`);
target.siblings('[data-tabid]').removeClass('active');
target.addClass('active');
panel.siblings('[id]').removeClass('active');
panel.addClass('active');
Editors.editors.each((index, editor) => {
let codemirror = $(editor).data('codemirror');
if (!codemirror) { return; }
if (codemirror.display.lastWrapWidth === 0) {
codemirror.refresh();
}
});
});

View File

@@ -0,0 +1,6 @@
import toastr from 'toastr';
toastr.options.positionClass = 'toast-top-right';
toastr.options.preventDuplicates = true;
export default toastr;

View File

@@ -0,0 +1,25 @@
import { config } from 'grav-config';
import request from '../utils/request';
export default ({ preview = false, exportScss = false, color_scheme = {}, fonts = {}, callback = () => {} } = {}) => {
let task = exportScss ? 'exportScss' : 'compileScss';
// console.log(config);
const URI = `${config.base_url_relative}.json/task:${task}`;
request(URI, {
method: 'post',
body: Object.assign({}, preview ? { preview } : null, color_scheme)
}, callback);
};
export const prepareElement = (element) => {
element.data('busy_right_now', true);
if (!element.data('current_icon')) {
element.data('current_icon', element.find('.fa').attr('class'));
}
element.find('.fa').attr('class', 'fa fa-fw fa-spin fa-refresh');
};
export const resetElement = (element) => {
element.data('busy_right_now', false);
element.find('.fa').attr('class', element.data('current_icon'));
};

View File

@@ -0,0 +1,93 @@
import $ from 'jquery';
import Compile, { prepareElement, resetElement } from './compile';
import Forms from '../forms';
import { hex2rgb } from '../utils/colors';
import './presets';
const body = $('body');
const FormState = Forms.FormState.Instance;
const compiler = (element, preview = false, exportScss = false, callback = () => {}) => {
prepareElement(element);
let fields = FormState.collect();
Compile({
preview,
exportScss,
color_scheme: !fields ? [] : fields.filter((value, key) => key.match(/^data\[whitelabel]\[color_scheme]/)).toJS(),
callback: (response) => {
callback.call(callback, response);
resetElement(element);
}
});
};
body.on('click', '[data-preview-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
if (element.data('busy_right_now')) { return false; }
compiler(element, true, false, (response) => {
if (response.files) {
Object.keys(response.files).forEach((key) => {
let file = $(`#admin-pro-preview-${key}`);
let timestamp = Date.now();
if (!file.length) {
file = $(`<link id="admin-pro-preview-${key}" type="text/css" rel="stylesheet" />`);
$('head').append(file);
if (!$('[data-reset-scss]').length) {
let reset = $('<button class="button" data-reset-scss style="margin-left: 5px;"><i class="fa fa-fw fa-history"></i> Reset</button>');
reset.insertAfter(element);
}
}
file.attr('href', `${response.files[key]}?${timestamp}`);
});
}
});
});
body.on('click', '[data-recompile-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
if (element.data('busy_right_now')) { return false; }
compiler(element, true, false);
});
body.on('click', '[data-export-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
if (element.data('busy_right_now')) { return false; }
compiler(element, true, true, (response) => {
if (response.files) {
Object.keys(response.files).forEach((key) => {
if (key === 'download') {
let element = document.createElement('a');
element.setAttribute('href', response.files[key]);
element.setAttribute('download', '');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
});
}
});
});
body.on('change._grav_colorpicker', '[data-grav-colorpicker]', (event, input, hex, opacity) => {
let RGB = hex2rgb(hex);
let YIQ = ((RGB.r * 299) + (RGB.g * 587) + (RGB.b * 114)) / 1000;
let contrast = YIQ >= 128 || opacity <= 0.50 ? 'dark' : 'light';
input.parent().removeClass('dark-text light-text').addClass(`${contrast}-text`);
});
body.ready(() => {
$('[data-grav-colorpicker]').trigger('keyup');
});

View File

@@ -0,0 +1,169 @@
import $ from 'jquery';
import Forms from '../forms';
let body = $('body');
let fields = [];
const FormState = Forms.FormState.Instance;
const setField = (field, value) => {
let name = field.prop('name');
let tag = field.prop('tagName').toLowerCase();
let type = field.prop('type');
fields.push(name);
switch (tag) {
case 'select':
field.val(value);
field.data('selectize').setValue(value);
field.trigger('change');
break;
case 'input':
if (type === 'radio') {
let strValue = value ? '1' : '0';
field.filter((index, radio) => $(radio).val() === strValue).prop('checked', true);
break;
}
if (type === 'checkbox') {
field.prop('checked', value);
break;
}
field.val(value);
field.trigger('keyup');
}
};
body.on('click', '[data-preset-values]', (event) => {
let target = $(event.currentTarget);
let data = target.data('preset-values');
Object.keys(data).forEach((section) => {
if (typeof data[section] === 'string') {
return;
}
Object.keys(data[section]).forEach((key) => {
let field = $(`[name="data[whitelabel][color_scheme][${section}][${key}]"], [name="data[${section}][${key}]"]`);
let value = data[section][key];
setField(field, value);
});
});
});
body.on('click', '[data-reset-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
let links = $('link[id^=admin-pro-preview-]');
element.remove();
links.remove();
fields.forEach((field) => {
let value = FormState.loadState.get(field);
setField($(`[name="${field}"]`), value);
});
fields = [];
});
// Horizontal Scroll Functionality
$.fn.hscrollarrows = function() {
return this.each(function() {
let navNext = $('<a class="nav-next hide"></a>');
let navPrev = $('<a class="nav-prev hide"></a>');
let scrollTime = null;
let resizeTime = null;
let scrolling = false;
let elm_w = 0;
let elem_data_w = 0;
let max_scroll = 0;
let inc_scroll = 0;
let calcData = function() {
elm_w = elem.width();
elem_data_w = elem_data.get(0).scrollWidth;
max_scroll = elem_data_w - elm_w;
inc_scroll = elm_w * 0.3; // 20%
};
let revalidate = function() {
calcData();
stateNavs();
};
let run = function() {
calcData();
setupNavs();
};
let setupNavs = function() {
elem.parent().prepend(navNext);
elem.parent().prepend(navPrev);
navNext.on('click', next);
navPrev.on('click', prev);
stateNavs();
$(elem).scroll(function() {
if (!scrolling) {
clearTimeout(scrollTime);
scrollTime = setTimeout(function() {
stateNavs();
}, 250);
}
});
$(window).resize(function() {
clearTimeout(resizeTime);
resizeTime = setTimeout(function() {
revalidate();
}, 250);
});
};
let stateNavs = function() {
let current_scroll = elem.scrollLeft();
if (current_scroll < max_scroll) {
navNext.removeClass('hide');
} else {
navNext.addClass('hide');
}
if (current_scroll > 0) {
navPrev.removeClass('hide');
} else {
navPrev.addClass('hide');
}
scrolling = false;
};
let next = function() {
let current_scroll = elem.scrollLeft();
if (current_scroll < max_scroll) {
scrolling = true;
elem.stop().animate({
scrollLeft: (current_scroll + inc_scroll)
}, stateNavs);
}
};
let prev = function() {
let current_scroll = elem.scrollLeft();
if (current_scroll > 0) {
scrolling = true;
elem.stop().animate({
scrollLeft: (current_scroll - inc_scroll)
}, stateNavs);
}
};
let elem = $(this);
let elem_data = $(':first-child', elem);
run();
});
};
$(document).ready(() => {
$('.jquery-horizontal-scroll').hscrollarrows();
});