Skip to content
Draft
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
package-lock.json
/node_modules
/tmp
/vendor/bundle
/.bundle
265 changes: 265 additions & 0 deletions color-demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trix colour-token attribute — demo</title>
<link rel="stylesheet" href="dist/trix.css">
<script src="dist/trix.js"></script>
<style>
:root {
--fg: #111;
--muted: #6b7280;
--border: #e5e7eb;
--bg-soft: #f9fafb;
--accent: #2563eb;

/* The theme tokens. In a real CMS these would come from the theme
definition and change per-site/per-page. */
--color-primary: #2563eb;
--color-secondary: #9333ea;
--color-accent: #f59e0b;
}

/* Theme mapping: the HTML carries semantic tokens; the theme
resolves them to actual colours. Change these 3 lines and every
document on the site re-themes. */
[data-color="primary"] { color: var(--color-primary); }
[data-color="secondary"] { color: var(--color-secondary); }
[data-color="accent"] { color: var(--color-accent); }

* { box-sizing: border-box; }
body {
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
color: var(--fg);
margin: 0;
background: white;
}
main { max-width: 880px; margin: 32px auto; padding: 0 16px 64px; }
h1 { margin: 0 0 8px; font-size: 22px; }
h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); margin: 24px 0 8px; }
p.lede { color: var(--muted); margin: 0 0 24px; }
code { background: var(--bg-soft); padding: 1px 6px; border-radius: 4px; font-size: 13px; }

.picker {
display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
padding: 12px; border: 1px solid var(--border); border-radius: 8px;
margin-bottom: 12px; background: var(--bg-soft);
}
.picker > span.label { font-size: 12px; color: var(--muted); margin-right: 4px; }

button.token {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px; border-radius: 20px;
font: 13px/1 inherit; cursor: pointer;
border: 1px solid var(--border); background: white; color: var(--fg);
}
button.token:hover { border-color: var(--accent); }
button.token.active { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(37,99,235,0.2); }
button.token .dot { width: 12px; height: 12px; border-radius: 50%; }
button.token[data-token="primary"] .dot { background: var(--color-primary); }
button.token[data-token="secondary"] .dot { background: var(--color-secondary); }
button.token[data-token="accent"] .dot { background: var(--color-accent); }

button.ghost {
font-size: 12px; padding: 6px 10px;
background: white; border: 1px solid var(--border); border-radius: 6px;
cursor: pointer; color: var(--fg);
}
button.ghost:hover { border-color: var(--accent); color: var(--accent); }
.status { margin-left: auto; font-size: 12px; color: var(--muted); }
.status b { color: var(--fg); font-weight: 600; }

trix-editor {
min-height: 160px; padding: 12px;
border: 1px solid var(--border); border-radius: 8px;
}
trix-editor:focus { outline: 2px solid var(--accent); outline-offset: -1px; }

.inspectors { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.inspectors pre {
margin: 0; padding: 10px 12px;
background: var(--bg-soft); border: 1px solid var(--border); border-radius: 6px;
font: 12px/1.4 ui-monospace, Menlo, monospace;
max-height: 240px; overflow: auto;
white-space: pre-wrap; word-break: break-word;
}
.actions { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
</style>
</head>
<body>

<!--
Runtime integration. Three concerns, in order:
1. Monkey-patch the HTMLSanitizer to allow `data-color` on load.
2. Monkey-patch PieceView.createElement so a new `attributeName`
config key causes the value to become an HTML attribute, not an
inline style.
3. Register `color` in Trix.config.textAttributes using that key.
All three MUST run before the <trix-editor> tag below is parsed.
-->
<script>
(function () {
// 1. Allow data-color to survive HTMLSanitizer. The sanitiser only
// keeps attributes in its allow-list (default: style href src
// width height class target) plus anything starting with
// data-trix-. data-color is in neither bucket, so we intercept.
var origSanitize = Trix.HTMLSanitizer.sanitize;
Trix.HTMLSanitizer.sanitize = function (html, options) {
options = options || {};
var baseline = ["style", "href", "src", "width", "height", "class", "target"];
var extra = ["data-color"];
if (!options.allowedAttributes) {
options.allowedAttributes = baseline.concat(extra);
} else {
extra.forEach(function (a) {
if (options.allowedAttributes.indexOf(a) < 0) options.allowedAttributes.push(a);
});
}
return origSanitize.call(this, html, options);
};

// 2. Extend PieceView.createElement to honour a new `attributeName`
// config key. The original function handles tagName, style, and
// styleProperty. We run it first, then append our attribute.
var origCreate = Trix.PieceView.prototype.createElement;
Trix.PieceView.prototype.createElement = function () {
var element = origCreate.call(this);
var attrs = this.attributes; // piece attributes, already extracted by constructor
for (var key in attrs) {
var config = Trix.config.textAttributes[key];
if (config && config.attributeName) {
if (!element) element = Trix.makeElement("span");
element.setAttribute(config.attributeName, attrs[key]);
}
}
return element;
};

// 3. Register the attribute.
Trix.config.textAttributes.color = {
attributeName: "data-color", // consumed by the patched createElement
inheritable: true,
parser: function (element) {
var found = Trix.findClosestElementFromNode(element, { matchingSelector: "[data-color]" });
return found ? found.getAttribute("data-color") : undefined;
},
};
})();
</script>

<main>
<h1>Trix colour-token attribute — demo</h1>
<p class="lede">
Runtime-registered <code>color</code> text attribute that emits
<code>data-color="primary|secondary|accent"</code> into the HTML.
The theme (see the <code>:root</code> CSS at the top of this file)
maps each token to an actual colour — change those three lines and
every document re-themes.
</p>

<div class="picker" id="picker">
<span class="label">Colour token:</span>
<button class="token" data-token="primary" tabindex="-1"><span class="dot"></span>primary</button>
<button class="token" data-token="secondary" tabindex="-1"><span class="dot"></span>secondary</button>
<button class="token" data-token="accent" tabindex="-1"><span class="dot"></span>accent</button>
<button class="ghost" id="clear-color" tabindex="-1">Clear</button>
<span class="status" id="status">no selection</span>
</div>

<form>
<input
type="hidden"
id="content"
name="content"
value='&lt;div&gt;Welcome to the &lt;span data-color=&quot;primary&quot;&gt;colour&lt;/span&gt; demo.&lt;/div&gt;&lt;div&gt;Try: &lt;strong data-color=&quot;secondary&quot;&gt;bold secondary&lt;/strong&gt;, &lt;em&gt;plain italic&lt;/em&gt;, and &lt;span data-color=&quot;accent&quot;&gt;accent&lt;/span&gt; words.&lt;/div&gt;&lt;div&gt;Select anything and pick a token above.&lt;/div&gt;'>
<trix-editor input="content" autofocus></trix-editor>
</form>

<div class="inspectors">
<div>
<h2>Active attributes at cursor</h2>
<pre id="attrs-out">(nothing yet)</pre>
</div>
<div>
<h2>Serialized HTML (hidden input value)</h2>
<pre id="html-out">(nothing yet)</pre>
</div>
</div>

<div class="actions">
<button class="ghost" id="reload-btn">Reload from serialized HTML</button>
<button class="ghost" id="reset-btn">Reset to sample content</button>
<button class="ghost" id="empty-btn">Empty editor</button>
</div>
</main>

<script>
(function () {
var editorEl = document.querySelector("trix-editor");
var picker = document.getElementById("picker");
var clearBtn = document.getElementById("clear-color");
var status = document.getElementById("status");
var attrsOut = document.getElementById("attrs-out");
var htmlOut = document.getElementById("html-out");
var contentEl = document.getElementById("content");
var reloadBtn = document.getElementById("reload-btn");
var resetBtn = document.getElementById("reset-btn");
var emptyBtn = document.getElementById("empty-btn");

var initialHTML = contentEl.value;
var tokens = picker.querySelectorAll("button.token");

function applyToken(token) {
if (!editorEl.editor) return;
editorEl.editor.recordUndoEntry("Colour: " + token, { consolidatable: true });
editorEl.editor.activateAttribute("color", token);
}
function clearToken() {
if (!editorEl.editor) return;
editorEl.editor.recordUndoEntry("Clear colour", { consolidatable: true });
editorEl.editor.deactivateAttribute("color");
}

// Use mousedown so we act before the editor loses focus to the button.
tokens.forEach(function (btn) {
btn.addEventListener("mousedown", function (e) {
e.preventDefault();
applyToken(btn.dataset.token);
});
});
clearBtn.addEventListener("mousedown", function (e) {
e.preventDefault();
clearToken();
});

editorEl.addEventListener("trix-selection-change", function () {
if (!editorEl.editor) return;
var range = editorEl.editor.getSelectedRange();
if (!range) {
status.innerHTML = "<b>no selection</b>";
} else if (range[0] === range[1]) {
status.innerHTML = "cursor at <b>" + range[0] + "</b> — token will apply to next typed text";
} else {
status.innerHTML = "selection <b>" + range[0] + "&ndash;" + range[1] + "</b> (" + (range[1] - range[0]) + " chars)";
}
});

editorEl.addEventListener("trix-attributes-change", function (e) {
attrsOut.textContent = JSON.stringify(e.attributes, null, 2);
var active = e.attributes && e.attributes.color;
tokens.forEach(function (btn) {
btn.classList.toggle("active", btn.dataset.token === active);
});
});

editorEl.addEventListener("trix-change", function () { htmlOut.textContent = contentEl.value; });
editorEl.addEventListener("trix-initialize", function () { htmlOut.textContent = contentEl.value; });

reloadBtn.addEventListener("click", function () { editorEl.editor.loadHTML(contentEl.value); });
resetBtn .addEventListener("click", function () { editorEl.editor.loadHTML(initialHTML); });
emptyBtn .addEventListener("click", function () { editorEl.editor.loadHTML(""); });
})();
</script>
</body>
</html>
97 changes: 97 additions & 0 deletions docs/01-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Overview

## What Trix is

Trix is a WYSIWYG rich-text editor originally written by Basecamp. Its central
design decision is to **avoid `document.execCommand` entirely** and treat the
browser's `contenteditable` behaviour as an I/O device:

1. Keystrokes, paste, and drop events are observed.
2. Events are translated into operations on an in-memory **document model**.
3. The model is re-rendered to the DOM; the DOM is never the source of truth.

This makes behaviour reproducible across browsers — every browser quirk lives on
the input side; the document model is a pure data structure.

## Fork history

This repository is a fork of `basecamp/trix`, published as `@jimdo/trix` on the
private Jimdo npm registry (`package.json:2`, `.npmrc`). Per the root
`README.md`, the fork was created because Trix was, at the time, the only
WYSIWYG editor with acceptable behaviour on Android GBoard. The fork adds
Jimdo-specific tweaks on top of upstream.

Recent Jimdo changes visible in `git log` include:
- Alignment block attributes (`alignRight`, `alignCenter`) added via
`inheritFromPreviousBlock` + `role: "alignment"`
(`src/trix/config/block_attributes.coffee:67-79`).
- List-delete semantics (`11750a9a fix(lists): change behavior of delete into lists`).
- Expanded `createContainerElement` so an `<a>` tag captures both `href` and
`target` attributes simultaneously (`src/trix/views/piece_view.coffee:78-92`).
- Publish workflow to the Jimdo npm registry (README, `.npmrc`).

The last upstream-style release bump is `0.12.0` (`package.json:3`). Upstream
Trix has since moved to v2.x with a different toolchain — those changes have
**not** been ported here.

## Tech stack

| Area | Choice |
|---|---|
| Language | CoffeeScript 1.9.1 (`Gemfile:6`) |
| Styling | Sass (`Gemfile:9`) |
| Asset pipeline | Sprockets 3.6.0 (`Gemfile:4`; pinned because 3.7+ strips banners) |
| Test runner | Blade 0.7.x (`Gemfile:13`), with SauceLabs for cross-browser (`Gemfile:14`) |
| Minifier | Uglifier (`Gemfile:10`) |
| SVG optimization | svgo via `sprockets-svgo` (`Gemfile:5`) and the npm `svgo` devDep (`package.json:24`) |
| Runner | Rack + Blade Rack adapter for dev (`config.ru`) |
| CI | Travis (`.travis.yml`) |
| Custom elements | Native `<trix-editor>` / `<trix-toolbar>` with a v0 polyfill fallback (`polyfills/custom-elements-v0-fallback.js.erb`) |

The toolchain is Ruby-centric. There is no webpack, no rollup, no tsc, no
babel, no jest. The only node dependency is `svgo`. Everything happens through
Sprockets compiling CoffeeScript/Sass/ERB sources into a single JS bundle.

## Output artifacts

`bin/blade build` produces three files in `dist/`:

| File | Contents |
|---|---|
| `dist/trix.js` | Full bundle: Trix + polyfills (custom elements v0, Set, etc.). This is what most consumers use. |
| `dist/trix-core.js` | Trix without polyfills. Use if you already ship polyfills yourself. |
| `dist/trix.css` | Default toolbar styles and `.trix-content` stored-content styles. |

`dist/` is checked into the repo and updated at release time by
`bin/release` (`bin/release:31, 131`).

## Entry points

- `assets/trix.coffee` → includes polyfills, then `src/trix/index.coffee.erb`.
- `assets/trix-core.coffee` → same, minus polyfills.
- `src/trix/index.coffee.erb` → the real source root: imports `core`,
`config`, and the two custom elements.
- `src/trix/elements/trix_editor_element.coffee:8` → registers the
`<trix-editor>` custom element.
- `src/trix/elements/trix_toolbar_element.coffee` → registers the
`<trix-toolbar>` custom element.

## Target browsers

Per `.blade.yml`, the test matrix includes Chrome, Firefox, Safari, Edge 16,
IE 11, iPhone (iOS 9.3–11.2), Android 4.4–6.0. A real port today would drop
IE 11 and old iOS/Android, but the current code still has polyfills and
branches for them — do not remove casually.

## Age / risk note

The repo has been untouched for years. Before making any change, expect:

- Ruby 2.x era dependencies. The pinned Sprockets (3.6.0) and coffee-script
(1.9.1) need rbenv + an old Ruby.
- Gemfile.lock may need regeneration against a modern bundler, but the
**versions must not float** — the Sprockets pin is load-bearing.
- `bin/release` assumes `github.user` and `github.token` in global git
config and an old `github_api` gem; for the Jimdo publish flow, only
steps 1–4 and the `npm publish` tail matter (see
[02-build-and-test.md](02-build-and-test.md)).
Loading