Initial commit

This commit is contained in:
2023-08-29 19:55:48 +02:00
commit 7c2eec4446
473 changed files with 40947 additions and 0 deletions

View File

@@ -0,0 +1,583 @@
var ActionEditor: ActionEditor = {
elements: {
up: {
container: document.querySelector('.event.up'),
selector: document.querySelector('.event.up').querySelector('.actionselector'),
actions: document.querySelector('.event.up').querySelector('.actions')
},
down: {
container: document.querySelector('.event.down'),
selector: document.querySelector('.event.down').querySelector('.actionselector'),
actions: document.querySelector('.event.down').querySelector('.actions')
},
latch: {
container: document.querySelector('.event.toggle'),
selector: document.querySelector('.event.toggle').querySelector('.tlatch').querySelector('.actionselector'),
actions: document.querySelector('.event.toggle').querySelector('.tlatch').querySelector('.actions')
},
unlatch: {
container: document.querySelector('.event.toggle'),
selector: document
.querySelector('.event.toggle')
.querySelector('.tunlatch')
.querySelector('.actionselector'),
actions: document.querySelector('.event.toggle').querySelector('.tunlatch').querySelector('.actions')
}
},
openEditors: { up: {}, down: {}, latch: {}, unlatch: {} },
listeners() {
if (ActionSelector) {
ActionSelector.register(ActionEditor.elements.up.selector, (integrationID: string, actionID: string) =>
handleNewAction(integrationID, actionID, 'up')
);
ActionSelector.register(ActionEditor.elements.down.selector, (integrationID: string, actionID: string) =>
handleNewAction(integrationID, actionID, 'down')
);
ActionSelector.register(ActionEditor.elements.latch.selector, (integrationID: string, actionID: string) =>
handleNewAction(integrationID, actionID, 'latch')
);
ActionSelector.register(ActionEditor.elements.unlatch.selector, (integrationID: string, actionID: string) =>
handleNewAction(integrationID, actionID, 'unlatch')
);
function handleNewAction(
integrationID: string,
actionID: string,
type: 'up' | 'down' | 'latch' | 'unlatch'
) {
socket.emit(
'actioneditor',
'create',
PageHandler.currentPageID,
Editor.currentX,
Editor.currentY,
type,
integrationID,
actionID,
(actionInstance: Page_Key_Action) => {
ActionEditor.openAction(actionInstance, type);
}
);
}
} else setTimeout(ActionEditor.listeners, 100);
},
open(actions: Page_Key_ActionTypes, x: string, y: string, key: Page_Key, pageID: string) {
for (var type in actions) {
ActionEditor.elements[type].actions.innerHTML = '';
for (var actionInstanceID in actions[type]) {
ActionEditor.openAction(actions[type][actionInstanceID], type);
}
}
},
openAction(actionInstance: Page_Key_Action, type: string) {
var actionInstanceID = actionInstance.actionInstanceID;
var container = <HTMLDivElement>ce('div', 'actioncontainer', { actionInstanceID });
var header = ce('div', 'header');
header.appendChild(
ce('div', 'integration', null, ActionSelector.maps.integrationNames[actionInstance.integrationID])
);
header.appendChild(ce('div', 'action', null, ActionSelector.maps.actionNames[actionInstance.actionID]));
var buttons = ce('div', 'buttons');
var actionLogs = ce(
'div',
[
'btn',
'logs'
],
null,
'Logs'
);
actionLogs.onclick = () => UndeckedNotification('Not implented yet', 'error');
var actionRemove = ce(
'div',
[
'btn',
'remove'
],
null,
'Remove'
);
actionRemove.onclick = () => {
if (container.parentElement) container.parentElement.removeChild(container);
if (
ActionEditor.openEditors[type] != undefined &&
ActionEditor.openEditors[type][actionInstanceID] != undefined
) {
ActionEditor.openEditors[type][actionInstanceID].destroy();
}
socket.emit(
'actioneditor',
'remove',
PageHandler.currentPageID,
Editor.currentX,
Editor.currentY,
actionInstanceID,
type
);
Editor.registerChange();
};
buttons.appendChild(actionLogs);
buttons.appendChild(actionRemove);
header.appendChild(buttons);
container.appendChild(header);
var fields = <HTMLDivElement>ce('div', 'fields');
container.appendChild(fields);
ActionEditor.elements[type].actions.appendChild(container);
ActionEditor.openEditors[type][actionInstanceID] = new Action(
{
integrationID: actionInstance.integrationID,
actionID: actionInstance.actionID,
actionType: <any>type,
keyX: Editor.currentX,
keyY: Editor.currentY,
actionInstanceID,
pageID: PageHandler.currentPageID
},
fields
);
},
close() {
for (var type in ActionEditor.openEditors) {
for (var actionInstanceID in ActionEditor.openEditors[type]) {
ActionEditor.openEditors[type][actionInstanceID].destroy();
}
}
ActionEditor.elements.up.actions.innerHTML = '';
ActionEditor.elements.down.actions.innerHTML = '';
}
};
ActionEditor.listeners();
var Action = class Action {
settings: Action_Settings;
container: HTMLDivElement;
actionEditorID: string;
lastFields: EditorAPI_Field[];
constructor(settings: Action_Settings, container: HTMLDivElement) {
this.settings = settings;
this.container = container;
socket.emit(
'actioneditor',
'start',
this.settings,
(error: string, actionEditorID: string, ready: () => void) => {
if (error) return UndeckedNotification(error, 'error', 5000);
this.actionEditorID = actionEditorID;
this.listeners();
this.emit('ready');
}
);
}
emit(query: string, ...args: any[]) {
socket.emit('actioneditor', 'instance', this.actionEditorID, query, ...args);
}
listeners() {
socket.on(`AE_${this.actionEditorID}`, (query: string, ...args: any[]) => {
switch (query) {
case 'fields':
var fields: EditorAPI_Field[] = args[0];
this.render(fields);
break;
}
});
}
destroy() {
if (this.container && this.container.parentElement) this.container.parentElement.removeChild(this.container);
this.emit('close');
}
render(fields: EditorAPI_Field[]) {
var valueNameMap = {};
var updateMulti = (selected: string[], input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => {
var text: string =
selected != undefined && selected.length > 0
? selected
.map((value) => {
return valueNameMap[value];
})
.join(', ')
: 'None';
input.innerHTML = '';
input.appendChild(ce('option', 'mutli', { value: selected.join(',') }, text));
};
var getMultiValues = (dropdowninner: HTMLDivElement): string[] => {
var values: string[] = [];
dropdowninner.querySelectorAll('.option').forEach((option: HTMLDivElement) => {
var checkbox: HTMLInputElement = option.querySelector('input');
var optionID = option.getAttribute('optionID');
if (checkbox.checked) values.push(optionID);
});
return values;
};
fields.forEach((field) => {
var fieldcontainer: HTMLDivElement = this.container.querySelector(`.field_${field.id}`);
if (fieldcontainer == null) {
fieldcontainer = <HTMLDivElement>ce('div', [
'field',
`field_${field.id}`
]);
this.container.appendChild(fieldcontainer);
}
var label: HTMLDivElement = fieldcontainer.querySelector('.fieldlabel');
if (label == null) {
label = <HTMLDivElement>ce('div', 'fieldlabel', null, `${field.name}${field.required ? ' *' : ''}`);
fieldcontainer.appendChild(label);
} else label.innerText = `${field.name}${field.required ? ' *' : ''}`;
var input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement = fieldcontainer.querySelector(
'.input'
);
if (input == null) {
switch (field.type) {
case 'number':
case 'text':
case 'color':
input = <HTMLInputElement>ce('input', 'input', { type: field.type, value: field.value });
fieldcontainer.appendChild(input);
break;
case 'select':
case 'connection':
if (field.multi != undefined && field.multi == true) {
var multiSelect = ce('div', [
'multiselect'
]);
input = <HTMLInputElement>ce('select', [
'input',
'multiinput'
]);
input.onmouseup = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
dropdown.style.display = 'block';
input.classList.add('open');
var handleWindowClick = (e: MouseEvent) => {
if (e.target) {
var target = <HTMLElement>e.target;
if (
!target.classList.contains('msdp') &&
!target.classList.contains('multiinput')
) {
window.removeEventListener('click', handleWindowClick);
dropdown.style.display = 'none';
input.classList.remove('open');
}
}
};
window.addEventListener('click', handleWindowClick);
};
input.onmousedown = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
};
multiSelect.appendChild(input);
var dropdown = ce('div', [
'dropdown',
'msdp'
]);
var dropdowninner = <HTMLDivElement>ce('div', [
'inner',
'msdp'
]);
dropdown.appendChild(dropdowninner);
multiSelect.appendChild(dropdown);
if (field.values != undefined && field.values.length > 0)
for (let i = 0; i < field.values.length; i++) {
((value) => {
valueNameMap[value.id] = field.values[i].text;
var item = ce(
'div',
[
'option',
'msdp'
],
{ optionID: value.id }
);
var checkbox = <HTMLInputElement>ce('input', 'msdp', {
type: 'checkbox'
});
checkbox.checked = field.value != undefined && field.value.includes(value.id);
item.onclick = () => {
checkbox.checked = !checkbox.checked;
updateMulti(getMultiValues(dropdowninner), input);
this.emit('fields', this.export());
};
item.appendChild(checkbox);
item.appendChild(
ce(
'div',
[
'text',
'msdp'
],
null,
value.text
)
);
dropdowninner.appendChild(item);
})(field.values[i]);
}
updateMulti(field.value, input);
fieldcontainer.appendChild(multiSelect);
} else {
input = <HTMLInputElement>ce('select', 'input');
if (field.values != undefined && field.values.length > 0)
for (let i = 0; i < field.values.length; i++) {
var option = ce(
'option',
null,
{ value: field.values[i].id },
field.values[i].text
);
if (field.value == field.values[i].id) option.setAttribute('selected', '');
input.appendChild(option);
}
fieldcontainer.appendChild(input);
}
break;
}
if (input) input.oninput = () => this.emit('fields', this.export());
} else {
input.value = field.value;
if (field.type == 'select' || field.type == 'connection') {
var parsedOptionValues = [];
if (field.multi != undefined && field.multi == true) {
var dropdown_inner = <HTMLDivElement>input.parentElement.querySelector('.inner');
if (field.values != undefined && field.values.length > 0)
for (let i = 0; i < field.values.length; i++) {
var selectValue = field.values[i];
var existingOption: HTMLDivElement = dropdown_inner.querySelector(
`.option[optionid="${selectValue.id}"]`
);
parsedOptionValues.push(selectValue.id);
if (existingOption) {
var text: HTMLDivElement = existingOption.querySelector('.text');
text.innerText = selectValue.text;
} else {
((value) => {
valueNameMap[value.id] = field.values[i].text;
var item = ce(
'div',
[
'option',
'msdp'
],
{ optionID: value.id }
);
var checkbox = <HTMLInputElement>ce('input', 'msdp', {
type: 'checkbox'
});
checkbox.checked = field.value != undefined && field.value.includes(value.id);
item.onclick = () => {
checkbox.checked = !checkbox.checked;
updateMulti(getMultiValues(dropdown_inner), input);
this.emit('fields', this.export());
};
item.appendChild(checkbox);
item.appendChild(
ce(
'div',
[
'text',
'msdp'
],
null,
value.text
)
);
dropdown_inner.appendChild(item);
})(selectValue);
}
}
else dropdown_inner.innerHTML = '';
dropdown_inner.querySelectorAll('.option').forEach((option: HTMLOptionElement) => {
var optionValue = option.getAttribute('optionID');
var checkbox: HTMLInputElement = option.querySelector('input');
if (!parsedOptionValues.includes(optionValue)) option.parentElement.removeChild(option);
else checkbox.checked = field.value.includes(optionValue);
});
} else {
if (field.values != undefined && field.values.length > 0)
for (let i = 0; i < field.values.length; i++) {
var selectValue = field.values[i];
var existing: HTMLOptionElement = input.querySelector(
`option[value="${selectValue.id}"]`
);
if (existing) existing.innerText = selectValue.text;
else {
existing = <HTMLOptionElement>ce(
'option',
null,
{ value: selectValue.id },
selectValue.text
);
input.appendChild(existing);
}
parsedOptionValues.push(selectValue.id);
}
else input.innerHTML = '';
input.querySelectorAll('option').forEach((option: HTMLOptionElement) => {
var optionValue = option.getAttribute('value');
option.removeAttribute('selected');
if (!parsedOptionValues.includes(optionValue)) option.parentElement.removeChild(option);
else if (field.value == option) option.setAttribute('selected', '');
});
}
// if (field.value != undefined && input.value.length > 0) input.value = field.value;
}
}
});
this.lastFields = fields;
}
export(): EditorAPI_Field[] {
var copyOfLast = JSON.parse(JSON.stringify(this.lastFields));
for (let i = 0; i < copyOfLast.length; i++) {
var field = copyOfLast[i];
var fieldcontainer: HTMLDivElement = this.container.querySelector(`.field_${field.id}`);
if (fieldcontainer) {
var input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement = fieldcontainer.querySelector(
'.input'
);
if (input) {
if (!input.classList.contains('multiinput')) copyOfLast[i].value = input.value;
else copyOfLast[i].value = input.value.split(',');
}
}
}
return copyOfLast;
}
};
interface Action_Settings {
integrationID: string;
actionID: string;
actionInstanceID: string;
pageID: string;
actionType: 'up' | 'down';
keyX: number | string;
keyY: number | string;
}
interface ActionEditor {
elements: {
up: {
container: HTMLDivElement;
selector: HTMLInputElement;
actions: HTMLDivElement;
};
down: {
container: HTMLDivElement;
selector: HTMLInputElement;
actions: HTMLDivElement;
};
latch: {
container: HTMLDivElement;
selector: HTMLInputElement;
actions: HTMLDivElement;
};
unlatch: {
container: HTMLDivElement;
selector: HTMLInputElement;
actions: HTMLDivElement;
};
};
openEditors: {
up: { [actionInstanceID: string]: typeof Action };
down: { [actionInstanceID: string]: typeof Action };
latch: { [actionInstanceID: string]: typeof Action };
unlatch: { [actionInstanceID: string]: typeof Action };
};
listeners: () => void;
open: (actions: Page_Key_ActionTypes, x: string, y: string, key: Page_Key, pageID: string) => void;
openAction: (actionInstance: Page_Key_Action, type: string) => void;
close: () => void;
}
interface Page_Key_ActionTypes {
up: Page_Key_Actions;
down: Page_Key_Actions;
}
interface Page_Key_Actions {
[actionInstanceID: string]: Page_Key_Action;
}
interface Page_Key_Action {
integrationID: string;
actionID: string;
actionInstanceID: string;
properties: { [property: string]: any };
logs: { timestamp: number; type: 'error' | 'info' | 'warning'; text: string }[];
}
interface EditorAPI_Field {
id: string;
name: string;
type: 'text' | 'number' | 'select' | 'connection' | 'color';
value: any;
values?: { id: string; text: string }[];
required?: boolean;
multi?: boolean;
}

View File

@@ -0,0 +1,110 @@
var ActionSelector: ActionSelector = {
elements: {
actiondialog: document.querySelector('.actiondialog')
},
fadeTimeout: null,
maps: {
integrationNames: {},
actionNames: {}
},
init() {
ActionSelector.elements.actiondialog.querySelectorAll('.item').forEach((item: HTMLDivElement) => {
var integrationID = item.getAttribute('integrationID');
var actionID = item.getAttribute('actionID');
var integrationName = (<HTMLDivElement>item.querySelector('.integration')).innerText.trim();
var actionName = (<HTMLDivElement>item.querySelector('.action')).innerText.trim();
if (ActionSelector.maps.integrationNames[integrationID] == undefined)
ActionSelector.maps.integrationNames[integrationID] = integrationName;
if (ActionSelector.maps.actionNames[actionID] == undefined)
ActionSelector.maps.actionNames[actionID] = actionName;
});
},
register(input: HTMLInputElement, callback: (integrationID: string, actionID: string) => void) {
input.onfocus = () => ActionSelector.show(input, callback);
input.onblur = () => setTimeout(ActionSelector.hide, 200);
input.oninput = () => ActionSelector.search(input.value);
},
show(input: HTMLInputElement, callback: (integrationID: string, actionID: string) => void) {
clearTimeout(ActionSelector.fadeTimeout);
ActionSelector.search('');
var boundingbox = input.getBoundingClientRect();
ActionSelector.elements.actiondialog.style.top = `${boundingbox.top - 31}px`;
ActionSelector.elements.actiondialog.style.left = `${boundingbox.left - 10}px`;
ActionSelector.elements.actiondialog.style.width = `${boundingbox.width - 4}px`;
ActionSelector.elements.actiondialog.style.maxHeight = `${Math.min(
window.innerHeight - boundingbox.top,
200
)}px`;
ActionSelector.elements.actiondialog.style.display = 'flex';
ActionSelector.elements.actiondialog.style.transitionDuration = '.3s';
ActionSelector.elements.actiondialog.style.opacity = '1';
ActionSelector.elements.actiondialog.style.pointerEvents = 'auto';
ActionSelector.elements.actiondialog.querySelectorAll('.item').forEach((item: HTMLDivElement) => {
item.onclick = () => {
var integrationID = item.getAttribute('integrationID');
var actionID = item.getAttribute('actionID');
callback(integrationID, actionID);
input.value = '';
};
});
},
hide() {
ActionSelector.elements.actiondialog.style.transitionDuration = '.3s';
ActionSelector.elements.actiondialog.style.opacity = '0';
ActionSelector.elements.actiondialog.style.pointerEvents = 'none';
clearTimeout(ActionSelector.fadeTimeout);
ActionSelector.fadeTimeout = setTimeout(() => {
ActionSelector.elements.actiondialog.style.display = 'none';
}, 300);
},
search(query: string) {
ActionSelector.elements.actiondialog.querySelectorAll('.item').forEach((item: HTMLDivElement) => {
var integration: HTMLDivElement = item.querySelector('.integration');
var action: HTMLDivElement = item.querySelector('.action');
var interactionQuery = integration.innerText.toLowerCase();
var actionQuery = action.innerText.toLowerCase();
query = query.toLowerCase();
if (interactionQuery.includes(query) || actionQuery.includes(query) || query.length == 0)
item.classList.remove('hidden');
else item.classList.add('hidden');
});
}
};
ActionSelector.init();
interface ActionSelector {
elements: {
actiondialog: HTMLDivElement;
};
fadeTimeout: any;
maps: {
integrationNames: { [integrationID: string]: string };
actionNames: { [actionID: string]: string };
};
init: () => void;
register: (input: HTMLInputElement, callback: (integrationID: string, actionID: string) => void) => void;
show: (input: HTMLInputElement, callback: (integrationID: string, actionID: string) => void) => void;
hide: () => void;
search: (query: string) => void;
}

View File

@@ -0,0 +1,72 @@
var UndeckedClipboard = new class UndeckedClipboard {
constructor() {}
hasKeyInClipboard() {
return localStorage.getItem('clipboard') != undefined && localStorage.getItem('clipboard').length > 0;
}
copyKey(originKeyX: number, originKeyY: number) {
localStorage.setItem('clipboard', `key_copy_${PageHandler.currentPageID}.${originKeyX}.${originKeyY}`);
UndeckedNotification('Key has been copied to clipboard');
}
copyGhostKey(originKeyX: number, originKeyY: number) {
localStorage.setItem('clipboard', `key_ghost_${PageHandler.currentPageID}.${originKeyX}.${originKeyY}`);
UndeckedNotification('Key has been copied to clipboard as a ghost');
}
cutKey(originKeyX: number, originKeyY: number) {
//TODO: Implement something in the front end to show that the item is being cut right now
localStorage.setItem('clipboard', `key_cut_${PageHandler.currentPageID}.${originKeyX}.${originKeyY}`);
UndeckedNotification('Key has been cut to clipboard');
}
pasteKey(destinationKeyX: number, destinationKeyY: number) {
if (this.hasKeyInClipboard()) {
var clipboard = this.decodeClipboard();
if (clipboard.elementType == 'key') {
socket.emit(
'page',
'operation',
clipboard.operationType,
clipboard.id,
clipboard.x,
clipboard.y,
PageHandler.currentPageID,
destinationKeyX,
destinationKeyY
);
if (clipboard.operationType == 'cut') localStorage.setItem('clipboard', '');
}
}
}
decodeClipboard(): {
elementType: 'key';
operationType: 'cut' | 'copy' | 'ghost';
id: string;
x?: number;
y?: number;
} {
var clipboard = {
elementType: null,
operationType: null,
id: null,
x: null,
y: null
};
if (this.hasKeyInClipboard()) {
var raw = localStorage.getItem('clipboard').split('_');
clipboard.elementType = raw[0];
clipboard.operationType = raw[1];
var args = raw[2].split('.');
clipboard.id = args[0];
clipboard.x = args[1];
clipboard.y = args[2];
return clipboard;
} else return clipboard;
}
}();

View File

@@ -0,0 +1,75 @@
declare var ce: (
type: string,
classList?: string | string[],
attributes?: { [key: string]: string },
innerText?: string,
innerHTML?: string
) => HTMLElement;
declare var UndeckedNotification: (message: string, type?: 'info' | 'error', time?: number) => void;
declare var io: any;
var responseToken = Math.random().toString(16).substr(2, 8);
var socket = io('/');
socket.on('connect', () => {
console.log('Connected to server');
socket.emit('init', 'home');
});
var fontSizeRatio: number = null;
var renderQuality: number = null;
socket.on('quality', (quality: number) => {
document.querySelectorAll('canvas.ready').forEach((canvas: HTMLCanvasElement) => {
canvas.width = quality;
canvas.height = quality;
var context = canvas.getContext('2d');
context.textBaseline = 'middle';
context.textAlign = 'center';
});
renderQuality = quality;
fontSizeRatio = quality / 100;
});
socket.on('pagelist', (pagelist: PageListItem[]) => {
(function render() {
if (fontSizeRatio != null) PageList.render(pagelist);
else setTimeout(render, 100);
})();
});
socket.on('connectedlist', (connected: ConnectedList[]) => Connections.renderConnected(connected));
socket.on('page', (query: string, ...args: any[]) => {
switch (query) {
case 'updatename':
var pageID: string = args[0];
var newName: string = args[1];
PageList.updateName(pageID, newName);
break;
case 'updatekey':
var pageID: string = args[0];
var x: string = args[1];
var y: string = args[2];
var key: Page_Key = args[3];
var returnResponseToken: string = args[4];
if (PageHandler.currentPageID == pageID) {
if (PageHandler.currentPage.keys[y] != undefined && PageHandler.currentPage.keys[y][x] != undefined)
PageHandler.currentPage.keys[y][x] = key;
if (responseToken != returnResponseToken) {
KeyHandler.render(x, y, key);
if (Editor.currentKey != undefined && Editor.currentKey.id == key.id) Editor.open(x, y, key);
}
}
break;
}
});

View File

@@ -0,0 +1,206 @@
var Connections: Connections = {
elements: {
connectionbrowser: document.querySelector('.connectionbrowser'),
table: document.querySelector('.connectedtable'),
dialog: {
container: document.querySelector('.connectiondialog'),
dialog: document.querySelector('.connectiondialog').querySelector('.dialog'),
fields: document.querySelector('.connectiondialog').querySelector('.fields'),
message: document.querySelector('.connectiondialog').querySelector('.message'),
link: document.querySelector('.connectiondialog').querySelector('.link'),
cancel: document.querySelector('.connectiondialog').querySelector('.cn'),
connect: document.querySelector('.connectiondialog').querySelector('.co')
}
},
init() {
Connections.elements.connectionbrowser.querySelectorAll('.available').forEach((item: HTMLDivElement) => {
var integrationID = item.getAttribute('integrationID');
var connectionType = item.getAttribute('connectionType');
var button: HTMLDivElement = item.querySelector('.button');
button.onclick = () => Connections.requestNewDevice(integrationID, connectionType);
});
},
requestNewDevice(integrationID: string, connectionType: string) {
socket.emit(
'connections',
'request',
integrationID,
connectionType,
(connectionRequestData: ConnectionRequestData) => {
if (connectionRequestData.fields && connectionRequestData.fields.length > 0)
Connections.openDialog(integrationID, connectionType, connectionRequestData);
else UndeckedNotification('Unable to add a new device of this type.', 'error', 5000);
}
);
},
openDialog(integrationID, connectionType, connectionRequestData: ConnectionRequestData) {
Connections.elements.dialog.fields.innerHTML = '';
var nameField: Connection_Field = {
id: '_internal_name',
name: 'Connection Name',
type: 'text'
};
connectionRequestData.fields = [
nameField,
...connectionRequestData.fields
];
if (connectionRequestData.message != undefined) {
Connections.elements.dialog.message.style.display = 'block';
Connections.elements.dialog.message.innerText = connectionRequestData.message;
} else Connections.elements.dialog.message.style.display = 'none';
if (connectionRequestData.link != undefined) {
Connections.elements.dialog.link.style.display = 'inline-block';
Connections.elements.dialog.link.innerText = connectionRequestData.link.title;
Connections.elements.dialog.link.href = connectionRequestData.link.address;
} else Connections.elements.dialog.link.style.display = 'none';
connectionRequestData.fields.forEach((field) => {
var fieldcontainer = ce('div', [
'field',
`field_${field.id}`
]);
Connections.elements.dialog.fields.appendChild(fieldcontainer);
var label = ce('div', 'fieldlabel', null, `${field.name}`);
fieldcontainer.appendChild(label);
var input: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement = null;
switch (field.type) {
case 'number':
case 'text':
input = <HTMLInputElement>ce('input', 'input', { type: field.type, fieldID: field.id });
if (field.value != undefined) input.value = field.value;
fieldcontainer.appendChild(input);
break;
case 'select':
input = <HTMLInputElement>ce('select', 'input', { fieldID: field.id });
if (field.values != undefined && field.values.length > 0)
for (let i = 0; i < field.values.length; i++) {
var option = ce('option', null, { value: field.values[i].id }, field.values[i].text);
if (field.value != undefined && field.values[i].id == field.value)
option.setAttribute('selected', '');
input.appendChild(option);
}
fieldcontainer.appendChild(input);
break;
}
});
Connections.elements.dialog.cancel.onclick = Connections.closeDialog;
Connections.elements.dialog.connect.onclick = () => {
var properties = {};
var inputs: NodeListOf<HTMLInputElement> = Connections.elements.dialog.fields.querySelectorAll('.input');
for (let i = 0; i < inputs.length; i++)
if (inputs[i].hasAttribute('fieldID')) properties[inputs[i].getAttribute('fieldID')] = inputs[i].value;
Connections.elements.dialog.dialog.style.display = 'none';
socket.emit(
'connections',
'create',
integrationID,
connectionType,
properties,
(succeed: boolean, errormessage?: string) => {
if (succeed == true) {
Connections.closeDialog();
} else {
Connections.elements.dialog.dialog.style.display = 'block';
UndeckedNotification(
errormessage != undefined ? errormessage : 'Unable to validate connection',
'error',
5000
);
}
}
);
};
Connections.elements.dialog.dialog.style.display = 'block';
Connections.elements.dialog.container.style.display = 'flex';
},
closeDialog() {
Connections.elements.dialog.container.style.display = 'none';
},
renderConnected(connectList: ConnectedList[]) {
var table = Connections.elements.table;
for (let i = 0; i < connectList.length; i++) {
var connected = connectList[i];
if (table.querySelector(`tr[connectionid="${connected.connectionID}"]`) == null) {
var row = ce('tr', null, { connectionID: connected.connectionID });
var status = ce('td', 'status');
var statuscontainer = ce('div', 'statuscontainer');
statuscontainer.appendChild(
ce('div', [
'value',
'online'
])
);
status.appendChild(statuscontainer);
row.appendChild(status);
row.appendChild(ce('td', 'name', null, connected.name));
row.appendChild(ce('td', 'integration', null, connected.integrationName));
row.appendChild(ce('td', 'type', null, connected.connectionType));
table.appendChild(row);
}
}
}
};
Connections.init();
interface Connections {
elements: {
connectionbrowser: HTMLDivElement;
table: HTMLTableElement;
dialog: {
container: HTMLDivElement;
dialog: HTMLDivElement;
message: HTMLDivElement;
link: HTMLAnchorElement;
fields: HTMLDivElement;
cancel: HTMLDivElement;
connect: HTMLDivElement;
};
};
init: () => void;
requestNewDevice: (integrationID: string, connectionType: string) => void;
openDialog: (integrationID, connectionID, connectionRequestData: ConnectionRequestData) => void;
closeDialog: () => void;
renderConnected: (connectList: ConnectedList[]) => void;
}
interface Connection_Field {
id: string;
name: string;
type: 'text' | 'number' | 'select';
values?: { id: string; text: string }[];
value?: string;
}
interface ConnectedList {
connectionID: string;
integrationName: string;
connectionType: string;
name: string;
online: boolean;
}
interface ConnectionRequestData {
fields: Connection_Field[];
message?: string;
link?: { address: string; title: string };
}

View File

@@ -0,0 +1,137 @@
var ContextMenu = new class ContextMenu {
elements: {
menu: HTMLDivElement;
items: {
copy: HTMLDivElement;
paste: HTMLDivElement;
cut: HTMLDivElement;
ghost: HTMLDivElement;
osc: HTMLDivElement;
http: HTMLDivElement;
};
};
open: boolean;
contextHolder: HTMLElement;
constructor() {
this.elements = {
menu: document.querySelector('.contextmenu'),
items: {
copy: document.querySelector('.contextmenu').querySelector('.copy'),
paste: document.querySelector('.contextmenu').querySelector('.paste'),
cut: document.querySelector('.contextmenu').querySelector('.cut'),
ghost: document.querySelector('.contextmenu').querySelector('.ghost'),
osc: document.querySelector('.contextmenu').querySelector('.osc'),
http: document.querySelector('.contextmenu').querySelector('.http')
}
};
this.open = false;
this.contextHolder = null;
window.addEventListener('contextmenu', (ev: MouseEvent) => this.handle(this.elements.menu, ev));
window.addEventListener('mousedown', (ev: MouseEvent) => {
if (this.open == true)
if (ev.target) {
var target = ev.target as HTMLElement;
if (
target.classList.contains('contextmenu') ||
(target.parentElement != undefined && target.parentElement.classList.contains('contextmenu'))
) {
} else {
this.close();
}
} else this.close();
});
}
setItems(items: DropdownItemTypes[]) {
for (var type in this.elements.items) {
if (items.includes(<DropdownItemTypes>type)) this.elements.items[type].style.display = 'block';
else this.elements.items[type].style.display = 'none';
}
}
close() {
this.elements.menu.style.display = 'none';
if (this.contextHolder != null) {
this.contextHolder.classList.remove('context');
this.contextHolder = null;
}
this.open = false;
}
handle(menu: HTMLDivElement, ev: MouseEvent) {
if (ev.target != undefined) {
var target = ev.target as HTMLElement;
var types = {
key: (element: HTMLElement) => {
if (element.hasAttribute('x') && element.hasAttribute('y')) {
var keyX = element.getAttribute('x');
var keyY = element.getAttribute('y');
var items: DropdownItemTypes[] = [
'osc',
'http'
];
if (PageHandler.currentPage.keys[keyY][keyX].state.type != 'empty') items.push('copy', 'cut');
if (PageHandler.currentPage.keys[keyY][keyX].state.type == 'custom') items.push('ghost');
if (UndeckedClipboard.hasKeyInClipboard()) items.push('paste');
this.setItems(items);
this.elements.items.copy.onclick = () => {
UndeckedClipboard.copyKey(parseInt(keyX), parseInt(keyY));
this.close();
};
this.elements.items.cut.onclick = () => {
UndeckedClipboard.cutKey(parseInt(keyX), parseInt(keyY));
this.close();
};
this.elements.items.paste.onclick = () => {
UndeckedClipboard.pasteKey(parseInt(keyX), parseInt(keyY));
this.close();
};
this.elements.items.ghost.onclick = () => {
UndeckedClipboard.copyGhostKey(parseInt(keyX), parseInt(keyY));
this.close();
};
return items.length > 0;
}
}
};
for (var type in types)
if (target.classList.contains(type)) {
ev.preventDefault();
var valid = types[type](target);
if (valid) {
this.contextHolder = target;
target.classList.add('context');
setTimeout(() => {
this.open = true;
}, 100);
menu.style.left = `${ev.pageX}px`;
menu.style.top = `${ev.pageY - 50}px`;
menu.style.display = 'block';
}
return;
}
}
}
}();
type DropdownItemTypes = 'copy' | 'paste' | 'cut' | 'ghost' | 'osc' | 'http';

View File

@@ -0,0 +1,468 @@
var Editor: Editor = {
elements: {
container: document.querySelector('.edit'),
appearence: {
text: {
content: document.querySelector('.ap_text_value'),
size: document.querySelector('.ap_text_size'),
color: document.querySelector('.ap_text_color'),
offsetX: document.querySelector('.ap_text_offsetx').querySelector('input[type="range"]'),
offsetY: document.querySelector('.ap_text_offsety').querySelector('input[type="range"]')
},
background: {
color: document.querySelector('.ap_background_color')
},
image: {
size: document.querySelector('.ap_image_size').querySelector('input[type="range"]'),
offsetX: document.querySelector('.ap_image_offsetx').querySelector('input[type="range"]'),
offsetY: document.querySelector('.ap_image_offsety').querySelector('input[type="range"]'),
rotation: document.querySelector('.ap_image_rotation').querySelector('input[type="range"]')
}
},
imageui: {
imagetypes: document.querySelector('.imagetype'),
imageinfopanels: document.querySelector('.imagemenu').querySelector('.info'),
imagepanels: document.querySelector('.row.image').querySelector('.panels'),
advanced: document.querySelector('.row.image').querySelector('.advanced')
},
buttonui: {
buttontypes: document.querySelector('.buttontype'),
visual: document.querySelector('.vis'),
actioninner: document.querySelector('.actioninner'),
containerTitles: document.querySelector('.editcontainer').querySelectorAll('.containertitle')
},
checks: {
toggle: {
container: document.querySelector('.checks').querySelector('.check.toggle'),
checkbox: document.querySelector('.checks').querySelector('.check.toggle').querySelector('input')
},
confirm: {
container: document.querySelector('.checks').querySelector('.check.confirm'),
checkbox: document.querySelector('.checks').querySelector('.check.confirm').querySelector('input')
}
}
},
currentX: null,
currentY: null,
currentKey: null,
currentImageType: null,
currentButtonType: null,
isToggle: null,
isConfirm: null,
isOpen: false,
registerChange() {
if (Editor.currentKey != null) {
var editorExport = Editor.export();
KeyHandler.render(Editor.currentX, Editor.currentY, editorExport);
socket.emit(
'page',
'setkey',
PageHandler.currentPageID,
Editor.currentX,
Editor.currentY,
editorExport,
responseToken
);
}
},
listeners() {
for (var inputCategory in Editor.elements.appearence) {
for (var inputName in Editor.elements.appearence[inputCategory]) {
var inputElement: HTMLInputElement = Editor.elements.appearence[inputCategory][inputName];
inputElement.oninput = Editor.registerChange;
if (inputElement.type == 'range')
((range: HTMLInputElement, number: HTMLInputElement) => {
range.addEventListener('input', () => {
number.value = range.value;
});
number.addEventListener('input', () => {
range.value = number.value;
Editor.registerChange();
});
})(inputElement, inputElement.parentElement.querySelector('input[type="number"]'));
}
}
Editor.elements.imageui.imagetypes.querySelectorAll('.selectoritem').forEach((item: HTMLDivElement) => {
item.onclick = () => {
var panelType: 'none' | 'icon' | 'upload' = <any>item.getAttribute('panel');
Editor.selectImageTab(panelType);
};
});
Editor.elements.buttonui.buttontypes.querySelectorAll('.buttonitem').forEach((item: HTMLDivElement) => {
item.onclick = () => {
var buttonType: 'empty' | 'custom' | 'pageup' | 'pagedown' = <any>item.getAttribute('type');
Editor.selectButtonType(buttonType);
};
});
Editor.elements.checks.toggle.container.onclick = () =>
Editor.setActionOptions(!Editor.elements.checks.toggle.checkbox.checked, Editor.isConfirm);
Editor.elements.checks.confirm.container.onclick = () =>
Editor.setActionOptions(Editor.isToggle, !Editor.elements.checks.confirm.checkbox.checked);
},
export(): Page_Key {
if (Editor.isOpen) {
return {
id: Editor.currentKey.id,
actions: Editor.currentKey.actions,
state: {
type: Editor.currentButtonType,
confirm: Editor.isConfirm,
toggle: Editor.isToggle
},
appearence: {
text: Editor.getElementCategoryValues('text'),
background: Editor.getElementCategoryValues('background'),
image: Editor.getElementCategoryValues('image')
}
};
}
return null;
},
open(x: string, y: string, key: Page_Key) {
if (Editor.isOpen) Editor.close();
Editor.currentKey = key;
Editor.currentX = x;
Editor.currentY = y;
Editor.isOpen = true;
document.querySelectorAll('.actionselector').forEach((selector: HTMLInputElement) => {
selector.value = '';
});
// --------- TEXT ---------
{
var textElems = Editor.elements.appearence.text;
var textData: Page_Key_Text = Editor.getAppearenceCategory('text', key);
textElems.content.value = textData.value != undefined ? textData.value : '';
textElems.size.value = textData.size != undefined ? String(textData.size) : '10';
textElems.color.value = textData.color != undefined ? textData.color : '#000000';
textElems.offsetX.value = textData.offsetX != undefined ? String(textData.offsetX) : '0';
var offsetXNumber: HTMLInputElement = textElems.offsetX.parentElement.querySelector('input[type="number"]');
offsetXNumber.value = textData.offsetX != undefined ? String(textData.offsetX) : '0';
textElems.offsetY.value = textData.offsetY != undefined ? String(textData.offsetY) : '0';
var offsetYNumber: HTMLInputElement = textElems.offsetY.parentElement.querySelector('input[type="number"]');
offsetYNumber.value = textData.offsetY != undefined ? String(textData.offsetY) : '0';
}
// --------- BACKGROUND ---------
{
var backgroundElems = Editor.elements.appearence.background;
var backgroundData: Page_Key_Background = Editor.getAppearenceCategory('background', key);
backgroundElems.color.value = backgroundData.color != undefined ? backgroundData.color : '#000000';
}
// --------- IMAGE ---------
{
var imageElems = Editor.elements.appearence.image;
var imageData: Page_Key_Image = Editor.getAppearenceCategory('image', key);
imageElems.size.value = imageData.size != undefined ? String(imageData.size) : '100';
var sizeNumber: HTMLInputElement = imageElems.size.parentElement.querySelector('input[type="number"]');
sizeNumber.value = imageData.size != undefined ? String(imageData.size) : '100';
imageElems.rotation.value = imageData.rotation != undefined ? String(imageData.rotation) : '0';
var rotationNumber: HTMLInputElement = imageElems.rotation.parentElement.querySelector(
'input[type="number"]'
);
rotationNumber.value = imageData.rotation != undefined ? String(imageData.rotation) : '0';
imageElems.offsetX.value = imageData.offsetX != undefined ? String(imageData.offsetX) : '0';
var offsetXNumber: HTMLInputElement = imageElems.offsetX.parentElement.querySelector(
'input[type="number"]'
);
offsetXNumber.value = imageData.offsetX != undefined ? String(imageData.offsetX) : '0';
imageElems.offsetY.value = imageData.offsetY != undefined ? String(imageData.offsetY) : '0';
var offsetYNumber: HTMLInputElement = imageElems.offsetY.parentElement.querySelector(
'input[type="number"]'
);
offsetYNumber.value = imageData.offsetY != undefined ? String(imageData.offsetY) : '0';
}
if (imageData.address != undefined) {
Editor.selectImageTab('upload', true);
} else if (imageData.iconid != undefined) {
Editor.selectImageTab('icon', true);
Icons.select(imageData.iconid);
}
Editor.selectButtonType(key.state.type, true);
Editor.setActionOptions(key.state.toggle, key.state.confirm, true);
var actions = key.actions != undefined ? key.actions : { up: {}, down: {} };
ActionEditor.open(actions, x, y, key, PageHandler.currentPageID);
Editor.elements.container.classList.remove('disabled');
},
close() {
Editor.isOpen = false;
Editor.currentKey = null;
Editor.currentX = null;
Editor.currentY = null;
document.querySelectorAll('.actionselector').forEach((selector: HTMLInputElement) => {
selector.value = '';
});
var text = Editor.elements.appearence.text;
text.content.value = 'Button';
text.size.value = '20';
text.color.value = '#ffffff';
var background = Editor.elements.appearence.background;
background.color.value = '#000000';
Editor.elements.container.classList.add('disabled');
Editor.selectImageTab('none', true);
ActionEditor.close();
Icons.deselect();
},
selectImageTab(panelType: 'none' | 'icon' | 'upload', ignoreUpdate = false) {
Editor.elements.imageui.imagetypes.querySelectorAll('.selectoritem').forEach((selectoritem: HTMLDivElement) => {
if (panelType == selectoritem.getAttribute('panel')) selectoritem.classList.add('selected');
else selectoritem.classList.remove('selected');
});
Editor.elements.imageui.imagepanels.querySelectorAll('.panel').forEach((panel: HTMLDivElement) => {
if (panelType == panel.getAttribute('panel')) panel.classList.add('selected');
else panel.classList.remove('selected');
});
Editor.elements.imageui.imageinfopanels.querySelectorAll('.infopanel').forEach((infopanel: HTMLDivElement) => {
if (panelType == infopanel.getAttribute('panel')) infopanel.classList.add('selected');
else infopanel.classList.remove('selected');
});
Editor.currentImageType = panelType != 'none' ? panelType : null;
if (panelType == 'none') Editor.elements.imageui.advanced.style.display = 'none';
else Editor.elements.imageui.advanced.style.display = 'flex';
if (panelType == 'icon') Icons.loadOnScreen();
if (ignoreUpdate == false) Editor.registerChange();
},
selectButtonType(buttonType: KeyTypes, ignoreUpdate = false) {
if (buttonType == 'ghost') buttonType = 'custom';
Editor.elements.buttonui.buttontypes.querySelectorAll('.buttonitem').forEach((selectoritem: HTMLDivElement) => {
if (buttonType == selectoritem.getAttribute('type')) selectoritem.classList.add('selected');
else selectoritem.classList.remove('selected');
});
Editor.currentButtonType = buttonType;
switch (buttonType) {
case 'empty':
case 'pageup':
case 'pagedown':
case 'currentpage':
Editor.elements.buttonui.visual.style.display = 'none';
Editor.elements.buttonui.actioninner.style.display = 'none';
Editor.elements.buttonui.containerTitles.forEach((title) => (title.style.display = 'none'));
break;
case 'custom':
Editor.elements.buttonui.visual.style.display = 'block';
Editor.elements.buttonui.actioninner.style.display = 'block';
Editor.elements.buttonui.containerTitles.forEach((title) => (title.style.display = 'flex'));
break;
}
if (ignoreUpdate == false) Editor.registerChange();
},
setActionOptions(toggle: boolean, confirm: boolean, ignoreUpdate = false) {
if (toggle) {
ActionEditor.elements.up.container.style.display = 'none';
ActionEditor.elements.latch.container.style.display = 'block';
} else {
ActionEditor.elements.up.container.style.display = 'block';
ActionEditor.elements.latch.container.style.display = 'none';
}
if (confirm) {
ActionEditor.elements.down.container.style.display = 'none';
} else {
ActionEditor.elements.down.container.style.display = 'block';
}
Editor.isToggle = toggle;
Editor.isConfirm = confirm;
Editor.elements.checks.toggle.checkbox.checked = toggle;
Editor.elements.checks.confirm.checkbox.checked = confirm;
if (ignoreUpdate == false) Editor.registerChange();
},
getAppearenceCategory(category: Editor_Categories, key: Page_Key = Editor.currentKey) {
switch (category) {
case 'text':
var textData: Page_Key_Text =
key.appearence != undefined && key.appearence.text != undefined
? key.appearence.text
: { value: '', size: 10, color: '#000000', offsetX: 0, offsetY: 0 };
return textData;
case 'background':
var backgroundData: Page_Key_Background =
key.appearence != undefined && key.appearence.background != undefined
? key.appearence.background
: { color: '#000000' };
return backgroundData;
case 'image':
var imageData: Page_Key_Image =
key.appearence != undefined && key.appearence.image != undefined
? key.appearence.image
: { offsetX: 0, offsetY: 0, size: 100, rotation: 0 };
return imageData;
}
},
getElementCategoryValues(category: Editor_Categories) {
switch (category) {
case 'text':
var textData: Page_Key_Text = {
value: Editor.currentButtonType != 'empty' ? Editor.elements.appearence.text.content.value : '',
color:
Editor.currentButtonType != 'empty' ? Editor.elements.appearence.text.color.value : '#ffffff',
size:
Editor.currentButtonType != 'empty' ? parseInt(Editor.elements.appearence.text.size.value) : 20,
offsetX:
Editor.currentButtonType != 'empty'
? parseFloat(Editor.elements.appearence.text.offsetX.value)
: 0,
offsetY:
Editor.currentButtonType != 'empty'
? parseFloat(Editor.elements.appearence.text.offsetY.value)
: 0
};
return textData;
case 'background':
var backgroundData: Page_Key_Background = {
color:
Editor.currentButtonType != 'empty'
? Editor.elements.appearence.background.color.value
: '#000000'
};
return backgroundData;
case 'image':
var imageData: Page_Key_Image = {
size:
Editor.currentButtonType != 'empty'
? parseFloat(Editor.elements.appearence.image.size.value)
: 100,
rotation:
Editor.currentButtonType != 'empty'
? parseFloat(Editor.elements.appearence.image.rotation.value)
: 0,
offsetX:
Editor.currentButtonType != 'empty'
? parseFloat(Editor.elements.appearence.image.offsetX.value)
: 0,
offsetY:
Editor.currentButtonType != 'empty'
? parseFloat(Editor.elements.appearence.image.offsetY.value)
: 0
};
if (Editor.currentImageType != null && Editor.currentButtonType != 'empty') {
switch (Editor.currentImageType) {
case 'icon':
imageData.iconid = Icons.currentSelected;
imageData.iconstyle = 'white';
break;
}
}
return imageData;
}
}
};
Editor.listeners();
interface Editor {
elements: {
container: HTMLDivElement;
appearence: {
text: {
content: HTMLInputElement;
size: HTMLSelectElement;
color: HTMLInputElement;
offsetX: HTMLInputElement;
offsetY: HTMLInputElement;
};
background: {
color: HTMLInputElement;
};
image: {
size: HTMLInputElement;
offsetX: HTMLInputElement;
offsetY: HTMLInputElement;
rotation: HTMLInputElement;
};
};
imageui: {
imagetypes: HTMLDivElement;
imageinfopanels: HTMLDivElement;
imagepanels: HTMLDivElement;
advanced: HTMLDivElement;
};
buttonui: {
buttontypes: HTMLDivElement;
visual: HTMLDivElement;
actioninner: HTMLDivElement;
containerTitles: NodeListOf<HTMLDivElement>;
};
checks: {
toggle: {
container: HTMLDivElement;
checkbox: HTMLInputElement;
};
confirm: {
container: HTMLDivElement;
checkbox: HTMLInputElement;
};
};
};
isOpen: boolean;
currentKey: Page_Key;
currentX: string;
currentY: string;
currentImageType: 'icon' | 'upload';
currentButtonType: KeyTypes;
isToggle: boolean;
isConfirm: boolean;
listeners: () => void;
export: () => Page_Key;
open: (x: string, y: string, key: Page_Key) => void;
close: () => void;
registerChange: () => void;
selectImageTab: (panelType: string, ignoreUpdate?: boolean) => void;
selectButtonType: (buttonType: KeyTypes, ignoreUpdate?: boolean) => void;
setActionOptions: (toggle: boolean, confirm: boolean, ignoreUpdate?: boolean) => void;
getAppearenceCategory: (category: Editor_Categories, key?: Page_Key) => any;
getElementCategoryValues: (category: Editor_Categories) => any;
}
type Editor_Categories = 'text' | 'background' | 'image';

View File

@@ -0,0 +1,85 @@
var Icons: Icons = {
elements: {
container: document.querySelector('.panel.icons').querySelector('.list')
},
resizeTimeout: null,
currentSelected: null,
loadOnScreen() {
Icons.elements.container.querySelectorAll('.icon').forEach((icon: HTMLDivElement) => {
var position = icon.getBoundingClientRect();
// checking for partial visibility
if (position.top < window.innerHeight && position.bottom >= 0) {
if (icon.hasAttribute('notloaded')) {
icon.removeAttribute('notloaded');
var iconID = icon.getAttribute('iconID');
var whiteImg: HTMLImageElement = icon.querySelector('img.white');
var blackImg: HTMLImageElement = icon.querySelector('img.black');
whiteImg.src = `/stc/materialicons/white/${iconID}.png`;
blackImg.src = `/stc/materialicons/black/${iconID}.png`;
whiteImg.style.opacity = '1';
blackImg.style.opacity = '0';
}
}
icon.onclick = () => Icons.select(icon.getAttribute('iconID'));
});
},
listeners() {
var counter = 0;
Icons.elements.container.onscroll = () => {
counter++;
if (counter > 5) {
Icons.loadOnScreen();
counter = 0;
}
clearTimeout(Icons.resizeTimeout);
Icons.resizeTimeout = setTimeout(() => {
Icons.loadOnScreen();
}, 100);
};
// Icons.loadOnScreen();
},
select(iconID: string, ignoreUpdate: boolean = false) {
var icons = Icons.elements.container.querySelectorAll('.icon');
for (let i = 0; i < icons.length; i++) {
var testIconID = icons[i].getAttribute('iconID');
if (iconID != testIconID) icons[i].classList.remove('selected');
else icons[i].classList.add('selected');
}
Icons.currentSelected = iconID;
if (ignoreUpdate == false) Editor.registerChange();
},
deselect() {
var icons = Icons.elements.container.querySelectorAll('.icon');
for (let i = 0; i < icons.length; i++) icons[i].classList.remove('selected');
Icons.currentSelected = null;
}
};
Icons.listeners();
interface Icons {
elements: {
container: HTMLDivElement;
};
resizeTimeout: any;
currentSelected: string;
loadOnScreen: () => void;
listeners: () => void;
select: (iconID: string, ignoreUpdate?: boolean) => void;
deselect: () => void;
}

View File

@@ -0,0 +1,57 @@
window.addEventListener('keydown', (ev: KeyboardEvent) => {
if (document.activeElement && document.activeElement.nodeName != 'INPUT') {
if ((isWin() && ev.ctrlKey) || (isMac() && ev.metaKey)) {
switch (ev.key) {
case 'c':
ev.preventDefault();
if (PageHandler.currentPageID && KeyHandler.selected.length == 1) {
UndeckedClipboard.copyKey(
parseInt(KeyHandler.selected[0].split(',')[0]),
parseInt(KeyHandler.selected[0].split(',')[1])
);
}
break;
case 'x':
ev.preventDefault();
if (PageHandler.currentPageID && KeyHandler.selected.length == 1) {
UndeckedClipboard.cutKey(
parseInt(KeyHandler.selected[0].split(',')[0]),
parseInt(KeyHandler.selected[0].split(',')[1])
);
}
break;
case 'v':
ev.preventDefault();
if (PageHandler.currentPageID && KeyHandler.selected.length == 1) {
UndeckedClipboard.pasteKey(
parseInt(KeyHandler.selected[0].split(',')[0]),
parseInt(KeyHandler.selected[0].split(',')[1])
);
}
break;
case 'g':
ev.preventDefault();
if (PageHandler.currentPageID && KeyHandler.selected.length == 1) {
UndeckedClipboard.copyGhostKey(
parseInt(KeyHandler.selected[0].split(',')[0]),
parseInt(KeyHandler.selected[0].split(',')[1])
);
}
break;
}
} else {
switch (ev.key) {
case 'Delete':
var x = parseInt(KeyHandler.selected[0].split(',')[0]);
var y = parseInt(KeyHandler.selected[0].split(',')[1]);
socket.emit('page', 'operation', 'delete', PageHandler.currentPageID, x, y);
Editor.close();
KeyHandler.select(String(x), String(y));
break;
}
}
}
});

View File

@@ -0,0 +1,372 @@
var KeyHandler: KeyHandler = {
elements: {
keys: {}
},
selected: [],
imagecache: {},
ghostImage: null,
init() {
KeyHandler.ghostImage = new Image();
KeyHandler.ghostImage.src = '/stc/icon/ghost.png';
KeyHandler.ghostImage.onload = () => {
for (var y = 0; y < 4; y++) {
if (KeyHandler.elements.keys[y] == undefined) KeyHandler.elements.keys[y] = {};
for (var x = 0; x < 8; x++) {
var keyCheck: HTMLDivElement = document.querySelector(`.key[y="${y}"][x="${x}"]`);
if (keyCheck) {
((keyX: number, keyY: number, key: HTMLDivElement) => {
var canvas = key.querySelector('canvas');
var context = canvas.getContext('2d');
KeyHandler.elements.keys[keyY][keyX] = { key, canvas, context };
context.textBaseline = 'middle';
context.textAlign = 'center';
key.onclick = () => {
if (altDown == false) KeyHandler.select(String(keyX), String(keyY));
else {
socket.emit('page', 'executekey', PageHandler.currentPageID, keyX, keyY);
}
};
})(x, y, keyCheck);
}
}
}
};
},
clear() {
for (var y in KeyHandler.elements.keys) {
for (var x in KeyHandler.elements.keys[y]) {
var key = KeyHandler.elements.keys[y][x];
key.context.clearRect(0, 0, 100, 100);
key.key.classList.remove('selected');
}
}
KeyHandler.selected = [];
Editor.close();
},
render(x: string, y: string, data: Page_Key) {
if (KeyHandler.elements.keys[String(y)]) {
if (KeyHandler.elements.keys[String(y)][String(x)]) {
var context = KeyHandler.elements.keys[String(y)][String(x)].context;
context.clearRect(0, 0, renderQuality, renderQuality);
if (data.state.type == 'custom') {
var appearence = data.appearence;
render(appearence);
} else if (data.state.type == 'ghost') {
var appearence = data.appearence;
if (appearence == undefined) appearence = {};
appearence.system = { ghost: true };
render(appearence);
} else if (data.state.type == 'pageup') {
render({
text: { value: 'Up', color: '#ffffff', size: 18, offsetX: 0, offsetY: 25 },
background: { color: '#4676b7' },
image: {
size: 100,
rotation: 0,
offsetX: 0,
offsetY: -15,
iconid: 'keyboard_arrow_up',
iconstyle: 'white'
},
system: {
border: {
color: '#253e5e',
thickness: 8
}
}
});
} else if (data.state.type == 'pagedown') {
render({
text: { value: 'Down', color: '#ffffff', size: 18, offsetX: 0, offsetY: -25 },
background: { color: '#4676b7' },
image: {
size: 100,
rotation: 0,
offsetX: 0,
offsetY: 15,
iconid: 'keyboard_arrow_down',
iconstyle: 'white'
},
system: {
border: {
color: '#253e5e',
thickness: 8
}
}
});
} else if (data.state.type == 'currentpage') {
render({
text: {
value: `Page\\n\\n${PageHandler.currentIndex + 1}`,
color: '#ffffff',
size: 22,
offsetX: 0,
offsetY: 0
},
background: { color: '#4676b7' },
system: {
border: {
color: '#253e5e',
thickness: 8
}
}
});
}
} else console.error(`Invalid x '${x}'`);
} else console.error(`Invalid y '${y}'`);
function render(appearence: Page_Key_Appearence) {
if (appearence.background != undefined) {
context.fillStyle = appearence.background.color;
context.fillRect(0, 0, renderQuality, renderQuality);
context.fill();
}
if (appearence.text != undefined) {
context.fillStyle = appearence.text.color;
context.font = `800 ${appearence.text.size * fontSizeRatio}px "Montserrat"`;
var text = appearence.text.value;
var lineHeight = appearence.text.size * fontSizeRatio;
var centerX = renderQuality / 2 + appearence.text.offsetX / 100 * (renderQuality * 2);
var centerY = renderQuality / 2 + appearence.text.offsetY / 100 * renderQuality;
var canvasYCounter = centerY;
var words = text.replace(/\\n/g, ' \\n ').split(' ');
var line = '';
var totalLineHeight = 0;
for (var n = 0; n < words.length; n++) {
if (words[n].length == 0) continue;
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if (words[n] != '\\n')
if (testWidth > renderQuality && n > 0) {
line = words[n] + ' ';
totalLineHeight += lineHeight;
} else {
line = testLine;
}
else {
totalLineHeight += lineHeight;
line = '';
}
}
line = '';
canvasYCounter = canvasYCounter - totalLineHeight / 2;
var firstSkip = false;
for (var n = 0; n < words.length; n++) {
if (words[n].length == 0) continue;
var testLine = line + words[n] + ' ';
var metrics = context.measureText(testLine);
var testWidth = metrics.width;
if (words[n] != '\\n')
if (testWidth > renderQuality && n > 0) {
context.fillText(line, centerX, canvasYCounter);
line = words[n] + ' ';
canvasYCounter += lineHeight;
} else {
line = testLine;
}
else {
context.fillText(line, centerX, canvasYCounter);
line = '';
canvasYCounter += firstSkip ? lineHeight * 2 : lineHeight;
if (firstSkip) firstSkip = false;
}
}
context.fillText(line, centerX, canvasYCounter);
}
if (appearence.image != undefined) {
var imageAddress =
appearence.image.address != undefined
? appearence.image.address
: appearence.image.iconid != undefined
? `/stc/materialicons/white/${appearence.image.iconid}.png`
: null;
var imageSize =
appearence.image.size != undefined ? appearence.image.size / 100 * renderQuality : renderQuality;
if (imageAddress) {
var centerX = renderQuality / 2 + appearence.image.offsetX / 100 * renderQuality;
var centerY = renderQuality / 2 + appearence.image.offsetY / 100 * renderQuality;
if (KeyHandler.imagecache[imageAddress] != undefined)
renderImage(KeyHandler.imagecache[imageAddress]);
else {
KeyHandler.imagecache[imageAddress] = new Image();
KeyHandler.imagecache[imageAddress].src = imageAddress;
KeyHandler.imagecache[imageAddress].onload = () =>
renderImage(KeyHandler.imagecache[imageAddress]);
}
function renderImage(image) {
context.save();
context.translate(centerX, centerY);
context.rotate(appearence.image.rotation * Math.PI / 180);
context.drawImage(
image,
imageSize / 2 - imageSize,
imageSize / 2 - imageSize,
imageSize,
imageSize
);
context.restore();
}
}
}
if (appearence.system != undefined) {
if (appearence.system.border != undefined) {
var relativeThickness = appearence.system.border.thickness / 100 * renderQuality;
context.fillStyle = appearence.system.border.color;
context.fillRect(0, 0, renderQuality, relativeThickness);
context.rect(0, renderQuality - relativeThickness, renderQuality, relativeThickness);
context.rect(0, 0, relativeThickness, renderQuality);
context.rect(renderQuality - relativeThickness, 0, relativeThickness, renderQuality);
context.fill();
}
if (appearence.system.ghost == true) {
var size = 50 / 100 * renderQuality;
context.save();
context.globalAlpha = 0.7;
context.translate(renderQuality / 2, renderQuality / 2);
context.drawImage(KeyHandler.ghostImage, size / 2 - size, size / 2 - size, size, size);
context.restore();
}
}
}
},
select(x: string, y: string, multi: boolean = false) {
if (PageHandler.currentPage != null) {
if (PageHandler.currentPage.keys[y] != undefined && PageHandler.currentPage.keys[y][x] != undefined) {
// var keyConfig = PageHandler.currentPage.keys[y][x];
var query = `${x},${y}`;
if (multi) {
if (!KeyHandler.selected.includes(query)) KeyHandler.selected.push(query);
} else
KeyHandler.selected = [
query
];
document.querySelectorAll('.key').forEach((key: HTMLDivElement) => {
var checkX = key.getAttribute('x');
var checkY = key.getAttribute('y');
var checkQuery = `${checkX},${checkY}`;
if (KeyHandler.selected.includes(checkQuery)) key.classList.add('selected');
else key.classList.remove('selected');
});
if (KeyHandler.selected.length > 1) Editor.close();
else {
socket.emit('page', 'getkey', PageHandler.currentPageID, x, y, (key: Page_Key) => {
Editor.open(x, y, key);
});
}
}
}
}
};
KeyHandler.init();
var altDown = false;
window.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key == 'alt' || e.altKey == true) altDown = true;
});
window.addEventListener('keyup', (e: KeyboardEvent) => {
if (e.key == 'alt' || e.altKey == false) altDown = false;
});
interface KeyHandler {
elements: {
keys: {
[y: string]: {
[x: string]: {
key: HTMLDivElement;
canvas: HTMLCanvasElement;
context: CanvasRenderingContext2D;
};
};
};
};
selected: string[];
imagecache: { [iconID: string]: HTMLImageElement };
ghostImage: any;
init: () => void;
clear: () => void;
render: (x: string, y: string, data: Page_Key) => void;
select: (x: string, y: string, multi?: boolean) => void;
}
interface Page_Key {
id: string;
state: {
type: KeyTypes;
toggle: boolean;
confirm: boolean;
};
actions: Page_Key_ActionTypes;
appearence: Page_Key_Appearence;
}
interface Page_Key_Appearence {
text?: Page_Key_Text;
image?: Page_Key_Image;
background?: Page_Key_Background;
system?: {
border?: {
color: string;
thickness: number;
};
ghost?: boolean;
};
}
interface Page_Key_Text {
value: string;
size: number;
color: string;
offsetX: number;
offsetY: number;
}
interface Page_Key_Image {
address?: string;
iconid?: string;
iconstyle?: 'black' | 'white';
size: number;
offsetX: number;
offsetY: number;
rotation: number;
}
interface Page_Key_Background {
color: string;
}
type KeyTypes = 'empty' | 'custom' | 'pageup' | 'pagedown' | 'currentpage' | 'ghost';

View File

@@ -0,0 +1,17 @@
function isMac() {
return navigator.appVersion.indexOf('Mac') != -1;
}
function isWin() {
return navigator.appVersion.indexOf('Win') != -1;
}
function isLinux() {
return navigator.appVersion.indexOf('Linux') != -1;
}
function isUnix() {
return navigator.appVersion.indexOf('X11') != -1;
}
document.querySelectorAll('.shortcutKey').forEach((command: HTMLDivElement) => {
if (isMac()) command.innerText = 'Command';
else command.innerText = 'Ctrl';
});

View File

@@ -0,0 +1,17 @@
function scaleOverview() {
var overview: HTMLDivElement = document.querySelector('.overview');
var deck: HTMLDivElement = overview.querySelector('.deck');
if (overview.clientWidth > 0) {
var margin = 10;
var widthScale = overview.clientWidth / (deck.clientWidth + margin * 2);
var heightScale = overview.clientHeight / (deck.clientHeight + margin * 2);
if (deck.clientHeight * widthScale > overview.clientHeight) deck.style.transform = `scale(${heightScale})`;
else deck.style.transform = `scale(${widthScale})`;
} else setTimeout(scaleOverview, 100);
}
window.addEventListener('resize', scaleOverview);
scaleOverview();

View File

@@ -0,0 +1,141 @@
var PageHandler: PageHandler = {
elements: {
pagename: document.querySelector('.pagename'),
left: document.querySelector('.pageselector').querySelector('.box.left'),
right: document.querySelector('.pageselector').querySelector('.box.right'),
add: document.querySelector('.pageselector').querySelector('.box.add')
},
currentPageID: null,
currentPage: null,
currentIndex: null,
request(pageID: string, index: number) {
socket.emit('page', 'request', pageID, (err?: string, page?: Page_Config) => {
if (err) UndeckedNotification(`Error whilst getting page ${pageID}: ${err}`, 'error', 5000);
else {
PageHandler.currentIndex = index;
PageHandler.render(page);
}
});
},
render(page: Page_Config) {
KeyHandler.clear();
PageHandler.currentPageID = page.pageID;
PageHandler.currentPage = page;
PageList.select(page.pageID);
PageHandler.elements.pagename.value = page.name;
PageHandler.elements.pagename.removeAttribute('disabled');
for (var y in page.keys) {
for (var x in page.keys[y]) {
KeyHandler.render(x, y, page.keys[y][x]);
}
}
},
listeners() {
PageHandler.elements.pagename.oninput = () => {
if (PageHandler.currentPageID != null && PageHandler.elements.pagename.value.length > 0) {
socket.emit('page', 'setname', PageHandler.currentPageID, PageHandler.elements.pagename.value);
}
};
PageHandler.elements.add.onclick = () => {
var pageName = prompt('New page name', 'Untitled page');
if (pageName && pageName.length > 0) {
socket.emit('page', 'create', pageName);
}
};
PageHandler.elements.left.onclick = () => {
var selected = document.querySelector('.pageitem.selected');
if (selected && selected.previousElementSibling) (<HTMLElement>selected.previousElementSibling).click();
};
PageHandler.elements.right.onclick = () => {
var selected = document.querySelector('.pageitem.selected');
if (selected && selected.nextElementSibling) (<HTMLElement>selected.nextElementSibling).click();
};
}
};
PageHandler.listeners();
interface PageHandler {
elements: {
pagename: HTMLInputElement;
left: HTMLDivElement;
right: HTMLDivElement;
add: HTMLDivElement;
};
currentPageID: string;
currentPage: Page_Config;
currentIndex: number;
request: (pageID: string, index: number) => void;
render: (page: Page_Config) => void;
listeners: () => void;
}
interface Page_Config {
pageID: string;
name?: string;
keys?: Page_Config_Keys;
}
interface Page_Config_Keys {
'0'?: {
'0'?: Page_Key;
'1'?: Page_Key;
'2'?: Page_Key;
'3'?: Page_Key;
'4'?: Page_Key;
'5'?: Page_Key;
'6'?: Page_Key;
'7'?: Page_Key;
'8'?: Page_Key;
};
'1'?: {
'0'?: Page_Key;
'1'?: Page_Key;
'2'?: Page_Key;
'3'?: Page_Key;
'4'?: Page_Key;
'5'?: Page_Key;
'6'?: Page_Key;
'7'?: Page_Key;
'8'?: Page_Key;
};
'2'?: {
'0'?: Page_Key;
'1'?: Page_Key;
'2'?: Page_Key;
'3'?: Page_Key;
'4'?: Page_Key;
'5'?: Page_Key;
'6'?: Page_Key;
'7'?: Page_Key;
'8'?: Page_Key;
};
'3'?: {
'0'?: Page_Key;
'1'?: Page_Key;
'2'?: Page_Key;
'3'?: Page_Key;
'4'?: Page_Key;
'5'?: Page_Key;
'6'?: Page_Key;
'7'?: Page_Key;
'8'?: Page_Key;
};
}
type Icon = '';

View File

@@ -0,0 +1,115 @@
var PageList: PageList = {
elements: {
container: document.querySelector('.pageselector')
},
order: null,
firstRender: true,
render(pagelist: PageListItem[]) {
var list = ce('div', 'list');
PageList.order = pagelist.map((page) => {
return page.pageID;
});
pagelist.forEach((pagelistitem, index) => {
var pageitem = ce('div', 'pageitem', { pageID: pagelistitem.pageID });
var name = ce('div', 'name', null, pagelistitem.name);
pageitem.appendChild(name);
var move = ce('div', 'move');
var up = ce('div', [
'moveitem',
'up'
]);
up.appendChild(ce('img', 'normal', { src: '/stc/icon/up.png' }));
up.appendChild(ce('img', 'selected', { src: '/stc/icon/up_gray.png' }));
var down = ce('div', [
'moveitem',
'down'
]);
down.appendChild(ce('img', 'normal', { src: '/stc/icon/down.png' }));
down.appendChild(ce('img', 'selected', { src: '/stc/icon/down_gray.png' }));
up.onclick = (e: MouseEvent) => {
e.stopPropagation();
var current = PageList.order.indexOf(pagelistitem.pageID);
var newIndex = Math.max(current - 1, 0);
PageList.order.splice(current, 1);
PageList.order.splice(newIndex, 0, pagelistitem.pageID);
socket.emit('page', 'setorder', PageList.order);
};
down.onclick = (e: MouseEvent) => {
e.stopPropagation();
var current = PageList.order.indexOf(pagelistitem.pageID);
var newIndex = Math.max(current + 1, 0);
PageList.order.splice(current, 1);
PageList.order.splice(newIndex, 0, pagelistitem.pageID);
socket.emit('page', 'setorder', PageList.order);
};
move.appendChild(up);
move.appendChild(down);
pageitem.appendChild(move);
pageitem.onclick = () => {
PageHandler.request(pagelistitem.pageID, index);
};
list.appendChild(pageitem);
if (PageHandler.currentPageID == pagelistitem.pageID) {
PageHandler.request(pagelistitem.pageID, index);
}
});
var existing = PageList.elements.container.querySelector('.list');
if (existing) existing.parentElement.removeChild(existing);
PageList.elements.container.appendChild(list);
},
select(pageID: string): boolean {
var selectedFound = false;
PageList.elements.container.querySelectorAll('.pageitem').forEach((item: HTMLDivElement) => {
var itemPageID = item.getAttribute('pageID');
if (itemPageID == pageID) {
item.classList.add('selected');
selectedFound = true;
} else item.classList.remove('selected');
});
if (selectedFound) {
history.pushState(null, null, `/pages/${pageID}`);
TabController.setTitle(`Page ${pageID}`);
}
return selectedFound;
},
updateName(pageID: string, name: string) {
var item = PageList.elements.container.querySelector(`.pageitem[pageid="${pageID}"]`);
if (item) {
var nameElement: HTMLDivElement = item.querySelector('.name');
nameElement.innerText = name;
}
}
};
interface PageList {
elements: {
container: HTMLDivElement;
};
firstRender: boolean;
order: string[];
render: (pagelist: PageListItem[]) => void;
select: (pageID: string) => boolean;
updateName: (pageID: string, name: string) => void;
}
interface PageListItem {
pageID: string;
name: string;
}

View File

@@ -0,0 +1,95 @@
var TabController: TabController = {
elements: {
menu: document.querySelector('.tabmenu'),
pages: document.querySelector('.tabpages')
},
setTitle(title: string) {
var titleElement = document.querySelector('title');
titleElement.innerText = `Undecked - ${title}`;
},
show(tabname: string) {
TabController.elements.menu.querySelectorAll('.item').forEach((element: HTMLDivElement) => {
var tab = element.getAttribute('tab');
if (tab == tabname) element.classList.add('active');
else element.classList.remove('active');
});
TabController.elements.pages.querySelectorAll('.item').forEach((element: HTMLDivElement) => {
var tab = element.getAttribute('tab');
if (tab == tabname) element.classList.add('active');
else element.classList.remove('active');
});
if (tabname == 'pages') {
var pageID = PageHandler.currentPageID;
history.pushState(null, null, `/pages/pageID`);
TabController.setTitle(`Page ${pageID}`);
} else {
history.pushState(null, null, `/${tabname}`);
TabController.setTitle(tabname.charAt(0).toUpperCase() + tabname.slice(1));
}
},
registerListeners() {
TabController.elements.menu.querySelectorAll('.item').forEach((element: HTMLDivElement) => {
var tab = element.getAttribute('tab');
element.onclick = () => {
TabController.show(tab);
};
});
},
init() {
var args = window.location.pathname.split('/');
if (args.length > 0) args.splice(0, 1);
var tab = args.length > 0 ? args[0] : null;
var subTab = args.length > 1 ? args[1] : null;
if (PageList.order != null) {
if (tab != undefined && tab.length > 0) {
if (tab == 'pages') {
var valid = PageList.select(subTab);
if (valid) {
var index = PageList.order.indexOf(subTab);
PageHandler.request(PageList.order[index], index);
TabController.show('pages');
} else {
PageHandler.request(PageList.order[0], 0);
TabController.show('pages');
}
} else {
PageHandler.request(PageList.order[0], 0);
TabController.show(tab);
}
} else {
PageList.firstRender = false;
PageHandler.request(PageList.order[0], 0);
TabController.show('pages');
}
} else
setTimeout(() => {
TabController.init();
}, 200);
}
};
TabController.init();
TabController.registerListeners();
interface TabController {
elements: {
menu: HTMLDivElement;
pages: HTMLDivElement;
};
setTitle: (title: string) => void;
show: (tabname: string) => void;
registerListeners: () => void;
init: () => void;
}