Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ alongside the solver. When you scaffold a new SolverForge project with
| `SF.showError(title, detail)` | `void` | Danger toast shorthand |
| `SF.showTab(tabId, root?)` | `void` | Activate matching tab panels in every tab container, or only within `root` when provided |

### Unsafe HTML APIs (opt-in)

Default content is always text-rendered. Use these fields only with trusted HTML:

| Factory | Unsafe HTML field |
|---------|-------------------|
| `SF.el(tag, attrs, ...)` | `unsafeHtml` |
| `SF.createModal(config)` | `unsafeBody` |
| `SF.createTabs(config)` | `tabs[].content.unsafeHtml` |
| `SF.createTable(config)` | `cells[].unsafeHtml` |
| `SF.gantt.create(config)` | `unsafePopupHtml`, `columns[].render(task).unsafeHtml` |

### Timeline Rail

| Factory | Returns | Description |
Expand Down Expand Up @@ -228,7 +240,9 @@ var gantt = SF.gantt.create({
{ key: 'start', label: 'Start' },
{ key: 'end', label: 'End' },
{ key: 'priority', label: 'P', render: function (t) {
return '<span class="sf-priority-badge priority-' + t.priority + '">P' + t.priority + '</span>';
return {
unsafeHtml: '<span class="sf-priority-badge priority-' + t.priority + '">P' + t.priority + '</span>',
};
}},
],
onTaskClick: function (task) { console.log('clicked', task.id); },
Expand Down
3 changes: 2 additions & 1 deletion js-src/00-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const SF = (function () {
}
else if (key.indexOf('on') === 0) el.addEventListener(key.slice(2).toLowerCase(), attrs[key]);
else if (key === 'dataset') Object.assign(el.dataset, attrs[key]);
else if (key === 'html') el.innerHTML = attrs[key];
else if (key === 'html') el.textContent = attrs[key];
else if (key === 'unsafeHtml') el.innerHTML = attrs[key];
else el.setAttribute(key, attrs[key]);
});
}
Expand Down
36 changes: 20 additions & 16 deletions js-src/06-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,22 @@
sf.createModal = function (config) {
var overlay = sf.el('div', { className: 'sf-modal-overlay' });
var dialog = sf.el('div', { className: 'sf-modal' });
var body = sf.el('div', { className: 'sf-modal-body' });

// Header
var header = sf.el('div', { className: 'sf-modal-header' });
header.appendChild(sf.el('div', { className: 'sf-modal-title' }, config.title || ''));

var closeBtn = sf.el('button', {
className: 'sf-modal-close',
html: '&times;',
onClick: function () { api.close(); },
});
}, '×');
header.appendChild(closeBtn);

dialog.appendChild(header);

// Body
var body = sf.el('div', { className: 'sf-modal-body' });
if (config.body) {
if (typeof config.body === 'string') {
body.innerHTML = config.body;
} else if (config.body instanceof Node) {
body.appendChild(config.body);
}
}
setBodyContent(body, config.body, config.unsafeBody);
dialog.appendChild(body);

// Footer
Expand Down Expand Up @@ -69,12 +63,7 @@
};

api.setBody = function (content) {
body.innerHTML = '';
if (typeof content === 'string') {
body.innerHTML = content;
} else if (content instanceof Node) {
body.appendChild(content);
}
setBodyContent(body, content);
};

if (config.width) {
Expand All @@ -84,4 +73,19 @@
return api;
};

function setBodyContent(target, content, explicitUnsafeHtml) {
target.textContent = '';
if (explicitUnsafeHtml != null) {
target.innerHTML = explicitUnsafeHtml;
} else if (typeof content === 'string') {
target.textContent = content;
} else if (content && content.unsafeBody) {
target.innerHTML = content.unsafeBody;
} else if (content && content.unsafeHtml) {
target.innerHTML = content.unsafeHtml;
} else if (content instanceof Node) {
target.appendChild(content);
}
}

})(SF);
3 changes: 2 additions & 1 deletion js-src/07-tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
dataset: { tabId: tab.id },
});
if (tab.content) {
if (typeof tab.content === 'string') panel.innerHTML = tab.content;
if (typeof tab.content === 'string') panel.textContent = tab.content;
else if (tab.content && tab.content.unsafeHtml) panel.innerHTML = tab.content.unsafeHtml;
else if (tab.content instanceof Node) panel.appendChild(tab.content);
}
container.appendChild(panel);
Expand Down
4 changes: 2 additions & 2 deletions js-src/08-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
td.textContent = cell;
} else if (cell instanceof Node) {
td.appendChild(cell);
} else if (cell && cell.html) {
td.innerHTML = cell.html;
} else if (cell && cell.unsafeHtml) {
td.innerHTML = cell.unsafeHtml;
}
var col = config.columns && config.columns[colIdx];
if (col && col.align) td.style.textAlign = col.align;
Expand Down
3 changes: 1 addition & 2 deletions js-src/09-toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,8 @@

var closeBtn = sf.el('button', {
className: 'sf-toast-close',
html: '&times;',
onClick: function () { dismiss(); },
});
}, '×');
toast.appendChild(closeBtn);

container.appendChild(toast);
Expand Down
30 changes: 25 additions & 5 deletions js-src/14-gantt.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,27 @@
var frappeTasks = tasksToFrappe(taskList);

if (frappeTasks.length === 0) {
chartContainer.innerHTML = '<div style="padding:24px;color:var(--sf-gray-400);font-family:var(--sf-font-mono);font-size:13px;">No scheduled tasks to display.</div>';
chartContainer.textContent = '';
chartContainer.appendChild(sf.el('div', {
className: 'sf-gantt-empty-state',
style: {
padding: '24px',
color: 'var(--sf-gray-400)',
fontFamily: 'var(--sf-font-mono)',
fontSize: '13px',
},
}, 'No scheduled tasks to display.'));
ganttChart = null;
return;
}

chartContainer.innerHTML = '<svg id="' + svgId + '"></svg>';
chartContainer.textContent = '';
chartContainer.appendChild(createSvgRoot(svgId));

ganttChart = new Gantt('#' + svgId, frappeTasks, {
view_mode: viewSelect.value || 'Quarter Day',
date_format: 'YYYY-MM-DD HH:mm',
custom_popup_html: config.popupHtml || defaultPopup,
custom_popup_html: config.unsafePopupHtml || config.popupHtml || defaultPopup,
on_click: function (task) {
ctrl.highlightTask(task.id);
if (config.onTaskClick) config.onTaskClick(task);
Expand All @@ -187,7 +197,7 @@
}

function renderGrid(taskList) {
grid.innerHTML = '';
while (grid.firstChild) grid.removeChild(grid.firstChild);
var table = sf.el('table', { className: 'sf-gantt-table' });

// Header
Expand Down Expand Up @@ -222,7 +232,8 @@
td.textContent = task.name || task.label || task.id;
} else if (col.render) {
var content = col.render(task);
if (typeof content === 'string') td.innerHTML = content;
if (typeof content === 'string') td.textContent = content;
else if (content && content.unsafeHtml) td.innerHTML = content.unsafeHtml;
else if (content instanceof Node) td.appendChild(content);
} else {
td.textContent = task[col.key] || '';
Expand All @@ -248,6 +259,15 @@
(t.pinned ? '<p class="sf-gantt-popup-pinned"><i class="fa-solid fa-thumbtack"></i> Pinned</p>' : '') +
'</div>';
}

function createSvgRoot(id) {
if (document.createElementNS) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = id;
return svg;
}
return sf.el('svg', { id: id });
}
};

})(SF);
79 changes: 52 additions & 27 deletions static/sf/sf.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const SF = (function () {
}
else if (key.indexOf('on') === 0) el.addEventListener(key.slice(2).toLowerCase(), attrs[key]);
else if (key === 'dataset') Object.assign(el.dataset, attrs[key]);
else if (key === 'html') el.innerHTML = attrs[key];
else if (key === 'html') el.textContent = attrs[key];
else if (key === 'unsafeHtml') el.innerHTML = attrs[key];
else el.setAttribute(key, attrs[key]);
});
}
Expand Down Expand Up @@ -484,28 +485,22 @@ const SF = (function () {
sf.createModal = function (config) {
var overlay = sf.el('div', { className: 'sf-modal-overlay' });
var dialog = sf.el('div', { className: 'sf-modal' });
var body = sf.el('div', { className: 'sf-modal-body' });

// Header
var header = sf.el('div', { className: 'sf-modal-header' });
header.appendChild(sf.el('div', { className: 'sf-modal-title' }, config.title || ''));

var closeBtn = sf.el('button', {
className: 'sf-modal-close',
html: '&times;',
onClick: function () { api.close(); },
});
}, '×');
header.appendChild(closeBtn);

dialog.appendChild(header);

// Body
var body = sf.el('div', { className: 'sf-modal-body' });
if (config.body) {
if (typeof config.body === 'string') {
body.innerHTML = config.body;
} else if (config.body instanceof Node) {
body.appendChild(config.body);
}
}
setBodyContent(body, config.body, config.unsafeBody);
dialog.appendChild(body);

// Footer
Expand Down Expand Up @@ -545,12 +540,7 @@ const SF = (function () {
};

api.setBody = function (content) {
body.innerHTML = '';
if (typeof content === 'string') {
body.innerHTML = content;
} else if (content instanceof Node) {
body.appendChild(content);
}
setBodyContent(body, content);
};

if (config.width) {
Expand All @@ -560,6 +550,21 @@ const SF = (function () {
return api;
};

function setBodyContent(target, content, explicitUnsafeHtml) {
target.textContent = '';
if (explicitUnsafeHtml != null) {
target.innerHTML = explicitUnsafeHtml;
} else if (typeof content === 'string') {
target.textContent = content;
} else if (content && content.unsafeBody) {
target.innerHTML = content.unsafeBody;
} else if (content && content.unsafeHtml) {
target.innerHTML = content.unsafeHtml;
} else if (content instanceof Node) {
target.appendChild(content);
}
}

})(SF);
/* ============================================================================
SolverForge UI — Tab Switching
Expand Down Expand Up @@ -590,7 +595,8 @@ const SF = (function () {
dataset: { tabId: tab.id },
});
if (tab.content) {
if (typeof tab.content === 'string') panel.innerHTML = tab.content;
if (typeof tab.content === 'string') panel.textContent = tab.content;
else if (tab.content && tab.content.unsafeHtml) panel.innerHTML = tab.content.unsafeHtml;
else if (tab.content instanceof Node) panel.appendChild(tab.content);
}
container.appendChild(panel);
Expand Down Expand Up @@ -650,8 +656,8 @@ const SF = (function () {
td.textContent = cell;
} else if (cell instanceof Node) {
td.appendChild(cell);
} else if (cell && cell.html) {
td.innerHTML = cell.html;
} else if (cell && cell.unsafeHtml) {
td.innerHTML = cell.unsafeHtml;
}
var col = config.columns && config.columns[colIdx];
if (col && col.align) td.style.textAlign = col.align;
Expand Down Expand Up @@ -710,9 +716,8 @@ const SF = (function () {

var closeBtn = sf.el('button', {
className: 'sf-toast-close',
html: '&times;',
onClick: function () { dismiss(); },
});
}, '×');
toast.appendChild(closeBtn);

container.appendChild(toast);
Expand Down Expand Up @@ -1343,17 +1348,27 @@ const SF = (function () {
var frappeTasks = tasksToFrappe(taskList);

if (frappeTasks.length === 0) {
chartContainer.innerHTML = '<div style="padding:24px;color:var(--sf-gray-400);font-family:var(--sf-font-mono);font-size:13px;">No scheduled tasks to display.</div>';
chartContainer.textContent = '';
chartContainer.appendChild(sf.el('div', {
className: 'sf-gantt-empty-state',
style: {
padding: '24px',
color: 'var(--sf-gray-400)',
fontFamily: 'var(--sf-font-mono)',
fontSize: '13px',
},
}, 'No scheduled tasks to display.'));
ganttChart = null;
return;
}

chartContainer.innerHTML = '<svg id="' + svgId + '"></svg>';
chartContainer.textContent = '';
chartContainer.appendChild(createSvgRoot(svgId));

ganttChart = new Gantt('#' + svgId, frappeTasks, {
view_mode: viewSelect.value || 'Quarter Day',
date_format: 'YYYY-MM-DD HH:mm',
custom_popup_html: config.popupHtml || defaultPopup,
custom_popup_html: config.unsafePopupHtml || config.popupHtml || defaultPopup,
on_click: function (task) {
ctrl.highlightTask(task.id);
if (config.onTaskClick) config.onTaskClick(task);
Expand All @@ -1365,7 +1380,7 @@ const SF = (function () {
}

function renderGrid(taskList) {
grid.innerHTML = '';
while (grid.firstChild) grid.removeChild(grid.firstChild);
var table = sf.el('table', { className: 'sf-gantt-table' });

// Header
Expand Down Expand Up @@ -1400,7 +1415,8 @@ const SF = (function () {
td.textContent = task.name || task.label || task.id;
} else if (col.render) {
var content = col.render(task);
if (typeof content === 'string') td.innerHTML = content;
if (typeof content === 'string') td.textContent = content;
else if (content && content.unsafeHtml) td.innerHTML = content.unsafeHtml;
else if (content instanceof Node) td.appendChild(content);
} else {
td.textContent = task[col.key] || '';
Expand All @@ -1426,6 +1442,15 @@ const SF = (function () {
(t.pinned ? '<p class="sf-gantt-popup-pinned"><i class="fa-solid fa-thumbtack"></i> Pinned</p>' : '') +
'</div>';
}

function createSvgRoot(id) {
if (document.createElementNS) {
var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.id = id;
return svg;
}
return sf.el('svg', { id: id });
}
};

})(SF);
Loading
Loading