NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name UserStyles.Org Tweaks
// @namespace almaceleste
// @version 0.2.1
// @description some fixes and tweaks for userstyle.org
// @description:ru несколько исправлений и твиков для userstyle.org
// @author GNU Affero GPL 3.0 🄯 2020 almaceleste (https://almaceleste.github.io)
// @license AGPL-3.0-or-later; http://www.gnu.org/licenses/agpl.txt
// @icon https://userstyles.org/ui/images/icons/favicon.png
// @icon64 https://userstyles.org/ui/images/icons/app_icon.png
// @homepageURL https://greasyfork.org/en/users/174037-almaceleste
// @homepageURL https://openuserjs.org/users/almaceleste
// @homepageURL https://github.com/almaceleste/userscripts
// @supportURL https://github.com/almaceleste/userscripts/issues
// @updateURL https://github.com/almaceleste/userscripts/raw/master/src/UserStyles.Org_Tweaks.user.js
// @downloadURL https://github.com/almaceleste/userscripts/raw/master/src/UserStyles.Org_Tweaks.user.js
// @downloadURL https://openuserjs.org/install/almaceleste/UserStyles.Org_Tweaks.user.js
// @require https://code.jquery.com/jquery-3.3.1.js
// @require https://raw.githubusercontent.com/uzairfarooq/arrive/master/minified/arrive.min.js
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @grant GM_getResourceText
// @resource css https://github.com/almaceleste/userscripts/raw/master/css/default.css
// @match http*://*.userstyles.org/styles/new
// @match http*://*.userstyles.org/d/styles/new
// @match http*://*.userstyles.org/styles/*/edit
// @match http*://*.userstyles.org/d/styles/*/edit
// ==/UserScript==
// ==OpenUserJS==
// @author almaceleste
// ==/OpenUserJS==
// script global variables
const offset = 50;
const frame = '#iframe';
const form = `body > .PageContent > form[action='/styles/create']`;
const newoptionbutton = `.new-option > input[type='button']`;
const newsettingbutton = `#new-setting > input[type='button']`;
const newdropdownbutton = `${newsettingbutton}:nth-of-type(1)`;
const newcolorbutton = `${newsettingbutton}:nth-of-type(2)`;
const newtextbutton = `${newsettingbutton}:nth-of-type(3)`;
const textarea = `${form} textarea`;
const img = `${form} img`;
const codeeditor = `${form} #enable-source-editor-code`;
const codetext = `${form} #css`;
const settingsection = `${form} #edit-style-settings`;
const dropdownsetting = `${settingsection} > .edit-dropdown-setting`;
const colorsetting = `${settingsection} > .edit-color-setting`;
const textsetting = `${settingsection} > .edit-text-setting`;
const settinglabel = `input:nth-of-type(1)`;
const settingkey = `input:nth-of-type(2)`;
const settingvalue = `input:nth-of-type(6)`;
const optionlabel = `input:nth-of-type(1)`;
const optionkey = `input:nth-of-type(2)`;
const optiondefault = `input:nth-of-type(3)`;
const optionvalue = `textarea`;
const quotes = "'" + '"' + '`';
// arrive.js options
const existing = {
existing: true
};
const onceonly = {
onceOnly: true,
existing: false
}
// config settings
const configId = 'usotweaksCfg';
const iconUrl = GM_info.script.icon64;
const pattern = {};
pattern[`#${configId}`] = /#configId/g;
pattern[`${iconUrl}`] = /iconUrl/g;
let css = GM_getResourceText('css');
Object.keys(pattern).forEach((key) => {
css = css.replace(pattern[key], key);
});
const windowcss = css;
const iframecss = `
height: 255px;
width: 435px;
border: 1px solid;
border-radius: 3px;
position: fixed;
z-index: 99999;
`;
GM_registerMenuCommand(`${GM_info.script.name} Settings`, () => {
GM_config.open();
GM_config.frame.style = iframecss;
});
GM_config.init({
id: `${configId}`,
title: `${GM_info.script.name} ${GM_info.script.version}`,
fields: {
frameWidth: {
section: ['', 'New style and Edit pages'],
label: 'frame width in px',
labelPos: 'left',
title: `editing frame width in pixels
0 - default width`,
type: 'int',
default: 0,
},
fixframeHeight: {
label: 'fix frame height on changing content',
labelPos: 'right',
title: `fix frame height when adding new settings and options or resizing text area`,
type: 'checkbox',
default: true,
},
fixtextareaWidth: {
label: 'fix textarea width',
labelPos: 'right',
title: ``,
type: 'checkbox',
default: true,
},
parsecode: {
label: 'add parse button to the new/edit page',
labelPos: 'right',
title: ``,
type: 'checkbox',
default: true,
},
parsetargets: {
title: 'only variables for the uso preprocessor yet',
type: 'multicheckbox',
options: {
// name: true,
// description: true,
variables: true,
},
default: {variables: true},
},
support: {
section: ['', 'Support'],
label: 'almaceleste.github.io',
title: 'more info on almaceleste.github.io',
type: 'button',
click: () => {
GM_openInTab('https://almaceleste.github.io', {
active: true,
insert: true,
setParent: true
});
}
},
},
types: {
multicheckbox: {
default: {},
toNode: function() {
let field = this.settings,
values = this.value,
options = field.options,
id = this.id,
configId = this.configId,
labelPos = field.labelPos,
create = this.create;
// console.log('toNode:', field, values, options);
function addLabel(pos, labelEl, parentNode, beforeEl) {
if (!beforeEl) beforeEl = parentNode.firstChild;
switch (pos) {
case 'right': case 'below':
if (pos == 'below')
parentNode.appendChild(create('br', {}));
parentNode.appendChild(labelEl);
break;
default:
if (pos == 'above')
parentNode.insertBefore(create('br', {}), beforeEl);
parentNode.insertBefore(labelEl, beforeEl);
}
}
let retNode = create('div', {
className: 'config_var multicheckbox',
id: `${configId}_${id}_var`,
title: field.title || ''
}),
firstProp;
// Retrieve the first prop
for (let i in field) { firstProp = i; break; }
let label = field.label ? create('label', {
className: 'field_label',
id: `${configId}_${id}_field_label`,
for: `${configId}_field_${id}`,
}, field.label) : null;
let wrap = create('ul', {
id: `${configId}_field_${id}`
});
this.node = wrap;
for (const key in values) {
// console.log('toNode:', key);
const inputId = `${configId}_${id}_${key}_checkbox`;
const li = wrap.appendChild(create('li', {
}));
li.appendChild(create('input', {
checked: values[key],
id: inputId,
type: 'checkbox',
value: key,
}));
li.appendChild(create('label', {
className: 'option_label',
for: inputId,
}, key));
}
retNode.appendChild(wrap);
if (label) {
// If the label is passed first, insert it before the field
// else insert it after
if (!labelPos)
labelPos = firstProp == "label" ? "left" : "right";
addLabel(labelPos, label, retNode);
}
return retNode;
},
toValue: function() {
let node = this.node,
id = node.id,
rval = {};
// console.log('toValue:', node, this);
if (!node) return rval;
let nodelist = node.querySelectorAll(`#${id} input`);
// console.log('nodelist:', document.querySelectorAll(`#${id} input:checked`), nodelist);
nodelist.forEach((input) => {
// console.log('toValue:', input);
const value = input.checked;
const key = input.value;
rval[key] = value;
});
// console.log('toValue:', rval);
return rval;
},
reset: function() {
let node = this.node,
values = this.default;
// console.log('reset:', node, values, Object.values(values));
const inputs = node.getElementsByTagName('input');
for (const index in inputs) {
const input = inputs[index];
input.checked = values[input.value];
}
}
}
},
css: windowcss,
events: {
save: function() {
GM_config.close();
}
},
});
// script code
function clicklistener(selector){
$(form).arrive(selector, existing, (element) => {
$(element).on({
click: () => {
fixframeHeight();
}
});
});
}
function addinglistener(selector){
$(form).arrive(selector, existing, () => {
fixframeHeight();
});
}
function resizelistener(selector){
$(form).arrive(selector, existing, (element) => {
const options = {once: true};
$(element).on({
mousedown: () => {
window.addEventListener('mouseup', fixframeHeight, options);
},
mouseup: () => {
fixframeHeight();
window.removeEventListener('mouseup', fixframeHeight, options);
}
});
});
}
function fixWidth(selector){
$(document).arrive(selector, existing, (element) => {
$(element).css({
resize: 'vertical',
})
});
}
function fixframeHeight(){
const height = $('body').height() + offset;
const frame = window.parent.document.getElementById('iframe');
$(frame).height(height);
}
function fixframeWidth(width){
const frame = window.parent.document.getElementById('iframe');
$(frame).css({
left: '50%',
position: 'relative',
transform: 'translateX(-50%)',
}).width(width);
}
function addparseButton() {
const parseButton = $('<button></button>', {
id: 'parseButton',
text: 'Parse Code',
type: 'button',
on: {
click: () => {
parseCode();
}
}
});
$(codetext).after(parseButton);
}
function parseCode() {
const parsetargets = GM_config.get('parsetargets');
if (Object.keys(parsetargets).length > 0) {
if ($(codeeditor).prop('checked')) {
$(codeeditor).trigger('click');
}
// get css code from css textarea
const code = $(codetext).val();
// get metadata block from css code
const meta = code.match(/\/\* ==UserStyle==[\s\S]*==\/UserStyle== \*\//)[0];
if (parsetargets.variables) {
// get preprocessor type from metadata
const preprocessor = meta.match(/@preprocessor *(.*)(?![\s\S]*@preprocessor)/)[1];
if (preprocessor == 'uso') {
// get array of variables from metadata
const variables = getVariables(meta);
// console.log('parseCode:', preprocessor, variables);
variables.forEach((variable) => {
parseVariable(variable);
});
// for (let index in variables) {
// parseVariable(variables[index]);
// }
}
}
}
}
function parseVariable(variable) {
const varprops = variable.match(/@var[\s]+([^\s]+)[\s]+([^\s]+)[\s]+([^\s'"`]*|'.*'|".*"|`.*`)[\s]+([\s\S]+)/);
// console.log('variable:', props);
const type = varprops[1];
const props = {
key: varprops[2],
label: trimChars(varprops[3], quotes),
value: trimChars(varprops[4], quotes)
};
switch (type) {
case 'text':
fillSetting(newtextbutton, textsetting, props);
break;
case 'color':
fillSetting(newcolorbutton, colorsetting, props);
break;
case 'checkbox':
// not supported in uso
break;
case 'select':
fillSetting(newdropdownbutton, dropdownsetting, props);
break;
case 'range':
// not supported in uso
break;
case 'number':
// not supported in uso
break;
default:
break;
}
}
function fillSetting(button, path, props) {
// console.log('fillSetting:', props.value);
$(button).one({
click: () => {
// console.log('click:', props);
$(form).arrive(path, onceonly, (element) => {
// console.log('arrive:', props);
let setting = $(element);
// console.log('fillSetting:', props.value.substr(0, 1), props.value.startsWith('{'), props);
if (/^(\[|\{)/.test(props.value)) {
// if (props.value.startsWith('{')) {
// console.log('fillSetting:', props.value.substr(0, 1), props.value.startsWith('{'), props);
setting = $(element).children('.edit-setting');
fillOptions($(element).find(newoptionbutton), $(element).children('.edit-style-options'), props.value);
}
else {
$(setting).children(settingvalue).val(props.value);
}
$(setting).children(settingkey).val(props.key);
$(setting).children(settinglabel).val(props.label);
});
}
}).click();
}
function fillOptions(button, path, options) {
let isArray;
switch (true) {
case options.startsWith('['):
options = trimChars(options, '\\[\\]\\s');
isArray = true;
break;
case options.startsWith('{'):
options = trimChars(options, '\\{\\}\\s');
isArray = false;
break;
default:
break;
}
const opts = options.split(/,[\s\n]*/);
console.log('fillOptions:', options, opts);
opts.forEach((opt, index) => {
const props = {};
if (isArray) {
props.key = trimChars(opt, quotes);
props.value = trimChars(props.key, '\\*');
}
else {
// split option to key and value
const keyvalue = opt.match(/(['"`])(.*)\1:[\s\n]*(['"`])([\s\S]*)\3/m);
props.key = trimChars(keyvalue[2], quotes + '\\n');
// console.log('fillOptions (split):', keyvalue, props.key);
// props.key = props.key.match(/(\s)*([^\s][\S\s]*)/);
props.value = trimChars(keyvalue.length > 1 ? keyvalue[4] : props.key, quotes + '\\n\\*');
props.value = shiftTabs(props.value);
}
props.default = props.key.endsWith('*'); // check if this option is default
props.key = trimChars(props.key, '\\*');
// split key to key and label
const keylabel = props.key.split(':', 2);
props.key = keylabel[0];
props.label = keylabel[keylabel.length > 1 ? 1 : 0];
// console.log('fillOptions (forEach):', opt, props);
const elementpath = `li:nth-of-type(${index + 1})`;
if (index < 2) {
const element = $(path).children(elementpath);
fillOption(element, props);
}
else {
// console.log('fillOptions (2+):', props);
$(button).one({
click: () => {
// console.log('click:', props);
$(path).arrive(elementpath, onceonly, (element) => {
// console.log('arrive:', props);
fillOption(element, props);
// $(path).unbindArrive(elementpath);
});
}
}).click();
}
});
}
function fillOption(element, props) {
const option = $(element).children('.edit-option');
// console.log('fillOption:', option, props);
$(option).children(optionkey).val(props.key);
$(option).children(optionlabel).val(props.label);
$(option).children(optiondefault).prop('checked', props.default);
$(element).children(optionvalue).val(props.value);
fixframeHeight();
}
function getVariables(meta) {
let variables = [];
let result;
const pattern = /(@var *[\s\S]*?)(?:\n\s*@|==\/UserStyle== \*\/)/g;
while ((result = pattern.exec(meta)) !== null) {
// console.log('while:', result);
variables.push(result[1]);
pattern.lastIndex--;
}
return variables;
}
function trimChars(string, chars) {
const pattern = RegExp(`^[${chars}]*(.*?)[${chars}]*$`, 'm');
string = string.replace(pattern, '$1');
// console.log('trimChars:', pattern, string);
return string;
}
function shiftTabs(string) {
const tabs = string.match(/^(\s)*/)[1];
const pattern = RegExp(`(\n)${tabs}`, 'g');
string = string.replace(pattern, '$1');
return string; //.trimLeft();
}
(function() {
'use strict';
$(document).ready(() => {
if (!(window.self === window.top)) {
const frameWidth = GM_config.get('frameWidth');
if (frameWidth != '0') {
fixframeWidth(frameWidth);
}
if (GM_config.get('fixframeHeight')) {
clicklistener(newsettingbutton);
clicklistener(newoptionbutton);
resizelistener(textarea);
addinglistener(img);
}
if (GM_config.get('fixtextareaWidth')) {
fixWidth(textarea);
}
if (GM_config.get('parsecode')) {
addparseButton();
}
}
});
})();