Initial commit

Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com>
Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com>
Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com>
Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com>
Co-Authored-By: Johan  <4934465+joolsus@users.noreply.github.com>
Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com>
Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
Michael Cartner
2024-01-26 11:52:55 +01:00
commit b9c60b07dc
2789 changed files with 868795 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import React from 'react';
import ReactDOM from 'react-dom';
import View from './view';
export interface ReactViewDefaultProps {
owner?: TSFixme;
}
export abstract class ReactView<TProps extends ReactViewDefaultProps> extends View {
private props: TProps;
public el: any;
constructor(props: TProps) {
super();
this.props = props;
}
public set owner(owner: TSFixme) {
this.props.owner = owner;
this.render();
}
public render() {
if (!this.el) {
this.el = $(document.createElement('div'));
this.el.css({
width: '100%',
height: '100%'
});
}
ReactDOM.render(React.createElement(this.renderReact.bind(this), this.props), this.el[0]);
return this.el;
}
public dispose() {
this.el && ReactDOM.unmountComponentAtNode(this.el[0]);
}
protected abstract renderReact(props: TProps): JSX.Element;
}

View File

@@ -0,0 +1,17 @@
module.exports = {
type: 'dev',
Tracker: {
trackExceptions: false
},
PreviewServer: {
port: 8574
},
devMode: true,
apiEndpoint: 'https://apidev.noodlcloud.com',
domainEndpoint: 'http://domains.noodlcloud.com',
aiEndpoint: 'https://ftii7qa6g2a3k3hlwi6etoo3z40qbbtw.lambda-url.us-east-1.on.aws'
// Test config during dev
// userConfig: require('./userconfig-dev')
};

View File

@@ -0,0 +1,13 @@
module.exports = {
type: 'dist',
Tracker: {
trackExceptions: true
},
PreviewServer: {
port: 8574
},
apiEndpoint: 'https://api.noodlcloud.com',
domainEndpoint: 'http://domains.noodlcloud.com',
aiEndpoint: 'https://p2qsqhrh6xd6relfoaye4tf6nm0bibpm.lambda-url.us-east-1.on.aws'
};

View File

@@ -0,0 +1,10 @@
module.exports = {
type: 'test',
Tracker: {
trackExceptions: false
},
PreviewServer: {
port: 8574
},
apiEndpoint: 'https://apidev.noodlcloud.com'
};

View File

@@ -0,0 +1,17 @@
const configDev = require('./config-dev');
const configDist = require('./config-dist');
function getProcess() {
try {
const remote = require('@electron/remote');
return remote ? remote.process : process;
} catch (exc) {
// Error: "@electron/remote" cannot be required in the browser process. Instead require("@electron/remote/main").
return process;
}
}
const _process = getProcess();
if (!_process.env.devMode) _process.env.devMode = (_process.argv || []).indexOf('--dev') !== -1 ? 'yes' : 'no';
module.exports = _process.env.devMode === 'yes' ? configDev : configDist;

View File

@@ -0,0 +1,15 @@
export default class Model {
events: { [key: string]: string };
listeners: TSFixme[];
static _listenersEnabled: boolean;
set(args: TSFixme): void;
on(event: string | string[], listener: (...args: any) => void, group?: number | string | object): Model;
once(event: string | string[], listener: (...args: any) => void): Model;
notifyListeners(event: string, ...args: any[]): Model;
off(group: number | string | object): Model;
removeAllListeners(): void;
}

View File

@@ -0,0 +1,100 @@
const { EventDispatcher } = require('./utils/EventDispatcher');
var Model = function () {
this.listeners = [];
this.listenersOnce = [];
};
Model._listenersEnabled = true;
Model.prototype.on = function (event, listener, group) {
this.listeners.push({
event: event,
listener: listener,
group: group
});
if (this.listeners.length > 10000) {
console.log('Warning: we have more that 10000 listeners on this model, is this sane?');
}
return this;
};
Model.prototype.once = function (event, listener) {
this.listenersOnce.push({
event: event,
listener: listener
});
return this;
};
Model.prototype.set = function (args) {
for (var i in args) this[i] = args[i];
this.notifyListeners('change', {
model: this
});
};
function shouldNotify(l, event) {
if (l.event.constructor == Array && l.event.indexOf(event) !== -1) {
return true;
} else if (l.event === event) {
return true;
} else if (l.event.indexOf !== undefined) {
// Support for dot notation in subscription
var index = l.event.indexOf(event);
if (index > 0 && l.event[index - 1] === '.') {
return true;
}
}
return false;
}
Model.prototype.notifyListeners = function (event, args) {
if (Model._listenersEnabled === false) return;
for (let index = 0; index < this.listeners.length; index++) {
const listener = this.listeners[index];
if (shouldNotify(listener, event)) {
listener.listener(args);
}
}
if (this.listenersOnce.length > 0) {
this.listenersOnce = this.listenersOnce.filter((listener) => {
if (shouldNotify(listener, event)) {
listener.listener(args);
return false;
}
return true;
});
}
// Dispatch global event
EventDispatcher.instance.notifyListeners('Model.' + event, {
model: this,
args: args
});
return this;
};
Model.prototype.off = function (group) {
for (var i = 0; i < this.listeners.length; i++) {
if (this.listeners[i].group === group) {
this.listeners.splice(i, 1);
i--;
}
}
return this;
};
Model.prototype.removeAllListeners = function () {
this.listeners = [];
this.listenersOnce = [];
};
module.exports = Model;

View File

@@ -0,0 +1,45 @@
export class EventDispatcher {
listeners: any[];
constructor() {
this.listeners = [];
}
on(event: string | string[], listener, group: unknown) {
this.listeners.push({ event: event, listener: listener, group: group });
}
notifyListeners(event: string, args?) {
const testForComps = event.split('.');
function notify(event, listener, eventName) {
const comps = event.split('.');
if (comps[0] === testForComps[0] && (comps[1] === '*' || comps[1] === testForComps[1])) listener(args, eventName);
}
for (const i in this.listeners) {
if (this.listeners[i].event instanceof Array) {
for (const j in this.listeners[i].event) notify(this.listeners[i].event[j], this.listeners[i].listener, event);
} else {
notify(this.listeners[i].event, this.listeners[i].listener, event);
}
}
}
emit(event: string, args?) {
this.notifyListeners(event, args);
}
off(group: unknown) {
if (group === undefined) return;
for (let i = 0; i < this.listeners.length; i++) {
if (this.listeners[i].group === group) {
this.listeners.splice(i, 1);
i--;
}
}
}
static instance = new EventDispatcher();
}

View File

@@ -0,0 +1,68 @@
const path = require('path');
const mkdir = require('mkdirp-sync');
const fs = require('fs');
const rimraf = require('rimraf');
const electron = require('electron');
const app = electron.app || require('@electron/remote').app;
function fileNameForKey(key) {
const keyFileName = path.basename(key, '.json') + '.json';
// Prevent ENOENT and other similar errors when using
// reserved characters in Windows filenames.
// See: https://en.wikipedia.org/wiki/Filename#Reserved%5Fcharacters%5Fand%5Fwords
const escapedFileName = encodeURIComponent(keyFileName);
const userDataPath = app.getPath('userData');
return path.join(userDataPath, escapedFileName);
}
/**
* @depracted This implementation is not cross-platform use this instead:
* ```ts
* import { JSONStorage } from '@noodl/platform';
* ```
*
* Can't remove this file because it's called from main thread,
* where the platform code is not setup.
*/
module.exports = {
get: function (key, callback) {
var filename = fileNameForKey(key);
fs.readFile(filename, { encoding: 'utf8' }, function (error, object) {
if (!error) {
var objectJSON = {};
try {
objectJSON = JSON.parse(object);
} catch (error) {
return callback(); //new Error('Invalid data'));
}
return callback(objectJSON);
}
if (error.code === 'ENOENT') {
return callback(JSON.stringify({}));
}
return callback();
});
/*storage.get(path, function(error, data) {
if (error) { callback(); return }
callback(data);
});*/
},
set: function (key, data) {
var filename = fileNameForKey(key);
const json = JSON.stringify(data);
mkdir(path.dirname(filename));
fs.writeFileSync(filename, json);
},
remove: function (key, callback) {
var filename = fileNameForKey(key);
rimraf(filename, callback);
// storage.remove(path,callback);
}
};

View File

@@ -0,0 +1,161 @@
const fs = require('fs');
class ProjectModules {
constructor() {}
scanProjectModules(projectDirectory, callback) {
if (projectDirectory === undefined) {
callback(); // No project directory, no modules
return;
}
var modulesPath = projectDirectory + '/noodl_modules';
fs.readdir(modulesPath, function (err, files) {
if (err) {
callback();
return;
}
//we only care about directories
const directories = files.filter((f) => {
var stats = fs.lstatSync(modulesPath + '/' + f);
return stats.isDirectory() || stats.isSymbolicLink();
});
if (directories.length === 0) {
callback();
} else {
// For each subfolder in noodl_modules
var modules = [];
var modulesLeft = 0;
function completeModule(path, manifest) {
if (path && manifest) {
// Module manifest is fetched, resolve local paths
var m = {
dependencies: [],
browser: manifest.browser,
runtimes: manifest.runtimes || ['browser'] //default to browser
};
if (manifest.main) {
m.index = path + '/' + manifest.main;
}
if (manifest.dependencies) {
for (var j = 0; j < manifest.dependencies.length; j++) {
var d = manifest.dependencies[j];
if (!d.startsWith['http']) d = path + '/' + d;
m.dependencies.push(d);
}
}
modules.push(m);
}
modulesLeft--;
if (modulesLeft === 0) {
//sort modules so the order is deterministics
//helps the editor understand when node libraries change, or are the same
const modulesWithIndexFile = modules.filter((m) => m.index);
const modulesWithoutIndexFile = modules.filter((m) => !m.index);
modulesWithIndexFile.sort((a, b) => a.index.localeCompare(b.index));
callback(modulesWithIndexFile.concat(modulesWithoutIndexFile));
}
}
for (var i = 0; i < directories.length; i++) {
const dir = directories[i];
modulesLeft++;
// Read manifest
fs.readFile(
modulesPath + '/' + dir + '/manifest.json',
'utf8',
(function () {
const _dir = dir;
return function (err, data) {
if (err) {
completeModule(); // Module load failed
return;
}
try {
completeModule('noodl_modules/' + _dir, JSON.parse(data));
} catch (e) {
// JSON not valid
completeModule(); // Module load failed
}
};
})()
);
}
}
});
}
injectIntoHtml(projectDirectory, template, pathPrefix, callback) {
this.scanProjectModules(projectDirectory, function (modules) {
var dependencies = '';
var modulesMain = '';
if (modules) {
const browserModules = modules.filter((m) => m.runtimes.indexOf('browser') !== -1);
for (var i = 0; i < browserModules.length; i++) {
var m = browserModules[i];
if (m.index) {
modulesMain += '<script type="text/javascript" src="' + pathPrefix + m.index + '"></script>\n';
}
// Module javascript dependencies
if (m.dependencies) {
for (var j = 0; j < m.dependencies.length; j++) {
var d = m.dependencies[j];
var dTag = '<script type="text/javascript" src="' + pathPrefix + d + '"></script>\n';
if (dependencies.indexOf(dTag) === -1) dependencies += dTag;
}
}
// Browser modules
if (m.browser) {
if (m.browser.head) {
var head = m.browser.head;
for (var j = 0; j < head.length; j++) {
dependencies += head[j] + '\n';
}
}
if (m.browser.styles) {
var styles = m.browser.styles;
for (var j = 0; j < styles.length; j++) {
dependencies += '<style>' + styles[j] + '</style>' + '\n';
}
}
if (m.browser.stylesheets) {
var sheets = m.browser.stylesheets;
for (var j = 0; j < sheets.length; j++) {
if (typeof sheets[j] === 'string') {
let path = sheets[j];
if (!path.startsWith('http')) {
path = pathPrefix + path;
}
dependencies += '<link href="' + path + '" rel="stylesheet">';
}
}
}
}
}
}
var injected = template.replace('<%modules_dependencies%>', dependencies);
injected = injected.replace('<%modules_main%>', modulesMain);
callback(injected);
});
}
}
ProjectModules.instance = new ProjectModules();
module.exports = ProjectModules;

View File

@@ -0,0 +1,20 @@
export default class View {
el: JQuery<HTMLElement>;
render(): TSFixme;
static $(el, selector): JQuery<HTMLElement>;
static showTooltip(args): void;
static hideTooltip(args): void;
$(selector): any;
on(event: string, listener: (...args: TSFixme) => void, group?: any): View;
off(group): View;
notifyListeners(event, args?): void;
cloneTemplate(tmpl): TSFixme;
bindView(el, obj?): TSFixme;
}

View File

@@ -0,0 +1,278 @@
const watch = function (obj, prop, handler) {
var value = obj[prop],
oldsetter = obj.__lookupSetter__(prop),
oldgetter = obj.__lookupGetter__(prop),
getter = function () {
return value;
},
setter = function (val) {
value = handler.call(obj, prop, value, val);
if (oldsetter) return oldsetter(value);
return value;
};
if (oldsetter || delete obj[prop]) {
// can't watch constants
if (Object.prototype.__defineGetter__ && Object.prototype.__defineSetter__) {
// legacy
Object.prototype.__defineGetter__.call(obj, prop, getter);
Object.prototype.__defineSetter__.call(obj, prop, setter);
}
}
};
const unwatch = function (obj, prop) {
var val = obj[prop];
delete obj[prop]; // remove accessors
obj[prop] = val;
};
var View = function () {
this.templates = {};
this.listeners = [];
};
View.$ = function (el, selector) {
return el.filter(selector).add(el.find(selector));
};
View.showTooltip = function (args) {
// Will be overridden externally
};
View.hideTooltip = function (el) {
// Will be overridden externally
};
View.prototype.$ = function (selector) {
const other = this.el.find(selector);
return this.el.filter(selector).add(other);
};
View.prototype.on = function (event, listener, group) {
this.listeners.push({
event: event,
listener: listener,
group
});
return this;
};
View.prototype.off = function (group) {
for (var i = 0; i < this.listeners.length; i++) {
if (this.listeners[i].group === group) {
this.listeners.splice(i, 1);
i--;
}
}
return this;
};
View.prototype.notifyListeners = function (event, args) {
for (var i in this.listeners) {
if (this.listeners[i].event === event) this.listeners[i].listener(args);
}
};
View.prototype.cloneTemplate = function (tmpl) {
return this.templates[tmpl].clone();
};
View.prototype.bindView = function (el, obj) {
var _this = this;
// Collect templates
el.find('[data-template]').each(function () {
var _el = $(this);
_el.remove();
_this.templates[_el.attr('data-template')] = _el;
});
var resolvePathOnObject = function (path, obj) {
var cmp = path.split('.');
for (var i = 0; i < cmp.length - 1; i++) {
obj = obj[cmp[i]];
}
return {
prop: cmp[cmp.length - 1],
obj: obj
};
};
var getObjectFromPath = function (path, obj) {
var resolved = resolvePathOnObject(path, obj);
return resolved.obj[resolved.prop];
};
function resolveStaticOrFromPath(attribute, obj) {
if (attribute.startsWith('path:')) {
const path = attribute.split('path:')[1];
const resolved = resolvePathOnObject(path, obj);
return resolved.obj[resolved.prop];
}
return attribute;
}
// Change events
View.$(el, '[data-change]').each(function () {
$(this).on('change', function (evt) {
_this[$(this).attr('data-change')].apply(_this, [obj, $(this), evt]);
});
});
// On click events
View.$(el, '[data-click]').each(function () {
$(this).on('click', function (evt) {
_this[$(this).attr('data-click')].apply(_this, [obj, $(this), evt]);
});
});
// On tooltip
var tooltipTimeout;
View.$(el, '[data-tooltip]').each(function () {
$(this)
.on('mouseenter', function (evt) {
var el = $(this);
let tooltip = resolveStaticOrFromPath(el.attr('data-tooltip'), obj);
if (!tooltip) return;
let content, extendedContent;
if (typeof tooltip === 'object') {
content = tooltip.standard;
extendedContent = tooltip.extended;
} else {
content = tooltip;
}
const position = el.attr('data-tooltip-position');
tooltipTimeout = setTimeout(() => {
const dimensions = View.showTooltip({
attachTo: el,
content,
position
});
if (extendedContent) {
tooltipTimeout = setTimeout(() => {
const offset = {
x: (300 - dimensions.contentWidth) / 2 //a hack to try to keep the arrow in the same position. Assumes the expanded popup is 300px wide.
};
View.showTooltip({
attachTo: el,
content: extendedContent,
position,
offset
});
}, 1500);
}
}, 500);
})
.on('mouseleave mousedown', function (evt) {
View.hideTooltip($(this));
clearTimeout(tooltipTimeout);
});
});
// Data text
View.$(el, '[data-text]').each(function () {
var _el = $(this);
var resolved = resolvePathOnObject($(this).attr('data-text'), obj);
_el.text(resolved.obj[resolved.prop]);
var bind = _el.attr('data-bind');
if (bind === 'watch') {
watch(resolved.obj, resolved.prop, function (prop, oldVal, newVal) {
_el.text(newVal);
return newVal;
});
}
});
// Data html
View.$(el, '[data-html]').each(function () {
var _el = $(this);
var resolved = resolvePathOnObject($(this).attr('data-html'), obj);
_el.html(resolved.obj[resolved.prop]);
var bind = _el.attr('data-bind');
if (bind === 'watch') {
watch(resolved.obj, resolved.prop, function (prop, oldVal, newVal) {
_el.html(newVal);
return newVal;
});
}
});
// Identifier (for easier input targeting)
View.$(el, '[data-identifier]').each(function () {
$(this).attr('data-identifier', obj?.name);
});
// Data val
View.$(el, '[data-val]').each(function () {
$(this).val(getObjectFromPath($(this).attr('data-val'), obj));
});
// Data src
View.$(el, '[data-src]').each(function () {
$(this).attr('src', getObjectFromPath($(this).attr('data-src'), obj));
});
// Data checked
View.$(el, '[data-checked]').each(function () {
$(this).prop('checked', getObjectFromPath($(this).attr('data-checked'), obj));
});
// Data class
View.$(el, '[data-class]').each(function () {
var _el = $(this);
var decl = $(this).attr('data-class');
var conds = decl.split(',');
for (var i in conds) {
var terms = conds[i].split(':');
var cls = terms[1];
var path = terms[0];
var invert = false;
if (path[0] === '!') {
invert = true;
path = path.substring(1);
}
var resolved = resolvePathOnObject(path, obj);
var check = invert ? !resolved.obj[resolved.prop] : resolved.obj[resolved.prop];
if (check) _el.addClass(cls);
else _el.removeClass(cls);
// bind with a watch, if the property changes
// update the class
watch(
resolved.obj,
resolved.prop,
(function () {
var _cls = cls;
var _invert = invert;
return function (prop, oldVal, newVal) {
if (oldVal !== newVal) {
var check = _invert ? !newVal : newVal;
if (check) _el.addClass(_cls);
else _el.removeClass(_cls);
}
return newVal;
};
})()
);
}
});
// Prevent buttons from focusing
View.$(el, 'button').on('focus', function () {
$(this).blur();
});
return el;
};
module.exports = View;