I would love this.
Would remove a lot of bloat, and I like having a common framework to build upon.
Is there a git repository of this somewhere, or are you just using Greasyfork directly?
I would love this.
Would remove a lot of bloat, and I like having a common framework to build upon.
Is there a git repository of this somewhere, or are you just using Greasyfork directly?
Itās currently just on my dev machine because Iām still making a lot of big design decisions. But if people want a really early look, I can throw something on github soon. A lot of it is still just snippets from various experiments.
I only put things on greasyfork when theyāre ready for release.
Iāve been pondering the frameworkās Settings module.
I had originally considered putting settings on a separate URL, similar to Wanikaniās built-in settings pages. That might work for some scripts, but other scripts really need quick access to settings without leaving the page⦠for example, my Self-Study Quiz script, where the settings allow you to configure quiz material.
So, I decided I definitely need some sort of pop-up settings dialog. Hereās an early sample:
I welcome any input from coders and graphic designers.
For you coders, hereās the (preliminary) code to create the above dialog box:
var settings = {
'grp_time': {
type: 'group',
label: 'Time Axis',
content: {
'absrel': {
type:'dropdown',
label:'Absolute/Relative',
content:{'abs':'Absolute','rel':'Relative'}
},
'hours24': {
type: 'dropdown',
label:'24 Hour',
content:{'12': '12-hour','24': '24-hour'}
},
'scale_redraw': {
type:'dropdown',
label:'Redraw While Scaling',
content:{'yes':'Yes','no':'No'},
default:'yes'
}
}
},
'grp_detail': {
type: 'group',
label: 'Review Details',
content: {
'show_detail': {
type:'dropdown',
label:'Show Review Details',
content:{'yes':'Yes','no':'No'}
}
}
},
'grp_graph': {
type: 'group',
label: 'Graph',
content: {
'bar_style': {
type:'dropdown',
label:'Bar Breakdown',
content:{'rkv':'Rad+Kan+Voc','srs':'SRS Levels','sum':'Summary Only'},
default:'rkv'
},
'special_bars': {
type:'dropdown',
label:'Special Bars',
content:{'none':'None','curr':'Current Level','burn':'Burn Items','both':'Both'},
default:'both'
},
'markers': {
type:'dropdown',
label:'<i>Current Level</i> Markers',
content:{'none':'None','rk':'Rad+Kan','rkv':'Rad+Kan+Voc'},
default:'rkv'
},
'graph_height':{
type:'integer',
label:'Graph Height (in pixels)',
default:100
},
'max_days':{
type:'integer',
label:'Slider Range Max (days)',
default:7
}
}
},
};
wkof.Settings.install('wk_timeln', 'Ultimate Timeline', settings);
Iām also planning to support something like a tab bar to allow multiple pages of grouped settings.
Any thoughts so far?
Look at you, putting my settings popup to shame 
Thanks for the inspiration 
Iāve added tab support to the Settings module:
Iāve also made it more responsive to resizing:
Feedback, anyone? I definitely want this to be something other script writers will want to use.
The json structure for tabs/pages is:
var settings = {
'pg_graph': {
type: 'page',
label: 'Graph',
content: {
...
}
},
'pg_detail': {
type: 'page',
label: 'Detail View',
content: {
...
}
}
};
Added color-picking to Settings module:

It uses the browserās built-in colorpicker. (Chrome shown below):

var settings = {
'clr_burn': {
type:'color',
label:'Burned Items Bar/Arrow',
default:'#000000'
}
}
[Note: iOS Safari doesnāt support HTML5 input[type=ācolorā]. If thereās anyone actually using userscripts on iOS Safari, Iāll consider adding [Spectrum Colorpicker] as a polyfill. Itās the same colorpicker used in Chrome, Firefox, and Safari devtools.]
Does ios safari even have a way of installing userscripts?
I donāt see ios safari in that list, just safari (which usually means desktop)
oh ok, thought there is only one safari 
How are you storing the settings? I was not a fan of my different item in jstorage for each setting, wanting to convert to json object, so each script only had one stored object, but never did. Looks like thatās where youāre probably headed with the structure youāre showing off.
Also, safeguards so that two scripts donāt end up with the same named setting?
Judging by what youāve done so far though, Iām sure youāve taken these into account already.
@DaisukeJigen,
Scripts that use the framework will register a unique name (e.g. āultimate_timelineā), which is used to keep various things isolated (events, settings, DOM ids, etc).
There will be one top-level object (wkof.settings), with a sub-object for each script (wkof.settings.ultimate_timeline) containing all settings for that script:
wkof.settings.ultimate_timeline = {
absrel: 'abs',
hours24: '12',
scale_redraw: 'yes',
...
}
By using one top-level object, users can choose to install a āsyncā script that can sync settings for all scripts to a 3rd-party location without having to understand the structure and content of the settings object.
For now, Iām storing the settings object in localStorage, but I may move it to indexeddb. It shouldnāt matter as long as people use the framework to load/save(/sync). There will be functions for each, easily typed from the console.
Scripts can also request to be notified under various circumstances:
Any thoughts on making an easy way to do validation, or at least dependencies? āx options only available when y, and if not y then set back to defaultā, āonly numeric values acceptedā, etc.
I imagine the fields will be named nicely, ātxtā + itemName, or whatever. So having to write outside the settings object isnāt too bad, as long as we get an indicator when form is fully loaded and populated.
[edited]
See the next post below for comments about validation.
For dependencies, you can use the callback for interactive changes, then manipulate the DOM yourself. Hopefully, the vanilla module will cover 80% of use-cases, and the callbacks will cover the rest.
The dialog elements will be primarily based on the name you pass in to the settings configuration. For example, given the following configuration line:
'scale_redraw': {
type:'dropdown',
label:'Redraw While Scaling',
content:{'yes':'Yes','no':'No'},
default:'yes'
}
Assuming Iāve registered my script as āultimate_timelineā, the element ID will be accessible via something like:
$('#ultimate_timeline_scale_redraw')
Itās long, but it keeps it simple and avoids collisions.
That same datapoint will then show up in the settings object as:
wkof.settings.ultimate_timeline.scale_redraw
Of course, you can shortcut the path:
var s = wkof.settings.ultimate_timeline;
console.log(s.scale_redraw);
Also, regarding validation, Iām currently thinking the following:
All items can add a validation function. Invalid values will be colored with a reddish background.
'graph_height': {
type: 'integer',
label: 'Graph Height (in pixels)',
default: 100,
validate: check_graph_height
},
Iām currently thinking the validation function will be able to return a simple true/false value, or an optional object like this:
return {
result: false,
msg: 'Expecting value between 75 and 200'
};
For integers, I may also support a min/max:
'graph_height': {
type: 'integer',
label: 'Graph Height (in pixels)',
default: 100,
min: 75,
max: 200
},
For text strings, I will probably also support a regex:
'apiv1_key': {
type: 'text',
label: 'APIv1 key',
match: /^[0-9a-f]{32}$/
},
The Settings module is almost done.
on_change property)All settings accept an on_change property:
dialog = new wkof.Settings('timeln', 'Ultimate Timeline', {
//...
'graph_height': {
type: 'number',
label: 'Graph Height (in pixels)',
default: 100,
min: 60,
on_change: graph_height_changed
}
//...
});
function graph_height_changed(value, config) {
// Redraw the graph immediately, even while
// the settings dialog is still open.
}
validate property)All settings accept a validate property:
dialog = new wkof.Settings('timeln', 'Ultimate Timeline', {
//...
'max_days': {
type: 'number',
label: 'Graph Height (in pixels)',
default: 7,
min: 1,
max: 14,
validate: validate_max_days
}
//...
});
function validate_max_days(value, config) {
// Make sure it's an integer
if (value !== Math.round(value))
return 'Must be a whole number of days';
else
return true;
}
min, max, and match)type:"number"
Accepts two optional validation properties, min and max. If the number is not within the specified bounds, an error message is generated automatically:
type:"text"
Accepts an optional validation property, match. This can be either a string pattern to match, or a regex object.
'apiv1_key': {
type: 'text',
label: 'APIv1 key',
match: /^[0-9a-f]{32}$/
}
dialog.on_save)If you only need to respond to events after the dialog box is closed, you can hook into the dialog.on_save property:
dialog.on_save = settings_changed;
function settings_changed() {
// Update based on new settings
var settings = wkof.settings.timeln;
$('#graph').height(settings.graph_height);
// etc...
}
I think both of you have scripts that install links in the WK menu, and Iād like to avoid collisions with what you already have.
Hereās what Iām picturing for the Open Framework:

I made it a separate submenu to avoid crowding out the official WK menu. It is also responsive for small screen sizes.
The code snippet below does the following:
Scripts header if not already presentSettings submenu if not already present//==[ BEGIN UNIVERSAL SECTION ]===============================================
// Although this code is for the Open Framework, any script can include this
// 'Universal Section' to support adding a link to the Wanikani menu in a
// standardized way.
// Install 'Scripts' header in menu, if not present.
function install_scripts_header() {
// Abort if already installed.
if ($('.scripts-header').length !== 0) return;
// Install html.
$('.nav-header:contains("Account")').before(
'<li class="scripts-header nav-header">Scripts</li>'
);
}
// Install 'Settings' menu, if not present.
function install_script_settings_menu() {
// Abort if already installed.
if ($('.scripts-settings').length !== 0) return;
var html =
'<li class="scripts-settings">'+
' <a href="#">Settings</a>'+
' <ul class="scripts-settings-menu dropdown-menu">'+
' </ul>'+
'</li>';
var css =
'html#main .navbar .scripts-settings {position:relative;}'+
'html#main .navbar .scripts-settings.open>.scripts-settings-menu {display:block;position:absolute;top:0px;}'+
'@media (max-width: 979px) {'+
' html#main .navbar .scripts-settings>a {display:none;}'+
' html#main .navbar .dropdown-menu>li:not(.nav-header).scripts-settings {display:block;width:100%;}'+
' html#main .navbar .scripts-settings>.scripts-settings-menu {display:block;padding:0;margin:0;box-shadow:none;}'+
' html#main .navbar .scripts-settings:hover>.scripts-settings-menu {position:relative;top:0px;left:initial;right:initial;}'+
' html#main .navbar .dropdown-menu>li:not(.nav-header).scripts-settings>.scripts-settings-submenu>li {width:auto;padding:0 1em;}'+
'}';
// Install css and html.
$('head').append('<style>'+css+'</style>');
$('.scripts-header').after(html);
// Click to open Settings menu.
$('.scripts-settings>a').on('click',function(e){
$('.scripts-settings').toggleClass('open');
var menu = $('.scripts-settings-menu');
// If we opened the menu, listen for off-menu clicks.
if ($('.scripts-settings').hasClass('open')) {
$('body').on('click.scripts-settings',function(e){
$('body').off('click.scripts-settings');
$('.scripts-settings').removeClass('open');
return true;
})
}
return false;
});
}
// Inserts script link into script settings menu.
function insert_script(script_id, title, callback, classes) {
// Abort if the script already exists
var link_id = script_id+'_settings_link';
if ($('#'+link_id).length !== 0) return;
// Append the script, and sort the menu.
var menu = $('.scripts-settings-menu');
var class_html = (typeof classes === 'string' ? ' class="'+classes+'"': '');
menu.append('<li id="'+link_id+'" name="'+script_id+'"'+class_html+'><a href="#">'+title+'</a></li>');
var children = menu.children().sort(function(a,b){
return a.innerText.localeCompare(b.innerText);
});
menu.append(children);
// Add a callback for when the link is clicked.
$('#'+link_id).on('click', function(e){
$('body').off('click.scripts-settings');
$('.dropdown.account').removeClass('open');
$('.scripts-settings').removeClass('open');
callback(e);
return false;
});
}
//==[ END UNIVERSAL SECTION ]=================================================
// Dummy callback to handle link clicks.
function open_settings(e){
// TODO: Open settings dialog, or go to settings URL.
console.log(e.target.parentNode.attributes.name.value+': Opening settings...');
}
// Insert some sample scripts.
install_scripts_header();
install_script_settings_menu();
insert_script('timeln' , 'Ultimate Timeline' , open_settings, 'wkof');
insert_script('burnmgr' , 'Burn Manager' , open_settings, 'wkof');
insert_script('dpp' , 'Dashboard Progress Plus' , open_settings, 'wkof');
insert_script('example1', 'Example Non-framework Script', open_settings);
Looks like currently we donāt step on each others toes, exactly. The menu will just have two scripts items, until I switch over to open framework of course. It will look a little dumb, but should work fine.
Same here. I donāt think itās all that important in the App Store case. Itās appropriate enough for it to be under a Scripts heading. Thanks for checking, @rfindley.