Skip to content

Commit

Permalink
Use requestAnimationFrame to fire each queue item.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmacarthur committed Jun 13, 2022
1 parent 72d834b commit b2c6e74
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 23 deletions.
89 changes: 89 additions & 0 deletions packages/typeit/__tests__/helpers/fireItem.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as beforePaint from "../../src/helpers/beforePaint";
import fireItem from "../../src/helpers/fireItem";

describe("all items have delays", () => {
it("does not group any items for execution.", async () => {
const beforePaintSpy = jest
.spyOn(beforePaint, "default")
.mockImplementation((cb) => cb());
const wait = jest.fn((cb) => cb());
const [mock1, mock2] = makeMocks();
const queueItems = [
[
Symbol(),
{
func: mock1,
delay: 1,
},
],
[
Symbol(),
{
func: mock2,
delay: 1,
},
],
];

const index = 0;
const resultIndex = await fireItem(index, queueItems, wait);

expect(beforePaintSpy).toHaveBeenCalledTimes(1);
expect(mock1).toHaveBeenCalledTimes(1);
expect(mock2).not.toHaveBeenCalled();

// Index was not modified.
expect(resultIndex).toBe(index);
expect(wait).toHaveBeenCalledTimes(1);
});
});

describe("some items have no delay", () => {
it("groups items for execution.", async () => {
const [mock1, mock2, mock3, mock4] = makeMocks();
const wait = jest.fn();
const queueItems = [
[
Symbol(),
{
func: mock1,
delay: 0,
},
],
[
Symbol(),
{
func: mock2,
delay: 0,
},
],
[
Symbol(),
{
func: mock3,
delay: 0,
},
],
[
Symbol(),
{
func: mock4,
delay: 1,
},
],
];

const index = 0;
const resultIndex = await fireItem(index, queueItems, wait);

[mock1, mock2, mock3].forEach((m) => {
expect(m).toHaveBeenCalledTimes(1);
});

expect(mock4).not.toHaveBeenCalled();

// Index was advanced.
expect(resultIndex).toBe(2);
expect(wait).not.toHaveBeenCalled();
});
});
17 changes: 17 additions & 0 deletions packages/typeit/__tests__/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,20 @@ global.verifyQueue = ({ queue, totalItems, totalTypeableItems }) => {
jest.fn().constructor.prototype.times = function () {
return this.mock.calls.length;
};

global.makeMocks = () => {
const iterator = {
next() {
return {
done: false,
value: jest.fn(),
};
},

[Symbol.iterator]() {
return iterator;
},
};

return iterator;
};
10 changes: 10 additions & 0 deletions packages/typeit/src/helpers/beforePaint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
let beforePaint = (cb): Promise<any> => {
return new Promise((resolve) => {
requestAnimationFrame(async () => {
resolve(await cb());
})
});
}

export default beforePaint;

54 changes: 41 additions & 13 deletions packages/typeit/src/helpers/fireItem.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,44 @@
import { QueueItem } from "../types";

let fireItem = async (queueItem: QueueItem, wait) => {
// Only break up the event loop if needed.
let execute = async () => queueItem.func?.call(this);

if (queueItem.delay) {
await wait(async () => {
await execute();
}, queueItem.delay);
} else {
await execute();
import { QueueItem, QueueMapPair } from "../types";
import beforePaint from "./beforePaint";

let execute = (queueItem: QueueItem) => queueItem.func?.call(this);

let fireItem = async (
index: number,
queueItems: QueueMapPair[],
wait
): Promise<number> => {
let queueItem = queueItems[index][1];
let instantQueue = [];
let tempIndex = index;
let futureItem = queueItem;
let shouldBeGrouped = () => futureItem && !futureItem.delay;

// Crawl through the queue and group together all items that
// do not have have a delay and can be executed instantly.
while (shouldBeGrouped()) {
instantQueue.push(futureItem);

shouldBeGrouped() && tempIndex++;
futureItem = queueItems[tempIndex] ? queueItems[tempIndex][1] : null;
}
}

if (instantQueue.length) {
// All are executed together before the browser has a chance to repaint.
await beforePaint(async () => {
for (let q of instantQueue) {
await execute(q);
}
});

// Important! Because we moved into the future, the index
// needs to be modified and returned for accurate remaining execution.
return tempIndex - 1;
}

await wait(() => beforePaint(() => execute(queueItem)), queueItem.delay);

return index;
};

export default fireItem;
44 changes: 34 additions & 10 deletions packages/typeit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
QueueItem,
ActionOpts,
TypeItInstance,
QueueMapPair,
} from "./types";
import {
CURSOR_CLASS,
Expand Down Expand Up @@ -184,7 +185,7 @@ const TypeIt: TypeItInstance = function (element, options = {}) {
let derivedCursorPosition = _getDerivedCursorPosition();
derivedCursorPosition && (await _move({ value: derivedCursorPosition }));

// Grab all characters currently mounted to the DOM,
// Grab all characters currently mounted to the DOM,
// in order to wipe the slate clean before restarting.
for (let _i of _getAllChars()) {
await _wait(_delete, _getPace(1));
Expand Down Expand Up @@ -251,24 +252,47 @@ const TypeIt: TypeItInstance = function (element, options = {}) {
// }, 0)
// );

let cleanUp = (qKey) => {
_disableCursorBlink(false);
_queue.done(qKey, !remember);
};

try {
for (let [queueKey, queueItem] of _queue.getQueue()) {
let queueItems = [..._queue.getQueue()] as QueueMapPair[];

for (let index = 0; index < queueItems.length; index++) {
let [queueKey, queueItem] = queueItems[index];

// Only execute items that aren't done yet.
if (queueItem.done) continue;

if (queueItem.typeable && !_statuses.frozen) _disableCursorBlink(true);

// Because calling .delete() with no parameters will attempt to
// Because calling .delete() with no parameters will attempt to
// delete all "typeable" characters, we may overfetch, since some characters
// in the queue may already be deleted. This ensures that we do not attempt to
// in the queue may already be deleted. This ensures that we do not attempt to
// delete a character that isn't actually mounted to the DOM.
if (!queueItem.deletable || (queueItem.deletable && _getAllChars().length)) {
await fireItem(queueItem, _wait);
if (
!queueItem.deletable ||
(queueItem.deletable && _getAllChars().length)
) {
let newIndex = await fireItem(index, queueItems, _wait);

// Ensure each skipped item goes through the cleanup process,
// so that methods like .flush() don't get messed up.
Array(newIndex - index)
.fill(index + 1)
.map((x, y) => x + y)
.forEach((i) => {
let [key] = queueItems[i];

cleanUp(key);
});

index = newIndex;
}

_disableCursorBlink(false);

_queue.done(queueKey, !remember);
cleanUp(queueKey);
}

if (!remember) {
Expand Down Expand Up @@ -381,7 +405,7 @@ const TypeIt: TypeItInstance = function (element, options = {}) {
{
func: _delete,
delay: instant ? 0 : _getPace(1),
deletable: true
deletable: true,
},
rounds
),
Expand Down
2 changes: 2 additions & 0 deletions packages/typeit/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export type QueueItem = {
deletable?: boolean;
};

export type QueueMapPair = [Symbol, QueueItem];

export type Element = HTMLElement &
CharacterData &
Node &
Expand Down

0 comments on commit b2c6e74

Please sign in to comment.