Files
ExpiryIndicator/sharepoint/assets/ExpiryIndicatorClassic.js

523 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function (window, document) {
'use strict';
var FIELD_CUSTOMIZER_ID = 'a555f4fc-d6a6-4421-8189-457449d9bbde';
var state = { context: null, field: null, config: null, loading: null };
function configuredExpiryField() {
var scripts = document.getElementsByTagName('script');
for (var index = scripts.length - 1; index >= 0; index--) {
var source = String(scripts[index].src || '');
if (source.indexOf('ExpiryIndicatorClassic.js') < 0) { continue; }
var match = /[?&]expiryField=([^&]+)/i.exec(source);
if (match) { return decodeURIComponent(match[1]); }
}
return '';
}
function discoverExpiryField() {
var id = listId(null);
if (!id) { return ''; }
try {
var xhr = new XMLHttpRequest();
xhr.open('GET', listUrl(id) +
'/fields?$select=InternalName,ClientSideComponentId', false);
xhr.setRequestHeader('Accept', 'application/json;odata=verbose');
xhr.setRequestHeader('OData-Version', '3.0');
xhr.send(null);
if (xhr.status < 200 || xhr.status >= 300) { return ''; }
var data = JSON.parse(xhr.responseText || '{}');
var fields = data.d ? data.d.results : (data.value || []);
for (var index = 0; index < fields.length; index++) {
var componentId = String(fields[index].ClientSideComponentId || '').replace(/[{}]/g, '').toLowerCase();
if (componentId === FIELD_CUSTOMIZER_ID) { return fields[index].InternalName; }
}
} catch (ignore) { }
return '';
}
function pageContext() {
return window._spPageContextInfo || {};
}
function webUrl() {
return String(pageContext().webAbsoluteUrl || '').replace(/\/$/, '');
}
function listId(context) {
return String((context && context.listName) || pageContext().pageListId || '').replace(/[{}]/g, '');
}
function request(method, url, body) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.setRequestHeader('Accept', 'application/json;odata=verbose');
xhr.setRequestHeader('Content-Type', 'application/json;odata=verbose');
xhr.setRequestHeader('OData-Version', '3.0');
if (method !== 'GET') {
var digest = document.getElementById('__REQUESTDIGEST');
if (digest) { xhr.setRequestHeader('X-RequestDigest', digest.value); }
}
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) { return; }
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText ? JSON.parse(xhr.responseText) : {});
} else {
var message = xhr.status + ' ' + xhr.statusText;
try {
var error = JSON.parse(xhr.responseText);
message = error.error && error.error.message ? error.error.message.value : message;
} catch (ignore) { }
reject(new Error(message));
}
};
xhr.send(body ? JSON.stringify(body) : null);
});
}
function listUrl(id) {
return webUrl() + "/_api/web/lists(guid'" + id + "')";
}
function defaults(expiryField) {
return {
baseField: 'Created',
expiryField: expiryField || 'ExpiryDate',
default: {
lifeTime: { value: 2, unit: 'years' },
columnRule: []
},
rules: [],
nullText: 'Kein Ablaufdatum',
confirmExtension: true
};
}
function loadConfig(context) {
var id = listId(context);
if (!id) { return Promise.reject(new Error('Keine Liste erkannt.')); }
if (state.loading && state.context === id) { return state.loading; }
state.context = id;
state.loading = request('GET', listUrl(id) +
'/fields?$select=Id,InternalName,ClientSideComponentId,ClientSideComponentProperties')
.then(function (data) {
var fields = data.d ? data.d.results : (data.value || []);
var field;
for (var index = 0; index < fields.length; index++) {
var componentId = String(fields[index].ClientSideComponentId || '').replace(/[{}]/g, '').toLowerCase();
if (componentId === FIELD_CUSTOMIZER_ID) { field = fields[index]; break; }
}
if (!field) { throw new Error('Kein ExpiryIndicator-Feld gebunden.'); }
var config = defaults(field.InternalName);
try {
if (field.ClientSideComponentProperties) {
var parsed = JSON.parse(field.ClientSideComponentProperties);
for (var name in parsed) {
if (Object.prototype.hasOwnProperty.call(parsed, name)) { config[name] = parsed[name]; }
}
}
} catch (ignore) { }
config.expiryField = field.InternalName;
state.field = field;
state.config = config;
return config;
});
return state.loading;
}
function ruleFields(config) {
var result = [];
function add(name) {
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name || '') && result.indexOf(name) < 0) { result.push(name); }
}
function visit(condition) {
if (!condition) { return; }
if (condition.conditions instanceof Array) {
for (var index = 0; index < condition.conditions.length; index++) { visit(condition.conditions[index]); }
} else { add(condition.columnName); }
}
for (var index = 0; index < (config.rules || []).length; index++) {
var rule = config.rules[index];
if (rule.condition) { visit(rule.condition); } else { add(rule.columnName); }
}
return result;
}
function equalsIgnoreCase(left, right) {
return String(left).toLowerCase() === String(right).toLowerCase();
}
function valueMatches(rawValue, configuredValue) {
if (rawValue === null || typeof rawValue === 'undefined') { return String(configuredValue) === ''; }
var values = rawValue instanceof Array ? rawValue :
(rawValue.results instanceof Array ? rawValue.results : [rawValue]);
for (var index = 0; index < values.length; index++) {
var value = values[index];
if (value && typeof value === 'object') {
if (value.TermGuid && equalsIgnoreCase(value.TermGuid, configuredValue)) { return true; }
value = value.Title || value.LookupValue || value.Label || value.Value || '';
}
var text = String(value);
if (text === String(configuredValue)) { return true; }
var separator = text.lastIndexOf('|');
if (separator >= 0 && equalsIgnoreCase(text.substring(separator + 1), configuredValue)) { return true; }
}
return false;
}
function conditionMatches(condition, values) {
if (condition && condition.conditions instanceof Array) {
if (condition.operator === 'and') {
for (var andIndex = 0; andIndex < condition.conditions.length; andIndex++) {
if (!conditionMatches(condition.conditions[andIndex], values)) { return false; }
}
return condition.conditions.length > 0;
}
if (condition.operator === 'or') {
for (var orIndex = 0; orIndex < condition.conditions.length; orIndex++) {
if (conditionMatches(condition.conditions[orIndex], values)) { return true; }
}
}
return false;
}
return !!condition && Object.prototype.hasOwnProperty.call(values, condition.columnName) &&
valueMatches(values[condition.columnName], condition.columnValue);
}
function validateRules(rules) {
var errors = [];
function safeName(value) { return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value || ''); }
function validateGroup(group, path, depth) {
if (depth > 10) { errors.push(path + ': maximale Verschachtelungstiefe 10 überschritten.'); return; }
if (!group || (group.operator !== 'and' && group.operator !== 'or')) {
errors.push(path + '.operator muss "and" oder "or" sein.');
}
if (!group || !(group.conditions instanceof Array) || !group.conditions.length) {
errors.push(path + '.conditions muss mindestens eine Bedingung enthalten.'); return;
}
for (var index = 0; index < group.conditions.length; index++) {
var condition = group.conditions[index];
var childPath = path + '.conditions[' + index + ']';
if (condition && typeof condition.operator !== 'undefined') { validateGroup(condition, childPath, depth + 1); }
else if (!condition || !safeName(condition.columnName) || typeof condition.columnValue === 'undefined') {
errors.push(childPath + ': columnName/columnValue fehlen oder sind ungültig.');
}
}
}
if (!(rules instanceof Array)) { return ['rules muss ein JSON-Array sein.']; }
for (var index = 0; index < rules.length; index++) {
var rule = rules[index];
if (rule && rule.condition) { validateGroup(rule.condition, 'rules[' + index + '].condition', 0); }
else if (!rule || !safeName(rule.columnName) || typeof rule.columnValue === 'undefined') {
errors.push('rules[' + index + '] benötigt condition oder columnName/columnValue.');
}
}
return errors;
}
function behavior(config, values) {
for (var index = 0; index < (config.rules || []).length; index++) {
var rule = config.rules[index];
var matched = rule.condition ? conditionMatches(rule.condition, values) :
Object.prototype.hasOwnProperty.call(values, rule.columnName) &&
valueMatches(values[rule.columnName], rule.columnValue);
if (matched) { return rule; }
}
return config.default;
}
function parseDate(value) {
if (!value) { return null; }
var match = /\/Date\((-?\d+)/.exec(String(value));
var result = match ? new Date(Number(match[1])) : new Date(value);
return isNaN(result.getTime()) ? null : result;
}
function formatDate(value) {
function pad(number) { return number < 10 ? '0' + number : String(number); }
return pad(value.getDate()) + '.' + pad(value.getMonth() + 1) + '.' + value.getFullYear();
}
function addDuration(source, duration) {
var result = new Date(source.getTime());
if (duration.unit === 'days') {
result.setUTCDate(result.getUTCDate() + duration.value);
return result;
}
var months = duration.unit === 'years' ? duration.value * 12 : duration.value;
var day = result.getUTCDate();
result.setUTCDate(1);
result.setUTCMonth(result.getUTCMonth() + months);
var lastDay = new Date(Date.UTC(result.getUTCFullYear(), result.getUTCMonth() + 1, 0)).getUTCDate();
result.setUTCDate(Math.min(day, lastDay));
return result;
}
function dayDifference(from, to) {
var fromDay = Date.UTC(from.getUTCFullYear(), from.getUTCMonth(), from.getUTCDate());
var toDay = Date.UTC(to.getUTCFullYear(), to.getUTCMonth(), to.getUTCDate());
return Math.round((toDay - fromDay) / 86400000);
}
function colorRule(days, rules) {
function matches(operator, threshold) {
if (operator === 'lessThan') { return days < threshold; }
if (operator === 'lessOrEqual') { return days <= threshold; }
if (operator === 'equal') { return days === threshold; }
if (operator === 'greaterOrEqual') { return days >= threshold; }
return operator === 'greaterThan' && days > threshold;
}
for (var index = 0; index < (rules || []).length; index++) {
if (matches(rules[index].operator, rules[index].daysUntilExpiry)) { return rules[index]; }
}
return null;
}
function findRow(id) {
var rows = document.querySelectorAll('tr[iid]');
for (var index = 0; index < rows.length; index++) {
var parts = String(rows[index].getAttribute('iid') || '').split(',');
if (String(parts[1]) === String(id)) { return rows[index]; }
}
return null;
}
function applyGradient(row, color, textColor, title) {
var allCells = row.querySelectorAll('td');
var cells = [];
for (var index = 0; index < allCells.length; index++) {
if (!allCells[index].querySelector('input[type="checkbox"]') &&
String(allCells[index].className).indexOf('ms-vb-itmcbx') < 0) {
cells.push(allCells[index]);
}
}
if (!cells.length) { return; }
var first = cells[0].getBoundingClientRect();
var last = cells[cells.length - 1].getBoundingClientRect();
var width = Math.max(1, last.right - first.left);
var gradient = 'linear-gradient(to right, #ffffff, ' + color + ')';
for (var cellIndex = 0; cellIndex < cells.length; cellIndex++) {
var rect = cells[cellIndex].getBoundingClientRect();
cells[cellIndex].style.backgroundColor = '#ffffff';
cells[cellIndex].style.backgroundImage = gradient;
cells[cellIndex].style.backgroundSize = width + 'px 100%';
cells[cellIndex].style.backgroundPosition = (-(rect.left - first.left)) + 'px 0';
cells[cellIndex].style.backgroundRepeat = 'no-repeat';
cells[cellIndex].style.color = textColor;
cells[cellIndex].title = title;
}
}
function render(context) {
loadConfig(context).then(function (config) {
var rows = context && context.ListData && context.ListData.Row ? context.ListData.Row : [];
var ids = [];
for (var index = 0; index < rows.length; index++) {
var id = Number(rows[index].ID || rows[index].Id);
if (id > 0) { ids.push(id); }
}
if (!ids.length) { return; }
var fields = [config.baseField, config.expiryField].concat(ruleFields(config));
var select = ['Id'];
for (var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
if (select.indexOf(fields[fieldIndex]) < 0) { select.push(fields[fieldIndex]); }
}
var filters = [];
for (var idIndex = 0; idIndex < ids.length; idIndex++) { filters.push('Id eq ' + ids[idIndex]); }
return request('GET', listUrl(listId(context)) + '/items?$select=' + select.join(',') +
'&$filter=' + encodeURIComponent(filters.join(' or ')) + '&$top=' + ids.length)
.then(function (data) {
var items = data.d ? data.d.results : (data.value || []);
for (var itemIndex = 0; itemIndex < items.length; itemIndex++) {
var item = items[itemIndex];
var created = parseDate(item[config.baseField]);
var expiry = parseDate(item[config.expiryField]);
if (!created && !expiry) { continue; }
var selected = behavior(config, item);
var effective = expiry || addDuration(created, selected.lifeTime);
var days = dayDifference(new Date(), effective);
var rule = colorRule(days, selected.columnRule);
var row = findRow(item.Id);
var marker = row ? row.querySelector('[data-expiry-classic-item-id="' + item.Id + '"]') : null;
if (marker) {
marker.innerHTML = '';
marker.appendChild(document.createTextNode(formatDate(effective) +
(rule && rule.label ? ' ' + rule.label : '')));
marker.title = days + ' Resttage' + (!expiry ? ' aus ' + config.baseField + ' berechnet' : '');
}
if (row && rule) {
applyGradient(row, rule.backgroundColor, rule.textColor,
days + ' Resttage' + (rule.label ? ' ' + rule.label : ''));
}
}
});
}).catch(function (error) {
if (window.console && console.warn) { console.warn('ExpiryIndicator Classic:', error.message); }
});
}
function selectedIds() {
try {
var selected = SP.ListOperation.Selection.getSelectedItems();
var ids = [];
for (var index = 0; index < selected.length; index++) { ids.push(Number(selected[index].id)); }
return ids;
} catch (ignore) { return []; }
}
function extendOne(id, config) {
var fields = [config.baseField, config.expiryField].concat(ruleFields(config));
return request('GET', listUrl(state.context) + '/items(' + id + ')?$select=Id,' + fields.join(','))
.then(function (data) {
var item = data.d || data;
var created = parseDate(item[config.baseField]);
var expiry = parseDate(item[config.expiryField]);
var base = expiry || addDuration(created, behavior(config, item).lifeTime);
var updated = addDuration(base, { value: 1, unit: 'years' });
var body = { '__metadata': { 'type': item.__metadata.type } };
body[config.expiryField] = updated.toISOString();
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('POST', listUrl(state.context) + '/items(' + id + ')', true);
xhr.setRequestHeader('Accept', 'application/json;odata=verbose');
xhr.setRequestHeader('Content-Type', 'application/json;odata=verbose');
xhr.setRequestHeader('OData-Version', '3.0');
xhr.setRequestHeader('IF-MATCH', '*');
xhr.setRequestHeader('X-HTTP-Method', 'MERGE');
var digest = document.getElementById('__REQUESTDIGEST');
if (digest) { xhr.setRequestHeader('X-RequestDigest', digest.value); }
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) { return; }
if (xhr.status >= 200 && xhr.status < 300) { resolve(); }
else { reject(new Error('#' + id + ': ' + xhr.status + ' ' + xhr.statusText)); }
};
xhr.send(JSON.stringify(body));
});
});
}
function extendSelected() {
var ids = selectedIds();
if (!ids.length) { alert('Bitte mindestens ein Element auswählen.'); return; }
loadConfig(state.context).then(function (config) {
if (config.confirmExtension && !confirm('Ablaufdatum für ' + ids.length + ' Element(e) um ein Jahr verlängern?')) {
return;
}
return Promise.all(ids.map(function (id) {
return extendOne(id, config).then(function () {
return { id: id, succeeded: true };
}, function (error) {
return { id: id, succeeded: false, error: error.message };
});
})).then(function (results) {
var succeeded = 0;
var errors = [];
for (var index = 0; index < results.length; index++) {
if (results[index].succeeded) { succeeded++; }
else { errors.push('#' + results[index].id + ': ' + results[index].error); }
}
var message = 'Erfolgreich aktualisiert: ' + succeeded + '; Fehler: ' + errors.length;
if (errors.length) { message += '\n\n' + errors.slice(0, 10).join('\n'); }
alert(message);
if (succeeded > 0) { window.location.reload(); }
});
}).catch(function (error) { alert(error.message); });
}
function canExtend() {
return selectedIds().length > 0;
}
function settingsRequested() {
return /(?:\?|&)expiryIndicatorSettings=(?:1|true|yes)(?:&|$)/i.test(window.location.search);
}
function openSettings() {
loadConfig(state.context).then(function (config) {
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;z-index:100000;inset:0;background:rgba(0,0,0,.35);padding:5vh 10vw';
var panel = document.createElement('div');
panel.style.cssText = 'background:#fff;padding:20px;height:80vh;font-family:Segoe UI,Arial;box-sizing:border-box';
panel.innerHTML = '<h2>ExpiryIndicator-Einstellungen</h2>' +
'<p>Vollständige JSON-Konfiguration für diese Liste/Bibliothek:</p>' +
'<textarea style="width:100%;height:60%;box-sizing:border-box;font-family:Consolas"></textarea>' +
'<div data-error style="color:#a4262c;min-height:24px"></div>' +
'<div style="text-align:right"><button data-cancel>Abbrechen</button> ' +
'<button data-save>Speichern</button></div>';
overlay.appendChild(panel);
document.body.appendChild(overlay);
var textarea = panel.querySelector('textarea');
textarea.value = JSON.stringify(config, null, 2);
panel.querySelector('[data-cancel]').onclick = function () { document.body.removeChild(overlay); };
panel.querySelector('[data-save]').onclick = function () {
var parsed;
try { parsed = JSON.parse(textarea.value); }
catch (error) { panel.querySelector('[data-error]').innerHTML = 'Ungültiges JSON: ' + error.message; return; }
var validationErrors = validateRules(parsed.rules);
if (validationErrors.length) {
panel.querySelector('[data-error]').innerHTML = validationErrors.join('<br>');
return;
}
parsed.expiryField = state.field.InternalName;
var body = {
'__metadata': { 'type': state.field.__metadata && state.field.__metadata.type || 'SP.Field' },
'ClientSideComponentProperties': JSON.stringify(parsed)
};
var fieldId = String(state.field.Id).replace(/[{}]/g, '');
var xhr = new XMLHttpRequest();
xhr.open('POST', listUrl(state.context) + "/fields(guid'" + fieldId + "')", true);
xhr.setRequestHeader('Accept', 'application/json;odata=verbose');
xhr.setRequestHeader('Content-Type', 'application/json;odata=verbose');
xhr.setRequestHeader('OData-Version', '3.0');
xhr.setRequestHeader('IF-MATCH', '*');
xhr.setRequestHeader('X-HTTP-Method', 'MERGE');
var digest = document.getElementById('__REQUESTDIGEST');
if (digest) { xhr.setRequestHeader('X-RequestDigest', digest.value); }
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) { return; }
if (xhr.status >= 200 && xhr.status < 300) { window.location.reload(); }
else { panel.querySelector('[data-error]').innerHTML = 'Speichern fehlgeschlagen: ' + xhr.statusText; }
};
xhr.send(JSON.stringify(body));
};
}).catch(function (error) { alert(error.message); });
}
window.ExpiryIndicatorClassic = {
canExtend: canExtend,
extendSelected: extendSelected,
openSettings: openSettings,
render: render
};
function register() {
if (window.SPClientTemplates && SPClientTemplates.TemplateManager) {
var overrides = { OnPostRender: render };
var expiryField = configuredExpiryField() || discoverExpiryField();
if (expiryField) {
overrides.Templates = { Fields: {} };
overrides.Templates.Fields[expiryField] = {
View: function (context) {
var item = context.CurrentItem || {};
var id = Number(item.ID || item.Id);
var date = parseDate(item[expiryField]);
var value = date ? formatDate(date) : '';
var encoded = window.STSHtmlEncode ? STSHtmlEncode(value) : value;
return '<span data-expiry-classic-item-id="' + id + '">' + encoded + '</span>';
}
};
}
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrides);
}
if (settingsRequested()) { setTimeout(openSettings, 0); }
}
if (window.SPClientTemplates) { register(); }
else if (window._spBodyOnLoadFunctionNames) { window._spBodyOnLoadFunctionNames.push('ExpiryIndicatorClassicRegister'); }
window.ExpiryIndicatorClassicRegister = register;
if (window.NotifyScriptLoadedAndExecuteWaitingJobs) {
window.NotifyScriptLoadedAndExecuteWaitingJobs('ExpiryIndicatorClassic.js');
}
})(window, document);