NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Mastodon Customizable Interface
// @version 0.3
// @updateURL https://openuserjs.org/meta/bl00m/Mastodon_Customizable_Interface.meta.js
// @description Allow to move and resize columns on a grid.
// @author bl00m
// @license MIT
// @match https://example.com/
// @match https://example.com/web/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// ==/UserScript==
class GridControl {
constructor(element, rules) {
this.element = element;
this.initialRules = rules;
this.template = [];
var templateStrLines = rules.templateArea.split('|');
for (var i=0; i<templateStrLines.length; i++) {
var templateLines = [];
var areas = templateStrLines[i].split(' ');
for (var j=0; j<areas.length; j++) {
templateLines.push(areas[j]);
}
this.template.push(templateLines);
}
this.cards = {};
for (var i=0; i<this.element.children.length; i++) {
this.cards[rules.areas[i]] = new CardControl(this.element.children[i], this, rules.areas[i]);
}
this.element.style.display = 'grid';
this.element.style.gridTemplateRows = rules.templateRows;
this.element.style.gridTemplateColumns = rules.templateColumns;
this.applyTemplate();
}
get rules() {
var rules = this.initialRules;
var templateArea = '';
for (var i=0; i<this.template.length; i++) {
for(var j=0; j<this.template[0].length; j++) {
templateArea += this.template[i][j];
if (j < this.template[0].length-1) templateArea += ' ';
}
if (i < this.template.length-1) templateArea += '|';
}
rules.templateArea = templateArea;
return rules;
}
applyTemplate() {
var res = '';
for (var i=0; i<this.template.length; i++) {
var line = '';
for (var j=0; j<this.template[i].length; j++) {
line += this.template[i][j];
if (this.template[i][j] == '_') line += i + '_' + j;
if (j < this.template[i].length-1) line += ' ';
}
res += '"' + line + '"';
if (i < this.template.length-1) res += ' ';
}
this.element.style.gridTemplateAreas = res;
}
getAreaPosition(area) {
var pos = null;
var i=0;
var found = false;
while (i<this.template.length && !found) {
var j=0;
while (j<this.template[i].length && !found) {
if (this.template[i][j] == area) {
found = true;
pos = {row: i, col: j, height: 0, width: 0};
}
j++;
}
i++;
}
if (found) {
var i = 1;
while (pos.col+i<this.template[0].length && this.template[pos.row][pos.col+i] == area) {
i++;
}
pos.width = i;
var i = 1;
while (pos.row+i<this.template.length && this.template[pos.row+i][pos.col] == area) {
i++;
}
pos.height = i;
}
return pos;
}
}
class CardControl {
constructor(element, grid, area) {
this.element = element;
this.grid = grid;
this.element.style.gridArea = area;
this.position = this.grid.getAreaPosition(area);
}
get area() {
return this.element.style.gridArea[this.element.style.gridArea.length-1];
}
spanRight() {
if (this.position.col + this.position.width-1 == this.grid.template.length[0]-1) return false;
for (var i = this.position.row; i < this.position.row + this.position.height; i++) {
if (this.grid.template[i][this.position.col+this.position.width] != '_') {
return false;
}
}
for (var i = this.position.row; i < this.position.row + this.position.height; i++) {
this.grid.template[i][this.position.col+this.position.width] = this.area;
}
this.position.width++;
this.grid.applyTemplate();
return true;
}
spanBottom() {
if (this.position.row + this.position.height-1 == grid.length-1) return false;
for (var i = this.position.col; i < this.position.col + this.position.width; i++) {
if (this.grid.template[this.position.row + this.position.height][i] != '_') return false;
}
for (var i = this.position.col; i < this.position.col + this.position.width; i++) {
this.grid.template[this.position.row + this.position.height][i] = this.area;
}
this.position.height++;
this.grid.applyTemplate();
return true;
}
unSpanRight() {
if (this.position.width == 1) return false;
for (var i = this.position.row; i < this.position.row + this.position.height; i++) {
this.grid.template[i][this.position.col+this.position.width-1] = '_';
}
this.position.width--;
this.grid.applyTemplate();
return true;
}
unSpanBottom() {
if (this.position.height == 1) return false;
for (var i = this.position.col; i < this.position.col + this.position.width; i++) {
this.grid.template[this.position.row + this.position.height-1][i] = '_';
}
this.position.height--;
this.grid.applyTemplate();
return true;
}
switchWith(otherCard) {
for (var i=this.position.row; i < this.position.row + this.position.height; i++) {
for (var j=this.position.col; j < this.position.col + this.position.width; j++) {
this.grid.template[i][j] = otherCard.area;
}
}
for (var i=otherCard.position.row; i < otherCard.position.row + otherCard.position.height; i++) {
for (var j=otherCard.position.col; j < otherCard.position.col + otherCard.position.width; j++) {
this.grid.template[i][j] = this.area;
}
}
var position = this.position;
this.position = otherCard.position;
otherCard.position = position;
this.grid.applyTemplate();
}
moveToEmptyCell(row, col) {
this.grid.template[row][col] = this.area;
for (var i=this.position.row; i < this.position.row + this.position.height; i++) {
for (var j=this.position.col; j < this.position.col + this.position.width; j++) {
this.grid.template[i][j] = '_';
}
}
this.position = {row: row, col: col, height: 1, width: 1};
this.grid.applyTemplate();
}
}
// Mastodon
function setupDropZones() {
var cpt = 0;
for (var i = 0; i < grid.template.length; i++) {
for (var j = 0; j < grid.template[i].length; j++) {
if (grid.template[i][j] == '_') {
var dropzone = document.createElement('div');
dropzone.className = "dropzone";
dropzone.id = i + '-' + j;
dropzone.addEventListener('dragover', function (event) {
event.preventDefault();
});
dropzone.addEventListener('drop', function (event) {
event.preventDefault();
var pos = this.id.split('-');
var card = grid.cards[event.dataTransfer.getData('text')];
card.moveToEmptyCell(parseInt(pos[0]), parseInt(pos[1]));
});
grid.element.appendChild(dropzone);
}
}
}
}
function removeDropZones() {
var dropzones = document.getElementsByClassName('dropzone');
while (dropzones.length > 0) {
grid.element.removeChild(dropzones[0]);
}
}
function setupCard(element) {
element.style.position = 'relative';
element.addEventListener('dragover', function (event) {
event.preventDefault();
});
element.addEventListener('drop', function (event) {
event.preventDefault();
var thisCard = grid.cards[this.style.gridArea[this.style.gridArea.length-1]];
var otherCard = grid.cards[event.dataTransfer.getData('text')];
thisCard.switchWith(otherCard);
});
var dragbtn = document.createElement('div');
dragbtn.className = 'dragbtn';
dragbtn.setAttribute('draggable', 'true');
element.appendChild(dragbtn);
dragbtn.addEventListener('dragstart', function (event) {
document.getElementsByClassName('upload-area')[0].style.display = 'none';
let gridArea = event.target.parentElement.style.gridArea;
event.dataTransfer.setData("Text", gridArea[gridArea.length-1]);
setupDropZones();
});
dragbtn.addEventListener('dragend', function () {
document.getElementsByClassName('upload-area')[0].style.display = null;
removeDropZones();
});
var resizebtn = document.createElement('div');
resizebtn.className = 'resizebtn';
element.appendChild(resizebtn);
resizebtn.addEventListener('mousedown', function (event) {
event.preventDefault();
let gridArea = this.parentNode.style.gridArea;
var card = grid.cards[gridArea[gridArea.length-1]];
var cellWidth = card.element.offsetWidth / card.position.width;
var cellHeight = card.element.offsetHeight / card.position.height;
var initialX = event.pageX;
var initialY = event.pageY;
document.onmousemove = function (event) {
document.body.style.cursor = "nw-resize";
if (event.pageX > initialX + cellWidth / 2 && card.spanRight()) {
initialX += cellWidth;
} else if (event.pageX < initialX - cellWidth / 2 && card.unSpanRight()) {
initialX -= cellWidth;
}
if (event.pageY > initialY + cellHeight / 2 && card.spanBottom()) {
initialY += cellHeight;
} else if (event.pageY < initialY - cellHeight / 2 && card.unSpanBottom()) {
initialY -= cellHeight;
}
};
document.onmouseup = function () {
document.body.style.cursor = "auto";
document.onmousemove = null;
document.onmouseup = null;
};
});
}
function setupGrid(columnsContainer, rules) {
var grid = new GridControl(columnsContainer, rules);
new MutationObserver(function () {
var columns = document.getElementsByClassName('columns-area')[0].children;
let i = 0;
for (let area of rules.areas) {
if (columns[i].style.gridArea == "") {
columns[i].style.gridArea = area;
grid.cards[area].element = columns[i];
setupCard(columns[i]);
}
i++;
}
}).observe(columnsContainer, { childList: true });
return grid;
}
var rules;
if (GM_getValue('savedRules')) {
rules = JSON.parse(GM_getValue('savedRules'));
} else {
rules = {
'areas': ['a', 'b', 'c', 'd'],
'templateArea': 'a b c d _|a b c d _',
'templateRows': 'repeat(2, 1fr)',
'templateColumns': 'repeat(5, 19.5vw)'
};
}
var grid;
var columnsContainer = document.getElementsByClassName('columns-area')[0];
if (columnsContainer) {
grid = setupGrid(columnsContainer, rules);
} else {
// In case the script is loaded before the components (As for GreaseMonkey)
new MutationObserver(function () {
columnsContainer = document.getElementsByClassName('columns-area')[0];
grid = setupGrid(columnsContainer, rules);
this.disconnect();
}).observe(document.body.firstElementChild, { childList: true });
}
window.onbeforeunload = function () {
GM_setValue('savedRules', JSON.stringify(grid.rules));
};
GM_addStyle('.columns-area {grid-gap: .5vw;padding: .5rem .25vw;height: 100vh; max-width: 100vw;} .column, .drawer {margin: 0 !important;padding: 0 !important;width: 100%;} .resizebtn {position: absolute;bottom: 0;right: 0;height: 20px;width: 20px;background-color: #ccc;border: none;border-radius: 50% 0 0 0;opacity: 0.2;cursor: nw-resize;} .resizebtn:hover {opacity: 1;} .dragbtn {position: absolute;bottom: 5px;left: 5px;height: 35px;width: 35px;background-color: #ccc;border: none;border-radius: 50%;opacity: 0.2;cursor: move;} .dragbtn:hover {opacity: 1;} .dropzone {border: 2px dashed #ccc;border-radius: .4rem;} .column:nth-child(3) .column-icon {top: 0}');