of startContainer offset: 1 165 | // And, subsequently, nodeEndOffset was node.childNodes.length 166 | // and deleting everything in startContainer 167 | // 168 | continue; 169 | for(let i=nodeEndOffset-1;i>=nodeStartOfffset;i--){ 170 | node.removeChild(node.childNodes[i]); 171 | } 172 | } 173 | else { 174 | // is TextNode 175 | node.deleteData(nodeStartOfffset, nodeEndOffset - nodeStartOfffset); 176 | } 177 | } 178 | 179 | let commonStopNode = commonParentElement.contains(editableHost) 180 | ? editableHost 181 | : commonParentElement 182 | ; 183 | let [noneEmptyStartAncestor, noneEmptyStartAncestorOffset] = _clearEmptyAncestorsUntil(startContainer, commonStopNode); 184 | _clearEmptyAncestorsUntil(endContainer, commonStopNode); 185 | 186 | // merging 187 | if( // both still in DOM 188 | startAncestorSibling.parentNode && endAncestorSibling.parentNode 189 | // no need to merge otherwise 190 | && startAncestorSibling !== endAncestorSibling 191 | // No need to merge textNodes, as we have element.normalize 192 | && startAncestorSibling.nodeType === Node.ELEMENT_NODE 193 | && endAncestorSibling.nodeType === Node.ELEMENT_NODE 194 | // And both are "compatible" The following should/could be a check 195 | // injected into this function, as compatibility depends on 196 | // content semantics. 197 | // 198 | // In this case, we want to merge paragaphs in order to remove 199 | // the line break withing the range... 200 | && startAncestorSibling.tagName === 'P' 201 | && endAncestorSibling.tagName === 'P') { 202 | // Can use Element API, because we checked nodeType. 203 | startAncestorSibling.append(...endAncestorSibling.childNodes); 204 | endAncestorSibling.remove(); 205 | } 206 | // could keep for cursor position, but this normalizes between FireFox/Chromium. 207 | if(!startAncestorSibling.childNodes.length && startAncestorSibling !== editableHost){ 208 | startAncestorSibling.remove(); 209 | return [commonParentElement, startAncestorSiblingOffset]; 210 | } 211 | if(startContainer.parentNode) { 212 | // startContainer is still there! 213 | return [startContainer, startOffset]; 214 | } 215 | if(noneEmptyStartAncestor && noneEmptyStartAncestor.parentNode){ 216 | return [noneEmptyStartAncestor, noneEmptyStartAncestorOffset]; 217 | } 218 | return [startAncestorSibling, startAncestorSiblingOffset]; 219 | } 220 | 221 | 222 | function _deleteRanges(domTool, editableHost, ranges) { 223 | // In reverse order, so previous ranges stay valid. 224 | // ASSERT: ranges don't overlap 225 | // ASSERT: ranges are not sorted 226 | // because ranges don't overlap, they can be sorted by means of 227 | // their startContainer position 228 | ranges.sort(_compareByStartContainerPosition); 229 | let lastResult; 230 | for(let i=ranges.length-1;i>=0;i--) 231 | lastResult = _deleteRange(domTool, ranges[i]); 232 | return lastResult; 233 | } 234 | 235 | /** 236 | * Includes node as first element in the returned array 237 | */ 238 | function _getNextSiblings(node) { 239 | let siblings = []; 240 | while(node) { 241 | siblings.push(node); 242 | node = node.nextSibling; 243 | } 244 | return siblings; 245 | } 246 | 247 | /** 248 | * That's the most comprehensive explanation of how InputEvents 249 | * are constituted: 250 | * https://w3c.github.io/input-events/#interface-InputEvent 251 | */ 252 | export function handleEditableDiv(domTool, event) { 253 | // This destroys the native undo/redo mechanism. 254 | // E.g. ctrl+z, ctrl+y don't do anything anymore and "historyUndo", 255 | // "historyRedo" don't appear in here anymore; However, that stuff is 256 | // not scriptable/available as API, so we can hardly do anything about 257 | // it. Instead, the overall app state could be used for history management. 258 | // 259 | // Using https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand 260 | // would actually keep that history management feature working, but 261 | // it is deprecated and a peculiar API anyways. 262 | event.preventDefault(); 263 | 264 | const { Range, Node } = domTool.window 265 | , editableHost = event.target.closest('[contenteditable]') 266 | ; 267 | let textContent; 268 | 269 | // In contenteditable, these use event.dataTransfer 270 | // and event.getTargetRanges() returns a "Non-empty Array" 271 | // "insertFromPaste", "insertFromPasteAsQuotation", "insertFromDrop", "insertReplacementText", "insertFromYank" 272 | 273 | // In contenteditable, these use event.data 274 | // and event.getTargetRanges() returns a "Non-emp ty Array" 275 | // "insertText", "insertCompositionText", "formatSetBlockTextDirection", "formatSetInlineTextDirection", "formatBackColor", "formatFontColor", "formatFontName", "insertLink" 276 | 277 | const hasDataTransfer = new Set(["insertFromPaste", "insertFromPasteAsQuotation", "insertFromDrop", "insertReplacementText", "insertFromYank"]) 278 | , hasData = new Set(["insertText", "insertCompositionText", "formatSetBlockTextDirection", "formatSetInlineTextDirection", "formatBackColor", "formatFontColor", "formatFontName", "insertLink"]) 279 | ; 280 | 281 | if(hasDataTransfer.has(event.inputType)) { 282 | textContent = event.dataTransfer.getData('text/plain'); 283 | } 284 | else if(hasData.has(event.inputType)) { 285 | // These are going to insert only text anyways, should be a bit 286 | // simpler to handle ... 287 | textContent = event.data; 288 | } 289 | else if(event.inputType === 'insertLineBreak') 290 | textContent = '\n'; 291 | 292 | 293 | // In contenteditable and plain text input fields: 294 | // event.dataTransfer === event.data === null 295 | // and event.getTargetRanges() returns an "Empty Array" 296 | // "historyUndo", "historyRedo" 297 | 298 | // In contenteditable and plain text input fields: 299 | // event.dataTransfer === event.data === null 300 | // and event.getTargetRanges() returns an "Non-empty Array" 301 | // All Remaining ??? 302 | 303 | let selection = event.target.ownerDocument.getSelection() 304 | , staticRanges = event.getTargetRanges() 305 | ; 306 | 307 | // Firefox will happily delete multiple ranges, // "historyUndo", "historyRedo" 308 | 309 | // but, it will collapse to the end of the first Range, while 310 | // it now collapses to the end of the last range. Inserting is then 311 | // done at the first range cursor. This seems to be not and 312 | // issue, can keep as it or change later. 313 | 314 | const liveRanges = []; 315 | for(let staticRange of staticRanges) { 316 | let liveRange = new Range() 317 | , [startContainer, startOffset] = editableHost.contains(staticRange.startContainer) 318 | ? [staticRange.startContainer, staticRange.startOffset] 319 | : [editableHost.firstChild, 0] 320 | , [endContainer, endOffset] = editableHost.contains(staticRange.endContainer) 321 | ? [staticRange.endContainer, staticRange.endOffset] 322 | : [editableHost.lastChild, editableHost.childNodes.length] 323 | ; 324 | liveRange.setStart(startContainer, startOffset); 325 | liveRange.setEnd(endContainer, endOffset); 326 | liveRanges.push(liveRange); 327 | } 328 | 329 | // delete 330 | let [startContainerAfterDelete, startOffsetAfterDelete] = _deleteRanges(domTool, editableHost, liveRanges) 331 | , rangeAfterDelete = new Range() 332 | ; 333 | // Set the cursor to the position from where to insert next. 334 | rangeAfterDelete.setStart(startContainerAfterDelete, startOffsetAfterDelete); 335 | rangeAfterDelete.collapse(); 336 | selection.removeAllRanges(); 337 | 338 | selection.addRange(rangeAfterDelete); 339 | 340 | // and insert 341 | if(event.inputType === 'insertParagraph') { 342 | let cursor = selection.getRangeAt(0) 343 | , startContainer = cursor.startContainer 344 | , startOffset = cursor.startOffset 345 | , firstInsertedNode, lastInsertedNode 346 | ; 347 | 348 | while(true) { 349 | if(startContainer.nodeType === Node.TEXT_NODE) { 350 | // startOffset references a position in the Text 351 | lastInsertedNode = startContainer.splitText(startOffset); 352 | if(!firstInsertedNode) 353 | firstInsertedNode = lastInsertedNode; 354 | startContainer = startContainer.parentElement; 355 | startOffset = [...startContainer.childNodes].indexOf(lastInsertedNode); 356 | continue; 357 | } 358 | if(startContainer.nodeType !== Node.ELEMENT_NODE) 359 | // It's also a failed assertion! 360 | throw new Error(`ASSERTION FAILED/NOT SUPPORTED insertParagraph into ` 361 | +`${startContainer.nodeName}/${startContainer.nodeType}`); 362 | // startContainer is an Element 363 | if(startContainer !== editableHost) { 364 | // If it is a
it should be a direct child of editableHost, 365 | // but we don't assert that! It could also be a span within a 366 | // p or even be nested further down within other inline elements. 367 | 368 | lastInsertedNode = startContainer.cloneNode(false); 369 | if(!firstInsertedNode) 370 | firstInsertedNode = lastInsertedNode; 371 | // split it at startOffset 372 | const children = _getNextSiblings(startContainer.childNodes[startOffset]); 373 | lastInsertedNode.append(...children); 374 | startContainer.after(lastInsertedNode); 375 | startOffset = [...startContainer.childNodes].indexOf(lastInsertedNode); 376 | startContainer = startContainer.parentElement; 377 | continue; 378 | } 379 | 380 | // startContainer === editableHost 381 | if(!lastInsertedNode) { 382 | let newP = domTool.createElement('p'); 383 | newP.append(''); 384 | 385 | // insert newP at startOffset 386 | if(startContainer.childNodes[startOffset]) 387 | domTool.insertAfter(newP, startContainer.childNodes[startOffset]); 388 | else 389 | startContainer.insertBefore(newP, null); 390 | 391 | firstInsertedNode = newP; 392 | } 393 | else if(lastInsertedNode.nodeType !== Node.ELEMENT_NODE || lastInsertedNode.tagName !== 'P') { 394 | let newP = domTool.createElement('p') 395 | , children = _getNextSiblings(lastInsertedNode) 396 | , indexOfFirstP = children.findIndex(element => element.tagName === 'P') 397 | ; 398 | if(indexOfFirstP !== -1) 399 | children = children.slice(0, indexOfFirstP); 400 | 401 | startContainer.replaceChild(newP, lastInsertedNode); 402 | newP.append(...children); 403 | } 404 | //else: all good already 405 | 406 | cursor.setStart(firstInsertedNode, 0); 407 | break; 408 | } 409 | } 410 | else if(textContent) { 411 | let cursor = selection.getRangeAt(0) 412 | , newContentsFragement = domTool.createTextNode(textContent) 413 | ; 414 | // TODO: 415 | // * make sure cursor is withing the contenteditable (let's take this as granted) 416 | // * maybe, e.g. on paste, insert multiple paragraphs in an orderly 417 | // fashion. 418 | 419 | // If the cursor is not within a
now, we should move it into one. 420 | // the easiest is to just to `Range.surroundContents(aNewP);` and later 421 | // clean up possible empty remaining
, as we have to do so anyways. 422 | // 423 | // This test works if the structure in editableHost is only flat 424 | //
elements and nothing else... 425 | let cursorElement = cursor.startContainer.nodeType === Node.ELEMENT_NODE 426 | ? cursor.startContainer 427 | : cursor.startContainer.parentElement 428 | ; 429 | 430 | if(!cursorElement.closest('[contenteditable] p')) { 431 | let newP = domTool.createElement('p'); 432 | cursor.surroundContents(newP); 433 | if(!newP.childNodes.length) { 434 | let newText = domTool.createTextNode(''); 435 | newP.append(newText); 436 | } 437 | cursor.setEnd(newP.lastChild, newP.lastChild.textContent.length); 438 | cursor.collapse(); 439 | } 440 | // cursor is now within a
!
441 | let parentElement = cursor.endContainer.parentElement
442 | , endOffset = cursor.endOffset
443 | ;
444 | cursor.insertNode(newContentsFragement);
445 | // Extra effort to move the cursor after the insertion for
446 | // iOS: see issue https://github.com/FontBureau/videoproof/issues/29
447 | // "Text is Inputting backwards"
448 | parentElement.normalize();
449 | cursor.setEnd(parentElement.firstChild, endOffset+textContent.length);
450 | selection.removeAllRanges();
451 | selection.addRange(cursor);
452 | }
453 | // use selection.collapseToStart() and experience the cursor moving
454 | // in the wrong direction when typing!
455 | // It's interesting, on iOS the cursor still moves backwards, on
456 | // entering text.
457 | selection.collapseToEnd();
458 | }
459 |
460 | export function handleEditableLine(domTool, event) {
461 | // This destroys the native undo/redo mechanism.
462 | // E.g. ctrl+z, ctrl+y don't do anything anymore and "historyUndo",
463 | // "historyRedo" don't appear in here anymore; However, that stuff is
464 | // not scriptable/available as API, so we can hardly do anything about
465 | // it. Instead, the overall app state could be used for history management.
466 | //
467 | // Using https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
468 | // would actually keep that history management feature working, but
469 | // it is deprecated and a peculiar API anyways.
470 | event.preventDefault();
471 |
472 | const { Range } = domTool.window
473 | , editableHost = event.target.closest('[contenteditable]')
474 | ;
475 | let textContent;
476 |
477 | // In contenteditable, these use event.dataTransfer
478 | // and event.getTargetRanges() returns a "Non-empty Array"
479 | // "insertFromPaste", "insertFromPasteAsQuotation", "insertFromDrop", "insertReplacementText", "insertFromYank"
480 |
481 | // In contenteditable, these use event.data
482 | // and event.getTargetRanges() returns a "Non-emp ty Array"
483 | // "insertText", "insertCompositionText", "formatSetBlockTextDirection", "formatSetInlineTextDirection", "formatBackColor", "formatFontColor", "formatFontName", "insertLink"
484 |
485 | const hasDataTransfer = new Set(["insertFromPaste", "insertFromPasteAsQuotation", "insertFromDrop", "insertReplacementText", "insertFromYank"])
486 | , hasData = new Set(["insertText", "insertCompositionText", "formatSetBlockTextDirection", "formatSetInlineTextDirection", "formatBackColor", "formatFontColor", "formatFontName", "insertLink"])
487 | ;
488 |
489 | if(hasDataTransfer.has(event.inputType)) {
490 | textContent = event.dataTransfer.getData('text/plain');
491 | }
492 | else if(hasData.has(event.inputType)) {
493 | // These are going to insert only text anyways, should be a bit
494 | // simpler to handle ...
495 | textContent = event.data;
496 | }
497 | else if(['insertLineBreak', 'insertParagraph'].includes(event.inputType))
498 | textContent = '\n';
499 |
500 | // In contenteditable and plain text input fields:
501 | // event.dataTransfer === event.data === null
502 | // and event.getTargetRanges() returns an "Empty Array"
503 | // "historyUndo", "historyRedo"
504 |
505 | // In contenteditable and plain text input fields:
506 | // event.dataTransfer === event.data === null
507 | // and event.getTargetRanges() returns an "Non-empty Array"
508 | // All Remaining ???
509 |
510 | let selection = event.target.ownerDocument.getSelection()
511 | , staticRanges = event.getTargetRanges()
512 | ;
513 |
514 | // Firefox will happily delete multiple ranges, // "historyUndo", "historyRedo"
515 |
516 | // but, it will collapse to the end of the first Range, while
517 | // it now collapses to the end of the last range. Inserting is then
518 | // done at the first range cursor. This seems to be not and
519 | // issue, can keep as it or change later.
520 |
521 | const liveRanges = [];
522 | for(let staticRange of staticRanges) {
523 | let liveRange = new Range()
524 | , [startContainer, startOffset] = editableHost.contains(staticRange.startContainer)
525 | ? [staticRange.startContainer, staticRange.startOffset]
526 | : [editableHost.firstChild, 0]
527 | , [endContainer, endOffset] = editableHost.contains(staticRange.endContainer)
528 | ? [staticRange.endContainer, staticRange.endOffset]
529 | : [editableHost.lastChild, editableHost.childNodes.length]
530 | ;
531 | liveRange.setStart(startContainer, startOffset);
532 | liveRange.setEnd(endContainer, endOffset);
533 | liveRanges.push(liveRange);
534 | }
535 |
536 | // delete
537 | let [startContainerAfterDelete, startOffsetAfterDelete] = _deleteRanges(domTool, editableHost, liveRanges)
538 | , rangeAfterDelete = new Range()
539 | ;
540 | // Set the cursor to the position from where to insert next.
541 | rangeAfterDelete.setStart(startContainerAfterDelete, startOffsetAfterDelete);
542 | rangeAfterDelete.collapse();
543 | selection.removeAllRanges();
544 |
545 | selection.addRange(rangeAfterDelete);
546 |
547 | if(textContent) {
548 | let cursor = selection.getRangeAt(0)
549 | , newContentsFragement = domTool.createTextNode(textContent)
550 | , parentElement = cursor.endContainer.parentElement
551 | , endOffset = cursor.endOffset
552 | ;
553 | // Extra effort to move the cursor after the insertion for
554 | // iOS: see issue https://github.com/FontBureau/videoproof/issues/29
555 | // "Text is Inputting backwards"
556 | cursor.insertNode(newContentsFragement);
557 | parentElement.normalize();
558 | // This failed sometimes, had an out og range error,
559 | // but even adding the Math.min didn't help.
560 | // the cursor.collapse(false) however works so far, I'm
561 | // leaving the failing line in case another issue comes up.
562 | // cursor.setEnd(parentElement.firstChild, Math.min(endOffset+textContent.length, parentElement.firstChild.textContent.length));
563 | cursor.collapse(false);
564 | selection.removeAllRanges();
565 | selection.addRange(cursor);
566 | }
567 | // use selection.collapseToStart() and experience the cursor moving
568 | // in the wrong direction when typing!
569 | // It's interesting, on iOS the cursor still moves backwards, on
570 | // entering text.
571 | selection.collapseToEnd();
572 | }
573 |
574 |
575 |
--------------------------------------------------------------------------------
/lib/js/diff_match_patch/build.sh:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env bash
2 |
3 | # Create an JavaScript/ECMAScript Module from the plain JavaScript file.
4 | # This requires the original diff_match_patch as a git submodule (or copy)
5 | # in the root directory:
6 | # $ git submodule add https://github.com/google/diff-match-patch.git diff-match-patch
7 |
8 | # There's a line:
9 | # '// CLOSURE:begin_strip'
10 | # After that line definitons to the global namespace are attempted.
11 | # Exclude everything after that line.
12 | #
13 | # These lines:
14 | # 'var DIFF_DELETE = -1;'
15 | # 'var DIFF_INSERT = 1;'
16 | # 'var DIFF_EQUAL = 0;'
17 | # Should become:
18 | # 'export const DIFF_DELETE = -1;'
19 | # 'export const DIFF_INSERT = 1;'
20 | # 'export const DIFF_EQUAL = 0;'
21 | # Thus find 'var DIFF_' and replace with 'export const DIFF_'.
22 | #
23 | # Finally add the line
24 | # 'export default diff_match_patch;'
25 |
26 |
27 |
28 | SOURCE=../../../diff-match-patch/javascript/diff_match_patch_uncompressed.js;
29 | TARGET=diff_match_patch.mjs;
30 |
31 | cat $SOURCE | \
32 | awk '
33 | BEGIN { print "// CAUTION - DONT EDIT: this file has been created automatically!" }
34 | # exit when this line occurs
35 | /^\/\/ CLOSURE:begin_strip$/ {exit}
36 | # find and replace:
37 | { sub(/^var DIFF_/,"export const DIFF_"); print }
38 | # finally add default export
39 | END { print "export default diff_match_patch;" }
40 | ' > $TARGET;
41 |
42 |
--------------------------------------------------------------------------------
/lib/js/domTool.mjs:
--------------------------------------------------------------------------------
1 | /* jshint browser: true, esversion: 7, laxcomma: true, laxbreak: true */
2 |
3 | // I took this from googlefonts/fontbakery-dashboard and made it into a es module.
4 | // `document` is not required/expected to be global anymore, it's injected
5 |
6 | // skip markdown parsing for now
7 | // import marked from 'marked.mjs';
8 | function marked(){
9 | throw new Error('Markdown parser `marked` is not available');
10 | }
11 |
12 | function ValueError(message) {
13 | this.name = 'ValueError';
14 | this.message = message || '(No message for ValueError)';
15 | this.stack = (new Error()).stack;
16 | }
17 | ValueError.prototype = Object.create(Error.prototype);
18 | ValueError.prototype.constructor = ValueError;
19 |
20 | export default class DOMTool {
21 | constructor(document){
22 | this.document = document;
23 | }
24 |
25 | get window(){
26 | return this.document.defaultView; // fancy way to do this
27 | }
28 |
29 | get documentElement() {
30 | return this.document.documentElement; // fancy way to do this
31 | }
32 |
33 | static appendChildren(elem, contents, cloneChildNodes) {
34 | var _contents = (contents === undefined || contents === null)
35 | ? []
36 | : (contents instanceof Array ? contents : [contents])
37 | ;
38 | for(let child of _contents) {
39 | if(!child || typeof child.nodeType !== 'number')
40 | child = DOMTool.createTextNode(elem.ownerDocument, child);
41 | else if(cloneChildNodes)
42 | child = child.cloneNode(true);//always a deep clone
43 | elem.appendChild(child);
44 | }
45 | }
46 |
47 | static createTextNode(document, text) {
48 | return document.createTextNode(text);
49 | }
50 |
51 | createTextNode(text) {
52 | return DOMTool.createTextNode(this.document, text);
53 | }
54 |
55 | static createElement(document, tagname, attr, contents, cloneChildNodes) {
56 | var elem = attr && 'xmlns' in attr
57 | ? document.createElementNS(attr.xmlns, tagname)
58 | : document.createElement(tagname)
59 | ;
60 |
61 | if(attr) for(let k in attr) {
62 | if(k === 'xmlns')
63 | continue;
64 | elem.setAttribute(k, attr[k]);
65 | }
66 |
67 | DOMTool.appendChildren(elem, contents, cloneChildNodes);
68 | return elem;
69 | }
70 |
71 | createElement(tagname, attr, contents, cloneChildNodes) {
72 | return DOMTool.createElement(this.document, tagname, attr,
73 | contents, cloneChildNodes);
74 | }
75 |
76 | static createChildElement(parent, tagname, attr, contents, cloneChildNodes) {
77 | var elem = DOMTool.createElement(parent.ownerDocument, tagname, attr, contents, cloneChildNodes);
78 | parent.appendChild(elem);
79 | return elem;
80 | }
81 |
82 | createChildElement(parent, tagname, attr, contents, cloneChildNodes){
83 | return DOMTool.createChildElement(parent, tagname, attr, contents, cloneChildNodes);
84 | }
85 |
86 | static createElementFromHTML(document, tag, attr, innerHTMl) {
87 | var frag = DOMTool.createFragmentFromHTML(document, innerHTMl);
88 | return DOMTool.createElement(document, tag, attr, frag);
89 | }
90 |
91 | static createElementfromHTML(...args) {
92 | return this.createElementFromHTML(...args);
93 | }
94 |
95 | createElementFromHTML(tag, attr, innerHTMl) {
96 | return DOMTool.createElementFromHTML(this.document, tag, attr, innerHTMl);
97 | }
98 |
99 | createElementfromHTML(...args) {
100 | return this.createElementFromHTML(...args);
101 | }
102 |
103 | static createElementFromMarkdown(document, tag, attr, markdownText) {
104 | return DOMTool.createElementfromHTML(document, tag, attr, marked(markdownText, {gfm: true}));
105 | }
106 |
107 | static createElementfromMarkdown(...args) {
108 | return this.createElementfromMarkdown(...args);
109 | }
110 |
111 | createElementFromMarkdown(document, tag, attr, markdownText) {
112 | return DOMTool.createElementfromMarkdown(this.document, tag, attr, markdownText);
113 | }
114 |
115 | createElementfromMarkdown(...args) {
116 | return this.createElementFromMarkdown(...args);
117 | }
118 |
119 | static createFragmentFromMarkdown(document, mardownText) {
120 | return DOMTool.createFragmentFromHTML(document, marked(mardownText, {gfm: true}));
121 | }
122 |
123 | createFragmentFromMarkdown(mardownText) {
124 | return DOMTool.createFragmentFromMarkdown(this.document, mardownText);
125 | }
126 |
127 | static appendHTML(document, elem, html) {
128 | var frag = DOMTool.createFragmentFromHTML(document, html);
129 | elem.appendChild(frag);
130 | }
131 | appendHTML(elem, html) {
132 | DOMTool.appendHTML(this.document, elem, html);
133 | }
134 |
135 | static appendMarkdown(document, elem, markdown) {
136 | DOMTool.appendHTML(document, elem, marked(markdown, {gfm: true}));
137 | }
138 |
139 | appendMarkdown(elem, markdown) {
140 | return DOMTool.appendMarkdown(this.document, elem, markdown);
141 | }
142 |
143 | static createFragmentFromHTML(document, html) {
144 | return document.createRange().createContextualFragment(html);
145 | }
146 |
147 | createFragmentFromHTML(html) {
148 | return DOMTool.createFragmentFromHTML(this.document, html);
149 | }
150 |
151 | static createFragment(document, contents, cloneChildNodes) {
152 | var frag = document.createDocumentFragment();
153 | DOMTool.appendChildren(frag, contents, cloneChildNodes);
154 | return frag;
155 | }
156 |
157 | createFragment(contents, cloneChildNodes) {
158 | return DOMTool.createFragment(this.document, contents, cloneChildNodes);
159 | }
160 |
161 | static createComment(document, text) {
162 | return document.createComment(text);
163 | }
164 |
165 | createComment(document, text){
166 | return DOMTool.createComment(this.document, text);
167 | }
168 |
169 |
170 | static isDOMElement(node) {
171 | return node && node.nodeType && node.nodeType === 1;
172 | }
173 |
174 | static replaceNode(newNode, oldNode) {
175 | if(oldNode.parentNode) // replace has no effect if oldNode has no place
176 | oldNode.parentNode.replaceChild(newNode, oldNode);
177 | }
178 |
179 | static removeNode(node) {
180 | if(node.parentNode)
181 | node.parentNode.removeChild(node);
182 | }
183 |
184 | static insertBefore(newElement, referenceElement) {
185 | if(referenceElement.parentElement && newElement !== referenceElement)
186 | referenceElement.parentElement.insertBefore(newElement
187 | , referenceElement);
188 | }
189 |
190 | static insertAfter(newElement, referenceElement) {
191 | // there is no element.insertAfter() in the DOM
192 | if(!referenceElement.nextSibling)
193 | referenceElement.parentElement.appendChild(newElement);
194 | else
195 | DOMTool.insertBefore(newElement, referenceElement.nextSibling);
196 | }
197 |
198 | static insert(element, position, child) {
199 | if(typeof child === 'string')
200 | child = DOMTool.createTextNode(element.ownerDocument, child);
201 | switch(position) {
202 | case 'append':
203 | element.appendChild(child);
204 | break;
205 | case 'prepend':
206 | if(element.firstChild)
207 | DOMTool.insertBefore(child, element.firstChild);
208 | else
209 | element.appendChild(child);
210 | break;
211 | case 'before':
212 | DOMTool.insertBefore(child, element);
213 | break;
214 | case 'after':
215 | DOMTool.insertAfter(child, element);
216 | break;
217 | default:
218 | throw new ValueError('Unknown position keyword "'+position+'".');
219 | }
220 | }
221 |
222 | static getChildElementForSelector(element, selector, deep) {
223 | var elements = Array.prototype.slice
224 | .call(element.querySelectorAll(selector));
225 | if(!deep)
226 | // I don't know an easier way to only allow
227 | // direct children.
228 | elements = elements.filter(elem=>elem.parentNode === element);
229 | return elements[0] || null;
230 | }
231 |
232 | static getMarkerComment(element, marker) {
233 | var frames = [[element && element.childNodes, 0]]
234 | , frame, nodelist, i, l, childNode
235 | ;
236 | main:
237 | while((frame = frames.pop()) !== undefined){
238 | nodelist = frame[0];
239 | for(i=frame[1],l=nodelist.length;i
tags. To achieve cleaner
90 | // contents, I prefer pre-wrap, but it could be turned off when
91 | // text-align is justify. Also, when we'll apply varla-varfo
92 | // parametric font justifiction, all this will become much more
93 | // complicated anyways!
94 | , 'j': 'justify'
95 | };
96 |
97 | function _propertyValueToCSSValue(key, value) {
98 | if(key === 'alignment')
99 | return value in ALIGNMENT2CSS
100 | ? ALIGNMENT2CSS[value]
101 | : value
102 | ;
103 | return value;
104 | }
105 |
106 |
107 | function _setProperty(elem, key, value) {
108 | const propertyName = _keyToProperty(key);
109 | let propertyValue;
110 | if(value === null){
111 | propertyValue = null;
112 | elem.style.removeProperty(propertyName);
113 | }
114 | else {
115 | propertyValue = _propertyValueToCSSValue(key, value);
116 | elem.style.setProperty(propertyName, propertyValue);
117 | }
118 | return [propertyName, propertyValue];
119 | }
120 |
121 | function keyFromAxisTag(axisTag) {
122 | return `font-var-${axisTag}`;
123 | }
124 |
125 | function applyPropertyChanges(contentElement, fontSize, manualAxisLocations, gridDimensionControls, variationSettingsFlags) {
126 | const applyDefaultsExplicitly = variationSettingsFlags.has('applyDefaultsExplicitly')
127 | , locations = _filterLocations(applyDefaultsExplicitly, manualAxisLocations)
128 | .map(([axisTag, {location, autoOPSZ}])=>[axisTag, location, autoOPSZ])
129 | , varProperties = []
130 | , gridDimensionAxisTags = Object.values(gridDimensionControls)
131 | .filter(({axisTag})=>axisTag!=='disabled')
132 | .map(({axisTag})=>axisTag)
133 | , seen = new Set()
134 | ;
135 |
136 | for(const [axisTag, location, autoOPSZ] of locations) {
137 | const key = keyFromAxisTag(axisTag);
138 | let propertyValue = gridDimensionAxisTags.includes(axisTag)
139 | ? null
140 | : location
141 | ;
142 | if(axisTag === 'opsz' && autoOPSZ) {
143 | // Don't set property, fallback to --font-size (FONT_SIZE_CUSTOM_PROPERTY) directly
144 | propertyValue = null; // deletes/removes
145 | }
146 | seen.add(axisTag);
147 | const [propertyName, ] = _setProperty(contentElement, key, propertyValue);
148 | varProperties.push([axisTag, propertyName, location, autoOPSZ]);
149 | }
150 |
151 | for(const {axisTag} of Object.values(gridDimensionControls)) {
152 | if(seen.has(axisTag))
153 | continue;
154 | if(['disabled', 'font-size'].includes(axisTag))
155 | continue;
156 |
157 | const key = keyFromAxisTag(axisTag)
158 | , [propertyName, ] = _setProperty(contentElement, key, null)
159 | ;
160 | varProperties.push([axisTag, propertyName]);
161 | }
162 |
163 | contentElement.style.setProperty(FONT_SIZE_CUSTOM_PROPERTY, fontSize);
164 | return varProperties;
165 | }
166 |
167 | function _parametersTextFromProperties(varProperties, gridDimensionControls) {
168 | const gridAxesTags = Object.values(gridDimensionControls).map(({axisTag})=>axisTag)
169 | , explicitFontSize = gridAxesTags.includes('font-size')
170 | ;
171 | let parametersText = varProperties
172 | .map(([axisTag, /*propertyName*/, location, autoOPSZ])=>{
173 | if(gridAxesTags.includes(axisTag))
174 | return `${axisTag} {${axisTag}}`;
175 |
176 | return axisTag === 'opsz' && autoOPSZ
177 | ? `${axisTag} (auto ${FONT_SIZE_PLACEHOLDER})`
178 | : `${axisTag} ${location}`;
179 | })
180 | .join(', ')
181 | ;
182 |
183 | if(explicitFontSize)
184 | parametersText += ` Font size: ${FONT_SIZE_PLACEHOLDER} pt`;
185 | return parametersText;
186 | }
187 |
188 | function _getParametersTextForFontSize(parametersText, proofFontSize, cellProperties) {
189 | const fontSize = 'font-size' in cellProperties
190 | ? cellProperties['font-size']
191 | : proofFontSize
192 | ;
193 | parametersText = parametersText.replaceAll(FONT_SIZE_PLACEHOLDER, fontSize);
194 | for(let [axisTag, value] of Object.entries(cellProperties))
195 | parametersText = parametersText.replaceAll(`{${axisTag}}`, value);
196 |
197 | return parametersText;
198 | }
199 |
200 | function _getParametersTextForCell(parametersText, cell) {
201 | const fontSize = cell.style.getPropertyValue(FONT_SIZE_CUSTOM_PROPERTY)
202 | , cellProperties = {}
203 | , propertyStart = '--font-var-'
204 | ;
205 | for(let propertyName of Array.from(cell.style)){
206 | if(!propertyName.startsWith(propertyStart))
207 | continue;
208 | const axisTag = propertyName.slice(propertyStart.length);
209 | cellProperties[axisTag] = cell.style.getPropertyValue(propertyName);
210 | }
211 | return _getParametersTextForFontSize(parametersText, fontSize, cellProperties);
212 | }
213 |
214 |
215 | function updateDisplayParameters(domTool, contentElement, varProperties, gridDimensionControls) {
216 | const parametersText = _parametersTextFromProperties(varProperties, gridDimensionControls);
217 | for(const cell of contentElement.querySelectorAll(CELL_SELECTOR)) {
218 | let parameters = cell.querySelector(SHOW_PARAMETERS_SELECTOR)
219 | , cellContent = cell.querySelector(CELL_CONTENT_SELECTOR)
220 | ;
221 | // Assert there's a parameters element!
222 | parameters.textContent = _getParametersTextForCell(parametersText, cellContent);
223 | }
224 | }
225 |
226 | function updateFontVariationsSettings(domTool, contentElement, varProperties) {
227 | const fontVariationSettings = _fontVariationSettingsFromProperties(varProperties);
228 | for(const cell of contentElement.querySelectorAll(CELL_CONTENT_SELECTOR))
229 | cell.style.setProperty('font-variation-settings', fontVariationSettings);
230 | }
231 |
232 | function makeCell(domTool, cellTextContent, proofFontSize, fontVariationSettings
233 | , parametersTextTemplate, variationSettingsFlags, cellProperties) {
234 |
235 | const parametersText = _getParametersTextForFontSize(parametersTextTemplate, proofFontSize, cellProperties)
236 | , cellContent = domTool.createElement('div',
237 | {'class': CELL_CONTENT_CLASS, title:parametersText}, cellTextContent)
238 | , cellElement = domTool.createElement('div', {'class': CELL_CLASS}, [
239 | domTool.createElement('span', {'class': TOGGLE_EDIT_CLASS, title: 'click here to edit line'})
240 | , cellContent
241 | ])
242 | ;
243 |
244 | if('font-size' in cellProperties)
245 | cellContent.style.setProperty(FONT_SIZE_CUSTOM_PROPERTY, `${cellProperties['font-size']}`);
246 | cellContent.style.setProperty('font-variation-settings', fontVariationSettings);
247 |
248 | for(const [axisTag, value] of Object.entries(cellProperties))
249 | _setProperty(cellContent, keyFromAxisTag(axisTag), value);
250 |
251 |
252 |
253 | if(variationSettingsFlags.has('displayParameters')) {
254 | // enable parameters
255 | const parameters = domTool.createElement(
256 | 'div', {'class': SHOW_PARAMETERS_CLASS}, parametersText);
257 | cellElement.append(parameters);
258 | }
259 | return cellElement;
260 | }
261 |
262 | function makeGrid(domTool, contentElement, userText, fontSize, alignment
263 | , gridDimensionControls, varProperties, variationSettingsFlags
264 | , manualFontLeading) {
265 | const cellTextContent = userText || DEFAULT_CONTENT
266 | , fontVariationSettings = _fontVariationSettingsFromProperties(varProperties)
267 | , parametersTextTemplate = _parametersTextFromProperties(varProperties, gridDimensionControls)
268 | , rows = []
269 | ;
270 | domTool.clear(contentElement);
271 |
272 | const {x, y} = gridDimensionControls
273 | , MAX_CELLS = 1000
274 | ;
275 | let i = 0;
276 | y:
277 | for(const yValue of gridDimensionGenerator(y)) {
278 | const cells = [];
279 | for(const xValue of gridDimensionGenerator(x)) {
280 | // both disabled or bot stepsSize === 0
281 | if(xValue === null && yValue === null) break y;
282 | if(i >= MAX_CELLS)
283 | break ;
284 | i++;
285 | cells.push(makeCell(domTool, cellTextContent, fontSize
286 | , fontVariationSettings, parametersTextTemplate, variationSettingsFlags
287 | , {[x.axisTag]: xValue, [y.axisTag]: yValue}));
288 | }
289 | rows.push(domTool.createElement('div', {'class': ROW_CLASS}, cells));
290 | if(i >= MAX_CELLS) {
291 | // Could add a a button to "load more ..." !
292 | rows.push(domTool.createElementFromHTML('div', {'class': 'to-many-cells'},
293 | `Cell creation aborted!
The maximum cell amount was created. `
294 | + `Too many cells can affect the website performance negatively`
295 | + ` (MAX_CELLS ${MAX_CELLS}).`));
296 | break;
297 | }
298 | }
299 | contentElement.style.setProperty(FONT_SIZE_CUSTOM_PROPERTY, `${fontSize}`);
300 | contentElement.style.setProperty('line-height', `${manualFontLeading}`);
301 | contentElement.append(...rows);
302 | }
303 |
304 | function _handleBeforeInput(domTool, event) {
305 | handleEditableLine(domTool, event);
306 | domTool.dispatchEvent(event.target, 'edited', {bubbles: true});
307 | }
308 |
309 | function _handleClick(domTool, e) {
310 | if(!e.target.matches(`${CELL_SELECTOR} ${TOGGLE_EDIT_SELECTOR}`))
311 | return;
312 | e.preventDefault();
313 | // Turning contentEditable on only when required has the advantage
314 | // that text selection works better, it's otherwise limited within
315 | // each contenteditable element and one cannot select accross elements.
316 | const line = e.target.closest(CELL_SELECTOR)
317 | , lineContent = line.querySelector(CELL_CONTENT_SELECTOR)
318 | ;
319 | lineContent.contentEditable = true;
320 | lineContent.focus();
321 | }
322 |
323 | function _handleDblclick(domTool, e) {
324 | if(!e.target.matches(`${CELL_SELECTOR} *`))
325 | return;
326 | e.preventDefault();
327 | // Turning contentEditable on only when required has the advantage
328 | // that text selection works better, it's otherwise limited within
329 | // each contenteditable element and one cannot select accross elements.
330 | const line = e.target.closest(CELL_SELECTOR)
331 | , lineContent = line.querySelector(CELL_CONTENT_SELECTOR)
332 | ;
333 | lineContent.contentEditable = true;
334 | lineContent.focus();
335 | }
336 |
337 | function _handleFocusOut(domTool, e) {
338 | if(!e.target.matches(CELL_CONTENT_SELECTOR))
339 | return;
340 | const lineContent = e.target;
341 | lineContent.contentEditable = false;
342 | }
343 |
344 | function updateTextContent(container, newText) {
345 | // Update all lines
346 | const cellContents = container.querySelectorAll(CELL_CONTENT_SELECTOR);
347 | for(const elem of cellContents) {
348 | if(elem.textContent === newText) continue;
349 | elem.textContent = newText;
350 | }
351 | }
352 |
353 | function _handleEdited(updateTextHandler, event) {
354 | // Update all lines
355 | const newText = event.target.textContent;
356 | updateTextHandler(newText);
357 | }
358 |
359 | export function init(proofElement, domTool, updateTextHandler, fontSize
360 | , manualAxisLocations, alignment, variationSettingsFlags
361 | , gridDimensionControls, userText, manualFontLeading) {
362 |
363 | console.log(`VARTOOLS_GRID init!`, variationSettingsFlags);
364 |
365 | const contentElement = domTool.createElement('div', {'class': GRID_CONTAINER_CLASS});
366 | proofElement.append(contentElement);
367 |
368 | const handleDestroy = (/*event*/)=>{
369 | for(const eventListener of eventListeners)
370 | proofElement.removeEventListener(...eventListener);
371 | }
372 | , eventListeners = [
373 | ['beforeinput', _handleBeforeInput.bind(null, domTool), false]
374 | , ['focusout', _handleFocusOut.bind(null, domTool), false]
375 | , ['edited', _handleEdited.bind(null, updateTextHandler), false]
376 | , ['click', _handleClick.bind(null, domTool), false]
377 | , ['dblclick', _handleDblclick.bind(null, domTool), false]
378 | , ['destroy', handleDestroy, false]
379 | ]
380 | ;
381 |
382 | for(const eventListener of eventListeners)
383 | proofElement.addEventListener(...eventListener);
384 |
385 |
386 | _setProperty(contentElement, 'alignment', alignment);
387 |
388 | const varProperties = applyPropertyChanges(contentElement, fontSize
389 | , manualAxisLocations, gridDimensionControls
390 | , variationSettingsFlags, userText);
391 | makeGrid(domTool, contentElement, userText, fontSize, alignment
392 | , gridDimensionControls, varProperties, variationSettingsFlags
393 | , manualFontLeading);
394 |
395 | return {
396 |
397 | // Update will run if the proof is not re-initalized anyways.
398 | update: (changedDependencyNamesSet, fontSize
399 | , manualAxisLocations, alignment, variationSettingsFlags
400 | , gridDimensionControls, userText, manualFontLeading)=>{
401 | _setProperty(contentElement, 'alignment', alignment);
402 | const varProperties = applyPropertyChanges(contentElement, fontSize, manualAxisLocations
403 | , gridDimensionControls, variationSettingsFlags);
404 |
405 |
406 | if(['fontName', 'variationSettingsFlags', 'gridDimensionControls'].some(k=>changedDependencyNamesSet.has(k))) {
407 | makeGrid(domTool, contentElement, userText, fontSize, alignment
408 | , gridDimensionControls, varProperties, variationSettingsFlags
409 | , manualFontLeading);
410 | return;
411 | }
412 | if(changedDependencyNamesSet.has('fontSizeGrid'))
413 | contentElement.style.setProperty(FONT_SIZE_CUSTOM_PROPERTY, `${fontSize}`);
414 |
415 | if(changedDependencyNamesSet.has('manualFontLeading'))
416 | contentElement.style.setProperty('line-height', `${manualFontLeading}`);
417 |
418 | if(changedDependencyNamesSet.has('userText') )
419 | updateTextContent(contentElement, userText);
420 | // What follows is covered in makeLines as well
421 | if(!variationSettingsFlags.has('applyDefaultsExplicitly')) {
422 | // When the default are not explicitly applied, font-variation-settings
423 | // must be updated when the variation properties change, as
424 | // they may contain more or less axis than before..
425 | updateFontVariationsSettings(domTool, contentElement, varProperties);
426 | }
427 | if(changedDependencyNamesSet.has('manualAxisLocations') && variationSettingsFlags.has('displayParameters')){
428 | updateDisplayParameters(domTool, contentElement, varProperties, gridDimensionControls);
429 | }
430 |
431 | }
432 | };
433 | }
434 |
435 |
--------------------------------------------------------------------------------
/lib/js/layouts/vartools-waterfall.mjs:
--------------------------------------------------------------------------------
1 | /* jshint browser: true, esversion: 9, laxcomma: true, laxbreak: true, unused:true, undef:true */
2 | import { handleEditableLine } from '../content-editable.mjs';
3 |
4 | // Duplicates from typespec!
5 | function _keyToProperty(key) {
6 | // 'HeLloWORLD'.replaceAll(/([A-Z])/g, (_, a)=> `-${a.toLowerCase()}`);
7 | // '-he-llo-w-o-r-l-d
8 |
9 | let property = key.startsWith('font-var-')
10 | ? key.toLowerCase()
11 | : key.replaceAll(/([A-Z])/g, (_, a)=> `-${a.toLowerCase()}`);
12 | return `--${property}`;
13 | }
14 |
15 | const ALIGNMENT2CSS = {
16 | 'l': 'left'
17 | , 'c': 'center'
18 | , 'r': 'right'
19 | // FIXME: "white-space: pre-wrap" on
20 | // #the-proof.typespec div[contenteditable] kills of justify!
21 | // In the legacy tool, "white-space: pre-wrap" was not used,
22 | // instead, the untampered content editable mechanisms would insert
23 | // non-breaking space characters and
tags. To achieve cleaner
24 | // contents, I prefer pre-wrap, but it could be turned off when
25 | // text-align is justify. Also, when we'll apply varla-varfo
26 | // parametric font justifiction, all this will become much more
27 | // complicated anyways!
28 | , 'j': 'justify'
29 | };
30 |
31 | function _propertyValueToCSSValue(key, value) {
32 | if(key === 'alignment')
33 | return value in ALIGNMENT2CSS
34 | ? ALIGNMENT2CSS[value]
35 | : value
36 | ;
37 | return value;
38 | }
39 |
40 | function _setProperty(elem, key, value) {
41 | const propertyName = _keyToProperty(key);
42 | let propertyValue;
43 | if(value === null){
44 | propertyValue = null;
45 | elem.style.removeProperty(propertyName);
46 | }
47 | else {
48 | propertyValue = _propertyValueToCSSValue(key, value);
49 | elem.style.setProperty(propertyName, propertyValue);
50 | }
51 | return [propertyName, propertyValue];
52 | }
53 |
54 | function _filterLocations(applyDefaultsExplicitly, state) {
55 | return state.filter(([axisTag, {location, 'default':def}])=>
56 | axisTag === 'opsz' // browsers set the wron default opsz, always apply explicitly
57 | || ( applyDefaultsExplicitly
58 | // Keep all
59 | ? true
60 | // Keep only locations that are not the default.
61 | : location !== def )
62 | );
63 | }
64 |
65 | function applyPropertyChanges(contentElement, manualAxisLocations, variationSettingsFlags) {
66 | const applyDefaultsExplicitly = variationSettingsFlags.has('applyDefaultsExplicitly')
67 | , locations = _filterLocations(applyDefaultsExplicitly, manualAxisLocations)
68 | .map(([axisTag, {location, autoOPSZ}])=>[axisTag, location, autoOPSZ])
69 | , varProperties = []
70 | ;
71 |
72 | for(const [axisTag, location, autoOPSZ] of locations) {
73 | const key = `font-var-${axisTag}`;
74 | let propertyValue = location;
75 | if(axisTag === 'opsz' && autoOPSZ) {
76 | // Don't set property, fallback to --font-size (FONT_SIZE_CUSTOM_PROPERTY) directly
77 | propertyValue = null; // deletes/removes
78 | }
79 | const [propertyName, ] = _setProperty(contentElement, key, propertyValue);
80 | varProperties.push([axisTag, propertyName, location, autoOPSZ]);
81 | }
82 |
83 | return varProperties;
84 | }
85 |
86 | const WATERFALL_CONTAINER_CLASS = 'waterfall-container'
87 | , LINE_CLASS = `waterfall-line`
88 | , LINE_CONTENT_CLASS = `waterfall-line_content`
89 | , TOGGLE_EDIT_CLASS = `waterfall-toggle_edit`
90 | , SHOW_PARAMETERS_CLASS = `waterfall-show_parameters`
91 | // Selectors for classes
92 | , LINE_SELECTOR = `.${LINE_CLASS}`
93 | , LINE_CONTENT_SELECTOR = `.${LINE_CONTENT_CLASS}`
94 | , TOGGLE_EDIT_SELECTOR = `.${TOGGLE_EDIT_CLASS}`
95 | , SHOW_PARAMETERS_SELECTOR = `.${SHOW_PARAMETERS_CLASS}`
96 | // something completely different
97 | , FONT_SIZE_PLACEHOLDER = '{font-size}'
98 | , FONT_SIZE_CUSTOM_PROPERTY = '--font-size'
99 | , DEFAULT_CONTENT = 'ABCDEFGHIJKLOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'
100 | ;
101 |
102 | function _parametersTextFromProperties(varProperties) {
103 | return varProperties
104 | .map(([axisTag, /*propertyName*/, location, autoOPSZ])=>{
105 | return axisTag === 'opsz' && autoOPSZ
106 | ? `${axisTag} (auto ${FONT_SIZE_PLACEHOLDER})`
107 | : `${axisTag} ${location}`;
108 | })
109 | .join(', ');
110 | }
111 |
112 | function _getParametersTextForFontSize(parametersText, fontSize) {
113 | return parametersText.replace(FONT_SIZE_PLACEHOLDER, fontSize);
114 | }
115 |
116 | function _getParametersTextForLine(parametersText, line) {
117 | const fontSize = line.style.getPropertyValue(FONT_SIZE_CUSTOM_PROPERTY);
118 | return _getParametersTextForFontSize(parametersText, fontSize);
119 | }
120 |
121 |
122 | function updateDisplayParameters(domTool, contentElement, varProperties) {
123 | const parametersText = _parametersTextFromProperties(varProperties)
124 | , lines = contentElement.querySelectorAll(LINE_SELECTOR)
125 | ;
126 | for(const line of lines) {
127 | let parameters = line.querySelector(SHOW_PARAMETERS_SELECTOR);
128 | // Assert there's a parameters element!
129 | parameters.textContent = _getParametersTextForLine(parametersText, line);
130 | }
131 | }
132 |
133 | function _fontVariationSettingsFromProperties(varProperties) {
134 | return varProperties
135 | .map(([axisTag, propertyName])=>{
136 | return axisTag === 'opsz'
137 | ? `"${axisTag}" var(${propertyName}, var(${FONT_SIZE_CUSTOM_PROPERTY}))`
138 | : `"${axisTag}" var(${propertyName}, 0)`
139 | ;
140 | }).join(', ');
141 | }
142 |
143 | function updateFontVariationsSettings(domTool, contentElement, varProperties) {
144 | const fontVariationSettings = _fontVariationSettingsFromProperties(varProperties)
145 | , lines = contentElement.querySelectorAll(LINE_SELECTOR)
146 | ;
147 | for(const line of lines)
148 | line.style.setProperty('font-variation-settings', fontVariationSettings);
149 | }
150 |
151 | function makeLines(domTool, contentElement, userText, fontSize, fontSizeTo, varProperties, variationSettingsFlags) {
152 | const waterfallContent = userText || DEFAULT_CONTENT
153 | , lines = []
154 | ;
155 | domTool.clear(contentElement);
156 |
157 | const fontVariationSettings = _fontVariationSettingsFromProperties(varProperties)
158 | , parametersText = variationSettingsFlags.has('displayParameters')
159 | ? _parametersTextFromProperties(varProperties)
160 | : ''
161 | , fromFontSize = parseInt(fontSize)
162 | , toFontSize = parseInt(fontSizeTo)
163 | , step = toFontSize < fromFontSize ? -1 : 1
164 | , endCondition = toFontSize < fromFontSize
165 | ? (i, l)=>i >= l
166 | : (i, l)=>i <= l
167 | ;
168 |
169 | for(let i=fromFontSize; endCondition(i, toFontSize); i+=step) {
170 | let line = domTool.createElement('div', {'class': LINE_CLASS}, [
171 | domTool.createElement('span', {'class': TOGGLE_EDIT_CLASS, title: 'click here to edit line'})
172 | , domTool.createElement('span', {'class': LINE_CONTENT_CLASS}, waterfallContent)
173 | ]);
174 | line.style.setProperty(FONT_SIZE_CUSTOM_PROPERTY, `${i}`);
175 | line.style.setProperty('font-variation-settings', fontVariationSettings);
176 |
177 | if(variationSettingsFlags.has('displayParameters')) {
178 | // enable parameters
179 | const parameters = domTool.createElement(
180 | 'div'
181 | , {'class': SHOW_PARAMETERS_CLASS}
182 | , _getParametersTextForFontSize(parametersText, i)
183 | );
184 | line.append(parameters);
185 | }
186 | lines.push(line);
187 | }
188 | contentElement.append(...lines);
189 | }
190 |
191 | function _handleBeforeInput(domTool, event) {
192 | handleEditableLine(domTool, event);
193 | domTool.dispatchEvent(event.target, 'edited', {bubbles: true});
194 | }
195 |
196 | function _handleClick(domTool, e) {
197 | if(!e.target.matches(`${LINE_SELECTOR} ${TOGGLE_EDIT_SELECTOR}`))
198 | return;
199 | e.preventDefault();
200 | // Turning contentEditable on only when required has the advantage
201 | // that text selection works better, it's otherwise limited within
202 | // each contenteditable element and one cannot select accross elements.
203 | const line = e.target.closest(LINE_SELECTOR)
204 | , lineContent = line.querySelector(LINE_CONTENT_SELECTOR)
205 | ;
206 | lineContent.contentEditable = true;
207 | lineContent.focus();
208 | }
209 |
210 | function _handleDblclick(domTool, e) {
211 | if(!e.target.matches(`${LINE_SELECTOR} *`))
212 | return;
213 | e.preventDefault();
214 | // Turning contentEditable on only when required has the advantage
215 | // that text selection works better, it's otherwise limited within
216 | // each contenteditable element and one cannot select accross elements.
217 | const line = e.target.closest(LINE_SELECTOR)
218 | , lineContent = line.querySelector(LINE_CONTENT_SELECTOR)
219 | ;
220 | lineContent.contentEditable = true;
221 | lineContent.focus();
222 | }
223 |
224 | function _handleFocusOut(domTool, e) {
225 | if(!e.target.matches(LINE_CONTENT_SELECTOR))
226 | return;
227 | const lineContent = e.target;
228 | lineContent.contentEditable = false;
229 | }
230 |
231 | function updateTextContent(container, newText) {
232 | // Update all lines
233 | const lineContents = container.querySelectorAll(LINE_CONTENT_SELECTOR);
234 | for(const elem of lineContents) {
235 | if(elem.textContent === newText) continue;
236 | elem.textContent = newText;
237 | }
238 | }
239 |
240 | function _handleEdited(updateTextHandler, event) {
241 | const newText = event.target.textContent;
242 | updateTextHandler(newText);
243 | }
244 |
245 | export function init(proofElement, domTool, updateTextHandler, fontSize, fontSizeTo
246 | , manualAxisLocations, alignment, variationSettingsFlags
247 | , userText, manualFontLeading) {
248 |
249 | console.log(`WATERFALL init!`, {
250 | fontSize, fontSizeTo, manualAxisLocations});
251 |
252 | const contentElement = domTool.createElement('div', {'class': WATERFALL_CONTAINER_CLASS});
253 | proofElement.append(contentElement);
254 |
255 |
256 | const handleDestroy = (/*event*/)=>{
257 | for(const eventListener of eventListeners)
258 | proofElement.removeEventListener(...eventListener);
259 | }
260 | , eventListeners = [
261 | ['beforeinput', _handleBeforeInput.bind(null, domTool), false]
262 | , ['focusout', _handleFocusOut.bind(null, domTool), false]
263 | , ['edited', _handleEdited.bind(null, updateTextHandler), false]
264 | //, ['focusin', _handleFocusIn.bind(null, domTool, setActiveTypoTarget), false]
265 | , ['click', _handleClick.bind(null, domTool), false]
266 | , ['dblclick', _handleDblclick.bind(null, domTool), false]
267 | , ['destroy', handleDestroy, false]
268 | ]
269 | ;
270 |
271 | for(const eventListener of eventListeners)
272 | proofElement.addEventListener(...eventListener);
273 |
274 |
275 | _setProperty(contentElement, 'alignment', alignment);
276 | contentElement.style.setProperty('line-height', `${manualFontLeading}`);
277 | const varProperties = applyPropertyChanges(contentElement, manualAxisLocations,
278 | variationSettingsFlags);
279 | makeLines(domTool, contentElement, userText, fontSize, fontSizeTo, varProperties, variationSettingsFlags);
280 |
281 | return {
282 | // Update will run if the proof is no re-initalized anyways.
283 | update: (changedDependencyNamesSet, fontSize, fontSizeTo
284 | , manualAxisLocations, alignment, variationSettingsFlags
285 | , userText, manualFontLeading)=>{
286 | _setProperty(contentElement, 'alignment', alignment);
287 | const varProperties = applyPropertyChanges(contentElement, manualAxisLocations,
288 | variationSettingsFlags);
289 | if(['fontSizeFrom', 'fontSizeTo', 'fontName', 'variationSettingsFlags'].some(k=>changedDependencyNamesSet.has(k))) {
290 | contentElement.style.setProperty('line-height', `${manualFontLeading}`);
291 | makeLines(domTool, contentElement, userText, fontSize, fontSizeTo, varProperties, variationSettingsFlags);
292 | return;
293 | }
294 | if(changedDependencyNamesSet.has('userText'))
295 | updateTextContent(contentElement, userText);
296 | // What follows is covered in makeLines as well
297 | if(!variationSettingsFlags.has('applyDefaultsExplicitly')) {
298 | // When the default are not explicitly applied, font-variation-settings
299 | // must be updated when the variation properties change, as
300 | // they may contain more or less axis than before..
301 | updateFontVariationsSettings(domTool, contentElement, varProperties);
302 | }
303 | if(changedDependencyNamesSet.has('manualAxisLocations') && variationSettingsFlags.has('displayParameters')){
304 | updateDisplayParameters(domTool, contentElement, varProperties);
305 | }
306 |
307 | if(changedDependencyNamesSet.has('manualFontLeading'))
308 | contentElement.style.setProperty('line-height', `${manualFontLeading}`);
309 |
310 | }
311 | };
312 | }
313 |
314 |
--------------------------------------------------------------------------------
/lib/js/layouts/videoproof-array.mjs:
--------------------------------------------------------------------------------
1 | /* jshint browser: true, esversion: 8, laxcomma: true, laxbreak: true */
2 |
3 | /**
4 | * TODO: the controller should take care of these events:
5 | * $(document).on('videoproof:fontLoaded.grid', populateGrid);
6 | * $('#select-glyphs').on('change.grid', populateGrid);
7 | * $('#show-extended-glyphs').on('change.grid', populateGrid);
8 | */
9 | export function init(proofElement, glyphset, fixLineBreaksFunc) {
10 | let cells = [];
11 | for(let char of glyphset) {
12 | var cell = proofElement.ownerDocument.createElement('span');
13 | cell.textContent = char;
14 | cells.push(cell);
15 | }
16 | proofElement.append(...cells);
17 | fixLineBreaksFunc(proofElement);
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/lib/js/layouts/videoproof-contextual.mjs:
--------------------------------------------------------------------------------
1 | /* jshint esversion: 11, browser: true, unused:true, undef:true, laxcomma: true, laxbreak: true */
2 |
3 | import DOMTool from '../domTool.mjs';
4 |
5 | function _testCharType(extendedCharGroups, c, re) {
6 | if (re.test(c))
7 | return true;
8 | //checks all items in extendedCharGroups
9 | for(let [k, extChars] of Object.entries(extendedCharGroups)) {
10 | if (!re.test(k))
11 | continue;
12 | if(extChars.indexOf(c) != -1)
13 | return true;
14 | }
15 | return false;
16 | }
17 |
18 | function _formatAuto(mode, autoFormatter, c) {
19 | for(let [test, format] of autoFormatter) {
20 | if(test(c))
21 | return format(c);
22 | }
23 | throw new Error(`Don't know how to format "${c}" in mode: ${mode}.`);
24 | }
25 |
26 | function _formatCustom(customPad, c) {
27 | return `${customPad}${c}${customPad}`;
28 | }
29 |
30 | function* _kernPaddingGen(outer, inner) {
31 | for(let o of outer)
32 | for(let i of inner)
33 | yield [o, i];
34 | }
35 |
36 | const _autoFormatters = {
37 | 'auto-short': [
38 | ['isNumeric', c=>`00${c}00`]
39 | , ['isLowercase', c=>`nn${c}nn`]
40 | // default, also isUppercase:
41 | , ['default', c=>`HH${c}HH`]
42 | ]
43 | , 'auto-long': [
44 | ['isNumeric', c=>`00${c}0101${c}11`]
45 | , ['isLowercase', c=>`nn${c}nono${c}oo`]
46 | // default, also isUppercase:
47 | , ['default', c=>`HH${c}HOHO${c}OO`]
48 | ]
49 | }
50 | , _kernFormatters = {
51 | 'kern-upper': ([o, i])=>`HO${o}${i}${o}OLA`
52 | , 'kern-mixed': ([o, i])=>`${o}${i}nnoy`
53 | , 'kern-lower': ([o, i])=>`no${o}${i}${o}ony`
54 | }
55 | , _kernModesCharsConfig = { // mode: [charsKey, outer]
56 | 'kern-upper': ['Latin.Uppercase', undefined]
57 | // CUSTOM! outer
58 | , 'kern-mixed': ['Latin.Lowercase', [..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"]]
59 | , 'kern-lower': ['Latin.Lowercase', undefined]
60 | }
61 | ;
62 |
63 | function _getKernChars(getCharsForKey, showExtended, mode) {
64 | if(!(mode in _kernModesCharsConfig))
65 | throw new Error(`Don't now how to get chars for mode: "${mode}".`);
66 | const [charsKey, customOuter] = _kernModesCharsConfig[mode]
67 | , [chars, extendedCharset] = getCharsForKey(charsKey)
68 | , outer = customOuter !== undefined ? customOuter : chars
69 | , inner = showExtended
70 | ? [...chars, ...extendedCharset]
71 | : chars
72 | ;
73 | return [outer, inner];
74 | }
75 |
76 | function _getWords(selectedChars, getCharsForKey, showExtended
77 | , extendedCharGroups, padMode, customPad) {
78 |
79 | // All only latin!
80 | let _formatterTests = {
81 | 'isNumeric': c=>_testCharType(extendedCharGroups, c, /[0-9]/)
82 | // , 'isUppercase': = c=>_testCharType(extendedCharGroups, c, /[A-Z]/)
83 | , 'isLowercase': c=>_testCharType(extendedCharGroups, c, /[a-z]/)
84 | , 'default': ()=>true
85 | }
86 | , _getAutoFormatter = padMode=>{
87 | let description = _autoFormatters[padMode]
88 | , result = []
89 | ;
90 | for(let [testName, format] of description) {
91 | result.push([_formatterTests[testName], format]);
92 | }
93 | return result;
94 | }
95 | , words = []
96 | ;
97 |
98 | let chars, formatter;
99 | if(padMode in _kernFormatters) {
100 | let [outerChars, innerChars] = _getKernChars(getCharsForKey, showExtended, padMode);
101 | chars = _kernPaddingGen(outerChars, innerChars);
102 | formatter = _kernFormatters[padMode];
103 | }
104 | else if(padMode === 'custom') {
105 | chars = selectedChars;
106 | formatter = c=>_formatCustom(customPad, c);
107 | }
108 | else if(padMode in _autoFormatters) {
109 | chars = selectedChars;
110 | formatter = c=>_formatAuto(padMode, _getAutoFormatter(padMode), c);
111 | }
112 | else
113 | throw new Error(`Don't know how to handle mode: "${padMode}".`);
114 |
115 | for(let c of chars)
116 | words.push(formatter(c));
117 | if(padMode in _kernFormatters)
118 | // Dunno why! Behavior form legacy code.
119 | words.push(0);
120 | return words;
121 | }
122 |
123 | export function init(proofElement, selectedChars, getCharsForKey
124 | , fixLineBreaks, showExtended, extendedCharGroups
125 | , padMode, customPad) {
126 | const domTool = new DOMTool(proofElement.ownerDocument)
127 | , words = _getWords(selectedChars, getCharsForKey, showExtended
128 | , extendedCharGroups, padMode, customPad)
129 | , cells = []
130 | ;
131 |
132 | domTool.clear(proofElement);
133 | for(let i=0,l=words.length;i
134 | 135 |