Press n or j to go to the next uncovered block, b, p or k for the previous block.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 | 1x 1x 1x 1x | import {withDefaults, say, poscmp, srcRangeIncludes, warn, createAnnouncement} from './utils'; import SHARED from './shared'; import {store} from './store'; import {performEdits, edit_insert, edit_delete, edit_replace, edit_overwrite} from './edits/performEdits'; // All editing actions are defined here. // // Many actions take a Target as an option. A Target may be constructed by any // of the methods on the `Targets` export below. // // Just about every action can take an optional `onSuccess` and `onError` // callback. If the action is successful, it will call `onSuccess(newAST)`. If // it fails, it will call `onError(theError)`. // // The implementation of actions is in the folder `src/edits/`. IT IS PRIVATE, // AND FOR THE MOST PART, NO FILE EXCEPT src/actions.js SHOULD NEED TO IMPORT IT. // (Exception: speculateChanges and commitChanges are sometimes imported.) // The implementation is complex, because while edits are best thought of as // operations on the AST, they must all be transformed into text edits, and the // only interface we have into the language's textual syntax are the `pretty` // and `parse` methods. // A _Target_ says where an action is directed. For example, a target may be a // node or a drop target. // // These are the kinds of targets: // - InsertTarget: insert at a location inside the AST. // - ReplaceNodeTarget: replace an ast node. // - OverwriteTarget: replace a range of text at the top level. // // These kinds of actions _have_ a target: // - Paste: the target says where to paste. // - Drag&Drop: the target says what's being dropped on. // - Insert/Edit: the target says where the text is being inserted/edited. // // Targets are defined at the bottom of this file. // Insert `text` at the given `target`. // See the comment at the top of the file for what kinds of `target` there are. export function insert(text, target, onSuccess, onError, annt) { checkTarget(target); const {ast} = store.getState(); const edits = [target.toEdit(text)]; performEdits('cmb:insert', ast, edits, onSuccess, onError, annt); } // Delete the given nodes. export function delete_(nodes, editWord) { // 'delete' is a reserved word if (nodes.length === 0) return; const {ast} = store.getState(); nodes.sort((a, b) => poscmp(b.from, a.from)); // To focus before first deletion const edits = nodes.map(node => edit_delete(node)); let annt = false; if (editWord) { annt = createAnnouncement(nodes, editWord); say(annt); } performEdits('cmb:delete-node', ast, edits, undefined, undefined, annt); store.dispatch({type: 'SET_SELECTIONS', selections: []}); } // Copy the given nodes onto the clipboard. export function copy(nodes, editWord) { if (nodes.length === 0) return; const {ast, focusId} = store.getState(); // Pretty-print each copied node. Join them with spaces, or newlines for // commented nodes (to prevent a comment from attaching itself to a // different node after pasting). nodes.sort((a, b) => poscmp(a.from, b.from)); let annt = false; if (editWord) { annt = createAnnouncement(nodes, editWord); say(annt); } let text = ""; let postfix = ""; for (let node of nodes) { let prefix = (node.options && node.options.comment) ? "\n" : postfix; text = text + prefix + node.toString(); postfix = (node.options && node.options.comment) ? "\n" : " "; } copyToClipboard(text); // Copy steals focus. Force it back to the node's DOM element // without announcing via activateByNid(). if (focusId) { ast.getNodeById(focusId).element.focus(); } } // Paste from the clipboard at the given `target`. // See the comment at the top of the file for what kinds of `target` there are. export function paste(target, onSuccess, onError) { checkTarget(target); pasteFromClipboard(text => { const {ast} = store.getState(); const edits = [target.toEdit(text)]; performEdits('cmb:paste', ast, edits, onSuccess, onError); store.dispatch({type: 'SET_SELECTIONS', selections: []}); }); } // Drag from `src` (which should be a d&d monitor thing) to `target`. // See the comment at the top of the file for what kinds of `target` there are. export function drop(src, target, onSuccess, onError) { checkTarget(target); const {id: srcId, content: srcContent} = src; let {ast, collapsedList} = store.getState(); // get the AST, and which nodes are collapsed const srcNode = srcId ? ast.getNodeById(srcId) : null; // null if dragged from toolbar const content = srcNode ? srcNode.toString() : srcContent; // If we dropped the node _inside_ where we dragged it from, do nothing. if (srcNode && srcRangeIncludes(srcNode.srcRange(), target.srcRange())) { return; } let edits = []; let droppedHash; // Assuming it did not come from the toolbar... // (1) Delete the text of the dragged node, (2) and save the id and hash if (srcNode !== null) { edits.push(edit_delete(srcNode)); droppedHash = ast.nodeIdMap.get(srcNode.id).hash; } // Insert or replace at the drop location, depending on what we dropped it on. edits.push(target.toEdit(content)); // Perform the edits. performEdits('cmb:drop-node', ast, edits, onSuccess, onError); // Assuming it did not come from the toolbar, and the srcNode was collapsed... // Find the matching node in the new tree and collapse it if((srcNode !== null) && collapsedList.find(id => id == srcNode.id)) { let {ast} = store.getState(); const newNode = [...ast.nodeIdMap.values()].find(n => n.hash == droppedHash); store.dispatch({type: 'COLLAPSE', id: newNode.id}); store.dispatch({type: 'UNCOLLAPSE', id: srcNode.id}); } } // Drag from `src` (which should be a d&d monitor thing) to the trash can, which // just deletes the block. export function dropOntoTrashCan(src) { const {ast} = store.getState(); const srcNode = src.id ? ast.getNodeById(src.id) : null; // null if dragged from toolbar if (!srcNode) return; // Someone dragged from the toolbar to the trash can. let edits = [edit_delete(srcNode)]; performEdits('cmb:trash-node', ast, edits); } // Set the cursor position. export function setCursor(cur) { return (dispatch, _getState) => { if (SHARED.cm && cur) { SHARED.cm.focus(); SHARED.search.setCursor(cur); SHARED.cm.setCursor(cur); } dispatch({type: 'SET_CURSOR', cur}); }; } // Activate the node with the given `nid`. export function activateByNid(nid, options) { //console.log('XXX actions:169 activateByNid called with', nid); return (dispatch, getState) => { options = withDefaults(options, {allowMove: true, record: true}); let {ast, focusId, collapsedList} = getState(); // If nid is null, try to get it from the focusId if (nid === null) { nid = ast?.getNodeById(focusId)?.nid; } // Get the currently-focused node *based strictly on focusId* // And the new node from the nid const currentNode = ast?.getNodeById(focusId); const newNode = ast?.getNodeByNId(nid); // If there is no valid node found in the AST, bail. // (This could also mean a node was selected in the toolbar! // It's ok to do nothing: screenreaders will still announce it - // we just don't want to activate them.) if (!newNode) { return; } // If there's a previously-focused node, see if the ids match // If so, we need to manually initiate a new focus event if (newNode.nid === currentNode?.nid) { //console.log('XXX actions.js:191, they are eq'); setTimeout(() => { if(newNode.element) newNode.element.focus(); }, 10); } clearTimeout(store.queuedAnnouncement); // clear any overrideable announcements // FIXME(Oak): if possible, let's not hard code like this if (['blank', 'literal'].includes(newNode.type) && !collapsedList.includes(newNode.id)) { say('Use enter to edit', 1250, true); // wait 1.25s, and allow to be overridden } // FIXME(Oak): here's a problem. When we double click, the click event will // be fired as well. That is, it tries to activate a node and then edit // this is bad because both `say(...)` and `.focus()` will be unnecessarily // invoked. // The proper way to fix this is to do some kind of debouncing to avoid // calling `activate` in the first place // but we will use a hacky one for now: we will let `activate` // happens, but we will detect that node.element is absent, so we won't do // anything // Note, however, that it is also a good thing that `activate` is invoked // when double click because we can set focusId on the to-be-focused node setTimeout(() => { //console.log('XXX actions:213 trying SET_FOCUS with fid=', focusId, 'fNId=', focusNId, 'xid=', node.id, 'xnid=', node.nid); dispatch({type: 'SET_FOCUS', focusId: newNode.id}); //console.log('XXX actions:216'); if (options.record) { SHARED.search.setCursor(newNode.from); } //console.log('XXX actions:220'); if (newNode.element) { const scroller = SHARED.cm.getScrollerElement(); const wrapper = SHARED.cm.getWrapperElement(); if (options.allowMove) { SHARED.cm.scrollIntoView(newNode.from); // get the *actual* bounding rect let {top, bottom, left, right} = newNode.element.getBoundingClientRect(); let offset = wrapper.getBoundingClientRect(); let scroll = SHARED.cm.getScrollInfo(); top = top + scroll.top - offset.top; bottom = bottom + scroll.top - offset.top; left = left + scroll.left - offset.left; right = right + scroll.left - offset.left; SHARED.cm.scrollIntoView({top, bottom, left, right}); } scroller.setAttribute('aria-activedescendent', newNode.element.id); newNode.element.focus(); } //console.log('XXX actions:240'); }, 25); }; } function checkTarget(target) { if (!(target instanceof Target)) { warn('actions', `Expected target ${target} to be an instance of the Target class.`); } } function copyToClipboard(text) { SHARED.buffer.value = text; SHARED.buffer.select(); document.execCommand('copy'); } function pasteFromClipboard(done) { SHARED.buffer.value = ''; SHARED.buffer.focus(); setTimeout(() => { done(SHARED.buffer.value); }, 50); } // The class of all targets. export class Target { constructor(from, to) { this.from = from; this.to = to; } srcRange() { return {from: this.from, to: this.to}; } } // Insert at a location inside the AST. export class InsertTarget extends Target { constructor(parentNode, fieldName, pos) { super(pos, pos); this.parent = parentNode; this.field = fieldName; this.pos = pos; } getText(_ast) { return ""; } toEdit(text) { return edit_insert(text, this.parent, this.field, this.pos); } } // Target an ASTNode. This will replace the node. export class ReplaceNodeTarget extends Target { constructor(node) { const range = node.srcRange(); super(range.from, range.to); this.node = node; } getText(ast) { const {from, to} = ast.getNodeById(this.node.id); return SHARED.cm.getRange(from, to); } toEdit(text) { return edit_replace(text, this.node); } } // Target a source range at the top level. This really has to be at the top // level: neither `from` nor `to` can be inside any root node. export class OverwriteTarget extends Target { constructor(from, to) { super(from, to); } getText(_ast) { return SHARED.cm.getRange(this.from, this.to); } toEdit(text) { return edit_overwrite(text, this.from, this.to); } } |